@openparachute/hub 0.5.14-rc.4 → 0.5.14-rc.6
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 +1 -1
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-cloudflare.test.ts +104 -1
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/hub-server.test.ts +299 -10
- package/src/__tests__/init.test.ts +344 -0
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/api-ready.ts +102 -0
- package/src/cli.ts +31 -4
- package/src/cloudflare/detect.ts +73 -6
- package/src/commands/expose-cloudflare.ts +102 -5
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/init.ts +213 -0
- package/src/commands/migrate.ts +293 -41
- package/src/help.ts +66 -15
- package/src/hub-server.ts +67 -13
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
package/package.json
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { handleApiReady } from "../api-ready.ts";
|
|
3
|
+
import type { ModuleState, Supervisor } from "../supervisor.ts";
|
|
4
|
+
|
|
5
|
+
function stubSupervisor(states: ModuleState[]): Supervisor {
|
|
6
|
+
return {
|
|
7
|
+
list: () => states,
|
|
8
|
+
get: (short: string) => states.find((s) => s.short === short),
|
|
9
|
+
start: async () => {
|
|
10
|
+
throw new Error("not implemented");
|
|
11
|
+
},
|
|
12
|
+
stop: async () => undefined,
|
|
13
|
+
restart: async () => undefined,
|
|
14
|
+
} as unknown as Supervisor;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function req(): Request {
|
|
18
|
+
return new Request("http://127.0.0.1/api/ready", {
|
|
19
|
+
headers: { accept: "application/json" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function moduleState(partial: Partial<ModuleState> & { short: string }): ModuleState {
|
|
24
|
+
return {
|
|
25
|
+
status: "running",
|
|
26
|
+
restartsInWindow: 0,
|
|
27
|
+
...partial,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("handleApiReady — no supervisor (CLI mode)", () => {
|
|
32
|
+
test("returns ready=true + empty arrays when supervisor absent", async () => {
|
|
33
|
+
const res = handleApiReady(req());
|
|
34
|
+
expect(res.status).toBe(200);
|
|
35
|
+
const body = (await res.json()) as {
|
|
36
|
+
ready: boolean;
|
|
37
|
+
ready_modules: string[];
|
|
38
|
+
transient_modules: string[];
|
|
39
|
+
persistent_modules: string[];
|
|
40
|
+
};
|
|
41
|
+
expect(body.ready).toBe(true);
|
|
42
|
+
expect(body.ready_modules).toEqual([]);
|
|
43
|
+
expect(body.transient_modules).toEqual([]);
|
|
44
|
+
expect(body.persistent_modules).toEqual([]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("handleApiReady — supervisor mode", () => {
|
|
49
|
+
test("all modules running past boot window → ready=true", async () => {
|
|
50
|
+
const now = 1_700_000_000_000;
|
|
51
|
+
const startedAt = new Date(now - 60_000).toISOString();
|
|
52
|
+
const sup = stubSupervisor([
|
|
53
|
+
moduleState({ short: "vault", status: "running", startedAt }),
|
|
54
|
+
moduleState({ short: "scribe", status: "running", startedAt }),
|
|
55
|
+
]);
|
|
56
|
+
const res = handleApiReady(req(), { supervisor: sup, now: () => now });
|
|
57
|
+
const body = (await res.json()) as {
|
|
58
|
+
ready: boolean;
|
|
59
|
+
ready_modules: string[];
|
|
60
|
+
transient_modules: string[];
|
|
61
|
+
persistent_modules: string[];
|
|
62
|
+
};
|
|
63
|
+
expect(body.ready).toBe(true);
|
|
64
|
+
expect(body.ready_modules.sort()).toEqual(["scribe", "vault"]);
|
|
65
|
+
expect(body.transient_modules).toEqual([]);
|
|
66
|
+
expect(body.persistent_modules).toEqual([]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("module inside boot window → transient, ready=false", async () => {
|
|
70
|
+
const now = 1_700_000_000_000;
|
|
71
|
+
const sup = stubSupervisor([
|
|
72
|
+
moduleState({
|
|
73
|
+
short: "vault",
|
|
74
|
+
status: "running",
|
|
75
|
+
startedAt: new Date(now - 10_000).toISOString(),
|
|
76
|
+
}),
|
|
77
|
+
]);
|
|
78
|
+
const res = handleApiReady(req(), { supervisor: sup, now: () => now });
|
|
79
|
+
const body = (await res.json()) as {
|
|
80
|
+
ready: boolean;
|
|
81
|
+
ready_modules: string[];
|
|
82
|
+
transient_modules: string[];
|
|
83
|
+
};
|
|
84
|
+
expect(body.ready).toBe(false);
|
|
85
|
+
expect(body.transient_modules).toEqual(["vault"]);
|
|
86
|
+
expect(body.ready_modules).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("starting + restarting + crashed all classified correctly", async () => {
|
|
90
|
+
const now = 1_700_000_000_000;
|
|
91
|
+
const sup = stubSupervisor([
|
|
92
|
+
moduleState({ short: "vault", status: "starting" }),
|
|
93
|
+
moduleState({ short: "scribe", status: "restarting" }),
|
|
94
|
+
moduleState({ short: "notes", status: "crashed" }),
|
|
95
|
+
moduleState({ short: "channel", status: "stopped" }),
|
|
96
|
+
moduleState({
|
|
97
|
+
short: "runner",
|
|
98
|
+
status: "running",
|
|
99
|
+
startedAt: new Date(now - 60_000).toISOString(),
|
|
100
|
+
}),
|
|
101
|
+
]);
|
|
102
|
+
const res = handleApiReady(req(), { supervisor: sup, now: () => now });
|
|
103
|
+
const body = (await res.json()) as {
|
|
104
|
+
ready: boolean;
|
|
105
|
+
ready_modules: string[];
|
|
106
|
+
transient_modules: string[];
|
|
107
|
+
persistent_modules: string[];
|
|
108
|
+
};
|
|
109
|
+
expect(body.ready).toBe(false);
|
|
110
|
+
expect(body.ready_modules).toEqual(["runner"]);
|
|
111
|
+
expect(body.transient_modules.sort()).toEqual(["scribe", "vault"]);
|
|
112
|
+
expect(body.persistent_modules.sort()).toEqual(["channel", "notes"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("only crashed/stopped + nothing transient → ready=false (still failing)", async () => {
|
|
116
|
+
const sup = stubSupervisor([moduleState({ short: "vault", status: "crashed" })]);
|
|
117
|
+
const res = handleApiReady(req(), { supervisor: sup });
|
|
118
|
+
const body = (await res.json()) as { ready: boolean };
|
|
119
|
+
expect(body.ready).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe("handleApiReady — method check", () => {
|
|
124
|
+
test("rejects non-GET", () => {
|
|
125
|
+
const r = new Request("http://127.0.0.1/api/ready", { method: "POST" });
|
|
126
|
+
const res = handleApiReady(r);
|
|
127
|
+
expect(res.status).toBe(405);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("accepts HEAD", () => {
|
|
131
|
+
const r = new Request("http://127.0.0.1/api/ready", { method: "HEAD" });
|
|
132
|
+
const res = handleApiReady(r);
|
|
133
|
+
expect(res.status).toBe(200);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -59,10 +59,65 @@ describe("cloudflare detect", () => {
|
|
|
59
59
|
}
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
describe("cloudflaredInstallHint", () => {
|
|
63
|
+
test("darwin: names brew + points at GitHub releases as fallback", () => {
|
|
64
|
+
const hint = cloudflaredInstallHint("darwin", "arm64");
|
|
65
|
+
expect(hint).toContain("brew install cloudflared");
|
|
66
|
+
expect(hint).toContain("https://github.com/cloudflare/cloudflared/releases/latest");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("linux x64: writes the curl line with the amd64 artifact suffix", () => {
|
|
70
|
+
// Refresh of stale URLs (2026-05-27). Aaron hit this on a fresh
|
|
71
|
+
// Amazon Linux 2023 install — `sudo dnf install cloudflared`
|
|
72
|
+
// returned 'No match for argument: cloudflared', and the hub's
|
|
73
|
+
// hint pointed at developers.cloudflare.com paths that 404. The
|
|
74
|
+
// GitHub release is the reliable cross-distro path.
|
|
75
|
+
const hint = cloudflaredInstallHint("linux", "x64");
|
|
76
|
+
expect(hint).toContain("curl -L -o /usr/local/bin/cloudflared");
|
|
77
|
+
expect(hint).toContain(
|
|
78
|
+
"https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64",
|
|
79
|
+
);
|
|
80
|
+
expect(hint).toContain("sudo chmod +x /usr/local/bin/cloudflared");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("linux arm64: writes the arm64 artifact suffix", () => {
|
|
84
|
+
const hint = cloudflaredInstallHint("linux", "arm64");
|
|
85
|
+
expect(hint).toContain("cloudflared-linux-arm64");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("linux arm (32-bit): writes the arm artifact suffix", () => {
|
|
89
|
+
const hint = cloudflaredInstallHint("linux", "arm");
|
|
90
|
+
expect(hint).toContain("cloudflared-linux-arm");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("linux exotic arch: falls back to a generic GitHub releases pointer", () => {
|
|
94
|
+
// riscv64 / ppc64 / mips64 — no cloudflared artifact published, so
|
|
95
|
+
// we don't fabricate a 404-bound download URL; we point the user at
|
|
96
|
+
// the releases page and surface what their arch is so they can pick.
|
|
97
|
+
const hint = cloudflaredInstallHint("linux", "riscv64");
|
|
98
|
+
expect(hint).toContain("https://github.com/cloudflare/cloudflared/releases/latest");
|
|
99
|
+
expect(hint).toContain("riscv64");
|
|
100
|
+
expect(hint).not.toContain("curl -L -o /usr/local/bin/cloudflared");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("no stale developers.cloudflare.com or pkg.cloudflare.com paths anywhere", () => {
|
|
104
|
+
// Aaron caught both URL shapes returning HTML/404 on 2026-05-27 —
|
|
105
|
+
// they had been the hub's installer instructions for months.
|
|
106
|
+
// Hard-assert they're gone so they don't regress.
|
|
107
|
+
for (const platform of ["darwin", "linux"] as const) {
|
|
108
|
+
for (const arch of ["x64", "arm64"] as const) {
|
|
109
|
+
const hint = cloudflaredInstallHint(platform, arch);
|
|
110
|
+
expect(hint).not.toContain("developers.cloudflare.com");
|
|
111
|
+
expect(hint).not.toContain("pkg.cloudflare.com");
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("non-Linux, non-darwin platform: GitHub releases pointer with no curl line", () => {
|
|
117
|
+
const hint = cloudflaredInstallHint("win32", "x64");
|
|
118
|
+
expect(hint).toContain("https://github.com/cloudflare/cloudflared/releases/latest");
|
|
119
|
+
expect(hint).not.toContain("brew install");
|
|
120
|
+
expect(hint).not.toContain("curl -L -o");
|
|
121
|
+
});
|
|
67
122
|
});
|
|
68
123
|
});
|
|
@@ -14,8 +14,13 @@ import {
|
|
|
14
14
|
exposeCloudflareOff,
|
|
15
15
|
exposeCloudflareUp,
|
|
16
16
|
} from "../commands/expose-cloudflare.ts";
|
|
17
|
+
import { writeHubPort } from "../hub-control.ts";
|
|
17
18
|
import type { CommandResult, Runner } from "../tailscale/run.ts";
|
|
18
19
|
|
|
20
|
+
// Default seeded hub port used by tests with `skipHub: true`. The cloudflared
|
|
21
|
+
// path reads `<configDir>/hub/run/hub.port` instead of spawning a real hub.
|
|
22
|
+
const TEST_HUB_PORT = 1939;
|
|
23
|
+
|
|
19
24
|
interface TestEnv {
|
|
20
25
|
configDir: string;
|
|
21
26
|
manifestPath: string;
|
|
@@ -41,6 +46,11 @@ function makeEnv(opts: { includeVault?: boolean; loggedIn?: boolean } = {}): Tes
|
|
|
41
46
|
require("node:fs").mkdirSync(configDir, { recursive: true });
|
|
42
47
|
require("node:fs").mkdirSync(cloudflaredHome, { recursive: true });
|
|
43
48
|
|
|
49
|
+
// Seed the hub port so `skipHub: true` invocations can resolve a port
|
|
50
|
+
// without spawning the actual hub process. Matches the seam pattern used
|
|
51
|
+
// by expose.test.ts (which threads `hubEnsureOpts` for the same purpose).
|
|
52
|
+
writeHubPort(TEST_HUB_PORT, configDir);
|
|
53
|
+
|
|
44
54
|
if (loggedIn) {
|
|
45
55
|
writeFileSync(join(cloudflaredHome, "cert.pem"), "---");
|
|
46
56
|
}
|
|
@@ -128,6 +138,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
128
138
|
configPath: env.configPath,
|
|
129
139
|
logPath: env.logPath,
|
|
130
140
|
cloudflaredHome: env.cloudflaredHome,
|
|
141
|
+
configDir: env.configDir,
|
|
142
|
+
skipHub: true,
|
|
131
143
|
now: () => new Date("2026-04-22T12:00:00Z"),
|
|
132
144
|
});
|
|
133
145
|
|
|
@@ -170,7 +182,12 @@ describe("exposeCloudflareUp", () => {
|
|
|
170
182
|
const yaml = readFileSync(env.configPath, "utf8");
|
|
171
183
|
expect(yaml).toContain(`tunnel: ${uuid}`);
|
|
172
184
|
expect(yaml).toContain("- hostname: vault.example.com");
|
|
173
|
-
|
|
185
|
+
// Routes through the hub (not directly at vault). The hub dispatches
|
|
186
|
+
// discovery / admin / OAuth / per-vault proxy / generic /<svc>/* —
|
|
187
|
+
// same shape Tailscale Funnel uses. Pre-2026-05-27 this was
|
|
188
|
+
// http://localhost:1940 (vault's port), which served vault's own 404
|
|
189
|
+
// page on every request that wasn't /vault/<name>/...
|
|
190
|
+
expect(yaml).toContain(`service: http://localhost:${TEST_HUB_PORT}`);
|
|
174
191
|
|
|
175
192
|
// Security copy surfaces both paths plus a pointer to the auth doc.
|
|
176
193
|
const joined = logs.join("\n");
|
|
@@ -209,6 +226,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
209
226
|
configPath: env.configPath,
|
|
210
227
|
logPath: env.logPath,
|
|
211
228
|
cloudflaredHome: env.cloudflaredHome,
|
|
229
|
+
configDir: env.configDir,
|
|
230
|
+
skipHub: true,
|
|
212
231
|
});
|
|
213
232
|
expect(code).toBe(0);
|
|
214
233
|
// No `tunnel create` — only list + route.
|
|
@@ -236,6 +255,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
236
255
|
configPath: env.configPath,
|
|
237
256
|
logPath: env.logPath,
|
|
238
257
|
cloudflaredHome: env.cloudflaredHome,
|
|
258
|
+
configDir: env.configDir,
|
|
259
|
+
skipHub: true,
|
|
239
260
|
});
|
|
240
261
|
|
|
241
262
|
expect(code).toBe(1);
|
|
@@ -262,6 +283,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
262
283
|
configPath: env.configPath,
|
|
263
284
|
logPath: env.logPath,
|
|
264
285
|
cloudflaredHome: env.cloudflaredHome,
|
|
286
|
+
configDir: env.configDir,
|
|
287
|
+
skipHub: true,
|
|
265
288
|
tunnelName: "bad name with spaces",
|
|
266
289
|
});
|
|
267
290
|
|
|
@@ -291,6 +314,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
291
314
|
configPath: env.configPath,
|
|
292
315
|
logPath: env.logPath,
|
|
293
316
|
cloudflaredHome: env.cloudflaredHome,
|
|
317
|
+
configDir: env.configDir,
|
|
318
|
+
skipHub: true,
|
|
294
319
|
});
|
|
295
320
|
|
|
296
321
|
expect(code).toBe(1);
|
|
@@ -317,6 +342,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
317
342
|
configPath: env.configPath,
|
|
318
343
|
logPath: env.logPath,
|
|
319
344
|
cloudflaredHome: env.cloudflaredHome,
|
|
345
|
+
configDir: env.configDir,
|
|
346
|
+
skipHub: true,
|
|
320
347
|
});
|
|
321
348
|
|
|
322
349
|
expect(code).toBe(1);
|
|
@@ -342,6 +369,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
342
369
|
configPath: env.configPath,
|
|
343
370
|
logPath: env.logPath,
|
|
344
371
|
cloudflaredHome: env.cloudflaredHome,
|
|
372
|
+
configDir: env.configDir,
|
|
373
|
+
skipHub: true,
|
|
345
374
|
});
|
|
346
375
|
|
|
347
376
|
expect(code).toBe(1);
|
|
@@ -376,6 +405,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
376
405
|
configPath: env.configPath,
|
|
377
406
|
logPath: env.logPath,
|
|
378
407
|
cloudflaredHome: env.cloudflaredHome,
|
|
408
|
+
configDir: env.configDir,
|
|
409
|
+
skipHub: true,
|
|
379
410
|
});
|
|
380
411
|
|
|
381
412
|
expect(code).toBe(1);
|
|
@@ -425,6 +456,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
425
456
|
configPath: env.configPath,
|
|
426
457
|
logPath: env.logPath,
|
|
427
458
|
cloudflaredHome: env.cloudflaredHome,
|
|
459
|
+
configDir: env.configDir,
|
|
460
|
+
skipHub: true,
|
|
428
461
|
});
|
|
429
462
|
|
|
430
463
|
expect(code).toBe(0);
|
|
@@ -462,6 +495,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
462
495
|
manifestPath: env.manifestPath,
|
|
463
496
|
statePath: env.statePath,
|
|
464
497
|
cloudflaredHome: env.cloudflaredHome,
|
|
498
|
+
configDir: env.configDir,
|
|
499
|
+
skipHub: true,
|
|
465
500
|
// Use defaults for configPath/logPath so they're per-tunnel-derived.
|
|
466
501
|
});
|
|
467
502
|
expect(code1).toBe(0);
|
|
@@ -487,6 +522,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
487
522
|
manifestPath: env.manifestPath,
|
|
488
523
|
statePath: env.statePath,
|
|
489
524
|
cloudflaredHome: env.cloudflaredHome,
|
|
525
|
+
configDir: env.configDir,
|
|
526
|
+
skipHub: true,
|
|
490
527
|
tunnelName: "second",
|
|
491
528
|
});
|
|
492
529
|
expect(code2).toBe(0);
|
|
@@ -544,6 +581,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
544
581
|
configPath: env.configPath,
|
|
545
582
|
logPath: env.logPath,
|
|
546
583
|
cloudflaredHome: env.cloudflaredHome,
|
|
584
|
+
configDir: env.configDir,
|
|
585
|
+
skipHub: true,
|
|
547
586
|
// No password, no 2FA — fully wide open. The warning should still
|
|
548
587
|
// fire; password-recovery copy already lives in `printAuthGuidance`.
|
|
549
588
|
vaultAuthStatus: {
|
|
@@ -592,6 +631,8 @@ describe("exposeCloudflareUp", () => {
|
|
|
592
631
|
configPath: env.configPath,
|
|
593
632
|
logPath: env.logPath,
|
|
594
633
|
cloudflaredHome: env.cloudflaredHome,
|
|
634
|
+
configDir: env.configDir,
|
|
635
|
+
skipHub: true,
|
|
595
636
|
vaultAuthStatus: {
|
|
596
637
|
hasOwnerPassword: true,
|
|
597
638
|
hasTotp: true,
|
|
@@ -612,6 +653,68 @@ describe("exposeCloudflareUp", () => {
|
|
|
612
653
|
}
|
|
613
654
|
});
|
|
614
655
|
});
|
|
656
|
+
|
|
657
|
+
describe("routes through hub, not vault", () => {
|
|
658
|
+
test("config.yml targets the hub port; success log mentions Admin + OAuth URLs", async () => {
|
|
659
|
+
// Regression guard for the 2026-05-27 cut. Aaron ran `parachute expose
|
|
660
|
+
// public` on a fresh EC2 box, configured Cloudflare with a custom
|
|
661
|
+
// domain, and hit it — and got vault's 404 page rather than the hub's
|
|
662
|
+
// discovery / admin. The pre-fix cloudflared config routed straight at
|
|
663
|
+
// vault's port; the fix routes at the hub, mirroring the Tailscale
|
|
664
|
+
// Funnel shape (single mount → hub catchall; hub dispatches per-request).
|
|
665
|
+
const env = makeEnv();
|
|
666
|
+
try {
|
|
667
|
+
// Re-seed hub port to a non-default value so the assertion is
|
|
668
|
+
// unambiguous about *which* port got into the yaml.
|
|
669
|
+
writeHubPort(1949, env.configDir);
|
|
670
|
+
|
|
671
|
+
const uuid = "ffff0000-0000-0000-0000-00000000beef";
|
|
672
|
+
const { runner } = queueRunner([
|
|
673
|
+
{ code: 0, stdout: "cloudflared 2024.1.0\n", stderr: "" },
|
|
674
|
+
{ code: 0, stdout: "[]", stderr: "" },
|
|
675
|
+
{
|
|
676
|
+
code: 0,
|
|
677
|
+
stdout: `Created tunnel parachute with id ${uuid}\n`,
|
|
678
|
+
stderr: "",
|
|
679
|
+
},
|
|
680
|
+
{ code: 0, stdout: "", stderr: "" },
|
|
681
|
+
]);
|
|
682
|
+
const { spawner } = fakeSpawner(60001);
|
|
683
|
+
const logs: string[] = [];
|
|
684
|
+
|
|
685
|
+
const code = await exposeCloudflareUp("gitcoin.parachute.computer", {
|
|
686
|
+
runner,
|
|
687
|
+
spawner,
|
|
688
|
+
alive: () => false,
|
|
689
|
+
kill: () => {},
|
|
690
|
+
log: (l) => logs.push(l),
|
|
691
|
+
manifestPath: env.manifestPath,
|
|
692
|
+
statePath: env.statePath,
|
|
693
|
+
configPath: env.configPath,
|
|
694
|
+
logPath: env.logPath,
|
|
695
|
+
cloudflaredHome: env.cloudflaredHome,
|
|
696
|
+
configDir: env.configDir,
|
|
697
|
+
skipHub: true,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
expect(code).toBe(0);
|
|
701
|
+
const yaml = readFileSync(env.configPath, "utf8");
|
|
702
|
+
// Routes through the hub on its loopback port.
|
|
703
|
+
expect(yaml).toContain("service: http://localhost:1949");
|
|
704
|
+
// Does NOT route directly at vault's port (1940 per makeEnv default).
|
|
705
|
+
expect(yaml).not.toContain("service: http://localhost:1940");
|
|
706
|
+
|
|
707
|
+
const joined = logs.join("\n");
|
|
708
|
+
// Discoverable surfaces: open / admin / vault / OAuth all surfaced.
|
|
709
|
+
expect(joined).toContain("https://gitcoin.parachute.computer/");
|
|
710
|
+
expect(joined).toContain("Admin: https://gitcoin.parachute.computer/admin/");
|
|
711
|
+
expect(joined).toContain("Vault: https://gitcoin.parachute.computer/vault/default");
|
|
712
|
+
expect(joined).toContain("OAuth: https://gitcoin.parachute.computer");
|
|
713
|
+
} finally {
|
|
714
|
+
env.cleanup();
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
});
|
|
615
718
|
});
|
|
616
719
|
|
|
617
720
|
describe("exposeCloudflareOff", () => {
|
|
@@ -469,10 +469,16 @@ describe("exposePublicInteractive — neither ready", () => {
|
|
|
469
469
|
expect(interactiveCalled).toBe(false);
|
|
470
470
|
expect(cloudflareCalled).toBe(false);
|
|
471
471
|
const joined = logs.join("\n");
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
472
|
+
// Post 2026-05-27 cloudflared-URL refresh: the install hint moved
|
|
473
|
+
// off apt-get / dnf / developers.cloudflare.com (all unreliable —
|
|
474
|
+
// Aaron hit `No match for argument: cloudflared` on AL2023 and
|
|
475
|
+
// 404s from the docs URL on the same box) onto the static binary
|
|
476
|
+
// from GitHub releases.
|
|
477
|
+
expect(joined).toContain("github.com/cloudflare/cloudflared/releases/latest");
|
|
478
|
+
expect(joined).toContain("curl -L -o /usr/local/bin/cloudflared");
|
|
479
|
+
expect(joined).not.toContain("developers.cloudflare.com");
|
|
480
|
+
expect(joined).not.toContain("pkg.cloudflare.com");
|
|
481
|
+
expect(joined).not.toContain("sudo dnf install cloudflared");
|
|
476
482
|
} finally {
|
|
477
483
|
env.cleanup();
|
|
478
484
|
}
|
|
@@ -126,7 +126,11 @@ describe("exposePublicAutoPick — neither ready", () => {
|
|
|
126
126
|
expect(code).toBe(1);
|
|
127
127
|
const joined = logs.join("\n");
|
|
128
128
|
expect(joined).toContain("tailscale.com/download");
|
|
129
|
-
|
|
129
|
+
// Post 2026-05-27 cloudflared-URL refresh: the install hint now points
|
|
130
|
+
// at GitHub releases (developers.cloudflare.com / pkg.cloudflare.com
|
|
131
|
+
// both returned HTML/404 on Aaron's fresh AL2023 EC2 box).
|
|
132
|
+
expect(joined).toContain("github.com/cloudflare/cloudflared/releases/latest");
|
|
133
|
+
expect(joined).not.toContain("developers.cloudflare.com");
|
|
130
134
|
expect(joined).toContain("--skip-provider-check");
|
|
131
135
|
});
|
|
132
136
|
|