all changes squashed

This commit is contained in:
Prakash 2026-05-21 21:17:09 +05:30
parent 622daa498c
commit 937b5ecdb9
7 changed files with 129 additions and 0 deletions

View file

@ -222,6 +222,21 @@ export function app(optionsArg: CompanionInitOptions) {
controllers.sendToken,
)
// Returns the decrypted Dropbox OAuth tokens for the current session so a client
// can drive a server-side /dropbox/import Assembly. Deliberately weakens the
// encrypted-token posture, so it is off unless explicitly enabled. The route keeps
// the :providerName segment because verifyToken and the provider middleware are
// keyed on it; the controller restricts the result to Dropbox.
if (options.enableDropboxTokensEndpoint) {
app.get(
'/:providerName/bridge-tokens',
middlewares.hasSessionAndProvider,
middlewares.hasOAuthProvider,
middlewares.verifyToken,
controllers.bridgeTokens,
)
}
app.post(
'/:providerName/simple-auth',
express.json(),

View file

@ -24,6 +24,7 @@ export const defaultOptions = {
},
enableUrlEndpoint: false,
enableGooglePickerEndpoint: false,
enableDropboxTokensEndpoint: false,
allowLocalUrls: false,
periodicPingUrls: defaultPeriodicPingUrls,
streamingUpload: true,

View file

@ -54,6 +54,12 @@ export interface CompanionInitOptions {
sendSelfEndpoint?: string | undefined
enableUrlEndpoint?: boolean | undefined
enableGooglePickerEndpoint?: boolean | undefined
/**
* Exposes the opt decrypted Dropbox OAuth tokens for the current session via
* `GET /dropbox/bridge-tokens`. This deliberately returns raw OAuth tokens to the
* client.
*/
enableDropboxTokensEndpoint?: boolean | undefined
metrics?: boolean | undefined
periodicPingUrls?: string[] | undefined
periodicPingInterval?: number | undefined

View file

@ -0,0 +1,33 @@
import type { Request, Response } from 'express'
/**
* Returns the decrypted Dropbox OAuth tokens for the current session.
**/
export default function bridgeTokens(req: Request, res: Response): void {
// Responses on this route may carry OAuth tokens — never let them be cached.
res.set('Cache-Control', 'no-store')
// The route is `/:providerName/bridge-tokens` because `verifyToken` and the
// provider middleware are keyed on `:providerName`. A token minted for another
// provider cannot be read here: `verifyToken` decrypts against this same param,
// so a mismatched path is rejected by the middleware before reaching this point.
if (req.params['providerName'] !== 'dropbox') {
res
.status(400)
.json({ error: 'The bridge-tokens endpoint only supports Dropbox.' })
return
}
const session = req.companion.providerUserSession
if (session?.accessToken == null) {
// The auth token decrypted fine, but this session has no connected Dropbox
// account — that is "not found", not an authentication failure.
res.status(404).json({ error: 'No Dropbox tokens for this session.' })
return
}
res.json({
accessToken: session.accessToken,
refreshToken: session.refreshToken ?? null,
})
}

View file

@ -1,3 +1,4 @@
export { default as bridgeTokens } from './bridge-tokens.js'
export { default as callback } from './callback.js'
export { default as connect } from './connect.js'
export { default as deauthorizationCallback } from './deauth-callback.js'

View file

@ -94,6 +94,7 @@ type StandaloneCompanionOptions = Pick<
| 's3'
| 'enableUrlEndpoint'
| 'enableGooglePickerEndpoint'
| 'enableDropboxTokensEndpoint'
| 'periodicPingUrls'
| 'periodicPingInterval'
| 'periodicPingStaticPayload'
@ -190,6 +191,8 @@ const getConfigFromEnv = (): StandaloneCompanionOptions => {
enableUrlEndpoint: process.env['COMPANION_ENABLE_URL_ENDPOINT'] === 'true',
enableGooglePickerEndpoint:
process.env['COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT'] === 'true',
enableDropboxTokensEndpoint:
process.env['COMPANION_ENABLE_DROPBOX_TOKENS_ENDPOINT'] === 'true',
periodicPingUrls: process.env['COMPANION_PERIODIC_PING_URLS']
? process.env['COMPANION_PERIODIC_PING_URLS'].split(',')
: [],

View file

@ -0,0 +1,70 @@
import request from 'supertest'
import { describe, expect, test, vi } from 'vitest'
import * as tokenService from '../src/server/helpers/jwt.js'
import { getServer } from './mockserver.js'
// getServer is called once per test; without this the prom-client metric
// registered inside the metrics middleware collides on the second server.
vi.mock('express-prom-bundle')
// Matches COMPANION_SECRET in test/mockserver.ts defaultEnv.
const secret = 'secret'
const enabledEnv = { COMPANION_ENABLE_DROPBOX_TOKENS_ENDPOINT: 'true' }
const disabledEnv = { COMPANION_ENABLE_DROPBOX_TOKENS_ENDPOINT: 'false' }
describe('GET /:providerName/bridge-tokens', () => {
test('returns the dropbox tokens, marked no-store, for a valid token', async () => {
const token = tokenService.generateEncryptedAuthToken(
{ dropbox: { accessToken: 'acc-123', refreshToken: 'ref-456' } },
secret,
)
return request(await getServer(enabledEnv))
.get('/dropbox/bridge-tokens')
.set('uppy-auth-token', token)
.expect('Cache-Control', 'no-store')
.expect(200)
.expect((res) => {
expect(res.body).toEqual({
accessToken: 'acc-123',
refreshToken: 'ref-456',
})
})
})
test('responds 404 when the Dropbox session has no access token', async () => {
// The auth token decrypts fine, but holds no usable Dropbox access token.
const token = tokenService.generateEncryptedAuthToken(
{ dropbox: {} },
secret,
)
return request(await getServer(enabledEnv))
.get('/dropbox/bridge-tokens')
.set('uppy-auth-token', token)
.expect(404)
})
test('rejects non-dropbox providers with 400', async () => {
const token = tokenService.generateEncryptedAuthToken(
{ drive: { accessToken: 'acc-123' } },
secret,
)
return request(await getServer(enabledEnv))
.get('/drive/bridge-tokens')
.set('uppy-auth-token', token)
.expect(400)
})
test('responds 401 when no auth token is provided', async () => {
return request(await getServer(enabledEnv))
.get('/dropbox/bridge-tokens')
.expect(401)
})
test('responds 404 when the option is disabled', async () => {
return request(await getServer(disabledEnv))
.get('/dropbox/bridge-tokens')
.set('uppy-auth-token', 'whatever')
.expect(404)
})
})