@openparachute/hub 0.7.3-rc.8 → 0.7.3
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__/cli.test.ts +20 -0
- package/src/__tests__/hub-command.test.ts +433 -0
- package/src/__tests__/hub-origins-env-set.test.ts +273 -0
- package/src/__tests__/hub-server.test.ts +240 -0
- package/src/__tests__/init.test.ts +148 -0
- package/src/__tests__/serve-boot.test.ts +45 -0
- package/src/__tests__/serve.test.ts +67 -1
- package/src/__tests__/setup-wizard.test.ts +121 -1
- package/src/__tests__/vault-hub-origin-env.test.ts +38 -4
- package/src/api-modules-ops.ts +12 -0
- package/src/cli.ts +35 -1
- package/src/commands/hub.ts +409 -0
- package/src/commands/init.ts +63 -0
- package/src/commands/serve-boot.ts +16 -2
- package/src/commands/serve.ts +39 -3
- package/src/help.ts +8 -0
- package/src/hub-origin.ts +64 -0
- package/src/hub-server.ts +46 -0
- package/src/jwt-sign.ts +14 -3
- package/src/setup-wizard.ts +20 -0
- package/src/vault-hub-origin-env.ts +109 -16
package/src/hub-origin.ts
CHANGED
|
@@ -14,6 +14,70 @@
|
|
|
14
14
|
|
|
15
15
|
export const HUB_ORIGIN_ENV = "PARACHUTE_HUB_ORIGIN";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* The env var carrying the SET of origins the hub legitimately answers on,
|
|
19
|
+
* comma-separated (multi-origin iss-set, onboarding-streamline 2026-06-25).
|
|
20
|
+
*
|
|
21
|
+
* Supervised resource servers (vault, scribe) read this in addition to the
|
|
22
|
+
* single canonical `PARACHUTE_HUB_ORIGIN` and widen their accepted-`iss` check
|
|
23
|
+
* from one string to this set — so a token minted under one URL of a
|
|
24
|
+
* multi-URL box (loopback ∪ `<ip>.sslip.io` ∪ a custom domain behind Caddy)
|
|
25
|
+
* validates when the resource is reached via another URL of the SAME box. The
|
|
26
|
+
* signing key is stable + origin-independent, so only the `iss` string varies.
|
|
27
|
+
*
|
|
28
|
+
* SECURITY INVARIANT: the value MUST be the hub's `buildHubBoundOrigins`
|
|
29
|
+
* output — configured issuer ∪ loopback aliases ∪ expose-state public origin ∪
|
|
30
|
+
* platform origin — published BY THE HUB. It must NEVER contain an unvalidated
|
|
31
|
+
* request `Host` / `X-Forwarded-Host`. Accepting `iss ∈ this-set` is safe
|
|
32
|
+
* ONLY because the resource server's JWKS signature verify runs first and
|
|
33
|
+
* proves the hub minted the token.
|
|
34
|
+
*
|
|
35
|
+
* `PARACHUTE_HUB_ORIGIN` (the single canonical origin) is ALWAYS written
|
|
36
|
+
* alongside this var for back-compat: a resource server on an older
|
|
37
|
+
* scope-guard that doesn't read `PARACHUTE_HUB_ORIGINS` keeps its existing
|
|
38
|
+
* single-origin behavior.
|
|
39
|
+
*/
|
|
40
|
+
export const HUB_ORIGINS_ENV = "PARACHUTE_HUB_ORIGINS";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Serialize an origin SET into the comma-separated `PARACHUTE_HUB_ORIGINS`
|
|
44
|
+
* wire form. Dedupes, drops empties, preserves first-seen order. Returns
|
|
45
|
+
* `undefined` when nothing survives (caller skips setting the env var so a
|
|
46
|
+
* resource server falls back to single-origin behavior). Pair with
|
|
47
|
+
* `buildHubBoundOrigins` to produce `origins`.
|
|
48
|
+
*/
|
|
49
|
+
export function serializeHubOrigins(origins: readonly string[]): string | undefined {
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
for (const raw of origins) {
|
|
52
|
+
if (typeof raw !== "string") continue;
|
|
53
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
54
|
+
if (trimmed.length > 0) seen.add(trimmed);
|
|
55
|
+
}
|
|
56
|
+
if (seen.size === 0) return undefined;
|
|
57
|
+
return Array.from(seen).join(",");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse the comma-separated `PARACHUTE_HUB_ORIGINS` wire form back into an
|
|
62
|
+
* origin array. Tolerant of surrounding whitespace + trailing slashes + empty
|
|
63
|
+
* segments. The resource-server inverse of `serializeHubOrigins`; lives here
|
|
64
|
+
* (rather than only in the consumer adapters) so the hub's own injection tests
|
|
65
|
+
* can round-trip against the canonical parser. Returns `[]` for an
|
|
66
|
+
* absent/empty/garbage value.
|
|
67
|
+
*/
|
|
68
|
+
export function parseHubOrigins(raw: string | undefined): string[] {
|
|
69
|
+
if (!raw) return [];
|
|
70
|
+
const out: string[] = [];
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
for (const seg of raw.split(",")) {
|
|
73
|
+
const trimmed = seg.trim().replace(/\/+$/, "");
|
|
74
|
+
if (trimmed.length === 0 || seen.has(trimmed)) continue;
|
|
75
|
+
seen.add(trimmed);
|
|
76
|
+
out.push(trimmed);
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
17
81
|
export interface DeriveHubOriginOpts {
|
|
18
82
|
/** Explicit user override (e.g., `--hub-origin`). Wins over everything else. */
|
|
19
83
|
override?: string;
|
package/src/hub-server.ts
CHANGED
|
@@ -578,6 +578,16 @@ export type RequestLayer = "loopback" | "tailnet" | "public";
|
|
|
578
578
|
* the cloak fires. When `peerAddr` is unknown (null/undefined — no Server
|
|
579
579
|
* threaded, e.g. a unit test calling `layerOf(req)` directly), we fail CLOSED
|
|
580
580
|
* to `public` rather than open to `loopback`.
|
|
581
|
+
*
|
|
582
|
+
* Caddy/nginx-direct (hub#704): a SAME-BOX reverse proxy dials loopback (peer
|
|
583
|
+
* is 127.0.0.1) but, unlike cloudflared/tailscale, sets NO cf/tailscale header
|
|
584
|
+
* — so a header-only-or-peer-only classifier would call every public request
|
|
585
|
+
* through it "loopback" (most-trusted). The discriminator is the standard
|
|
586
|
+
* reverse-proxy forwarding headers (X-Forwarded-For / X-Forwarded-Host /
|
|
587
|
+
* Forwarded): a loopback peer that carries one is a proxied PUBLIC request →
|
|
588
|
+
* `public`; a header-less loopback peer (direct on-box caller — CLI, probes,
|
|
589
|
+
* the init bootstrap-token loopback probe) stays `loopback`. See the inline
|
|
590
|
+
* comment in the function for the full rationale + spoof analysis.
|
|
581
591
|
*/
|
|
582
592
|
export function layerOf(req: Request, peerAddr?: string | null): RequestLayer {
|
|
583
593
|
const h = req.headers;
|
|
@@ -590,6 +600,42 @@ export function layerOf(req: Request, peerAddr?: string | null): RequestLayer {
|
|
|
590
600
|
// value to compare against, hence the presence-check above.
|
|
591
601
|
if (h.get("tailscale-funnel-request") === "?1") return "public";
|
|
592
602
|
if (h.get("tailscale-user-login") !== null) return "tailnet";
|
|
603
|
+
// Caddy/nginx-direct deploy (hub#704): a same-box reverse proxy terminates
|
|
604
|
+
// TLS and `reverse_proxy 127.0.0.1:1939` — so it dials loopback (peer is
|
|
605
|
+
// 127.0.0.1) and, unlike cloudflared/tailscale, stamps NO cf/tailscale
|
|
606
|
+
// header. Without this branch every PUBLIC request through such a proxy
|
|
607
|
+
// would classify "loopback" (the MOST-trusted layer): the GET /admin/setup
|
|
608
|
+
// bootstrap-token JSON probe would hand the token to any public visitor, and
|
|
609
|
+
// the publicExposure:"loopback" 404-cloak would stop hiding loopback-only
|
|
610
|
+
// services/vaults from the network.
|
|
611
|
+
//
|
|
612
|
+
// The discriminator is the standard reverse-proxy forwarding headers. A
|
|
613
|
+
// same-box proxy carrying a PUBLIC request sets X-Forwarded-For /
|
|
614
|
+
// X-Forwarded-Host / Forwarded; a direct on-box caller (the CLI, health
|
|
615
|
+
// probes, the init bootstrap-token loopback probe `curl 127.0.0.1/admin/setup`,
|
|
616
|
+
// the hub's own loopback self-requests) sets none of them — the hub never
|
|
617
|
+
// injects X-Forwarded-* on the INBOUND request it classifies (it only stamps
|
|
618
|
+
// X-Forwarded-Host/Proto on OUTBOUND proxy requests to modules). So a
|
|
619
|
+
// loopback peer that ALSO carries a forwarding header is a proxied public
|
|
620
|
+
// request → "public"; a header-less loopback peer stays "loopback".
|
|
621
|
+
//
|
|
622
|
+
// No spoof vector: a NON-loopback peer is already "public" regardless of
|
|
623
|
+
// headers (the branch below), so adding these headers can only DOWNGRADE a
|
|
624
|
+
// loopback caller (the on-box operator hurting only their own request) —
|
|
625
|
+
// never upgrade a network peer to "loopback".
|
|
626
|
+
//
|
|
627
|
+
// Presence check (`!== null`), NOT a trim: an empty/whitespace forwarding
|
|
628
|
+
// header still means "a proxy is in front" → err to public. Downgrading on
|
|
629
|
+
// ambiguity is the safe direction for a trust classifier; a future ".trim()
|
|
630
|
+
// tidy-up" that let an empty XFF fall back to loopback would re-open the leak.
|
|
631
|
+
if (
|
|
632
|
+
isLoopbackPeer(peerAddr) &&
|
|
633
|
+
(h.get("x-forwarded-for") !== null ||
|
|
634
|
+
h.get("x-forwarded-host") !== null ||
|
|
635
|
+
h.get("forwarded") !== null)
|
|
636
|
+
) {
|
|
637
|
+
return "public";
|
|
638
|
+
}
|
|
593
639
|
// No proxy headers — classify by peer address, failing closed when unknown.
|
|
594
640
|
return isLoopbackPeer(peerAddr) ? "loopback" : "public";
|
|
595
641
|
}
|
package/src/jwt-sign.ts
CHANGED
|
@@ -55,9 +55,20 @@ export interface SignAccessTokenOpts {
|
|
|
55
55
|
clientId: string;
|
|
56
56
|
/**
|
|
57
57
|
* Hub origin — sets the `iss` claim. Required: every consumer (vault,
|
|
58
|
-
* scribe,
|
|
59
|
-
*
|
|
60
|
-
*
|
|
58
|
+
* scribe, agent) validates `iss`, and a missing claim is rejected. Callers
|
|
59
|
+
* derive this via `deriveHubOrigin()` or thread it from `OAuthDeps.issuer`
|
|
60
|
+
* (the per-request origin from `resolveIssuer`).
|
|
61
|
+
*
|
|
62
|
+
* Multi-origin iss-set (onboarding-streamline 2026-06-25): consumers no
|
|
63
|
+
* longer pin `iss` to a SINGLE `PARACHUTE_HUB_ORIGIN`. The hub publishes the
|
|
64
|
+
* SET of origins it legitimately answers on via `PARACHUTE_HUB_ORIGINS`
|
|
65
|
+
* (`buildHubBoundOrigins` — issuer ∪ loopback ∪ expose-state ∪ platform),
|
|
66
|
+
* and a scope-guard ≥0.5.0 consumer accepts `iss` ∈ that set. So a token
|
|
67
|
+
* minted here with one URL of a multi-URL box validates when the resource is
|
|
68
|
+
* reached via another URL of the SAME box. Mint stays per-request as-is: the
|
|
69
|
+
* canonical configured origin (the intended Caddy/zero-SSH deploy) is minted
|
|
70
|
+
* on every request AND is a member of the published set, so mint and validate
|
|
71
|
+
* already align — no clamp needed here.
|
|
61
72
|
*/
|
|
62
73
|
issuer: string;
|
|
63
74
|
/**
|
package/src/setup-wizard.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
SETUP_EXPOSE_MODES,
|
|
61
61
|
type SetupExposeMode,
|
|
62
62
|
deleteSetting,
|
|
63
|
+
getHubOrigin,
|
|
63
64
|
getSetting,
|
|
64
65
|
isSetupExposeMode,
|
|
65
66
|
openFirstClientAutoApproveWindow,
|
|
@@ -89,6 +90,7 @@ import {
|
|
|
89
90
|
} from "./sessions.ts";
|
|
90
91
|
import type { Supervisor } from "./supervisor.ts";
|
|
91
92
|
import { createUser, userCount } from "./users.ts";
|
|
93
|
+
import { sanitizePublicOrigin } from "./vault-hub-origin-env.ts";
|
|
92
94
|
import { DEFAULT_VAULT_NAME, validateVaultName } from "./vault-name.ts";
|
|
93
95
|
|
|
94
96
|
// --- shared chrome --------------------------------------------------------
|
|
@@ -352,6 +354,24 @@ export function deriveWizardState(deps: {
|
|
|
352
354
|
setSetting(deps.db, "setup_expose_mode", seeded);
|
|
353
355
|
}
|
|
354
356
|
}
|
|
357
|
+
// hub Caddy-direct case: a reverse-proxy box (`parachute init --hub-origin
|
|
358
|
+
// https://<host>`, or `parachute hub set-origin`) persists a real public
|
|
359
|
+
// origin to `hub_settings.hub_origin` (also honored from PARACHUTE_HUB_ORIGIN).
|
|
360
|
+
// That origin already answers "how is this hub reached?" — Caddy/Let's-Encrypt
|
|
361
|
+
// fronts the loopback hub, no `parachute expose` and no Render/Fly env var.
|
|
362
|
+
// Auto-seed `setup_expose_mode = "public"` so the wizard skips the expose step
|
|
363
|
+
// exactly as the Render/Fly + live-tailscale branches do. `sanitizePublicOrigin`
|
|
364
|
+
// (the SAME guard `resolveIssuer`/`exposeIssuerOrigin` use) requires an absolute
|
|
365
|
+
// http(s) URL and REJECTS loopback/0.0.0.0 — so a laptop/loopback/unset origin
|
|
366
|
+
// returns undefined and the expose step still renders (the operator chooses).
|
|
367
|
+
if (getSetting(deps.db, "setup_expose_mode") === undefined) {
|
|
368
|
+
const configured =
|
|
369
|
+
sanitizePublicOrigin(getHubOrigin(deps.db) ?? undefined) ??
|
|
370
|
+
sanitizePublicOrigin(deps.env?.PARACHUTE_HUB_ORIGIN ?? process.env.PARACHUTE_HUB_ORIGIN);
|
|
371
|
+
if (configured !== undefined) {
|
|
372
|
+
setSetting(deps.db, "setup_expose_mode", "public");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
355
375
|
const hasExposeMode = getSetting(deps.db, "setup_expose_mode") !== undefined;
|
|
356
376
|
let step: WizardStep;
|
|
357
377
|
// Note: `"account"` is a visual-only step in the progress header —
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { parseEnvFile, removeEnvLine, upsertEnvLine, writeEnvFile } from "./env-file.ts";
|
|
31
31
|
import { EXPOSE_STATE_PATH, readExposeState } from "./expose-state.ts";
|
|
32
|
-
import {
|
|
32
|
+
import { readHubPort } from "./hub-control.ts";
|
|
33
|
+
import { HUB_ORIGINS_ENV, HUB_ORIGIN_ENV, serializeHubOrigins } from "./hub-origin.ts";
|
|
34
|
+
import { HUB_UNIT_DEFAULT_PORT } from "./hub-unit.ts";
|
|
35
|
+
import { buildHubBoundOrigins } from "./origin-check.ts";
|
|
33
36
|
|
|
34
37
|
/**
|
|
35
38
|
* Loopback origins (`http://127.0.0.1:<port>`, `localhost`, `[::1]`) are the
|
|
@@ -91,11 +94,80 @@ function vaultEnvPath(configDir: string): string {
|
|
|
91
94
|
return join(configDir, "vault", ".env");
|
|
92
95
|
}
|
|
93
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Compose `https://${FLY_APP_NAME}.fly.dev` when FLY_APP_NAME is a plausible
|
|
99
|
+
* Fly app slug (no slashes — Fly slugs don't contain them). Mirrors the
|
|
100
|
+
* private helper in operator-token.ts / hub-server.ts; kept local so the
|
|
101
|
+
* origin-SET assembly here doesn't reach across modules for a 3-line guard.
|
|
102
|
+
*/
|
|
103
|
+
function flyDefaultOriginFromEnv(env: NodeJS.ProcessEnv): string | undefined {
|
|
104
|
+
const app = env.FLY_APP_NAME;
|
|
105
|
+
if (typeof app !== "string" || app.length === 0 || app.includes("/")) return undefined;
|
|
106
|
+
return `https://${app}.fly.dev`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Assemble the comma-separated `PARACHUTE_HUB_ORIGINS` value the hub injects
|
|
111
|
+
* into supervised resource servers (multi-origin iss-set). The SET is the
|
|
112
|
+
* hub's own legitimate origins — the env-injection sibling of the operator
|
|
113
|
+
* token's `buildKnownIssuersForOperatorToken` and the per-request
|
|
114
|
+
* `buildHubBoundOrigins` call in hub-server.ts. Inputs:
|
|
115
|
+
*
|
|
116
|
+
* - `issuer` — the canonical hub origin the child also receives as the
|
|
117
|
+
* single `PARACHUTE_HUB_ORIGIN` (the seed; always included).
|
|
118
|
+
* - loopback aliases — `http://127.0.0.1:<port>` ∪ `http://localhost:<port>`
|
|
119
|
+
* for the hub's port (`readHubPort(configDir) ?? HUB_UNIT_DEFAULT_PORT`).
|
|
120
|
+
* - the expose-state public origin — `expose-state.json`'s `hubOrigin`.
|
|
121
|
+
* - the platform/env public origin — `RENDER_EXTERNAL_URL` ∪ the composed
|
|
122
|
+
* Fly default (container deploys where the public origin comes from the
|
|
123
|
+
* platform, not expose-state).
|
|
124
|
+
*
|
|
125
|
+
* SECURITY INVARIANT: every input is hub/operator-controlled config or
|
|
126
|
+
* on-disk state — NEVER an unvalidated request `Host` / `X-Forwarded-Host`.
|
|
127
|
+
* The accepted-`iss` widening this enables is safe only because the resource
|
|
128
|
+
* server verifies the JWKS signature first; this set is the belt-and-
|
|
129
|
+
* suspenders allowlist layered on top.
|
|
130
|
+
*
|
|
131
|
+
* Returns `undefined` when the seed issuer is absent AND nothing else resolves
|
|
132
|
+
* (caller skips the env var so the child keeps single-origin behavior).
|
|
133
|
+
*/
|
|
134
|
+
export function buildHubOriginsEnvValue(
|
|
135
|
+
configDir: string,
|
|
136
|
+
issuer: string | undefined,
|
|
137
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
138
|
+
exposeStatePath: string = EXPOSE_STATE_PATH,
|
|
139
|
+
): string | undefined {
|
|
140
|
+
const loopbackPort = readHubPort(configDir) ?? HUB_UNIT_DEFAULT_PORT;
|
|
141
|
+
let exposeHubOrigin: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
exposeHubOrigin = readExposeState(exposeStatePath)?.hubOrigin;
|
|
144
|
+
} catch {
|
|
145
|
+
// A malformed expose-state.json must never block a module spawn — the seed
|
|
146
|
+
// issuer + loopback aliases already cover legitimate access.
|
|
147
|
+
exposeHubOrigin = undefined;
|
|
148
|
+
}
|
|
149
|
+
const platformOrigin = env.RENDER_EXTERNAL_URL ?? flyDefaultOriginFromEnv(env);
|
|
150
|
+
const origins = buildHubBoundOrigins({
|
|
151
|
+
// buildHubBoundOrigins requires `issuer`; pass "" when absent (it's dropped
|
|
152
|
+
// by the URL parse) so the loopback aliases still seed the set.
|
|
153
|
+
issuer: issuer ?? "",
|
|
154
|
+
loopbackPort,
|
|
155
|
+
...(exposeHubOrigin !== undefined ? { exposeHubOrigin } : {}),
|
|
156
|
+
...(platformOrigin !== undefined ? { platformOrigin } : {}),
|
|
157
|
+
});
|
|
158
|
+
return serializeHubOrigins(origins);
|
|
159
|
+
}
|
|
160
|
+
|
|
94
161
|
/**
|
|
95
162
|
* Upsert `PARACHUTE_HUB_ORIGIN=<origin>` into `vault/.env` when `origin` is a
|
|
96
|
-
* non-loopback public origin
|
|
97
|
-
*
|
|
98
|
-
*
|
|
163
|
+
* non-loopback public origin, AND the `PARACHUTE_HUB_ORIGINS` set (the
|
|
164
|
+
* multi-origin iss-set: origin ∪ loopback aliases ∪ expose-state ∪ platform)
|
|
165
|
+
* so the daemon-boot path validates `iss` against every URL the box answers on
|
|
166
|
+
* (a Caddy-fronted box reached via loopback + sslip.io + a custom domain at
|
|
167
|
+
* once). The set is assembled from hub-controlled inputs only (see
|
|
168
|
+
* `buildHubOriginsEnvValue`'s security invariant). Idempotent — skips the
|
|
169
|
+
* write (and the log) when BOTH values are already current so repeated
|
|
170
|
+
* `start`s don't churn the file. Returns true iff the file was written.
|
|
99
171
|
*/
|
|
100
172
|
export function persistVaultHubOrigin(
|
|
101
173
|
configDir: string,
|
|
@@ -105,26 +177,47 @@ export function persistVaultHubOrigin(
|
|
|
105
177
|
if (isLoopbackOrigin(origin)) return false;
|
|
106
178
|
const path = vaultEnvPath(configDir);
|
|
107
179
|
const parsed = parseEnvFile(path);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
180
|
+
const originsValue = buildHubOriginsEnvValue(configDir, origin);
|
|
181
|
+
const originCurrent = parsed.values[HUB_ORIGIN_ENV] === origin;
|
|
182
|
+
const originsCurrent =
|
|
183
|
+
originsValue === undefined || parsed.values[HUB_ORIGINS_ENV] === originsValue;
|
|
184
|
+
if (originCurrent && originsCurrent) return false;
|
|
185
|
+
let lines = parsed.lines;
|
|
186
|
+
if (!originCurrent) lines = upsertEnvLine(lines, HUB_ORIGIN_ENV, origin);
|
|
187
|
+
if (!originsCurrent && originsValue !== undefined) {
|
|
188
|
+
lines = upsertEnvLine(lines, HUB_ORIGINS_ENV, originsValue);
|
|
189
|
+
}
|
|
190
|
+
writeEnvFile(path, lines);
|
|
191
|
+
if (!originCurrent) {
|
|
192
|
+
log(` persisted ${HUB_ORIGIN_ENV}=${origin} to ${path} (survives daemon restart)`);
|
|
193
|
+
}
|
|
194
|
+
if (!originsCurrent && originsValue !== undefined) {
|
|
195
|
+
log(` persisted ${HUB_ORIGINS_ENV}=${originsValue} to ${path} (multi-origin iss-set)`);
|
|
196
|
+
}
|
|
111
197
|
return true;
|
|
112
198
|
}
|
|
113
199
|
|
|
114
200
|
/**
|
|
115
|
-
* Drop a previously-persisted `PARACHUTE_HUB_ORIGIN`
|
|
116
|
-
* on `expose … off`: once exposure is torn down, a
|
|
117
|
-
* with a loopback `iss`, so a stale public origin
|
|
118
|
-
* cause the mismatch. Removing the
|
|
119
|
-
* (`getHubOrigin`), which matches what the local
|
|
120
|
-
* false) when
|
|
201
|
+
* Drop a previously-persisted `PARACHUTE_HUB_ORIGIN` (and `PARACHUTE_HUB_ORIGINS`)
|
|
202
|
+
* from `vault/.env`. Called on `expose … off`: once exposure is torn down, a
|
|
203
|
+
* local-only hub mints tokens with a loopback `iss`, so a stale public origin
|
|
204
|
+
* left in `.env` would itself cause the mismatch. Removing the lines reverts
|
|
205
|
+
* vault to its loopback default (`getHubOrigin`), which matches what the local
|
|
206
|
+
* hub now stamps. No-op (returns false) when neither key is present. Returns
|
|
207
|
+
* true iff the file was rewritten.
|
|
121
208
|
*/
|
|
122
209
|
export function clearVaultHubOrigin(configDir: string, log: (line: string) => void): boolean {
|
|
123
210
|
const path = vaultEnvPath(configDir);
|
|
124
211
|
const parsed = parseEnvFile(path);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
212
|
+
const hadOrigin = parsed.values[HUB_ORIGIN_ENV] !== undefined;
|
|
213
|
+
const hadOrigins = parsed.values[HUB_ORIGINS_ENV] !== undefined;
|
|
214
|
+
if (!hadOrigin && !hadOrigins) return false;
|
|
215
|
+
let lines = parsed.lines;
|
|
216
|
+
if (hadOrigin) lines = removeEnvLine(lines, HUB_ORIGIN_ENV);
|
|
217
|
+
if (hadOrigins) lines = removeEnvLine(lines, HUB_ORIGINS_ENV);
|
|
218
|
+
writeEnvFile(path, lines);
|
|
219
|
+
if (hadOrigin) log(` cleared ${HUB_ORIGIN_ENV} from ${path} (exposure torn down)`);
|
|
220
|
+
if (hadOrigins) log(` cleared ${HUB_ORIGINS_ENV} from ${path} (exposure torn down)`);
|
|
128
221
|
return true;
|
|
129
222
|
}
|
|
130
223
|
|