@vellumai/cli 0.8.11 → 0.8.12-dev.202606122239.169d5e4
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/bun.lock +49 -56
- package/node_modules/@vellumai/local-mode/src/__tests__/wake.test.ts +19 -0
- package/node_modules/@vellumai/local-mode/src/index.ts +1 -1
- package/node_modules/@vellumai/local-mode/src/wake.ts +12 -1
- package/package.json +3 -3
- package/src/__tests__/login-loopback.test.ts +71 -0
- package/src/__tests__/platform-client.test.ts +107 -0
- package/src/__tests__/platform-releases.test.ts +117 -0
- package/src/__tests__/upgrade-preflight.test.ts +203 -0
- package/src/__tests__/version-compat.test.ts +31 -0
- package/src/__tests__/wake.test.ts +15 -4
- package/src/__tests__/workos-pkce.test.ts +314 -0
- package/src/commands/login.ts +123 -59
- package/src/commands/upgrade.ts +303 -41
- package/src/commands/wake.ts +7 -5
- package/src/lib/platform-client.ts +68 -0
- package/src/lib/platform-releases.ts +125 -103
- package/src/lib/upgrade-lifecycle.ts +12 -21
- package/src/lib/upgrade-preflight.ts +127 -0
- package/src/lib/version-compat.ts +18 -0
- package/src/lib/workos-pkce.ts +160 -0
package/src/commands/login.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { createServer } from "http";
|
|
2
1
|
import { spawn } from "child_process";
|
|
3
2
|
import { randomBytes } from "crypto";
|
|
3
|
+
import { createServer } from "http";
|
|
4
|
+
import type { AddressInfo } from "net";
|
|
4
5
|
|
|
5
6
|
import {
|
|
6
7
|
getActiveAssistant,
|
|
7
|
-
resolveAssistant,
|
|
8
8
|
loadAllAssistants,
|
|
9
9
|
removeAssistantEntry,
|
|
10
|
+
resolveAssistant,
|
|
10
11
|
setActiveAssistant,
|
|
11
12
|
} from "../lib/assistant-config";
|
|
12
13
|
import { computeDeviceId } from "../lib/guardian-token";
|
|
13
|
-
import {
|
|
14
|
-
fetchAssistantIngressUrl,
|
|
15
|
-
fetchCurrentVersion,
|
|
16
|
-
} from "../lib/upgrade-lifecycle.js";
|
|
17
14
|
import {
|
|
18
15
|
clearPlatformToken,
|
|
19
16
|
ensureSelfHostedLocalRegistration,
|
|
@@ -21,7 +18,6 @@ import {
|
|
|
21
18
|
fetchOrganizationId,
|
|
22
19
|
fetchPlatformAssistants,
|
|
23
20
|
getPlatformUrl,
|
|
24
|
-
getWebUrl,
|
|
25
21
|
injectCredentialsIntoAssistant,
|
|
26
22
|
readGatewayCredential,
|
|
27
23
|
readPlatformToken,
|
|
@@ -29,6 +25,18 @@ import {
|
|
|
29
25
|
savePlatformToken,
|
|
30
26
|
} from "../lib/platform-client";
|
|
31
27
|
import { syncCloudAssistants } from "../lib/sync-cloud-assistants";
|
|
28
|
+
import {
|
|
29
|
+
fetchAssistantIngressUrl,
|
|
30
|
+
fetchCurrentVersion,
|
|
31
|
+
} from "../lib/upgrade-lifecycle.js";
|
|
32
|
+
import {
|
|
33
|
+
CALLBACK_PATH,
|
|
34
|
+
buildAuthorizeUrl,
|
|
35
|
+
exchangeAccessTokenForSession,
|
|
36
|
+
exchangeCodeWithWorkos,
|
|
37
|
+
fetchWorkosClientId,
|
|
38
|
+
generatePkcePair,
|
|
39
|
+
} from "../lib/workos-pkce";
|
|
32
40
|
|
|
33
41
|
const LOGIN_TIMEOUT_MS = 120_000; // 2 minutes
|
|
34
42
|
|
|
@@ -41,7 +49,11 @@ function escapeHtml(s: string): string {
|
|
|
41
49
|
.replace(/'/g, "'");
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
function renderLoginPage(
|
|
52
|
+
function renderLoginPage(
|
|
53
|
+
title: string,
|
|
54
|
+
subtitle: string,
|
|
55
|
+
success: boolean,
|
|
56
|
+
): string {
|
|
45
57
|
const checkmarkSvg = `<svg class="icon" viewBox="0 0 56 56" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
46
58
|
<circle cx="28" cy="28" r="28" fill="var(--positive-bg)"/>
|
|
47
59
|
<path class="check" d="M17 28.5L24.5 36L39 21" stroke="var(--positive-fg)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
@@ -175,78 +187,131 @@ function openBrowser(url: string): void {
|
|
|
175
187
|
child.unref();
|
|
176
188
|
}
|
|
177
189
|
|
|
190
|
+
export interface LoopbackListener {
|
|
191
|
+
/** The full `http://127.0.0.1:<port>/auth/callback` redirect URI. */
|
|
192
|
+
redirectUri: string;
|
|
193
|
+
/** Resolves with the authorization code once the state-matched callback arrives. */
|
|
194
|
+
waitForCode: Promise<string>;
|
|
195
|
+
/** Tear down the server, rejecting any pending waiter with `reason`. */
|
|
196
|
+
close: (reason?: string) => void;
|
|
197
|
+
}
|
|
198
|
+
|
|
178
199
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
200
|
+
* Bind an ephemeral 127.0.0.1 listener and wait for the OAuth redirect.
|
|
201
|
+
* Exported for tests; production callers go through `workosPkceLogin`.
|
|
181
202
|
*/
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
203
|
+
export function startLoopbackListener(
|
|
204
|
+
expectedState: string,
|
|
205
|
+
): Promise<LoopbackListener> {
|
|
206
|
+
return new Promise((resolveListener, rejectListener) => {
|
|
207
|
+
let settle: {
|
|
208
|
+
resolve: (code: string) => void;
|
|
209
|
+
reject: (err: Error) => void;
|
|
210
|
+
};
|
|
211
|
+
const waitForCode = new Promise<string>((resolve, reject) => {
|
|
212
|
+
settle = { resolve, reject };
|
|
213
|
+
});
|
|
185
214
|
|
|
186
215
|
const server = createServer((req, res) => {
|
|
187
|
-
const url = new URL(req.url ?? "/",
|
|
188
|
-
|
|
189
|
-
|
|
216
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
217
|
+
if (
|
|
218
|
+
url.pathname !== CALLBACK_PATH ||
|
|
219
|
+
url.searchParams.get("state") !== expectedState
|
|
220
|
+
) {
|
|
190
221
|
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
191
222
|
res.end("Not found");
|
|
192
223
|
return;
|
|
193
224
|
}
|
|
194
225
|
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
if (receivedState !== state) {
|
|
226
|
+
const error = url.searchParams.get("error");
|
|
227
|
+
const code = url.searchParams.get("code");
|
|
228
|
+
if (error || !code) {
|
|
199
229
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
200
|
-
res.end(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
res.end(
|
|
231
|
+
renderLoginPage(
|
|
232
|
+
"Login Failed",
|
|
233
|
+
"Please try again from your terminal.",
|
|
234
|
+
false,
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
server.close();
|
|
238
|
+
settle.reject(
|
|
239
|
+
new Error(
|
|
240
|
+
`Authentication failed: ${error ?? "no authorization code received"}`,
|
|
241
|
+
),
|
|
242
|
+
);
|
|
209
243
|
return;
|
|
210
244
|
}
|
|
211
245
|
|
|
212
246
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
213
|
-
res.end(
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
function cleanup(error: string | null, token?: string): void {
|
|
222
|
-
clearTimeout(timeout);
|
|
247
|
+
res.end(
|
|
248
|
+
renderLoginPage(
|
|
249
|
+
"Login Successful",
|
|
250
|
+
"You can close this window and return to your terminal.",
|
|
251
|
+
true,
|
|
252
|
+
),
|
|
253
|
+
);
|
|
223
254
|
server.close();
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
} else if (token) {
|
|
227
|
-
resolve(token);
|
|
228
|
-
} else {
|
|
229
|
-
reject(new Error("Unknown error during login."));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
255
|
+
settle.resolve(code);
|
|
256
|
+
});
|
|
232
257
|
|
|
233
|
-
server.on("error",
|
|
258
|
+
server.on("error", rejectListener);
|
|
234
259
|
server.listen(0, "127.0.0.1", () => {
|
|
235
260
|
const addr = server.address();
|
|
236
261
|
if (!addr || typeof addr === "string") {
|
|
237
|
-
|
|
262
|
+
rejectListener(new Error("Failed to start local server."));
|
|
238
263
|
return;
|
|
239
264
|
}
|
|
265
|
+
const { port } = addr as AddressInfo;
|
|
266
|
+
resolveListener({
|
|
267
|
+
redirectUri: `http://127.0.0.1:${port}${CALLBACK_PATH}`,
|
|
268
|
+
waitForCode,
|
|
269
|
+
close: (reason?: string) => {
|
|
270
|
+
server.close();
|
|
271
|
+
settle.reject(new Error(reason ?? "Login cancelled."));
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** App-held WorkOS PKCE login */
|
|
279
|
+
async function workosPkceLogin(platformUrl: string): Promise<string> {
|
|
280
|
+
const clientId = await fetchWorkosClientId(platformUrl);
|
|
281
|
+
const { verifier, challenge } = generatePkcePair();
|
|
282
|
+
const state = randomBytes(32).toString("hex");
|
|
240
283
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
284
|
+
const listener = await startLoopbackListener(state);
|
|
285
|
+
const timeout = setTimeout(() => {
|
|
286
|
+
listener.close("Login timed out. Please try again.");
|
|
287
|
+
}, LOGIN_TIMEOUT_MS);
|
|
244
288
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
289
|
+
try {
|
|
290
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
291
|
+
clientId,
|
|
292
|
+
redirectUri: listener.redirectUri,
|
|
293
|
+
challenge,
|
|
294
|
+
state,
|
|
248
295
|
});
|
|
249
|
-
|
|
296
|
+
|
|
297
|
+
console.log("Opening browser for login...");
|
|
298
|
+
console.log(`If the browser doesn't open, visit: ${authorizeUrl}`);
|
|
299
|
+
openBrowser(authorizeUrl);
|
|
300
|
+
|
|
301
|
+
const code = await listener.waitForCode;
|
|
302
|
+
const accessToken = await exchangeCodeWithWorkos({
|
|
303
|
+
clientId,
|
|
304
|
+
code,
|
|
305
|
+
verifier,
|
|
306
|
+
});
|
|
307
|
+
return await exchangeAccessTokenForSession(
|
|
308
|
+
platformUrl,
|
|
309
|
+
clientId,
|
|
310
|
+
accessToken,
|
|
311
|
+
);
|
|
312
|
+
} finally {
|
|
313
|
+
clearTimeout(timeout);
|
|
314
|
+
}
|
|
250
315
|
}
|
|
251
316
|
|
|
252
317
|
export async function login(): Promise<void> {
|
|
@@ -306,11 +371,10 @@ export async function login(): Promise<void> {
|
|
|
306
371
|
}
|
|
307
372
|
}
|
|
308
373
|
|
|
309
|
-
// If no --token flag, use
|
|
374
|
+
// If no --token flag, use app-held WorkOS PKCE login.
|
|
310
375
|
if (!token) {
|
|
311
|
-
const webUrl = getWebUrl();
|
|
312
376
|
try {
|
|
313
|
-
token = await
|
|
377
|
+
token = await workosPkceLogin(getPlatformUrl());
|
|
314
378
|
} catch (error) {
|
|
315
379
|
console.error(`❌ ${error instanceof Error ? error.message : error}`);
|
|
316
380
|
process.exit(1);
|