@openparachute/hub 0.5.13-rc.23 → 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 +7 -4
- package/package.json +1 -1
- 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-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 +19 -1
- package/src/supervisor.ts +4 -0
- package/src/tailscale/run.ts +7 -1
package/README.md
CHANGED
|
@@ -24,11 +24,14 @@ One-click Render deploy via the `render.yaml` Blueprint in this repo. Provisions
|
|
|
24
24
|
|
|
25
25
|
**Want pre-release / rc modules?** Set `PARACHUTE_INSTALL_CHANNEL=rc` in your Render dashboard env vars (useful for dev/testing against the rc release line).
|
|
26
26
|
|
|
27
|
-
After deploy:
|
|
27
|
+
After deploy completes:
|
|
28
28
|
|
|
29
|
-
1. Open
|
|
30
|
-
2.
|
|
31
|
-
3.
|
|
29
|
+
1. Open Render Logs → search for `parachute-bootstrap-` to find your one-time admin setup token (printed in a prominent banner on first boot).
|
|
30
|
+
2. Visit your Render service URL's `/admin/setup` → paste the token → create your admin account.
|
|
31
|
+
3. Set custom domain (optional) → set `PARACHUTE_HUB_ORIGIN` env to match.
|
|
32
|
+
4. Install modules via the admin SPA at `/admin/modules` (or via the wizard).
|
|
33
|
+
|
|
34
|
+
Operators who want env-var-driven seeding (CI, scripted deploys) can still set `PARACHUTE_INITIAL_ADMIN_USERNAME` + `PARACHUTE_INITIAL_ADMIN_PASSWORD` manually in the Render dashboard — hub honors them when present.
|
|
32
35
|
|
|
33
36
|
Render's docs on Blueprints: <https://render.com/docs/blueprint-spec>
|
|
34
37
|
|
package/package.json
CHANGED
|
@@ -117,6 +117,56 @@ describe("resolveIssuer — precedence chain", () => {
|
|
|
117
117
|
// Pass 3 — back to request origin.
|
|
118
118
|
expect(resolveIssuer(req(baseUrl), db, undefined)).toBe("http://127.0.0.1:1939");
|
|
119
119
|
});
|
|
120
|
+
|
|
121
|
+
test("X-Forwarded-Proto: https upgrades the request-origin fallback", () => {
|
|
122
|
+
// Render / Tailscale Funnel / cloudflared terminate TLS at the edge
|
|
123
|
+
// and forward plain HTTP. Without honoring the header, hub publishes
|
|
124
|
+
// `http://...` in OAuth discovery — mixed-content blocked when the
|
|
125
|
+
// page loaded over https://. See hub#355 (the notes app's
|
|
126
|
+
// /oauth/register call surfaced this).
|
|
127
|
+
const r = new Request("http://parachute-hub.onrender.com/.well-known/oauth-authorization-server", {
|
|
128
|
+
method: "GET",
|
|
129
|
+
headers: { "X-Forwarded-Proto": "https" },
|
|
130
|
+
});
|
|
131
|
+
expect(resolveIssuer(r, db, undefined)).toBe("https://parachute-hub.onrender.com");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("X-Forwarded-Proto with comma-separated values takes the first", () => {
|
|
135
|
+
// Multi-hop proxies append; the leftmost is the original client → edge
|
|
136
|
+
// hop. RFC-style parsing (consistent with isHttpsRequest).
|
|
137
|
+
const r = new Request("http://hub.internal/oauth/token", {
|
|
138
|
+
method: "GET",
|
|
139
|
+
headers: { "X-Forwarded-Proto": "https, http" },
|
|
140
|
+
});
|
|
141
|
+
expect(resolveIssuer(r, db, undefined)).toBe("https://hub.internal");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("missing X-Forwarded-Proto leaves the URL scheme as-is (localhost dev)", () => {
|
|
145
|
+
// No reverse proxy → no header → keep http for the local-dev shape.
|
|
146
|
+
// Operators on plain HTTP localhost depend on this.
|
|
147
|
+
const r = new Request("http://127.0.0.1:1939/oauth/token", { method: "GET" });
|
|
148
|
+
expect(resolveIssuer(r, db, undefined)).toBe("http://127.0.0.1:1939");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("X-Forwarded-Proto is IGNORED when hub_settings or env wins", () => {
|
|
152
|
+
// Precedence guard: X-Forwarded-Proto should only affect the
|
|
153
|
+
// request-origin fallback branch. Explicit operator config
|
|
154
|
+
// (settings row, env var) always wins as-is, including its scheme.
|
|
155
|
+
// Without this guard, a future refactor could accidentally let the
|
|
156
|
+
// header override an operator's deliberate choice.
|
|
157
|
+
const r = new Request("http://hub.internal/oauth/token", {
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: { "X-Forwarded-Proto": "https" },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Env layer wins, even though the header says https — the env value
|
|
163
|
+
// is returned verbatim (preserving whatever scheme the operator set).
|
|
164
|
+
expect(resolveIssuer(r, db, "http://configured.example")).toBe("http://configured.example");
|
|
165
|
+
|
|
166
|
+
// Settings layer wins above env, also verbatim.
|
|
167
|
+
setHubOrigin(db, "http://settings.example");
|
|
168
|
+
expect(resolveIssuer(r, db, "https://env.example")).toBe("http://settings.example");
|
|
169
|
+
});
|
|
120
170
|
});
|
|
121
171
|
|
|
122
172
|
describe("resolveIssuerSource — attribution for SPA", () => {
|
|
@@ -184,6 +184,15 @@ describe("formatBootstrapTokenBanner", () => {
|
|
|
184
184
|
expect(line.startsWith("[wizard]")).toBe(true);
|
|
185
185
|
}
|
|
186
186
|
});
|
|
187
|
+
|
|
188
|
+
test("uses ═ delimiters and an ALL-CAPS heading so operators spot the block in log viewers", () => {
|
|
189
|
+
const banner = formatBootstrapTokenBanner("parachute-bootstrap-visual-token");
|
|
190
|
+
// The ═ box-drawing char is the visual cue an operator scrolling
|
|
191
|
+
// Render's log tab keys off; this assertion locks the new shape so
|
|
192
|
+
// a stylistic regression doesn't silently demote the banner.
|
|
193
|
+
expect(banner).toContain("═");
|
|
194
|
+
expect(banner).toContain("PARACHUTE BOOTSTRAP TOKEN");
|
|
195
|
+
});
|
|
187
196
|
});
|
|
188
197
|
|
|
189
198
|
// --- bootstrap-token generation under needs-setup (Issue 1 wiring) -------
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression test for hub#349: Bun.spawn defaults to an EMPTY env, which meant
|
|
3
|
+
* subprocess `bun add -g` didn't see TMPDIR, BUN_INSTALL, or any other env vars
|
|
4
|
+
* set by the Dockerfile / Render env. All `defaultRun`-style helpers were
|
|
5
|
+
* updated to pass `env: process.env`.
|
|
6
|
+
*
|
|
7
|
+
* This test asserts that property end-to-end: spawn a real child via `bun -e`
|
|
8
|
+
* and have it print one parent-set env var. Pre-fix, the child would not see
|
|
9
|
+
* the var; post-fix, it does.
|
|
10
|
+
*
|
|
11
|
+
* We exercise the production path by importing one representative helper.
|
|
12
|
+
* The full set of seven explicit + several inherited fix sites all use the
|
|
13
|
+
* same `env: process.env` pattern; testing one is sufficient to lock the
|
|
14
|
+
* pattern in place — the others are mechanical applications of it.
|
|
15
|
+
*/
|
|
16
|
+
import { describe, expect, test } from "bun:test";
|
|
17
|
+
|
|
18
|
+
describe("Bun.spawn env propagation (hub#349)", () => {
|
|
19
|
+
test("child process sees parent env when defaultRun-style helper is used", async () => {
|
|
20
|
+
// Unique marker so we can't false-positive against leftover env from
|
|
21
|
+
// another test or the harness itself.
|
|
22
|
+
const markerKey = "PARACHUTE_HUB_SPAWN_ENV_TEST_MARKER";
|
|
23
|
+
const markerValue = `marker-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
24
|
+
|
|
25
|
+
const originalValue = process.env[markerKey];
|
|
26
|
+
process.env[markerKey] = markerValue;
|
|
27
|
+
try {
|
|
28
|
+
// Spawn a child the same way every defaultRun helper does:
|
|
29
|
+
// `env: process.env`. The child prints its view of the marker var.
|
|
30
|
+
const proc = Bun.spawn(
|
|
31
|
+
["bun", "-e", `process.stdout.write(process.env.${markerKey} ?? "MISSING")`],
|
|
32
|
+
{
|
|
33
|
+
stdout: "pipe",
|
|
34
|
+
stderr: "pipe",
|
|
35
|
+
env: process.env,
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
const stdout = await new Response(proc.stdout).text();
|
|
39
|
+
const exitCode = await proc.exited;
|
|
40
|
+
|
|
41
|
+
expect(exitCode).toBe(0);
|
|
42
|
+
expect(stdout).toBe(markerValue);
|
|
43
|
+
} finally {
|
|
44
|
+
if (originalValue === undefined) delete process.env[markerKey];
|
|
45
|
+
else process.env[markerKey] = originalValue;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("child process does NOT see parent env when env is omitted (negative control)", async () => {
|
|
50
|
+
// The bug we're guarding against: without `env: process.env`, Bun.spawn
|
|
51
|
+
// hands the child an empty env. This test pins the failure mode so a
|
|
52
|
+
// future regression (someone removing `env: process.env`) is caught here,
|
|
53
|
+
// not in production on Render.
|
|
54
|
+
const markerKey = "PARACHUTE_HUB_SPAWN_ENV_TEST_MARKER_NEG";
|
|
55
|
+
const markerValue = `marker-${Date.now()}`;
|
|
56
|
+
|
|
57
|
+
const originalValue = process.env[markerKey];
|
|
58
|
+
process.env[markerKey] = markerValue;
|
|
59
|
+
try {
|
|
60
|
+
const proc = Bun.spawn(
|
|
61
|
+
["bun", "-e", `process.stdout.write(process.env.${markerKey} ?? "MISSING")`],
|
|
62
|
+
{
|
|
63
|
+
stdout: "pipe",
|
|
64
|
+
stderr: "pipe",
|
|
65
|
+
// intentionally NO env: process.env
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
const stdout = await new Response(proc.stdout).text();
|
|
69
|
+
const exitCode = await proc.exited;
|
|
70
|
+
|
|
71
|
+
expect(exitCode).toBe(0);
|
|
72
|
+
expect(stdout).toBe("MISSING");
|
|
73
|
+
} finally {
|
|
74
|
+
if (originalValue === undefined) delete process.env[markerKey];
|
|
75
|
+
else process.env[markerKey] = originalValue;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/admin-vaults.ts
CHANGED
|
@@ -206,7 +206,12 @@ function buildEntry(
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
async function defaultRunCommand(cmd: readonly string[]): Promise<RunResult> {
|
|
209
|
-
|
|
209
|
+
// Inherit env so the child sees PATH, HOME, BUN_INSTALL, etc. Bun.spawn
|
|
210
|
+
// defaults to empty env — see api-modules-ops.ts:defaultRun for the rationale.
|
|
211
|
+
const proc = Bun.spawn([...cmd], {
|
|
212
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
213
|
+
env: process.env,
|
|
214
|
+
});
|
|
210
215
|
// Drain both pipes in parallel — leaving stderr unread can deadlock long
|
|
211
216
|
// installs once the OS pipe buffer fills (#97). Captured stderr is folded
|
|
212
217
|
// into the orchestration error message on non-zero exit.
|
package/src/api-modules-ops.ts
CHANGED
|
@@ -322,7 +322,14 @@ async function resolveSpawnSpec(
|
|
|
322
322
|
}
|
|
323
323
|
|
|
324
324
|
function defaultRun(cmd: readonly string[]): Promise<number> {
|
|
325
|
-
|
|
325
|
+
// Inherit env so child `bun add` sees TMPDIR, BUN_INSTALL, PARACHUTE_*,
|
|
326
|
+
// etc. set by the Dockerfile / Render env. Bun.spawn defaults to empty
|
|
327
|
+
// env — without this, bun-add fails with cross-mount rename errors on
|
|
328
|
+
// Render (where TMPDIR points at the persistent disk). See hub#349.
|
|
329
|
+
const proc = Bun.spawn([...cmd], {
|
|
330
|
+
stdio: ["ignore", "inherit", "inherit"],
|
|
331
|
+
env: process.env,
|
|
332
|
+
});
|
|
326
333
|
return proc.exited;
|
|
327
334
|
}
|
|
328
335
|
|
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
|
@@ -167,6 +167,7 @@ import {
|
|
|
167
167
|
} from "./oauth-handlers.ts";
|
|
168
168
|
import { buildHubBoundOrigins } from "./origin-check.ts";
|
|
169
169
|
import { clearPid, writePid } from "./process-state.ts";
|
|
170
|
+
import { isHttpsRequest } from "./request-protocol.ts";
|
|
170
171
|
import {
|
|
171
172
|
FIRST_PARTY_FALLBACKS,
|
|
172
173
|
KNOWN_MODULES,
|
|
@@ -944,7 +945,24 @@ export function resolveIssuer(
|
|
|
944
945
|
if (stored) return stored;
|
|
945
946
|
}
|
|
946
947
|
if (configuredIssuer) return configuredIssuer;
|
|
947
|
-
|
|
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;
|
|
948
966
|
}
|
|
949
967
|
|
|
950
968
|
/**
|
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(),
|