@vellumai/cli 0.11.2 → 0.11.3-staging.2

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.
Files changed (97) hide show
  1. package/knip.json +1 -0
  2. package/node_modules/@vellumai/environments/package.json +2 -1
  3. package/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  4. package/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  5. package/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  6. package/node_modules/@vellumai/environments/src/index.ts +17 -6
  7. package/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  8. package/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  9. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  10. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.test.ts +36 -0
  11. package/node_modules/@vellumai/ipc-server-utils/src/endpoint.ts +142 -0
  12. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +18 -0
  13. package/node_modules/@vellumai/ipc-server-utils/src/ipc-framing.ts +295 -0
  14. package/node_modules/@vellumai/ipc-server-utils/src/listen-options.ts +3 -0
  15. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +444 -0
  16. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +236 -0
  17. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/package.json +2 -1
  19. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/cloud-assistant-hub-url.test.ts +38 -0
  20. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/install-layout.test.ts +98 -0
  21. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/__tests__/package-boundary.test.ts +5 -5
  22. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/index.ts +17 -6
  23. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/install-layout.ts +49 -0
  24. package/node_modules/@vellumai/local-mode/node_modules/@vellumai/environments/src/seeds.ts +29 -0
  25. package/node_modules/@vellumai/local-mode/node_modules/nanoid/LICENSE +20 -0
  26. package/node_modules/@vellumai/local-mode/node_modules/nanoid/README.md +38 -0
  27. package/node_modules/@vellumai/local-mode/node_modules/nanoid/bin/nanoid.js +55 -0
  28. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.browser.js +29 -0
  29. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.d.ts +106 -0
  30. package/node_modules/@vellumai/local-mode/node_modules/nanoid/index.js +47 -0
  31. package/node_modules/@vellumai/local-mode/node_modules/nanoid/nanoid.js +1 -0
  32. package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.d.ts +48 -0
  33. package/node_modules/@vellumai/local-mode/node_modules/nanoid/non-secure/index.js +21 -0
  34. package/node_modules/@vellumai/local-mode/node_modules/nanoid/package.json +46 -0
  35. package/node_modules/@vellumai/local-mode/node_modules/nanoid/url-alphabet/index.js +2 -0
  36. package/node_modules/@vellumai/local-mode/package.json +1 -0
  37. package/node_modules/@vellumai/local-mode/src/__tests__/environment.test.ts +63 -1
  38. package/node_modules/@vellumai/local-mode/src/__tests__/gateway-proxy.test.ts +503 -0
  39. package/node_modules/@vellumai/local-mode/src/__tests__/guardian-token.test.ts +203 -0
  40. package/node_modules/@vellumai/local-mode/src/__tests__/loopback-auth.test.ts +46 -1
  41. package/node_modules/@vellumai/local-mode/src/__tests__/package-boundary.test.ts +6 -4
  42. package/node_modules/@vellumai/local-mode/src/__tests__/pair.test.ts +621 -0
  43. package/node_modules/@vellumai/local-mode/src/__tests__/status.test.ts +5 -0
  44. package/node_modules/@vellumai/local-mode/src/__tests__/unpair.test.ts +232 -0
  45. package/node_modules/@vellumai/local-mode/src/config.ts +126 -20
  46. package/node_modules/@vellumai/local-mode/src/environment.ts +36 -15
  47. package/node_modules/@vellumai/local-mode/src/gateway-proxy.ts +313 -25
  48. package/node_modules/@vellumai/local-mode/src/guardian-token.ts +222 -13
  49. package/node_modules/@vellumai/local-mode/src/index.ts +35 -3
  50. package/node_modules/@vellumai/local-mode/src/lockfile-contract.test.ts +18 -0
  51. package/node_modules/@vellumai/local-mode/src/lockfile-contract.ts +72 -0
  52. package/node_modules/@vellumai/local-mode/src/lockfile.test.ts +130 -0
  53. package/node_modules/@vellumai/local-mode/src/lockfile.ts +134 -45
  54. package/node_modules/@vellumai/local-mode/src/pair.ts +354 -0
  55. package/node_modules/@vellumai/local-mode/src/paths.ts +49 -0
  56. package/node_modules/@vellumai/local-mode/src/status.ts +10 -8
  57. package/node_modules/@vellumai/local-mode/src/unpair.ts +101 -0
  58. package/node_modules/@vellumai/local-mode/src/util.ts +34 -0
  59. package/node_modules/@vellumai/service-contracts/src/channels.ts +39 -0
  60. package/node_modules/@vellumai/service-contracts/src/ingress.ts +10 -0
  61. package/node_modules/@vellumai/service-contracts/src/remote-web-pairing.ts +6 -0
  62. package/package.json +3 -1
  63. package/src/__tests__/client-token.test.ts +26 -1
  64. package/src/__tests__/guardian-token.test.ts +4 -0
  65. package/src/__tests__/nginx-ingress-command.test.ts +176 -16
  66. package/src/__tests__/nginx-ingress.test.ts +1821 -101
  67. package/src/__tests__/pair.test.ts +104 -2
  68. package/src/__tests__/sleep.test.ts +7 -3
  69. package/src/__tests__/tailscale-tunnel.test.ts +0 -1
  70. package/src/__tests__/tunnel.test.ts +1099 -55
  71. package/src/__tests__/wake.test.ts +166 -99
  72. package/src/commands/client.ts +182 -27
  73. package/src/commands/connect/import.ts +35 -156
  74. package/src/commands/nginx-ingress.ts +87 -110
  75. package/src/commands/pair.ts +84 -21
  76. package/src/commands/tunnel.ts +196 -85
  77. package/src/commands/upgrade.ts +3 -2
  78. package/src/commands/wake.ts +5 -163
  79. package/src/index.ts +1 -1
  80. package/src/lib/__tests__/local-ces.test.ts +10 -1
  81. package/src/lib/cloudflare-tunnel.ts +3 -16
  82. package/src/lib/environments/__tests__/paths.test.ts +20 -1
  83. package/src/lib/environments/paths.ts +29 -41
  84. package/src/lib/environments/resolve.ts +7 -5
  85. package/src/lib/guardian-token.ts +22 -62
  86. package/src/lib/ingress-config.ts +25 -0
  87. package/src/lib/local.ts +77 -20
  88. package/src/lib/nginx-ingress.ts +523 -76
  89. package/src/lib/ngrok.ts +154 -61
  90. package/src/lib/orphan-detection.test.ts +167 -0
  91. package/src/lib/orphan-detection.ts +40 -0
  92. package/src/lib/pair.test.ts +87 -0
  93. package/src/lib/pair.ts +55 -0
  94. package/src/lib/platform-client.ts +8 -5
  95. package/src/lib/tailscale-tunnel.ts +7 -18
  96. package/src/lib/tunnel-edge.ts +114 -0
  97. package/src/lib/xdg-log.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/cli",
3
- "version": "0.11.2",
3
+ "version": "0.11.3-staging.2",
4
4
  "description": "CLI tools for vellum-assistant",
5
5
  "type": "module",
6
6
  "exports": {
@@ -30,6 +30,7 @@
30
30
  "license": "MIT",
31
31
  "dependencies": {
32
32
  "@vellumai/environments": "file:../packages/environments",
33
+ "@vellumai/ipc-server-utils": "file:../packages/ipc-server-utils",
33
34
  "@vellumai/local-mode": "file:../packages/local-mode",
34
35
  "@vellumai/service-contracts": "file:../packages/service-contracts",
35
36
  "chalk": "5.6.2",
@@ -40,6 +41,7 @@
40
41
  },
41
42
  "bundledDependencies": [
42
43
  "@vellumai/environments",
44
+ "@vellumai/ipc-server-utils",
43
45
  "@vellumai/local-mode",
44
46
  "@vellumai/service-contracts"
45
47
  ],
@@ -27,11 +27,36 @@ const testDir = mkdtempSync(join(tmpdir(), "client-token-test-"));
27
27
  const ORIGINAL_LOCKFILE_DIR = process.env.VELLUM_LOCKFILE_DIR;
28
28
  const ORIGINAL_ARGV = [...process.argv];
29
29
 
30
- import { parseArgs } from "../commands/client.js";
30
+ import { isSameOriginRequest, parseArgs } from "../commands/client.js";
31
31
 
32
32
  // A clearly non-local URL so maybeSwapToLocalhost won't rewrite it to 127.0.0.1.
33
33
  const REMOTE_URL = "http://192.0.2.50:7830";
34
34
 
35
+ describe("client web proxy credential proof", () => {
36
+ test("rejects a request without browser same-origin proof", () => {
37
+ expect(
38
+ isSameOriginRequest(
39
+ new Request("http://127.0.0.1:3000/assistant", {
40
+ headers: { host: "127.0.0.1:3000" },
41
+ }),
42
+ ),
43
+ ).toBe(false);
44
+ });
45
+
46
+ test("accepts a request with a matching Origin", () => {
47
+ expect(
48
+ isSameOriginRequest(
49
+ new Request("http://127.0.0.1:3000/assistant", {
50
+ headers: {
51
+ host: "127.0.0.1:3000",
52
+ origin: "http://127.0.0.1:3000",
53
+ },
54
+ }),
55
+ ),
56
+ ).toBe(true);
57
+ });
58
+ });
59
+
35
60
  describe("client --token (ephemeral)", () => {
36
61
  beforeEach(() => {
37
62
  process.env.VELLUM_LOCKFILE_DIR = testDir;
@@ -261,6 +261,7 @@ describe("refreshGuardianToken", () => {
261
261
  isNew: false,
262
262
  deviceId: "dev",
263
263
  leasedAt: new Date().toISOString(),
264
+ pairedGatewayUrl: "https://gw.example.com",
264
265
  });
265
266
  }
266
267
 
@@ -302,6 +303,9 @@ describe("refreshGuardianToken", () => {
302
303
 
303
304
  expect(result?.accessToken).toBe("new-acc");
304
305
  expect(loadGuardianToken("px")?.accessToken).toBe("new-acc");
306
+ expect(loadGuardianToken("px")?.pairedGatewayUrl).toBe(
307
+ "https://gw.example.com",
308
+ );
305
309
  expect(sawSignal).toBe(true); // fetch carries a timeout AbortSignal
306
310
  expect(existsSync(lockPath("px"))).toBe(false); // lock released
307
311
  });
@@ -1,9 +1,45 @@
1
- import { afterAll, beforeEach, describe, expect, test } from "bun:test";
1
+ import {
2
+ afterAll,
3
+ afterEach,
4
+ beforeEach,
5
+ describe,
6
+ expect,
7
+ mock,
8
+ spyOn,
9
+ test,
10
+ } from "bun:test";
2
11
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
12
  import { tmpdir } from "node:os";
4
13
  import { join } from "node:path";
5
14
 
6
- import { resolveNginxIngressTarget } from "../commands/nginx-ingress.js";
15
+ import * as nginxIngressLib from "../lib/nginx-ingress.js";
16
+
17
+ const realNginxIngressLib = { ...nginxIngressLib };
18
+
19
+ const ensureTunnelEdgeMock = mock<typeof nginxIngressLib.ensureTunnelEdge>(
20
+ async () => ({ port: 7840, started: true, includesWebApp: true }),
21
+ );
22
+ const getIngressPidMock = mock<typeof nginxIngressLib.getIngressPid>(
23
+ () => null,
24
+ );
25
+
26
+ mock.module("../lib/nginx-ingress.js", () => ({
27
+ ...nginxIngressLib,
28
+ ensureTunnelEdge: ensureTunnelEdgeMock,
29
+ getIngressPid: getIngressPidMock,
30
+ }));
31
+
32
+ // Restore the real module once this file finishes so the mock does not leak
33
+ // into sibling test files in the same `bun test` run.
34
+ afterAll(() => {
35
+ mock.module("../lib/nginx-ingress.js", () => realNginxIngressLib);
36
+ });
37
+
38
+ import {
39
+ resolveNginxIngressTarget,
40
+ status,
41
+ up,
42
+ } from "../commands/nginx-ingress.js";
7
43
  import type { AssistantEntry } from "../lib/assistant-config.js";
8
44
 
9
45
  const testDir = mkdtempSync(join(tmpdir(), "cli-nginx-ingress-command-test-"));
@@ -29,6 +65,20 @@ function writeLockfile(
29
65
  );
30
66
  }
31
67
 
68
+ afterAll(() => {
69
+ if (originalLockfileDir === undefined) {
70
+ delete process.env.VELLUM_LOCKFILE_DIR;
71
+ } else {
72
+ process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
73
+ }
74
+ if (originalWorkspaceDir === undefined) {
75
+ delete process.env.VELLUM_WORKSPACE_DIR;
76
+ } else {
77
+ process.env.VELLUM_WORKSPACE_DIR = originalWorkspaceDir;
78
+ }
79
+ rmSync(testDir, { recursive: true, force: true });
80
+ });
81
+
32
82
  describe("resolveNginxIngressTarget", () => {
33
83
  beforeEach(() => {
34
84
  process.env.VELLUM_LOCKFILE_DIR = testDir;
@@ -36,20 +86,6 @@ describe("resolveNginxIngressTarget", () => {
36
86
  rmSync(join(testDir, ".vellum.lock.json"), { force: true });
37
87
  });
38
88
 
39
- afterAll(() => {
40
- if (originalLockfileDir === undefined) {
41
- delete process.env.VELLUM_LOCKFILE_DIR;
42
- } else {
43
- process.env.VELLUM_LOCKFILE_DIR = originalLockfileDir;
44
- }
45
- if (originalWorkspaceDir === undefined) {
46
- delete process.env.VELLUM_WORKSPACE_DIR;
47
- } else {
48
- process.env.VELLUM_WORKSPACE_DIR = originalWorkspaceDir;
49
- }
50
- rmSync(testDir, { recursive: true, force: true });
51
- });
52
-
53
89
  test("derives the gateway port from runtimeUrl when resources are absent", () => {
54
90
  writeLockfile([
55
91
  {
@@ -67,3 +103,127 @@ describe("resolveNginxIngressTarget", () => {
67
103
  });
68
104
  });
69
105
  });
106
+
107
+ describe("up", () => {
108
+ let logs: string[];
109
+ let logSpy: ReturnType<typeof spyOn<typeof console, "log">>;
110
+
111
+ beforeEach(() => {
112
+ ensureTunnelEdgeMock.mockClear();
113
+ logs = [];
114
+ logSpy = spyOn(console, "log").mockImplementation((...args: unknown[]) => {
115
+ logs.push(args.join(" "));
116
+ });
117
+ });
118
+
119
+ afterEach(() => {
120
+ logSpy.mockRestore();
121
+ });
122
+
123
+ test("starts the webhooks-only edge when the flag is off and states the mode", async () => {
124
+ ensureTunnelEdgeMock.mockResolvedValue({
125
+ port: 7845,
126
+ started: true,
127
+ includesWebApp: false,
128
+ });
129
+
130
+ await up({
131
+ assistantId: "assistant-1",
132
+ workspaceDir,
133
+ gatewayPort: 7830,
134
+ });
135
+
136
+ expect(ensureTunnelEdgeMock).toHaveBeenCalledTimes(1);
137
+ expect(ensureTunnelEdgeMock.mock.calls[0]?.[0]).toMatchObject({
138
+ assistantId: "assistant-1",
139
+ workspaceDir,
140
+ gatewayPort: 7830,
141
+ });
142
+ const output = logs.join("\n");
143
+ expect(output).toContain("http://127.0.0.1:7845 (webhooks only)");
144
+ expect(output).toContain("web-remote-ingress");
145
+ expect(output).toContain("vellum tunnel --provider ngrok");
146
+ });
147
+
148
+ test("states the remote web mode when the flag is on", async () => {
149
+ ensureTunnelEdgeMock.mockResolvedValue({
150
+ port: 7840,
151
+ started: true,
152
+ includesWebApp: true,
153
+ });
154
+
155
+ await up({
156
+ assistantId: "assistant-1",
157
+ workspaceDir,
158
+ gatewayPort: 7830,
159
+ });
160
+
161
+ const output = logs.join("\n");
162
+ expect(output).toContain("http://127.0.0.1:7840 (remote web + webhooks)");
163
+ expect(output).not.toContain("Enable the web-remote-ingress feature flag");
164
+ });
165
+ });
166
+
167
+ describe("status", () => {
168
+ const statusWorkspace = join(testDir, "status-workspace");
169
+ let logs: string[];
170
+ let logSpy: ReturnType<typeof spyOn<typeof console, "log">>;
171
+
172
+ beforeEach(() => {
173
+ getIngressPidMock.mockReset();
174
+ getIngressPidMock.mockReturnValue(4242);
175
+ logs = [];
176
+ logSpy = spyOn(console, "log").mockImplementation((...args: unknown[]) => {
177
+ logs.push(args.join(" "));
178
+ });
179
+ });
180
+
181
+ afterEach(() => {
182
+ logSpy.mockRestore();
183
+ rmSync(statusWorkspace, { recursive: true, force: true });
184
+ });
185
+
186
+ function writeIngressState(nginx: Record<string, unknown>): void {
187
+ mkdirSync(statusWorkspace, { recursive: true });
188
+ writeFileSync(
189
+ join(statusWorkspace, "config.json"),
190
+ JSON.stringify({ ingress: { nginx } }) + "\n",
191
+ );
192
+ }
193
+
194
+ test("prints the recorded gateway upstream, not the requested one", async () => {
195
+ writeIngressState({
196
+ listenPort: 7845,
197
+ includeWebApp: false,
198
+ gatewayPort: 7900,
199
+ });
200
+
201
+ await status({ workspaceDir: statusWorkspace, gatewayPort: 7830 });
202
+
203
+ const output = logs.join("\n");
204
+ expect(output).toContain("nginx ingress: running");
205
+ expect(output).toContain("Listen: http://127.0.0.1:7845");
206
+ expect(output).toContain("Gateway: http://127.0.0.1:7900");
207
+ expect(output).not.toContain("http://127.0.0.1:7830");
208
+ expect(output).not.toContain("(unverified)");
209
+ expect(output).toContain("Mode: webhooks only");
210
+ });
211
+
212
+ test("marks the requested gateway port unverified when the record predates the field", async () => {
213
+ writeIngressState({ listenPort: 7845 });
214
+
215
+ await status({ workspaceDir: statusWorkspace, gatewayPort: 7830 });
216
+
217
+ const output = logs.join("\n");
218
+ expect(output).toContain("Gateway: http://127.0.0.1:7830 (unverified)");
219
+ expect(output).toContain("Mode: remote web + webhooks");
220
+ });
221
+
222
+ test("reports a stopped edge", async () => {
223
+ getIngressPidMock.mockReturnValue(null);
224
+
225
+ await status({ workspaceDir: statusWorkspace, gatewayPort: 7830 });
226
+
227
+ expect(logs.join("\n")).toContain("nginx ingress: not running");
228
+ });
229
+ });