@vercel/oidc 3.0.5 → 3.1.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 +10 -0
- package/dist/auth-config.d.ts +30 -0
- package/dist/auth-config.js +84 -0
- package/dist/get-vercel-oidc-token.js +7 -3
- package/dist/get-vercel-oidc-token.test.d.ts +1 -0
- package/dist/get-vercel-oidc-token.test.js +245 -0
- 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 +1 -1
- package/dist/token-util.js +90 -68
- package/dist/token-util.test.d.ts +1 -0
- package/dist/token-util.test.js +229 -0
- package/dist/token.js +3 -3
- package/dist/token.test.js +1 -1
- package/docs/README.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @vercel/oidc
|
|
2
2
|
|
|
3
|
+
## 3.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Allow vercel/oidc to refresh the vercel CLI auth token when running locally ([#14543](https://github.com/vercel/vercel/pull/14543))
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- improve error messages for package consumers ([#14449](https://github.com/vercel/vercel/pull/14449))
|
|
12
|
+
|
|
3
13
|
## 3.0.5
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
|
@@ -0,0 +1,30 @@
|
|
|
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): boolean;
|
|
@@ -0,0 +1,84 @@
|
|
|
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) {
|
|
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
|
+
return authConfig.expiresAt >= nowInSeconds;
|
|
78
|
+
}
|
|
79
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
80
|
+
0 && (module.exports = {
|
|
81
|
+
isValidAccessToken,
|
|
82
|
+
readAuthConfig,
|
|
83
|
+
writeAuthConfig
|
|
84
|
+
});
|
|
@@ -52,11 +52,15 @@ async function getVercelOidcToken() {
|
|
|
52
52
|
token = getVercelOidcTokenSync();
|
|
53
53
|
}
|
|
54
54
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
let message = err instanceof Error ? err.message : "";
|
|
56
|
+
if (error instanceof Error) {
|
|
57
|
+
message = `${message}
|
|
57
58
|
${error.message}`;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
if (message) {
|
|
61
|
+
throw new import_token_error.VercelOidcTokenError(message);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
60
64
|
}
|
|
61
65
|
return token;
|
|
62
66
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,245 @@
|
|
|
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_crypto = require("crypto");
|
|
25
|
+
var import_vitest = require("vitest");
|
|
26
|
+
var os = __toESM(require("os"));
|
|
27
|
+
var path = __toESM(require("path"));
|
|
28
|
+
var fs = __toESM(require("fs"));
|
|
29
|
+
var import_token_io = require("./token-io");
|
|
30
|
+
var import_get_vercel_oidc_token = require("./get-vercel-oidc-token");
|
|
31
|
+
var tokenUtil = __toESM(require("./token-util"));
|
|
32
|
+
import_vitest.vi.mock("./token-io");
|
|
33
|
+
import_vitest.vi.mock("./get-context", () => ({
|
|
34
|
+
getContext: () => ({ headers: {} })
|
|
35
|
+
}));
|
|
36
|
+
(0, import_vitest.describe)("getVercelOidcToken - Error Scenarios", () => {
|
|
37
|
+
let rootDir;
|
|
38
|
+
let userDataDir;
|
|
39
|
+
let cliDataDir;
|
|
40
|
+
let tokenDataDir;
|
|
41
|
+
const projectId = "test-project-id";
|
|
42
|
+
const teamId = "test-team-id";
|
|
43
|
+
(0, import_vitest.beforeEach)(() => {
|
|
44
|
+
import_vitest.vi.clearAllMocks();
|
|
45
|
+
import_vitest.vi.restoreAllMocks();
|
|
46
|
+
process.env.VERCEL_OIDC_TOKEN = void 0;
|
|
47
|
+
const random = `test-${(0, import_crypto.randomUUID)()}`;
|
|
48
|
+
rootDir = path.join(os.tmpdir(), random);
|
|
49
|
+
userDataDir = path.join(rootDir, "data");
|
|
50
|
+
cliDataDir = path.join(userDataDir, "com.vercel.cli");
|
|
51
|
+
tokenDataDir = path.join(userDataDir, "com.vercel.token");
|
|
52
|
+
fs.mkdirSync(cliDataDir, { recursive: true });
|
|
53
|
+
fs.mkdirSync(tokenDataDir, { recursive: true });
|
|
54
|
+
fs.mkdirSync(path.join(rootDir, ".vercel"), {
|
|
55
|
+
recursive: true
|
|
56
|
+
});
|
|
57
|
+
import_vitest.vi.spyOn(process, "cwd").mockReturnValue(rootDir);
|
|
58
|
+
import_vitest.vi.mocked(import_token_io.findRootDir).mockReturnValue(rootDir);
|
|
59
|
+
import_vitest.vi.mocked(import_token_io.getUserDataDir).mockReturnValue(userDataDir);
|
|
60
|
+
});
|
|
61
|
+
(0, import_vitest.afterEach)(() => {
|
|
62
|
+
if (fs.existsSync(rootDir)) {
|
|
63
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
(0, import_vitest.test)("should throw helpful error when CLI auth file is missing", async () => {
|
|
67
|
+
fs.writeFileSync(
|
|
68
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
69
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
70
|
+
);
|
|
71
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
72
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
73
|
+
/Failed to refresh OIDC token: Log in to Vercel CLI and link your project with `vc link`/
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
(0, import_vitest.test)("should throw helpful error when project.json is missing", async () => {
|
|
77
|
+
fs.writeFileSync(
|
|
78
|
+
path.join(cliDataDir, "auth.json"),
|
|
79
|
+
JSON.stringify({ token: "test-auth-token" })
|
|
80
|
+
);
|
|
81
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
82
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
83
|
+
/project\.json not found, have you linked your project with `vc link\?`/
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
(0, import_vitest.test)("should throw helpful error when root directory cannot be found", async () => {
|
|
87
|
+
import_vitest.vi.mocked(import_token_io.findRootDir).mockReturnValue(null);
|
|
88
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
89
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
90
|
+
/Unable to find project root directory\. Have you linked your project with `vc link\?`/
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
(0, import_vitest.test)("should throw helpful error when user data directory cannot be found", async () => {
|
|
94
|
+
fs.writeFileSync(
|
|
95
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
96
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
97
|
+
);
|
|
98
|
+
import_vitest.vi.mocked(import_token_io.getUserDataDir).mockReturnValue(null);
|
|
99
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
100
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
101
|
+
/Unable to find user data directory\. Please reach out to Vercel support\./
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
(0, import_vitest.test)("should throw helpful error when API returns non-200", async () => {
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(cliDataDir, "auth.json"),
|
|
107
|
+
JSON.stringify({ token: "test-auth-token" })
|
|
108
|
+
);
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
111
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
112
|
+
);
|
|
113
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockRejectedValue(
|
|
114
|
+
new Error("Failed to refresh OIDC token: Unauthorized")
|
|
115
|
+
);
|
|
116
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
117
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
118
|
+
/Failed to refresh OIDC token: Unauthorized/
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
(0, import_vitest.test)("should throw helpful error when token response is malformed", async () => {
|
|
122
|
+
fs.writeFileSync(
|
|
123
|
+
path.join(cliDataDir, "auth.json"),
|
|
124
|
+
JSON.stringify({ token: "test-auth-token" })
|
|
125
|
+
);
|
|
126
|
+
fs.writeFileSync(
|
|
127
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
128
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
129
|
+
);
|
|
130
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockRejectedValue(
|
|
131
|
+
new TypeError(
|
|
132
|
+
"Vercel OIDC token is malformed. Expected a string-valued token property. Please run `vc env pull` and try again"
|
|
133
|
+
)
|
|
134
|
+
);
|
|
135
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
136
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
137
|
+
/Vercel OIDC token is malformed\. Expected a string-valued token property\. Please run `vc env pull` and try again/
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
(0, import_vitest.test)("should throw helpful error when token has invalid format", async () => {
|
|
141
|
+
process.env.VERCEL_OIDC_TOKEN = "not-a-valid-jwt-token";
|
|
142
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockImplementation(() => {
|
|
143
|
+
throw new Error("Invalid token. Please run `vc env pull` and try again");
|
|
144
|
+
});
|
|
145
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
146
|
+
/Invalid token\. Please run `vc env pull` and try again/
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
(0, import_vitest.test)("should throw error when token expiry check fails", async () => {
|
|
150
|
+
process.env.VERCEL_OIDC_TOKEN = "not-a-jwt-token";
|
|
151
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(
|
|
152
|
+
/Invalid token\. Please run `vc env pull` and try again/
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
(0, import_vitest.test)("should fail when no token exists and no CLI credentials available", async () => {
|
|
156
|
+
process.env.VERCEL_OIDC_TOKEN = void 0;
|
|
157
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(/Invalid token/);
|
|
158
|
+
});
|
|
159
|
+
(0, import_vitest.test)("should propagate filesystem errors when saving token fails", async () => {
|
|
160
|
+
fs.writeFileSync(
|
|
161
|
+
path.join(cliDataDir, "auth.json"),
|
|
162
|
+
JSON.stringify({ token: "test-auth-token" })
|
|
163
|
+
);
|
|
164
|
+
fs.writeFileSync(
|
|
165
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
166
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
167
|
+
);
|
|
168
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({
|
|
169
|
+
token: "new-valid-token"
|
|
170
|
+
});
|
|
171
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockReturnValue({
|
|
172
|
+
sub: "test-sub",
|
|
173
|
+
name: "test-name",
|
|
174
|
+
exp: Date.now() / 1e3 - 1e3
|
|
175
|
+
});
|
|
176
|
+
import_vitest.vi.spyOn(tokenUtil, "saveToken").mockImplementation(() => {
|
|
177
|
+
throw new Error("EACCES: permission denied");
|
|
178
|
+
});
|
|
179
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
180
|
+
await (0, import_vitest.expect)((0, import_get_vercel_oidc_token.getVercelOidcToken)()).rejects.toThrow(/EACCES|permission/i);
|
|
181
|
+
});
|
|
182
|
+
(0, import_vitest.test)("should succeed when valid token exists in env", async () => {
|
|
183
|
+
const validToken = createValidToken();
|
|
184
|
+
process.env.VERCEL_OIDC_TOKEN = validToken;
|
|
185
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockReturnValue({
|
|
186
|
+
sub: "test-sub",
|
|
187
|
+
name: "test-name",
|
|
188
|
+
exp: Date.now() / 1e3 + 43200
|
|
189
|
+
});
|
|
190
|
+
const token = await (0, import_get_vercel_oidc_token.getVercelOidcToken)();
|
|
191
|
+
(0, import_vitest.expect)(token).toBe(validToken);
|
|
192
|
+
});
|
|
193
|
+
(0, import_vitest.test)("should refresh when token is expired but all configs are valid", async () => {
|
|
194
|
+
fs.writeFileSync(
|
|
195
|
+
path.join(cliDataDir, "auth.json"),
|
|
196
|
+
JSON.stringify({ token: "test-auth-token" })
|
|
197
|
+
);
|
|
198
|
+
fs.writeFileSync(
|
|
199
|
+
path.join(rootDir, ".vercel", "project.json"),
|
|
200
|
+
JSON.stringify({ projectId, orgId: teamId })
|
|
201
|
+
);
|
|
202
|
+
const newToken = createValidToken("new-token");
|
|
203
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({
|
|
204
|
+
token: newToken
|
|
205
|
+
});
|
|
206
|
+
import_vitest.vi.spyOn(tokenUtil, "getTokenPayload").mockReturnValueOnce({
|
|
207
|
+
sub: "test-sub",
|
|
208
|
+
name: "test-name",
|
|
209
|
+
exp: Date.now() / 1e3 - 1e3
|
|
210
|
+
}).mockReturnValue({
|
|
211
|
+
sub: "test-sub",
|
|
212
|
+
name: "test-name",
|
|
213
|
+
exp: Date.now() / 1e3 + 43200
|
|
214
|
+
});
|
|
215
|
+
process.env.VERCEL_OIDC_TOKEN = createExpiredToken();
|
|
216
|
+
const token = await (0, import_get_vercel_oidc_token.getVercelOidcToken)();
|
|
217
|
+
(0, import_vitest.expect)(token).toBe(newToken);
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
function createExpiredToken() {
|
|
221
|
+
const header = Buffer.from(
|
|
222
|
+
JSON.stringify({ alg: "RS256", typ: "JWT" })
|
|
223
|
+
).toString("base64url");
|
|
224
|
+
const payload = Buffer.from(
|
|
225
|
+
JSON.stringify({
|
|
226
|
+
sub: "test-sub",
|
|
227
|
+
exp: 1,
|
|
228
|
+
iat: 1
|
|
229
|
+
})
|
|
230
|
+
).toString("base64url");
|
|
231
|
+
return `${header}.${payload}.fake_signature`;
|
|
232
|
+
}
|
|
233
|
+
function createValidToken(value = "valid-token") {
|
|
234
|
+
const header = Buffer.from(
|
|
235
|
+
JSON.stringify({ alg: "RS256", typ: "JWT" })
|
|
236
|
+
).toString("base64url");
|
|
237
|
+
const payload = Buffer.from(
|
|
238
|
+
JSON.stringify({
|
|
239
|
+
sub: "test-sub",
|
|
240
|
+
exp: Math.floor(Date.now() / 1e3) + 43200,
|
|
241
|
+
iat: Math.floor(Date.now() / 1e3)
|
|
242
|
+
})
|
|
243
|
+
).toString("base64url");
|
|
244
|
+
return `${header}.${payload}.fake_signature_${value}`;
|
|
245
|
+
}
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface TokenSet {
|
|
2
|
+
access_token: string;
|
|
3
|
+
token_type: 'Bearer';
|
|
4
|
+
expires_in: number;
|
|
5
|
+
refresh_token?: string;
|
|
6
|
+
scope?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Refresh an OAuth access token using a refresh token
|
|
10
|
+
*/
|
|
11
|
+
export declare function refreshTokenRequest(options: {
|
|
12
|
+
refresh_token: string;
|
|
13
|
+
}): Promise<Response>;
|
|
14
|
+
/**
|
|
15
|
+
* Process the token response and extract the token set
|
|
16
|
+
*/
|
|
17
|
+
export declare function processTokenResponse(response: Response): Promise<[Error] | [null, TokenSet]>;
|
package/dist/oauth.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var oauth_exports = {};
|
|
20
|
+
__export(oauth_exports, {
|
|
21
|
+
processTokenResponse: () => processTokenResponse,
|
|
22
|
+
refreshTokenRequest: () => refreshTokenRequest
|
|
23
|
+
});
|
|
24
|
+
module.exports = __toCommonJS(oauth_exports);
|
|
25
|
+
var import_os = require("os");
|
|
26
|
+
const VERCEL_ISSUER = "https://vercel.com";
|
|
27
|
+
const VERCEL_CLI_CLIENT_ID = "cl_HYyOPBNtFMfHhaUn9L4QPfTZz6TP47bp";
|
|
28
|
+
const userAgent = `@vercel/oidc node-${process.version} ${(0, import_os.platform)()} (${(0, import_os.arch)()}) ${(0, import_os.hostname)()}`;
|
|
29
|
+
let _tokenEndpoint = null;
|
|
30
|
+
async function getTokenEndpoint() {
|
|
31
|
+
if (_tokenEndpoint) {
|
|
32
|
+
return _tokenEndpoint;
|
|
33
|
+
}
|
|
34
|
+
const discoveryUrl = `${VERCEL_ISSUER}/.well-known/openid-configuration`;
|
|
35
|
+
const response = await fetch(discoveryUrl, {
|
|
36
|
+
headers: { "user-agent": userAgent }
|
|
37
|
+
});
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error("Failed to discover OAuth endpoints");
|
|
40
|
+
}
|
|
41
|
+
const metadata = await response.json();
|
|
42
|
+
if (!metadata || typeof metadata.token_endpoint !== "string") {
|
|
43
|
+
throw new Error("Invalid OAuth discovery response");
|
|
44
|
+
}
|
|
45
|
+
const endpoint = metadata.token_endpoint;
|
|
46
|
+
_tokenEndpoint = endpoint;
|
|
47
|
+
return endpoint;
|
|
48
|
+
}
|
|
49
|
+
async function refreshTokenRequest(options) {
|
|
50
|
+
const tokenEndpoint = await getTokenEndpoint();
|
|
51
|
+
return await fetch(tokenEndpoint, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
headers: {
|
|
54
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
55
|
+
"user-agent": userAgent
|
|
56
|
+
},
|
|
57
|
+
body: new URLSearchParams({
|
|
58
|
+
client_id: VERCEL_CLI_CLIENT_ID,
|
|
59
|
+
grant_type: "refresh_token",
|
|
60
|
+
...options
|
|
61
|
+
})
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
async function processTokenResponse(response) {
|
|
65
|
+
const json = await response.json();
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
const errorMsg = typeof json === "object" && json && "error" in json ? String(json.error) : "Token refresh failed";
|
|
68
|
+
return [new Error(errorMsg)];
|
|
69
|
+
}
|
|
70
|
+
if (typeof json !== "object" || json === null) {
|
|
71
|
+
return [new Error("Invalid token response")];
|
|
72
|
+
}
|
|
73
|
+
if (typeof json.access_token !== "string") {
|
|
74
|
+
return [new Error("Missing access_token in response")];
|
|
75
|
+
}
|
|
76
|
+
if (json.token_type !== "Bearer") {
|
|
77
|
+
return [new Error("Invalid token_type in response")];
|
|
78
|
+
}
|
|
79
|
+
if (typeof json.expires_in !== "number") {
|
|
80
|
+
return [new Error("Missing expires_in in response")];
|
|
81
|
+
}
|
|
82
|
+
return [null, json];
|
|
83
|
+
}
|
|
84
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
85
|
+
0 && (module.exports = {
|
|
86
|
+
processTokenResponse,
|
|
87
|
+
refreshTokenRequest
|
|
88
|
+
});
|
package/dist/token-io.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function findRootDir(): string;
|
|
1
|
+
export declare function findRootDir(): string | null;
|
|
2
2
|
export declare function getUserDataDir(): string | null;
|
package/dist/token-io.js
CHANGED
|
@@ -51,7 +51,7 @@ function findRootDir() {
|
|
|
51
51
|
"Token refresh only supported in node server environments"
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
return null;
|
|
55
55
|
}
|
|
56
56
|
function getUserDataDir() {
|
|
57
57
|
if (process.env.XDG_DATA_HOME) {
|
package/dist/token-util.d.ts
CHANGED
package/dist/token-util.js
CHANGED
|
@@ -43,6 +43,8 @@ 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");
|
|
46
48
|
function getVercelDataDir() {
|
|
47
49
|
const vercelFolder = "com.vercel.cli";
|
|
48
50
|
const dataDir = (0, import_token_io.getUserDataDir)();
|
|
@@ -51,106 +53,126 @@ function getVercelDataDir() {
|
|
|
51
53
|
}
|
|
52
54
|
return path.join(dataDir, vercelFolder);
|
|
53
55
|
}
|
|
54
|
-
function getVercelCliToken() {
|
|
55
|
-
const
|
|
56
|
-
if (!
|
|
56
|
+
async function getVercelCliToken() {
|
|
57
|
+
const authConfig = (0, import_auth_config.readAuthConfig)();
|
|
58
|
+
if (!authConfig) {
|
|
57
59
|
return null;
|
|
58
60
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if ((0, import_auth_config.isValidAccessToken)(authConfig)) {
|
|
62
|
+
return authConfig.token || null;
|
|
63
|
+
}
|
|
64
|
+
if (!authConfig.refreshToken) {
|
|
65
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
61
66
|
return null;
|
|
62
67
|
}
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
try {
|
|
69
|
+
const tokenResponse = await (0, import_oauth.refreshTokenRequest)({
|
|
70
|
+
refresh_token: authConfig.refreshToken
|
|
71
|
+
});
|
|
72
|
+
const [tokensError, tokens] = await (0, import_oauth.processTokenResponse)(tokenResponse);
|
|
73
|
+
if (tokensError || !tokens) {
|
|
74
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const updatedConfig = {
|
|
78
|
+
token: tokens.access_token,
|
|
79
|
+
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
80
|
+
};
|
|
81
|
+
if (tokens.refresh_token) {
|
|
82
|
+
updatedConfig.refreshToken = tokens.refresh_token;
|
|
83
|
+
}
|
|
84
|
+
(0, import_auth_config.writeAuthConfig)(updatedConfig);
|
|
85
|
+
return updatedConfig.token ?? null;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
(0, import_auth_config.writeAuthConfig)({});
|
|
65
88
|
return null;
|
|
66
89
|
}
|
|
67
|
-
return JSON.parse(token).token;
|
|
68
90
|
}
|
|
69
91
|
async function getVercelOidcToken(authToken, projectId, teamId) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Authorization: `Bearer ${authToken}`
|
|
76
|
-
}
|
|
77
|
-
});
|
|
78
|
-
if (!res.ok) {
|
|
79
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
80
|
-
`Failed to refresh OIDC token: ${res.statusText}`
|
|
81
|
-
);
|
|
92
|
+
const url = `https://api.vercel.com/v1/projects/${projectId}/token?source=vercel-oidc-refresh${teamId ? `&teamId=${teamId}` : ""}`;
|
|
93
|
+
const res = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: `Bearer ${authToken}`
|
|
82
97
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
101
|
+
`Failed to refresh OIDC token: ${res.statusText}`
|
|
102
|
+
);
|
|
88
103
|
}
|
|
104
|
+
const tokenRes = await res.json();
|
|
105
|
+
assertVercelOidcTokenResponse(tokenRes);
|
|
106
|
+
return tokenRes;
|
|
89
107
|
}
|
|
90
108
|
function assertVercelOidcTokenResponse(res) {
|
|
91
109
|
if (!res || typeof res !== "object") {
|
|
92
|
-
throw new TypeError(
|
|
110
|
+
throw new TypeError(
|
|
111
|
+
"Vercel OIDC token is malformed. Expected an object. Please run `vc env pull` and try again"
|
|
112
|
+
);
|
|
93
113
|
}
|
|
94
114
|
if (!("token" in res) || typeof res.token !== "string") {
|
|
95
|
-
throw new TypeError(
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
"Vercel OIDC token is malformed. Expected a string-valued token property. Please run `vc env pull` and try again"
|
|
117
|
+
);
|
|
96
118
|
}
|
|
97
119
|
}
|
|
98
120
|
function findProjectInfo() {
|
|
99
121
|
const dir = (0, import_token_io.findRootDir)();
|
|
100
122
|
if (!dir) {
|
|
101
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
123
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
124
|
+
"Unable to find project root directory. Have you linked your project with `vc link?`"
|
|
125
|
+
);
|
|
102
126
|
}
|
|
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);
|
|
127
|
+
const prjPath = path.join(dir, ".vercel", "project.json");
|
|
128
|
+
if (!fs.existsSync(prjPath)) {
|
|
129
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
130
|
+
"project.json not found, have you linked your project with `vc link?`"
|
|
131
|
+
);
|
|
115
132
|
}
|
|
133
|
+
const prj = JSON.parse(fs.readFileSync(prjPath, "utf8"));
|
|
134
|
+
if (typeof prj.projectId !== "string" && typeof prj.orgId !== "string") {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
"Expected a string-valued projectId property. Try running `vc link` to re-link your project."
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return { projectId: prj.projectId, teamId: prj.orgId };
|
|
116
140
|
}
|
|
117
141
|
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);
|
|
142
|
+
const dir = (0, import_token_io.getUserDataDir)();
|
|
143
|
+
if (!dir) {
|
|
144
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
145
|
+
"Unable to find user data directory. Please reach out to Vercel support."
|
|
146
|
+
);
|
|
131
147
|
}
|
|
148
|
+
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
|
|
149
|
+
const tokenJson = JSON.stringify(token);
|
|
150
|
+
fs.mkdirSync(path.dirname(tokenPath), { mode: 504, recursive: true });
|
|
151
|
+
fs.writeFileSync(tokenPath, tokenJson);
|
|
152
|
+
fs.chmodSync(tokenPath, 432);
|
|
153
|
+
return;
|
|
132
154
|
}
|
|
133
155
|
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);
|
|
156
|
+
const dir = (0, import_token_io.getUserDataDir)();
|
|
157
|
+
if (!dir) {
|
|
158
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
159
|
+
"Unable to find user data directory. Please reach out to Vercel support."
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
const tokenPath = path.join(dir, "com.vercel.token", `${projectId}.json`);
|
|
163
|
+
if (!fs.existsSync(tokenPath)) {
|
|
164
|
+
return null;
|
|
148
165
|
}
|
|
166
|
+
const token = JSON.parse(fs.readFileSync(tokenPath, "utf8"));
|
|
167
|
+
assertVercelOidcTokenResponse(token);
|
|
168
|
+
return token;
|
|
149
169
|
}
|
|
150
170
|
function getTokenPayload(token) {
|
|
151
171
|
const tokenParts = token.split(".");
|
|
152
172
|
if (tokenParts.length !== 3) {
|
|
153
|
-
throw new import_token_error.VercelOidcTokenError(
|
|
173
|
+
throw new import_token_error.VercelOidcTokenError(
|
|
174
|
+
"Invalid token. Please run `vc env pull` and try again"
|
|
175
|
+
);
|
|
154
176
|
}
|
|
155
177
|
const base64 = tokenParts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
156
178
|
const padded = base64.padEnd(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
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
|
+
import_vitest.vi.mock("fs");
|
|
29
|
+
import_vitest.vi.mock("./token-io", () => ({
|
|
30
|
+
getUserDataDir: import_vitest.vi.fn(() => "/mock/user/data"),
|
|
31
|
+
findRootDir: import_vitest.vi.fn(() => "/mock/root")
|
|
32
|
+
}));
|
|
33
|
+
(0, import_vitest.describe)("getVercelCliToken", () => {
|
|
34
|
+
(0, import_vitest.beforeEach)(() => {
|
|
35
|
+
import_vitest.vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
(0, import_vitest.afterEach)(() => {
|
|
38
|
+
import_vitest.vi.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
(0, import_vitest.it)("should return token if valid and not expired", async () => {
|
|
41
|
+
const validToken = {
|
|
42
|
+
token: "valid-access-token",
|
|
43
|
+
refreshToken: "refresh-token",
|
|
44
|
+
expiresAt: Math.floor(Date.now() / 1e3) + 3600
|
|
45
|
+
// expires in 1 hour
|
|
46
|
+
};
|
|
47
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(validToken);
|
|
48
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
49
|
+
});
|
|
50
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
51
|
+
(0, import_vitest.expect)(token).toBe("valid-access-token");
|
|
52
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
(0, import_vitest.it)("should return null if auth config does not exist", async () => {
|
|
55
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(null);
|
|
56
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
57
|
+
(0, import_vitest.expect)(token).toBeNull();
|
|
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.getVercelCliToken)();
|
|
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 return null 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
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
103
|
+
(0, import_vitest.expect)(token).toBeNull();
|
|
104
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
105
|
+
});
|
|
106
|
+
(0, import_vitest.it)("should clear auth if refresh fails with OAuth error", async () => {
|
|
107
|
+
const expiredToken = {
|
|
108
|
+
token: "expired-access-token",
|
|
109
|
+
refreshToken: "invalid-refresh-token",
|
|
110
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
111
|
+
};
|
|
112
|
+
const mockErrorResponse = {
|
|
113
|
+
ok: false,
|
|
114
|
+
json: async () => ({
|
|
115
|
+
error: "invalid_grant",
|
|
116
|
+
error_description: "Refresh token expired"
|
|
117
|
+
})
|
|
118
|
+
};
|
|
119
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
120
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
121
|
+
});
|
|
122
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockErrorResponse);
|
|
123
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
124
|
+
(0, import_vitest.expect)(token).toBeNull();
|
|
125
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
126
|
+
});
|
|
127
|
+
(0, import_vitest.it)("should clear auth if refresh fails with network error", async () => {
|
|
128
|
+
const expiredToken = {
|
|
129
|
+
token: "expired-access-token",
|
|
130
|
+
refreshToken: "valid-refresh-token",
|
|
131
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
132
|
+
};
|
|
133
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
134
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
135
|
+
});
|
|
136
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockRejectedValue(
|
|
137
|
+
new Error("Network error")
|
|
138
|
+
);
|
|
139
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
140
|
+
(0, import_vitest.expect)(token).toBeNull();
|
|
141
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith({});
|
|
142
|
+
});
|
|
143
|
+
(0, import_vitest.it)("should treat token as valid if expiresAt is missing (--token case)", async () => {
|
|
144
|
+
const tokenWithoutExpiry = {
|
|
145
|
+
token: "cli-provided-token"
|
|
146
|
+
};
|
|
147
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(tokenWithoutExpiry);
|
|
148
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
149
|
+
});
|
|
150
|
+
const token = await (0, import_token_util.getVercelCliToken)();
|
|
151
|
+
(0, import_vitest.expect)(token).toBe("cli-provided-token");
|
|
152
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).not.toHaveBeenCalled();
|
|
153
|
+
});
|
|
154
|
+
(0, import_vitest.it)("should preserve new refresh token if provided in response", async () => {
|
|
155
|
+
const expiredToken = {
|
|
156
|
+
token: "expired-access-token",
|
|
157
|
+
refreshToken: "old-refresh-token",
|
|
158
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
159
|
+
};
|
|
160
|
+
const mockResponse = {
|
|
161
|
+
ok: true,
|
|
162
|
+
json: async () => ({
|
|
163
|
+
access_token: "new-access-token",
|
|
164
|
+
token_type: "Bearer",
|
|
165
|
+
expires_in: 3600,
|
|
166
|
+
refresh_token: "new-refresh-token"
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
170
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
171
|
+
});
|
|
172
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
173
|
+
await (0, import_token_util.getVercelCliToken)();
|
|
174
|
+
(0, import_vitest.expect)(authConfig.writeAuthConfig).toHaveBeenCalledWith(
|
|
175
|
+
import_vitest.expect.objectContaining({
|
|
176
|
+
refreshToken: "new-refresh-token"
|
|
177
|
+
})
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
(0, import_vitest.it)("should not overwrite refresh token if not provided in response", async () => {
|
|
181
|
+
const expiredToken = {
|
|
182
|
+
token: "expired-access-token",
|
|
183
|
+
refreshToken: "existing-refresh-token",
|
|
184
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
185
|
+
};
|
|
186
|
+
const mockResponse = {
|
|
187
|
+
ok: true,
|
|
188
|
+
json: async () => ({
|
|
189
|
+
access_token: "new-access-token",
|
|
190
|
+
token_type: "Bearer",
|
|
191
|
+
expires_in: 3600
|
|
192
|
+
// No refresh_token in response
|
|
193
|
+
})
|
|
194
|
+
};
|
|
195
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
196
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
197
|
+
});
|
|
198
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
199
|
+
await (0, import_token_util.getVercelCliToken)();
|
|
200
|
+
const writeCall = import_vitest.vi.mocked(authConfig.writeAuthConfig).mock.calls[0][0];
|
|
201
|
+
(0, import_vitest.expect)(writeCall).not.toHaveProperty("refreshToken");
|
|
202
|
+
});
|
|
203
|
+
(0, import_vitest.it)("should calculate expiresAt correctly from expires_in", async () => {
|
|
204
|
+
const expiredToken = {
|
|
205
|
+
token: "expired-access-token",
|
|
206
|
+
refreshToken: "valid-refresh-token",
|
|
207
|
+
expiresAt: Math.floor(Date.now() / 1e3) - 3600
|
|
208
|
+
};
|
|
209
|
+
const mockResponse = {
|
|
210
|
+
ok: true,
|
|
211
|
+
json: async () => ({
|
|
212
|
+
access_token: "new-access-token",
|
|
213
|
+
token_type: "Bearer",
|
|
214
|
+
expires_in: 7200
|
|
215
|
+
// 2 hours
|
|
216
|
+
})
|
|
217
|
+
};
|
|
218
|
+
import_vitest.vi.spyOn(authConfig, "readAuthConfig").mockReturnValue(expiredToken);
|
|
219
|
+
import_vitest.vi.spyOn(authConfig, "writeAuthConfig").mockImplementation(() => {
|
|
220
|
+
});
|
|
221
|
+
import_vitest.vi.spyOn(oauth, "refreshTokenRequest").mockResolvedValue(mockResponse);
|
|
222
|
+
const beforeCall = Math.floor(Date.now() / 1e3);
|
|
223
|
+
await (0, import_token_util.getVercelCliToken)();
|
|
224
|
+
const afterCall = Math.floor(Date.now() / 1e3);
|
|
225
|
+
const writeCall = import_vitest.vi.mocked(authConfig.writeAuthConfig).mock.calls[0][0];
|
|
226
|
+
(0, import_vitest.expect)(writeCall.expiresAt).toBeGreaterThanOrEqual(beforeCall + 7200);
|
|
227
|
+
(0, import_vitest.expect)(writeCall.expiresAt).toBeLessThanOrEqual(afterCall + 7200);
|
|
228
|
+
});
|
|
229
|
+
});
|
package/dist/token.js
CHANGED
|
@@ -27,15 +27,15 @@ async function refreshToken() {
|
|
|
27
27
|
const { projectId, teamId } = (0, import_token_util.findProjectInfo)();
|
|
28
28
|
let maybeToken = (0, import_token_util.loadToken)(projectId);
|
|
29
29
|
if (!maybeToken || (0, import_token_util.isExpired)((0, import_token_util.getTokenPayload)(maybeToken.token))) {
|
|
30
|
-
const authToken = (0, import_token_util.getVercelCliToken)();
|
|
30
|
+
const authToken = await (0, import_token_util.getVercelCliToken)();
|
|
31
31
|
if (!authToken) {
|
|
32
32
|
throw new import_token_error.VercelOidcTokenError(
|
|
33
|
-
"Failed to refresh OIDC token:
|
|
33
|
+
"Failed to refresh OIDC token: Log in to Vercel CLI and link your project with `vc link`"
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
if (!projectId) {
|
|
37
37
|
throw new import_token_error.VercelOidcTokenError(
|
|
38
|
-
"Failed to refresh OIDC token: project
|
|
38
|
+
"Failed to refresh OIDC token: Try re-linking your project with `vc link`"
|
|
39
39
|
);
|
|
40
40
|
}
|
|
41
41
|
maybeToken = await (0, import_token_util.getVercelOidcToken)(authToken, projectId, teamId);
|
package/dist/token.test.js
CHANGED
|
@@ -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, "getVercelCliToken").
|
|
60
|
+
import_vitest.vi.spyOn(tokenUtil, "getVercelCliToken").mockResolvedValue("test");
|
|
61
61
|
import_vitest.vi.spyOn(tokenUtil, "getVercelOidcToken").mockResolvedValue({
|
|
62
62
|
token: "test-token"
|
|
63
63
|
});
|
package/docs/README.md
CHANGED
|
@@ -101,4 +101,4 @@ The OIDC token.
|
|
|
101
101
|
|
|
102
102
|
#### Defined in
|
|
103
103
|
|
|
104
|
-
[get-vercel-oidc-token.ts:
|
|
104
|
+
[get-vercel-oidc-token.ts:85](https://github.com/vercel/vercel/blob/main/packages/oidc/src/get-vercel-oidc-token.ts#L85)
|