Ver código fonte

Merge pull request #1966 from transloadit/instagram-graph

companion: support new Instagram Graph API
Ifedapo .A. Olarewaju 5 anos atrás
pai
commit
cb75538fcd

+ 13 - 30
package-lock.json

@@ -6350,8 +6350,8 @@
         "express-interceptor": "1.2.0",
         "express-prom-bundle": "3.3.0",
         "express-request-id": "1.4.1",
-        "express-session": "1.17.0",
-        "grant-express": "4.1.2",
+        "express-session": "1.15.6",
+        "grant": "4.6.5",
         "helmet": "3.21.2",
         "isobject": "3.0.1",
         "jsonwebtoken": "8.5.1",
@@ -6493,35 +6493,19 @@
           "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
         },
         "express-session": {
-          "version": "1.17.0",
-          "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.17.0.tgz",
-          "integrity": "sha512-t4oX2z7uoSqATbMfsxWMbNjAL0T5zpvcJCk3Z9wnPPN7ibddhnmDZXHfEcoBMG2ojKXZoCyPMc5FbtK+G7SoDg==",
+          "version": "1.15.6",
+          "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz",
+          "integrity": "sha512-r0nrHTCYtAMrFwZ0kBzZEXa1vtPVrw0dKvGSrKP4dahwBQ1BJpF2/y1Pp4sCD/0kvxV4zZeclyvfmw0B4RMJQA==",
           "requires": {
-            "cookie": "0.4.0",
+            "cookie": "0.3.1",
             "cookie-signature": "1.0.6",
+            "crc": "3.4.4",
             "debug": "2.6.9",
-            "depd": "~2.0.0",
-            "on-headers": "~1.0.2",
-            "parseurl": "~1.3.3",
-            "safe-buffer": "5.2.0",
-            "uid-safe": "~2.1.5"
-          },
-          "dependencies": {
-            "cookie": {
-              "version": "0.4.0",
-              "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
-              "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
-            },
-            "depd": {
-              "version": "2.0.0",
-              "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
-              "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
-            },
-            "safe-buffer": {
-              "version": "5.2.0",
-              "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
-              "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
-            }
+            "depd": "~1.1.1",
+            "on-headers": "~1.0.1",
+            "parseurl": "~1.3.2",
+            "uid-safe": "~2.1.5",
+            "utils-merge": "1.0.1"
           }
         },
         "frameguard": {
@@ -12933,8 +12917,7 @@
     "crc": {
       "version": "3.4.4",
       "resolved": "https://registry.npmjs.org/crc/-/crc-3.4.4.tgz",
-      "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms=",
-      "dev": true
+      "integrity": "sha1-naHpgOO9RPxck79as9ozeNheRms="
     },
     "crc32-stream": {
       "version": "3.0.1",

+ 2 - 2
packages/@uppy/companion/package.json

@@ -42,8 +42,8 @@
     "express-interceptor": "1.2.0",
     "express-prom-bundle": "3.3.0",
     "express-request-id": "1.4.1",
-    "express-session": "1.17.0",
-    "grant-express": "4.1.2",
+    "express-session": "1.15.6",
+    "grant": "4.6.5",
     "helmet": "3.21.2",
     "isobject": "3.0.1",
     "jsonwebtoken": "8.5.1",

+ 3 - 3
packages/@uppy/companion/src/companion.js

@@ -1,6 +1,6 @@
 const express = require('express')
 // @ts-ignore
-const Grant = require('grant-express')
+const Grant = require('grant').express()
 const grantConfig = require('./config/grant')()
 const providerManager = require('./server/provider')
 const controllers = require('./server/controllers')
@@ -19,7 +19,6 @@ const { STORAGE_PREFIX } = require('./server/Uploader')
 const middlewares = require('./server/middlewares')
 const { shortenToken } = require('./server/Uploader')
 
-const providers = providerManager.getDefaultProviders()
 const defaultOptions = {
   server: {
     protocol: 'http',
@@ -44,6 +43,7 @@ const defaultOptions = {
  */
 module.exports.app = (options = {}) => {
   options = merge({}, defaultOptions, options)
+  const providers = providerManager.getDefaultProviders(options)
   providerManager.addProviderOptions(options, grantConfig)
 
   const customProviders = options.customProviders
@@ -61,7 +61,7 @@ module.exports.app = (options = {}) => {
   app.use(cookieParser()) // server tokens are added to cookies
 
   app.use(interceptGrantErrorResponse)
-  app.use(new Grant(grantConfig))
+  app.use(Grant(grantConfig))
   app.use((req, res, next) => {
     res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, DELETE')
     res.header(

+ 2 - 1
packages/@uppy/companion/src/server/controllers/send-token.js

@@ -19,7 +19,8 @@ module.exports = function sendToken (req, res, next) {
   // add the token to cookies for thumbnail/image requests
   tokenService.addToCookies(res, uppyAuthToken, req.companion.options, req.companion.provider.authProvider)
 
-  const state = (req.session.grant || {}).state
+  const dynamic = (req.session.grant || {}).dynamic || {}
+  const state = dynamic.state
   if (state) {
     const origin = oAuthState.getFromState(state, 'origin', req.companion.options.secret)
     const clientVersion = oAuthState.getFromState(

+ 64 - 0
packages/@uppy/companion/src/server/provider/Provider.js

@@ -0,0 +1,64 @@
+/**
+ * Provider interface defines the specifications of any provider implementation
+ */
+class Provider {
+  /**
+   *
+   * @param {object} options
+   */
+  constructor (options) {
+    return this
+  }
+
+  /**
+   * config to extend the grant config
+   */
+  static getExtraConfig () {
+    return {}
+  }
+
+  /**
+   * list the files and folders in the provider account
+   * @param {object} options
+   * @param {function} cb
+   */
+  list (options, cb) {
+    throw new Error('method not implemented')
+  }
+
+  /**
+   * download a certain file from the provider account
+   * @param {object} options
+   * @param {function} cb
+   */
+  download (options, cb) {
+    throw new Error('method not implemented')
+  }
+
+  /**
+   * return a thumbnail for a provider file
+   * @param {object} options
+   * @param {function} cb
+   */
+  thumbnail (options, cb) {
+    throw new Error('method not implemented')
+  }
+
+  /**
+   * get the size of a certain file in the provider account
+   * @param {object} options
+   * @param {function} cb
+   */
+  size (options, cb) {
+    throw new Error('method not implemented')
+  }
+
+  /**
+   * @returns {string}
+   */
+  static get authProvider () {
+    return ''
+  }
+}
+
+module.exports = Provider

+ 4 - 1
packages/@uppy/companion/src/server/provider/drive/index.js

@@ -1,3 +1,5 @@
+const Provider = require('../Provider')
+
 const request = require('request')
 // @ts-ignore
 const purest = require('purest')({ request })
@@ -9,8 +11,9 @@ 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 = '*'
 
-class Drive {
+class Drive extends Provider {
   constructor (options) {
+    super(options)
     this.authProvider = options.provider = Drive.authProvider
     options.alias = 'drive'
     options.version = 'v3'

+ 4 - 1
packages/@uppy/companion/src/server/provider/dropbox/index.js

@@ -1,3 +1,5 @@
+const Provider = require('../Provider')
+
 const request = require('request')
 const purest = require('purest')({ request })
 const logger = require('../../logger')
@@ -17,8 +19,9 @@ function httpHeaderSafeJson (v) {
   )
 }
 
-class DropBox {
+class DropBox extends Provider {
   constructor (options) {
+    super(options)
     this.authProvider = options.provider = DropBox.authProvider
     this.client = purest(options)
   }

+ 4 - 1
packages/@uppy/companion/src/server/provider/facebook/index.js

@@ -1,3 +1,5 @@
+const Provider = require('../Provider')
+
 const request = require('request')
 const purest = require('purest')({ request })
 const utils = require('../../helpers/utils')
@@ -5,8 +7,9 @@ const logger = require('../../logger')
 const adapter = require('./adapter')
 const AuthError = require('../error')
 
-class Facebook {
+class Facebook extends Provider {
   constructor (options) {
+    super(options)
     this.authProvider = options.provider = Facebook.authProvider
     this.client = purest(options)
   }

+ 29 - 63
packages/@uppy/companion/src/server/provider/index.js

@@ -6,62 +6,13 @@ const config = require('@purest/providers')
 const dropbox = require('./dropbox')
 const drive = require('./drive')
 const instagram = require('./instagram')
+const instagramGraph = require('./instagram/graph')
 const facebook = require('./facebook')
 const onedrive = require('./onedrive')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
-
-/**
- * Provider interface defines the specifications of any provider implementation
- *
- * @interface
- */
-class Provider {
-  /**
-   *
-   * @param {object} options
-   */
-  constructor (options) {
-    return this
-  }
-
-  /**
-   *
-   * @param {object} options
-   * @param {function} cb
-   */
-  list (options, cb) {}
-
-  /**
-   *
-   * @param {object} options
-   * @param {function} cb
-   */
-  download (options, cb) {}
-
-  /**
-   *
-   * @param {object} options
-   * @param {function} cb
-   */
-  thumbnail (options, cb) {}
-
-  /**
-   *
-   * @param {object} options
-   * @param {function} cb
-   */
-  size (options, cb) {}
-
-  /**
-   * @returns {string}
-   */
-  static get authProvider () {
-    return ''
-  }
-}
-
-module.exports.ProviderInterface = Provider
+// eslint-disable-next-line
+const Provider = require('./Provider')
 
 /**
  * adds the desired provider module to the request object,
@@ -90,10 +41,21 @@ module.exports.getProviderMiddleware = (providers) => {
 }
 
 /**
+ * @param {{server: object, providerOptions: object}} companionOptions
  * @return {Object.<string, typeof Provider>}
  */
-module.exports.getDefaultProviders = () => {
-  return { dropbox, drive, instagram, facebook, onedrive }
+module.exports.getDefaultProviders = (companionOptions) => {
+  const { providerOptions } = companionOptions || { providerOptions: null }
+  const providers = { dropbox, drive, facebook, onedrive }
+  // Instagram's Graph API key is just numbers, while the old API key is hex
+  const usesGraphAPI = () => /^\d+$/.test(providerOptions.instagram.key)
+  if (providerOptions && providerOptions.instagram && usesGraphAPI()) {
+    providers.instagram = instagramGraph
+  } else {
+    providers.instagram = instagram
+  }
+
+  return providers
 }
 
 /**
@@ -113,17 +75,17 @@ module.exports.addCustomProviders = (customProviders, providers, grantConfig) =>
 
 /**
  *
- * @param {{server: object, providerOptions: object}} options
+ * @param {{server: object, providerOptions: object}} companionOptions
  * @param {object} grantConfig
  */
-module.exports.addProviderOptions = (options, grantConfig) => {
-  const { server, providerOptions } = options
+module.exports.addProviderOptions = (companionOptions, grantConfig) => {
+  const { server, providerOptions } = companionOptions
   if (!validOptions({ server })) {
     logger.warn('invalid provider options detected. Providers will not be loaded', 'provider.options.invalid')
     return
   }
 
-  grantConfig.server = {
+  grantConfig.defaults = {
     host: server.host,
     protocol: server.protocol,
     path: server.path
@@ -136,13 +98,15 @@ module.exports.addProviderOptions = (options, grantConfig) => {
       // explicitly add providerOptions so users don't override other providerOptions.
       grantConfig[authProvider].key = providerOptions[authProvider].key
       grantConfig[authProvider].secret = providerOptions[authProvider].secret
+      const { provider, name } = authNameToProvider(authProvider, companionOptions)
+      Object.assign(grantConfig[authProvider], provider.getExtraConfig())
 
       // override grant.js redirect uri with companion's custom redirect url
       if (oauthDomain) {
-        const providerName = authToProviderName(authProvider)
+        const providerName = name
         const redirectPath = `/${providerName}/redirect`
         const isExternal = !!server.implicitPath
-        const fullRedirectPath = getURLBuilder(options)(redirectPath, isExternal, true)
+        const fullRedirectPath = getURLBuilder(companionOptions)(redirectPath, isExternal, true)
         grantConfig[authProvider].redirect_uri = `${server.protocol}://${oauthDomain}${fullRedirectPath}`
       }
 
@@ -161,14 +125,16 @@ module.exports.addProviderOptions = (options, grantConfig) => {
 /**
  *
  * @param {string} authProvider
+ * @param {{server: object, providerOptions: object}} options
+ * @return {{name: string, provider: typeof Provider}}
  */
-const authToProviderName = (authProvider) => {
-  const providers = exports.getDefaultProviders()
+const authNameToProvider = (authProvider, options) => {
+  const providers = exports.getDefaultProviders(options)
   const providerNames = Object.keys(providers)
   for (const name of providerNames) {
     const provider = providers[name]
     if (provider.authProvider === authProvider) {
-      return name
+      return { name, provider }
     }
   }
 }

+ 72 - 0
packages/@uppy/companion/src/server/provider/instagram/graph/adapter.js

@@ -0,0 +1,72 @@
+const querystring = require('querystring')
+
+const MEDIA_TYPES = Object.freeze({
+  video: 'VIDEO',
+  carousel: 'CAROUSEL_ALBUM',
+  image: 'IMAGE'
+})
+
+const isVideo = (item) => item.media_type === MEDIA_TYPES.video
+
+exports.isFolder = (_) => {
+  return false
+}
+
+exports.getItemIcon = (item) => {
+  return isVideo(item) ? item.thumbnail_url : item.media_url
+}
+
+exports.getItemSubList = (item) => {
+  const newItems = []
+  item.data.forEach((subItem) => {
+    // exclude videos because of bug https://developers.facebook.com/support/bugs/801145630390846/
+    // @todo remove this clause when bug is fixed
+    if (isVideo(subItem)) {
+      return
+    }
+
+    if (subItem.media_type === MEDIA_TYPES.carousel) {
+      subItem.children.data.forEach((i) => {
+        // exclude videos because of bug https://developers.facebook.com/support/bugs/801145630390846/
+        // @todo remove this clause when bug is fixed
+        if (isVideo(i)) {
+          return
+        }
+
+        newItems.push(i)
+      })
+    } else {
+      newItems.push(subItem)
+    }
+  })
+  return newItems
+}
+
+exports.getItemName = (item, index) => {
+  const ext = isVideo(item) ? 'mp4' : 'jpeg'
+  // adding index, so the name is unique
+  return `Instagram ${item.timestamp}${index}.${ext}`
+}
+
+exports.getMimeType = (item) => {
+  return isVideo(item) ? 'video/mp4' : 'image/jpeg'
+}
+
+exports.getItemId = (item) => item.id
+
+exports.getItemRequestPath = (item) => item.id
+
+exports.getItemModifiedDate = (item) => item.timestamp
+
+exports.getItemThumbnailUrl = (item) => exports.getItemIcon(item)
+
+exports.getNextPagePath = (data, currentQuery, currentPath) => {
+  if (!data.paging || !data.paging.cursors) {
+    return null
+  }
+
+  const query = Object.assign({}, currentQuery, {
+    cursor: data.paging.cursors.after
+  })
+  return `${currentPath || ''}?${querystring.stringify(query)}`
+}

+ 154 - 0
packages/@uppy/companion/src/server/provider/instagram/graph/index.js

@@ -0,0 +1,154 @@
+const Provider = require('../../Provider')
+
+const request = require('request')
+const purest = require('purest')({ request })
+const utils = require('../../../helpers/utils')
+const logger = require('../../../logger')
+const adapter = require('./adapter')
+const AuthError = require('../../error')
+
+class Instagram extends Provider {
+  constructor (options) {
+    super(options)
+    this.authProvider = options.provider = Instagram.authProvider
+    this.client = purest(options)
+  }
+
+  static getExtraConfig () {
+    return {
+      protocol: 'https',
+      scope: ['user_profile', 'user_media']
+    }
+  }
+
+  static get authProvider () {
+    return 'instagram'
+  }
+
+  list ({ directory, token, query = {} }, done) {
+    const qs = {
+      fields: 'id,media_type,thumbnail_url,media_url,timestamp,children{media_type,media_url,thumbnail_url,timestamp}'
+    }
+
+    if (query.cursor) {
+      qs.after = query.cursor
+    }
+
+    this.client
+      .get('https://graph.instagram.com/me/media')
+      .qs(qs)
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.instagram.list.error')
+          return done(err)
+        } else {
+          this._getUsername(token, (err, username) => {
+            err ? done(err) : done(null, this.adaptData(body, username, directory, query))
+          })
+        }
+      })
+  }
+
+  _getUsername (token, done) {
+    this.client
+      .get('https://graph.instagram.com/me')
+      .qs({ fields: 'username' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.instagram.user.error')
+          return done(err)
+        } else {
+          done(null, body.username)
+        }
+      })
+  }
+
+  download ({ id, token }, onData) {
+    return this.client
+      .get(`https://graph.instagram.com/${id}`)
+      .qs({ fields: 'media_url' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err) return logger.error(err, 'provider.instagram.download.error')
+        request(body.media_url)
+          .on('data', onData)
+          .on('end', () => onData(null))
+          .on('error', (err) => {
+            logger.error(err, 'provider.instagram.download.url.error')
+          })
+      })
+  }
+
+  thumbnail (_, done) {
+    // not implementing this because a public thumbnail from instagram will be used instead
+    const err = new Error('call to thumbnail is not implemented')
+    logger.error(err, 'provider.instagram.thumbnail.error')
+    return done(err)
+  }
+
+  size ({ id, token }, done) {
+    return this.client
+      .get(`https://graph.instagram.com/${id}`)
+      .qs({ fields: 'media_url' })
+      .auth(token)
+      .request((err, resp, body) => {
+        if (err || resp.statusCode !== 200) {
+          err = this._error(err, resp)
+          logger.error(err, 'provider.instagram.size.error')
+          return done(err)
+        }
+
+        utils.getURLMeta(body.media_url)
+          .then(({ size }) => done(null, size))
+          .catch((err) => {
+            logger.error(err, 'provider.instagram.size.error')
+            done()
+          })
+      })
+  }
+
+  logout (_, done) {
+    // access revoke is not supported by Instagram's API
+    done(null, { revoked: false, manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/' })
+  }
+
+  adaptData (res, username, directory, currentQuery) {
+    const data = { username: username, items: [] }
+    const items = adapter.getItemSubList(res)
+    items.forEach((item, i) => {
+      data.items.push({
+        isFolder: adapter.isFolder(item),
+        icon: adapter.getItemIcon(item),
+        name: adapter.getItemName(item, i),
+        mimeType: adapter.getMimeType(item),
+        id: adapter.getItemId(item),
+        thumbnail: adapter.getItemThumbnailUrl(item),
+        requestPath: adapter.getItemRequestPath(item),
+        modifiedDate: adapter.getItemModifiedDate(item)
+      })
+    })
+
+    data.nextPagePath = adapter.getNextPagePath(res, currentQuery, directory)
+    return data
+  }
+
+  _error (err, resp) {
+    if (resp) {
+      if (resp.body && resp.body.error.code === 190) {
+        // Invalid OAuth 2.0 Access Token
+        return new AuthError()
+      }
+
+      const msg = resp.body && resp.body.error ? resp.body.error.message : ''
+      return new Error(`request to ${this.authProvider} returned status: ${resp.statusCode}, message: ${msg}`)
+    }
+
+    return err
+  }
+}
+
+module.exports = Instagram

+ 4 - 1
packages/@uppy/companion/src/server/provider/instagram/index.js

@@ -1,3 +1,5 @@
+const Provider = require('../Provider')
+
 const request = require('request')
 const purest = require('purest')({ request })
 const utils = require('../../helpers/utils')
@@ -5,8 +7,9 @@ const logger = require('../../logger')
 const adapter = require('./adapter')
 const AuthError = require('../error')
 
-class Instagram {
+class Instagram extends Provider {
   constructor (options) {
+    super(options)
     this.authProvider = options.provider = Instagram.authProvider
     this.client = purest(options)
   }

+ 4 - 1
packages/@uppy/companion/src/server/provider/onedrive/index.js

@@ -1,11 +1,14 @@
+const Provider = require('../Provider')
+
 const request = require('request')
 const purest = require('purest')({ request })
 const logger = require('../../logger')
 const adapter = require('./adapter')
 const AuthError = require('../error')
 
-class OneDrive {
+class OneDrive extends Provider {
   constructor (options) {
+    super(options)
     this.authProvider = options.provider = OneDrive.authProvider
     this.client = purest(options)
   }

+ 35 - 2
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -1,13 +1,14 @@
 /* global jest:false, test:false, expect:false, describe:false, beforeEach:false */
 
 const providerManager = require('../../src/server/provider')
+const { getCompanionOptions } = require('../../src/standalone/helper')
 let grantConfig
 let companionOptions
 
 describe('Test Provider options', () => {
   beforeEach(() => {
     grantConfig = require('../../src/config/grant')()
-    companionOptions = require('../../src/standalone/helper').getCompanionOptions()
+    companionOptions = getCompanionOptions()
   })
 
   test('adds provider options', () => {
@@ -22,12 +23,44 @@ describe('Test Provider options', () => {
     expect(grantConfig.instagram.secret).toBe('instagram_secret')
   })
 
+  test('adds extra provider config', () => {
+    process.env.COMPANION_INSTAGRAM_KEY = '123456'
+    providerManager.addProviderOptions(getCompanionOptions(), grantConfig)
+    expect(grantConfig.instagram).toEqual({
+      transport: 'session',
+      callback: '/instagram/callback',
+      key: '123456',
+      secret: 'instagram_secret',
+      protocol: 'https',
+      scope: ['user_profile', 'user_media']
+    })
+
+    expect(grantConfig.dropbox).toEqual({
+      key: 'dropbox_key',
+      secret: 'dropbox_secret',
+      transport: 'session',
+      authorize_url: 'https://www.dropbox.com/oauth2/authorize',
+      access_url: 'https://api.dropbox.com/oauth2/token',
+      callback: '/dropbox/callback'
+    })
+
+    expect(grantConfig.google).toEqual({
+      key: 'google_key',
+      secret: 'google_secret',
+      transport: 'session',
+      scope: [
+        'https://www.googleapis.com/auth/drive.readonly'
+      ],
+      callback: '/drive/callback'
+    })
+  })
+
   test('adds provider options for secret files', () => {
     process.env.COMPANION_DROPBOX_SECRET_FILE = process.env.PWD + '/test/resources/dropbox_secret_file'
     process.env.COMPANION_GOOGLE_SECRET_FILE = process.env.PWD + '/test/resources/google_secret_file'
     process.env.COMPANION_INSTAGRAM_SECRET_FILE = process.env.PWD + '/test/resources/instagram_secret_file'
 
-    companionOptions = require('../../src/standalone/helper').getCompanionOptions()
+    companionOptions = getCompanionOptions()
 
     providerManager.addProviderOptions(companionOptions, grantConfig)
 

+ 1 - 1
packages/@uppy/companion/test/mockserver.js

@@ -12,7 +12,7 @@ authServer.all('*/callback', (req, res, next) => {
   next()
 })
 authServer.all('/drive/send-token', (req, res, next) => {
-  req.session.grant = { state: req.query.state || 'non-empty-value' }
+  req.session.grant = { dynamic: { state: req.query.state || 'non-empty-value' } }
   next()
 })