Refactor Companion to ESM (#5803)

- convert cjs to esm
- refactor from jest to vitest

closes #3979

---------

Co-authored-by: Merlijn Vos <merlijn@soverin.net>
This commit is contained in:
Mikael Finstad 2025-07-29 19:07:48 +02:00 committed by GitHub
parent 017e8ae608
commit acdc683d47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
105 changed files with 1242 additions and 2144 deletions

View file

@ -0,0 +1,5 @@
---
"@uppy/companion": major
---
Make Companion ESM-only. As of Node.js 20.19.0, you can require(esm) if you haven't transitioned yet.

View file

@ -31,7 +31,7 @@ function adaptData(res) {
/**
* an example of a custom provider module. It implements @uppy/companion's Provider interface
*/
class MyCustomProvider {
export default class MyCustomProvider {
static version = 2
static get oauthProvider() {
@ -76,5 +76,3 @@ class MyCustomProvider {
return { stream: Readable.fromWeb(resp.body), size }
}
}
module.exports = MyCustomProvider

View file

@ -0,0 +1,22 @@
class Gauge {
set = () => {}
}
export default function () {
const middleware = (req, res, next) => {
// simulate prometheus metrics endpoint:
if (req.url === '/metrics') {
res.setHeader('Content-Type', 'text/plain')
res.end('# Dummy metrics\n')
return
}
next()
}
middleware.promClient = {
collectDefaultMetrics: () => {},
Gauge,
}
return middleware
}

View file

@ -1,4 +1,4 @@
class Upload {
export class Upload {
constructor(file, options) {
this.url = 'https://tus.endpoint/files/foo-bar'
this.options = options
@ -18,4 +18,4 @@ class Upload {
}
}
module.exports = { Upload }
export default { Upload }

View file

@ -1,3 +1,3 @@
#!/usr/bin/env node
require('../lib/standalone/start-server')
import '../lib/standalone/start-server.js'

View file

@ -7,6 +7,7 @@
"author": "Transloadit.com",
"license": "MIT",
"homepage": "https://github.com/transloadit/uppy#readme",
"type": "module",
"repository": {
"type": "git",
"url": "git+https://github.com/transloadit/uppy.git"
@ -43,12 +44,12 @@
"escape-string-regexp": "4.0.0",
"express": "4.21.2",
"express-interceptor": "1.2.0",
"express-prom-bundle": "7.0.0",
"express-prom-bundle": "7",
"express-session": "1.18.1",
"fast-safe-stringify": "^2.1.1",
"formdata-node": "^6.0.3",
"got": "^13.0.0",
"grant": "5.4.22",
"grant": "^5.4.24",
"helmet": "^7.1.0",
"ioredis": "^5.3.2",
"ipaddr.js": "^2.0.1",
@ -67,7 +68,7 @@
"supports-color": "8.x",
"tus-js-client": "^4.1.0",
"validator": "^13.0.0",
"webdav": "5.7.1",
"webdav": "^5.8.0",
"ws": "8.17.1"
},
"devDependencies": {
@ -88,7 +89,6 @@
"@types/ws": "8.5.3",
"execa": "^9.6.0",
"http-proxy": "^1.18.1",
"jest": "^29.0.0",
"nock": "^13.1.3",
"supertest": "6.2.4",
"typescript": "^5.8.3",
@ -98,21 +98,16 @@
"bin/",
"lib/"
],
"jest": {
"testEnvironment": "node",
"testTimeout": 10000,
"automock": false
},
"scripts": {
"build": "tsc -p .",
"build": "tsc --build tsconfig.build.json",
"deploy": "kubectl apply -f infra/kube/companion-kube.yml",
"start": "node ./lib/standalone/start-server.js",
"start:dev": "bash start-dev",
"test": "NODE_OPTIONS=--experimental-vm-modules jest --runInBand --silent",
"typecheck": "tsc --build"
"typecheck": "tsc --build",
"test": "vitest run --silent='passed-only'"
},
"engines": {
"node": "^18.20.0 || ^20.15.0 || >=22.0.0"
"node": "^20.19.3 || >=22.0.0"
},
"installConfig": {
"hoistingLimits": "workspaces"

View file

@ -1,40 +1,42 @@
const express = require('express')
const Grant = require('grant').default.express()
const merge = require('lodash/merge')
const cookieParser = require('cookie-parser')
const interceptor = require('express-interceptor')
const { randomUUID } = require('node:crypto')
const grantConfig = require('./config/grant')()
const providerManager = require('./server/provider')
const controllers = require('./server/controllers')
const s3 = require('./server/controllers/s3')
const url = require('./server/controllers/url')
const googlePicker = require('./server/controllers/googlePicker')
const createEmitter = require('./server/emitter')
const redis = require('./server/redis')
const jobs = require('./server/jobs')
const logger = require('./server/logger')
const middlewares = require('./server/middlewares')
const {
getMaskableSecrets,
import { randomUUID } from 'node:crypto'
import cookieParser from 'cookie-parser'
import express from 'express'
import interceptor from 'express-interceptor'
import grant from 'grant'
import merge from 'lodash/merge.js'
import packageJson from '../package.json' with { type: 'json' }
import {
defaultOptions,
getMaskableSecrets,
validateConfig,
} = require('./config/companion')
const {
} from './config/companion.js'
import grantConfigFn from './config/grant.js'
import googlePicker from './server/controllers/googlePicker.js'
import * as controllers from './server/controllers/index.js'
import s3 from './server/controllers/s3.js'
import url from './server/controllers/url.js'
import createEmitter from './server/emitter/index.js'
import { getURLBuilder } from './server/helpers/utils.js'
import * as jobs from './server/jobs.js'
import logger from './server/logger.js'
import * as middlewares from './server/middlewares.js'
import { getCredentialsOverrideMiddleware } from './server/provider/credentials.js'
import {
ProviderApiError,
ProviderUserError,
ProviderAuthError,
} = require('./server/provider/error')
const {
getCredentialsOverrideMiddleware,
} = require('./server/provider/credentials')
const { getURLBuilder } = require('./server/helpers/utils')
// @ts-ignore
const { version } = require('../package.json')
const { isOAuthProvider } = require('./server/provider/Provider')
ProviderUserError,
} from './server/provider/error.js'
import * as providerManager from './server/provider/index.js'
import { isOAuthProvider } from './server/provider/Provider.js'
import * as redis from './server/redis.js'
function setLoggerProcessName({ loggerProcessName }) {
import socket from './server/socket.js'
export { socket }
const grantConfig = grantConfigFn()
export function setLoggerProcessName({ loggerProcessName }) {
if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
}
@ -72,14 +74,11 @@ const interceptGrantErrorResponse = interceptor((req, res) => {
})
// make the errors available publicly for custom providers
module.exports.errors = {
export const errors = {
ProviderApiError,
ProviderUserError,
ProviderAuthError,
}
module.exports.socket = require('./server/socket')
module.exports.setLoggerProcessName = setLoggerProcessName
/**
* Entry point into initializing the Companion app.
@ -87,7 +86,7 @@ module.exports.setLoggerProcessName = setLoggerProcessName
* @param {object} optionsArg
* @returns {{ app: import('express').Express, emitter: any }}}
*/
module.exports.app = (optionsArg = {}) => {
export function app(optionsArg = {}) {
setLoggerProcessName(optionsArg)
validateConfig(optionsArg)
@ -131,7 +130,7 @@ module.exports.app = (optionsArg = {}) => {
express.urlencoded({ extended: false }),
getCredentialsOverrideMiddleware(providers, options),
)
app.use(Grant(grantConfig))
app.use(grant.default.express(grantConfig))
app.use((req, res, next) => {
if (options.sendSelfEndpoint) {
@ -307,7 +306,7 @@ module.exports.app = (optionsArg = {}) => {
interval: options.periodicPingInterval,
count: options.periodicPingCount,
staticPayload: options.periodicPingStaticPayload,
version,
version: packageJson.version,
processId,
})

View file

@ -1,9 +1,9 @@
const fs = require('node:fs')
const { isURL } = require('validator')
const logger = require('../server/logger')
const { defaultGetKey } = require('../server/helpers/utils')
import fs from 'node:fs'
import validator from 'validator'
import { defaultGetKey } from '../server/helpers/utils.js'
import logger from '../server/logger.js'
const defaultOptions = {
export const defaultOptions = {
server: {
protocol: 'http',
path: '',
@ -28,7 +28,7 @@ const defaultOptions = {
/**
* @param {object} companionOptions
*/
function getMaskableSecrets(companionOptions) {
export function getMaskableSecrets(companionOptions) {
const secrets = []
const { providerOptions, customProviders, s3 } = companionOptions
@ -60,7 +60,7 @@ function getMaskableSecrets(companionOptions) {
*
* @param {object} companionOptions
*/
const validateConfig = (companionOptions) => {
export const validateConfig = (companionOptions) => {
const mandatoryOptions = ['secret', 'filePath', 'server.host']
/** @type {string[]} */
const unspecified = []
@ -148,7 +148,7 @@ const validateConfig = (companionOptions) => {
(!Array.isArray(periodicPingUrls) ||
periodicPingUrls.some(
(url2) =>
!isURL(url2, {
!validator.isURL(url2, {
protocols: ['http', 'https'],
require_protocol: true,
require_tld: false,
@ -162,9 +162,3 @@ const validateConfig = (companionOptions) => {
throw new TypeError('Option maxFilenameLength must be greater than 0')
}
}
module.exports = {
defaultOptions,
getMaskableSecrets,
validateConfig,
}

View file

@ -4,7 +4,7 @@ const defaults = {
}
// oauth configuration for provider services that are used.
module.exports = () => {
export default () => {
return {
// we need separate auth providers because scopes are different,
// and because it would be a too big rewrite to allow reuse of the same provider.

View file

@ -1,31 +1,27 @@
const tus = require('tus-js-client')
const { randomUUID } = require('node:crypto')
const validator = require('validator')
const { pipeline } = require('node:stream/promises')
const { join } = require('node:path')
const fs = require('node:fs')
const throttle = require('lodash/throttle')
const { once } = require('node:events')
const { FormData } = require('formdata-node')
const { Upload } = require('@aws-sdk/lib-storage')
const {
rfc2047EncodeMetadata,
import { randomUUID } from 'node:crypto'
import { once } from 'node:events'
import fs, { createReadStream, createWriteStream, ReadStream } from 'node:fs'
import { stat, unlink } from 'node:fs/promises'
import { join } from 'node:path'
import { pipeline } from 'node:stream/promises'
import { Upload } from '@aws-sdk/lib-storage'
import { FormData } from 'formdata-node'
import got from 'got'
import throttle from 'lodash/throttle.js'
import { serializeError } from 'serialize-error'
import tus from 'tus-js-client'
import validator from 'validator'
import emitter from './emitter/index.js'
import headerSanitize from './header-blacklist.js'
import {
getBucket,
hasMatch,
jsonStringify,
rfc2047EncodeMetadata,
truncateFilename,
} = require('./helpers/utils')
const got = require('./got')
const { createReadStream, createWriteStream, ReadStream } = fs
const { stat, unlink } = fs.promises
const emitter = require('./emitter')
const { jsonStringify, hasMatch } = require('./helpers/utils')
const logger = require('./logger')
const headerSanitize = require('./header-blacklist')
const redis = require('./redis')
} from './helpers/utils.js'
import * as logger from './logger.js'
import * as redis from './redis.js'
// Need to limit length or we can get
// "MetadataTooLarge: Your metadata headers exceed the maximum allowed metadata size" in tus / S3
@ -40,7 +36,7 @@ function exceedsMaxFileSize(maxFileSize, size) {
return maxFileSize && size && size > maxFileSize
}
class ValidationError extends Error {
export class ValidationError extends Error {
name = 'ValidationError'
}
@ -126,7 +122,7 @@ const states = {
done: 'done',
}
class Uploader {
export default class Uploader {
/** @type {import('ioredis').Redis} */
storage
@ -527,8 +523,6 @@ class Uploader {
async #emitError(err) {
// delete stack to avoid sending server info to client
// see PR discussion https://github.com/transloadit/uppy/pull/3832
// @ts-ignore
const { serializeError } = await import('serialize-error')
const { stack, ...serializedErr } = serializeError(err)
const dataToEmit = {
action: 'error',
@ -688,7 +682,7 @@ class Uploader {
try {
const httpMethod =
(this.options.httpMethod || '').toUpperCase() === 'PUT' ? 'put' : 'post'
const runRequest = (await got)[httpMethod]
const runRequest = await got[httpMethod]
const response = await runRequest(url, reqOptions)
@ -779,6 +773,3 @@ class Uploader {
Uploader.FILE_NAME_PREFIX = 'uppy-file'
Uploader.STORAGE_PREFIX = 'companion'
module.exports = Uploader
module.exports.ValidationError = ValidationError

View file

@ -1,11 +1,10 @@
/**
* oAuth callback. Encrypts the access token and sends the new token with the response,
*/
const serialize = require('serialize-javascript')
const tokenService = require('../helpers/jwt')
const logger = require('../logger')
const oAuthState = require('../helpers/oauth-state')
import serialize from 'serialize-javascript'
import * as tokenService from '../helpers/jwt.js'
import * as oAuthState from '../helpers/oauth-state.js'
import logger from '../logger.js'
const closePageHtml = (origin) => `
<!DOCTYPE html>
@ -28,7 +27,7 @@ const closePageHtml = (origin) => `
* @param {object} res
* @param {Function} next
*/
module.exports = function callback(req, res, next) {
export default function callback(req, res, next) {
const { providerName } = req.params
const grant = req.session.grant || {}

View file

@ -1,4 +1,4 @@
const oAuthState = require('../helpers/oauth-state')
import * as oAuthState from '../helpers/oauth-state.js'
/**
* Derived from `cors` npm package.
@ -82,7 +82,7 @@ function getClientOrigin(base64EncodedState) {
* @param {object} req
* @param {object} res
*/
module.exports = function connect(req, res, next) {
export default function connect(req, res, next) {
const stateObj = oAuthState.generateState()
if (req.companion.options.server.oauthDomain) {
@ -116,4 +116,5 @@ module.exports = function connect(req, res, next) {
}
encodeStateAndRedirect(req, res, stateObj)
}
module.exports.isOriginAllowed = isOriginAllowed
export { isOriginAllowed }

View file

@ -1,6 +1,10 @@
const { respondWithError } = require('../provider/error')
import { respondWithError } from '../provider/error.js'
async function deauthCallback({ body, companion, headers }, res, next) {
export default async function deauthCallback(
{ body, companion, headers },
res,
next,
) {
// we need the provider instance to decide status codes because
// this endpoint does not cater to a uniform client.
// It doesn't respond to Uppy client like other endpoints.
@ -17,5 +21,3 @@ async function deauthCallback({ body, companion, headers }, res, next) {
next(err)
}
}
module.exports = deauthCallback

View file

@ -1,8 +1,8 @@
const logger = require('../logger')
const { startDownUpload } = require('../helpers/upload')
const { respondWithError } = require('../provider/error')
import { startDownUpload } from '../helpers/upload.js'
import logger from '../logger.js'
import { respondWithError } from '../provider/error.js'
async function get(req, res) {
export default async function get(req, res) {
const { id } = req.params
const { providerUserSession } = req.companion
const { provider } = req.companion
@ -22,5 +22,3 @@ async function get(req, res) {
res.status(500).json({ message: 'Failed to download file' })
}
}
module.exports = get

View file

@ -1,12 +1,11 @@
const express = require('express')
const assert = require('node:assert')
const { startDownUpload } = require('../helpers/upload')
const { validateURL } = require('../helpers/request')
const logger = require('../logger')
const { downloadURL } = require('../download')
const { streamGoogleFile } = require('../provider/google/drive')
const { respondWithError } = require('../provider/error')
import assert from 'node:assert'
import express from 'express'
import { downloadURL } from '../download.js'
import { validateURL } from '../helpers/request.js'
import { startDownUpload } from '../helpers/upload.js'
import logger from '../logger.js'
import { respondWithError } from '../provider/error.js'
import { streamGoogleFile } from '../provider/google/drive/index.js'
const getAuthHeader = (token) => ({ authorization: `Bearer ${token}` })
@ -47,4 +46,4 @@ const get = async (req, res) => {
}
}
module.exports = () => express.Router().post('/get', express.json(), get)
export default () => express.Router().post('/get', express.json(), get)

View file

@ -1,14 +1,12 @@
module.exports = {
callback: require('./callback'),
deauthorizationCallback: require('./deauth-callback'),
sendToken: require('./send-token'),
get: require('./get'),
thumbnail: require('./thumbnail'),
list: require('./list'),
simpleAuth: require('./simple-auth'),
logout: require('./logout'),
connect: require('./connect'),
preauth: require('./preauth'),
redirect: require('./oauth-redirect'),
refreshToken: require('./refresh-token'),
}
export { default as callback } from './callback.js'
export { default as connect } from './connect.js'
export { default as deauthorizationCallback } from './deauth-callback.js'
export { default as get } from './get.js'
export { default as list } from './list.js'
export { default as logout } from './logout.js'
export { default as redirect } from './oauth-redirect.js'
export { default as preauth } from './preauth.js'
export { default as refreshToken } from './refresh-token.js'
export { default as sendToken } from './send-token.js'
export { default as simpleAuth } from './simple-auth.js'
export { default as thumbnail } from './thumbnail.js'

View file

@ -1,6 +1,6 @@
const { respondWithError } = require('../provider/error')
import { respondWithError } from '../provider/error.js'
async function list({ query, params, companion }, res, next) {
export default async function list({ query, params, companion }, res, next) {
const { providerUserSession } = companion
try {
@ -16,5 +16,3 @@ async function list({ query, params, companion }, res, next) {
next(err)
}
}
module.exports = list

View file

@ -1,12 +1,12 @@
const tokenService = require('../helpers/jwt')
const { respondWithError } = require('../provider/error')
import * as tokenService from '../helpers/jwt.js'
import { respondWithError } from '../provider/error.js'
/**
*
* @param {object} req
* @param {object} res
*/
async function logout(req, res, next) {
export default async function logout(req, res, next) {
const cleanSession = () => {
if (req.session.grant) {
req.session.grant.state = null
@ -40,5 +40,3 @@ async function logout(req, res, next) {
next(err)
}
}
module.exports = logout

View file

@ -1,14 +1,14 @@
const qs = require('node:querystring')
const { URL } = require('node:url')
const { hasMatch } = require('../helpers/utils')
const oAuthState = require('../helpers/oauth-state')
import qs from 'node:querystring'
import { URL } from 'node:url'
import * as oAuthState from '../helpers/oauth-state.js'
import { hasMatch } from '../helpers/utils.js'
/**
*
* @param {object} req
* @param {object} res
*/
module.exports = function oauthRedirect(req, res) {
export default function oauthRedirect(req, res) {
const params = qs.stringify(req.query)
const { oauthProvider } = req.companion.providerClass
if (!req.companion.options.server.oauthDomain) {

View file

@ -1,7 +1,7 @@
const tokenService = require('../helpers/jwt')
const logger = require('../logger')
import * as tokenService from '../helpers/jwt.js'
import logger from '../logger.js'
function preauth(req, res) {
export default function preauth(req, res) {
if (!req.body || !req.body.params) {
logger.info('invalid request data received', 'preauth.bad')
return res.sendStatus(400)
@ -19,5 +19,3 @@ function preauth(req, res) {
)
return res.json({ token: preAuthToken })
}
module.exports = preauth

View file

@ -1,11 +1,11 @@
const tokenService = require('../helpers/jwt')
const { respondWithError } = require('../provider/error')
const logger = require('../logger')
import * as tokenService from '../helpers/jwt.js'
import logger from '../logger.js'
import { respondWithError } from '../provider/error.js'
// https://www.dropboxforum.com/t5/Dropbox-API-Support-Feedback/Get-refresh-token-from-access-token/td-p/596739
// https://developers.dropbox.com/oauth-guide
// https://github.com/simov/grant/issues/149
async function refreshToken(req, res, next) {
export default async function refreshToken(req, res, next) {
const { providerName } = req.params
const { key: clientId, secret: clientSecret } = req.companion.options
@ -61,5 +61,3 @@ async function refreshToken(req, res, next) {
next(err)
}
}
module.exports = refreshToken

View file

@ -1,23 +1,21 @@
const express = require('express')
const {
import {
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
CreateMultipartUploadCommand,
ListPartsCommand,
UploadPartCommand,
AbortMultipartUploadCommand,
CompleteMultipartUploadCommand,
} = require('@aws-sdk/client-s3')
const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts')
const { createPresignedPost } = require('@aws-sdk/s3-presigned-post')
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
const {
rfc2047EncodeMetadata,
} from '@aws-sdk/client-s3'
import { GetFederationTokenCommand, STSClient } from '@aws-sdk/client-sts'
import { createPresignedPost } from '@aws-sdk/s3-presigned-post'
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
import express from 'express'
import {
getBucket,
rfc2047EncodeMetadata,
truncateFilename,
} = require('../helpers/utils')
} from '../helpers/utils.js'
module.exports = function s3(config) {
export default function s3(config) {
if (typeof config.acl !== 'string' && config.acl != null) {
throw new TypeError('s3: The `acl` option must be a string or null')
}

View file

@ -1,7 +1,6 @@
const serialize = require('serialize-javascript')
const { isOriginAllowed } = require('./connect')
const oAuthState = require('../helpers/oauth-state')
import serialize from 'serialize-javascript'
import * as oAuthState from '../helpers/oauth-state.js'
import { isOriginAllowed } from './connect.js'
/**
*
@ -51,7 +50,7 @@ const htmlContent = (token, origin) => {
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
module.exports = function sendToken(req, res, next) {
export default function sendToken(req, res, next) {
// @ts-expect-error untyped
const { companion } = req
const uppyAuthToken = companion.authToken

View file

@ -1,8 +1,8 @@
const tokenService = require('../helpers/jwt')
const { respondWithError } = require('../provider/error')
const logger = require('../logger')
import * as tokenService from '../helpers/jwt.js'
import logger from '../logger.js'
import { respondWithError } from '../provider/error.js'
async function simpleAuth(req, res, next) {
export default async function simpleAuth(req, res, next) {
const { providerName } = req.params
try {
@ -39,5 +39,3 @@ async function simpleAuth(req, res, next) {
next(err)
}
}
module.exports = simpleAuth

View file

@ -1,4 +1,4 @@
const { respondWithError } = require('../provider/error')
import { respondWithError } from '../provider/error.js'
/**
*
@ -22,4 +22,4 @@ async function thumbnail(req, res, next) {
}
}
module.exports = thumbnail
export default thumbnail

View file

@ -1,11 +1,9 @@
const express = require('express')
const { startDownUpload } = require('../helpers/upload')
const { downloadURL } = require('../download')
const { validateURL } = require('../helpers/request')
const { getURLMeta } = require('../helpers/request')
const logger = require('../logger')
const { respondWithError } = require('../provider/error')
import express from 'express'
import { downloadURL } from '../download.js'
import { getURLMeta, validateURL } from '../helpers/request.js'
import { startDownUpload } from '../helpers/upload.js'
import logger from '../logger.js'
import { respondWithError } from '../provider/error.js'
/**
* @callback downloadCallback
@ -73,7 +71,7 @@ const get = async (req, res) => {
}
}
module.exports = () =>
export default () =>
express
.Router()
.post('/meta', express.json(), meta)

View file

@ -1,6 +1,6 @@
const logger = require('./logger')
const { getProtectedGot } = require('./helpers/request')
const { prepareStream } = require('./helpers/utils')
import { getProtectedGot } from './helpers/request.js'
import { prepareStream } from './helpers/utils.js'
import logger from './logger.js'
/**
* Downloads the content in the specified url, and passes the data
@ -11,9 +11,9 @@ const { prepareStream } = require('./helpers/utils')
* @param {string} traceId
* @returns {Promise}
*/
const downloadURL = async (url, allowLocalIPs, traceId, options) => {
export const downloadURL = async (url, allowLocalIPs, traceId, options) => {
try {
const protectedGot = await getProtectedGot({ allowLocalIPs })
const protectedGot = getProtectedGot({ allowLocalIPs })
const stream = protectedGot.stream.get(url, {
responseType: 'json',
...options,
@ -25,7 +25,3 @@ const downloadURL = async (url, allowLocalIPs, traceId, options) => {
throw err
}
}
module.exports = {
downloadURL,
}

View file

@ -1,5 +1,5 @@
const { EventEmitter } = require('node:events')
import { EventEmitter } from 'node:events'
module.exports = () => {
export default function defaultEmitter() {
return new EventEmitter()
}

View file

@ -1,5 +1,5 @@
const nodeEmitter = require('./default-emitter')
const redisEmitter = require('./redis-emitter')
import nodeEmitter from './default-emitter.js'
import redisEmitter from './redis-emitter.js'
let emitter
@ -8,7 +8,7 @@ let emitter
* Used to transmit events (such as progress, upload completion) from controllers,
* such as the Google Drive 'get' controller, along to the client.
*/
module.exports = (redisClient, redisPubSubScope) => {
export default function getEmitter(redisClient, redisPubSubScope) {
if (!emitter) {
emitter = redisClient
? redisEmitter(redisClient, redisPubSubScope)

View file

@ -1,7 +1,6 @@
const { EventEmitter } = require('node:events')
const { default: safeStringify } = require('fast-safe-stringify')
const logger = require('../logger')
import { EventEmitter } from 'node:events'
import safeStringify from 'fast-safe-stringify'
import * as logger from '../logger.js'
function replacer(key, value) {
// Remove the circular structure and internal ones
@ -17,7 +16,7 @@ function replacer(key, value) {
* @param {string} redisPubSubScope
* @returns
*/
module.exports = (redisClient, redisPubSubScope) => {
export default function redisEmitter(redisClient, redisPubSubScope) {
const prefix = redisPubSubScope ? `${redisPubSubScope}:` : ''
const getPrefixedEventName = (eventName) => `${prefix}${eventName}`
@ -176,7 +175,7 @@ module.exports = (redisClient, redisPubSubScope) => {
await runWhenConnected(async ({ publisher }) =>
publisher.publish(
getPrefixedEventName(eventName),
safeStringify(args, replacer),
safeStringify.default(args, replacer),
),
)
}

View file

@ -1,3 +0,0 @@
const gotPromise = import('got')
module.exports = gotPromise.then((got) => got.default)

View file

@ -1,4 +1,4 @@
const logger = require('./logger')
import * as logger from './logger.js'
/**
* Forbidden header names.
@ -49,7 +49,7 @@ const isForbiddenHeader = (header) => {
return forbidden
}
module.exports = (headers) => {
export default function headerBlacklist(headers) {
if (
headers == null ||
typeof headers !== 'object' ||

View file

@ -1,5 +1,5 @@
const jwt = require('jsonwebtoken')
const { encrypt, decrypt } = require('./utils')
import jwt from 'jsonwebtoken'
import { decrypt, encrypt } from './utils.js'
// The Uppy auth token is an encrypted JWT & JSON encoded container.
// It used to simply contain an OAuth access_token and refresh_token for a specific provider.
@ -17,13 +17,8 @@ const { encrypt, decrypt } = require('./utils')
// even though the provider refresh token would still have been accepted and
// there's no way for them to retry their failed files.
// With 400 days, there's still a theoretical possibility but very low.
const MAX_AGE_REFRESH_TOKEN = 60 * 60 * 24 * 400
const MAX_AGE_24H = 60 * 60 * 24
module.exports.MAX_AGE_24H = MAX_AGE_24H
module.exports.MAX_AGE_REFRESH_TOKEN = MAX_AGE_REFRESH_TOKEN
export const MAX_AGE_REFRESH_TOKEN = 60 * 60 * 24 * 400
export const MAX_AGE_24H = 60 * 60 * 24
/**
*
* @param {*} data
@ -49,7 +44,7 @@ const verifyToken = (token, secret) => {
* @param {*} payload
* @param {string} secret
*/
module.exports.generateEncryptedToken = (
export const generateEncryptedToken = (
payload,
secret,
maxAge = MAX_AGE_24H,
@ -62,12 +57,8 @@ module.exports.generateEncryptedToken = (
* @param {*} payload
* @param {string} secret
*/
module.exports.generateEncryptedAuthToken = (payload, secret, maxAge) => {
return module.exports.generateEncryptedToken(
JSON.stringify(payload),
secret,
maxAge,
)
export const generateEncryptedAuthToken = (payload, secret, maxAge) => {
return generateEncryptedToken(JSON.stringify(payload), secret, maxAge)
}
/**
@ -75,7 +66,7 @@ module.exports.generateEncryptedAuthToken = (payload, secret, maxAge) => {
* @param {string} token
* @param {string} secret
*/
module.exports.verifyEncryptedToken = (token, secret) => {
export const verifyEncryptedToken = (token, secret) => {
const ret = verifyToken(decrypt(token, secret), secret)
if (!ret) throw new Error('No payload')
return ret
@ -86,8 +77,8 @@ module.exports.verifyEncryptedToken = (token, secret) => {
* @param {string} token
* @param {string} secret
*/
module.exports.verifyEncryptedAuthToken = (token, secret, providerName) => {
const json = module.exports.verifyEncryptedToken(token, secret)
export const verifyEncryptedAuthToken = (token, secret, providerName) => {
const json = verifyEncryptedToken(token, secret)
const tokens = JSON.parse(json)
if (!tokens[providerName])
throw new Error(`Missing token payload for provider ${providerName}`)
@ -133,7 +124,7 @@ const addToCookies = ({
res.cookie(getCookieName(oauthProvider), token, cookieOptions)
}
module.exports.addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
export const addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
// some providers need the token in cookies for thumbnail/image requests
if (req.companion.provider.needsCookieAuth) {
addToCookies({
@ -152,7 +143,7 @@ module.exports.addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
* @param {object} companionOptions
* @param {string} oauthProvider
*/
module.exports.removeFromCookies = (res, companionOptions, oauthProvider) => {
export const removeFromCookies = (res, companionOptions, oauthProvider) => {
// options must be identical to those given to res.cookie(), excluding expires and maxAge.
// https://expressjs.com/en/api.html#res.clearCookie
const cookieOptions = getCommonCookieOptions({ companionOptions })

View file

@ -1,26 +1,26 @@
const crypto = require('node:crypto')
const { encrypt, decrypt } = require('./utils')
import crypto from 'node:crypto'
import { decrypt, encrypt } from './utils.js'
module.exports.encodeState = (state, secret) => {
export const encodeState = (state, secret) => {
const encodedState = Buffer.from(JSON.stringify(state)).toString('base64')
return encrypt(encodedState, secret)
}
module.exports.decodeState = (state, secret) => {
export const decodeState = (state, secret) => {
const encodedState = decrypt(state, secret)
return JSON.parse(atob(encodedState))
}
module.exports.generateState = () => {
export const generateState = () => {
return {
id: crypto.randomBytes(10).toString('hex'),
}
}
module.exports.getFromState = (state, name, secret) => {
return module.exports.decodeState(state, secret)[name]
export const getFromState = (state, name, secret) => {
return decodeState(state, secret)[name]
}
module.exports.getGrantDynamicFromRequest = (req) => {
export const getGrantDynamicFromRequest = (req) => {
return req.session.grant?.dynamic ?? {}
}

View file

@ -1,14 +1,13 @@
const http = require('node:http')
const https = require('node:https')
const dns = require('node:dns')
const ipaddr = require('ipaddr.js')
const path = require('node:path')
const contentDisposition = require('content-disposition')
const validator = require('validator')
import dns from 'node:dns'
import http from 'node:http'
import https from 'node:https'
import path from 'node:path'
import contentDisposition from 'content-disposition'
import got from 'got'
import ipaddr from 'ipaddr.js'
import validator from 'validator'
const got = require('../got')
const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
export const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
// Example scary IPs that should return false (ipv6-to-ipv4 mapped):
// ::FFFF:127.0.0.1
@ -16,8 +15,6 @@ const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
const isDisallowedIP = (ipAddress) =>
ipaddr.parse(ipAddress).range() !== 'unicast'
module.exports.FORBIDDEN_IP_ADDRESS = FORBIDDEN_IP_ADDRESS
/**
* Validates that the download URL is secure
*
@ -41,7 +38,7 @@ const validateURL = (url, allowLocalUrls) => {
return true
}
module.exports.validateURL = validateURL
export { validateURL }
/**
* Returns http Agent that will prevent requests to private IPs (to prevent SSRF)
@ -100,9 +97,9 @@ const getProtectedHttpAgent = ({ protocol, allowLocalIPs }) => {
}
}
module.exports.getProtectedHttpAgent = getProtectedHttpAgent
export { getProtectedHttpAgent }
async function getProtectedGot({ allowLocalIPs }) {
function getProtectedGot({ allowLocalIPs }) {
const HttpAgent = getProtectedHttpAgent({ protocol: 'http', allowLocalIPs })
const HttpsAgent = getProtectedHttpAgent({
protocol: 'https',
@ -112,10 +109,10 @@ async function getProtectedGot({ allowLocalIPs }) {
const httpsAgent = new HttpsAgent()
// @ts-ignore
return (await got).extend({ agent: { http: httpAgent, https: httpsAgent } })
return got.extend({ agent: { http: httpAgent, https: httpsAgent } })
}
module.exports.getProtectedGot = getProtectedGot
export { getProtectedGot }
/**
* Gets the size and content type of a url's content
@ -124,13 +121,13 @@ module.exports.getProtectedGot = getProtectedGot
* @param {boolean} allowLocalIPs
* @returns {Promise<{name: string, type: string, size: number}>}
*/
exports.getURLMeta = async (
export async function getURLMeta(
url,
allowLocalIPs = false,
options = undefined,
) => {
) {
async function requestWithMethod(method) {
const protectedGot = await getProtectedGot({ allowLocalIPs })
const protectedGot = getProtectedGot({ allowLocalIPs })
const stream = protectedGot.stream(url, {
method,
throwHttpErrors: false,

View file

@ -1,7 +1,7 @@
const Uploader = require('../Uploader')
const logger = require('../logger')
import logger from '../logger.js'
import Uploader from '../Uploader.js'
async function startDownUpload({ req, res, getSize, download }) {
export async function startDownUpload({ req, res, getSize, download }) {
logger.debug('Starting download stream.', null, req.id)
const { stream, size: maybeSize } = await download()
@ -48,5 +48,3 @@ async function startDownUpload({ req, res, getSize, download }) {
// NOTE: the Uploader will continue running after the http request is responded
res.status(200).json({ token: uploader.token })
}
module.exports = { startDownUpload }

View file

@ -1,4 +1,4 @@
const crypto = require('node:crypto')
import crypto from 'node:crypto'
const authTagLength = 16
const nonceLength = 16
@ -11,7 +11,7 @@ const ivLength = 12
* @param {string[]} criteria
* @returns {boolean}
*/
exports.hasMatch = (value, criteria) => {
export const hasMatch = (value, criteria) => {
return criteria.some((i) => {
return value === i || new RegExp(i).test(value)
})
@ -22,7 +22,7 @@ exports.hasMatch = (value, criteria) => {
* @param {object} data
* @returns {string}
*/
exports.jsonStringify = (data) => {
export const jsonStringify = (data) => {
const cache = []
return JSON.stringify(data, (key, value) => {
if (typeof value === 'object' && value !== null) {
@ -42,7 +42,7 @@ exports.jsonStringify = (data) => {
*
* @param {object} options companion options
*/
module.exports.getURLBuilder = (options) => {
export function getURLBuilder(options) {
/**
* Builds companion targeted url
*
@ -73,7 +73,7 @@ module.exports.getURLBuilder = (options) => {
return buildURL
}
module.exports.getRedirectPath = (providerName) => `/${providerName}/redirect`
export const getRedirectPath = (providerName) => `/${providerName}/redirect`
/**
* Create an AES-CCM encryption key and initialization vector from the provided secret
@ -104,7 +104,7 @@ function createSecrets(secret, nonce) {
* @param {string|Buffer} secret
* @returns {string} Ciphertext as a hex string, prefixed with 32 hex characters containing the iv.
*/
module.exports.encrypt = (input, secret) => {
export const encrypt = (input, secret) => {
const nonce = crypto.randomBytes(nonceLength)
const { key, iv } = createSecrets(secret, nonce)
const cipher = crypto.createCipheriv('aes-256-ccm', key, iv, {
@ -126,7 +126,7 @@ module.exports.encrypt = (input, secret) => {
* @param {string|Buffer} secret
* @returns {string} Decrypted value.
*/
module.exports.decrypt = (encrypted, secret) => {
export const decrypt = (encrypted, secret) => {
const nonceHexLength = nonceLength * 2 // because hex encoding uses 2 bytes per byte
// NOTE: The first 32 characters are the nonce, in hex format.
@ -164,14 +164,14 @@ module.exports.decrypt = (encrypted, secret) => {
return decrypted.toString('utf8')
}
module.exports.defaultGetKey = ({ filename }) => {
export const defaultGetKey = ({ filename }) => {
return `${crypto.randomUUID()}-${filename}`
}
/**
* Our own HttpError in cases where we can't use `got`'s `HTTPError`
*/
class HttpError extends Error {
export class HttpError extends Error {
statusCode
responseJson
@ -184,9 +184,7 @@ class HttpError extends Error {
}
}
module.exports.HttpError = HttpError
module.exports.prepareStream = async (stream) =>
export const prepareStream = async (stream) =>
new Promise((resolve, reject) => {
stream
.on('response', (response) => {
@ -229,7 +227,7 @@ module.exports.prepareStream = async (stream) =>
})
})
module.exports.getBasicAuthHeader = (key, secret) => {
export const getBasicAuthHeader = (key, secret) => {
const base64 = Buffer.from(`${key}:${secret}`, 'binary').toString('base64')
return `Basic ${base64}`
}
@ -241,7 +239,7 @@ const rfc2047Encode = (dataIn) => {
return `=?UTF-8?B?${Buffer.from(data).toString('base64')}?=` // We encode non-ASCII strings
}
module.exports.rfc2047EncodeMetadata = (metadata) =>
export const rfc2047EncodeMetadata = (metadata) =>
Object.fromEntries(
Object.entries(metadata).map((entry) => entry.map(rfc2047Encode)),
)
@ -260,7 +258,7 @@ module.exports.rfc2047EncodeMetadata = (metadata) =>
* }} param0
* @returns
*/
module.exports.getBucket = ({ bucketOrFn, req, metadata, filename }) => {
export const getBucket = ({ bucketOrFn, req, metadata, filename }) => {
const bucket =
typeof bucketOrFn === 'function'
? bucketOrFn({ req, metadata, filename })
@ -282,6 +280,6 @@ module.exports.getBucket = ({ bucketOrFn, req, metadata, filename }) => {
* @param {number} maxFilenameLength
* @returns {string}
*/
module.exports.truncateFilename = (filename, maxFilenameLength) => {
export const truncateFilename = (filename, maxFilenameLength) => {
return filename.slice(maxFilenameLength * -1)
}

View file

@ -1,12 +1,10 @@
const schedule = require('node-schedule')
const fs = require('node:fs')
const path = require('node:path')
const { setTimeout: sleep } = require('node:timers/promises')
const got = require('./got')
const { FILE_NAME_PREFIX } = require('./Uploader')
const logger = require('./logger')
import fs from 'node:fs'
import path from 'node:path'
import { setTimeout as sleep } from 'node:timers/promises'
import got from 'got'
import schedule from 'node-schedule'
import * as logger from './logger.js'
import Uploader from './Uploader.js'
const cleanUpFinishedUploads = (dirPath) => {
logger.info(
@ -24,7 +22,7 @@ const cleanUpFinishedUploads = (dirPath) => {
// if it does not contain FILE_NAME_PREFIX then it probably wasn't created by companion.
// this is to avoid deleting unintended files, e.g if a wrong path was accidentally given
// by a developer.
if (!file.startsWith(FILE_NAME_PREFIX)) {
if (!file.startsWith(Uploader.FILE_NAME_PREFIX)) {
logger.info(`skipping file ${file}`, 'jobs.cleanup.skip')
return
}
@ -56,7 +54,7 @@ const cleanUpFinishedUploads = (dirPath) => {
*
* @param {string} dirPath path to the directory which you want to clean
*/
exports.startCleanUpJob = (dirPath) => {
export function startCleanUpJob(dirPath) {
logger.info('starting clean up job', 'jobs.cleanup.start')
// run once a day
schedule.scheduleJob('0 23 * * *', () => cleanUpFinishedUploads(dirPath))
@ -67,7 +65,7 @@ async function runPeriodicPing({ urls, payload, requestTimeout }) {
await Promise.all(
urls.map(async (url) => {
try {
await (await got).post(url, {
await got.post(url, {
json: payload,
timeout: { request: requestTimeout },
})
@ -80,14 +78,14 @@ async function runPeriodicPing({ urls, payload, requestTimeout }) {
// This function is used to start a periodic POST request against a user-defined URL
// or set of URLs, for example as a watch dog health check.
exports.startPeriodicPingJob = async ({
export async function startPeriodicPingJob({
urls,
interval = 60000,
count,
staticPayload = {},
version,
processId,
}) => {
}) {
if (urls.length === 0) return
logger.info('Starting periodic ping job', 'jobs.periodic.ping.start')

View file

@ -1,20 +1,16 @@
const escapeStringRegexp = require('escape-string-regexp')
const util = require('node:util')
const supportsColors = require('supports-color')
import util from 'node:util'
import escapeStringRegexp from 'escape-string-regexp'
import supportsColors from 'supports-color'
const valuesToMask = []
let valuesToMask = []
/**
* Adds a list of strings that should be masked by the logger.
* This function can only be called once through out the life of the server.
*
* @param {Array} maskables a list of strings to be masked
*/
exports.setMaskables = (maskables) => {
maskables.forEach((i) => {
valuesToMask.push(escapeStringRegexp(i))
})
Object.freeze(valuesToMask)
export function setMaskables(maskables) {
valuesToMask = maskables.map((i) => escapeStringRegexp(i))
}
/**
@ -34,7 +30,7 @@ function maskMessage(msg) {
let processName = 'companion'
exports.setProcessName = (newProcessName) => {
export function setProcessName(newProcessName) {
processName = newProcessName
}
@ -90,7 +86,7 @@ const log = ({ arg, tag = '', level, traceId = '', color = [] }) => {
* @param {string} [tag] a unique tag to easily search for this message
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.info = (msg, tag, traceId) => {
export function info(msg, tag, traceId) {
log({ arg: msg, tag, level: 'info', traceId })
}
@ -101,7 +97,7 @@ exports.info = (msg, tag, traceId) => {
* @param {string} [tag] a unique tag to easily search for this message
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.warn = (msg, tag, traceId) => {
export function warn(msg, tag, traceId) {
log({ arg: msg, tag, level: 'warn', traceId, color: ['bold', 'yellow'] })
}
@ -112,7 +108,7 @@ exports.warn = (msg, tag, traceId) => {
* @param {string} [tag] a unique tag to easily search for this message
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.error = (msg, tag, traceId) => {
export function error(msg, tag, traceId) {
log({ arg: msg, tag, level: 'error', traceId, color: ['bold', 'red'] })
}
@ -123,8 +119,11 @@ exports.error = (msg, tag, traceId) => {
* @param {string} [tag] a unique tag to easily search for this message
* @param {string} [traceId] a unique id to easily trace logs tied to a request
*/
exports.debug = (msg, tag, traceId) => {
export function debug(msg, tag, traceId) {
if (process.env.NODE_ENV !== 'production') {
log({ arg: msg, tag, level: 'debug', traceId, color: ['bold', 'blue'] })
}
}
const logger = { setMaskables, setProcessName, info, warn, error, debug }
export default logger

View file

@ -1,15 +1,14 @@
const cors = require('cors')
const promBundle = require('express-prom-bundle')
import corsImport from 'cors'
import promBundle from 'express-prom-bundle'
// @ts-ignore
const { version } = require('../../package.json')
const tokenService = require('./helpers/jwt')
const logger = require('./logger')
const getS3Client = require('./s3-client')
const { getURLBuilder } = require('./helpers/utils')
const { isOAuthProvider } = require('./provider/Provider')
import packageJson from '../../package.json' with { type: 'json' }
import * as tokenService from './helpers/jwt.js'
import { getURLBuilder } from './helpers/utils.js'
import * as logger from './logger.js'
import { isOAuthProvider } from './provider/Provider.js'
import getS3Client from './s3-client.js'
exports.hasSessionAndProvider = (req, res, next) => {
export const hasSessionAndProvider = (req, res, next) => {
if (!req.session) {
logger.debug(
'No session attached to req object. Exiting dispatcher.',
@ -40,7 +39,7 @@ const isSimpleAuthProviderReq = (req) =>
* Middleware can be used to verify that the current request is to an OAuth provider
* This is because not all requests are supported by non-oauth providers (formerly known as SearchProviders)
*/
exports.hasOAuthProvider = (req, res, next) => {
export const hasOAuthProvider = (req, res, next) => {
if (!isOAuthProviderReq(req)) {
logger.debug('Provider does not support OAuth.', null, req.id)
return res.sendStatus(400)
@ -49,7 +48,7 @@ exports.hasOAuthProvider = (req, res, next) => {
return next()
}
exports.hasSimpleAuthProvider = (req, res, next) => {
export const hasSimpleAuthProvider = (req, res, next) => {
if (!isSimpleAuthProviderReq(req)) {
logger.debug('Provider does not support simple auth.', null, req.id)
return res.sendStatus(400)
@ -58,7 +57,7 @@ exports.hasSimpleAuthProvider = (req, res, next) => {
return next()
}
exports.hasBody = (req, res, next) => {
export const hasBody = (req, res, next) => {
if (!req.body) {
logger.debug(
'No body attached to req object. Exiting dispatcher.',
@ -71,7 +70,7 @@ exports.hasBody = (req, res, next) => {
return next()
}
exports.hasSearchQuery = (req, res, next) => {
export const hasSearchQuery = (req, res, next) => {
if (typeof req.query.q !== 'string') {
logger.debug(
'search request has no search query',
@ -84,7 +83,7 @@ exports.hasSearchQuery = (req, res, next) => {
return next()
}
exports.verifyToken = (req, res, next) => {
export const verifyToken = (req, res, next) => {
if (isOAuthProviderReq(req) || isSimpleAuthProviderReq(req)) {
// For OAuth / simple auth provider, we find the encrypted auth token from the header:
const token = req.companion.authToken
@ -133,7 +132,7 @@ exports.verifyToken = (req, res, next) => {
}
// does not fail if token is invalid
exports.gentleVerifyToken = (req, res, next) => {
export const gentleVerifyToken = (req, res, next) => {
const { providerName } = req.params
if (req.companion.authToken) {
try {
@ -150,13 +149,13 @@ exports.gentleVerifyToken = (req, res, next) => {
next()
}
exports.cookieAuthToken = (req, res, next) => {
export const cookieAuthToken = (req, res, next) => {
req.companion.authToken =
req.cookies[`uppyAuthToken--${req.companion.providerClass.oauthProvider}`]
return next()
}
exports.cors =
export const cors =
(options = {}) =>
(req, res, next) => {
// HTTP headers are not case sensitive, and express always handles them in lower case, so that's why we lower case them.
@ -207,7 +206,7 @@ exports.cors =
const { corsOrigins: origin = true } = options
// Because we need to merge with existing headers, we need to call cors inside our own middleware
return cors({
return corsImport({
credentials: true,
origin,
methods: Array.from(allowMethodsSet),
@ -216,7 +215,7 @@ exports.cors =
})(req, res, next)
}
exports.metrics = ({ path = undefined } = {}) => {
export const metrics = ({ path = undefined } = {}) => {
const metricsMiddleware = promBundle({
includeMethod: true,
metricsPath: path ? `${path}/metrics` : undefined,
@ -231,7 +230,7 @@ exports.metrics = ({ path = undefined } = {}) => {
name: 'companion_version',
help: 'npm version as an integer',
})
const numberVersion = Number(version.replace(/\D/g, ''))
const numberVersion = Number(packageJson.version.replace(/\D/g, ''))
versionGauge.set(numberVersion)
return metricsMiddleware
}
@ -240,7 +239,7 @@ exports.metrics = ({ path = undefined } = {}) => {
*
* @param {object} options
*/
exports.getCompanionMiddleware = (options) => {
export const getCompanionMiddleware = (options) => {
/**
* @param {object} req
* @param {object} res

View file

@ -1,9 +1,9 @@
const { MAX_AGE_24H } = require('../helpers/jwt')
import { MAX_AGE_24H } from '../helpers/jwt.js'
/**
* Provider interface defines the specifications of any provider implementation
*/
class Provider {
export default class Provider {
/**
*
* @param {{providerName: string, allowLocalUrls: boolean, providerGrantConfig?: object, secret: string}} options
@ -114,7 +114,6 @@ class Provider {
}
}
module.exports = Provider
// OAuth providers are those that have an `oauthProvider` set. It means they require OAuth authentication to work
module.exports.isOAuthProvider = (oauthProvider) =>
export const isOAuthProvider = (oauthProvider) =>
typeof oauthProvider === 'string' && oauthProvider.length > 0

View file

@ -1,5 +1,5 @@
const mime = require('mime-types')
const querystring = require('node:querystring')
import querystring from 'node:querystring'
import mime from 'mime-types'
const isFolder = (item) => {
return item.type === 'folder'
@ -52,7 +52,7 @@ const getNextPagePath = (data) => {
return `?${querystring.stringify(query)}`
}
module.exports = function adaptData(res, username, companion) {
const adaptData = function adaptData(res, username, companion) {
const data = { username, items: [] }
const items = getItemSubList(res)
items.forEach((item) => {
@ -73,3 +73,5 @@ module.exports = function adaptData(res, username, companion) {
return data
}
export default adaptData

View file

@ -1,15 +1,14 @@
const Provider = require('../Provider')
const adaptData = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream } = require('../../helpers/utils')
const got = require('../../got')
import got from 'got'
import { prepareStream } from '../../helpers/utils.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import adaptData from './adapter.js'
const BOX_FILES_FIELDS = 'id,modified_at,name,permissions,size,type'
const BOX_THUMBNAIL_SIZE = 256
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: 'https://api.box.com/2.0',
headers: {
authorization: `Bearer ${token}`,
@ -17,14 +16,12 @@ const getClient = async ({ token }) =>
})
async function getUserInfo({ token }) {
return (await getClient({ token }))
.get('users/me', { responseType: 'json' })
.json()
return getClient({ token }).get('users/me', { responseType: 'json' }).json()
}
async function list({ directory, query, token }) {
const rootFolderID = '0'
return (await getClient({ token }))
return getClient({ token })
.get(`folders/${directory || rootFolderID}/items`, {
searchParams: {
fields: BOX_FILES_FIELDS,
@ -39,7 +36,7 @@ async function list({ directory, query, token }) {
/**
* Adapter for API https://developer.box.com/reference/
*/
class Box extends Provider {
export default class Box extends Provider {
constructor(options) {
super(options)
// needed for the thumbnails fetched via companion
@ -77,10 +74,9 @@ class Box extends Provider {
async download({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.download.error', async () => {
const stream = (await getClient({ token })).stream.get(
`files/${id}/content`,
{ responseType: 'json' },
)
const stream = getClient({ token }).stream.get(`files/${id}/content`, {
responseType: 'json',
})
const { size } = await prepareStream(stream)
return { stream, size }
@ -100,7 +96,7 @@ class Box extends Provider {
// At that time, retry this endpoint to retrieve the thumbnail.
//
// This can be reproduced more easily by changing extension to png and trying on a newly uploaded image
const stream = (await getClient({ token })).stream.get(
const stream = getClient({ token }).stream.get(
`files/${id}/thumbnail.${extension}`,
{
searchParams: {
@ -118,7 +114,7 @@ class Box extends Provider {
async size({ id, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.box.size.error', async () => {
const { size } = await (await getClient({ token }))
const { size } = await getClient({ token })
.get(`files/${id}`, { responseType: 'json' })
.json()
return parseInt(size, 10)
@ -128,7 +124,7 @@ class Box extends Provider {
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', {
await getClient({ token }).post('oauth2/revoke', {
prefixUrl: 'https://api.box.com',
form: {
client_id: key,
@ -152,5 +148,3 @@ class Box extends Provider {
})
}
}
module.exports = Box

View file

@ -1,12 +1,10 @@
const { htmlEscape } = require('escape-goat')
const logger = require('../logger')
const oAuthState = require('../helpers/oauth-state')
const tokenService = require('../helpers/jwt')
const { getURLBuilder, getRedirectPath } = require('../helpers/utils')
// biome-ignore lint/correctness/noUnusedVariables: used in types
const Provider = require('./Provider')
const got = require('../got')
import { htmlEscape } from 'escape-goat'
import got from 'got'
import * as tokenService from '../helpers/jwt.js'
import * as oAuthState from '../helpers/oauth-state.js'
import { getRedirectPath, getURLBuilder } from '../helpers/utils.js'
import logger from '../logger.js'
import Provider from './Provider.js'
/**
* @param {string} url
@ -15,7 +13,7 @@ const got = require('../got')
*/
async function fetchKeys(url, providerName, credentialRequestParams) {
try {
const { credentials } = await (await got)
const { credentials } = await got
.post(url, {
json: { provider: providerName, parameters: credentialRequestParams },
})
@ -79,7 +77,10 @@ async function fetchProviderKeys(
* @param {object} companionOptions companion options object
* @returns {import('express').RequestHandler}
*/
exports.getCredentialsOverrideMiddleware = (providers, companionOptions) => {
export const getCredentialsOverrideMiddleware = (
providers,
companionOptions,
) => {
return async (req, res, next) => {
try {
const { oauthProvider, override } = req.params
@ -215,11 +216,7 @@ exports.getCredentialsOverrideMiddleware = (providers, companionOptions) => {
* @param {object} req the express request object for the said request
* @returns {(providerName: string, companionOptions: object, credentialRequestParams?: object) => Promise}
*/
module.exports.getCredentialsResolver = (
providerName,
companionOptions,
req,
) => {
export const getCredentialsResolver = (providerName, companionOptions, req) => {
const credentialsResolver = () => {
const encodedCredentialsParams = req.header('uppy-credentials-params')
let credentialRequestParams = null

View file

@ -1,5 +1,5 @@
const mime = require('mime-types')
const querystring = require('node:querystring')
import querystring from 'node:querystring'
import mime from 'mime-types'
const isFolder = (item) => {
return item['.tag'] === 'folder'
@ -49,7 +49,7 @@ const getNextPagePath = (data) => {
return `?${querystring.stringify(query)}`
}
module.exports = (res, email, buildURL) => {
const adaptData = (res, email, buildURL) => {
const items = getItemSubList(res).map((item) => ({
isFolder: isFolder(item),
icon: getItemIcon(item),
@ -69,3 +69,5 @@ module.exports = (res, email, buildURL) => {
nextPagePath: getNextPagePath(res),
}
}
export default adaptData

View file

@ -1,16 +1,15 @@
const Provider = require('../Provider')
const adaptData = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream } = require('../../helpers/utils')
const { MAX_AGE_REFRESH_TOKEN } = require('../../helpers/jwt')
const logger = require('../../logger')
const gotPromise = require('../../got')
// From https://www.dropbox.com/developers/reference/json-encoding:
//
// This function is simple and has OK performance compared to more
// complicated ones: http://jsperf.com/json-escape-unicode/4
import got from 'got'
import { MAX_AGE_REFRESH_TOKEN } from '../../helpers/jwt.js'
import { prepareStream } from '../../helpers/utils.js'
import logger from '../../logger.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import adaptData from './adapter.js'
const charsToEncode = /[\u007f-\uffff]/g
function httpHeaderSafeJson(v) {
return JSON.stringify(v).replace(charsToEncode, (c) => {
@ -25,8 +24,6 @@ async function getUserInfo({ client }) {
}
async function getClient({ token, namespaced }) {
const got = await gotPromise
const makeClient = (namespace) =>
got.extend({
prefixUrl: 'https://api.dropboxapi.com/2',
@ -70,8 +67,8 @@ async function getClient({ token, namespaced }) {
}
}
const getOauthClient = async () =>
(await gotPromise).extend({
const getOauthClient = () =>
got.extend({
prefixUrl: 'https://api.dropboxapi.com/oauth2',
})
@ -102,7 +99,7 @@ async function list({ client, directory, query }) {
/**
* Adapter for API https://www.dropbox.com/developers/documentation/http/documentation
*/
class DropBox extends Provider {
export default class Dropbox extends Provider {
constructor(options) {
super(options)
this.needsCookieAuth = true
@ -211,7 +208,7 @@ class DropBox extends Provider {
return this.#withErrorHandling(
'provider.dropbox.token.refresh.error',
async () => {
const { access_token: accessToken } = await (await getOauthClient())
const { access_token: accessToken } = await getOauthClient()
.post('token', {
form: {
refresh_token: refreshToken,
@ -230,11 +227,9 @@ class DropBox extends Provider {
return withProviderErrorHandling({
fn,
tag,
providerName: DropBox.oauthProvider,
providerName: Dropbox.oauthProvider,
isAuthError: (response) => response.statusCode === 401,
getJsonErrorMessage: (body) => body?.error_summary,
})
}
}
module.exports = DropBox

View file

@ -2,7 +2,7 @@
* ProviderApiError is error returned when an adapter encounters
* an http error while communication with its corresponding provider
*/
class ProviderApiError extends Error {
export class ProviderApiError extends Error {
/**
* @param {string} message error message
* @param {number} statusCode the http status code from the provider api
@ -15,7 +15,7 @@ class ProviderApiError extends Error {
}
}
class ProviderUserError extends ProviderApiError {
export class ProviderUserError extends ProviderApiError {
/**
* @param {object} json arbitrary JSON.stringify-able object that will be passed to the client
*/
@ -32,7 +32,7 @@ class ProviderUserError extends ProviderApiError {
* this signals to the client that the access token is invalid and needs to be
* refreshed or the user needs to re-authenticate
*/
class ProviderAuthError extends ProviderApiError {
export class ProviderAuthError extends ProviderApiError {
constructor() {
super('invalid access token detected by Provider', 401)
this.name = 'AuthError'
@ -40,7 +40,7 @@ class ProviderAuthError extends ProviderApiError {
}
}
function parseHttpError(err) {
export function parseHttpError(err) {
if (err?.name === 'HTTPError') {
return {
statusCode: err.response?.statusCode,
@ -108,7 +108,7 @@ function errorToResponse(err) {
return undefined
}
function respondWithError(err, res) {
export function respondWithError(err, res) {
const errResp = errorToResponse(err)
if (errResp) {
res.status(errResp.code).json(errResp.json)
@ -116,11 +116,3 @@ function respondWithError(err, res) {
}
return false
}
module.exports = {
ProviderAuthError,
ProviderApiError,
ProviderUserError,
respondWithError,
parseHttpError,
}

View file

@ -1,10 +1,10 @@
const querystring = require('node:querystring')
import querystring from 'node:querystring'
const isFolder = (item) => {
return !!item.type
}
exports.sortImages = (images) => {
export const sortImages = (images) => {
// sort in ascending order of dimension
return images.slice().sort((a, b) => a.width - b.width)
}
@ -13,7 +13,7 @@ const getItemIcon = (item) => {
if (isFolder(item)) {
return 'folder'
}
return exports.sortImages(item.images)[0].source
return sortImages(item.images)[0].source
}
const getItemSubList = (item) => {
@ -41,7 +41,7 @@ const getItemModifiedDate = (item) => {
}
const getItemThumbnailUrl = (item) => {
return isFolder(item) ? null : exports.sortImages(item.images)[0].source
return isFolder(item) ? null : sortImages(item.images)[0].source
}
const getNextPagePath = (data, currentQuery, currentPath) => {
@ -53,7 +53,7 @@ const getNextPagePath = (data, currentQuery, currentPath) => {
return `${currentPath || ''}?${querystring.stringify(query)}`
}
exports.adaptData = (res, username, directory, currentQuery) => {
export const adaptData = (res, username, directory, currentQuery) => {
const data = { username, items: [] }
const items = getItemSubList(res)
items.forEach((item) => {

View file

@ -1,13 +1,10 @@
const crypto = require('node:crypto')
const Provider = require('../Provider')
const logger = require('../../logger')
const { adaptData, sortImages } = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream } = require('../../helpers/utils')
const { HttpError } = require('../../helpers/utils')
const got = require('../../got')
import crypto from 'node:crypto'
import got from 'got'
import { HttpError, prepareStream } from '../../helpers/utils.js'
import logger from '../../logger.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import { adaptData, sortImages } from './adapter.js'
async function runRequestBatch({ secret, token, requests }) {
// https://developers.facebook.com/docs/facebook-login/security/#appsecret
@ -26,7 +23,7 @@ async function runRequestBatch({ secret, token, requests }) {
batch: JSON.stringify(requests),
}
const responsesRaw = await (await got)
const responsesRaw = await got
.post('https://graph.facebook.com', { form })
.json()
@ -65,7 +62,7 @@ async function getMediaUrl({ secret, token, id }) {
/**
* Adapter for API https://developers.facebook.com/docs/graph-api/using-graph-api/
*/
class Facebook extends Provider {
export default class Facebook extends Provider {
static get oauthProvider() {
return 'facebook'
}
@ -109,7 +106,7 @@ class Facebook extends Provider {
'provider.facebook.download.error',
async () => {
const url = await getMediaUrl({ secret: this.secret, token, id })
const stream = (await got).stream.get(url, { responseType: 'json' })
const stream = got.stream.get(url, { responseType: 'json' })
const { size } = await prepareStream(stream)
return { stream, size }
},
@ -151,5 +148,3 @@ class Facebook extends Provider {
})
}
}
module.exports = Facebook

View file

@ -1,10 +1,10 @@
const querystring = require('node:querystring')
import querystring from 'node:querystring'
const getUsername = (data) => {
return data.user.emailAddress
}
exports.isGsuiteFile = (mimeType) => {
export const isGsuiteFile = (mimeType) => {
return mimeType?.startsWith('application/vnd.google')
}
@ -19,7 +19,7 @@ const isFolder = (item) => {
)
}
exports.isShortcut = (mimeType) => {
export const isShortcut = (mimeType) => {
return mimeType === 'application/vnd.google-apps.shortcut'
}
@ -60,7 +60,7 @@ const getItemSubList = (item) => {
return item.files.filter((i) => {
return (
isFolder(i) ||
!exports.isGsuiteFile(i.mimeType) ||
!isGsuiteFile(i.mimeType) ||
allowedGSuiteTypes.includes(i.mimeType)
)
})
@ -83,7 +83,7 @@ const getItemName = (item) => {
return item.name ? item.name : '/'
}
exports.getGsuiteExportType = (mimeType) => {
export const getGsuiteExportType = (mimeType) => {
const typeMaps = {
'application/vnd.google-apps.document':
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
@ -100,14 +100,14 @@ exports.getGsuiteExportType = (mimeType) => {
}
function getMimeType2(mimeType) {
if (exports.isGsuiteFile(mimeType)) {
return exports.getGsuiteExportType(mimeType)
if (isGsuiteFile(mimeType)) {
return getGsuiteExportType(mimeType)
}
return mimeType
}
const getMimeType = (item) => {
if (exports.isShortcut(item.mimeType)) {
if (isShortcut(item.mimeType)) {
return getMimeType2(item.shortcutDetails.targetMimeType)
}
return getMimeType2(item.mimeType)
@ -152,9 +152,9 @@ const getVideoWidth = (item) => item.videoMediaMetadata?.width
const getVideoDurationMillis = (item) => item.videoMediaMetadata?.durationMillis
// Hopefully this name will not be used by Google
exports.VIRTUAL_SHARED_DIR = 'shared-with-me'
export const VIRTUAL_SHARED_DIR = 'shared-with-me'
exports.adaptData = (
export const adaptData = (
listFilesResp,
sharedDrivesResp,
directory,
@ -194,8 +194,8 @@ exports.adaptData = (
icon: 'folder',
name: 'Shared with me',
mimeType: 'application/vnd.google-apps.folder',
id: exports.VIRTUAL_SHARED_DIR,
requestPath: exports.VIRTUAL_SHARED_DIR,
id: VIRTUAL_SHARED_DIR,
requestPath: VIRTUAL_SHARED_DIR,
}
const adaptedItems = [

View file

@ -1,19 +1,18 @@
const got = require('../../../got')
const { logout, refreshToken } = require('../index')
const logger = require('../../../logger')
const {
VIRTUAL_SHARED_DIR,
import got from 'got'
import { MAX_AGE_REFRESH_TOKEN } from '../../../helpers/jwt.js'
import { prepareStream } from '../../../helpers/utils.js'
import logger from '../../../logger.js'
import { ProviderAuthError } from '../../error.js'
import Provider from '../../Provider.js'
import { withGoogleErrorHandling } from '../../providerErrors.js'
import { logout, refreshToken } from '../index.js'
import {
adaptData,
isShortcut,
isGsuiteFile,
getGsuiteExportType,
} = require('./adapter')
const { prepareStream } = require('../../../helpers/utils')
const { MAX_AGE_REFRESH_TOKEN } = require('../../../helpers/jwt')
const { ProviderAuthError } = require('../../error')
const { withGoogleErrorHandling } = require('../../providerErrors')
const Provider = require('../../Provider')
isGsuiteFile,
isShortcut,
VIRTUAL_SHARED_DIR,
} from './adapter.js'
// For testing refresh token:
// first run a download with mockAccessTokenExpiredError = true
@ -29,8 +28,8 @@ const DRIVE_FILES_FIELDS = `kind,nextPageToken,incompleteSearch,files(${DRIVE_FI
// using wildcard to get all 'drive' fields because specifying fields seems no to work for the /drives endpoint
const SHARED_DRIVE_FIELDS = '*'
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: 'https://www.googleapis.com/drive/v3',
headers: {
authorization: `Bearer ${token}`,
@ -38,7 +37,7 @@ const getClient = async ({ token }) =>
})
async function getStats({ id, token }) {
const client = await getClient({ token })
const client = getClient({ token })
const getStatsInner = async (statsOfId) =>
client
@ -56,8 +55,8 @@ async function getStats({ id, token }) {
return stats
}
async function streamGoogleFile({ token, id: idIn }) {
const client = await getClient({ token })
export async function streamGoogleFile({ token, id: idIn }) {
const client = getClient({ token })
const { mimeType, id, exportLinks } = await getStats({ id: idIn, token })
@ -76,12 +75,10 @@ async function streamGoogleFile({ token, id: idIn }) {
// Implemented based on the answer from StackOverflow: https://stackoverflow.com/a/59168288
const mimeTypeExportLink = exportLinks?.[mimeType2]
if (mimeTypeExportLink) {
const gSuiteFilesClient = (await got).extend({
stream = got.stream.get(mimeTypeExportLink, {
headers: {
authorization: `Bearer ${token}`,
},
})
stream = gSuiteFilesClient.stream.get(mimeTypeExportLink, {
responseType: 'json',
})
} else {
@ -104,7 +101,7 @@ async function streamGoogleFile({ token, id: idIn }) {
/**
* Adapter for API https://developers.google.com/drive/api/v3/
*/
class Drive extends Provider {
export class Drive extends Provider {
static get oauthProvider() {
return 'googledrive'
}
@ -127,7 +124,7 @@ class Drive extends Provider {
const isRoot = directory === 'root'
const isVirtualSharedDirRoot = directory === VIRTUAL_SHARED_DIR
const client = await getClient({ token })
const client = getClient({ token })
async function fetchSharedDrives(pageToken = null) {
const shouldListSharedDrives = isRoot && !query.cursor
@ -228,8 +225,3 @@ class Drive extends Provider {
Drive.prototype.logout = logout
Drive.prototype.refreshToken = refreshToken
module.exports = {
Drive,
streamGoogleFile,
}

View file

@ -1,17 +1,16 @@
const got = require('../../got')
const { withGoogleErrorHandling } = require('../providerErrors')
import got from 'got'
import { withGoogleErrorHandling } from '../providerErrors.js'
/**
* Reusable google stuff
*/
const getOauthClient = async () =>
(await got).extend({
const getOauthClient = () =>
got.extend({
prefixUrl: 'https://oauth2.googleapis.com',
})
async function refreshToken({
export async function refreshToken({
clientId,
clientSecret,
refreshToken: theRefreshToken,
@ -20,7 +19,7 @@ async function refreshToken({
'google',
'provider.google.token.refresh.error',
async () => {
const { access_token: accessToken } = await (await getOauthClient())
const { access_token: accessToken } = await getOauthClient()
.post('token', {
responseType: 'json',
form: {
@ -36,12 +35,12 @@ async function refreshToken({
)
}
async function logout({ providerUserSession: { accessToken: token } }) {
export async function logout({ providerUserSession: { accessToken: token } }) {
return withGoogleErrorHandling(
'google',
'provider.google.logout.error',
async () => {
await (await got).post('https://accounts.google.com/o/oauth2/revoke', {
await got.post('https://accounts.google.com/o/oauth2/revoke', {
searchParams: { token },
responseType: 'json',
})
@ -50,8 +49,3 @@ async function logout({ providerUserSession: { accessToken: token } }) {
},
)
}
module.exports = {
refreshToken,
logout,
}

View file

@ -1,21 +1,20 @@
/**
* @module provider
*/
const dropbox = require('./dropbox')
const box = require('./box')
const { Drive } = require('./google/drive')
const instagram = require('./instagram/graph')
const facebook = require('./facebook')
const onedrive = require('./onedrive')
const unsplash = require('./unsplash')
const webdav = require('./webdav')
const zoom = require('./zoom')
const { getURLBuilder, getRedirectPath } = require('../helpers/utils')
const logger = require('../logger')
const { getCredentialsResolver } = require('./credentials')
const Provider = require('./Provider')
const { isOAuthProvider } = Provider
import { getRedirectPath, getURLBuilder } from '../helpers/utils.js'
import * as logger from '../logger.js'
import box from './box/index.js'
import { getCredentialsResolver } from './credentials.js'
import dropbox from './dropbox/index.js'
import facebook from './facebook/index.js'
import { Drive } from './google/drive/index.js'
import instagram from './instagram/graph/index.js'
import onedrive from './onedrive/index.js'
import Provider, { isOAuthProvider } from './Provider.js'
import unsplash from './unsplash/index.js'
import webdav from './webdav/index.js'
import zoom from './zoom/index.js'
/**
*
@ -31,7 +30,7 @@ const validOptions = (options) => {
*
* @param {Record<string, typeof Provider>} providers
*/
module.exports.getProviderMiddleware = (providers, grantConfig) => {
export function getProviderMiddleware(providers, grantConfig) {
/**
*
* @param {object} req
@ -81,7 +80,7 @@ module.exports.getProviderMiddleware = (providers, grantConfig) => {
/**
* @returns {Record<string, typeof Provider>}
*/
module.exports.getDefaultProviders = () => {
export function getDefaultProviders() {
const providers = {
dropbox,
box,
@ -105,11 +104,7 @@ module.exports.getDefaultProviders = () => {
* @param {Record<string, typeof Provider>} providers
* @param {object} grantConfig
*/
module.exports.addCustomProviders = (
customProviders,
providers,
grantConfig,
) => {
export function addCustomProviders(customProviders, providers, grantConfig) {
Object.keys(customProviders).forEach((providerName) => {
const customProvider = customProviders[providerName]
@ -135,11 +130,11 @@ module.exports.addCustomProviders = (
* @param {object} grantConfig
* @param {(a: string) => string} getOauthProvider
*/
module.exports.addProviderOptions = (
export function addProviderOptions(
companionOptions,
grantConfig,
getOauthProvider,
) => {
) {
const { server, providerOptions } = companionOptions
if (!validOptions({ server })) {
logger.warn(
@ -173,7 +168,7 @@ module.exports.addProviderOptions = (
]
}
const provider = exports.getDefaultProviders()[providerName]
const provider = getDefaultProviders()[providerName]
Object.assign(grantConfig[oauthProvider], provider.getExtraGrantConfig())
// override grant.js redirect uri with companion's custom redirect url

View file

@ -1,4 +1,4 @@
const querystring = require('node:querystring')
import querystring from 'node:querystring'
const MEDIA_TYPES = Object.freeze({
video: 'VIDEO',
@ -55,7 +55,7 @@ const getNextPagePath = (data, currentQuery, currentPath) => {
return `${currentPath || ''}?${querystring.stringify(query)}`
}
module.exports = (res, username, directory, currentQuery) => {
const adaptData = (res, username, directory, currentQuery) => {
const data = { username, items: [] }
const items = getItemSubList(res)
items.forEach((item, i) => {
@ -75,3 +75,5 @@ module.exports = (res, username, directory, currentQuery) => {
data.nextPagePath = getNextPagePath(res, currentQuery, directory)
return data
}
export default adaptData

View file

@ -1,13 +1,12 @@
const Provider = require('../../Provider')
const logger = require('../../../logger')
const adaptData = require('./adapter')
const { withProviderErrorHandling } = require('../../providerErrors')
const { prepareStream } = require('../../../helpers/utils')
import got from 'got'
import { prepareStream } from '../../../helpers/utils.js'
import logger from '../../../logger.js'
import Provider from '../../Provider.js'
import { withProviderErrorHandling } from '../../providerErrors.js'
import adaptData from './adapter.js'
const got = require('../../../got')
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: 'https://graph.instagram.com',
headers: {
authorization: `Bearer ${token}`,
@ -15,7 +14,7 @@ const getClient = async ({ token }) =>
})
async function getMediaUrl({ token, id }) {
const body = await (await getClient({ token }))
const body = await getClient({ token })
.get(String(id), {
searchParams: { fields: 'media_url' },
responseType: 'json',
@ -27,7 +26,7 @@ async function getMediaUrl({ token, id }) {
/**
* Adapter for API https://developers.facebook.com/docs/instagram-api/overview
*/
class Instagram extends Provider {
export default class Instagram extends Provider {
// for "grant"
static getExtraGrantConfig() {
return {
@ -55,7 +54,7 @@ class Instagram extends Provider {
if (query.cursor) qs.after = query.cursor
const client = await getClient({ token })
const client = getClient({ token })
const [{ username }, list] = await Promise.all([
client
@ -78,7 +77,7 @@ class Instagram extends Provider {
'provider.instagram.download.error',
async () => {
const url = await getMediaUrl({ token, id })
const stream = (await got).stream.get(url, { responseType: 'json' })
const stream = got.stream.get(url, { responseType: 'json' })
const { size } = await prepareStream(stream)
return { stream, size }
},
@ -113,5 +112,3 @@ class Instagram extends Provider {
})
}
}
module.exports = Instagram

View file

@ -61,7 +61,7 @@ const getNextPagePath = ({ res, query: currentQuery, directory }) => {
return `${directory ?? ''}?${new URLSearchParams(query).toString()}`
}
module.exports = (res, username, query, directory) => {
const adaptData = (res, username, query, directory) => {
const data = { username, items: [] }
const items = getItemSubList(res)
items.forEach((item) => {
@ -82,3 +82,5 @@ module.exports = (res, username, query, directory) => {
return data
}
export default adaptData

View file

@ -1,21 +1,20 @@
const Provider = require('../Provider')
const logger = require('../../logger')
const adaptData = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream } = require('../../helpers/utils')
import got from 'got'
import { prepareStream } from '../../helpers/utils.js'
import logger from '../../logger.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import adaptData from './adapter.js'
const got = require('../../got')
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: 'https://graph.microsoft.com/v1.0',
headers: {
authorization: `Bearer ${token}`,
},
})
const getOauthClient = async () =>
(await got).extend({
const getOauthClient = () =>
got.extend({
prefixUrl: 'https://login.live.com',
})
@ -25,7 +24,7 @@ const getRootPath = (query) =>
/**
* Adapter for API https://docs.microsoft.com/en-us/onedrive/developer/rest-api/
*/
class OneDrive extends Provider {
export default class OneDrive extends Provider {
static get oauthProvider() {
return 'microsoft'
}
@ -54,7 +53,7 @@ class OneDrive extends Provider {
qs.$skiptoken = query.cursor
}
const client = await getClient({ token })
const client = getClient({ token })
const [{ mail, userPrincipalName }, list] = await Promise.all([
client.get('me', { responseType: 'json' }).json(),
@ -74,7 +73,7 @@ class OneDrive extends Provider {
return this.#withErrorHandling(
'provider.onedrive.download.error',
async () => {
const stream = (await getClient({ token })).stream.get(
const stream = getClient({ token }).stream.get(
`${getRootPath(query)}/items/${id}/content`,
{ responseType: 'json' },
)
@ -95,7 +94,7 @@ class OneDrive extends Provider {
async size({ id, query, providerUserSession: { accessToken: token } }) {
return this.#withErrorHandling('provider.onedrive.size.error', async () => {
const { size } = await (await getClient({ token }))
const { size } = await getClient({ token })
.get(`${getRootPath(query)}/items/${id}`, { responseType: 'json' })
.json()
return size
@ -114,7 +113,7 @@ class OneDrive extends Provider {
return this.#withErrorHandling(
'provider.onedrive.token.refresh.error',
async () => {
const { access_token: accessToken } = await (await getOauthClient())
const { access_token: accessToken } = await getOauthClient()
.post('oauth20_token.srf', {
responseType: 'json',
form: {
@ -147,5 +146,3 @@ class OneDrive extends Provider {
})
}
}
module.exports = OneDrive

View file

@ -1,10 +1,12 @@
const logger = require('../logger')
const {
import * as logger from '../logger.js'
import {
ProviderApiError,
ProviderUserError,
ProviderAuthError,
ProviderUserError,
parseHttpError,
} = require('./error')
} from './error.js'
export { parseHttpError }
/**
*
@ -18,7 +20,7 @@ const {
* }} param0
* @returns
*/
async function withProviderErrorHandling({
export async function withProviderErrorHandling({
fn,
tag,
providerName,
@ -71,7 +73,7 @@ async function withProviderErrorHandling({
}
}
async function withGoogleErrorHandling(providerName, tag, fn) {
export async function withGoogleErrorHandling(providerName, tag, fn) {
return withProviderErrorHandling({
fn,
tag,
@ -82,9 +84,3 @@ async function withGoogleErrorHandling(providerName, tag, fn) {
getJsonErrorMessage: (body) => body?.error?.message,
})
}
module.exports = {
withProviderErrorHandling,
withGoogleErrorHandling,
parseHttpError,
}

View file

@ -1,4 +1,4 @@
const querystring = require('node:querystring')
import querystring from 'node:querystring'
const isFolder = (item) => {
return false
@ -55,7 +55,7 @@ const getAuthor = (item) => {
return { name: item.user.name, url: item.user.links.html }
}
module.exports = (body, currentQuery) => {
const adaptData = (body, currentQuery) => {
const { total_pages: pagesCount } = body
const { cursor, q } = currentQuery
const currentPage = Number(cursor || 1)
@ -80,3 +80,5 @@ module.exports = (body, currentQuery) => {
nextPageQuery: hasNextPage ? getNextPageQuery(currentQuery) : null,
}
}
export default adaptData

View file

@ -1,15 +1,14 @@
const Provider = require('../Provider')
const adaptData = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream } = require('../../helpers/utils')
const { ProviderApiError } = require('../error')
const got = require('../../got')
import got from 'got'
import { prepareStream } from '../../helpers/utils.js'
import { ProviderApiError } from '../error.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import adaptData from './adapter.js'
const BASE_URL = 'https://api.unsplash.com'
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: BASE_URL,
headers: {
authorization: `Client-ID ${token}`,
@ -22,7 +21,7 @@ const getPhotoMeta = async (client, id) =>
/**
* Adapter for API https://api.unsplash.com
*/
class Unsplash extends Provider {
export default class Unsplash extends Provider {
async list({
providerUserSession: { accessToken: token },
query = { cursor: null, q: null },
@ -35,7 +34,7 @@ class Unsplash extends Provider {
const qs = { per_page: 40, query: query.q }
if (query.cursor) qs.page = query.cursor
const response = await (await getClient({ token }))
const response = await getClient({ token })
.get('search/photos', { searchParams: qs, responseType: 'json' })
.json()
return adaptData(response, query)
@ -46,13 +45,13 @@ class Unsplash extends Provider {
return this.#withErrorHandling(
'provider.unsplash.download.error',
async () => {
const client = await getClient({ token })
const client = getClient({ token })
const {
links: { download: url, download_location: attributionUrl },
} = await getPhotoMeta(client, id)
const stream = (await got).stream.get(url, { responseType: 'json' })
const stream = got.stream.get(url, { responseType: 'json' })
const { size } = await prepareStream(stream)
// To attribute the author of the image, we call the `download_location`
@ -79,5 +78,3 @@ class Unsplash extends Provider {
})
}
}
module.exports = Unsplash

View file

@ -1,15 +1,19 @@
const Provider = require('../Provider')
const { getProtectedHttpAgent, validateURL } = require('../../helpers/request')
const { ProviderApiError, ProviderAuthError } = require('../error')
const { ProviderUserError } = require('../error')
const logger = require('../../logger')
import { AuthType, createClient } from 'webdav'
import { getProtectedHttpAgent, validateURL } from '../../helpers/request.js'
import logger from '../../logger.js'
import {
ProviderApiError,
ProviderAuthError,
ProviderUserError,
} from '../error.js'
import Provider from '../Provider.js'
const defaultDirectory = '/'
/**
* Adapter for WebDAV servers that support simple auth (non-OAuth).
*/
class WebdavProvider extends Provider {
export default class WebdavProvider extends Provider {
static get hasSimpleAuth() {
return true
}
@ -25,11 +29,6 @@ class WebdavProvider extends Provider {
throw new Error('invalid public link url')
}
// dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM
// todo implement as regular require as soon as Node 20.17 or 22 is required
// or as regular import when Companion is ported to ESM
const { AuthType } = await import('webdav')
// Is this an ownCloud or Nextcloud public link URL? e.g. https://example.com/s/kFy9Lek5sm928xP
// they have specific urls that we can identify
// todo not sure if this is the right way to support nextcloud and other webdavs
@ -84,10 +83,6 @@ class WebdavProvider extends Provider {
allowLocalIPs: !allowLocalUrls,
})
// dynamic import because Companion currently uses CommonJS and webdav is shipped as ESM
// todo implement as regular require as soon as Node 20.17 or 22 is required
// or as regular import when Companion is ported to ESM
const { createClient } = await import('webdav')
return createClient(url, {
...options,
[`${protocol}Agent`]: new HttpAgentClass(),
@ -174,5 +169,3 @@ class WebdavProvider extends Provider {
}
}
}
module.exports = WebdavProvider

View file

@ -1,4 +1,4 @@
const moment = require('moment-timezone')
import moment from 'moment-timezone'
const MIMETYPES = {
MP4: 'video/mp4',
@ -109,7 +109,7 @@ const getItemTopic = (item) => {
return item.topic
}
exports.adaptData = (userResponse, results) => {
const adaptData = (userResponse, results) => {
if (!results) {
return { items: [] }
}
@ -162,3 +162,5 @@ exports.adaptData = (userResponse, results) => {
})
return data
}
export default adaptData

View file

@ -1,20 +1,17 @@
const moment = require('moment-timezone')
const Provider = require('../Provider')
const { adaptData } = require('./adapter')
const { withProviderErrorHandling } = require('../providerErrors')
const { prepareStream, getBasicAuthHeader } = require('../../helpers/utils')
const got = require('../../got')
const pMap = import('p-map')
import got from 'got'
import moment from 'moment-timezone'
import pMap from 'p-map'
import { getBasicAuthHeader, prepareStream } from '../../helpers/utils.js'
import Provider from '../Provider.js'
import { withProviderErrorHandling } from '../providerErrors.js'
import adaptData from './adapter.js'
const BASE_URL = 'https://zoom.us/v2'
const PAGE_SIZE = 300
const DEAUTH_EVENT_NAME = 'app_deauthorized'
const getClient = async ({ token }) =>
(await got).extend({
const getClient = ({ token }) =>
got.extend({
prefixUrl: BASE_URL,
headers: {
authorization: `Bearer ${token}`,
@ -38,7 +35,7 @@ async function findFile({ client, meetingId, fileId, recordingStart }) {
/**
* Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
*/
class Zoom extends Provider {
export default class Zoom extends Provider {
static get oauthProvider() {
return 'zoom'
}
@ -52,7 +49,7 @@ class Zoom extends Provider {
const meetingId = options.directory || ''
const requestedYear = query.year ? parseInt(query.year, 10) : null
const client = await getClient({ token })
const client = getClient({ token })
const user = await client.get('users/me', { responseType: 'json' }).json()
const { timezone } = user
const userTz = timezone || 'UTC'
@ -74,9 +71,7 @@ class Zoom extends Provider {
// Run each month in parallel:
const allMeetingsInYear = (
await (
await pMap
).default(
await pMap(
monthsToCheck,
async (month) => {
const startDate = moment
@ -153,7 +148,7 @@ class Zoom extends Provider {
// cc files don't have an ID or size
const { recordingStart, recordingId: fileId } = query
const client = await getClient({ token })
const client = getClient({ token })
const foundFile = await findFile({
client,
@ -179,7 +174,7 @@ class Zoom extends Provider {
query,
}) {
return this.#withErrorHandling('provider.zoom.size.error', async () => {
const client = await getClient({ token })
const client = getClient({ token })
const { recordingStart, recordingId: fileId } = query
const foundFile = await findFile({
@ -197,7 +192,7 @@ class Zoom extends Provider {
return this.#withErrorHandling('provider.zoom.logout.error', async () => {
const { key, secret } = await companion.getProviderCredentials()
const { status } = await (await got)
const { status } = await got
.post('https://zoom.us/oauth/revoke', {
searchParams: { token },
headers: { Authorization: getBasicAuthHeader(key, secret) },
@ -223,7 +218,7 @@ class Zoom extends Provider {
return { data: {}, status: 400 }
}
await (await got).post('https://api.zoom.us/oauth/data/compliance', {
await got.post('https://api.zoom.us/oauth/data/compliance', {
headers: { Authorization: getBasicAuthHeader(key, secret) },
json: {
client_id: key,
@ -254,5 +249,3 @@ class Zoom extends Provider {
})
}
}
module.exports = Zoom

View file

@ -1,6 +1,5 @@
const Redis = require('ioredis').default
const logger = require('./logger')
import { Redis } from 'ioredis'
import * as logger from './logger.js'
/** @type {import('ioredis').Redis} */
let redisClient
@ -27,9 +26,9 @@ function createClient(redisUrl, redisOptions) {
return redisClient
}
module.exports.client = (
export function client(
{ redisUrl, redisOptions } = { redisUrl: undefined, redisOptions: undefined },
) => {
) {
if (!redisUrl && !redisOptions) {
return redisClient
}

View file

@ -1,4 +1,4 @@
const { S3Client } = require('@aws-sdk/client-s3')
import { S3Client } from '@aws-sdk/client-s3'
/**
* instantiates the aws-sdk s3 client that will be used for s3 uploads.
@ -6,7 +6,10 @@ const { S3Client } = require('@aws-sdk/client-s3')
* @param {object} companionOptions the companion options object
* @param {boolean} createPresignedPostMode whether this s3 client is for createPresignedPost
*/
module.exports = (companionOptions, createPresignedPostMode = false) => {
export default function s3Client(
companionOptions,
createPresignedPostMode = false,
) {
let s3Client = null
if (companionOptions.s3) {
const { s3 } = companionOptions

View file

@ -1,17 +1,17 @@
const SocketServer = require('ws').WebSocketServer
const { jsonStringify } = require('./helpers/utils')
const emitter = require('./emitter')
const redis = require('./redis')
const logger = require('./logger')
const { STORAGE_PREFIX, shortenToken } = require('./Uploader')
import { WebSocketServer } from 'ws'
import emitter from './emitter/index.js'
import { jsonStringify } from './helpers/utils.js'
import * as logger from './logger.js'
import * as redis from './redis.js'
import Uploader from './Uploader.js'
/**
* the socket is used to send progress events during an upload
*
* @param {import('http').Server | import('https').Server} server
*/
module.exports = (server) => {
const wss = new SocketServer({ server })
export default function setupSocket(server) {
const wss = new WebSocketServer({ server })
const redisClient = redis.client()
// A new connection is usually created when an upload begins,
@ -30,7 +30,8 @@ module.exports = (server) => {
*/
function send(data) {
ws.send(jsonStringify(data), (err) => {
if (err) logger.error(err, 'socket.redis.error', shortenToken(token))
if (err)
logger.error(err, 'socket.redis.error', Uploader.shortenToken(token))
})
}
@ -38,7 +39,7 @@ module.exports = (server) => {
// if we have any already stored state on the upload.
if (redisClient) {
redisClient
.get(`${STORAGE_PREFIX}:${token}`)
.get(`${Uploader.STORAGE_PREFIX}:${token}`)
.then((data) => {
if (data) {
const dataObj = JSON.parse(data.toString())
@ -46,7 +47,7 @@ module.exports = (server) => {
}
})
.catch((err) =>
logger.error(err, 'socket.redis.error', shortenToken(token)),
logger.error(err, 'socket.redis.error', Uploader.shortenToken(token)),
)
}
@ -64,10 +65,10 @@ module.exports = (server) => {
logger.error(
'WebSocket message too large',
'websocket.error',
shortenToken(token),
Uploader.shortenToken(token),
)
} else {
logger.error(err, 'websocket.error', shortenToken(token))
logger.error(err, 'websocket.error', Uploader.shortenToken(token))
}
})

View file

@ -1,12 +1,10 @@
const fs = require('node:fs')
const merge = require('lodash/merge')
const stripIndent = require('common-tags/lib/stripIndent')
const crypto = require('node:crypto')
const utils = require('../server/helpers/utils')
const logger = require('../server/logger')
// @ts-ignore
const { version } = require('../../package.json')
import crypto from 'node:crypto'
import fs from 'node:fs'
import { stripIndent } from 'common-tags'
import merge from 'lodash/merge.js'
import packageJson from '../../package.json' with { type: 'json' }
import * as utils from '../server/helpers/utils.js'
import logger from '../server/logger.js'
/**
* Tries to read the secret from a file if the according environment variable is set.
@ -28,7 +26,7 @@ const getSecret = (baseEnvVar) => {
*
* @returns {string}
*/
exports.generateSecret = (secretName) => {
export const generateSecret = (secretName) => {
logger.warn(
`auto-generating server ${secretName} because none was specified`,
'startup.secret',
@ -261,11 +259,11 @@ const getConfigFromFile = () => {
*
* @returns {object}
*/
exports.getCompanionOptions = (options = {}) => {
export const getCompanionOptions = (options = {}) => {
return merge({}, getConfigFromEnv(), getConfigFromFile(), options)
}
exports.buildHelpfulStartupMessage = (companionOptions) => {
export const buildHelpfulStartupMessage = (companionOptions) => {
const buildURL = utils.getURLBuilder(companionOptions)
const callbackURLs = []
Object.keys(companionOptions.providerOptions).forEach((providerName) => {
@ -273,7 +271,7 @@ exports.buildHelpfulStartupMessage = (companionOptions) => {
})
return stripIndent`
Welcome to Companion v${version}
Welcome to Companion v${packageJson.version}
===================================
Congratulations on setting up Companion! Thanks for joining our cause, you have taken

View file

@ -1,27 +1,26 @@
const express = require('express')
const qs = require('node:querystring')
const { randomUUID } = require('node:crypto')
const helmet = require('helmet')
const morgan = require('morgan')
const { URL } = require('node:url')
const session = require('express-session')
const RedisStore = require('connect-redis').default
const logger = require('../server/logger')
const redis = require('../server/redis')
const companion = require('../companion')
const {
getCompanionOptions,
generateSecret,
import { randomUUID } from 'node:crypto'
import qs from 'node:querystring'
import { URL } from 'node:url'
import RedisStore from 'connect-redis'
import express from 'express'
import session from 'express-session'
import helmet from 'helmet'
import morgan from 'morgan'
import * as companion from '../companion.js'
import logger from '../server/logger.js'
import * as redis from '../server/redis.js'
import {
buildHelpfulStartupMessage,
} = require('./helper')
generateSecret,
getCompanionOptions,
} from './helper.js'
/**
* Configures an Express app for running Companion standalone
*
* @returns {object}
*/
module.exports = function server(inputCompanionOptions) {
export default function server(inputCompanionOptions) {
const companionOptions = getCompanionOptions(inputCompanionOptions)
companion.setLoggerProcessName(companionOptions)

View file

@ -1,9 +1,8 @@
#!/usr/bin/env node
const companion = require('../companion')
// @ts-ignore
const { version } = require('../../package.json')
const standalone = require('.')
const logger = require('../server/logger')
import packageJson from '../../package.json' with { type: 'json' }
import * as companion from '../companion.js'
import logger from '../server/logger.js'
import standalone from './index.js'
const port = process.env.COMPANION_PORT || process.env.PORT || 3020
@ -11,5 +10,5 @@ const { app } = standalone()
companion.socket(app.listen(port))
logger.info(`Welcome to Companion! v${version}`)
logger.info(`Welcome to Companion! v${packageJson.version}`)
logger.info(`Listening on http://localhost:${port}`)

View file

@ -1,27 +1,17 @@
const mockOauthState = require('../mockoauthstate')()
import request from 'supertest'
import { describe, expect, test, vi } from 'vitest'
import * as tokenService from '../src/server/helpers/jwt.js'
import mockOauthState from './mockoauthstate.js'
import { getServer, grantToken } from './mockserver.js'
const request = require('supertest')
const tokenService = require('../../src/server/helpers/jwt')
const { getServer, grantToken } = require('../mockserver')
vi.mock('express-prom-bundle')
mockOauthState()
jest.mock('../../src/server/helpers/oauth-state', () => ({
...jest.requireActual('../../src/server/helpers/oauth-state'),
...mockOauthState,
}))
const authServer = getServer()
const authData = {
dropbox: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(
authData,
process.env.COMPANION_SECRET,
)
const secret = 'secret'
describe('test authentication callback', () => {
test('authentication callback redirects to send-token url', () => {
return request(authServer)
test('authentication callback redirects to send-token url', async () => {
return request(await getServer())
.get('/drive/callback')
.expect(302)
.expect((res) => {
@ -31,9 +21,8 @@ describe('test authentication callback', () => {
})
})
test('authentication callback sets cookie', () => {
console.log(process.env.COMPANION_SECRET)
return request(authServer)
test('authentication callback sets cookie', async () => {
return request(await getServer())
.get('/dropbox/callback')
.expect(302)
.expect((res) => {
@ -47,17 +36,23 @@ describe('test authentication callback', () => {
)
const payload = tokenService.verifyEncryptedAuthToken(
authToken,
process.env.COMPANION_SECRET,
secret,
'dropbox',
)
expect(payload).toEqual({ dropbox: { accessToken: grantToken } })
})
})
test('the token gets sent via html', () => {
test('the token gets sent via html', async () => {
const authData = {
dropbox: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(authData, secret)
// see mock ../../src/server/helpers/oauth-state above for state values
return request(authServer)
.get(`/dropbox/send-token?uppyAuthToken=${token}`)
return request(await getServer())
.get(`/dropbox/send-token?uppyAuthToken=${encodeURIComponent(token)}`)
.expect(200)
.expect((res) => {
expect(res.text).toMatch(`var data = {"token":"${token}"};`)

View file

@ -1,46 +1,40 @@
const nock = require('nock')
const request = require('supertest')
import nock from 'nock'
import request from 'supertest'
import { afterAll, describe, expect, it, test, vi } from 'vitest'
import packageJson from '../package.json' with { type: 'json' }
import * as tokenService from '../src/server/helpers/jwt.js'
import * as defaults from './fixtures/constants.js'
import { nockGoogleDownloadFile } from './fixtures/drive.js'
import mockOauthState from './mockoauthstate.js'
import { getServer } from './mockserver.js'
const mockOauthState = require('../mockoauthstate')
const { version } = require('../../package.json')
const { nockGoogleDownloadFile } = require('../fixtures/drive')
const defaults = require('../fixtures/constants')
jest.mock('tus-js-client')
jest.mock('../../src/server/helpers/oauth-state', () => ({
...jest.requireActual('../../src/server/helpers/oauth-state'),
...mockOauthState(),
}))
vi.mock('express-prom-bundle')
vi.mock('tus-js-client')
mockOauthState()
const fakeLocalhost = 'localhost.com'
jest.mock('node:dns', () => {
const actual = jest.requireActual('node:dns')
return {
...actual,
vi.mock('node:dns', () => ({
default: {
lookup: (hostname, options, callback) => {
if (fakeLocalhost === hostname || hostname === 'localhost') {
return callback(null, '127.0.0.1', 4)
}
return callback(new Error(`Unexpected call to hostname ${hostname}`))
},
}
})
},
}))
const tokenService = require('../../src/server/helpers/jwt')
const { getServer } = require('../mockserver')
const getServerWithEnv = async () =>
getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
// todo don't share server between tests. rewrite to not use env variables
const authServer = getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
const secret = 'secret'
const authData = {
dropbox: { accessToken: 'token value' },
box: { accessToken: 'token value' },
drive: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(
authData,
process.env.COMPANION_SECRET,
)
const token = tokenService.generateEncryptedAuthToken(authData, secret)
const OAUTH_STATE = 'some-cool-nice-encrytpion'
afterAll(() => {
@ -49,7 +43,7 @@ afterAll(() => {
})
describe('validate upload data', () => {
test('access token expired or invalid when starting provider download', () => {
test('access token expired or invalid when starting provider download', async () => {
const meta = {
size: null,
mimeType: 'video/mp4',
@ -73,7 +67,7 @@ describe('validate upload data', () => {
},
})
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -90,10 +84,10 @@ describe('validate upload data', () => {
)
})
test('invalid upload protocol gets rejected', () => {
test('invalid upload protocol gets rejected', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -107,10 +101,10 @@ describe('validate upload data', () => {
)
})
test('invalid upload fieldname gets rejected', () => {
test('invalid upload fieldname gets rejected', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -125,10 +119,10 @@ describe('validate upload data', () => {
)
})
test('invalid upload metadata gets rejected', () => {
test('invalid upload metadata gets rejected', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -143,10 +137,10 @@ describe('validate upload data', () => {
)
})
test('invalid upload headers get rejected', () => {
test('invalid upload headers get rejected', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -159,10 +153,10 @@ describe('validate upload data', () => {
.then((res) => expect(res.body.message).toBe('headers must be an object'))
})
test('invalid upload HTTP Method gets rejected', () => {
test('invalid upload HTTP Method gets rejected', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -177,10 +171,10 @@ describe('validate upload data', () => {
)
})
test('valid upload data is allowed - tus', () => {
test('valid upload data is allowed - tus', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -199,10 +193,10 @@ describe('validate upload data', () => {
.expect(200)
})
test('valid upload data is allowed - s3-multipart', () => {
test('valid upload data is allowed - s3-multipart', async () => {
nockGoogleDownloadFile()
return request(authServer)
return request(await getServerWithEnv())
.post('/drive/get/DUMMY-FILE-ID')
.set('uppy-auth-token', token)
.set('Content-Type', 'application/json')
@ -222,8 +216,8 @@ describe('validate upload data', () => {
})
})
describe('handle main oauth redirect', () => {
const serverWithMainOauth = getServer({
describe('handle main oauth redirect', async () => {
const serverWithMainOauth = await getServer({
COMPANION_OAUTH_DOMAIN: 'localhost:3040',
})
test('redirect to a valid uppy instance', () => {
@ -246,33 +240,36 @@ describe('handle main oauth redirect', () => {
})
})
it('periodically pings', (done) => {
nock('http://localhost')
.post(
'/ping',
(body) =>
body.some === 'value' &&
body.version === version &&
typeof body.processId === 'string',
)
.reply(200, () => done())
getServer({
COMPANION_PERIODIC_PING_URLS: 'http://localhost/ping',
COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD: '{"some": "value"}',
COMPANION_PERIODIC_PING_INTERVAL: '10',
COMPANION_PERIODIC_PING_COUNT: '1',
})
it('periodically pings', async () => {
await Promise.all([
getServer({
COMPANION_PERIODIC_PING_URLS: 'http://localhost/ping',
COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD: '{"some": "value"}',
COMPANION_PERIODIC_PING_INTERVAL: '10',
COMPANION_PERIODIC_PING_COUNT: '1',
}),
new Promise((resolve) => {
nock('http://localhost')
.post(
'/ping',
(body) =>
body.some === 'value' &&
body.version === packageJson.version &&
typeof body.processId === 'string',
)
.reply(200, () => resolve())
}),
])
}, 3000)
async function runUrlMetaTest(url) {
const server = getServer()
const server = await getServer()
return request(server).post('/url/meta').send({ url })
}
async function runUrlGetTest(url) {
const server = getServer()
const server = await getServer()
return request(server).post('/url/get').send({
fileId: url,

View file

@ -1,6 +1,9 @@
const { cors } = require('../../src/server/middlewares')
import { describe, expect, test, vi } from 'vitest'
import { cors } from '../src/server/middlewares.js'
function testWithMock({
// @ts-ignore
corsOptions,
get = () => {},
origin = 'https://localhost:1234',
@ -8,8 +11,8 @@ function testWithMock({
const res = {
get,
getHeader: get,
setHeader: jest.fn(),
end: jest.fn(),
setHeader: vi.fn(),
end: vi.fn(),
}
const req = {
method: 'OPTIONS',
@ -17,7 +20,7 @@ function testWithMock({
origin,
},
}
const next = jest.fn()
const next = vi.fn()
cors(corsOptions)(req, res, next)
return { res }
}
@ -35,6 +38,7 @@ describe('cors', () => {
}
const { res } = testWithMock({
// @ts-ignore
corsOptions: {
sendSelfEndpoint: true,
corsOrigins: /^https:\/\/localhost:.*$/,
@ -72,12 +76,14 @@ describe('cors', () => {
})
test('should support disabling cors', () => {
// @ts-ignore
const { res } = testWithMock({ corsOptions: { corsOrigins: false } })
expect(res.setHeader.mock.calls).toEqual([])
})
test('should support incorrect url', () => {
const { res } = testWithMock({
// @ts-ignore
corsOptions: { corsOrigins: /^incorrect$/ },
})
expect(res.setHeader.mock.calls).toEqual([
@ -94,6 +100,7 @@ describe('cors', () => {
test('should support array origin', () => {
const { res } = testWithMock({
// @ts-ignore
corsOptions: {
corsOrigins: ['http://google.com', 'https://localhost:1234'],
},

View file

@ -1,22 +1,25 @@
const request = require('supertest')
const nock = require('nock')
const tokenService = require('../../src/server/helpers/jwt')
const { getServer } = require('../mockserver')
const { nockZoomRevoke } = require('../fixtures/zoom')
import nock from 'nock'
import request from 'supertest'
import { afterAll, describe, expect, test, vi } from 'vitest'
import * as tokenService from '../src/server/helpers/jwt.js'
import { nockZoomRevoke, expects as zoomExpects } from './fixtures/zoom.js'
import { getServer } from './mockserver.js'
const { remoteZoomKey, remoteZoomSecret, remoteZoomVerificationToken } =
require('../fixtures/zoom').expects
zoomExpects
const authServer = getServer({
COMPANION_ZOOM_KEYS_ENDPOINT: 'http://localhost:2111/zoom-keys',
})
vi.mock('express-prom-bundle')
const secret = 'secret'
const getZoomServer = async () =>
getServer({
COMPANION_ZOOM_KEYS_ENDPOINT: 'http://localhost:2111/zoom-keys',
})
const authData = {
zoom: { accessToken: 'token value' },
}
const token = tokenService.generateEncryptedAuthToken(
authData,
process.env.COMPANION_SECRET,
)
const token = tokenService.generateEncryptedAuthToken(authData, secret)
afterAll(() => {
nock.cleanAll()
@ -27,6 +30,7 @@ describe('providers requests with remote oauth keys', () => {
// mocking request module used to fetch custom oauth credentials
nock('http://localhost:2111')
.post('/zoom-keys')
// @ts-ignore
.reply((uri, { provider, parameters }) => {
if (provider !== 'zoom' || parameters !== 'ZOOM-CREDENTIALS-PARAMS')
return [400]
@ -52,7 +56,7 @@ describe('providers requests with remote oauth keys', () => {
JSON.stringify(params),
'binary',
).toString('base64')
const res = await request(authServer)
const res = await request(await getZoomServer())
.get('/zoom/logout/')
.set('uppy-auth-token', token)
.set('uppy-credentials-params', encodedParams)
@ -64,13 +68,13 @@ describe('providers requests with remote oauth keys', () => {
})
})
test('zoom logout with wrong credentials params', () => {
test('zoom logout with wrong credentials params', async () => {
const params = { params: 'WRONG-ZOOM-CREDENTIALS-PARAMS' }
const encodedParams = Buffer.from(
JSON.stringify(params),
'binary',
).toString('base64')
return request(authServer)
return request(await getZoomServer())
.get('/zoom/logout/')
.set('uppy-auth-token', token)
.set('uppy-credentials-params', encodedParams)

View file

@ -1,8 +1,9 @@
const nock = require('nock')
const request = require('supertest')
const { getServer } = require('../mockserver')
import nock from 'nock'
import request from 'supertest'
import { afterAll, describe, test, vi } from 'vitest'
import { getServer } from './mockserver.js'
const authServer = getServer()
vi.mock('express-prom-bundle')
afterAll(() => {
nock.cleanAll()
@ -12,8 +13,8 @@ afterAll(() => {
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)
test('providers without support for callback endpoint', async () => {
return request(await getServer())
.post('/dropbox/deauthorization/callback')
.set('Content-Type', 'application/json')
.send({
@ -22,8 +23,8 @@ describe('handle deauthorization callback', () => {
.expect(500)
})
test('validate that request credentials match', () => {
return request(authServer)
test('validate that request credentials match', async () => {
return request(await getServer())
.post('/zoom/deauthorization/callback')
.set('Content-Type', 'application/json')
.set('Authorization', 'wrong-verfication-token')
@ -42,9 +43,9 @@ describe('handle deauthorization callback', () => {
.expect(400)
})
test('validate request credentials is present', () => {
test('validate request credentials is present', async () => {
// Authorization header is absent
return request(authServer)
return request(await getServer())
.post('/zoom/deauthorization/callback')
.set('Content-Type', 'application/json')
.send({
@ -62,8 +63,8 @@ describe('handle deauthorization callback', () => {
.expect(400)
})
test('validate request content', () => {
return request(authServer)
test('validate request content', async () => {
return request(await getServer())
.post('/zoom/deauthorization/callback')
.set('Content-Type', 'application/json')
.set('Authorization', 'zoom_verfication_token')
@ -73,8 +74,8 @@ describe('handle deauthorization callback', () => {
.expect(400)
})
test('validate request content (event name)', () => {
return request(authServer)
test('validate request content (event name)', async () => {
return request(await getServer())
.post('/zoom/deauthorization/callback')
.set('Content-Type', 'application/json')
.set('Authorization', 'zoom_verfication_token')
@ -93,8 +94,8 @@ describe('handle deauthorization callback', () => {
.expect(400)
})
test('allow valid request', () => {
return request(authServer)
test('allow valid request', async () => {
return request(await getServer())
.post('/zoom/deauthorization/callback')
.set('Content-Type', 'application/json')
.set('Authorization', 'zoom_verfication_token')

View file

@ -1,3 +1,3 @@
module.exports.expects = {
export const expects = {
itemIcon: 'file',
}

View file

@ -1,9 +1,9 @@
module.exports.NEXT_PAGE_TOKEN = 'DUMMY-NEXT-PAGE-TOKEN'
module.exports.ITEM_ID = 'DUMMY-FILE-ID'
module.exports.ITEM_NAME = 'MY DUMMY FILE NAME.mp4'
module.exports.ICON = 'https://DUMMY-THUMBNAIL.com/file.jpg'
module.exports.THUMBNAIL_URL = 'https://DUMMY-THUMBNAIL.com/file.jpg'
module.exports.MODIFIED_DATE = '2016-07-10T20:00:08.096Z'
module.exports.MIME_TYPE = 'video/mp4'
module.exports.USERNAME = 'john.doe@transloadit.com'
module.exports.FILE_SIZE = 758051
export const NEXT_PAGE_TOKEN = 'DUMMY-NEXT-PAGE-TOKEN'
export const ITEM_ID = 'DUMMY-FILE-ID'
export const ITEM_NAME = 'MY DUMMY FILE NAME.mp4'
export const ICON = 'https://DUMMY-THUMBNAIL.com/file.jpg'
export const THUMBNAIL_URL = 'https://DUMMY-THUMBNAIL.com/file.jpg'
export const MODIFIED_DATE = '2016-07-10T20:00:08.096Z'
export const MIME_TYPE = 'video/mp4'
export const USERNAME = 'john.doe@transloadit.com'
export const FILE_SIZE = 758051

View file

@ -1,14 +1,14 @@
const nock = require('nock')
const defaults = require('./constants')
import nock from 'nock'
import * as defaults from './constants.js'
module.exports.expects = {}
export const expects = {}
module.exports.nockGoogleDriveAboutCall = () =>
export const nockGoogleDriveAboutCall = () =>
nock('https://www.googleapis.com')
.get((uri) => uri.includes('about'))
.reply(200, { user: { emailAddress: 'john.doe@transloadit.com' } })
module.exports.nockGoogleDownloadFile = ({ times = 2 } = {}) => {
export const nockGoogleDownloadFile = ({ times = 2 } = {}) => {
nock('https://www.googleapis.com')
.get(
`/drive/v3/files/${defaults.ITEM_ID}?fields=kind%2Cid%2CimageMediaMetadata%2Cname%2CmimeType%2CownedByMe%2Csize%2CmodifiedTime%2CiconLink%2CthumbnailLink%2CteamDriveId%2CvideoMediaMetadata%2CexportLinks%2CshortcutDetails%28targetId%2CtargetMimeType%29&supportsAllDrives=true`,
@ -32,5 +32,5 @@ module.exports.nockGoogleDownloadFile = ({ times = 2 } = {}) => {
nock('https://www.googleapis.com')
.get(`/drive/v3/files/${defaults.ITEM_ID}?alt=media&supportsAllDrives=true`)
.reply(200, {})
module.exports.nockGoogleDriveAboutCall()
nockGoogleDriveAboutCall()
}

View file

@ -1,4 +1,4 @@
module.exports.expects = {
export const expects = {
itemIcon: 'file',
itemRequestPath: '%2Fhomework%2Fmath%2Fprime_numbers.txt',
}

View file

@ -1,6 +1,6 @@
const defaults = require('./constants')
import * as defaults from './constants.js'
module.exports.expects = {
export const expects = {
listPath: 'ALBUM-ID',
itemName: `${defaults.ITEM_ID} 2015-07-17T17:26:50+0000`,
itemMimeType: 'image/jpeg',

View file

@ -1,12 +1,13 @@
const box = require('./box')
const drive = require('./drive')
const dropbox = require('./dropbox')
const instagram = require('./instagram')
const onedrive = require('./onedrive')
const facebook = require('./facebook')
const zoom = require('./zoom')
import * as box from './box.js'
import * as constants from './constants.js'
import * as drive from './drive.js'
import * as dropbox from './dropbox.js'
import * as facebook from './facebook.js'
import * as instagram from './instagram.js'
import * as onedrive from './onedrive.js'
import * as zoom from './zoom.js'
module.exports.providers = {
export const providers = {
box,
drive,
dropbox,
@ -16,4 +17,4 @@ module.exports.providers = {
zoom,
}
module.exports.defaults = require('./constants')
export const defaults = constants

View file

@ -1,4 +1,4 @@
module.exports.expects = {
export const expects = {
itemName: 'Instagram 2017-08-31T18:10:00+00000.jpeg',
itemMimeType: 'image/jpeg',
itemSize: null,

View file

@ -1,5 +1,5 @@
const defaults = require('./constants')
import * as defaults from './constants.js'
module.exports.expects = {
export const expects = {
itemRequestPath: `${defaults.ITEM_ID}?driveId=DUMMY-DRIVE-ID`,
}

View file

@ -1,8 +1,7 @@
const nock = require('nock')
import nock from 'nock'
import { getBasicAuthHeader } from '../../src/server/helpers/utils.js'
const { getBasicAuthHeader } = require('../../src/server/helpers/utils')
module.exports.expects = {
export const expects = {
listPath: 'DUMMY-UUID%3D%3D',
itemName:
'DUMMY TOPIC - shared screen with speaker view (2020-05-29, 13:23).mp4',
@ -17,7 +16,7 @@ module.exports.expects = {
remoteZoomVerificationToken: 'REMOTE-ZOOM-VERIFICATION-TOKEN',
}
module.exports.nockZoomRecordings = ({ times = 1 } = {}) => {
export const nockZoomRecordings = ({ times = 1 } = {}) => {
nock('https://zoom.us')
.get('/v2/meetings/DUMMY-UUID%3D%3D/recordings')
.times(times)
@ -51,12 +50,11 @@ module.exports.nockZoomRecordings = ({ times = 1 } = {}) => {
})
}
module.exports.nockZoomRevoke = ({ key, secret }) => {
export const nockZoomRevoke = ({ key, secret }) => {
nock('https://zoom.us')
.post('/oauth/revoke?token=token+value')
.reply(function () {
const { headers } = this.req
const expected = getBasicAuthHeader(key, secret)
const success = headers.authorization === expected
return success ? [200, { status: 'success' }] : [400]

View file

@ -1,4 +1,5 @@
const headerSanitize = require('../../src/server/header-blacklist')
import { describe, expect, test } from 'vitest'
import headerSanitize from '../src/server/header-blacklist.js'
describe('Header black-list testing', () => {
test('All headers invalid by name', () => {

View file

@ -1,6 +1,9 @@
const nock = require('nock')
const { FORBIDDEN_IP_ADDRESS } = require('../../src/server/helpers/request')
const { getProtectedGot } = require('../../src/server/helpers/request')
import nock from 'nock'
import { afterAll, describe, expect, test } from 'vitest'
import {
FORBIDDEN_IP_ADDRESS,
getProtectedGot,
} from '../src/server/helpers/request.js'
afterAll(() => {
nock.cleanAll()
@ -11,31 +14,28 @@ describe('test protected request Agent', () => {
test('allows URLs without IP addresses', async () => {
nock('https://transloadit.com').get('/').reply(200)
const url = 'https://transloadit.com'
return (await getProtectedGot({ allowLocalIPs: false })).get(url)
return getProtectedGot({ allowLocalIPs: false }).get(url)
})
test('blocks url that resolves to forbidden IP', async () => {
const url = 'https://localhost'
const promise = getProtectedGot({ allowLocalIPs: false }).then((got) =>
got.get(url),
)
await expect(promise).rejects.toThrow(/^Forbidden resolved IP address/)
await expect(
getProtectedGot({ allowLocalIPs: false }).get(url),
).rejects.toThrow(/^Forbidden resolved IP address/)
})
test('blocks private http IP address', async () => {
const url = 'http://172.20.10.4:8090'
const promise = getProtectedGot({ allowLocalIPs: false }).then((got) =>
got.get(url),
)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
await expect(
getProtectedGot({ allowLocalIPs: false }).get(url),
).rejects.toThrow(FORBIDDEN_IP_ADDRESS)
})
test('blocks private https IP address', async () => {
const url = 'https://172.20.10.4:8090'
const promise = getProtectedGot({ allowLocalIPs: false }).then((got) =>
got.get(url),
)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
await expect(
getProtectedGot({ allowLocalIPs: false }).get(url),
).rejects.toThrow(FORBIDDEN_IP_ADDRESS)
})
test('blocks various private IP addresses', async () => {
@ -62,17 +62,15 @@ describe('test protected request Agent', () => {
for (const ip of ipv4s) {
const url = `http://${ip}:8090`
const promise = getProtectedGot({ allowLocalIPs: false }).then((got) =>
got.get(url),
)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
await expect(
getProtectedGot({ allowLocalIPs: false }).get(url),
).rejects.toThrow(FORBIDDEN_IP_ADDRESS)
}
for (const ip of ipv6s) {
const url = `http://[${ip}]:8090`
const promise = getProtectedGot({ allowLocalIPs: false }).then((got) =>
got.get(url),
)
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
await expect(
getProtectedGot({ allowLocalIPs: false }).get(url),
).rejects.toThrow(FORBIDDEN_IP_ADDRESS)
}
})
})

View file

@ -1,6 +1,9 @@
import { beforeAll, describe, expect, test } from 'vitest'
// We don't care about colors in our tests, so force `supports-color` to disable colors.
process.env.FORCE_COLOR = 'false'
const logger = require('../../src/server/logger')
import logger from '../src/server/logger.js'
const maskables = ['ToBeMasked1', 'toBeMasked2', 'toBeMasked(And)?Escaped']
@ -104,6 +107,7 @@ describe('Test Logger secret mask', () => {
test('masks inside object', () => {
const loggedMessage = captureConsoleLog(() => {
// @ts-ignore
logger.warn({
a: 1,
deep: { secret: 'there is a ToBeMasked1 hiding here' },

View file

@ -1,7 +1,9 @@
module.exports = () => {
return {
generateState: () => 'some-cool-nice-encrytpion',
addToState: () => 'some-cool-nice-encrytpion',
import { vi } from 'vitest'
const mockOauthState = () => {
vi.mock('../src/server/helpers/oauth-state.js', async () => ({
...(await vi.importActual('../src/server/helpers/oauth-state.js')),
generateState: () => ({}),
getFromState: (state) => {
if (state === 'state-with-invalid-instance-url') {
return 'http://localhost:3452'
@ -10,5 +12,7 @@ module.exports = () => {
return 'http://localhost:3020'
},
encodeState: () => 'some-cool-nice-encrytpion',
}
}))
}
export default mockOauthState

View file

@ -1,9 +1,9 @@
const express = require('express')
const session = require('express-session')
import express from 'express'
import session from 'express-session'
import { expects as zoomExpects } from './fixtures/zoom.js'
const {
expects: { localZoomKey, localZoomSecret, localZoomVerificationToken },
} = require('./fixtures/zoom')
const { localZoomKey, localZoomSecret, localZoomVerificationToken } =
zoomExpects
const defaultEnv = {
NODE_ENV: 'test',
@ -24,22 +24,28 @@ const defaultEnv = {
COMPANION_DROPBOX_KEY: 'dropbox_key',
COMPANION_DROPBOX_SECRET: 'dropbox_secret',
COMPANION_DROPBOX_KEYS_ENDPOINT: undefined,
COMPANION_BOX_KEY: 'box_key',
COMPANION_BOX_SECRET: 'box_secret',
COMPANION_BOX_KEYS_ENDPOINT: undefined,
COMPANION_GOOGLE_KEY: 'google_key',
COMPANION_GOOGLE_SECRET: 'google_secret',
COMPANION_GOOGLE_KEYS_ENDPOINT: undefined,
COMPANION_INSTAGRAM_KEY: 'instagram_key',
COMPANION_INSTAGRAM_SECRET: 'instagram_secret',
COMPANION_INSTAGRAM_KEYS_ENDPOINT: undefined,
COMPANION_FACEBOOK_KEY: 'facebook_key',
COMPANION_FACEBOOK_SECRET: 'facebook_secret',
COMPANION_FACEBOOK_KEYS_ENDPOINT: undefined,
COMPANION_ZOOM_KEY: localZoomKey,
COMPANION_ZOOM_SECRET: localZoomSecret,
COMPANION_ZOOM_VERIFICATION_TOKEN: localZoomVerificationToken,
COMPANION_ZOOM_KEYS_ENDPOINT: undefined,
COMPANION_PATH: '',
@ -54,15 +60,22 @@ const defaultEnv = {
function updateEnv(env) {
Object.keys(env).forEach((key) => {
process.env[key] = env[key]
const value = env[key]
if (value == null) delete process.env[key]
else process.env[key] = value
})
}
module.exports.setDefaultEnv = () => updateEnv(defaultEnv)
export const setDefaultEnv = () => updateEnv(defaultEnv)
module.exports.grantToken = 'fake token'
export const grantToken = 'fake token'
// companion stores certain global state, so the user needs to reset modules for each test
// todo rewrite companion to not use global state
// https://github.com/transloadit/uppy/issues/3284
export const getServer = async (extraEnv) => {
const { default: standalone } = await import('../src/standalone/index.js')
module.exports.getServer = (extraEnv) => {
const env = {
...defaultEnv,
...extraEnv,
@ -70,23 +83,20 @@ module.exports.getServer = (extraEnv) => {
updateEnv(env)
// companion stores certain global state like emitter, metrics, logger (frozen object), so we need to reset modules
// todo rewrite companion to not use global state
// https://github.com/transloadit/uppy/issues/3284
jest.resetModules()
const standalone = require('../src/standalone')
const authServer = express()
authServer.use(
session({ secret: 'grant', resave: true, saveUninitialized: true }),
)
authServer.all('*/callback', (req, res, next) => {
// @ts-ignore
req.session.grant = {
response: { access_token: module.exports.grantToken },
response: { access_token: grantToken },
}
next()
})
authServer.all(['*/send-token', '*/redirect'], (req, res, next) => {
// @ts-ignore
req.session.grant = {
dynamic: { state: req.query.state || 'non-empty-value' },
}

View file

@ -1,10 +1,10 @@
const emitter = require('../src/server/emitter')
import emitter from '../src/server/emitter/index.js'
module.exports.connect = (uploadToken) => {
export const connect = (uploadToken) => {
emitter().emit(`connection:${uploadToken}`)
}
module.exports.onProgress = (uploadToken, cb) => {
export const onProgress = (uploadToken, cb) => {
emitter().on(uploadToken, (message) => {
if (message.action === 'progress') {
cb(message)
@ -12,7 +12,7 @@ module.exports.onProgress = (uploadToken, cb) => {
})
}
module.exports.onUploadSuccess = (uploadToken, cb) => {
export const onUploadSuccess = (uploadToken, cb) => {
emitter().on(uploadToken, (message) => {
if (message.action === 'success') {
cb(message)
@ -20,7 +20,7 @@ module.exports.onUploadSuccess = (uploadToken, cb) => {
})
}
module.exports.onUploadError = (uploadToken, cb) => {
export const onUploadError = (uploadToken, cb) => {
emitter().on(uploadToken, (message) => {
if (message.action === 'error') {
cb(message)

View file

@ -1,4 +1,10 @@
jest.mock('../../src/server/helpers/jwt', () => {
import request from 'supertest'
import { describe, expect, test, vi } from 'vitest'
import { getServer } from './mockserver.js'
vi.mock('express-prom-bundle')
vi.mock('../src/server/helpers/jwt.js', () => {
return {
generateEncryptedToken: () => 'dummy token',
verifyEncryptedToken: () => '',
@ -7,19 +13,15 @@ jest.mock('../../src/server/helpers/jwt', () => {
}
})
const request = require('supertest')
const { getServer } = require('../mockserver')
// the order in which getServer is called matters because, once an env is passed,
// it won't be overridden when you call getServer without an argument
const serverWithFixedOauth = getServer()
const serverWithDynamicOauth = getServer({
COMPANION_DROPBOX_KEYS_ENDPOINT: 'http://localhost:1000/endpoint',
})
const getServerWithDynamicOauth = async () =>
getServer({
COMPANION_DROPBOX_KEYS_ENDPOINT: 'http://localhost:1000/endpoint',
})
describe('handle preauth endpoint', () => {
test('happy path', () => {
test('happy path', async () => {
return (
request(serverWithDynamicOauth)
request(await getServerWithDynamicOauth())
.post('/dropbox/preauth')
.set('Content-Type', 'application/json')
.send({
@ -31,8 +33,8 @@ describe('handle preauth endpoint', () => {
)
})
test('preauth request without params in body', () => {
return request(serverWithDynamicOauth)
test('preauth request without params in body', async () => {
return request(await getServerWithDynamicOauth())
.post('/dropbox/preauth')
.set('Content-Type', 'application/json')
.send({
@ -41,8 +43,8 @@ describe('handle preauth endpoint', () => {
.expect(400)
})
test('providers with dynamic credentials disabled', () => {
return request(serverWithDynamicOauth)
test('providers with dynamic credentials disabled', async () => {
return request(await getServerWithDynamicOauth())
.post('/drive/preauth')
.set('Content-Type', 'application/json')
.send({
@ -51,8 +53,8 @@ describe('handle preauth endpoint', () => {
.expect(501)
})
test('server with dynamic credentials disabled', () => {
return request(serverWithFixedOauth)
test('server with dynamic credentials disabled', async () => {
return request(await getServer())
.post('/dropbox/preauth')
.set('Content-Type', 'application/json')
.send({

View file

@ -1,6 +1,8 @@
const providerManager = require('../../src/server/provider')
const { getCompanionOptions } = require('../../src/standalone/helper')
const { setDefaultEnv } = require('../mockserver')
import { beforeEach, describe, expect, test } from 'vitest'
import GrantConfig from '../src/config/grant.js'
import * as providerManager from '../src/server/provider/index.js'
import { getCompanionOptions } from '../src/standalone/helper.js'
import { setDefaultEnv } from './mockserver.js'
let grantConfig
let companionOptions
@ -11,7 +13,7 @@ const getOauthProvider = (providerName) =>
describe('Test Provider options', () => {
beforeEach(() => {
setDefaultEnv()
grantConfig = require('../../src/config/grant')()
grantConfig = GrantConfig()
companionOptions = getCompanionOptions()
})
@ -198,6 +200,7 @@ describe('Test Custom Provider options', () => {
key: 'foo_key',
secret: 'foo_secret',
},
// @ts-ignore
module: { oauthProvider: 'some_provider' },
},
},

View file

@ -1,32 +1,39 @@
const request = require('supertest')
const nock = require('nock')
import nock from 'nock'
import request from 'supertest'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
import * as tokenService from '../src/server/helpers/jwt.js'
import * as providerModule from '../src/server/provider/index.js'
import * as defaults from './fixtures/constants.js'
import { nockGoogleDownloadFile } from './fixtures/drive.js'
import * as fixtures from './fixtures/index.js'
import {
nockZoomRecordings,
nockZoomRevoke,
expects as zoomExpects,
} from './fixtures/zoom.js'
import mockOauthState from './mockoauthstate.js'
import { getServer } from './mockserver.js'
const mockOauthState = require('../mockoauthstate')
const { localZoomKey, localZoomSecret } = zoomExpects
jest.mock('tus-js-client')
jest.mock('../../src/server/helpers/request', () => {
vi.mock('express-prom-bundle')
vi.mock('tus-js-client')
mockOauthState()
vi.mock('../../src/server/helpers/request.js', () => {
return {
getURLMeta: () => Promise.resolve({ size: 758051 }),
}
})
jest.mock('../../src/server/helpers/oauth-state', () => mockOauthState())
const fixtures = require('../fixtures')
const { nockGoogleDownloadFile } = require('../fixtures/drive')
const {
nockZoomRecordings,
nockZoomRevoke,
expects: { localZoomKey, localZoomSecret },
} = require('../fixtures/zoom')
const defaults = require('../fixtures/constants')
const tokenService = require('../../src/server/helpers/jwt')
const { getServer } = require('../mockserver')
// todo don't share server between tests. rewrite to not use env variables
const authServer = getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
const getServerWithEnv = async () =>
getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
const OAUTH_STATE = 'some-cool-nice-encrytpion'
const providers = require('../../src/server/provider').getDefaultProviders()
const secret = 'secret'
const providers = providerModule.getDefaultProviders()
const providerNames = Object.keys(providers)
const oauthProviders = Object.fromEntries(
@ -38,10 +45,7 @@ const authData = {}
providerNames.forEach((provider) => {
authData[provider] = { accessToken: 'token value' }
})
const token = tokenService.generateEncryptedAuthToken(
authData,
process.env.COMPANION_SECRET,
)
const token = tokenService.generateEncryptedAuthToken(authData, secret)
const thisOrThat = (value1, value2) => {
if (value1 !== undefined) {
@ -88,7 +92,7 @@ afterAll(() => {
describe('list provider files', () => {
async function runTest(providerName) {
const providerFixture = fixtures.providers[providerName]?.expects ?? {}
return request(authServer)
return request(await getServerWithEnv())
.get(`/${providerName}/list/${providerFixture.listPath || ''}`)
.set('uppy-auth-token', token)
.expect(200)
@ -376,7 +380,7 @@ describe('list provider files', () => {
describe('provider file gets downloaded from', () => {
async function runTest(providerName) {
const providerFixture = fixtures.providers[providerName]?.expects ?? {}
const res = await request(authServer)
const res = await request(await getServerWithEnv())
.post(
`/${providerName}/get/${providerFixture.itemRequestPath || defaults.ITEM_ID}`,
)
@ -492,7 +496,7 @@ describe('connect to provider', () => {
if (oauthProvider == null) return
await request(authServer)
await request(await getServerWithEnv())
.get(`/${providerName}/connect?foo=bar`)
.set('uppy-auth-token', token)
.expect(302)
@ -506,7 +510,7 @@ describe('connect to provider', () => {
describe('logout of provider', () => {
async function runTest(providerName) {
const res = await request(authServer)
const res = await request(await getServerWithEnv())
.get(`/${providerName}/logout/`)
.set('uppy-auth-token', token)
.expect(200)

View file

@ -1,8 +1,11 @@
const request = require('supertest')
const { getServer } = require('../mockserver')
import request from 'supertest'
import { it, test, vi } from 'vitest'
import { getServer } from './mockserver.js'
vi.mock('express-prom-bundle')
it('can be served under a subpath', async () => {
const server = getServer({ COMPANION_PATH: '/subpath' })
const server = await getServer({ COMPANION_PATH: '/subpath' })
await request(server).get('/subpath').expect(200)
await request(server).get('/subpath/metrics').expect(200)
@ -11,7 +14,7 @@ it('can be served under a subpath', async () => {
})
test('can be served without a subpath', async () => {
const server = getServer()
const server = await getServer()
await request(server).get('/').expect(200)
await request(server).get('/metrics').expect(200)

View file

@ -1,15 +1,16 @@
jest.mock('tus-js-client')
import { once } from 'node:events'
import fs from 'node:fs'
import { createServer } from 'node:http'
import { Readable } from 'node:stream'
import nock from 'nock'
import { afterAll, describe, expect, test, vi } from 'vitest'
import Emitter from '../src/server/emitter/index.js'
import Uploader, { ValidationError } from '../src/server/Uploader.js'
import standalone from '../src/standalone/index.js'
import * as socketClient from './mocksocket.js'
const { Readable } = require('node:stream')
const fs = require('node:fs')
const { createServer } = require('node:http')
const { once } = require('node:events')
const nock = require('nock')
const Uploader = require('../../src/server/Uploader')
const socketClient = require('../mocksocket')
const standalone = require('../../src/standalone')
const Emitter = require('../../src/server/emitter')
vi.mock('tus-js-client')
vi.mock('express-prom-bundle')
afterAll(() => {
nock.cleanAll()
@ -33,8 +34,9 @@ describe('uploader with tus protocol', () => {
},
}
// @ts-ignore
expect(() => new Uploader(opts)).toThrow(
new Uploader.ValidationError(
new ValidationError(
'upload destination does not match any allowed destinations',
),
)
@ -49,6 +51,7 @@ describe('uploader with tus protocol', () => {
},
}
// @ts-ignore
new Uploader(opts) // no validation error
})
@ -61,6 +64,7 @@ describe('uploader with tus protocol', () => {
},
}
// @ts-ignore
new Uploader(opts) // no validation error
})
@ -75,16 +79,18 @@ describe('uploader with tus protocol', () => {
pathPrefix: companionOptions.filePath,
}
// @ts-ignore
const uploader = new Uploader(opts)
const uploadToken = uploader.token
expect(uploadToken).toBeTruthy()
let firstReceivedProgress
const onProgress = jest.fn()
const onUploadSuccess = jest.fn()
const onBeginUploadEvent = jest.fn()
const onUploadEvent = jest.fn()
const onProgress = vi.fn()
const onUploadSuccess = vi.fn()
const onUploadError = vi.fn()
const onBeginUploadEvent = vi.fn()
const onUploadEvent = vi.fn()
const emitter = Emitter()
emitter.on('upload-start', onBeginUploadEvent)
@ -98,10 +104,14 @@ describe('uploader with tus protocol', () => {
firstReceivedProgress = message.payload.bytesUploaded
onProgress(message)
})
socketClient.onUploadError(uploadToken, onUploadError)
socketClient.onUploadSuccess(uploadToken, onUploadSuccess)
await promise
// @ts-ignore
await uploader.tryUploadStream(stream, mockReq)
expect(onUploadError).not.toHaveBeenCalled()
expect(firstReceivedProgress).toBe(8)
expect(onProgress).toHaveBeenLastCalledWith(
@ -139,6 +149,7 @@ describe('uploader with tus protocol', () => {
pathPrefix: companionOptions.filePath,
}
// @ts-ignore
const uploader = new Uploader(opts)
const originalTryDeleteTmpPath = uploader.tryDeleteTmpPath.bind(uploader)
uploader.tryDeleteTmpPath = async () => {
@ -156,8 +167,10 @@ describe('uploader with tus protocol', () => {
return new Promise((resolve, reject) => {
// validate that the test is resolved on socket connection
uploader.awaitReady(60000).then(() => {
// @ts-ignore
uploader.tryUploadStream(stream, mockReq).then(() => {
try {
// @ts-ignore
expect(fs.existsSync(uploader.path)).toBe(false)
resolve()
} catch (err) {
@ -188,7 +201,9 @@ describe('uploader with tus protocol', () => {
})
async function runMultipartTest({
// @ts-ignore
metadata,
// @ts-ignore
useFormData,
includeSize = true,
address = 'localhost',
@ -207,6 +222,7 @@ describe('uploader with tus protocol', () => {
}
const uploader = new Uploader(opts)
// @ts-ignore
return uploader.uploadStream(stream)
}
@ -227,6 +243,7 @@ describe('uploader with tus protocol', () => {
await once(server, 'listening')
const ret = await runMultipartTest({
// @ts-ignore
address: `localhost:${server.address().port}`,
})
expect(ret).toMatchObject({
@ -244,6 +261,7 @@ describe('uploader with tus protocol', () => {
test('upload functions with xhr formdata', async () => {
nock('http://localhost').post('/', formDataNoMetaMatch).reply(200)
// @ts-ignore
const ret = await runMultipartTest({ useFormData: true })
expect(ret).toMatchObject({
url: null,
@ -255,6 +273,7 @@ describe('uploader with tus protocol', () => {
nock('http://localhost').post('/', formDataNoMetaMatch).reply(200)
const ret = await runMultipartTest({
// @ts-ignore
useFormData: true,
includeSize: false,
})
@ -271,6 +290,7 @@ describe('uploader with tus protocol', () => {
'/',
/^--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="key1"\r\n\r\nnull\r\n--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="key2"\r\n\r\ntrue\r\n--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="key3"\r\n\r\n\d+\r\n--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="key4"\r\n\r\n\[object Object\]\r\n--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="key5"\r\n\r\n\(\) => \{\}\r\n--form-data-boundary-[a-z0-9]+\r\nContent-Disposition: form-data; name="files\[\]"; filename="uppy-file-[^"]+"\r\nContent-Type: application\/octet-stream\r\n\r\nSome file content\r\n--form-data-boundary-[a-z0-9]+--\r\n\r\n$/,
)
.times(10)
.reply(200)
const metadata = {
@ -280,6 +300,7 @@ describe('uploader with tus protocol', () => {
key4: {},
key5: () => {},
}
// @ts-ignore
const ret = await runMultipartTest({ useFormData: true, metadata })
expect(ret).toMatchObject({
url: null,
@ -293,10 +314,12 @@ describe('uploader with tus protocol', () => {
endpoint: 'http://localhost',
}
// @ts-ignore
new Uploader({ ...opts, metadata: { key: 'string value' } })
// @ts-ignore
expect(() => new Uploader({ ...opts, metadata: '' })).toThrow(
new Uploader.ValidationError('metadata must be an object'),
new ValidationError('metadata must be an object'),
)
})
@ -307,8 +330,9 @@ describe('uploader with tus protocol', () => {
size: 101,
}
// @ts-ignore
expect(() => new Uploader(opts)).toThrow(
new Uploader.ValidationError('maxFileSize exceeded'),
new ValidationError('maxFileSize exceeded'),
)
})
@ -319,6 +343,7 @@ describe('uploader with tus protocol', () => {
size: 99,
}
// @ts-ignore
new Uploader(opts) // no validation error
})
@ -333,12 +358,14 @@ describe('uploader with tus protocol', () => {
pathPrefix: companionOptions.filePath,
}
// @ts-ignore
const uploader = new Uploader(opts)
const uploadToken = uploader.token
// validate that the test is resolved on socket connection
uploader
.awaitReady(60000)
// @ts-ignore
.then(() => uploader.tryUploadStream(stream, mockReq))
socketClient.connect(uploadToken)

View file

@ -1,18 +1,22 @@
const nock = require('nock')
const request = require('supertest')
import nock from 'nock'
import request from 'supertest'
import { afterAll, beforeAll, describe, expect, test, vi } from 'vitest'
jest.mock('tus-js-client')
jest.mock('../../src/server/helpers/request', () => {
import { getServer } from './mockserver.js'
vi.mock('express-prom-bundle')
vi.mock('tus-js-client')
vi.mock('../src/server/helpers/request.js', async () => {
return {
...jest.requireActual('../../src/server/helpers/request'),
...(await vi.importActual('../src/server/helpers/request.js')),
getURLMeta: () => {
return Promise.resolve({ size: 7580, type: 'image/jpg' })
},
}
})
const { getServer } = require('../mockserver')
const mockServer = getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
const getMockServer = async () =>
getServer({ COMPANION_CLIENT_SOCKET_CONNECT_TIMEOUT: '0' })
beforeAll(() => {
nock('http://url.myendpoint.com')
@ -33,8 +37,8 @@ const invalids = [
]
describe('url meta', () => {
test("return a url's meta data", () => {
return request(mockServer)
test("return a url's meta data", async () => {
return request(await getMockServer())
.post('/url/meta')
.set('Content-Type', 'application/json')
.send({
@ -47,8 +51,8 @@ describe('url meta', () => {
})
})
test.each(invalids)('return 400 for invalid url', (urlCase) => {
return request(mockServer)
test.each(invalids)('return 400 for invalid url', async (urlCase) => {
return request(await getMockServer())
.post('/url/meta')
.set('Content-Type', 'application/json')
.send({
@ -60,8 +64,8 @@ describe('url meta', () => {
})
describe('url get', () => {
test('url download gets instanitated', () => {
return request(mockServer)
test('url download gets instanitated', async () => {
return request(await getMockServer())
.post('/url/get')
.set('Content-Type', 'application/json')
.send({
@ -75,8 +79,8 @@ describe('url get', () => {
test.each(invalids)(
'downloads are not instantiated for invalid urls',
(urlCase) => {
return request(mockServer)
async (urlCase) => {
return request(await getMockServer())
.post('/url/get')
.set('Content-Type', 'application/json')
.send({

View file

@ -0,0 +1,8 @@
{
"extends": "./tsconfig.shared",
"compilerOptions": {
"outDir": "./lib",
"noEmitOnError": true
},
"include": ["src/**/*"]
}

View file

@ -1,16 +1,7 @@
{
"extends": "./tsconfig.shared",
"compilerOptions": {
"outDir": "./lib",
"module": "Node16",
"moduleResolution": "node16",
"declaration": true,
"target": "es2022",
"noImplicitAny": false,
"sourceMap": false,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"noEmitOnError": true
"noEmit": true
},
"include": ["src/**/*"]
"include": ["src/**/*", "test/**/*"]
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "NodeNext",
"declaration": true,
"target": "es2022",
"noImplicitAny": false,
"sourceMap": false,
"allowJs": true,
"checkJs": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": []
}
}

View file

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
include: ['{src,test}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
},
})

View file

@ -21,19 +21,19 @@ const sampleImage = fs.readFileSync(
)
const file1 = {
source: 'jest',
source: 'test',
name: 'image-1.jpeg',
type: 'image/jpeg',
data: new File([sampleImage], 'image-1.jpeg', { type: 'image/jpeg' }),
}
const file2 = {
source: 'jest',
source: 'test',
name: 'yolo',
type: 'image/jpeg',
data: new File([sampleImage], 'yolo', { type: 'image/jpeg' }),
}
const file3 = {
source: 'jest',
source: 'test',
name: 'my.file.is.weird.png',
type: 'image/png',
data: new File([sampleImage], 'my.file.is.weird.png', { type: 'image/png' }),

Some files were not shown because too many files have changed in this diff Show more