@vellumai/cli 0.3.10 → 0.3.12

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/src/lib/local.ts CHANGED
@@ -4,7 +4,7 @@ import { createRequire } from "module";
4
4
  import { homedir } from "os";
5
5
  import { dirname, join } from "path";
6
6
 
7
- import { loadAllAssistants, loadLatestAssistant } from "./assistant-config.js";
7
+ import { loadLatestAssistant } from "./assistant-config.js";
8
8
  import { GATEWAY_PORT } from "./constants.js";
9
9
  import { stopProcessByPidFile } from "./process.js";
10
10
 
@@ -288,7 +288,7 @@ export async function startLocalDaemon(): Promise<void> {
288
288
  }
289
289
  }
290
290
 
291
- export async function startGateway(): Promise<string> {
291
+ export async function startGateway(assistantId?: string): Promise<string> {
292
292
  const publicUrl = await discoverPublicUrl();
293
293
  if (publicUrl) {
294
294
  console.log(` Public URL: ${publicUrl}`);
@@ -296,12 +296,12 @@ export async function startGateway(): Promise<string> {
296
296
 
297
297
  console.log("🌐 Starting gateway...");
298
298
 
299
- // Only auto-configure default routing when the workspace has exactly one
300
- // assistant. In multi-assistant deployments, falling back to "default"
301
- // would silently deliver unmapped Telegram chats to whichever assistant was
302
- // most recently hatched — keep the "reject" policy instead.
303
- const assistants = loadAllAssistants();
304
- const isSingleAssistant = assistants.length === 1;
299
+ // Resolve the default assistant ID for the gateway. Prefer the explicitly
300
+ // provided assistantId (from hatch), then env override, then lockfile.
301
+ const resolvedAssistantId =
302
+ assistantId
303
+ || process.env.GATEWAY_DEFAULT_ASSISTANT_ID
304
+ || loadLatestAssistant()?.assistantId;
305
305
 
306
306
  // Read the bearer token so the gateway can authenticate proxied requests
307
307
  // (e.g. from paired iOS devices). Respect VELLUM_HTTP_TOKEN_PATH and
@@ -318,12 +318,13 @@ export async function startGateway(): Promise<string> {
318
318
 
319
319
  // If no token is available (first startup — daemon hasn't written it yet),
320
320
  // poll for the file to appear. The daemon writes the token shortly after
321
- // startup, so a short wait avoids starting the gateway without auth
322
- // (which would leave it permanently unauthenticated since the gateway
323
- // config is loaded once at startup and never reloads).
321
+ // startup, so we wait generously the daemon socket wait is 15s, and
322
+ // the token may be written after the socket. Starting the gateway without
323
+ // auth is a security risk since the config is loaded once at startup and
324
+ // never reloads, so we fail rather than silently disabling auth.
324
325
  if (!runtimeProxyBearerToken) {
325
326
  console.log(" Waiting for bearer token file...");
326
- const maxWait = 10000;
327
+ const maxWait = 30000;
327
328
  const pollInterval = 500;
328
329
  const start = Date.now();
329
330
  while (Date.now() - start < maxWait) {
@@ -340,36 +341,30 @@ export async function startGateway(): Promise<string> {
340
341
  }
341
342
  }
342
343
 
343
- // If the token still isn't available after polling, fall back to starting
344
- // the gateway without auth so it doesn't block forever. This is a degraded
345
- // mode — the proxy will be broken because the runtime expects a token.
346
- const proxyRequireAuth = runtimeProxyBearerToken ? "true" : "false";
347
344
  if (!runtimeProxyBearerToken) {
348
- console.log(" ⚠️ Bearer token not found after 10s — gateway proxy auth disabled");
345
+ throw new Error(
346
+ `Bearer token file not found at ${httpTokenPath} after 30s.\n` +
347
+ " The gateway cannot start without authentication — this would leave the proxy permanently unauthenticated.\n" +
348
+ " Ensure the daemon is running and has written the token file, or set VELLUM_HTTP_TOKEN_PATH to the correct path.",
349
+ );
349
350
  }
350
351
 
351
352
  const gatewayEnv: Record<string, string> = {
352
353
  ...process.env as Record<string, string>,
353
354
  GATEWAY_RUNTIME_PROXY_ENABLED: "true",
354
- GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: proxyRequireAuth,
355
+ GATEWAY_RUNTIME_PROXY_REQUIRE_AUTH: "true",
356
+ RUNTIME_PROXY_BEARER_TOKEN: runtimeProxyBearerToken,
355
357
  RUNTIME_HTTP_PORT: process.env.RUNTIME_HTTP_PORT || "7821",
356
358
  };
357
359
 
358
- if (runtimeProxyBearerToken) {
359
- gatewayEnv.RUNTIME_PROXY_BEARER_TOKEN = runtimeProxyBearerToken;
360
- }
361
-
362
360
  if (process.env.GATEWAY_UNMAPPED_POLICY) {
363
361
  gatewayEnv.GATEWAY_UNMAPPED_POLICY = process.env.GATEWAY_UNMAPPED_POLICY;
364
- } else if (isSingleAssistant) {
362
+ } else {
365
363
  gatewayEnv.GATEWAY_UNMAPPED_POLICY = "default";
366
364
  }
367
365
 
368
- if (process.env.GATEWAY_DEFAULT_ASSISTANT_ID) {
369
- gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID = process.env.GATEWAY_DEFAULT_ASSISTANT_ID;
370
- } else if (isSingleAssistant) {
371
- gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID =
372
- assistants[0].assistantId || loadLatestAssistant()?.assistantId || "default";
366
+ if (resolvedAssistantId) {
367
+ gatewayEnv.GATEWAY_DEFAULT_ASSISTANT_ID = resolvedAssistantId;
373
368
  }
374
369
  const workspaceIngressPublicBaseUrl = readWorkspaceIngressPublicBaseUrl();
375
370
  const ingressPublicBaseUrl =
@@ -0,0 +1,74 @@
1
+ import { chmodSync, readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
2
+ import { homedir } from "os";
3
+ import { join, dirname } from "path";
4
+
5
+ const DEFAULT_PLATFORM_URL = "https://platform.vellum.ai";
6
+
7
+ function getPlatformTokenPath(): string {
8
+ const base = process.env.BASE_DATA_DIR || homedir();
9
+ return join(base, ".vellum", "platform-token");
10
+ }
11
+
12
+ export function getPlatformUrl(): string {
13
+ return process.env.VELLUM_PLATFORM_URL ?? DEFAULT_PLATFORM_URL;
14
+ }
15
+
16
+ export function readPlatformToken(): string | null {
17
+ try {
18
+ return readFileSync(getPlatformTokenPath(), "utf-8").trim();
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ export function savePlatformToken(token: string): void {
25
+ const tokenPath = getPlatformTokenPath();
26
+ const dir = dirname(tokenPath);
27
+ if (!existsSync(dir)) {
28
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
29
+ }
30
+ writeFileSync(tokenPath, token + "\n", { mode: 0o600 });
31
+ chmodSync(tokenPath, 0o600);
32
+ }
33
+
34
+ export function clearPlatformToken(): void {
35
+ try {
36
+ unlinkSync(getPlatformTokenPath());
37
+ } catch {
38
+ // already doesn't exist
39
+ }
40
+ }
41
+
42
+ export interface PlatformUser {
43
+ id: string;
44
+ email: string;
45
+ display: string;
46
+ }
47
+
48
+ interface AllauthSessionResponse {
49
+ status: number;
50
+ data: {
51
+ user: {
52
+ id: string;
53
+ email: string;
54
+ display: string;
55
+ };
56
+ };
57
+ }
58
+
59
+ export async function fetchCurrentUser(token: string): Promise<PlatformUser> {
60
+ const url = `${getPlatformUrl()}/_allauth/app/v1/auth/session`;
61
+ const response = await fetch(url, {
62
+ headers: { "X-Session-Token": token },
63
+ });
64
+
65
+ if (!response.ok) {
66
+ if (response.status === 401 || response.status === 403 || response.status === 410) {
67
+ throw new Error("Invalid or expired token. Please login again.");
68
+ }
69
+ throw new Error(`Platform API error: ${response.status} ${response.statusText}`);
70
+ }
71
+
72
+ const body = (await response.json()) as AllauthSessionResponse;
73
+ return body.data.user;
74
+ }
@@ -0,0 +1,4 @@
1
+ declare module "*.sh" {
2
+ const content: string;
3
+ export default content;
4
+ }