Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blob-use-oidc-package.md
Original file line number Diff line number Diff line change
@@ -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`.
24 changes: 24 additions & 0 deletions packages/blob/jest.config.cjs
Original file line number Diff line number Diff line change
@@ -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,
},
};
8 changes: 1 addition & 7 deletions packages/blob/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 5 additions & 2 deletions packages/blob/src/vercel-oidc-token.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<symbol, { get: () => unknown }>)[
REQUEST_CONTEXT_SYMBOL
Expand All @@ -53,6 +56,6 @@ describe('vercel-oidc-token', () => {
}),
};

expect(getVercelOidcToken()).toBe('jwt-from-env');
expect(getVercelOidcToken()).toBeUndefined();
});
});
46 changes: 16 additions & 30 deletions packages/blob/src/vercel-oidc-token.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
type Context = {
headers?: Record<string, string | undefined>;
};

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');
}
62 changes: 62 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading