@openparachute/hub 0.5.13-rc.21 → 0.5.13-rc.29
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 +21 -0
- package/package.json +1 -1
- package/src/__tests__/api-hub.test.ts +251 -0
- package/src/__tests__/hub-origin-resolution.test.ts +50 -0
- package/src/__tests__/serve.test.ts +9 -0
- package/src/__tests__/spawn-env-propagation.test.ts +78 -0
- package/src/admin-vaults.ts +6 -1
- package/src/api-hub.ts +201 -0
- package/src/api-modules-ops.ts +8 -1
- package/src/commands/auth.ts +7 -1
- package/src/commands/expose-auth-preflight.ts +6 -1
- package/src/commands/expose-cloudflare.ts +6 -1
- package/src/commands/expose-interactive.ts +7 -1
- package/src/commands/install.ts +7 -1
- package/src/commands/lifecycle.ts +10 -1
- package/src/commands/serve.ts +18 -8
- package/src/commands/upgrade.ts +5 -0
- package/src/commands/vault-tokens-create-interactive.ts +7 -1
- package/src/commands/vault.ts +3 -0
- package/src/hub-control.ts +6 -1
- package/src/hub-server.ts +32 -1
- package/src/supervisor.ts +4 -0
- package/src/tailscale/run.ts +7 -1
- package/web/ui/dist/assets/{index-BqjySZ_7.js → index-CG229ge6.js} +14 -14
- package/web/ui/dist/assets/{index-5Mj6FqPg.css → index-DArp3eO_.css} +1 -1
- package/web/ui/dist/index.html +2 -2
package/src/commands/auth.ts
CHANGED
|
@@ -60,7 +60,13 @@ export interface Runner {
|
|
|
60
60
|
|
|
61
61
|
export const defaultRunner: Runner = {
|
|
62
62
|
async run(cmd) {
|
|
63
|
-
|
|
63
|
+
// Inherit env so the child (e.g. parachute-vault subprocess) sees PATH,
|
|
64
|
+
// HOME, PARACHUTE_HOME, etc. Bun.spawn defaults to empty env — see
|
|
65
|
+
// api-modules-ops.ts:defaultRun.
|
|
66
|
+
const proc = Bun.spawn([...cmd], {
|
|
67
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
68
|
+
env: process.env,
|
|
69
|
+
});
|
|
64
70
|
return await proc.exited;
|
|
65
71
|
},
|
|
66
72
|
};
|
|
@@ -26,7 +26,12 @@ import { type VaultAuthStatus, readVaultAuthStatus } from "../vault/auth-status.
|
|
|
26
26
|
export type InteractiveRunner = (cmd: readonly string[]) => Promise<number>;
|
|
27
27
|
|
|
28
28
|
const defaultInteractiveRunner: InteractiveRunner = async (cmd) => {
|
|
29
|
-
|
|
29
|
+
// Inherit env so subprocesses see PATH (to find `tailscale`), HOME, etc.
|
|
30
|
+
// Bun.spawn defaults to empty env — see api-modules-ops.ts:defaultRun.
|
|
31
|
+
const proc = Bun.spawn([...cmd], {
|
|
32
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
33
|
+
env: process.env,
|
|
34
|
+
});
|
|
30
35
|
return await proc.exited;
|
|
31
36
|
};
|
|
32
37
|
|
|
@@ -69,7 +69,12 @@ export const defaultCloudflaredSpawner: CloudflaredSpawner = {
|
|
|
69
69
|
spawn(cmd, logFile) {
|
|
70
70
|
mkdirSync(dirname(logFile), { recursive: true });
|
|
71
71
|
const fd = openSync(logFile, "a");
|
|
72
|
-
|
|
72
|
+
// Inherit env so cloudflared sees HOME (where it reads ~/.cloudflared/),
|
|
73
|
+
// PATH, etc. Bun.spawn defaults to empty env — see api-modules-ops.ts.
|
|
74
|
+
const proc = Bun.spawn([...cmd], {
|
|
75
|
+
stdio: ["ignore", fd, fd],
|
|
76
|
+
env: process.env,
|
|
77
|
+
});
|
|
73
78
|
proc.unref();
|
|
74
79
|
return proc.pid;
|
|
75
80
|
},
|
|
@@ -46,7 +46,13 @@ import { type ExposeOpts, exposePublic } from "./expose.ts";
|
|
|
46
46
|
export type InteractiveRunner = (cmd: readonly string[]) => Promise<number>;
|
|
47
47
|
|
|
48
48
|
const defaultInteractiveRunner: InteractiveRunner = async (cmd) => {
|
|
49
|
-
|
|
49
|
+
// Inherit env so interactive subprocesses (e.g. `brew install cloudflared`,
|
|
50
|
+
// `cloudflared tunnel login`) see PATH, HOME, etc. Bun.spawn defaults to
|
|
51
|
+
// empty env — see api-modules-ops.ts:defaultRun.
|
|
52
|
+
const proc = Bun.spawn([...cmd], {
|
|
53
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
54
|
+
env: process.env,
|
|
55
|
+
});
|
|
50
56
|
return await proc.exited;
|
|
51
57
|
};
|
|
52
58
|
|
package/src/commands/install.ts
CHANGED
|
@@ -250,7 +250,13 @@ export interface InstallOpts {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
async function defaultRunner(cmd: readonly string[]): Promise<number> {
|
|
253
|
-
|
|
253
|
+
// Inherit env (TMPDIR, BUN_INSTALL, PATH, HOME, PARACHUTE_*, etc.) — see
|
|
254
|
+
// api-modules-ops.ts:defaultRun for the rationale. Same Bun.spawn-defaults-
|
|
255
|
+
// to-empty-env bug; same one-line fix. See hub#349.
|
|
256
|
+
const proc = Bun.spawn([...cmd], {
|
|
257
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
258
|
+
env: process.env,
|
|
259
|
+
});
|
|
254
260
|
return await proc.exited;
|
|
255
261
|
}
|
|
256
262
|
|
|
@@ -67,6 +67,10 @@ export const defaultSpawner: Spawner = {
|
|
|
67
67
|
// wrapped startCmds like `pnpm exec tsx server.ts` leave the tsx
|
|
68
68
|
// grandchild bound to the port after stop → restart hits EADDRINUSE.
|
|
69
69
|
detached: true,
|
|
70
|
+
// Inherit env so child sees PATH, HOME, PARACHUTE_HOME, etc.
|
|
71
|
+
// Bun.spawn defaults to empty env — see api-modules-ops.ts:defaultRun.
|
|
72
|
+
// Per-call `opts.env` overrides merge on top below.
|
|
73
|
+
env: process.env,
|
|
70
74
|
};
|
|
71
75
|
if (opts?.env) spawnOpts.env = { ...process.env, ...opts.env };
|
|
72
76
|
if (opts?.cwd) spawnOpts.cwd = opts.cwd;
|
|
@@ -647,7 +651,12 @@ export async function logs(svc: string, opts: LogsOpts = {}): Promise<number> {
|
|
|
647
651
|
if (follow) {
|
|
648
652
|
const spawner = opts.tailSpawner ?? {
|
|
649
653
|
spawn(cmd) {
|
|
650
|
-
|
|
654
|
+
// Inherit env so `tail` sees PATH, etc. Bun.spawn defaults to empty
|
|
655
|
+
// env — see api-modules-ops.ts:defaultRun.
|
|
656
|
+
const proc = Bun.spawn([...cmd], {
|
|
657
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
658
|
+
env: process.env,
|
|
659
|
+
});
|
|
651
660
|
return proc.pid;
|
|
652
661
|
},
|
|
653
662
|
};
|
package/src/commands/serve.ts
CHANGED
|
@@ -149,16 +149,26 @@ export async function seedInitialAdminIfNeeded(
|
|
|
149
149
|
* the token can't proceed; an attacker who reads it before the
|
|
150
150
|
* operator wins the race).
|
|
151
151
|
*/
|
|
152
|
-
export function formatBootstrapTokenBanner(token: string): string {
|
|
152
|
+
export function formatBootstrapTokenBanner(token: string, hubUrl?: string): string {
|
|
153
|
+
const rule = "═".repeat(64);
|
|
154
|
+
// Substitute the actual hub URL when known (PARACHUTE_HUB_ORIGIN). Operators
|
|
155
|
+
// staring at the banner in Render Logs shouldn't have to figure out their
|
|
156
|
+
// own URL — show the literal placeholder only when the issuer isn't set.
|
|
157
|
+
const url = hubUrl && hubUrl.length > 0 ? hubUrl.replace(/\/+$/, "") : "<hub-url>";
|
|
153
158
|
return [
|
|
154
|
-
"[wizard]
|
|
155
|
-
|
|
156
|
-
"[wizard]
|
|
159
|
+
"[wizard]",
|
|
160
|
+
`[wizard] ${rule}`,
|
|
161
|
+
"[wizard] PARACHUTE BOOTSTRAP TOKEN",
|
|
162
|
+
`[wizard] ${rule}`,
|
|
157
163
|
"[wizard]",
|
|
158
164
|
`[wizard] ${token}`,
|
|
159
165
|
"[wizard]",
|
|
160
|
-
|
|
161
|
-
"[wizard] admin
|
|
166
|
+
`[wizard] → Visit ${url}/admin/setup and paste this token to create`,
|
|
167
|
+
"[wizard] your admin account.",
|
|
168
|
+
"[wizard] → Expires when admin is created OR when hub restarts.",
|
|
169
|
+
"[wizard]",
|
|
170
|
+
`[wizard] ${rule}`,
|
|
171
|
+
"[wizard]",
|
|
162
172
|
].join("\n");
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -199,14 +209,14 @@ export async function serve(opts: ServeOpts = {}): Promise<{
|
|
|
199
209
|
|
|
200
210
|
if (adminBootstrap === "needs-setup") {
|
|
201
211
|
log(
|
|
202
|
-
"parachute serve: no admin account configured.
|
|
212
|
+
"parachute serve: no admin account configured. Visit /admin/setup once the hub is reachable, or seed via PARACHUTE_INITIAL_ADMIN_USERNAME + PARACHUTE_INITIAL_ADMIN_PASSWORD env vars for scripted deploys.",
|
|
203
213
|
);
|
|
204
214
|
// Mint a bootstrap token + log it. The wizard's account POST will
|
|
205
215
|
// require this token, so an attacker who beats the operator to the
|
|
206
216
|
// freshly-provisioned URL still can't claim the admin row without
|
|
207
217
|
// shell access to the platform's startup logs.
|
|
208
218
|
const token = generateBootstrapToken();
|
|
209
|
-
log(formatBootstrapTokenBanner(token));
|
|
219
|
+
log(formatBootstrapTokenBanner(token, issuer));
|
|
210
220
|
}
|
|
211
221
|
|
|
212
222
|
const supervisor = opts.supervisor ?? new Supervisor();
|
package/src/commands/upgrade.ts
CHANGED
|
@@ -74,17 +74,22 @@ export interface UpgradeRunner {
|
|
|
74
74
|
|
|
75
75
|
export const defaultRunner: UpgradeRunner = {
|
|
76
76
|
async run(cmd, opts) {
|
|
77
|
+
// Inherit env so `bun add -g` etc. see TMPDIR, BUN_INSTALL, PATH, HOME.
|
|
78
|
+
// Bun.spawn defaults to empty env — see api-modules-ops.ts:defaultRun.
|
|
77
79
|
const proc = Bun.spawn([...cmd], {
|
|
78
80
|
cwd: opts?.cwd,
|
|
79
81
|
stdio: ["inherit", "inherit", "inherit"],
|
|
82
|
+
env: process.env,
|
|
80
83
|
});
|
|
81
84
|
return await proc.exited;
|
|
82
85
|
},
|
|
83
86
|
async capture(cmd, opts) {
|
|
87
|
+
// Inherit env — same rationale as `run` above.
|
|
84
88
|
const proc = Bun.spawn([...cmd], {
|
|
85
89
|
cwd: opts?.cwd,
|
|
86
90
|
stdout: "pipe",
|
|
87
91
|
stderr: "pipe",
|
|
92
|
+
env: process.env,
|
|
88
93
|
});
|
|
89
94
|
const [stdout, stderr] = await Promise.all([
|
|
90
95
|
new Response(proc.stdout).text(),
|
|
@@ -30,7 +30,13 @@ import { createInterface } from "node:readline/promises";
|
|
|
30
30
|
export type InteractiveRunner = (cmd: readonly string[]) => Promise<number>;
|
|
31
31
|
|
|
32
32
|
const defaultInteractiveRunner: InteractiveRunner = async (cmd) => {
|
|
33
|
-
|
|
33
|
+
// Inherit env so the child (parachute-vault subprocess) sees PATH, HOME,
|
|
34
|
+
// PARACHUTE_HOME, etc. Bun.spawn defaults to empty env — see
|
|
35
|
+
// api-modules-ops.ts:defaultRun.
|
|
36
|
+
const proc = Bun.spawn([...cmd], {
|
|
37
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
38
|
+
env: process.env,
|
|
39
|
+
});
|
|
34
40
|
return await proc.exited;
|
|
35
41
|
};
|
|
36
42
|
|
package/src/commands/vault.ts
CHANGED
|
@@ -2,6 +2,9 @@ export async function dispatchVault(args: readonly string[]): Promise<number> {
|
|
|
2
2
|
try {
|
|
3
3
|
const proc = Bun.spawn(["parachute-vault", ...args], {
|
|
4
4
|
stdio: ["inherit", "inherit", "inherit"],
|
|
5
|
+
// Inherit env so parachute-vault sees PATH, HOME, PARACHUTE_HOME, etc.
|
|
6
|
+
// Bun.spawn defaults to empty env — see api-modules-ops.ts:defaultRun.
|
|
7
|
+
env: process.env,
|
|
5
8
|
});
|
|
6
9
|
return await proc.exited;
|
|
7
10
|
} catch (err) {
|
package/src/hub-control.ts
CHANGED
|
@@ -74,7 +74,12 @@ export interface HubSpawner {
|
|
|
74
74
|
export const defaultHubSpawner: HubSpawner = {
|
|
75
75
|
spawn(cmd, logFile) {
|
|
76
76
|
const fd = openSync(logFile, "a");
|
|
77
|
-
|
|
77
|
+
// Inherit env so the hub child process sees PATH, HOME, PARACHUTE_HOME,
|
|
78
|
+
// etc. Bun.spawn defaults to empty env — see api-modules-ops.ts.
|
|
79
|
+
const proc = Bun.spawn([...cmd], {
|
|
80
|
+
stdio: ["ignore", fd, fd],
|
|
81
|
+
env: process.env,
|
|
82
|
+
});
|
|
78
83
|
proc.unref();
|
|
79
84
|
return proc.pid;
|
|
80
85
|
},
|
package/src/hub-server.ts
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* /admin/host-admin-token (GET) → SPA bearer mint (cookie-gated)
|
|
47
47
|
* /admin/vault-admin-token/<n> (GET) → per-vault bearer mint (cookie-gated)
|
|
48
48
|
* /api/me (GET) → who-am-I (session+CSRF or hasSession:false)
|
|
49
|
+
* /api/hub (GET) → hub version + uptime + install-source (host:admin)
|
|
49
50
|
* /api/modules (GET) → curated + installed module catalog (host:auth)
|
|
50
51
|
* /api/modules/channel (PUT) → operator channel toggle (host:admin)
|
|
51
52
|
* /api/modules/:short/install (POST) → bun add + spawn (async op)
|
|
@@ -117,6 +118,7 @@ import { handleHostAdminToken } from "./admin-host-admin-token.ts";
|
|
|
117
118
|
import { handleVaultAdminToken } from "./admin-vault-admin-token.ts";
|
|
118
119
|
import { handleCreateVault } from "./admin-vaults.ts";
|
|
119
120
|
import { handleAccountChangePasswordGet, handleAccountChangePasswordPost } from "./api-account.ts";
|
|
121
|
+
import { handleApiHub } from "./api-hub.ts";
|
|
120
122
|
import { handleApiMe } from "./api-me.ts";
|
|
121
123
|
import { handleApiMintToken } from "./api-mint-token.ts";
|
|
122
124
|
import { handleApiModulesConfig, parseModulesConfigPath } from "./api-modules-config.ts";
|
|
@@ -165,6 +167,7 @@ import {
|
|
|
165
167
|
} from "./oauth-handlers.ts";
|
|
166
168
|
import { buildHubBoundOrigins } from "./origin-check.ts";
|
|
167
169
|
import { clearPid, writePid } from "./process-state.ts";
|
|
170
|
+
import { isHttpsRequest } from "./request-protocol.ts";
|
|
168
171
|
import {
|
|
169
172
|
FIRST_PARTY_FALLBACKS,
|
|
170
173
|
KNOWN_MODULES,
|
|
@@ -942,7 +945,24 @@ export function resolveIssuer(
|
|
|
942
945
|
if (stored) return stored;
|
|
943
946
|
}
|
|
944
947
|
if (configuredIssuer) return configuredIssuer;
|
|
945
|
-
|
|
948
|
+
// Reverse-proxy aware: Render / Tailscale Funnel / cloudflared terminate
|
|
949
|
+
// TLS at the edge and forward plain HTTP to hub. Without X-Forwarded-Proto
|
|
950
|
+
// honoring, `req.url.origin` is `http://...` and hub publishes mixed-content
|
|
951
|
+
// URLs in OAuth discovery (`registration_endpoint`, `authorization_endpoint`,
|
|
952
|
+
// etc.) — browsers block them when the page itself loaded over https://.
|
|
953
|
+
// The `isHttpsRequest` helper is the canonical place where this trust
|
|
954
|
+
// is established (also used for the Secure cookie attribute).
|
|
955
|
+
//
|
|
956
|
+
// We do NOT honor X-Forwarded-Host. Render, Tailscale Funnel, and
|
|
957
|
+
// cloudflared all preserve the Host header end-to-end, so `req.url`'s
|
|
958
|
+
// host already reflects the public hostname. Operators on a proxy that
|
|
959
|
+
// rewrites Host (some nginx / Caddy configs) should set hub_origin via
|
|
960
|
+
// the admin SPA — that path bypasses this fallback entirely.
|
|
961
|
+
const url = new URL(req.url);
|
|
962
|
+
if (isHttpsRequest(req)) {
|
|
963
|
+
url.protocol = "https:";
|
|
964
|
+
}
|
|
965
|
+
return url.origin;
|
|
946
966
|
}
|
|
947
967
|
|
|
948
968
|
/**
|
|
@@ -1526,6 +1546,17 @@ export function hubFetch(
|
|
|
1526
1546
|
return handleApiMe(req, { db: getDb() });
|
|
1527
1547
|
}
|
|
1528
1548
|
|
|
1549
|
+
// Hub version + uptime + install-source — drives the admin SPA's
|
|
1550
|
+
// version badge (hub#348). Bearer-gated on `parachute:host:admin`
|
|
1551
|
+
// (same as the rest of the operator-only admin surface).
|
|
1552
|
+
if (pathname === "/api/hub") {
|
|
1553
|
+
if (!getDb) return dbNotConfigured();
|
|
1554
|
+
return handleApiHub(req, {
|
|
1555
|
+
db: getDb(),
|
|
1556
|
+
issuer: oauthDeps(req).issuer,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1529
1560
|
if (pathname === "/api/modules") {
|
|
1530
1561
|
if (!getDb) return dbNotConfigured();
|
|
1531
1562
|
const modulesDeps: Parameters<typeof handleApiModules>[1] = {
|
package/src/supervisor.ts
CHANGED
|
@@ -403,6 +403,10 @@ async function pumpLines(
|
|
|
403
403
|
const defaultSpawnFn: SpawnFn = (req) => {
|
|
404
404
|
const spawnOpts: Parameters<typeof Bun.spawn>[1] = {
|
|
405
405
|
stdio: ["ignore", "pipe", "pipe"],
|
|
406
|
+
// Inherit env so supervised module sees PATH, HOME, PARACHUTE_HOME, etc.
|
|
407
|
+
// Bun.spawn defaults to empty env — see api-modules-ops.ts:defaultRun.
|
|
408
|
+
// Per-call `req.env` overrides merge on top below.
|
|
409
|
+
env: process.env,
|
|
406
410
|
};
|
|
407
411
|
if (req.cwd) spawnOpts.cwd = req.cwd;
|
|
408
412
|
if (req.env) spawnOpts.env = { ...process.env, ...req.env };
|
package/src/tailscale/run.ts
CHANGED
|
@@ -7,7 +7,13 @@ export interface CommandResult {
|
|
|
7
7
|
export type Runner = (cmd: readonly string[]) => Promise<CommandResult>;
|
|
8
8
|
|
|
9
9
|
export async function defaultRunner(cmd: readonly string[]): Promise<CommandResult> {
|
|
10
|
-
|
|
10
|
+
// Inherit env so `tailscale` sees PATH (and HOME for state dir). Bun.spawn
|
|
11
|
+
// defaults to empty env — see api-modules-ops.ts:defaultRun for rationale.
|
|
12
|
+
const proc = Bun.spawn([...cmd], {
|
|
13
|
+
stdout: "pipe",
|
|
14
|
+
stderr: "pipe",
|
|
15
|
+
env: process.env,
|
|
16
|
+
});
|
|
11
17
|
const [stdout, stderr, code] = await Promise.all([
|
|
12
18
|
new Response(proc.stdout).text(),
|
|
13
19
|
new Response(proc.stderr).text(),
|