Skip to content

@uppy/companion: configurable OAuth scope and customParams via providerOptions#6277

Open
rdinita wants to merge 1 commit into
transloadit:mainfrom
rdinita:feat/companion-configurable-oauth-scope
Open

@uppy/companion: configurable OAuth scope and customParams via providerOptions#6277
rdinita wants to merge 1 commit into
transloadit:mainfrom
rdinita:feat/companion-configurable-oauth-scope

Conversation

@rdinita
Copy link
Copy Markdown

@rdinita rdinita commented Apr 28, 2026

Fixes a recurring need where operators want to narrow OAuth scope per provider without forking or patching node_modules.

Problem

OAuth scopes are hardcoded in src/config/grant.js. providerOptions only carries key / secret / credentialsURL, so there is no API to override the scope. Concrete cases:

  • A Microsoft tenant whose security review rejects Files.Read.All (the consent prompt covers shared + SharePoint files which auditors flag) and only needs /me/drive (Files.Read).
  • Google Workspace integrations adding prompt: 'select_account' for multi-account UX without losing the existing access_type: 'offline' handshake.
  • Operators wanting to set login_hint per tenant.

Conceptually adjacent to #4793 (closed). The maintainer outcome there was to keep drive.readonly as the default but signal that operator-side configurability is the right path. This PR provides that path uniformly across providers, with no default changes.

Solution

Two optional fields on each per-provider entry of providerOptions:

companion.app({
  providerOptions: {
    drive: {
      key: '…', secret: '…',
      scope: ['https://www.googleapis.com/auth/drive.file'],
      customParams: { login_hint: 'user@example.com' },
    },
  },
})
  • scope: string[] replaces the OAuth scope.
  • customParams: Record<string, string> is shallow-merged onto the provider's default custom_params.

Both are validated (Array.isArray for scope; non-null, non-array object for customParams) so misconfiguration cannot crash companion.

The merge is placed after Object.assign(grantConfig[oauthProvider], provider.getExtraGrantConfig()), so user-supplied values beat provider-class defaults (covered by a new test that overrides Instagram's hardcoded scope). scope and custom_params are not in companion's per-request dynamic list, so the static merge is not shadowed.

When neither field is supplied, the new code paths are no-ops. Defaults match upstream.

Tests

4 new tests in provider-manager.test.js cover:

  • scope replacement (googledrive)
  • getExtraGrantConfig override (instagram)
  • customParams merge with defaults preserved (googledrive)
  • non-array scope and non-object customParams ignored

All 119 existing companion tests pass with the change in place. Verified with yarn workspace @uppy/companion test --run and yarn workspace @uppy/companion typecheck.

Notes

While running yarn test locally I hit a flaky failure in @uppy/dashboard's GlobalSearch.browser.test.ts > Search deep folder -> open it -> click ancestor breadcrumb (timed out finding getByRole('button', { name: 'second' })); same test passed on a rerun without code change. Independent of @uppy/companion. Happy to file a separate issue if useful.

README and a .changeset/ entry (minor, additive) included.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: 60e029f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@uppy/companion Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

…erOptions

OAuth scopes were hardcoded in grant.js for every provider — operators
with stricter consent-screen requirements (e.g. Microsoft tenants that
reject 'Files.Read.All' on procurement grounds, or compliance-driven
narrowing of Drive scope) had to fork or patch node_modules.

This adds two optional fields on each per-provider entry of providerOptions:

  - scope: string[]                  — replaces the default OAuth scope.
                                       Applied AFTER getExtraGrantConfig()
                                       so it also overrides scopes set by
                                       provider classes (e.g. Instagram).
  - customParams: Record<string, string> — shallow-merged onto the
                                       provider's default custom_params
                                       (e.g. Google's access_type:'offline'
                                       is preserved unless overridden).

Defaults are unchanged. Operators are responsible for verifying that any
narrowed scope still satisfies the listing/download paths their
integration uses (e.g. Drive's picker requires drive.readonly; narrowing
to drive.file returns an empty listing — see transloadit#4793 for the prior
maintainer discussion of this trade-off, which framed configurability
as the right path).

Tests:
  - 4 new tests in provider-manager.test.js covering scope replacement,
    getExtraGrantConfig override (Instagram), customParams merge with
    defaults preserved (googledrive), and validation of non-array /
    non-object inputs.
  - All 119 existing companion tests pass with the change in place.
@rdinita rdinita force-pushed the feat/companion-configurable-oauth-scope branch from 567d190 to 60e029f Compare April 28, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant