@vercel/oidc 3.5.0 → 3.6.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,18 @@
1
1
  # @vercel/oidc
2
2
 
3
+ ## 3.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - fddeb55: Add configurable credentials storage handling across the CLI auth stack. Storage of credentials can be configured by the new `credStorage` key in global `config.json` or the new `VERCEL_TOKEN_STORAGE` environment variable. The environment variable takes precedence over the configuration key. Accepted values are `file` (store credentials in `auth.json`), `keyring` (store credentials in system keyring, e.g macOS Keychain or Secrets Service on Linux), and `auto` (try storing in keyring if available, fall back to `file` if keyring is not available).
8
+
9
+ `@vercel/oidc` supports keyring-stored authentication credentials by delegating the OIDC minting to the CLI executable via `@vercel/cli-exec`.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [fddeb55]
14
+ - @vercel/cli-config@0.2.0
15
+
3
16
  ## 3.5.0
4
17
 
5
18
  ### Minor Changes
@@ -32,8 +32,11 @@ class AccessTokenMissingError extends Error {
32
32
  }
33
33
  class RefreshAccessTokenFailedError extends Error {
34
34
  constructor(cause) {
35
- super("Failed to refresh authentication token.", { cause });
35
+ super("Failed to refresh authentication token.");
36
36
  this.name = "RefreshAccessTokenFailedError";
37
+ if (cause !== void 0) {
38
+ this.cause = cause;
39
+ }
37
40
  }
38
41
  }
39
42
  // Annotate the CommonJS export names for ESM import in node:
@@ -27,8 +27,19 @@ var os = __toESM(require("os"));
27
27
  var path = __toESM(require("path"));
28
28
  var fs = __toESM(require("fs"));
29
29
  var import_token_io = require("./token-io");
30
+ var import_cli_config = require("@vercel/cli-config");
30
31
  var import_get_vercel_oidc_token_with_refresh = require("./get-vercel-oidc-token-with-refresh");
31
32
  var tokenUtil = __toESM(require("./token-util"));
33
+ import_vitest.vi.mock("@vercel/cli-config", async (importOriginal) => ({
34
+ ...await importOriginal(),
35
+ getGlobalPathConfig: import_vitest.vi.fn(),
36
+ getLikelyEffectiveCredStorage: import_vitest.vi.fn(() => "file")
37
+ }));
38
+ import_vitest.vi.mock("@vercel/cli-exec", () => ({
39
+ execVercelCli: import_vitest.vi.fn(),
40
+ VercelCliError: class VercelCliError extends Error {
41
+ }
42
+ }));
32
43
  import_vitest.vi.mock("./token-io");
33
44
  import_vitest.vi.mock("./get-context", () => ({
34
45
  getContext: () => ({ headers: {} })
@@ -57,6 +68,7 @@ import_vitest.vi.mock("./get-context", () => ({
57
68
  import_vitest.vi.spyOn(process, "cwd").mockReturnValue(rootDir);
58
69
  import_vitest.vi.mocked(import_token_io.findRootDir).mockReturnValue(rootDir);
59
70
  import_vitest.vi.mocked(import_token_io.getUserDataDir).mockReturnValue(userDataDir);
71
+ import_vitest.vi.mocked(import_cli_config.getGlobalPathConfig).mockReturnValue(cliDataDir);
60
72
  });
61
73
  (0, import_vitest.afterEach)(() => {
62
74
  if (fs.existsSync(rootDir)) {
@@ -129,28 +141,24 @@ import_vitest.vi.mock("./get-context", () => ({
129
141
  );
130
142
  import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockRejectedValue(
131
143
  new TypeError(
132
- "Vercel OIDC token is malformed. Expected a string-valued token property. Please run `vc env pull` and try again"
144
+ "Vercel OIDC token is malformed. Expected a string-valued token property."
133
145
  )
134
146
  );
135
147
  process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
136
148
  await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(
137
- /Vercel OIDC token is malformed\. Expected a string-valued token property\. Please run `vc env pull` and try again/
149
+ /Vercel OIDC token is malformed\. Expected a string-valued token property\./
138
150
  );
139
151
  });
140
152
  (0, import_vitest.test)("should throw helpful error when token has invalid format", async () => {
141
153
  process.env.VERCEL_OIDC_TOKEN = "not-a-valid-jwt-token";
142
154
  import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockImplementation(() => {
143
- throw new Error("Invalid token. Please run `vc env pull` and try again");
155
+ throw new Error("Invalid token.");
144
156
  });
145
- await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(
146
- /Invalid token\. Please run `vc env pull` and try again/
147
- );
157
+ await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(/Invalid token\./);
148
158
  });
149
159
  (0, import_vitest.test)("should throw error when token expiry check fails", async () => {
150
160
  process.env.VERCEL_OIDC_TOKEN = "not-a-jwt-token";
151
- await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(
152
- /Invalid token\. Please run `vc env pull` and try again/
153
- );
161
+ await (0, import_vitest.expect)((0, import_get_vercel_oidc_token_with_refresh.getVercelOidcToken)()).rejects.toThrow(/Invalid token\./);
154
162
  });
155
163
  (0, import_vitest.test)("should fail when no token exists and no CLI credentials available", async () => {
156
164
  process.env.VERCEL_OIDC_TOKEN = void 0;
@@ -24,6 +24,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  var import_vitest = require("vitest");
25
25
  var DefaultImports = __toESM(require("./index"));
26
26
  var BrowserImports = __toESM(require("./index-browser"));
27
+ import_vitest.vi.mock("@vercel/cli-exec", () => ({
28
+ execVercelCli: import_vitest.vi.fn(),
29
+ VercelCliError: class VercelCliError extends Error {
30
+ }
31
+ }));
27
32
  (0, import_vitest.describe)("browser export", () => {
28
33
  (0, import_vitest.test)("should match the default export", async () => {
29
34
  (0, import_vitest.expect)(Object.keys(BrowserImports).sort()).toStrictEqual(
@@ -1,4 +1,3 @@
1
- export declare function getVercelDataDir(): string | null;
2
1
  export interface GetVercelTokenOptions {
3
2
  /**
4
3
  * Optional time buffer in milliseconds before token expiry to consider it expired.
@@ -11,6 +10,7 @@ export declare function getVercelToken(options?: GetVercelTokenOptions): Promise
11
10
  interface VercelTokenResponse {
12
11
  token: string;
13
12
  }
13
+ export declare function getVercelOidcTokenFromCli(projectId: string, teamId?: string): Promise<VercelTokenResponse>;
14
14
  export declare function getVercelOidcToken(authToken: string, projectId: string, teamId?: string): Promise<VercelTokenResponse | null>;
15
15
  export declare function assertVercelOidcTokenResponse(res: unknown): asserts res is VercelTokenResponse;
16
16
  export declare function findProjectInfo(): {
@@ -31,8 +31,8 @@ __export(token_util_exports, {
31
31
  assertVercelOidcTokenResponse: () => assertVercelOidcTokenResponse,
32
32
  findProjectInfo: () => findProjectInfo,
33
33
  getTokenPayload: () => getTokenPayload,
34
- getVercelDataDir: () => getVercelDataDir,
35
34
  getVercelOidcToken: () => getVercelOidcToken,
35
+ getVercelOidcTokenFromCli: () => getVercelOidcTokenFromCli,
36
36
  getVercelToken: () => getVercelToken,
37
37
  isExpired: () => isExpired,
38
38
  loadToken: () => loadToken,
@@ -41,29 +41,23 @@ __export(token_util_exports, {
41
41
  module.exports = __toCommonJS(token_util_exports);
42
42
  var path = __toESM(require("path"));
43
43
  var fs = __toESM(require("fs"));
44
+ var import_cli_exec = require("@vercel/cli-exec");
45
+ var import_cli_config = require("@vercel/cli-config");
44
46
  var import_token_error = require("./token-error");
45
47
  var import_token_io = require("./token-io");
46
- var import_auth_config = require("./auth-config");
47
48
  var import_oauth = require("./oauth");
48
49
  var import_auth_errors = require("./auth-errors");
49
- function getVercelDataDir() {
50
- const vercelFolder = "com.vercel.cli";
51
- const dataDir = (0, import_token_io.getUserDataDir)();
52
- if (!dataDir) {
53
- return null;
54
- }
55
- return path.join(dataDir, vercelFolder);
56
- }
57
50
  async function getVercelToken(options) {
58
- const authConfig = (0, import_auth_config.readAuthConfig)();
59
- if (!authConfig?.token) {
51
+ const configDir = (0, import_cli_config.getGlobalPathConfig)();
52
+ const authConfig = (0, import_cli_config.tryReadAuthConfig)(configDir);
53
+ if (!authConfig || !authConfig.token && !authConfig.refreshToken) {
60
54
  throw new import_auth_errors.AccessTokenMissingError();
61
55
  }
62
- if ((0, import_auth_config.isValidAccessToken)(authConfig, options?.expirationBufferMs)) {
56
+ if (isValidAccessToken(authConfig, options?.expirationBufferMs)) {
63
57
  return authConfig.token;
64
58
  }
65
59
  if (!authConfig.refreshToken) {
66
- (0, import_auth_config.writeAuthConfig)({});
60
+ (0, import_cli_config.writeAuthConfig)(configDir, {});
67
61
  throw new import_auth_errors.RefreshAccessTokenFailedError("No refresh token available");
68
62
  }
69
63
  try {
@@ -72,26 +66,70 @@ async function getVercelToken(options) {
72
66
  });
73
67
  const [tokensError, tokens] = await (0, import_oauth.processTokenResponse)(tokenResponse);
74
68
  if (tokensError || !tokens) {
75
- (0, import_auth_config.writeAuthConfig)({});
69
+ (0, import_cli_config.writeAuthConfig)(configDir, {});
76
70
  throw new import_auth_errors.RefreshAccessTokenFailedError(tokensError);
77
71
  }
78
72
  const updatedConfig = {
79
73
  token: tokens.access_token,
80
- expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
74
+ expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in,
75
+ refreshToken: tokens.refresh_token
81
76
  };
82
- if (tokens.refresh_token) {
83
- updatedConfig.refreshToken = tokens.refresh_token;
84
- }
85
- (0, import_auth_config.writeAuthConfig)(updatedConfig);
77
+ (0, import_cli_config.writeAuthConfig)(configDir, updatedConfig);
86
78
  return updatedConfig.token;
87
79
  } catch (error) {
88
- (0, import_auth_config.writeAuthConfig)({});
80
+ (0, import_cli_config.writeAuthConfig)(configDir, {});
89
81
  if (error instanceof import_auth_errors.AccessTokenMissingError || error instanceof import_auth_errors.RefreshAccessTokenFailedError) {
90
82
  throw error;
91
83
  }
92
84
  throw new import_auth_errors.RefreshAccessTokenFailedError(error);
93
85
  }
94
86
  }
87
+ function isValidAccessToken(authConfig, expirationBufferMs = 0) {
88
+ if (!authConfig.token)
89
+ return false;
90
+ if (typeof authConfig.expiresAt !== "number")
91
+ return true;
92
+ const nowInSeconds = Math.floor(Date.now() / 1e3);
93
+ const bufferInSeconds = expirationBufferMs / 1e3;
94
+ return authConfig.expiresAt >= nowInSeconds + bufferInSeconds;
95
+ }
96
+ async function getVercelOidcTokenFromCli(projectId, teamId) {
97
+ const args = ["project", "token", projectId, "--format=json"];
98
+ if (teamId) {
99
+ args.push("--scope", teamId);
100
+ }
101
+ try {
102
+ const { stdout } = await (0, import_cli_exec.execVercelCli)(args);
103
+ let parsedOutput;
104
+ if (typeof stdout !== "string") {
105
+ throw new import_token_error.VercelOidcTokenError(
106
+ "Failed to refresh OIDC token: `vercel project token` did not return stdout"
107
+ );
108
+ }
109
+ try {
110
+ parsedOutput = JSON.parse(stdout);
111
+ } catch {
112
+ throw new import_token_error.VercelOidcTokenError(
113
+ "Failed to refresh OIDC token: `vercel project token` returned invalid JSON: " + stdout
114
+ );
115
+ }
116
+ assertVercelOidcTokenResponse(parsedOutput);
117
+ return parsedOutput;
118
+ } catch (error) {
119
+ if (error instanceof import_token_error.VercelOidcTokenError) {
120
+ throw error;
121
+ }
122
+ let message = error instanceof Error ? error.message : "";
123
+ const stderr = error instanceof import_cli_exec.VercelCliError ? error.stderr?.trim() : void 0;
124
+ if (stderr && !message.includes(stderr)) {
125
+ message = `${message}
126
+ ${stderr}`.trim();
127
+ }
128
+ throw new import_token_error.VercelOidcTokenError(
129
+ message ? `Failed to refresh OIDC token with the Vercel CLI: ${message}` : "Failed to refresh OIDC token with the Vercel CLI"
130
+ );
131
+ }
132
+ }
95
133
  async function getVercelOidcToken(authToken, projectId, teamId) {
96
134
  const url = `https://api.vercel.com/v1/projects/${projectId}/token?source=vercel-oidc-refresh${teamId ? `&teamId=${teamId}` : ""}`;
97
135
  const res = await fetch(url, {
@@ -111,13 +149,11 @@ async function getVercelOidcToken(authToken, projectId, teamId) {
111
149
  }
112
150
  function assertVercelOidcTokenResponse(res) {
113
151
  if (!res || typeof res !== "object") {
114
- throw new TypeError(
115
- "Vercel OIDC token is malformed. Expected an object. Please run `vc env pull` and try again"
116
- );
152
+ throw new TypeError("Vercel OIDC token is malformed. Expected an object.");
117
153
  }
118
154
  if (!("token" in res) || typeof res.token !== "string") {
119
155
  throw new TypeError(
120
- "Vercel OIDC token is malformed. Expected a string-valued token property. Please run `vc env pull` and try again"
156
+ "Vercel OIDC token is malformed. Expected a string-valued token property."
121
157
  );
122
158
  }
123
159
  }
@@ -174,9 +210,7 @@ function loadToken(projectId) {
174
210
  function getTokenPayload(token) {
175
211
  const tokenParts = token.split(".");
176
212
  if (tokenParts.length !== 3) {
177
- throw new import_token_error.VercelOidcTokenError(
178
- "Invalid token. Please run `vc env pull` and try again"
179
- );
213
+ throw new import_token_error.VercelOidcTokenError("Invalid token.");
180
214
  }
181
215
  const base64 = tokenParts[1].replace(/-/g, "+").replace(/_/g, "/");
182
216
  const padded = base64.padEnd(
@@ -193,8 +227,8 @@ function isExpired(token, bufferMs = 0) {
193
227
  assertVercelOidcTokenResponse,
194
228
  findProjectInfo,
195
229
  getTokenPayload,
196
- getVercelDataDir,
197
230
  getVercelOidcToken,
231
+ getVercelOidcTokenFromCli,
198
232
  getVercelToken,
199
233
  isExpired,
200
234
  loadToken,
@@ -23,9 +23,19 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  var import_vitest = require("vitest");
25
25
  var import_token_util = require("./token-util");
26
- var authConfig = __toESM(require("./auth-config"));
26
+ var cliConfig = __toESM(require("@vercel/cli-config"));
27
27
  var oauth = __toESM(require("./oauth"));
28
28
  var import_auth_errors = require("./auth-errors");
29
+ import_vitest.vi.mock("@vercel/cli-exec", () => ({
30
+ execVercelCli: import_vitest.vi.fn(),
31
+ VercelCliError: class VercelCliError extends Error {
32
+ }
33
+ }));
34
+ import_vitest.vi.mock("@vercel/cli-config", () => ({
35
+ getGlobalPathConfig: import_vitest.vi.fn(() => "/mock/user/data/com.vercel.cli"),
36
+ tryReadAuthConfig: import_vitest.vi.fn(),
37
+ writeAuthConfig: import_vitest.vi.fn()
38
+ }));
29
39
  import_vitest.vi.mock("fs");
30
40
  import_vitest.vi.mock("./token-io", () => ({
31
41
  getUserDataDir: import_vitest.vi.fn(() => "/mock/user/data"),
@@ -45,15 +55,13 @@ import_vitest.vi.mock("./token-io", () => ({
45
55
  expiresAt: Math.floor(Date.now() / 1e3) + 3600
46
56
  // expires in 1 hour
47
57
  };
48
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(validToken);
49
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
50
- });
58
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(validToken);
51
59
  const token = await (0, import_token_util.getVercelToken)();
52
60
  (0, import_vitest.expect)(token).toBe("valid-access-token");
53
- (0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
61
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).not.toHaveBeenCalled();
54
62
  });
55
63
  (0, import_vitest.it)("should throw AccessTokenMissingError if auth config does not exist", async () => {
56
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(null);
64
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(null);
57
65
  await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(import_auth_errors.AccessTokenMissingError);
58
66
  });
59
67
  (0, import_vitest.it)("should refresh token if expired and refresh token exists", async () => {
@@ -72,16 +80,15 @@ import_vitest.vi.mock("./token-io", () => ({
72
80
  refresh_token: "new-refresh-token"
73
81
  })
74
82
  };
75
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
76
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
77
- });
83
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
78
84
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
79
85
  const token = await (0, import_token_util.getVercelToken)();
80
86
  (0, import_vitest.expect)(token).toBe("new-access-token");
81
87
  (0, import_vitest.expect)(oauth.refreshTokenRequest).toHaveBeenCalledWith({
82
88
  refresh_token: "valid-refresh-token"
83
89
  });
84
- (0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith(
90
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).toHaveBeenCalledWith(
91
+ import_vitest.expect.any(String),
85
92
  import_vitest.expect.objectContaining({
86
93
  token: "new-access-token",
87
94
  refreshToken: "new-refresh-token",
@@ -89,20 +96,41 @@ import_vitest.vi.mock("./token-io", () => ({
89
96
  })
90
97
  );
91
98
  });
99
+ (0, import_vitest.it)("should refresh token when only refreshToken is present", async () => {
100
+ const refreshOnlyToken = {
101
+ refreshToken: "valid-refresh-token"
102
+ };
103
+ const mockResponse = {
104
+ ok: true,
105
+ json: async () => ({
106
+ access_token: "new-access-token",
107
+ token_type: "Bearer",
108
+ expires_in: 3600
109
+ })
110
+ };
111
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(refreshOnlyToken);
112
+ import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
113
+ const token = await (0, import_token_util.getVercelToken)();
114
+ (0, import_vitest.expect)(token).toBe("new-access-token");
115
+ (0, import_vitest.expect)(oauth.refreshTokenRequest).toHaveBeenCalledWith({
116
+ refresh_token: "valid-refresh-token"
117
+ });
118
+ });
92
119
  (0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if token expired and no refresh token", async () => {
93
120
  const expiredTokenNoRefresh = {
94
121
  token: "expired-access-token",
95
122
  expiresAt: Math.floor(Date.now() / 1e3) - 3600
96
123
  };
97
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(
124
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(
98
125
  expiredTokenNoRefresh
99
126
  );
100
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
101
- });
102
127
  await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
103
128
  import_auth_errors.RefreshAccessTokenFailedError
104
129
  );
105
- (0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
130
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).toHaveBeenCalledWith(
131
+ import_vitest.expect.any(String),
132
+ {}
133
+ );
106
134
  });
107
135
  (0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if refresh fails with OAuth error", async () => {
108
136
  const expiredToken = {
@@ -117,14 +145,15 @@ import_vitest.vi.mock("./token-io", () => ({
117
145
  error_description: "Refresh token expired"
118
146
  })
119
147
  };
120
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
121
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
122
- });
148
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
123
149
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockErrorResponse);
124
150
  await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
125
151
  import_auth_errors.RefreshAccessTokenFailedError
126
152
  );
127
- (0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
153
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).toHaveBeenCalledWith(
154
+ import_vitest.expect.any(String),
155
+ {}
156
+ );
128
157
  });
129
158
  (0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if refresh fails with network error", async () => {
130
159
  const expiredToken = {
@@ -132,27 +161,26 @@ import_vitest.vi.mock("./token-io", () => ({
132
161
  refreshToken: "valid-refresh-token",
133
162
  expiresAt: Math.floor(Date.now() / 1e3) - 3600
134
163
  };
135
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
136
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
137
- });
164
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
138
165
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockRejectedValue(
139
166
  new Error("Network error")
140
167
  );
141
168
  await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
142
169
  import_auth_errors.RefreshAccessTokenFailedError
143
170
  );
144
- (0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
171
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).toHaveBeenCalledWith(
172
+ import_vitest.expect.any(String),
173
+ {}
174
+ );
145
175
  });
146
176
  (0, import_vitest.it)("should treat token as valid if expiresAt is missing (--token case)", async () => {
147
177
  const tokenWithoutExpiry = {
148
178
  token: "cli-provided-token"
149
179
  };
150
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(tokenWithoutExpiry);
151
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
152
- });
180
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(tokenWithoutExpiry);
153
181
  const token = await (0, import_token_util.getVercelToken)();
154
182
  (0, import_vitest.expect)(token).toBe("cli-provided-token");
155
- (0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
183
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).not.toHaveBeenCalled();
156
184
  });
157
185
  (0, import_vitest.it)("should preserve new refresh token if provided in response", async () => {
158
186
  const expiredToken = {
@@ -169,18 +197,17 @@ import_vitest.vi.mock("./token-io", () => ({
169
197
  refresh_token: "new-refresh-token"
170
198
  })
171
199
  };
172
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
173
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
174
- });
200
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
175
201
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
176
202
  await (0, import_token_util.getVercelToken)();
177
- (0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith(
203
+ (0, import_vitest.expect)(cliConfig.writeAuthConfig).toHaveBeenCalledWith(
204
+ import_vitest.expect.any(String),
178
205
  import_vitest.expect.objectContaining({
179
206
  refreshToken: "new-refresh-token"
180
207
  })
181
208
  );
182
209
  });
183
- (0, import_vitest.it)("should not overwrite refresh token if not provided in response", async () => {
210
+ (0, import_vitest.it)("should preserve refresh token if not provided in response", async () => {
184
211
  const expiredToken = {
185
212
  token: "expired-access-token",
186
213
  refreshToken: "existing-refresh-token",
@@ -195,13 +222,11 @@ import_vitest.vi.mock("./token-io", () => ({
195
222
  // No refresh_token in response
196
223
  })
197
224
  };
198
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
199
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
200
- });
225
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
201
226
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
202
227
  await (0, import_token_util.getVercelToken)();
203
- const writeCall = import_vitest.vi.mocked(authConfig.writeAuthConfig).mock.calls[0][0];
204
- (0, import_vitest.expect)(writeCall).not.toHaveProperty("refreshToken");
228
+ const writeCall = import_vitest.vi.mocked(cliConfig.writeAuthConfig).mock.calls[0][1];
229
+ (0, import_vitest.expect)(writeCall).toHaveProperty("refreshToken", "existing-refresh-token");
205
230
  });
206
231
  (0, import_vitest.it)("should calculate expiresAt correctly from expires_in", async () => {
207
232
  const expiredToken = {
@@ -218,14 +243,12 @@ import_vitest.vi.mock("./token-io", () => ({
218
243
  // 2 hours
219
244
  })
220
245
  };
221
- import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
222
- import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
223
- });
246
+ import_vitest.vi.mocked(cliConfig.tryReadAuthConfig).mockReturnValue(expiredToken);
224
247
  import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
225
248
  const beforeCall = Math.floor(Date.now() / 1e3);
226
249
  await (0, import_token_util.getVercelToken)();
227
250
  const afterCall = Math.floor(Date.now() / 1e3);
228
- const writeCall = import_vitest.vi.mocked(authConfig.writeAuthConfig).mock.calls[0][0];
251
+ const writeCall = import_vitest.vi.mocked(cliConfig.writeAuthConfig).mock.calls[0][1];
229
252
  (0, import_vitest.expect)(writeCall.expiresAt).toBeGreaterThanOrEqual(beforeCall + 7200);
230
253
  (0, import_vitest.expect)(writeCall.expiresAt).toBeLessThanOrEqual(afterCall + 7200);
231
254
  });
package/dist/token.js CHANGED
@@ -21,6 +21,7 @@ __export(token_exports, {
21
21
  refreshToken: () => refreshToken
22
22
  });
23
23
  module.exports = __toCommonJS(token_exports);
24
+ var import_cli_config = require("@vercel/cli-config");
24
25
  var import_token_error = require("./token-error");
25
26
  var import_token_util = require("./token-util");
26
27
  async function refreshToken(options) {
@@ -42,10 +43,15 @@ async function refreshToken(options) {
42
43
  }
43
44
  let maybeToken = (0, import_token_util.loadToken)(projectId);
44
45
  if (!maybeToken || (0, import_token_util.isExpired)((0, import_token_util.getTokenPayload)(maybeToken.token), options?.expirationBufferMs)) {
45
- const authToken = await (0, import_token_util.getVercelToken)({
46
- expirationBufferMs: options?.expirationBufferMs
47
- });
48
- maybeToken = await (0, import_token_util.getVercelOidcToken)(authToken, projectId, teamId);
46
+ const configDir = (0, import_cli_config.getGlobalPathConfig)();
47
+ if ((0, import_cli_config.getLikelyEffectiveCredStorage)(configDir) === "keyring") {
48
+ maybeToken = await (0, import_token_util.getVercelOidcTokenFromCli)(projectId, teamId);
49
+ } else {
50
+ const authToken = await (0, import_token_util.getVercelToken)({
51
+ expirationBufferMs: options?.expirationBufferMs
52
+ });
53
+ maybeToken = await (0, import_token_util.getVercelOidcToken)(authToken, projectId, teamId);
54
+ }
49
55
  if (!maybeToken) {
50
56
  throw new import_token_error.VercelOidcTokenError("Failed to refresh OIDC token");
51
57
  }
@@ -26,9 +26,25 @@ var import_vitest = require("vitest");
26
26
  var os = __toESM(require("os"));
27
27
  var path = __toESM(require("path"));
28
28
  var fs = __toESM(require("fs"));
29
+ var import_cli_config = require("@vercel/cli-config");
30
+ var import_cli_exec = require("@vercel/cli-exec");
29
31
  var import_token_io = require("./token-io");
30
32
  var tokenUtil = __toESM(require("./token-util"));
31
33
  var import_token = require("./token");
34
+ import_vitest.vi.mock("@vercel/cli-config", () => ({
35
+ getGlobalPathConfig: import_vitest.vi.fn(),
36
+ getLikelyEffectiveCredStorage: import_vitest.vi.fn(() => "file")
37
+ }));
38
+ import_vitest.vi.mock("@vercel/cli-exec", () => ({
39
+ execVercelCli: import_vitest.vi.fn(),
40
+ VercelCliError: class VercelCliError extends Error {
41
+ constructor(options) {
42
+ super(options.message);
43
+ this.name = "VercelCliError";
44
+ this.stderr = options.stderr;
45
+ }
46
+ }
47
+ }));
32
48
  import_vitest.vi.mock("./token-io");
33
49
  (0, import_vitest.describe)("refreshToken", () => {
34
50
  let rootDir;
@@ -38,6 +54,7 @@ import_vitest.vi.mock("./token-io");
38
54
  const projectId = "prj_test123";
39
55
  (0, import_vitest.beforeEach)(() => {
40
56
  import_vitest.vi.clearAllMocks();
57
+ import_vitest.vi.mocked(import_cli_config.getLikelyEffectiveCredStorage).mockReturnValue("file");
41
58
  process.env.VERCEL_OIDC_TOKEN = void 0;
42
59
  const random = `test-${(0, import_crypto.randomUUID)()}`;
43
60
  rootDir = path.join(os.tmpdir(), random);
@@ -49,7 +66,10 @@ import_vitest.vi.mock("./token-io");
49
66
  fs.mkdirSync(path.join(rootDir, ".vercel"), {
50
67
  recursive: true
51
68
  });
52
- fs.writeFileSync(path.join(cliDataDir, "auth.json"), '{token: "test"}');
69
+ fs.writeFileSync(
70
+ path.join(cliDataDir, "auth.json"),
71
+ JSON.stringify({ token: "test" })
72
+ );
53
73
  fs.writeFileSync(
54
74
  path.join(rootDir, ".vercel", "project.json"),
55
75
  JSON.stringify({ projectId })
@@ -57,6 +77,7 @@ import_vitest.vi.mock("./token-io");
57
77
  import_vitest.vi.spyOn(process, "cwd").mockReturnValue(rootDir);
58
78
  import_vitest.vi.mocked(import_token_io.findRootDir).mockReturnValue(rootDir);
59
79
  import_vitest.vi.mocked(import_token_io.getUserDataDir).mockReturnValue(userDataDir);
80
+ import_vitest.vi.mocked(import_cli_config.getGlobalPathConfig).mockReturnValue(cliDataDir);
60
81
  import_vitest.vi.spyOn(tokenUtil, "getVercelToken").mockResolvedValue("test");
61
82
  import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({
62
83
  token: "test-token"
@@ -66,6 +87,15 @@ import_vitest.vi.mock("./token-io");
66
87
  name: "test-name",
67
88
  exp: Date.now() + 1e5
68
89
  });
90
+ import_vitest.vi.mocked(import_cli_exec.execVercelCli).mockResolvedValue({
91
+ stdout: JSON.stringify({ token: "cli-token" }),
92
+ stderr: "",
93
+ invocation: {
94
+ command: "vercel",
95
+ commandArgs: [],
96
+ source: "path"
97
+ }
98
+ });
69
99
  });
70
100
  (0, import_vitest.test)("should correctly load saved token from file", async () => {
71
101
  const token = { token: "test-saved" };
@@ -81,6 +111,66 @@ import_vitest.vi.mock("./token-io");
81
111
  const token = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
82
112
  (0, import_vitest.expect)(token).toEqual({ token: "test-token" });
83
113
  });
114
+ (0, import_vitest.test)("should use cli-exec when keyring-backed credentials are likely", async () => {
115
+ import_vitest.vi.mocked(import_cli_config.getLikelyEffectiveCredStorage).mockReturnValue("keyring");
116
+ await (0, import_token.refreshToken)();
117
+ (0, import_vitest.expect)(import_cli_exec.execVercelCli).toHaveBeenCalledWith([
118
+ "project",
119
+ "token",
120
+ projectId,
121
+ "--format=json"
122
+ ]);
123
+ (0, import_vitest.expect)(tokenUtil.getVercelToken).not.toHaveBeenCalled();
124
+ (0, import_vitest.expect)(tokenUtil.getVercelOidcToken).not.toHaveBeenCalled();
125
+ (0, import_vitest.expect)(process.env.VERCEL_OIDC_TOKEN).toBe("cli-token");
126
+ });
127
+ (0, import_vitest.test)("should pass scope to cli-exec when team is known", async () => {
128
+ const customTeamId = "team_customscope123";
129
+ import_vitest.vi.mocked(import_cli_config.getLikelyEffectiveCredStorage).mockReturnValue("keyring");
130
+ await (0, import_token.refreshToken)({ team: customTeamId, project: projectId });
131
+ (0, import_vitest.expect)(import_cli_exec.execVercelCli).toHaveBeenCalledWith([
132
+ "project",
133
+ "token",
134
+ projectId,
135
+ "--format=json",
136
+ "--scope",
137
+ customTeamId
138
+ ]);
139
+ });
140
+ (0, import_vitest.test)("should fail with a cli-specific error for malformed cli json output", async () => {
141
+ import_vitest.vi.mocked(import_cli_config.getLikelyEffectiveCredStorage).mockReturnValue("keyring");
142
+ import_vitest.vi.mocked(import_cli_exec.execVercelCli).mockResolvedValue({
143
+ stdout: JSON.stringify({ nope: "missing-token" }),
144
+ stderr: "",
145
+ invocation: {
146
+ command: "vercel",
147
+ commandArgs: [],
148
+ source: "path"
149
+ }
150
+ });
151
+ await (0, import_vitest.expect)((0, import_token.refreshToken)()).rejects.toThrow(
152
+ "Failed to refresh OIDC token with the Vercel CLI: Vercel OIDC token is malformed. Expected a string-valued token property."
153
+ );
154
+ });
155
+ (0, import_vitest.test)("should include cli stderr when cli-exec fails", async () => {
156
+ import_vitest.vi.mocked(import_cli_config.getLikelyEffectiveCredStorage).mockReturnValue("keyring");
157
+ import_vitest.vi.mocked(import_cli_exec.execVercelCli).mockRejectedValue(
158
+ new import_cli_exec.VercelCliError({
159
+ code: "VERCEL_CLI_ERRORED",
160
+ message: "Command failed with exit code 1",
161
+ stderr: "Project not found for this scope",
162
+ exitCode: 1,
163
+ invocation: {
164
+ command: "vercel",
165
+ commandArgs: [],
166
+ source: "path"
167
+ }
168
+ })
169
+ );
170
+ await (0, import_vitest.expect)((0, import_token.refreshToken)()).rejects.toThrow(
171
+ "Failed to refresh OIDC token with the Vercel CLI: Command failed with exit code 1\nProject not found for this scope"
172
+ );
173
+ });
84
174
  (0, import_vitest.test)("should use provided team and project instead of reading project.json", async () => {
85
175
  const customProjectId = "prj_custom123";
86
176
  const customTeamId = "team_custom456";
@@ -6,7 +6,7 @@
6
6
 
7
7
  > **getVercelToken**(`options?`): [`Promise`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<`string`\>
8
8
 
9
- Defined in: [packages/oidc/src/token-util.ts:35](https://github.com/vercel/vercel/blob/main/packages/oidc/src/token-util.ts#L35)
9
+ Defined in: [packages/oidc/src/token-util.ts:27](https://github.com/vercel/vercel/blob/main/packages/oidc/src/token-util.ts#L27)
10
10
 
11
11
  ## Parameters
12
12
 
package/package.json CHANGED
@@ -18,7 +18,7 @@
18
18
  "require": "./dist/index.js"
19
19
  }
20
20
  },
21
- "version": "3.5.0",
21
+ "version": "3.6.0",
22
22
  "repository": {
23
23
  "directory": "packages/oidc",
24
24
  "type": "git",
@@ -27,6 +27,10 @@
27
27
  "bugs": {
28
28
  "url": "https://github.com/vercel/vercel/issues"
29
29
  },
30
+ "dependencies": {
31
+ "@vercel/cli-config": "0.2.0",
32
+ "@vercel/cli-exec": "0.1.1"
33
+ },
30
34
  "devDependencies": {
31
35
  "prettier": "3.8.3",
32
36
  "tinyspawn": "1.3.1",
@@ -1,30 +0,0 @@
1
- /**
2
- * Auth configuration stored in ~/.../com.vercel.cli/auth.json
3
- */
4
- export interface AuthConfig {
5
- /** An `access_token` obtained using the OAuth Device Authorization flow. */
6
- token?: string;
7
- /** A `refresh_token` obtained using the OAuth Device Authorization flow. */
8
- refreshToken?: string;
9
- /**
10
- * The absolute time (seconds) when the token expires.
11
- * Used to optimistically check if the token is still valid.
12
- */
13
- expiresAt?: number;
14
- /** Whether to skip writing this config to disk. */
15
- skipWrite?: boolean;
16
- }
17
- /**
18
- * Read the auth config from disk
19
- * Returns null if the file doesn't exist or cannot be read
20
- */
21
- export declare function readAuthConfig(): AuthConfig | null;
22
- /**
23
- * Write the auth config to disk with proper permissions
24
- */
25
- export declare function writeAuthConfig(config: AuthConfig): void;
26
- /**
27
- * Check if an access token is valid (not expired)
28
- * Copied from packages/cli/src/util/client.ts:72-81
29
- */
30
- export declare function isValidAccessToken(authConfig: AuthConfig, expirationBufferMs?: number): boolean;
@@ -1,85 +0,0 @@
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 __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
- var auth_config_exports = {};
30
- __export(auth_config_exports, {
31
- isValidAccessToken: () => isValidAccessToken,
32
- readAuthConfig: () => readAuthConfig,
33
- writeAuthConfig: () => writeAuthConfig
34
- });
35
- module.exports = __toCommonJS(auth_config_exports);
36
- var fs = __toESM(require("fs"));
37
- var path = __toESM(require("path"));
38
- var import_token_util = require("./token-util");
39
- function getAuthConfigPath() {
40
- const dataDir = (0, import_token_util.getVercelDataDir)();
41
- if (!dataDir) {
42
- throw new Error(
43
- `Unable to find Vercel CLI data directory. Your platform: ${process.platform}. Supported: darwin, linux, win32.`
44
- );
45
- }
46
- return path.join(dataDir, "auth.json");
47
- }
48
- function readAuthConfig() {
49
- try {
50
- const authPath = getAuthConfigPath();
51
- if (!fs.existsSync(authPath)) {
52
- return null;
53
- }
54
- const content = fs.readFileSync(authPath, "utf8");
55
- if (!content) {
56
- return null;
57
- }
58
- return JSON.parse(content);
59
- } catch (_error) {
60
- return null;
61
- }
62
- }
63
- function writeAuthConfig(config) {
64
- const authPath = getAuthConfigPath();
65
- const authDir = path.dirname(authPath);
66
- if (!fs.existsSync(authDir)) {
67
- fs.mkdirSync(authDir, { mode: 504, recursive: true });
68
- }
69
- fs.writeFileSync(authPath, JSON.stringify(config, null, 2), { mode: 384 });
70
- }
71
- function isValidAccessToken(authConfig, expirationBufferMs = 0) {
72
- if (!authConfig.token)
73
- return false;
74
- if (typeof authConfig.expiresAt !== "number")
75
- return true;
76
- const nowInSeconds = Math.floor(Date.now() / 1e3);
77
- const bufferInSeconds = expirationBufferMs / 1e3;
78
- return authConfig.expiresAt >= nowInSeconds + bufferInSeconds;
79
- }
80
- // Annotate the CommonJS export names for ESM import in node:
81
- 0 && (module.exports = {
82
- isValidAccessToken,
83
- readAuthConfig,
84
- writeAuthConfig
85
- });