diff --git a/.changeset/blob-use-oidc-package.md b/.changeset/blob-use-oidc-package.md new file mode 100644 index 000000000..c71098b0e --- /dev/null +++ b/.changeset/blob-use-oidc-package.md @@ -0,0 +1,5 @@ +--- +'@vercel/blob': patch +--- + +Read the Vercel OIDC token via the `@vercel/oidc` package (`getVercelOidcTokenSync`) instead of an inlined copy. This makes the dependency explicit and discoverable, and matches how other Vercel packages consume OIDC. Behavior is unchanged except for one edge case: a blank `x-vercel-oidc-token` request-context header now resolves to no token rather than falling back to `VERCEL_OIDC_TOKEN`. diff --git a/packages/blob/jest.config.cjs b/packages/blob/jest.config.cjs new file mode 100644 index 000000000..506f43674 --- /dev/null +++ b/packages/blob/jest.config.cjs @@ -0,0 +1,24 @@ +const path = require('node:path'); + +// `@vercel/oidc` pulls in `jose` transitively (via `verifyVercelOidcToken`, +// which Blob never calls). `jose` ships an ESM-only browser build that the +// jsdom and edge-runtime jest environments resolve and then fail to parse +// ("Unexpected token 'export'"), since jest doesn't transform node_modules. +// Blob doesn't use `jose` directly, so pin it to its CJS build in every test +// environment. Resolved via `@vercel/oidc` because `jose` isn't a direct +// dependency of this package under pnpm's strict layout. +const josePath = require.resolve('jose', { + paths: [path.dirname(require.resolve('@vercel/oidc'))], +}); + +/** @type {import('jest').Config} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testEnvironmentOptions: { + url: 'http://localhost:3000', + }, + moduleNameMapper: { + '^jose$': josePath, + }, +}; diff --git a/packages/blob/package.json b/packages/blob/package.json index cf3f00c92..5f9ad4b53 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -51,14 +51,8 @@ "test:node": "jest --env node .node.test.ts", "type-check": "tsc --noEmit" }, - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "testEnvironmentOptions": { - "url": "http://localhost:3000" - } - }, "dependencies": { + "@vercel/oidc": "^3.6.1", "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", diff --git a/packages/blob/src/vercel-oidc-token.node.test.ts b/packages/blob/src/vercel-oidc-token.node.test.ts index 79a53157b..d354cd1f4 100644 --- a/packages/blob/src/vercel-oidc-token.node.test.ts +++ b/packages/blob/src/vercel-oidc-token.node.test.ts @@ -41,7 +41,10 @@ describe('vercel-oidc-token', () => { expect(getVercelOidcToken()).toBe('jwt-from-request-context'); }); - it('getVercelOidcToken ignores empty request context header values', () => { + it('getVercelOidcToken returns undefined for a blank request context header', () => { + // @vercel/oidc selects the header over the env var as long as the header + // key is present, so a blank header resolves to undefined here (it does + // not fall back to VERCEL_OIDC_TOKEN). process.env.VERCEL_OIDC_TOKEN = 'jwt-from-env'; (globalThis as typeof globalThis & Record unknown }>)[ REQUEST_CONTEXT_SYMBOL @@ -53,6 +56,6 @@ describe('vercel-oidc-token', () => { }), }; - expect(getVercelOidcToken()).toBe('jwt-from-env'); + expect(getVercelOidcToken()).toBeUndefined(); }); }); diff --git a/packages/blob/src/vercel-oidc-token.ts b/packages/blob/src/vercel-oidc-token.ts index e30567fc6..1e1b95626 100644 --- a/packages/blob/src/vercel-oidc-token.ts +++ b/packages/blob/src/vercel-oidc-token.ts @@ -1,36 +1,22 @@ -type Context = { - headers?: Record; -}; - -const SYMBOL_FOR_REQ_CONTEXT = Symbol.for('@vercel/request-context'); - -const getContext = (): Context => { - const fromSymbol: typeof globalThis & { - [SYMBOL_FOR_REQ_CONTEXT]?: { get?: () => Context }; - } = globalThis; - - return fromSymbol[SYMBOL_FOR_REQ_CONTEXT]?.get?.() ?? {}; -}; - -function readEnv(name: string): string | undefined { - try { - const value = process.env[name]; - return typeof value === 'string' && value.trim() !== '' - ? value.trim() - : undefined; - } catch { - return undefined; - } -} +import { getVercelOidcTokenSync } from '@vercel/oidc'; /** - * Gets the current OIDC token from request context headers or environment. + * Gets the current OIDC token from the request context header or the + * `VERCEL_OIDC_TOKEN` env var, or `undefined` when none is set. + * + * Delegates to `@vercel/oidc`'s `getVercelOidcTokenSync`, which reads the + * `x-vercel-oidc-token` request-context header (falling back to + * `VERCEL_OIDC_TOKEN`) and throws when neither is present. We convert that + * throw to `undefined` so callers (see `resolveBlobAuth`) can fall through to + * a `BLOB_READ_WRITE_TOKEN`, and treat a blank token as absent. + * + * Do not cache this value, as it is subject to change in production! */ export function getVercelOidcToken(): string | undefined { - const tokenFromContext = getContext().headers?.['x-vercel-oidc-token']; - if (typeof tokenFromContext === 'string' && tokenFromContext.trim() !== '') { - return tokenFromContext.trim(); + try { + const token = getVercelOidcTokenSync().trim(); + return token === '' ? undefined : token; + } catch { + return undefined; } - - return readEnv('VERCEL_OIDC_TOKEN'); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5da689c8..fd47d1abe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,6 +38,9 @@ importers: packages/blob: dependencies: + '@vercel/oidc': + specifier: ^3.6.1 + version: 3.6.1 async-retry: specifier: ^1.3.3 version: 1.3.3 @@ -1672,6 +1675,17 @@ packages: cpu: [x64] os: [win32] + '@vercel/cli-config@0.2.0': + resolution: {integrity: sha512-fJRRRB7734BDuXZ89yBEaA2ncYhH7bWX30mk04W80J6VAfQc+4iB8lyzAdaGpFV3/vNlkt9VZt+/uoQoWX6UsQ==} + + '@vercel/cli-exec@0.1.1': + resolution: {integrity: sha512-LMRMEai3Z+BODyxGcU9+KiWrS/UElNiOLKiNRfGNt2Vu3NTEmXgFeXG9wBfocAnTe5yJCX/DY6k3k7S/LkPp/g==} + engines: {node: '>= 18'} + + '@vercel/oidc@3.6.1': + resolution: {integrity: sha512-8ipTFoiX3WBRrvXLjSrmgAiwtMDQk3EgSxe8N7v2rXBz39NBIIyoGXeVbJRoBcP8WEuVnvjvIQsggbGU7ZKrMw==} + engines: {node: '>= 20'} + acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} @@ -2619,6 +2633,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -2986,6 +3003,10 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + os-paths@4.4.0: + resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} + engines: {node: '>= 6.0'} + outdent@0.5.0: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} @@ -3665,6 +3686,14 @@ packages: utf-8-validate: optional: true + xdg-app-paths@5.5.1: + resolution: {integrity: sha512-hI3flOB4PLZIy5prbtTpirobtPE2ZtZ52szO+2mM9Efp6ErM398La+C1lIpNWDfNoQk+6Lsi6nMcCwVB7pxeMQ==} + engines: {node: '>= 6.0'} + + xdg-portable@7.3.0: + resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==} + engines: {node: '>= 6.0'} + xml-name-validator@5.0.0: resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} engines: {node: '>=18'} @@ -3700,6 +3729,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.1.11: + resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} + snapshots: '@alloc/quick-lru@5.2.0': {} @@ -5052,6 +5084,21 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vercel/cli-config@0.2.0': + dependencies: + xdg-app-paths: 5.5.1 + zod: 4.1.11 + + '@vercel/cli-exec@0.1.1': + dependencies: + execa: 5.1.1 + + '@vercel/oidc@3.6.1': + dependencies: + '@vercel/cli-config': 0.2.0 + '@vercel/cli-exec': 0.1.1 + jose: 5.10.0 + acorn-walk@8.2.0: {} acorn@8.11.3: {} @@ -6253,6 +6300,8 @@ snapshots: jiti@2.6.1: {} + jose@5.10.0: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -6583,6 +6632,8 @@ snapshots: dependencies: mimic-function: 5.0.1 + os-paths@4.4.0: {} + outdent@0.5.0: {} p-cancelable@4.0.1: {} @@ -7273,6 +7324,15 @@ snapshots: optionalDependencies: bufferutil: 4.0.8 + xdg-app-paths@5.5.1: + dependencies: + os-paths: 4.4.0 + xdg-portable: 7.3.0 + + xdg-portable@7.3.0: + dependencies: + os-paths: 4.4.0 + xml-name-validator@5.0.0: {} xmlchars@2.2.0: {} @@ -7298,3 +7358,5 @@ snapshots: yn@3.1.1: {} yocto-queue@0.1.0: {} + + zod@4.1.11: {}