@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.
@@ -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(title: string, subtitle: string, success: boolean): string {
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
- * Start a local HTTP server, open the browser to the platform login page,
180
- * and wait for the platform to redirect back with the session token.
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 browserLogin(webUrl: string): Promise<string> {
183
- return new Promise((resolve, reject) => {
184
- const state = randomBytes(32).toString("hex");
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 ?? "/", `http://localhost`);
188
-
189
- if (url.pathname !== "/callback") {
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 receivedState = url.searchParams.get("state");
196
- const sessionToken = url.searchParams.get("session_token");
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(renderLoginPage("Login Failed", "State mismatch. Please try again.", false));
201
- cleanup("State mismatch — possible CSRF attack.");
202
- return;
203
- }
204
-
205
- if (!sessionToken) {
206
- res.writeHead(400, { "Content-Type": "text/html" });
207
- res.end(renderLoginPage("Login Failed", "No session token received. Please try again.", false));
208
- cleanup("No session token received from platform.");
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(renderLoginPage("Login Successful", "You can close this window and return to your terminal.", true));
214
- cleanup(null, sessionToken);
215
- });
216
-
217
- const timeout = setTimeout(() => {
218
- cleanup("Login timed out. Please try again.");
219
- }, LOGIN_TIMEOUT_MS);
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
- if (error) {
225
- reject(new Error(error));
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", (err) => cleanup(err.message));
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
- cleanup("Failed to start local server.");
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
- const port = addr.port;
242
- const returnTo = `/accounts/cli/callback?port=${port}&state=${state}`;
243
- const loginUrl = `${webUrl}/account/login?returnTo=${encodeURIComponent(returnTo)}`;
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
- console.log("Opening browser for login...");
246
- console.log(`If the browser doesn't open, visit: ${loginUrl}`);
247
- openBrowser(loginUrl);
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 browser-based login
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 browserLogin(webUrl);
377
+ token = await workosPkceLogin(getPlatformUrl());
314
378
  } catch (error) {
315
379
  console.error(`❌ ${error instanceof Error ? error.message : error}`);
316
380
  process.exit(1);