@vercel/oidc 3.8.10 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @vercel/oidc
2
2
 
3
+ ## 4.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - d032cfb: Add experimental Connect event delivery authentication scoped to project, environment, registered path, preview branch and delivery purpose, with envelope schema validation. Distinguish OIDC identity failures from temporary signing-key outages.
8
+
9
+ Preserve optional MCP event metadata and opaque cursors within the total delivery body limit.
10
+
11
+ ## 4.0.0
12
+
13
+ ### Major Changes
14
+
15
+ - ec76b68: Read the Vercel OIDC token from the file specified by `VERCEL_OIDC_TOKEN_FILE` on each call.
16
+
3
17
  ## 3.8.10
4
18
 
5
19
  ### Patch Changes
@@ -48,18 +48,20 @@ export type GetVercelOidcTokenOptions = (GetVercelOidcTokenBaseOptions & {
48
48
  skipCache?: never;
49
49
  });
50
50
  /**
51
- * Gets the current OIDC token from the request context or the environment variable.
51
+ * Gets the current OIDC token from the request context, the Vercel OIDC token
52
+ * file, or the environment variable.
52
53
  *
53
54
  * Do not cache this value, as it is subject to change in production!
54
55
  *
55
- * This function is used to retrieve the OIDC token from the request context or the environment variable.
56
- * It checks for the `x-vercel-oidc-token` header in the request context and falls back to the `VERCEL_OIDC_TOKEN` environment variable if the header is not present.
56
+ * It checks for the `x-vercel-oidc-token` header in the request context, then
57
+ * reads the file named by `VERCEL_OIDC_TOKEN_FILE` when set, then falls back to
58
+ * the `VERCEL_OIDC_TOKEN` environment variable.
57
59
  *
58
60
  * Unlike the `getVercelOidcTokenSync` function, this function will refresh the token if it is expired in a development environment.
59
61
  *
60
62
  * @param {GetVercelOidcTokenOptions} [options] - Optional configuration for token retrieval.
61
63
  * @returns {Promise<string>} A promise that resolves to the OIDC token.
62
- * @throws {Error} If the `x-vercel-oidc-token` header is missing from the request context and the environment variable `VERCEL_OIDC_TOKEN` is not set. If the token
64
+ * @throws {Error} If no token source is available or the configured file cannot be read. If the token
63
65
  * is expired in a development environment, will also throw an error if the token cannot be refreshed: no CLI credentials are available, CLI credentials are expired, no project configuration is available
64
66
  * or the token refresh request fails.
65
67
  *
@@ -31,14 +31,17 @@ __export(get_vercel_oidc_token_with_refresh_exports, {
31
31
  getVercelOidcToken: () => getVercelOidcToken
32
32
  });
33
33
  module.exports = __toCommonJS(get_vercel_oidc_token_with_refresh_exports);
34
+ var import_promises = require("node:fs/promises");
34
35
  var import_exchange_vercel_oidc_token = require("./exchange-vercel-oidc-token");
36
+ var import_get_context = require("./get-context");
35
37
  var import_get_vercel_oidc_token_sync = require("./get-vercel-oidc-token-sync");
36
38
  var import_token_error = require("./token-error");
37
39
  async function getVercelOidcToken(options) {
38
40
  let token = "";
39
41
  let err;
42
+ const tokenFromFile = (0, import_get_context.getContext)().headers?.["x-vercel-oidc-token"] ? null : await tryOidcTokenFromFile();
40
43
  try {
41
- token = (0, import_get_vercel_oidc_token_sync.getVercelOidcTokenSync)();
44
+ token = tokenFromFile ?? (0, import_get_vercel_oidc_token_sync.getVercelOidcTokenSync)();
42
45
  } catch (error) {
43
46
  err = error;
44
47
  }
@@ -72,6 +75,19 @@ ${error.message}`;
72
75
  }
73
76
  return token;
74
77
  }
78
+ async function tryOidcTokenFromFile() {
79
+ const tokenFile = process.env.VERCEL_OIDC_TOKEN_FILE;
80
+ if (!tokenFile) {
81
+ return null;
82
+ }
83
+ const token = (await (0, import_promises.readFile)(tokenFile, "utf8")).trim();
84
+ if (!token) {
85
+ throw new import_token_error.VercelOidcTokenError(
86
+ `The Vercel OIDC token file is empty: ${tokenFile}`
87
+ );
88
+ }
89
+ return token;
90
+ }
75
91
  // Annotate the CommonJS export names for ESM import in node:
76
92
  0 && (module.exports = {
77
93
  getVercelOidcToken
@@ -30,6 +30,9 @@ var import_token_io = require("./token-io");
30
30
  var import_cli_config = require("@vercel/cli-config");
31
31
  var import_get_vercel_oidc_token_with_refresh = require("./get-vercel-oidc-token-with-refresh");
32
32
  var tokenUtil = __toESM(require("./token-util"));
33
+ const requestHeaders = import_vitest.vi.hoisted(() => ({
34
+ current: {}
35
+ }));
33
36
  import_vitest.vi.mock("@vercel/cli-config", async (importOriginal) => ({
34
37
  ...await importOriginal(),
35
38
  getGlobalPathConfig: import_vitest.vi.fn(),
@@ -42,7 +45,7 @@ import_vitest.vi.mock("@vercel/cli-exec", () => ({
42
45
  }));
43
46
  import_vitest.vi.mock("./token-io");
44
47
  import_vitest.vi.mock("./get-context", () => ({
45
- getContext: () => ({ headers: {} })
48
+ getContext: () => ({ headers: requestHeaders.current })
46
49
  }));
47
50
  (0, import_vitest.describe)("getVercelOidcToken - Error Scenarios", () => {
48
51
  let rootDir;
@@ -54,7 +57,9 @@ import_vitest.vi.mock("./get-context", () => ({
54
57
  (0, import_vitest.beforeEach)(() => {
55
58
  import_vitest.vi.clearAllMocks();
56
59
  import_vitest.vi.restoreAllMocks();
60
+ requestHeaders.current = {};
57
61
  process.env.VERCEL_OIDC_TOKEN = void 0;
62
+ delete process.env.VERCEL_OIDC_TOKEN_FILE;
58
63
  const random = `test-${(0, import_crypto.randomUUID)()}`;
59
64
  rootDir = path.join(os.tmpdir(), random);
60
65
  userDataDir = path.join(rootDir, "data");
@@ -198,6 +203,124 @@ import_vitest.vi.mock("./get-context", () => ({
198
203
  const token = await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)();
199
204
  (0, import_vitest.expect)(token).toBe(validToken);
200
205
  });
206
+ (0, import_vitest.test)("reads the Vercel OIDC token file on each call after rotation", async () => {
207
+ delete process.env.VERCEL_OIDC_TOKEN;
208
+ const tokenFile = path.join(rootDir, "oidc-token");
209
+ process.env.VERCEL_OIDC_TOKEN_FILE = tokenFile;
210
+ const firstToken = createValidToken("first");
211
+ const secondToken = createValidToken("second");
212
+ fs.writeFileSync(tokenFile, `${firstToken}
213
+ `);
214
+ (0, import_vitest.expect)(await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).toBe(firstToken);
215
+ fs.writeFileSync(tokenFile, `${secondToken}
216
+ `);
217
+ (0, import_vitest.expect)(await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).toBe(secondToken);
218
+ });
219
+ (0, import_vitest.test)("prefers the advertised file over a static environment token", async () => {
220
+ process.env.VERCEL_OIDC_TOKEN = createValidToken("stale");
221
+ const tokenFile = path.join(rootDir, "oidc-token");
222
+ process.env.VERCEL_OIDC_TOKEN_FILE = tokenFile;
223
+ const currentToken = createValidToken("current");
224
+ fs.writeFileSync(tokenFile, currentToken);
225
+ const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch");
226
+ (0, import_vitest.expect)(
227
+ await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)({
228
+ team: teamId,
229
+ project: projectId,
230
+ expirationBufferMs: 6e4
231
+ })
232
+ ).toBe(currentToken);
233
+ (0, import_vitest.expect)(fetchSpy).not.toHaveBeenCalled();
234
+ });
235
+ (0, import_vitest.test)("keeps the request token ahead of the advertised file", async () => {
236
+ const requestToken = createValidToken("request");
237
+ requestHeaders.current = { "x-vercel-oidc-token": requestToken };
238
+ process.env.VERCEL_OIDC_TOKEN_FILE = path.join(rootDir, "missing-token");
239
+ const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch");
240
+ (0, import_vitest.expect)(
241
+ await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)({
242
+ team: teamId,
243
+ project: projectId,
244
+ expirationBufferMs: 6e4
245
+ })
246
+ ).toBe(requestToken);
247
+ (0, import_vitest.expect)(fetchSpy).not.toHaveBeenCalled();
248
+ });
249
+ (0, import_vitest.test)("includes the file path when the Vercel OIDC token file is empty", async () => {
250
+ delete process.env.VERCEL_OIDC_TOKEN;
251
+ const tokenFile = path.join(rootDir, "oidc-token");
252
+ process.env.VERCEL_OIDC_TOKEN_FILE = tokenFile;
253
+ fs.writeFileSync(tokenFile, " \n");
254
+ await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(
255
+ `The Vercel OIDC token file is empty: ${tokenFile}`
256
+ );
257
+ });
258
+ (0, import_vitest.test)("rejects a missing advertised token file instead of using the environment token", async () => {
259
+ process.env.VERCEL_OIDC_TOKEN = createValidToken("environment");
260
+ process.env.VERCEL_OIDC_TOKEN_FILE = path.join(rootDir, "missing-token");
261
+ await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow("ENOENT");
262
+ });
263
+ (0, import_vitest.test)("refreshes an expired file token using the provided team and project", async () => {
264
+ delete process.env.VERCEL_OIDC_TOKEN;
265
+ const tokenFile = path.join(rootDir, "oidc-token");
266
+ process.env.VERCEL_OIDC_TOKEN_FILE = tokenFile;
267
+ fs.writeFileSync(tokenFile, createExpiredToken());
268
+ fs.writeFileSync(
269
+ path.join(cliDataDir, "auth.json"),
270
+ JSON.stringify({ token: "test-auth-token" })
271
+ );
272
+ const refreshedToken = createValidToken("refreshed");
273
+ const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch").mockResolvedValue(
274
+ new Response(JSON.stringify({ token: refreshedToken }))
275
+ );
276
+ (0, import_vitest.expect)(await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)({ team: teamId, project: projectId })).toBe(
277
+ refreshedToken
278
+ );
279
+ (0, import_vitest.expect)(fetchSpy).toHaveBeenCalledWith(
280
+ `https://api.vercel.com/v1/projects/${projectId}/token?source=vercel-oidc-refresh&teamId=${teamId}`,
281
+ import_vitest.expect.objectContaining({ method: "POST" })
282
+ );
283
+ });
284
+ (0, import_vitest.test)("refreshes a file token within the expiration buffer before audience exchange", async () => {
285
+ delete process.env.VERCEL_OIDC_TOKEN;
286
+ const tokenFile = path.join(rootDir, "oidc-token");
287
+ process.env.VERCEL_OIDC_TOKEN_FILE = tokenFile;
288
+ fs.writeFileSync(
289
+ tokenFile,
290
+ createValidToken("near-expiry", Math.floor(Date.now() / 1e3) + 30)
291
+ );
292
+ fs.writeFileSync(
293
+ path.join(cliDataDir, "auth.json"),
294
+ JSON.stringify({ token: "test-auth-token" })
295
+ );
296
+ const refreshedToken = createValidToken("refreshed");
297
+ const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
298
+ new Response(JSON.stringify({ token: refreshedToken }))
299
+ ).mockResolvedValueOnce(
300
+ new Response(JSON.stringify({ token: "audience-specific-token" }))
301
+ );
302
+ (0, import_vitest.expect)(
303
+ await (0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)({
304
+ team: teamId,
305
+ project: projectId,
306
+ expirationBufferMs: 6e4,
307
+ audience: "custom-audience",
308
+ jti: "custom-jti",
309
+ skipCache: true
310
+ })
311
+ ).toBe("audience-specific-token");
312
+ (0, import_vitest.expect)(fetchSpy).toHaveBeenCalledWith(
313
+ "https://oidc.vercel.com/~token",
314
+ import_vitest.expect.objectContaining({
315
+ method: "POST",
316
+ body: JSON.stringify({
317
+ token: refreshedToken,
318
+ aud: "custom-audience",
319
+ jti: "custom-jti"
320
+ })
321
+ })
322
+ );
323
+ });
201
324
  (0, import_vitest.test)("should refresh when token is expired but all configs are valid", async () => {
202
325
  fs.writeFileSync(
203
326
  path.join(cliDataDir, "auth.json"),
@@ -356,14 +479,14 @@ function createExpiredToken() {
356
479
  ).toString("base64url");
357
480
  return `${header}.${payload}.fake_signature`;
358
481
  }
359
- function createValidToken(value = "valid-token") {
482
+ function createValidToken(value = "valid-token", expiresAt = Math.floor(Date.now() / 1e3) + 43200) {
360
483
  const header = Buffer.from(
361
484
  JSON.stringify({ alg: "RS256", typ: "JWT" })
362
485
  ).toString("base64url");
363
486
  const payload = Buffer.from(
364
487
  JSON.stringify({
365
488
  sub: "test-sub",
366
- exp: Math.floor(Date.now() / 1e3) + 43200,
489
+ exp: expiresAt,
367
490
  iat: Math.floor(Date.now() / 1e3)
368
491
  })
369
492
  ).toString("base64url");
@@ -28,6 +28,13 @@ const VERCEL_OIDC_JWKS_URL = new URL(
28
28
  );
29
29
  const DEFAULT_ALGORITHMS = ["RS256"];
30
30
  const VERCEL_OIDC_JWKS = (0, import_jose.createRemoteJWKSet)(VERCEL_OIDC_JWKS_URL);
31
+ class VercelOidcTokenValidationError extends TypeError {
32
+ constructor(message) {
33
+ super(message);
34
+ this.code = "ERR_VERCEL_OIDC_TOKEN_VALIDATION";
35
+ this.name = "VercelOidcTokenValidationError";
36
+ }
37
+ }
31
38
  async function verifyVercelOidcToken(token, options) {
32
39
  const {
33
40
  algorithms,
@@ -72,7 +79,7 @@ function hasAudienceVerification(audience) {
72
79
  }
73
80
  function validateIssuer(actual) {
74
81
  if (actual !== VERCEL_OIDC_ISSUER && (typeof actual !== "string" || !actual.startsWith(`${VERCEL_OIDC_ISSUER}/`))) {
75
- throw new TypeError(
82
+ throw new VercelOidcTokenValidationError(
76
83
  `Expected Vercel OIDC token iss claim to be "${VERCEL_OIDC_ISSUER}" or to start with "${VERCEL_OIDC_ISSUER}/".`
77
84
  );
78
85
  }
@@ -96,7 +103,7 @@ function validateClaim({
96
103
  return;
97
104
  }
98
105
  if (actual !== expected) {
99
- throw new TypeError(
106
+ throw new VercelOidcTokenValidationError(
100
107
  Array.isArray(expected) ? `Expected Vercel OIDC token ${claim} claim to be one of: ${expected.map((value) => `"${value}"`).join(", ")}.` : `Expected Vercel OIDC token ${claim} claim to be "${expected}".`
101
108
  );
102
109
  }
@@ -110,7 +117,7 @@ function validateOptionalClaim({
110
117
  return;
111
118
  }
112
119
  if (actual !== expected) {
113
- throw new TypeError(
120
+ throw new VercelOidcTokenValidationError(
114
121
  `Expected Vercel OIDC token ${claim} claim to be "${expected}".`
115
122
  );
116
123
  }
@@ -227,6 +227,17 @@ import_vitest.vi.mock("jose", () => ({
227
227
  "Expected VERCEL_PROJECT_ID to be set or projectId to be provided. Pass projectId: '*' to allow any project_id claim."
228
228
  );
229
229
  });
230
+ (0, import_vitest.test)("distinguishes token identity failures from signing-key outages", async () => {
231
+ mockVerifiedPayload({ project_id: "prj_other" });
232
+ const failure = (0, import_verify_vercel_oidc_token.verifyVercelOidcToken)("token");
233
+ await (0, import_vitest.expect)(failure).rejects.toBeInstanceOf(TypeError);
234
+ await (0, import_vitest.expect)(failure).rejects.toMatchObject({
235
+ code: "ERR_VERCEL_OIDC_TOKEN_VALIDATION"
236
+ });
237
+ const outage = new TypeError("fetch failed");
238
+ import_vitest.vi.mocked(import_jose.jwtVerify).mockRejectedValueOnce(outage);
239
+ await (0, import_vitest.expect)((0, import_verify_vercel_oidc_token.verifyVercelOidcToken)("token")).rejects.toBe(outage);
240
+ });
230
241
  (0, import_vitest.test)("rejects a token from a different owner", async () => {
231
242
  mockVerifiedPayload({
232
243
  owner_id: "team_other"
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.8.10";
1
+ export declare const version = "4.0.1";
package/dist/version.js CHANGED
@@ -21,7 +21,7 @@ __export(version_exports, {
21
21
  version: () => version
22
22
  });
23
23
  module.exports = __toCommonJS(version_exports);
24
- const version = "3.8.10";
24
+ const version = "4.0.1";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -6,14 +6,16 @@
6
6
 
7
7
  > **getVercelOidcToken**(`options?`): [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<`string`\>
8
8
 
9
- Defined in: [packages/oidc/src/get-vercel-oidc-token-with-refresh.ts:96](https://github.com/vercel/vercel/blob/main/packages/oidc/src/get-vercel-oidc-token-with-refresh.ts#L96)
9
+ Defined in: [packages/oidc/src/get-vercel-oidc-token-with-refresh.ts:100](https://github.com/vercel/vercel/blob/main/packages/oidc/src/get-vercel-oidc-token-with-refresh.ts#L100)
10
10
 
11
- Gets the current OIDC token from the request context or the environment variable.
11
+ Gets the current OIDC token from the request context, the Vercel OIDC token
12
+ file, or the environment variable.
12
13
 
13
14
  Do not cache this value, as it is subject to change in production!
14
15
 
15
- This function is used to retrieve the OIDC token from the request context or the environment variable.
16
- It checks for the `x-vercel-oidc-token` header in the request context and falls back to the `VERCEL_OIDC_TOKEN` environment variable if the header is not present.
16
+ It checks for the `x-vercel-oidc-token` header in the request context, then
17
+ reads the file named by `VERCEL_OIDC_TOKEN_FILE` when set, then falls back to
18
+ the `VERCEL_OIDC_TOKEN` environment variable.
17
19
 
18
20
  Unlike the `getVercelOidcTokenSync` function, this function will refresh the token if it is expired in a development environment.
19
21
 
@@ -33,7 +35,7 @@ A promise that resolves to the OIDC token.
33
35
 
34
36
  ## Throws
35
37
 
36
- If the `x-vercel-oidc-token` header is missing from the request context and the environment variable `VERCEL_OIDC_TOKEN` is not set. If the token
38
+ If no token source is available or the configured file cannot be read. If the token
37
39
  is expired in a development environment, will also throw an error if the token cannot be refreshed: no CLI credentials are available, CLI credentials are expired, no project configuration is available
38
40
  or the token refresh request fails.
39
41
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **verifyVercelOidcToken**\<`PayloadType`\>(`token`, `options?`): [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<`JWTVerifyResult`\<`PayloadType`\>\>
8
8
 
9
- Defined in: [packages/oidc/src/verify-vercel-oidc-token.ts:53](https://github.com/vercel/vercel/blob/main/packages/oidc/src/verify-vercel-oidc-token.ts#L53)
9
+ Defined in: [packages/oidc/src/verify-vercel-oidc-token.ts:63](https://github.com/vercel/vercel/blob/main/packages/oidc/src/verify-vercel-oidc-token.ts#L63)
10
10
 
11
11
  Verifies a Vercel OIDC token against Vercel's remote JWKS.
12
12
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **VercelOidcPayload** = `JWTPayload` & `object`
8
8
 
9
- Defined in: [packages/oidc/src/verify-vercel-oidc-token.ts:16](https://github.com/vercel/vercel/blob/main/packages/oidc/src/verify-vercel-oidc-token.ts#L16)
9
+ Defined in: [packages/oidc/src/verify-vercel-oidc-token.ts:26](https://github.com/vercel/vercel/blob/main/packages/oidc/src/verify-vercel-oidc-token.ts#L26)
10
10
 
11
11
  ## Type Declaration
12
12
 
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "require": "./dist/index.js"
19
19
  }
20
20
  },
21
- "version": "3.8.10",
21
+ "version": "4.0.1",
22
22
  "repository": {
23
23
  "directory": "packages/oidc",
24
24
  "type": "git",