@vercel/oidc 3.7.1 → 3.8.0

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,11 @@
1
1
  # @vercel/oidc
2
2
 
3
+ ## 3.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d29a8f9: Cache exchanged OIDC tokens in memory, keyed by a hash of the source token, audience, and jti, so repeated exchanges reuse the result instead of calling the token-exchange endpoint every time. Cached tokens are evicted when the API-provided expiry passes, and the cache is bounded with least-recently-used eviction to avoid unbounded growth. Add a `skipCache` option (surfaced as `skipTokenCache` on `awsCredentialsProvider`) to bypass the cache. `jti` and the cache-skip flags are now only accepted alongside an `audience`, since they only take effect during a token exchange.
8
+
3
9
  ## 3.7.1
4
10
 
5
11
  ### Patch Changes
@@ -5,6 +5,7 @@
5
5
  * @property {string} token - The token to exchange.
6
6
  * @property {string} audience - Optional audience to set on the exchanged token.
7
7
  * @property {string} jti - Optional JTI to set on the exchanged token.
8
+ * @property {boolean} skipCache - Optional flag to bypass the in-memory cache.
8
9
  */
9
10
  export interface ExchangeVercelOidcTokenOptions {
10
11
  /**
@@ -21,6 +22,12 @@ export interface ExchangeVercelOidcTokenOptions {
21
22
  * @default undefined
22
23
  */
23
24
  jti?: string;
25
+ /**
26
+ * When `true`, bypasses the in-memory exchange cache and performs a fresh
27
+ * token exchange. The freshly exchanged token still replaces any cached entry.
28
+ * @default false
29
+ */
30
+ skipCache?: boolean;
24
31
  }
25
32
  /**
26
33
  * Exchanges a Vercel OIDC token for a Vercel token with a custom audience.
@@ -29,6 +36,7 @@ export interface ExchangeVercelOidcTokenOptions {
29
36
  * @param {string} options.token - The token to exchange.
30
37
  * @param {string} options.audience - Optional audience to set on the exchanged token.
31
38
  * @param {string} options.jti - Optional JTI to set on the exchanged token.
39
+ * @param {boolean} options.skipCache - Optional flag to bypass the in-memory cache and force a fresh exchange.
32
40
  * @throws {Error} If the token exchange fails.
33
41
  * @returns {Promise<string>} A promise that resolves to the exchanged token.
34
42
  */
@@ -22,7 +22,66 @@ __export(exchange_vercel_oidc_token_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(exchange_vercel_oidc_token_exports);
24
24
  var import_version = require("./version");
25
+ class TokenCache {
26
+ constructor(maxEntries) {
27
+ this.maxEntries = maxEntries;
28
+ this.entries = /* @__PURE__ */ new Map();
29
+ }
30
+ /**
31
+ * Returns a cached token for the key when present and unexpired, refreshing
32
+ * its recency for LRU eviction. Expired entries are removed on access.
33
+ */
34
+ get(key) {
35
+ const entry = this.entries.get(key);
36
+ if (entry === void 0) {
37
+ return void 0;
38
+ }
39
+ if (entry.expiresAt <= Date.now()) {
40
+ this.entries.delete(key);
41
+ return void 0;
42
+ }
43
+ this.entries.delete(key);
44
+ this.entries.set(key, entry);
45
+ return entry.token;
46
+ }
47
+ /**
48
+ * Stores a token under the key and evicts the least-recently-used entries
49
+ * once the cache exceeds its size limit.
50
+ */
51
+ set({
52
+ key,
53
+ token,
54
+ expiresAt
55
+ }) {
56
+ this.entries.delete(key);
57
+ this.entries.set(key, { token, expiresAt });
58
+ while (this.entries.size > this.maxEntries) {
59
+ const oldest = this.entries.keys().next().value;
60
+ if (oldest === void 0) {
61
+ break;
62
+ }
63
+ this.entries.delete(oldest);
64
+ }
65
+ }
66
+ }
67
+ const MAX_CACHE_ENTRIES = 1e3;
68
+ const tokenCache = new TokenCache(MAX_CACHE_ENTRIES);
69
+ async function getCacheKey(options) {
70
+ const input = JSON.stringify([options.token, options.audience, options.jti]);
71
+ const digest = await crypto.subtle.digest(
72
+ "SHA-256",
73
+ new TextEncoder().encode(input)
74
+ );
75
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
76
+ }
25
77
  async function exchangeVercelOidcToken(options) {
78
+ const cacheKey = await getCacheKey(options);
79
+ if (!options.skipCache) {
80
+ const cached = tokenCache.get(cacheKey);
81
+ if (cached !== void 0) {
82
+ return cached;
83
+ }
84
+ }
26
85
  const response = await fetch("https://oidc.vercel.com/~token", {
27
86
  method: "POST",
28
87
  headers: {
@@ -52,7 +111,15 @@ async function exchangeVercelOidcToken(options) {
52
111
  "Failed to exchange token: response did not contain a token"
53
112
  );
54
113
  }
55
- return data.token;
114
+ const { token } = data;
115
+ const expiry = "expiry" in data && typeof data.expiry === "number" ? data.expiry : void 0;
116
+ if (expiry !== void 0) {
117
+ const expiresAt = expiry * 1e3;
118
+ if (expiresAt > Date.now()) {
119
+ tokenCache.set({ key: cacheKey, token, expiresAt });
120
+ }
121
+ }
122
+ return token;
56
123
  }
57
124
  async function readErrorMessage(response) {
58
125
  try {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (let key of __getOwnPropNames(from))
11
+ if (!__hasOwnProp.call(to, key) && key !== except)
12
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
13
+ }
14
+ return to;
15
+ };
16
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
17
+ // If the importer is in node compatibility mode or this is not an ESM
18
+ // file that has been converted to a CommonJS file using a Babel-
19
+ // compatible transform (i.e. "__esModule" has not been set), then set
20
+ // "default" to the CommonJS "module.exports" for node compatibility.
21
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
22
+ mod
23
+ ));
24
+ var import_vitest = require("vitest");
25
+ function jsonResponse(body, ok = true) {
26
+ return {
27
+ ok,
28
+ status: ok ? 200 : 400,
29
+ statusText: ok ? "OK" : "Bad Request",
30
+ json: async () => body
31
+ };
32
+ }
33
+ function futureExpiry(secondsFromNow = 3600) {
34
+ return Math.floor(Date.now() / 1e3) + secondsFromNow;
35
+ }
36
+ (0, import_vitest.describe)("exchangeVercelOidcToken", () => {
37
+ let fetchMock;
38
+ (0, import_vitest.beforeEach)(() => {
39
+ import_vitest.vi.resetModules();
40
+ fetchMock = import_vitest.vi.fn();
41
+ import_vitest.vi.stubGlobal("fetch", fetchMock);
42
+ });
43
+ (0, import_vitest.afterEach)(() => {
44
+ import_vitest.vi.unstubAllGlobals();
45
+ import_vitest.vi.useRealTimers();
46
+ });
47
+ async function loadExchange() {
48
+ const mod = await import("./exchange-vercel-oidc-token");
49
+ return mod.exchangeVercelOidcToken;
50
+ }
51
+ (0, import_vitest.it)("reuses the cached token when the API returns an expiry", async () => {
52
+ const exchange = await loadExchange();
53
+ fetchMock.mockResolvedValue(
54
+ jsonResponse({ token: "exchanged", expiry: futureExpiry() })
55
+ );
56
+ const first = await exchange({
57
+ token: "t",
58
+ audience: "https://a.example.com"
59
+ });
60
+ const second = await exchange({
61
+ token: "t",
62
+ audience: "https://a.example.com"
63
+ });
64
+ (0, import_vitest.expect)(first).toBe("exchanged");
65
+ (0, import_vitest.expect)(second).toBe("exchanged");
66
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(1);
67
+ });
68
+ (0, import_vitest.it)("does not cache when the API omits the expiry", async () => {
69
+ const exchange = await loadExchange();
70
+ fetchMock.mockResolvedValue(jsonResponse({ token: "exchanged" }));
71
+ await exchange({ token: "t", audience: "https://a.example.com" });
72
+ await exchange({ token: "t", audience: "https://a.example.com" });
73
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(2);
74
+ });
75
+ (0, import_vitest.it)("skipCache bypasses the cache but still refreshes it", async () => {
76
+ const exchange = await loadExchange();
77
+ fetchMock.mockResolvedValue(
78
+ jsonResponse({ token: "exchanged", expiry: futureExpiry() })
79
+ );
80
+ await exchange({ token: "t", audience: "https://a.example.com" });
81
+ await exchange({
82
+ token: "t",
83
+ audience: "https://a.example.com",
84
+ skipCache: true
85
+ });
86
+ await exchange({ token: "t", audience: "https://a.example.com" });
87
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(2);
88
+ });
89
+ (0, import_vitest.it)("re-exchanges once the cached token has expired", async () => {
90
+ import_vitest.vi.useFakeTimers();
91
+ import_vitest.vi.setSystemTime(/* @__PURE__ */ new Date("2025-01-01T00:00:00Z"));
92
+ const exchange = await loadExchange();
93
+ fetchMock.mockResolvedValue(
94
+ jsonResponse({ token: "exchanged", expiry: futureExpiry(60) })
95
+ );
96
+ await exchange({ token: "t", audience: "https://a.example.com" });
97
+ import_vitest.vi.advanceTimersByTime(61e3);
98
+ await exchange({ token: "t", audience: "https://a.example.com" });
99
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(2);
100
+ });
101
+ (0, import_vitest.it)("keys the cache by token, audience, and jti", async () => {
102
+ const exchange = await loadExchange();
103
+ fetchMock.mockResolvedValue(
104
+ jsonResponse({ token: "exchanged", expiry: futureExpiry() })
105
+ );
106
+ await exchange({ token: "t", audience: "https://a.example.com" });
107
+ await exchange({ token: "t", audience: "https://b.example.com" });
108
+ await exchange({ token: "t", audience: "https://a.example.com", jti: "x" });
109
+ await exchange({ token: "u", audience: "https://a.example.com" });
110
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(4);
111
+ });
112
+ (0, import_vitest.it)("evicts the least-recently-used entry beyond the size limit", async () => {
113
+ const exchange = await loadExchange();
114
+ fetchMock.mockResolvedValue(
115
+ jsonResponse({ token: "exchanged", expiry: futureExpiry() })
116
+ );
117
+ for (let i = 0; i <= 1e3; i++) {
118
+ await exchange({
119
+ token: `token-${i}`,
120
+ audience: "https://a.example.com"
121
+ });
122
+ }
123
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(1001);
124
+ await exchange({ token: "token-0", audience: "https://a.example.com" });
125
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(1002);
126
+ await exchange({ token: "token-1000", audience: "https://a.example.com" });
127
+ (0, import_vitest.expect)(fetchMock).toHaveBeenCalledTimes(1002);
128
+ });
129
+ });
@@ -1,8 +1,4 @@
1
- import { type ExchangeVercelOidcTokenOptions } from './exchange-vercel-oidc-token';
2
- /**
3
- * Options for getting the Vercel OIDC token.
4
- */
5
- export interface GetVercelOidcTokenOptions extends Omit<ExchangeVercelOidcTokenOptions, 'token'> {
1
+ interface GetVercelOidcTokenBaseOptions {
6
2
  /**
7
3
  * Optional team ID (team_*) or slug to use for token refresh.
8
4
  * When provided, this team will be used instead of reading from `.vercel/project.json`.
@@ -20,6 +16,37 @@ export interface GetVercelOidcTokenOptions extends Omit<ExchangeVercelOidcTokenO
20
16
  */
21
17
  expirationBufferMs?: number;
22
18
  }
19
+ /**
20
+ * Options for getting the Vercel OIDC token.
21
+ *
22
+ * `jti` and `skipCache` are only accepted when `audience` is provided, because
23
+ * they only take effect while exchanging the token for a custom audience —
24
+ * without an `audience` there is no exchange.
25
+ */
26
+ export type GetVercelOidcTokenOptions = (GetVercelOidcTokenBaseOptions & {
27
+ /**
28
+ * Audience to set on the exchanged token.
29
+ */
30
+ audience: string;
31
+ /**
32
+ * Optional JTI to set on the exchanged token.
33
+ * @default undefined
34
+ */
35
+ jti?: string;
36
+ /**
37
+ * When `true`, bypasses the in-memory exchange cache and performs a fresh
38
+ * token exchange.
39
+ * @default false
40
+ */
41
+ skipCache?: boolean;
42
+ }) | (GetVercelOidcTokenBaseOptions & {
43
+ /**
44
+ * @default undefined
45
+ */
46
+ audience?: undefined;
47
+ jti?: never;
48
+ skipCache?: never;
49
+ });
23
50
  /**
24
51
  * Gets the current OIDC token from the request context or the environment variable.
25
52
  *
@@ -59,3 +86,4 @@ export interface GetVercelOidcTokenOptions extends Omit<ExchangeVercelOidcTokenO
59
86
  * ```
60
87
  */
61
88
  export declare function getVercelOidcToken(options?: GetVercelOidcTokenOptions): Promise<string>;
89
+ export {};
@@ -66,7 +66,8 @@ ${error.message}`;
66
66
  token = await (0, import_exchange_vercel_oidc_token.exchangeVercelOidcToken)({
67
67
  token,
68
68
  audience: options.audience,
69
- jti: options.jti
69
+ jti: options.jti,
70
+ skipCache: options.skipCache
70
71
  });
71
72
  }
72
73
  return token;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "3.7.1";
1
+ export declare const version = "3.8.0";
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.7.1";
24
+ const version = "3.8.0";
25
25
  // Annotate the CommonJS export names for ESM import in node:
26
26
  0 && (module.exports = {
27
27
  version
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **exchangeVercelOidcToken**(`options`): [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<`string`\>
8
8
 
9
- Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:38](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L38)
9
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:138](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L138)
10
10
 
11
11
  Exchanges a Vercel OIDC token for a Vercel token with a custom audience.
12
12
 
@@ -6,7 +6,7 @@
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:69](https://github.com/vercel/vercel/blob/main/packages/oidc/src/get-vercel-oidc-token-with-refresh.ts#L69)
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)
10
10
 
11
11
  Gets the current OIDC token from the request context or the environment variable.
12
12
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  # Interface: ExchangeVercelOidcTokenOptions
6
6
 
7
- Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:11](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L11)
7
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:12](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L12)
8
8
 
9
9
  The options for the `exchangeVercelOidcToken` function.
10
10
 
@@ -14,7 +14,7 @@ The options for the `exchangeVercelOidcToken` function.
14
14
 
15
15
  > `optional` **audience?**: `string`
16
16
 
17
- Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:20](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L20)
17
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:21](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L21)
18
18
 
19
19
  Optional audience to set on the exchanged token.
20
20
 
@@ -24,16 +24,26 @@ Optional audience to set on the exchanged token.
24
24
 
25
25
  > `optional` **jti?**: `string`
26
26
 
27
- Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:25](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L25)
27
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:26](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L26)
28
28
 
29
29
  Optional JTI to set on the exchanged token.
30
30
 
31
31
  ---
32
32
 
33
+ ### skipCache?
34
+
35
+ > `optional` **skipCache?**: `boolean`
36
+
37
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:32](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L32)
38
+
39
+ Optional flag to bypass the in-memory cache.
40
+
41
+ ---
42
+
33
43
  ### token
34
44
 
35
45
  > **token**: `string`
36
46
 
37
- Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:15](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L15)
47
+ Defined in: [packages/oidc/src/exchange-vercel-oidc-token.ts:16](https://github.com/vercel/vercel/blob/main/packages/oidc/src/exchange-vercel-oidc-token.ts#L16)
38
48
 
39
49
  The token to exchange.
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "require": "./dist/index.js"
19
19
  }
20
20
  },
21
- "version": "3.7.1",
21
+ "version": "3.8.0",
22
22
  "repository": {
23
23
  "directory": "packages/oidc",
24
24
  "type": "git",
@@ -29,8 +29,8 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "jose": "^5.9.6",
32
- "@vercel/cli-config": "0.2.0",
33
- "@vercel/cli-exec": "1.0.0"
32
+ "@vercel/cli-exec": "1.0.0",
33
+ "@vercel/cli-config": "0.2.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "prettier": "3.8.3",