@openparachute/hub 0.5.13 → 0.5.14-rc.10
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/README.md +109 -15
- package/package.json +2 -2
- package/src/__tests__/account-home-ui.test.ts +205 -0
- package/src/__tests__/admin-handlers.test.ts +74 -0
- package/src/__tests__/admin-host-admin-token.test.ts +62 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +44 -0
- package/src/__tests__/admin-vaults.test.ts +70 -4
- package/src/__tests__/api-account.test.ts +191 -1
- package/src/__tests__/api-mint-token.test.ts +682 -3
- package/src/__tests__/api-modules-config.test.ts +16 -10
- package/src/__tests__/api-modules-ops.test.ts +97 -0
- package/src/__tests__/api-modules.test.ts +100 -83
- package/src/__tests__/api-ready.test.ts +135 -0
- package/src/__tests__/api-revoke-token.test.ts +384 -0
- package/src/__tests__/api-users.test.ts +390 -13
- package/src/__tests__/chrome-strip.test.ts +15 -15
- package/src/__tests__/cli.test.ts +7 -5
- package/src/__tests__/cloudflare-detect.test.ts +60 -5
- package/src/__tests__/expose-auth-preflight.test.ts +58 -50
- package/src/__tests__/expose-cloudflare.test.ts +114 -3
- package/src/__tests__/expose-interactive.test.ts +10 -4
- package/src/__tests__/expose-public-auto.test.ts +5 -1
- package/src/__tests__/expose.test.ts +49 -1
- package/src/__tests__/hub-db.test.ts +194 -29
- package/src/__tests__/hub-server.test.ts +322 -33
- package/src/__tests__/hub.test.ts +11 -0
- package/src/__tests__/init.test.ts +827 -0
- package/src/__tests__/lifecycle.test.ts +33 -1
- package/src/__tests__/migrate.test.ts +433 -51
- package/src/__tests__/notes-redirect.test.ts +20 -20
- package/src/__tests__/oauth-handlers.test.ts +1060 -29
- package/src/__tests__/oauth-ui.test.ts +12 -1
- package/src/__tests__/proxy-error-ui.test.ts +212 -0
- package/src/__tests__/proxy-state.test.ts +192 -0
- package/src/__tests__/resource-binding.test.ts +97 -0
- package/src/__tests__/scope-explanations.test.ts +36 -0
- package/src/__tests__/serve.test.ts +9 -9
- package/src/__tests__/services-manifest.test.ts +40 -40
- package/src/__tests__/setup-wizard.test.ts +1114 -66
- package/src/__tests__/setup.test.ts +1 -1
- package/src/__tests__/status.test.ts +39 -0
- package/src/__tests__/users.test.ts +396 -9
- package/src/__tests__/vault-auth-status.test.ts +271 -11
- package/src/__tests__/vault-hub-origin-env.test.ts +126 -0
- package/src/__tests__/well-known.test.ts +9 -9
- package/src/__tests__/wizard.test.ts +372 -0
- package/src/account-home-ui.ts +547 -0
- package/src/admin-handlers.ts +49 -17
- package/src/admin-host-admin-token.ts +25 -0
- package/src/admin-login-ui.ts +4 -4
- package/src/admin-vault-admin-token.ts +17 -0
- package/src/admin-vaults.ts +48 -15
- package/src/api-account.ts +72 -6
- package/src/api-mint-token.ts +132 -24
- package/src/api-modules-ops.ts +52 -16
- package/src/api-modules.ts +31 -14
- package/src/api-ready.ts +102 -0
- package/src/api-revoke-token.ts +107 -21
- package/src/api-users.ts +497 -58
- package/src/bun-link.ts +55 -0
- package/src/chrome-strip.ts +6 -6
- package/src/cli.ts +93 -24
- package/src/cloudflare/config.ts +10 -4
- package/src/cloudflare/detect.ts +73 -6
- package/src/commands/expose-auth-preflight.ts +55 -63
- package/src/commands/expose-cloudflare.ts +114 -10
- package/src/commands/expose-interactive.ts +10 -11
- package/src/commands/expose-public-auto.ts +6 -4
- package/src/commands/expose.ts +8 -0
- package/src/commands/init.ts +563 -0
- package/src/commands/install.ts +41 -23
- package/src/commands/lifecycle.ts +12 -0
- package/src/commands/migrate.ts +293 -41
- package/src/commands/status.ts +10 -1
- package/src/commands/wizard.ts +843 -0
- package/src/env-file.ts +10 -0
- package/src/help.ts +157 -17
- package/src/hub-db.ts +42 -0
- package/src/hub-server.ts +136 -23
- package/src/hub-settings.ts +13 -2
- package/src/hub.ts +16 -9
- package/src/notes-redirect.ts +5 -5
- package/src/oauth-handlers.ts +342 -173
- package/src/oauth-ui.ts +28 -2
- package/src/proxy-error-ui.ts +506 -0
- package/src/proxy-state.ts +131 -0
- package/src/resource-binding.ts +134 -0
- package/src/scope-attenuation.ts +85 -0
- package/src/scope-explanations.ts +94 -5
- package/src/service-spec.ts +39 -18
- package/src/setup-wizard.ts +1173 -117
- package/src/users.ts +307 -29
- package/src/vault/auth-status.ts +152 -25
- package/src/vault-hub-origin-env.ts +100 -0
- package/web/ui/dist/assets/index-2SSK7JbM.js +61 -0
- package/web/ui/dist/assets/index-B28SdMSz.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/vault-tokens-create-interactive.test.ts +0 -183
- package/src/commands/vault-tokens-create-interactive.ts +0 -143
- package/web/ui/dist/assets/index-7DtAXz7y.css +0 -1
- package/web/ui/dist/assets/index-Dzrbe6EP.js +0 -61
|
@@ -26,11 +26,19 @@ import {
|
|
|
26
26
|
findTunnelByName,
|
|
27
27
|
routeDns,
|
|
28
28
|
} from "../cloudflare/tunnel.ts";
|
|
29
|
-
import { SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
29
|
+
import { CONFIG_DIR, SERVICES_MANIFEST_PATH } from "../config.ts";
|
|
30
|
+
import {
|
|
31
|
+
type EnsureHubOpts,
|
|
32
|
+
HUB_DEFAULT_PORT,
|
|
33
|
+
ensureHubRunning,
|
|
34
|
+
readHubPort,
|
|
35
|
+
} from "../hub-control.ts";
|
|
36
|
+
import { deriveHubOrigin } from "../hub-origin.ts";
|
|
30
37
|
import { type AliveFn, defaultAlive } from "../process-state.ts";
|
|
31
38
|
import { readManifest } from "../services-manifest.ts";
|
|
32
39
|
import { type Runner, defaultRunner } from "../tailscale/run.ts";
|
|
33
40
|
import type { VaultAuthStatus } from "../vault/auth-status.ts";
|
|
41
|
+
import { WELL_KNOWN_DIR } from "../well-known.ts";
|
|
34
42
|
import { printPublic2FAWarning } from "./expose-2fa-warning.ts";
|
|
35
43
|
|
|
36
44
|
const AUTH_DOC_URL =
|
|
@@ -112,6 +120,37 @@ export interface ExposeCloudflareOpts {
|
|
|
112
120
|
logPath?: string;
|
|
113
121
|
/** Override `~/.cloudflared` for tests and `$HOME`-free environments. */
|
|
114
122
|
cloudflaredHome?: string;
|
|
123
|
+
/**
|
|
124
|
+
* Config root for hub PID / port / log files. Defaults to `~/.parachute`.
|
|
125
|
+
* Threaded into `ensureHubRunning` so cloudflared's ingress target stays
|
|
126
|
+
* in sync with where the hub actually bound.
|
|
127
|
+
*/
|
|
128
|
+
configDir?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Override the public hub origin (the `iss` claim baked into the OAuth
|
|
131
|
+
* issuer). Mirrors the Tailscale path — when set, this URL is what the
|
|
132
|
+
* hub advertises rather than the cloudflared hostname.
|
|
133
|
+
*/
|
|
134
|
+
hubOrigin?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Overrides for hub lifecycle — primarily for tests. Tests pass
|
|
137
|
+
* `skipHubLifecycle: true` (above) plus a seeded `hub.port` file so the
|
|
138
|
+
* cloudflare path can resolve a port without actually spawning a hub.
|
|
139
|
+
*/
|
|
140
|
+
hubEnsureOpts?: Omit<EnsureHubOpts, "configDir" | "wellKnownDir" | "log">;
|
|
141
|
+
/**
|
|
142
|
+
* Directory holding hub.html (passed through to the hub server on first
|
|
143
|
+
* spawn). Defaults to the same `well-known/` resolution the Tailscale
|
|
144
|
+
* path uses.
|
|
145
|
+
*/
|
|
146
|
+
wellKnownDir?: string;
|
|
147
|
+
/**
|
|
148
|
+
* Skip spawning the hub server. Tests flip this on and pre-seed
|
|
149
|
+
* `<configDir>/hub/run/hub.port` so `readHubPort` can resolve the
|
|
150
|
+
* cloudflared target without a live process. Production always leaves
|
|
151
|
+
* this off so the bringup self-heals a missing hub.
|
|
152
|
+
*/
|
|
153
|
+
skipHub?: boolean;
|
|
115
154
|
now?: () => Date;
|
|
116
155
|
/**
|
|
117
156
|
* Override `~/.parachute/vault` for the 2FA-enrollment probe. Tests
|
|
@@ -139,6 +178,11 @@ interface Resolved {
|
|
|
139
178
|
configPath: string;
|
|
140
179
|
logPath: string;
|
|
141
180
|
cloudflaredHome: string;
|
|
181
|
+
configDir: string;
|
|
182
|
+
hubOrigin: string | undefined;
|
|
183
|
+
hubEnsureOpts: Omit<EnsureHubOpts, "configDir" | "wellKnownDir" | "log">;
|
|
184
|
+
wellKnownDir: string;
|
|
185
|
+
skipHub: boolean;
|
|
142
186
|
now: () => Date;
|
|
143
187
|
vaultHome: string | undefined;
|
|
144
188
|
vaultAuthStatus: VaultAuthStatus | undefined;
|
|
@@ -146,7 +190,12 @@ interface Resolved {
|
|
|
146
190
|
|
|
147
191
|
function resolve(opts: ExposeCloudflareOpts): Resolved {
|
|
148
192
|
const tunnelName = opts.tunnelName ?? DEFAULT_TUNNEL_NAME;
|
|
149
|
-
const
|
|
193
|
+
const configDir = opts.configDir ?? CONFIG_DIR;
|
|
194
|
+
// Derive per-tunnel config/log paths from the *resolved* configDir, not the
|
|
195
|
+
// real `CONFIG_DIR`. When a test threads a tmp `configDir` but omits explicit
|
|
196
|
+
// `configPath`/`logPath`, this keeps the derived files inside the tmp dir
|
|
197
|
+
// instead of writing fixtures into the operator's real ~/.parachute.
|
|
198
|
+
const paths = cloudflaredPathsFor(tunnelName, configDir);
|
|
150
199
|
return {
|
|
151
200
|
runner: opts.runner ?? defaultRunner,
|
|
152
201
|
spawner: opts.spawner ?? defaultCloudflaredSpawner,
|
|
@@ -159,6 +208,11 @@ function resolve(opts: ExposeCloudflareOpts): Resolved {
|
|
|
159
208
|
configPath: opts.configPath ?? paths.configPath,
|
|
160
209
|
logPath: opts.logPath ?? paths.logPath,
|
|
161
210
|
cloudflaredHome: opts.cloudflaredHome ?? DEFAULT_CLOUDFLARED_HOME,
|
|
211
|
+
configDir,
|
|
212
|
+
hubOrigin: opts.hubOrigin,
|
|
213
|
+
hubEnsureOpts: opts.hubEnsureOpts ?? {},
|
|
214
|
+
wellKnownDir: opts.wellKnownDir ?? WELL_KNOWN_DIR,
|
|
215
|
+
skipHub: opts.skipHub ?? false,
|
|
162
216
|
now: opts.now ?? (() => new Date()),
|
|
163
217
|
vaultHome: opts.vaultHome,
|
|
164
218
|
vaultAuthStatus: opts.vaultAuthStatus,
|
|
@@ -179,11 +233,13 @@ function printAuthGuidance(log: (line: string) => void, vaultUrl: string): void
|
|
|
179
233
|
log(" then point your connector at:");
|
|
180
234
|
log(` ${vaultUrl}`);
|
|
181
235
|
log("");
|
|
182
|
-
log(" Scripts / machines:");
|
|
183
|
-
log(" parachute
|
|
184
|
-
log(" Authorization: Bearer
|
|
236
|
+
log(" Scripts / machines (hub-issued JWT — set the owner password first):");
|
|
237
|
+
log(" parachute auth mint-token --scope vault:<name>:read # or :write");
|
|
238
|
+
log(" Authorization: Bearer <hub-jwt> # attach the printed token to every request");
|
|
239
|
+
log(" (or: Admin → Vaults → Connect mints one and shows the header for you)");
|
|
185
240
|
log("");
|
|
186
|
-
log("
|
|
241
|
+
log("The owner password gates both paths — browser sign-in and minting tokens.");
|
|
242
|
+
log("Full auth reference:");
|
|
187
243
|
log(` ${AUTH_DOC_URL}`);
|
|
188
244
|
}
|
|
189
245
|
|
|
@@ -239,6 +295,46 @@ export async function exposeCloudflareUp(
|
|
|
239
295
|
return 1;
|
|
240
296
|
}
|
|
241
297
|
|
|
298
|
+
// Resolve the public hub origin before spawning the hub server — it gets
|
|
299
|
+
// baked into the OAuth `iss` claim via the `--issuer` flag. For Cloudflare
|
|
300
|
+
// ingress the canonical origin is the user-supplied hostname (mirrors the
|
|
301
|
+
// Tailscale Funnel path which uses the tailnet FQDN). Falling back to the
|
|
302
|
+
// request origin would put `http://127.0.0.1:<port>` in tokens, which any
|
|
303
|
+
// client following RFC 8414 would reject.
|
|
304
|
+
const canonicalOrigin = `https://${hostname}`;
|
|
305
|
+
const hubOrigin =
|
|
306
|
+
deriveHubOrigin({ override: r.hubOrigin, exposeFqdn: hostname }) ?? canonicalOrigin;
|
|
307
|
+
|
|
308
|
+
// Ensure the hub is running and figure out the loopback port cloudflared
|
|
309
|
+
// should target. The hub does all internal routing (discovery, admin,
|
|
310
|
+
// OAuth, well-known, per-vault proxy, generic /<svc>/* dispatch) — same
|
|
311
|
+
// shape the Tailscale Funnel path uses (see `planEntries` in expose.ts).
|
|
312
|
+
// Pre-2026-05-27 the cloudflared config routed straight at vault's port,
|
|
313
|
+
// so a public URL like https://gitcoin.parachute.computer/ returned 404
|
|
314
|
+
// from vault itself instead of the hub's discovery page; admin / OAuth
|
|
315
|
+
// were unreachable. Aaron hit this on a fresh EC2 install.
|
|
316
|
+
let hubPort: number;
|
|
317
|
+
if (r.skipHub) {
|
|
318
|
+
const existing = readHubPort(r.configDir);
|
|
319
|
+
if (existing === undefined) {
|
|
320
|
+
throw new Error("skipHub set but no hub.port on disk — tests must seed one");
|
|
321
|
+
}
|
|
322
|
+
hubPort = existing;
|
|
323
|
+
} else {
|
|
324
|
+
const hub = await ensureHubRunning({
|
|
325
|
+
reservedPorts: manifest.services.map((s) => s.port),
|
|
326
|
+
...r.hubEnsureOpts,
|
|
327
|
+
configDir: r.configDir,
|
|
328
|
+
wellKnownDir: r.wellKnownDir,
|
|
329
|
+
issuer: hubOrigin,
|
|
330
|
+
log: r.log,
|
|
331
|
+
});
|
|
332
|
+
hubPort = hub.port;
|
|
333
|
+
if (hub.started) r.log(`✓ hub started (pid ${hub.pid}, port ${hub.port}).`);
|
|
334
|
+
else r.log(`✓ hub already running (pid ${hub.pid}, port ${hub.port}).`);
|
|
335
|
+
}
|
|
336
|
+
if (hubPort === 0) hubPort = HUB_DEFAULT_PORT;
|
|
337
|
+
|
|
242
338
|
let tunnel: Tunnel | undefined;
|
|
243
339
|
try {
|
|
244
340
|
tunnel = await findTunnelByName(r.runner, r.tunnelName);
|
|
@@ -282,7 +378,13 @@ export async function exposeCloudflareUp(
|
|
|
282
378
|
tunnelUuid: tunnel.id,
|
|
283
379
|
credentialsFile: credsFile,
|
|
284
380
|
hostname,
|
|
285
|
-
|
|
381
|
+
// Route into the hub, not vault directly. The hub dispatches
|
|
382
|
+
// discovery / admin / OAuth / per-vault proxy / generic /<svc>/*
|
|
383
|
+
// — same shape Tailscale Funnel uses (single mount → hub catchall).
|
|
384
|
+
// Pre-fix this was `vaultEntry.port`, which served vault's own 404
|
|
385
|
+
// page on every request that wasn't /vault/<name>/… — admin SPA and
|
|
386
|
+
// OAuth surfaces were unreachable from the public URL.
|
|
387
|
+
servicePort: hubPort,
|
|
286
388
|
},
|
|
287
389
|
r.configPath,
|
|
288
390
|
);
|
|
@@ -329,9 +431,11 @@ export async function exposeCloudflareUp(
|
|
|
329
431
|
|
|
330
432
|
r.log("");
|
|
331
433
|
r.log(`✓ Cloudflare tunnel up (pid ${pid}).`);
|
|
332
|
-
r.log(`
|
|
333
|
-
r.log(`
|
|
334
|
-
r.log(`
|
|
434
|
+
r.log(` Open: ${baseUrl}/`);
|
|
435
|
+
r.log(` Admin: ${baseUrl}/admin/`);
|
|
436
|
+
r.log(` Vault: ${vaultUrl}`);
|
|
437
|
+
r.log(` OAuth: ${hubOrigin}`);
|
|
438
|
+
r.log(` Logs: ${r.logPath}`);
|
|
335
439
|
r.log("");
|
|
336
440
|
r.log("Point a claude.ai / ChatGPT connector at:");
|
|
337
441
|
r.log(` ${vaultUrl}`);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { createInterface } from "node:readline/promises";
|
|
14
14
|
import {
|
|
15
15
|
DEFAULT_CLOUDFLARED_HOME,
|
|
16
|
+
cloudflaredInstallHint,
|
|
16
17
|
isCloudflaredInstalled,
|
|
17
18
|
isCloudflaredLoggedIn,
|
|
18
19
|
} from "../cloudflare/detect.ts";
|
|
@@ -273,19 +274,17 @@ async function guideCloudflareSetup(
|
|
|
273
274
|
return false;
|
|
274
275
|
}
|
|
275
276
|
} else {
|
|
277
|
+
// 2026-05-27 refresh: distro-package paths (`apt-get`, `dnf`) are
|
|
278
|
+
// unreliable across versions — Aaron hit `No match for argument:
|
|
279
|
+
// cloudflared` on Amazon Linux 2023 — and the
|
|
280
|
+
// pkg.cloudflare.com / developers.cloudflare.com paths the old hint
|
|
281
|
+
// pointed at now serve HTML/404. Defer to `cloudflaredInstallHint`,
|
|
282
|
+
// which writes the canonical GitHub-release static-binary path
|
|
283
|
+
// matching the host's architecture.
|
|
276
284
|
r.log("");
|
|
277
285
|
r.log("Cloudflare Tunnel uses the `cloudflared` binary, which isn't installed yet.");
|
|
278
|
-
r.log("
|
|
279
|
-
r.
|
|
280
|
-
r.log(
|
|
281
|
-
" curl -L https://pkg.cloudflare.com/install.sh | sudo bash && sudo apt-get install -y cloudflared",
|
|
282
|
-
);
|
|
283
|
-
r.log(" RHEL / Fedora:");
|
|
284
|
-
r.log(" sudo dnf install cloudflared");
|
|
285
|
-
r.log(" Tarball / other:");
|
|
286
|
-
r.log(
|
|
287
|
-
" https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/",
|
|
288
|
-
);
|
|
286
|
+
r.log("");
|
|
287
|
+
for (const line of cloudflaredInstallHint(r.platform).split("\n")) r.log(line);
|
|
289
288
|
r.log("");
|
|
290
289
|
r.log("After install, re-run: parachute expose public");
|
|
291
290
|
return false;
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* tailscale/cloudflared.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { DEFAULT_CLOUDFLARED_HOME } from "../cloudflare/detect.ts";
|
|
31
|
+
import { DEFAULT_CLOUDFLARED_HOME, cloudflaredInstallHint } from "../cloudflare/detect.ts";
|
|
32
32
|
import {
|
|
33
33
|
type DetectProvidersOpts,
|
|
34
34
|
type ProviderAvailability,
|
|
@@ -109,9 +109,11 @@ function reportNeitherReady(r: Resolved, p: ProviderAvailability): number {
|
|
|
109
109
|
r.log("");
|
|
110
110
|
r.log(" Option B — Cloudflare Tunnel (your own domain, Cloudflare DNS):");
|
|
111
111
|
if (!p.cloudflare.available) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
// 2026-05-27 refresh: defer to the shared install-hint helper so the
|
|
113
|
+
// canonical install path stays in one place. Pre-refresh this surface
|
|
114
|
+
// hard-coded a developers.cloudflare.com URL that now serves HTML/404.
|
|
115
|
+
r.log(" 1. Install cloudflared:");
|
|
116
|
+
for (const line of cloudflaredInstallHint().split("\n")) r.log(` ${line}`);
|
|
115
117
|
r.log(" 2. Log in: cloudflared tunnel login");
|
|
116
118
|
r.log(" 3. Re-run with --domain: parachute expose public --cloudflare --domain <hostname>");
|
|
117
119
|
} else {
|
package/src/commands/expose.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { type ServiceEntry, readManifest } from "../services-manifest.ts";
|
|
|
24
24
|
import { type ServeEntry, bringupCommand, teardownCommand } from "../tailscale/commands.ts";
|
|
25
25
|
import { getFqdn, isTailscaleInstalled } from "../tailscale/detect.ts";
|
|
26
26
|
import { type Runner, defaultRunner } from "../tailscale/run.ts";
|
|
27
|
+
import { clearVaultHubOrigin } from "../vault-hub-origin-env.ts";
|
|
27
28
|
import type { VaultAuthStatus } from "../vault/auth-status.ts";
|
|
28
29
|
import {
|
|
29
30
|
WELL_KNOWN_DIR,
|
|
@@ -438,6 +439,13 @@ export async function exposeOff(layer: ExposeLayer, opts: ExposeOpts = {}): Prom
|
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
clearExposeState(statePath);
|
|
442
|
+
// Drop the persisted PARACHUTE_HUB_ORIGIN from vault's `.env`. `expose up`
|
|
443
|
+
// (via the vault restart) persisted the public origin so the launchd /
|
|
444
|
+
// systemd daemon validates `iss` against it. With exposure gone, a
|
|
445
|
+
// local-only hub mints loopback-`iss` tokens, so a stale public origin left
|
|
446
|
+
// in `.env` would itself cause the mismatch on the next daemon restart.
|
|
447
|
+
// Reverting to vault's loopback default (`getHubOrigin`) keeps them aligned.
|
|
448
|
+
clearVaultHubOrigin(configDir, log);
|
|
441
449
|
// Pair to the debug-only write at expose-up — clean up the inspection artifact
|
|
442
450
|
// on teardown so it doesn't outlive the layer it described.
|
|
443
451
|
if (existsSync(wellKnownFilePath)) {
|