@vellumai/cli 0.10.11-dev.202607220859.296f4b0 → 0.10.11-dev.202607220955.d4134f8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.10.11-dev.202607220859.296f4b0",
3
+ "version": "0.10.11-dev.202607220955.d4134f8",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -931,4 +931,307 @@ describe("pair command", () => {
931
931
  expect(errors.join("\n")).toContain("--qr");
932
932
  expect(fetchCalled).toBe(false);
933
933
  });
934
+
935
+ test("--qr with no --url uses the tunnel-saved ingress URL", async () => {
936
+ const workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pair-ingress-"));
937
+ // Point the synthesized instance dir at an empty temp XDG root so the
938
+ // resolution deterministically falls through to VELLUM_WORKSPACE_DIR.
939
+ const xdgDir = mkdtempSync(join(tmpdir(), "vellum-pair-xdg-"));
940
+ process.env.XDG_DATA_HOME = xdgDir;
941
+ writeFileSync(
942
+ join(workspaceDir, "config.json"),
943
+ JSON.stringify({
944
+ ingress: {
945
+ publicBaseUrl: "https://saved.example.ts.net",
946
+ enabled: true,
947
+ },
948
+ }),
949
+ );
950
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
951
+
952
+ const calls: string[] = [];
953
+ const origFetch = globalThis.fetch;
954
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
955
+ calls.push(url);
956
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
957
+ return new Response(
958
+ JSON.stringify({
959
+ flags: [{ key: "web-remote-ingress", enabled: true }],
960
+ }),
961
+ { status: 200, headers: { "content-type": "application/json" } },
962
+ );
963
+ }
964
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
965
+ // The minted challenge must advertise the SAVED ingress URL.
966
+ expect(JSON.parse(init?.body as string)).toEqual({
967
+ publicBaseUrl: "https://saved.example.ts.net",
968
+ });
969
+ return new Response(
970
+ JSON.stringify({
971
+ deviceCode: "device-code",
972
+ userCode: "ABCD-EFGH",
973
+ verificationUri: "https://saved.example.ts.net/assistant/pair",
974
+ expiresAt: "2026-06-04T00:10:00.000Z",
975
+ expiresInSeconds: 600,
976
+ }),
977
+ { status: 200, headers: { "content-type": "application/json" } },
978
+ );
979
+ }
980
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
981
+ return new Response(
982
+ JSON.stringify({
983
+ status: "approved",
984
+ verificationUri: "https://saved.example.ts.net/assistant/pair",
985
+ expiresAt: "2026-06-04T00:10:00.000Z",
986
+ }),
987
+ { status: 200, headers: { "content-type": "application/json" } },
988
+ );
989
+ }
990
+ return new Response("not found", { status: 404 });
991
+ }) as unknown as typeof fetch;
992
+
993
+ const logs: string[] = [];
994
+ const logSpy = spyOn(console, "log").mockImplementation(
995
+ (...a: unknown[]) => {
996
+ logs.push(a.join(" "));
997
+ },
998
+ );
999
+
1000
+ process.argv = ["bun", "vellum", "pair", "--qr"];
1001
+ try {
1002
+ await pair();
1003
+ } finally {
1004
+ logSpy.mockRestore();
1005
+ globalThis.fetch = origFetch;
1006
+ delete process.env.VELLUM_WORKSPACE_DIR;
1007
+ delete process.env.XDG_DATA_HOME;
1008
+ rmSync(workspaceDir, { recursive: true, force: true });
1009
+ rmSync(xdgDir, { recursive: true, force: true });
1010
+ }
1011
+
1012
+ expect(calls).toContain(`${LOCAL_URL}/v1/remote-web/pairing-challenge`);
1013
+ const output = logs.join("\n");
1014
+ expect(output).toContain(
1015
+ "Using saved ingress URL https://saved.example.ts.net",
1016
+ );
1017
+ expect(output).toContain(
1018
+ "https://saved.example.ts.net/assistant/pair#device_code=device-code",
1019
+ );
1020
+ });
1021
+
1022
+ test("--url beats the saved ingress URL", async () => {
1023
+ const workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pair-ingress-"));
1024
+ // Point the synthesized instance dir at an empty temp XDG root so the
1025
+ // resolution deterministically falls through to VELLUM_WORKSPACE_DIR.
1026
+ const xdgDir = mkdtempSync(join(tmpdir(), "vellum-pair-xdg-"));
1027
+ process.env.XDG_DATA_HOME = xdgDir;
1028
+ writeFileSync(
1029
+ join(workspaceDir, "config.json"),
1030
+ JSON.stringify({
1031
+ ingress: {
1032
+ publicBaseUrl: "https://saved.example.ts.net",
1033
+ enabled: true,
1034
+ },
1035
+ }),
1036
+ );
1037
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
1038
+
1039
+ const origFetch = globalThis.fetch;
1040
+ globalThis.fetch = (async (url: string, init?: RequestInit) => {
1041
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
1042
+ return new Response(
1043
+ JSON.stringify({
1044
+ flags: [{ key: "web-remote-ingress", enabled: true }],
1045
+ }),
1046
+ { status: 200, headers: { "content-type": "application/json" } },
1047
+ );
1048
+ }
1049
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-challenge`) {
1050
+ expect(JSON.parse(init?.body as string)).toEqual({
1051
+ publicBaseUrl: "https://explicit.example.ts.net",
1052
+ });
1053
+ return new Response(
1054
+ JSON.stringify({
1055
+ deviceCode: "device-code",
1056
+ userCode: "ABCD-EFGH",
1057
+ verificationUri: "https://explicit.example.ts.net/assistant/pair",
1058
+ expiresAt: "2026-06-04T00:10:00.000Z",
1059
+ expiresInSeconds: 600,
1060
+ }),
1061
+ { status: 200, headers: { "content-type": "application/json" } },
1062
+ );
1063
+ }
1064
+ if (url === `${LOCAL_URL}/v1/remote-web/pairing-verification`) {
1065
+ return new Response(
1066
+ JSON.stringify({
1067
+ status: "approved",
1068
+ verificationUri: "https://explicit.example.ts.net/assistant/pair",
1069
+ expiresAt: "2026-06-04T00:10:00.000Z",
1070
+ }),
1071
+ { status: 200, headers: { "content-type": "application/json" } },
1072
+ );
1073
+ }
1074
+ return new Response("not found", { status: 404 });
1075
+ }) as unknown as typeof fetch;
1076
+
1077
+ const logs: string[] = [];
1078
+ const logSpy = spyOn(console, "log").mockImplementation(
1079
+ (...a: unknown[]) => {
1080
+ logs.push(a.join(" "));
1081
+ },
1082
+ );
1083
+
1084
+ process.argv = [
1085
+ "bun",
1086
+ "vellum",
1087
+ "pair",
1088
+ "--qr",
1089
+ "--url",
1090
+ "https://explicit.example.ts.net",
1091
+ ];
1092
+ try {
1093
+ await pair();
1094
+ } finally {
1095
+ logSpy.mockRestore();
1096
+ globalThis.fetch = origFetch;
1097
+ delete process.env.VELLUM_WORKSPACE_DIR;
1098
+ delete process.env.XDG_DATA_HOME;
1099
+ rmSync(workspaceDir, { recursive: true, force: true });
1100
+ rmSync(xdgDir, { recursive: true, force: true });
1101
+ }
1102
+
1103
+ expect(logs.join("\n")).not.toContain("Using saved ingress URL");
1104
+ });
1105
+
1106
+ test("a non-https saved ingress URL is ignored", async () => {
1107
+ const workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pair-ingress-"));
1108
+ // Point the synthesized instance dir at an empty temp XDG root so the
1109
+ // resolution deterministically falls through to VELLUM_WORKSPACE_DIR.
1110
+ const xdgDir = mkdtempSync(join(tmpdir(), "vellum-pair-xdg-"));
1111
+ process.env.XDG_DATA_HOME = xdgDir;
1112
+ writeFileSync(
1113
+ join(workspaceDir, "config.json"),
1114
+ JSON.stringify({
1115
+ ingress: {
1116
+ publicBaseUrl: "http://insecure.example.com",
1117
+ enabled: true,
1118
+ },
1119
+ }),
1120
+ );
1121
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
1122
+
1123
+ const origFetch = globalThis.fetch;
1124
+ let minted = false;
1125
+ globalThis.fetch = (async (url: string) => {
1126
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
1127
+ return new Response(
1128
+ JSON.stringify({
1129
+ flags: [{ key: "web-remote-ingress", enabled: true }],
1130
+ }),
1131
+ { status: 200, headers: { "content-type": "application/json" } },
1132
+ );
1133
+ }
1134
+ minted = true;
1135
+ return new Response("{}", { status: 200 });
1136
+ }) as unknown as typeof fetch;
1137
+
1138
+ const errors: string[] = [];
1139
+ const errSpy = spyOn(console, "error").mockImplementation(
1140
+ (...a: unknown[]) => {
1141
+ errors.push(a.join(" "));
1142
+ },
1143
+ );
1144
+ const exitSpy = spyOn(process, "exit").mockImplementation(((
1145
+ code?: number,
1146
+ ) => {
1147
+ throw new Error(`exit:${code}`);
1148
+ }) as never);
1149
+
1150
+ process.argv = ["bun", "vellum", "pair", "--qr"];
1151
+ let exited = false;
1152
+ try {
1153
+ await pair();
1154
+ } catch (e) {
1155
+ exited = (e as Error).message === "exit:1";
1156
+ } finally {
1157
+ errSpy.mockRestore();
1158
+ exitSpy.mockRestore();
1159
+ globalThis.fetch = origFetch;
1160
+ delete process.env.VELLUM_WORKSPACE_DIR;
1161
+ delete process.env.XDG_DATA_HOME;
1162
+ rmSync(workspaceDir, { recursive: true, force: true });
1163
+ rmSync(xdgDir, { recursive: true, force: true });
1164
+ }
1165
+
1166
+ // The saved http URL is skipped, so --qr falls through to the (non-https)
1167
+ // runtime URL and refuses — proving the saved value was not advertised.
1168
+ expect(exited).toBe(true);
1169
+ expect(errors.join("\n")).toContain(RUNTIME_URL);
1170
+ expect(minted).toBe(false);
1171
+ });
1172
+
1173
+ test("a saved ingress URL with ingress disabled is ignored", async () => {
1174
+ const workspaceDir = mkdtempSync(join(tmpdir(), "vellum-pair-ingress-"));
1175
+ // Point the synthesized instance dir at an empty temp XDG root so the
1176
+ // resolution deterministically falls through to VELLUM_WORKSPACE_DIR.
1177
+ const xdgDir = mkdtempSync(join(tmpdir(), "vellum-pair-xdg-"));
1178
+ process.env.XDG_DATA_HOME = xdgDir;
1179
+ writeFileSync(
1180
+ join(workspaceDir, "config.json"),
1181
+ JSON.stringify({
1182
+ ingress: {
1183
+ publicBaseUrl: "https://saved.example.ts.net",
1184
+ enabled: false,
1185
+ },
1186
+ }),
1187
+ );
1188
+ process.env.VELLUM_WORKSPACE_DIR = workspaceDir;
1189
+
1190
+ const origFetch = globalThis.fetch;
1191
+ let minted = false;
1192
+ globalThis.fetch = (async (url: string) => {
1193
+ if (url === `${LOCAL_URL}/v1/assistants/pair-test/feature-flags`) {
1194
+ return new Response(
1195
+ JSON.stringify({
1196
+ flags: [{ key: "web-remote-ingress", enabled: true }],
1197
+ }),
1198
+ { status: 200, headers: { "content-type": "application/json" } },
1199
+ );
1200
+ }
1201
+ minted = true;
1202
+ return new Response("{}", { status: 200 });
1203
+ }) as unknown as typeof fetch;
1204
+
1205
+ const errors: string[] = [];
1206
+ const errSpy = spyOn(console, "error").mockImplementation(
1207
+ (...a: unknown[]) => {
1208
+ errors.push(a.join(" "));
1209
+ },
1210
+ );
1211
+ const exitSpy = spyOn(process, "exit").mockImplementation(((
1212
+ code?: number,
1213
+ ) => {
1214
+ throw new Error(`exit:${code}`);
1215
+ }) as never);
1216
+
1217
+ process.argv = ["bun", "vellum", "pair", "--qr"];
1218
+ let exited = false;
1219
+ try {
1220
+ await pair();
1221
+ } catch (e) {
1222
+ exited = (e as Error).message === "exit:1";
1223
+ } finally {
1224
+ errSpy.mockRestore();
1225
+ exitSpy.mockRestore();
1226
+ globalThis.fetch = origFetch;
1227
+ delete process.env.VELLUM_WORKSPACE_DIR;
1228
+ delete process.env.XDG_DATA_HOME;
1229
+ rmSync(workspaceDir, { recursive: true, force: true });
1230
+ rmSync(xdgDir, { recursive: true, force: true });
1231
+ }
1232
+
1233
+ expect(exited).toBe(true);
1234
+ expect(errors.join("\n")).toContain(RUNTIME_URL);
1235
+ expect(minted).toBe(false);
1236
+ });
934
1237
  });
@@ -10,6 +10,8 @@
10
10
  * independent, separately-revocable device (see `vellum unpair`, forthcoming).
11
11
  */
12
12
 
13
+ import { join } from "node:path";
14
+
13
15
  import { nanoid } from "nanoid";
14
16
  // Call `qrcodeTerminal.generate` as a method — the library reads its default
15
17
  // error-correction level off `this`, so a destructured import renders nothing.
@@ -33,6 +35,10 @@ import {
33
35
  isAssistantFeatureFlagEnabled,
34
36
  WEB_REMOTE_INGRESS_FLAG,
35
37
  } from "../lib/feature-flags.js";
38
+ import {
39
+ getDefaultWorkspaceDir,
40
+ loadIngressUrl,
41
+ } from "../lib/ingress-config.js";
36
42
  import { getLocalLanIPv4 } from "../lib/local.js";
37
43
  import { isLoopbackUrl, loopbackSafeFetch } from "../lib/loopback-fetch.js";
38
44
 
@@ -40,6 +46,45 @@ function assistantDisplayName(entry: AssistantEntry): string {
40
46
  return entry.name || entry.assistantName || entry.assistantId;
41
47
  }
42
48
 
49
+ /**
50
+ * The tunnel-saved ingress URL, when usable as a remote-web advertised
51
+ * default: https and non-loopback (the bar `--qr`/`--web` URLs must clear).
52
+ * Checks the workspaces a tunnel provider may have written: the instance's
53
+ * own workspace (managed/XDG layouts), then the default workspace (legacy
54
+ * `~/.vellum/workspace` layouts, where the gateway reads its config).
55
+ */
56
+ function loadUsableIngressUrl(entry: AssistantEntry): string | null {
57
+ const candidates = entry.resources
58
+ ? [
59
+ join(entry.resources.instanceDir, ".vellum", "workspace"),
60
+ getDefaultWorkspaceDir(),
61
+ ]
62
+ : [getDefaultWorkspaceDir()];
63
+ for (const workspaceDir of candidates) {
64
+ let saved: string | null = null;
65
+ try {
66
+ saved = loadIngressUrl(workspaceDir);
67
+ } catch {
68
+ continue;
69
+ }
70
+ if (!saved) {
71
+ continue;
72
+ }
73
+ try {
74
+ if (new URL(saved).protocol !== "https:") {
75
+ continue;
76
+ }
77
+ } catch {
78
+ continue;
79
+ }
80
+ if (isLoopbackUrl(saved)) {
81
+ continue;
82
+ }
83
+ return saved;
84
+ }
85
+ return null;
86
+ }
87
+
43
88
  function printUsage(): void {
44
89
  console.log(`vellum pair [beta] - Mint a device-scoped token for another machine
45
90
 
@@ -348,17 +393,30 @@ export async function pair(): Promise<void> {
348
393
 
349
394
  // Mint over loopback (localUrl avoids mDNS for same-machine calls), but
350
395
  // advertise a REACHABLE url in the bundle — the loopback url would point the
351
- // other machine at its own localhost. Prefer an explicit --url, then the
396
+ // other machine at its own localhost. Prefer an explicit --url, then (for
397
+ // the remote-web flows) the ingress URL a tunnel provider saved, then the
352
398
  // runtime (LAN/tunnel) url.
353
399
  const mintUrl = (
354
400
  entry.localUrl ||
355
401
  entry.runtimeUrl ||
356
402
  `http://127.0.0.1:${GATEWAY_PORT}`
357
403
  ).replace(/\/+$/, "");
358
- const advertisedUrl = (urlOverride || entry.runtimeUrl || mintUrl).replace(
359
- /\/+$/,
360
- "",
361
- );
404
+ const savedIngressUrl =
405
+ !urlOverride && (qrPairing || webPairing)
406
+ ? loadUsableIngressUrl(entry)
407
+ : null;
408
+ const advertisedUrl = (
409
+ urlOverride ||
410
+ savedIngressUrl ||
411
+ entry.runtimeUrl ||
412
+ mintUrl
413
+ ).replace(/\/+$/, "");
414
+ if (savedIngressUrl && !jsonOutput) {
415
+ console.log(
416
+ `Using saved ingress URL ${savedIngressUrl} ` +
417
+ "(from vellum tunnel; override with --url).",
418
+ );
419
+ }
362
420
 
363
421
  // A local hatch's runtimeUrl is itself loopback (http://localhost:<port>),
364
422
  // so without an explicit --url the bundle would point the other machine at
@@ -51,6 +51,20 @@ export function saveIngressUrl(workspaceDir: string, publicUrl: string): void {
51
51
  saveRawConfig(workspaceDir, config);
52
52
  }
53
53
 
54
+ /**
55
+ * Read the saved public ingress URL, or null when ingress is unset or
56
+ * explicitly disabled.
57
+ */
58
+ export function loadIngressUrl(workspaceDir: string): string | null {
59
+ const config = loadRawConfig(workspaceDir);
60
+ const ingress = config.ingress as Record<string, unknown> | undefined;
61
+ if (!ingress || ingress.enabled === false) {
62
+ return null;
63
+ }
64
+ const url = ingress.publicBaseUrl;
65
+ return typeof url === "string" && url.trim() ? url.trim() : null;
66
+ }
67
+
54
68
  /** Clear the ingress public base URL from the workspace config. */
55
69
  export function clearIngressUrl(workspaceDir: string): void {
56
70
  const config = loadRawConfig(workspaceDir);