@vercel/oidc 3.0.5 → 3.2.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 +18 -0
- package/README.md +38 -0
- package/dist/auth-config.d.ts +30 -0
- package/dist/auth-config.js +85 -0
- package/dist/auth-errors.d.ts +16 -0
- package/dist/auth-errors.js +43 -0
- package/dist/get-vercel-oidc-token.d.ts +34 -1
- package/dist/get-vercel-oidc-token.js +10 -6
- package/dist/get-vercel-oidc-token.test.d.ts +1 -0
- package/dist/get-vercel-oidc-token.test.js +363 -0
- package/dist/index-browser.d.ts +2 -0
- package/dist/index-browser.js +12 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +10 -2
- package/dist/oauth.d.ts +17 -0
- package/dist/oauth.js +88 -0
- package/dist/token-io.d.ts +1 -1
- package/dist/token-io.js +1 -1
- package/dist/token-util.d.ts +10 -2
- package/dist/token-util.js +101 -75
- package/dist/token-util.test.d.ts +1 -0
- package/dist/token-util.test.js +232 -0
- package/dist/token.d.ts +19 -1
- package/dist/token.js +21 -14
- package/dist/token.test.js +108 -2
- package/docs/README.md +49 -4
- package/docs/classes/AccessTokenMissingError.md +160 -0
- package/docs/classes/RefreshAccessTokenFailedError.md +166 -0
- package/package.json +1 -1
package/dist/token-util.js
CHANGED
|
@@ -31,9 +31,9 @@ __export(token_util_exports, {
|
|
|
31
31
|
assertVercelOidcTokenResponse: () => assertVercelOidcTokenResponse,
|
|
32
32
|
findProjectInfo: () => findProjectInfo,
|
|
33
33
|
getTokenPayload: () => getTokenPayload,
|
|
34
|
-
getVercelCliToken: () => getVercelCliToken,
|
|
35
34
|
getVercelDataDir: () => getVercelDataDir,
|
|
36
35
|
getVercelOidcToken: () => getVercelOidcToken,
|
|
36
|
+
getVercelToken: () => getVercelToken,
|
|
37
37
|
isExpired: () => isExpired,
|
|
38
38
|
loadToken: () => loadToken,
|
|
39
39
|
saveToken: () => saveToken
|
|
@@ -43,6 +43,9 @@ var path = __toESM(require("path"));
|
|
|
43
43
|
var fs = __toESM(require("fs"));
|
|
44
44
|
var import_token_error = require("./token-error");
|
|
45
45
|
var import_token_io = require("./token-io");
|
|
46
|
+
var import_auth_config = require("./auth-config");
|
|
47
|
+
var import_oauth = require("./oauth");
|
|
48
|
+
var import_auth_errors = require("./auth-errors");
|
|
46
49
|
function getVercelDataDir() {
|
|
47
50
|
const vercelFolder = "com.vercel.cli";
|
|
48
51
|
const dataDir = (0, import_token_io.getUserDataDir)();
|
|
@@ -51,106 +54,129 @@ function getVercelDataDir() {
|
|
|
51
54
|
}
|
|
52
55
|
return path.join(dataDir, vercelFolder);
|
|
53
56
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
57
|
-
|
|
57
|
+
async function getVercelToken(options) {
|
|
58
|
+
const authConfig = (0, import_auth_config.readAuthConfig)();
|
|
59
|
+
if (!authConfig?.token) {
|
|
60
|
+
throw new import_auth_errors.AccessTokenMissingError();
|
|
58
61
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
return null;
|
|
62
|
+
if ((0, import_auth_config.isValidAccessToken)(authConfig, options?.expirationBufferMs)) {
|
|
63
|
+
return authConfig.token;
|
|
62
64
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
if (!authConfig.refreshToken) {
|
|
66
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
67
|
+
throw new import_auth_errors.RefreshAccessTokenFailedError("No refresh token available");
|
|
66
68
|
}
|
|
67
|
-
return JSON.parse(token).token;
|
|
68
|
-
}
|
|
69
|
-
async function getVercelOidcToken(authToken, projectId, teamId) {
|
|
70
69
|
try {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
Authorization: `Bearer ${authToken}`
|
|
76
|
-
}
|
|
70
|
+
const tokenResponse = await (0, import_oauth.refreshTokenRequest)({
|
|
71
|
+
refresh_token: authConfig.refreshToken
|
|
77
72
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
73
|
+
const [tokensError, tokens] = await (0, import_oauth.processTokenResponse)(tokenResponse);
|
|
74
|
+
if (tokensError || !tokens) {
|
|
75
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
76
|
+
throw new import_auth_errors.RefreshAccessTokenFailedError(tokensError);
|
|
82
77
|
}
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
const updatedConfig = {
|
|
79
|
+
token: tokens.access_token,
|
|
80
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
81
|
+
};
|
|
82
|
+
if (tokens.refresh_token) {
|
|
83
|
+
updatedConfig.refreshToken = tokens.refresh_token;
|
|
84
|
+
}
|
|
85
|
+
(0, import_auth_config.writeAuthConfig)(updatedConfig);
|
|
86
|
+
return updatedConfig.token;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
89
|
+
if (error instanceof import_auth_errors.AccessTokenMissingError || error instanceof import_auth_errors.RefreshAccessTokenFailedError) {
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
throw new import_auth_errors.RefreshAccessTokenFailedError(error);
|
|
88
93
|
}
|
|
89
94
|
}
|
|
95
|
+
async function getVercelOidcToken(authToken, projectId, teamId) {
|
|
96
|
+
const url = `https://api.vercel.com/v1/projects/${projectId}/token?source=vercel-oidc-refresh${teamId ? `&teamId=${teamId}` : ""}`;
|
|
97
|
+
const res = await fetch(url, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
Authorization: `Bearer ${authToken}`
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (!res.ok) {
|
|
104
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
105
|
+
`Failed to refresh OIDC token: ${res.statusText}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
const tokenRes = await res.json();
|
|
109
|
+
assertVercelOidcTokenResponse(tokenRes);
|
|
110
|
+
return tokenRes;
|
|
111
|
+
}
|
|
90
112
|
function assertVercelOidcTokenResponse(res) {
|
|
91
113
|
if (!res || typeof res !== "object") {
|
|
92
|
-
throw new TypeError(
|
|
114
|
+
throw new TypeError(
|
|
115
|
+
"Vercel OIDC token is malformed. Expected an object. Please run `vc env pull` and try again"
|
|
116
|
+
);
|
|
93
117
|
}
|
|
94
118
|
if (!("token" in res) || typeof res.token !== "string") {
|
|
95
|
-
throw new TypeError(
|
|
119
|
+
throw new TypeError(
|
|
120
|
+
"Vercel OIDC token is malformed. Expected a string-valued token property. Please run `vc env pull` and try again"
|
|
121
|
+
);
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
function findProjectInfo() {
|
|
99
125
|
const dir = (0, import_token_io.findRootDir)();
|
|
100
126
|
if (!dir) {
|
|
101
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
127
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
128
|
+
"Unable to find project root directory. Have you linked your project with `vc link?`"
|
|
129
|
+
);
|
|
102
130
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const prj = JSON.parse(fs.readFileSync(prjPath, "utf8"));
|
|
109
|
-
if (typeof prj.projectId !== "string" && typeof prj.orgId !== "string") {
|
|
110
|
-
throw new TypeError("Expected a string-valued projectId property");
|
|
111
|
-
}
|
|
112
|
-
return { projectId: prj.projectId, teamId: prj.orgId };
|
|
113
|
-
} catch (e) {
|
|
114
|
-
throw new import_token_error.VercelOidcTokenError(`Unable to find project ID`, e);
|
|
131
|
+
const prjPath = path.join(dir, ".vercel", "project.json");
|
|
132
|
+
if (!fs.existsSync(prjPath)) {
|
|
133
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
134
|
+
"project.json not found, have you linked your project with `vc link?`"
|
|
135
|
+
);
|
|
115
136
|
}
|
|
137
|
+
const prj = JSON.parse(fs.readFileSync(prjPath, "utf8"));
|
|
138
|
+
if (typeof prj.projectId !== "string" && typeof prj.orgId !== "string") {
|
|
139
|
+
throw new TypeError(
|
|
140
|
+
"Expected a string-valued projectId property. Try running `vc link` to re-link your project."
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return { projectId: prj.projectId, teamId: prj.orgId };
|
|
116
144
|
}
|
|
117
145
|
function saveToken(token, projectId) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
|
|
124
|
-
const tokenJson = JSON.stringify(token);
|
|
125
|
-
fs.mkdirSync(path.dirname(tokenPath), { mode: 504, recursive: true });
|
|
126
|
-
fs.writeFileSync(tokenPath, tokenJson);
|
|
127
|
-
fs.chmodSync(tokenPath, 432);
|
|
128
|
-
return;
|
|
129
|
-
} catch (e) {
|
|
130
|
-
throw new import_token_error.VercelOidcTokenError(`Failed to save token`, e);
|
|
146
|
+
const dir = (0, import_token_io.getUserDataDir)();
|
|
147
|
+
if (!dir) {
|
|
148
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
149
|
+
"Unable to find user data directory. Please reach out to Vercel support."
|
|
150
|
+
);
|
|
131
151
|
}
|
|
152
|
+
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
|
|
153
|
+
const tokenJson = JSON.stringify(token);
|
|
154
|
+
fs.mkdirSync(path.dirname(tokenPath), { mode: 504, recursive: true });
|
|
155
|
+
fs.writeFileSync(tokenPath, tokenJson);
|
|
156
|
+
fs.chmodSync(tokenPath, 432);
|
|
157
|
+
return;
|
|
132
158
|
}
|
|
133
159
|
function loadToken(projectId) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const token = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
144
|
-
assertVercelOidcTokenResponse(token);
|
|
145
|
-
return token;
|
|
146
|
-
} catch (e) {
|
|
147
|
-
throw new import_token_error.VercelOidcTokenError(`Failed to load token`, e);
|
|
160
|
+
const dir = (0, import_token_io.getUserDataDir)();
|
|
161
|
+
if (!dir) {
|
|
162
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
163
|
+
"Unable to find user data directory. Please reach out to Vercel support."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
|
|
167
|
+
if (!fs.existsSync(tokenPath)) {
|
|
168
|
+
return null;
|
|
148
169
|
}
|
|
170
|
+
const token = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
171
|
+
assertVercelOidcTokenResponse(token);
|
|
172
|
+
return token;
|
|
149
173
|
}
|
|
150
174
|
function getTokenPayload(token) {
|
|
151
175
|
const tokenParts = token.split(".");
|
|
152
176
|
if (tokenParts.length !== 3) {
|
|
153
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
177
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
178
|
+
"Invalid token. Please run `vc env pull` and try again"
|
|
179
|
+
);
|
|
154
180
|
}
|
|
155
181
|
const base64 = tokenParts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
156
182
|
const padded = base64.padEnd(
|
|
@@ -159,17 +185,17 @@ function getTokenPayload(token) {
|
|
|
159
185
|
);
|
|
160
186
|
return JSON.parse(Buffer.from(padded, "base64").toString("utf8"));
|
|
161
187
|
}
|
|
162
|
-
function isExpired(token) {
|
|
163
|
-
return token.exp * 1e3 < Date.now();
|
|
188
|
+
function isExpired(token, bufferMs = 0) {
|
|
189
|
+
return token.exp * 1e3 < Date.now() + bufferMs;
|
|
164
190
|
}
|
|
165
191
|
// Annotate the CommonJS export names for ESM import in node:
|
|
166
192
|
0 && (module.exports = {
|
|
167
193
|
assertVercelOidcTokenResponse,
|
|
168
194
|
findProjectInfo,
|
|
169
195
|
getTokenPayload,
|
|
170
|
-
getVercelCliToken,
|
|
171
196
|
getVercelDataDir,
|
|
172
197
|
getVercelOidcToken,
|
|
198
|
+
getVercelToken,
|
|
173
199
|
isExpired,
|
|
174
200
|
loadToken,
|
|
175
201
|
saveToken
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
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
|
+
var import_token_util = require("./token-util");
|
|
26
|
+
var authConfig = __toESM(require("./auth-config"));
|
|
27
|
+
var oauth = __toESM(require("./oauth"));
|
|
28
|
+
var import_auth_errors = require("./auth-errors");
|
|
29
|
+
import_vitest.vi.mock("fs");
|
|
30
|
+
import_vitest.vi.mock("./token-io", () => ({
|
|
31
|
+
getUserDataDir: import_vitest.vi.fn(() => "/mock/user/data"),
|
|
32
|
+
findRootDir: import_vitest.vi.fn(() => "/mock/root")
|
|
33
|
+
}));
|
|
34
|
+
(0, import_vitest.describe)("getVercelToken", () => {
|
|
35
|
+
(0, import_vitest.beforeEach)(() => {
|
|
36
|
+
import_vitest.vi.clearAllMocks();
|
|
37
|
+
});
|
|
38
|
+
(0, import_vitest.afterEach)(() => {
|
|
39
|
+
import_vitest.vi.restoreAllMocks();
|
|
40
|
+
});
|
|
41
|
+
(0, import_vitest.it)("should return token if valid and not expired", async () => {
|
|
42
|
+
const validToken = {
|
|
43
|
+
token: "valid-access-token",
|
|
44
|
+
refreshToken: "refresh-token",
|
|
45
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 3600
|
|
46
|
+
// expires in 1 hour
|
|
47
|
+
};
|
|
48
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(validToken);
|
|
49
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
50
|
+
});
|
|
51
|
+
const token = await (0, import_token_util.getVercelToken)();
|
|
52
|
+
(0, import_vitest.expect)(token).toBe("valid-access-token");
|
|
53
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
(0, import_vitest.it)("should throw AccessTokenMissingError if auth config does not exist", async () => {
|
|
56
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(null);
|
|
57
|
+
await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(import_auth_errors.AccessTokenMissingError);
|
|
58
|
+
});
|
|
59
|
+
(0, import_vitest.it)("should refresh token if expired and refresh token exists", async () => {
|
|
60
|
+
const expiredToken = {
|
|
61
|
+
token: "expired-access-token",
|
|
62
|
+
refreshToken: "valid-refresh-token",
|
|
63
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
64
|
+
// expired 1 hour ago
|
|
65
|
+
};
|
|
66
|
+
const mockResponse = {
|
|
67
|
+
ok: true,
|
|
68
|
+
json: async () => ({
|
|
69
|
+
access_token: "new-access-token",
|
|
70
|
+
token_type: "Bearer",
|
|
71
|
+
expires_in: 3600,
|
|
72
|
+
refresh_token: "new-refresh-token"
|
|
73
|
+
})
|
|
74
|
+
};
|
|
75
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
76
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
77
|
+
});
|
|
78
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
79
|
+
const token = await (0, import_token_util.getVercelToken)();
|
|
80
|
+
(0, import_vitest.expect)(token).toBe("new-access-token");
|
|
81
|
+
(0, import_vitest.expect)(oauth.refreshTokenRequest).toHaveBeenCalledWith({
|
|
82
|
+
refresh_token: "valid-refresh-token"
|
|
83
|
+
});
|
|
84
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith(
|
|
85
|
+
import_vitest.expect.objectContaining({
|
|
86
|
+
token: "new-access-token",
|
|
87
|
+
refreshToken: "new-refresh-token",
|
|
88
|
+
expiresAt: import_vitest.expect.any(Number)
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
(0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if token expired and no refresh token", async () => {
|
|
93
|
+
const expiredTokenNoRefresh = {
|
|
94
|
+
token: "expired-access-token",
|
|
95
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
96
|
+
};
|
|
97
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(
|
|
98
|
+
expiredTokenNoRefresh
|
|
99
|
+
);
|
|
100
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
101
|
+
});
|
|
102
|
+
await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
|
|
103
|
+
import_auth_errors.RefreshAccessTokenFailedError
|
|
104
|
+
);
|
|
105
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
106
|
+
});
|
|
107
|
+
(0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if refresh fails with OAuth error", async () => {
|
|
108
|
+
const expiredToken = {
|
|
109
|
+
token: "expired-access-token",
|
|
110
|
+
refreshToken: "invalid-refresh-token",
|
|
111
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
112
|
+
};
|
|
113
|
+
const mockErrorResponse = {
|
|
114
|
+
ok: false,
|
|
115
|
+
json: async () => ({
|
|
116
|
+
error: "invalid_grant",
|
|
117
|
+
error_description: "Refresh token expired"
|
|
118
|
+
})
|
|
119
|
+
};
|
|
120
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
121
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
122
|
+
});
|
|
123
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockErrorResponse);
|
|
124
|
+
await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
|
|
125
|
+
import_auth_errors.RefreshAccessTokenFailedError
|
|
126
|
+
);
|
|
127
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
128
|
+
});
|
|
129
|
+
(0, import_vitest.it)("should clear auth and throw RefreshAccessTokenFailedError if refresh fails with network error", async () => {
|
|
130
|
+
const expiredToken = {
|
|
131
|
+
token: "expired-access-token",
|
|
132
|
+
refreshToken: "valid-refresh-token",
|
|
133
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
134
|
+
};
|
|
135
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
136
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
137
|
+
});
|
|
138
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockRejectedValue(
|
|
139
|
+
new Error("Network error")
|
|
140
|
+
);
|
|
141
|
+
await (0, import_vitest.expect)((0, import_token_util.getVercelToken)()).rejects.toThrow(
|
|
142
|
+
import_auth_errors.RefreshAccessTokenFailedError
|
|
143
|
+
);
|
|
144
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
145
|
+
});
|
|
146
|
+
(0, import_vitest.it)("should treat token as valid if expiresAt is missing (--token case)", async () => {
|
|
147
|
+
const tokenWithoutExpiry = {
|
|
148
|
+
token: "cli-provided-token"
|
|
149
|
+
};
|
|
150
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(tokenWithoutExpiry);
|
|
151
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
152
|
+
});
|
|
153
|
+
const token = await (0, import_token_util.getVercelToken)();
|
|
154
|
+
(0, import_vitest.expect)(token).toBe("cli-provided-token");
|
|
155
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
(0, import_vitest.it)("should preserve new refresh token if provided in response", async () => {
|
|
158
|
+
const expiredToken = {
|
|
159
|
+
token: "expired-access-token",
|
|
160
|
+
refreshToken: "old-refresh-token",
|
|
161
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
162
|
+
};
|
|
163
|
+
const mockResponse = {
|
|
164
|
+
ok: true,
|
|
165
|
+
json: async () => ({
|
|
166
|
+
access_token: "new-access-token",
|
|
167
|
+
token_type: "Bearer",
|
|
168
|
+
expires_in: 3600,
|
|
169
|
+
refresh_token: "new-refresh-token"
|
|
170
|
+
})
|
|
171
|
+
};
|
|
172
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
173
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
174
|
+
});
|
|
175
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
176
|
+
await (0, import_token_util.getVercelToken)();
|
|
177
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith(
|
|
178
|
+
import_vitest.expect.objectContaining({
|
|
179
|
+
refreshToken: "new-refresh-token"
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
(0, import_vitest.it)("should not overwrite refresh token if not provided in response", async () => {
|
|
184
|
+
const expiredToken = {
|
|
185
|
+
token: "expired-access-token",
|
|
186
|
+
refreshToken: "existing-refresh-token",
|
|
187
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
188
|
+
};
|
|
189
|
+
const mockResponse = {
|
|
190
|
+
ok: true,
|
|
191
|
+
json: async () => ({
|
|
192
|
+
access_token: "new-access-token",
|
|
193
|
+
token_type: "Bearer",
|
|
194
|
+
expires_in: 3600
|
|
195
|
+
// No refresh_token in response
|
|
196
|
+
})
|
|
197
|
+
};
|
|
198
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
199
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
200
|
+
});
|
|
201
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
202
|
+
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");
|
|
205
|
+
});
|
|
206
|
+
(0, import_vitest.it)("should calculate expiresAt correctly from expires_in", async () => {
|
|
207
|
+
const expiredToken = {
|
|
208
|
+
token: "expired-access-token",
|
|
209
|
+
refreshToken: "valid-refresh-token",
|
|
210
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
211
|
+
};
|
|
212
|
+
const mockResponse = {
|
|
213
|
+
ok: true,
|
|
214
|
+
json: async () => ({
|
|
215
|
+
access_token: "new-access-token",
|
|
216
|
+
token_type: "Bearer",
|
|
217
|
+
expires_in: 7200
|
|
218
|
+
// 2 hours
|
|
219
|
+
})
|
|
220
|
+
};
|
|
221
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
222
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
223
|
+
});
|
|
224
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
225
|
+
const beforeCall = Math.floor(Date.now() / 1e3);
|
|
226
|
+
await (0, import_token_util.getVercelToken)();
|
|
227
|
+
const afterCall = Math.floor(Date.now() / 1e3);
|
|
228
|
+
const writeCall = import_vitest.vi.mocked(authConfig.writeAuthConfig).mock.calls[0][0];
|
|
229
|
+
(0, import_vitest.expect)(writeCall.expiresAt).toBeGreaterThanOrEqual(beforeCall + 7200);
|
|
230
|
+
(0, import_vitest.expect)(writeCall.expiresAt).toBeLessThanOrEqual(afterCall + 7200);
|
|
231
|
+
});
|
|
232
|
+
});
|
package/dist/token.d.ts
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface RefreshTokenOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Team ID (team_*) or slug to use for token refresh.
|
|
4
|
+
* When provided, this team will be used instead of reading from `.vercel/project.json`.
|
|
5
|
+
*/
|
|
6
|
+
team?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Project ID (prj_*) or slug to use for token refresh.
|
|
9
|
+
* When provided, this project will be used instead of reading from `.vercel/project.json`.
|
|
10
|
+
*/
|
|
11
|
+
project?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Optional time buffer in milliseconds before token expiry to consider it expired.
|
|
14
|
+
* When provided, the token will be refreshed if it expires within this buffer time.
|
|
15
|
+
* @default 0
|
|
16
|
+
*/
|
|
17
|
+
expirationBufferMs?: number;
|
|
18
|
+
}
|
|
19
|
+
export declare function refreshToken(options?: RefreshTokenOptions): Promise<void>;
|
package/dist/token.js
CHANGED
|
@@ -23,21 +23,28 @@ __export(token_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(token_exports);
|
|
24
24
|
var import_token_error = require("./token-error");
|
|
25
25
|
var import_token_util = require("./token-util");
|
|
26
|
-
async function refreshToken() {
|
|
27
|
-
|
|
26
|
+
async function refreshToken(options) {
|
|
27
|
+
let projectId = options?.project;
|
|
28
|
+
let teamId = options?.team;
|
|
29
|
+
if (!projectId && !teamId) {
|
|
30
|
+
const projectInfo = (0, import_token_util.findProjectInfo)();
|
|
31
|
+
projectId = projectInfo.projectId;
|
|
32
|
+
teamId = projectInfo.teamId;
|
|
33
|
+
} else if (!projectId || !teamId) {
|
|
34
|
+
const projectInfo = (0, import_token_util.findProjectInfo)();
|
|
35
|
+
projectId = projectId ?? projectInfo.projectId;
|
|
36
|
+
teamId = teamId ?? projectInfo.teamId;
|
|
37
|
+
}
|
|
38
|
+
if (!projectId) {
|
|
39
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
40
|
+
"Failed to refresh OIDC token: No project specified. Try re-linking your project with `vc link`"
|
|
41
|
+
);
|
|
42
|
+
}
|
|
28
43
|
let maybeToken = (0, import_token_util.loadToken)(projectId);
|
|
29
|
-
if (!maybeToken || (0, import_token_util.isExpired)((0, import_token_util.getTokenPayload)(maybeToken.token))) {
|
|
30
|
-
const authToken = (0, import_token_util.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"Failed to refresh OIDC token: login to vercel cli"
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
if (!projectId) {
|
|
37
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
38
|
-
"Failed to refresh OIDC token: project id not found"
|
|
39
|
-
);
|
|
40
|
-
}
|
|
44
|
+
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
|
+
});
|
|
41
48
|
maybeToken = await (0, import_token_util.getVercelOidcToken)(authToken, projectId, teamId);
|
|
42
49
|
if (!maybeToken) {
|
|
43
50
|
throw new import_token_error.VercelOidcTokenError("Failed to refresh OIDC token");
|
package/dist/token.test.js
CHANGED
|
@@ -35,7 +35,7 @@ import_vitest.vi.mock("./token-io");
|
|
|
35
35
|
let userDataDir;
|
|
36
36
|
let cliDataDir;
|
|
37
37
|
let tokenDataDir;
|
|
38
|
-
const projectId = "
|
|
38
|
+
const projectId = "prj_test123";
|
|
39
39
|
(0, import_vitest.beforeEach)(() => {
|
|
40
40
|
import_vitest.vi.clearAllMocks();
|
|
41
41
|
process.env.VERCEL_OIDC_TOKEN = void 0;
|
|
@@ -57,7 +57,7 @@ import_vitest.vi.mock("./token-io");
|
|
|
57
57
|
import_vitest.vi.spyOn(process, "cwd").mockReturnValue(rootDir);
|
|
58
58
|
import_vitest.vi.mocked(import_token_io.findRootDir).mockReturnValue(rootDir);
|
|
59
59
|
import_vitest.vi.mocked(import_token_io.getUserDataDir).mockReturnValue(userDataDir);
|
|
60
|
-
import_vitest.vi.spyOn(tokenUtil, "
|
|
60
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelToken").mockResolvedValue("test");
|
|
61
61
|
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({
|
|
62
62
|
token: "test-token"
|
|
63
63
|
});
|
|
@@ -81,4 +81,110 @@ import_vitest.vi.mock("./token-io");
|
|
|
81
81
|
const token = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
82
82
|
(0, import_vitest.expect)(token).toEqual({ token: "test-token" });
|
|
83
83
|
});
|
|
84
|
+
(0, import_vitest.test)("should use provided team and project instead of reading project.json", async () => {
|
|
85
|
+
const customProjectId = "prj_custom123";
|
|
86
|
+
const customTeamId = "team_custom456";
|
|
87
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken");
|
|
88
|
+
const findRootDirSpy = import_vitest.vi.mocked(import_token_io.findRootDir);
|
|
89
|
+
await (0, import_token.refreshToken)({ team: customTeamId, project: customProjectId });
|
|
90
|
+
(0, import_vitest.expect)(findRootDirSpy).not.toHaveBeenCalled();
|
|
91
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).toHaveBeenCalledWith(
|
|
92
|
+
"test",
|
|
93
|
+
customProjectId,
|
|
94
|
+
customTeamId
|
|
95
|
+
);
|
|
96
|
+
const tokenPath = path.join(tokenDataDir, `${customProjectId}.json`);
|
|
97
|
+
(0, import_vitest.expect)(fs.existsSync(tokenPath)).toBe(true);
|
|
98
|
+
const savedToken = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
99
|
+
(0, import_vitest.expect)(savedToken).toEqual({ token: "test-token" });
|
|
100
|
+
});
|
|
101
|
+
(0, import_vitest.test)("should merge provided project with team from project.json", async () => {
|
|
102
|
+
const customProjectId = "prj_custom234";
|
|
103
|
+
const projectTeamId = "team_fromproject567";
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
106
|
+
JSON.stringify({ projectId, orgId: projectTeamId })
|
|
107
|
+
);
|
|
108
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken");
|
|
109
|
+
await (0, import_token.refreshToken)({ project: customProjectId });
|
|
110
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).toHaveBeenCalledWith(
|
|
111
|
+
"test",
|
|
112
|
+
customProjectId,
|
|
113
|
+
projectTeamId
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
(0, import_vitest.test)("should merge provided team with project from project.json", async () => {
|
|
117
|
+
const customTeamId = "team_custom789";
|
|
118
|
+
const projectProjectId = "prj_fromjson123";
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
121
|
+
JSON.stringify({ projectId: projectProjectId, orgId: "original-team" })
|
|
122
|
+
);
|
|
123
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken");
|
|
124
|
+
await (0, import_token.refreshToken)({ team: customTeamId });
|
|
125
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).toHaveBeenCalledWith(
|
|
126
|
+
"test",
|
|
127
|
+
projectProjectId,
|
|
128
|
+
customTeamId
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
(0, import_vitest.test)("should use cached token when valid with custom project", async () => {
|
|
132
|
+
const customProjectId = "prj_customcached123";
|
|
133
|
+
const customTeamId = "team_customcached456";
|
|
134
|
+
const cachedToken = { token: "cached-valid-token" };
|
|
135
|
+
const tokenPath = path.join(tokenDataDir, `${customProjectId}.json`);
|
|
136
|
+
fs.writeFileSync(tokenPath, JSON.stringify(cachedToken));
|
|
137
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken");
|
|
138
|
+
await (0, import_token.refreshToken)({ team: customTeamId, project: customProjectId });
|
|
139
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).not.toHaveBeenCalled();
|
|
140
|
+
(0, import_vitest.expect)(process.env.VERCEL_OIDC_TOKEN).toBe("cached-valid-token");
|
|
141
|
+
});
|
|
142
|
+
(0, import_vitest.test)("should refresh token when cached token expires within buffer", async () => {
|
|
143
|
+
const customProjectId = "prj_buffertest123";
|
|
144
|
+
const customTeamId = "team_buffertest456";
|
|
145
|
+
const cachedToken = { token: "expiring-soon-token" };
|
|
146
|
+
const tokenPath = path.join(tokenDataDir, `${customProjectId}.json`);
|
|
147
|
+
fs.writeFileSync(tokenPath, JSON.stringify(cachedToken));
|
|
148
|
+
const expiresIn3Minutes = Math.floor((Date.now() + 18e4) / 1e3);
|
|
149
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockReturnValue({
|
|
150
|
+
sub: "test-sub",
|
|
151
|
+
name: "test-name",
|
|
152
|
+
exp: expiresIn3Minutes
|
|
153
|
+
});
|
|
154
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({ token: "fresh-token" });
|
|
155
|
+
await (0, import_token.refreshToken)({
|
|
156
|
+
team: customTeamId,
|
|
157
|
+
project: customProjectId,
|
|
158
|
+
expirationBufferMs: 3e5
|
|
159
|
+
});
|
|
160
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).toHaveBeenCalledWith(
|
|
161
|
+
"test",
|
|
162
|
+
customProjectId,
|
|
163
|
+
customTeamId
|
|
164
|
+
);
|
|
165
|
+
(0, import_vitest.expect)(process.env.VERCEL_OIDC_TOKEN).toBe("fresh-token");
|
|
166
|
+
const savedToken = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
167
|
+
(0, import_vitest.expect)(savedToken).toEqual({ token: "fresh-token" });
|
|
168
|
+
});
|
|
169
|
+
(0, import_vitest.test)("should use cached token when it does not expire within buffer", async () => {
|
|
170
|
+
const customProjectId = "prj_buffervalid123";
|
|
171
|
+
const customTeamId = "team_buffervalid456";
|
|
172
|
+
const cachedToken = { token: "valid-cached-token" };
|
|
173
|
+
const tokenPath = path.join(tokenDataDir, `${customProjectId}.json`);
|
|
174
|
+
fs.writeFileSync(tokenPath, JSON.stringify(cachedToken));
|
|
175
|
+
const expiresIn10Minutes = Math.floor((Date.now() + 6e5) / 1e3);
|
|
176
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockReturnValue({
|
|
177
|
+
sub: "test-sub",
|
|
178
|
+
name: "test-name",
|
|
179
|
+
exp: expiresIn10Minutes
|
|
180
|
+
});
|
|
181
|
+
const getVercelOidcTokenSpy = import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken");
|
|
182
|
+
await (0, import_token.refreshToken)({
|
|
183
|
+
team: customTeamId,
|
|
184
|
+
project: customProjectId,
|
|
185
|
+
expirationBufferMs: 3e5
|
|
186
|
+
});
|
|
187
|
+
(0, import_vitest.expect)(getVercelOidcTokenSpy).not.toHaveBeenCalled();
|
|
188
|
+
(0, import_vitest.expect)(process.env.VERCEL_OIDC_TOKEN).toBe("valid-cached-token");
|
|
189
|
+
});
|
|
84
190
|
});
|