@uppy/companion: fix (breaking) todo comments (#5802)

This commit is contained in:
Mikael Finstad 2025-07-14 12:20:08 +02:00 committed by Murderlon
parent 0294539af6
commit 06ddd99cd1
No known key found for this signature in database
GPG key ID: 1FF861FF1DDBB953
27 changed files with 112 additions and 162 deletions

View file

@ -1,4 +1,3 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import deepFreeze from 'deep-freeze'
/* eslint-disable no-underscore-dangle */

View file

@ -38,7 +38,6 @@ class MyCustomProvider {
return 'myunsplash'
}
// eslint-disable-next-line class-methods-use-this
async list({ token, directory }) {
const path = directory ? `/${directory}/photos` : ''
@ -55,7 +54,6 @@ class MyCustomProvider {
return adaptData(await resp.json())
}
// eslint-disable-next-line class-methods-use-this
async download({ id, token }) {
const resp = await fetch(`${BASE_URL}/photos/${id}`, {
headers: {

View file

@ -9,7 +9,6 @@ const upload = multer({
function uploadRoute(req, res) {
res.json({
files: req.files.map((file) => {
// eslint-disable-next-line no-param-reassign
delete file.buffer
return file
}),

View file

@ -36,13 +36,14 @@ describe('isOriginAllowed', () => {
expect(isOriginAllowed('a', [/^.+$/])).toBeTruthy()
expect(isOriginAllowed('a', ['^.+$'])).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['www\\.transloadit\\.com']),
isOriginAllowed('www.transloadit.com', ['^www\\.transloadit\\.com$']),
).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['transloadit\\.com']),
isOriginAllowed('www.transloadit.com', ['^transloadit\\.com$']),
).toBeFalsy()
expect(isOriginAllowed('match', ['fail', 'match'])).toBeTruthy()
// todo maybe next major:
// expect(isOriginAllowed('www.transloadit.com', ['\\.transloadit\\.com$'])).toBeTruthy()
expect(
isOriginAllowed('www.transloadit.com', ['\\.transloadit\\.com$']),
).toBeTruthy()
})
})

View file

@ -5,8 +5,7 @@ function escapeRegex(string: string) {
function wrapInRegex(value?: string | RegExp): RegExp | undefined {
if (typeof value === 'string') {
// TODO in the next major we should change this to `new RegExp(value)` so that the user can control start/end characters
return new RegExp(`^${value}$`) // throws if invalid regex
return new RegExp(value) // throws if invalid regex
}
if (value instanceof RegExp) {
return value

View file

@ -82,7 +82,6 @@ function validateOptions(options) {
}
// validate protocol
// @todo this validation should not be conditional once the protocol field is mandatory
if (
options.protocol &&
!Object.keys(PROTOCOLS).some((key) => PROTOCOLS[key] === options.protocol)
@ -227,8 +226,6 @@ class Uploader {
}
_getUploadProtocol() {
// todo a default protocol should not be set. We should ensure that the user specifies their protocol.
// after we drop old versions of uppy client we can remove this
return this.options.protocol || PROTOCOLS.multipart
}

View file

@ -5,25 +5,14 @@ const { respondWithError } = require('../provider/error')
async function get(req, res) {
const { id } = req.params
const { providerUserSession } = req.companion
const { accessToken } = providerUserSession
const { provider } = req.companion
async function getSize() {
return provider.size({
id,
token: accessToken,
providerUserSession,
query: req.query,
})
return provider.size({ id, providerUserSession, query: req.query })
}
const download = () =>
provider.download({
id,
token: accessToken,
providerUserSession,
query: req.query,
})
provider.download({ id, providerUserSession, query: req.query })
try {
await startDownUpload({ req, res, getSize, download })

View file

@ -2,13 +2,10 @@ const { respondWithError } = require('../provider/error')
async function list({ query, params, companion }, res, next) {
const { providerUserSession } = companion
const { accessToken } = providerUserSession
try {
// todo remove backward compat `token` param from all provider methods (because it can be found in providerUserSession)
const data = await companion.provider.list({
companion,
token: accessToken,
providerUserSession,
directory: params.id,
query,

View file

@ -23,9 +23,7 @@ async function logout(req, res, next) {
}
try {
const { accessToken } = providerUserSession
const data = await companion.provider.logout({
token: accessToken,
providerUserSession,
companion,
})

View file

@ -8,12 +8,10 @@ const { respondWithError } = require('../provider/error')
async function thumbnail(req, res, next) {
const { id } = req.params
const { provider, providerUserSession } = req.companion
const { accessToken } = providerUserSession
try {
const { stream, contentType } = await provider.thumbnail({
id,
token: accessToken,
providerUserSession,
})
if (contentType != null) res.set('Content-Type', contentType)

View file

@ -181,8 +181,6 @@ exports.getURLMeta = async (
}
if (urlMeta.statusCode >= 300) {
// @todo possibly set a status code in the error object to get a more helpful
// hint at what the cause of error is.
throw new Error(`URL server responded with status: ${urlMeta.statusCode}`)
}

View file

@ -1,5 +1,4 @@
const crypto = require('node:crypto')
const logger = require('../logger.js')
const authTagLength = 16
const nonceLength = 16
@ -120,43 +119,6 @@ module.exports.encrypt = (input, secret) => {
return `${nonce.toString('hex')}${encrypted.toString('base64url')}`
}
// todo backwards compat for old tokens - remove in the future
function compatDecrypt(encrypted, secret) {
// Need at least 32 chars for the iv
if (encrypted.length < 32) {
throw new Error(
'Invalid encrypted value. Maybe it was generated with an old Companion version?',
)
}
// NOTE: The first 32 characters are the iv, in hex format. The rest is the encrypted string, in base64 format.
const iv = Buffer.from(encrypted.slice(0, 32), 'hex')
const encryptionWithoutIv = encrypted.slice(32)
let decipher
try {
const secretHashed = crypto.createHash('sha256')
secretHashed.update(secret)
decipher = crypto.createDecipheriv('aes256', secretHashed.digest(), iv)
} catch (err) {
if (err.code === 'ERR_CRYPTO_INVALID_IV') {
throw new Error('Invalid initialization vector')
} else {
throw err
}
}
const urlDecode = (encoded) =>
encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/~/g, '=')
let decrypted = decipher.update(
urlDecode(encryptionWithoutIv),
'base64',
'utf8',
)
decrypted += decipher.final('utf8')
return decrypted
}
/**
* Decrypt an iv-prefixed or string with AES256. The iv should be in the first 32 hex characters.
*
@ -165,47 +127,41 @@ function compatDecrypt(encrypted, secret) {
* @returns {string} Decrypted value.
*/
module.exports.decrypt = (encrypted, secret) => {
try {
const nonceHexLength = nonceLength * 2 // because hex encoding uses 2 bytes per byte
const nonceHexLength = nonceLength * 2 // because hex encoding uses 2 bytes per byte
// NOTE: The first 32 characters are the nonce, in hex format.
const nonce = Buffer.from(encrypted.slice(0, nonceHexLength), 'hex')
// The rest is the encrypted string, in base64url format.
const encryptionWithoutNonce = Buffer.from(
encrypted.slice(nonceHexLength),
'base64url',
// NOTE: The first 32 characters are the nonce, in hex format.
const nonce = Buffer.from(encrypted.slice(0, nonceHexLength), 'hex')
// The rest is the encrypted string, in base64url format.
const encryptionWithoutNonce = Buffer.from(
encrypted.slice(nonceHexLength),
'base64url',
)
// The last 16 bytes of the rest is the authentication tag
const authTag = encryptionWithoutNonce.subarray(-authTagLength)
// and the rest (from beginning) is the encrypted data
const encryptionWithoutNonceAndTag = encryptionWithoutNonce.subarray(
0,
-authTagLength,
)
if (nonce.length < nonceLength) {
throw new Error(
'Invalid encrypted value. Maybe it was generated with an old Companion version?',
)
// The last 16 bytes of the rest is the authentication tag
const authTag = encryptionWithoutNonce.subarray(-authTagLength)
// and the rest (from beginning) is the encrypted data
const encryptionWithoutNonceAndTag = encryptionWithoutNonce.subarray(
0,
-authTagLength,
)
if (nonce.length < nonceLength) {
throw new Error(
'Invalid encrypted value. Maybe it was generated with an old Companion version?',
)
}
const { key, iv } = createSecrets(secret, nonce)
const decipher = crypto.createDecipheriv('aes-256-ccm', key, iv, {
authTagLength,
})
decipher.setAuthTag(authTag)
const decrypted = Buffer.concat([
decipher.update(encryptionWithoutNonceAndTag),
decipher.final(),
])
return decrypted.toString('utf8')
} catch (err) {
// todo backwards compat for old tokens - remove in the future
logger.info('Failed to decrypt - trying old encryption format instead', err)
return compatDecrypt(encrypted, secret)
}
const { key, iv } = createSecrets(secret, nonce)
const decipher = crypto.createDecipheriv('aes-256-ccm', key, iv, {
authTagLength,
})
decipher.setAuthTag(authTag)
const decrypted = Buffer.concat([
decipher.update(encryptionWithoutNonceAndTag),
decipher.final(),
])
return decrypted.toString('utf8')
}
module.exports.defaultGetKey = ({ filename }) => {

View file

@ -74,7 +74,6 @@ class Provider {
* @returns {Promise}
*/
async deauthorizationCallback(options) {
// @todo consider doing something like throw new NotImplementedError() instead
throw new Error('method not implemented')
}

View file

@ -56,10 +56,15 @@ class Box extends Provider {
* @param {object} options
* @param {string} options.directory
* @param {any} options.query
* @param {string} options.token
* @param {{ accessToken: string }} options.providerUserSession
* @param {unknown} options.companion
*/
async list({ directory, token, query, companion }) {
async list({
directory,
providerUserSession: { accessToken: token },
query,
companion,
}) {
return this.#withErrorHandling('provider.box.list.error', async () => {
const [userInfo, files] = await Promise.all([
getUserInfo({ token }),
@ -70,7 +75,7 @@ class Box extends Provider {
})
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.download.error', async () => {
const stream = (await getClient({ token })).stream.get(
`files/${id}/content`,
@ -82,7 +87,7 @@ class Box extends Provider {
})
}
async thumbnail({ id, token }) {
async thumbnail({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.thumbnail.error', async () => {
const extension = 'jpg' // you can set this to png to more easily reproduce http 202 retry-after
@ -111,7 +116,7 @@ class Box extends Provider {
})
}
async size({ id, token }) {
async size({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.size.error', async () => {
const { size } = await (await getClient({ token }))
.get(`files/${id}`, { responseType: 'json' })
@ -120,7 +125,7 @@ class Box extends Provider {
})
}
logout({ companion, token }) {
logout({ companion, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.logout.error', async () => {
const { key, secret } = companion.options.providerOptions.box
await (await getClient({ token })).post('oauth2/revoke', {

View file

@ -187,8 +187,6 @@ exports.getCredentialsOverrideMiddleware = (providers, companionOptions) => {
next()
} catch (keyErr) {
// TODO we should return an html page here that can communicate the error
// back to the Uppy client, just like /send-token does
res.send(`
<!DOCTYPE html>
<html>

View file

@ -123,7 +123,7 @@ class DropBox extends Provider {
async list(options) {
return this.#withErrorHandling('provider.dropbox.list.error', async () => {
const { client, userInfo } = await getClient({
token: options.token,
token: options.providerUserSession.accessToken,
namespaced: true,
})
@ -133,7 +133,7 @@ class DropBox extends Provider {
})
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.dropbox.download.error',
async () => {
@ -155,7 +155,7 @@ class DropBox extends Provider {
)
}
async thumbnail({ id, token }) {
async thumbnail({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.dropbox.thumbnail.error',
async () => {
@ -180,7 +180,7 @@ class DropBox extends Provider {
)
}
async size({ id, token }) {
async size({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.dropbox.size.error', async () => {
const { size } = await (
await getClient({ token, namespaced: true })
@ -194,7 +194,7 @@ class DropBox extends Provider {
})
}
async logout({ token }) {
async logout({ providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.dropbox.logout.error',
async () => {

View file

@ -70,7 +70,11 @@ class Facebook extends Provider {
return 'facebook'
}
async list({ directory, token, query = { cursor: null } }) {
async list({
directory,
providerUserSession: { accessToken: token },
query = { cursor: null },
}) {
return this.#withErrorHandling('provider.facebook.list.error', async () => {
const qs = { fields: 'name,cover_photo,created_time,type' }
@ -100,7 +104,7 @@ class Facebook extends Provider {
})
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.facebook.download.error',
async () => {
@ -121,7 +125,7 @@ class Facebook extends Provider {
throw new Error('call to thumbnail is not implemented')
}
async logout({ token }) {
async logout({ providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.facebook.logout.error',
async () => {

View file

@ -120,7 +120,9 @@ class Drive extends Provider {
async () => {
const directory = options.directory || 'root'
const query = options.query || {}
const { token } = options
const {
providerUserSession: { accessToken: token },
} = options
const isRoot = directory === 'root'
const isVirtualSharedDirRoot = directory === VIRTUAL_SHARED_DIR
@ -204,7 +206,7 @@ class Drive extends Provider {
)
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
if (mockAccessTokenExpiredError != null) {
logger.warn(`Access token: ${token}`)

View file

@ -36,7 +36,7 @@ async function refreshToken({
)
}
async function logout({ token }) {
async function logout({ providerUserSession: { accessToken: token } }) {
return withGoogleErrorHandling(
'google',
'provider.google.logout.error',

View file

@ -40,7 +40,11 @@ class Instagram extends Provider {
return 'instagram'
}
async list({ directory, token, query = { cursor: null } }) {
async list({
directory,
providerUserSession: { accessToken: token },
query = { cursor: null },
}) {
return this.#withErrorHandling(
'provider.instagram.list.error',
async () => {
@ -69,7 +73,7 @@ class Instagram extends Provider {
)
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.instagram.download.error',
async () => {

View file

@ -37,9 +37,13 @@ class OneDrive extends Provider {
* @param {object} options
* @param {string} options.directory
* @param {any} options.query
* @param {string} options.token
* @param {{ accessToken: string }} options.providerUserSession
*/
async list({ directory, query, token }) {
async list({
directory,
query,
providerUserSession: { accessToken: token },
}) {
return this.#withErrorHandling('provider.onedrive.list.error', async () => {
const path = directory ? `items/${directory}` : 'root'
// https://learn.microsoft.com/en-us/graph/query-parameters?tabs=http#top-parameter
@ -66,7 +70,7 @@ class OneDrive extends Provider {
})
}
async download({ id, token, query }) {
async download({ id, providerUserSession: { accessToken: token }, query }) {
return this.#withErrorHandling(
'provider.onedrive.download.error',
async () => {
@ -89,7 +93,7 @@ class OneDrive extends Provider {
throw new Error('call to thumbnail is not implemented')
}
async size({ id, query, token }) {
async size({ id, query, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.onedrive.size.error', async () => {
const { size } = await (await getClient({ token }))
.get(`${getRootPath(query)}/items/${id}`, { responseType: 'json' })

View file

@ -23,7 +23,10 @@ const getPhotoMeta = async (client, id) =>
* Adapter for API https://api.unsplash.com
*/
class Unsplash extends Provider {
async list({ token, query = { cursor: null, q: null } }) {
async list({
providerUserSession: { accessToken: token },
query = { cursor: null, q: null },
}) {
if (typeof query.q !== 'string') {
throw new ProviderApiError('Search query missing', 400)
}
@ -39,7 +42,7 @@ class Unsplash extends Provider {
})
}
async download({ id, token }) {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling(
'provider.unsplash.download.error',
async () => {

View file

@ -69,7 +69,6 @@ class WebdavProvider extends Provider {
if (['ECONNREFUSED', 'ENOTFOUND'].includes(err.code)) {
throw new ProviderUserError({ message: 'Cannot connect to server' })
}
// todo report back to the user what actually went wrong
throw err
}
}
@ -168,7 +167,7 @@ class WebdavProvider extends Provider {
let err2 = err
if (err.status === 401) err2 = new ProviderAuthError()
if (err.response) {
err2 = new ProviderApiError('WebDAV API error', err.status) // todo improve (read err?.response?.body readable stream and parse response)
err2 = new ProviderApiError('WebDAV API error', err.status)
}
logger.error(err2, tag)
throw err2

View file

@ -45,7 +45,9 @@ class Zoom extends Provider {
async list(options) {
return this.#withErrorHandling('provider.zoom.list.error', async () => {
const { token } = options
const {
providerUserSession: { accessToken: token },
} = options
const query = options.query || {}
const meetingId = options.directory || ''
const requestedYear = query.year ? parseInt(query.year, 10) : null
@ -141,7 +143,11 @@ class Zoom extends Provider {
})
}
async download({ id: meetingId, token, query }) {
async download({
id: meetingId,
providerUserSession: { accessToken: token },
query,
}) {
return this.#withErrorHandling('provider.zoom.download.error', async () => {
// meeting id + file id required
// cc files don't have an ID or size
@ -167,7 +173,11 @@ class Zoom extends Provider {
})
}
async size({ id: meetingId, token, query }) {
async size({
id: meetingId,
providerUserSession: { accessToken: token },
query,
}) {
return this.#withErrorHandling('provider.zoom.size.error', async () => {
const client = await getClient({ token })
const { recordingStart, recordingId: fileId } = query
@ -183,7 +193,7 @@ class Zoom extends Provider {
})
}
async logout({ companion, token }) {
async logout({ companion, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.zoom.logout.error', async () => {
const { key, secret } = await companion.getProviderCredentials()

View file

@ -13,16 +13,13 @@ describe('handle deauthorization callback', () => {
nock('https://api.zoom.us').post('/oauth/data/compliance').reply(200)
test('providers without support for callback endpoint', () => {
return (
request(authServer)
.post('/dropbox/deauthorization/callback')
.set('Content-Type', 'application/json')
.send({
foo: 'bar',
})
// @todo consider receiving 501 instead
.expect(500)
)
return request(authServer)
.post('/dropbox/deauthorization/callback')
.set('Content-Type', 'application/json')
.send({
foo: 'bar',
})
.expect(500)
})
test('validate that request credentials match', () => {

View file

@ -117,14 +117,11 @@ class CameraScreen extends Component<CameraScreenProps> {
/>
</div>
) : (
// eslint-disable-next-line jsx-a11y/media-has-caption
<video
/* eslint-disable-next-line no-return-assign */
ref={(videoElement) => (this.videoElement = videoElement!)}
className={`uppy-Webcam-video ${
mirror ? 'uppy-Webcam-video--mirrored' : ''
}`}
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...videoProps}
/>
)}

View file

@ -47,7 +47,6 @@ const paths = Object.fromEntries(
const require = createRequire(packageRoot)
for (const pkg of uppyDeps) {
const nickname = pkg.slice('@uppy/'.length)
// eslint-disable-next-line import/no-dynamic-require
const pkgJson = require(`../${nickname}/package.json`)
if (pkgJson.main) {
yield [
@ -89,7 +88,7 @@ for await (const dirent of dir) {
if (!dirent.isDirectory()) {
const { name } = dirent
const ext = extname(name)
if (ext !== '.js' && ext !== '.jsx') continue // eslint-disable-line no-continue
if (ext !== '.js' && ext !== '.jsx') continue
const filePath =
basename(dirent.path) === name
? dirent.path // Some versions of Node.js give the full path as dirent.path.
@ -100,14 +99,14 @@ for await (const dirent of dir) {
.replace(
// The following regex aims to capture all imports and reexports of local .js(x) files to replace it to .ts(x)
// It's far from perfect and will have false positives and false negatives.
/((?:^|\n)(?:import(?:\s+\w+\s+from)?|export\s*\*\s*from|(?:import|export)\s*(?:\{[^}]*\}|\*\s*as\s+\w+\s)\s*from)\s*["']\.\.?\/[^'"]+\.)js(x?["'])/g, // eslint-disable-line max-len
/((?:^|\n)(?:import(?:\s+\w+\s+from)?|export\s*\*\s*from|(?:import|export)\s*(?:\{[^}]*\}|\*\s*as\s+\w+\s)\s*from)\s*["']\.\.?\/[^'"]+\.)js(x?["'])/g,
'$1ts$2',
)
.replace(
// The following regex aims to capture all local package.json imports.
/\nimport \w+ from ['"]..\/([^'"]+\/)*package.json['"]\n/g,
(originalImport) =>
`\n// eslint-disable-next-line @typescript-eslint/ban-ts-comment\n` +
`\n` +
`// @ts-ignore We don't want TS to generate types for the package.json${originalImport}`,
),
)