@openparachute/hub 0.3.0-rc.1 → 0.5.1
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 +19 -17
- package/package.json +15 -4
- package/src/__tests__/admin-auth.test.ts +197 -0
- package/src/__tests__/admin-config.test.ts +281 -0
- package/src/__tests__/admin-grants.test.ts +271 -0
- package/src/__tests__/admin-handlers.test.ts +530 -0
- package/src/__tests__/admin-host-admin-token.test.ts +115 -0
- package/src/__tests__/admin-vault-admin-token.test.ts +190 -0
- package/src/__tests__/admin-vaults.test.ts +615 -0
- package/src/__tests__/auth-codes.test.ts +253 -0
- package/src/__tests__/auth.test.ts +1063 -17
- package/src/__tests__/cli.test.ts +50 -0
- package/src/__tests__/clients.test.ts +264 -0
- package/src/__tests__/cloudflare-state.test.ts +167 -7
- package/src/__tests__/csrf.test.ts +117 -0
- package/src/__tests__/expose-cloudflare.test.ts +232 -37
- package/src/__tests__/expose-off-auto.test.ts +15 -9
- package/src/__tests__/expose-public-auto.test.ts +153 -0
- package/src/__tests__/expose.test.ts +216 -24
- package/src/__tests__/grants.test.ts +164 -0
- package/src/__tests__/hub-db.test.ts +153 -0
- package/src/__tests__/hub-server.test.ts +984 -26
- package/src/__tests__/hub.test.ts +56 -49
- package/src/__tests__/install.test.ts +327 -3
- package/src/__tests__/jwks.test.ts +37 -0
- package/src/__tests__/jwt-sign.test.ts +361 -0
- package/src/__tests__/lifecycle.test.ts +616 -5
- package/src/__tests__/module-manifest.test.ts +183 -0
- package/src/__tests__/oauth-handlers.test.ts +3112 -0
- package/src/__tests__/oauth-ui.test.ts +253 -0
- package/src/__tests__/operator-token.test.ts +140 -0
- package/src/__tests__/providers-detect.test.ts +158 -0
- package/src/__tests__/scope-explanations.test.ts +108 -0
- package/src/__tests__/scope-registry.test.ts +220 -0
- package/src/__tests__/services-manifest.test.ts +137 -1
- package/src/__tests__/sessions.test.ts +116 -0
- package/src/__tests__/setup.test.ts +361 -0
- package/src/__tests__/signing-keys.test.ts +153 -0
- package/src/__tests__/upgrade.test.ts +541 -0
- package/src/__tests__/users.test.ts +154 -0
- package/src/__tests__/well-known.test.ts +127 -10
- package/src/admin-auth.ts +126 -0
- package/src/admin-config-ui.ts +534 -0
- package/src/admin-config.ts +226 -0
- package/src/admin-grants.ts +160 -0
- package/src/admin-handlers.ts +365 -0
- package/src/admin-host-admin-token.ts +83 -0
- package/src/admin-vault-admin-token.ts +98 -0
- package/src/admin-vaults.ts +359 -0
- package/src/auth-codes.ts +189 -0
- package/src/cli.ts +202 -25
- package/src/clients.ts +210 -0
- package/src/cloudflare/config.ts +25 -6
- package/src/cloudflare/state.ts +108 -28
- package/src/commands/auth.ts +851 -19
- package/src/commands/expose-cloudflare.ts +85 -45
- package/src/commands/expose-interactive.ts +20 -44
- package/src/commands/expose-off-auto.ts +27 -11
- package/src/commands/expose-public-auto.ts +179 -0
- package/src/commands/expose.ts +63 -32
- package/src/commands/install.ts +337 -48
- package/src/commands/lifecycle.ts +269 -38
- package/src/commands/setup.ts +366 -0
- package/src/commands/status.ts +4 -1
- package/src/commands/upgrade.ts +429 -0
- package/src/csrf.ts +101 -0
- package/src/grants.ts +142 -0
- package/src/help.ts +133 -19
- package/src/hub-control.ts +12 -0
- package/src/hub-db.ts +164 -0
- package/src/hub-server.ts +643 -22
- package/src/hub.ts +97 -390
- package/src/jwks.ts +41 -0
- package/src/jwt-audience.ts +40 -0
- package/src/jwt-sign.ts +275 -0
- package/src/module-manifest.ts +435 -0
- package/src/oauth-handlers.ts +1175 -0
- package/src/oauth-ui.ts +582 -0
- package/src/operator-token.ts +129 -0
- package/src/providers/detect.ts +97 -0
- package/src/scope-explanations.ts +137 -0
- package/src/scope-registry.ts +158 -0
- package/src/service-spec.ts +270 -97
- package/src/services-manifest.ts +57 -1
- package/src/sessions.ts +115 -0
- package/src/signing-keys.ts +120 -0
- package/src/users.ts +144 -0
- package/src/well-known.ts +62 -26
- package/web/ui/dist/assets/index-BKzPDdB0.js +60 -0
- package/web/ui/dist/assets/index-Dyk6g7vT.css +1 -0
- package/web/ui/dist/index.html +14 -0
package/src/commands/expose.ts
CHANGED
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
buildWellKnown,
|
|
32
32
|
isVaultEntry,
|
|
33
33
|
shortName,
|
|
34
|
-
vaultInstanceName,
|
|
35
34
|
writeWellKnownFile,
|
|
36
35
|
} from "../well-known.ts";
|
|
37
36
|
import { restart } from "./lifecycle.ts";
|
|
@@ -59,7 +58,7 @@ export interface ExposeOpts {
|
|
|
59
58
|
statePath?: string;
|
|
60
59
|
wellKnownPath?: string;
|
|
61
60
|
hubPath?: string;
|
|
62
|
-
/** Directory holding hub.html
|
|
61
|
+
/** Directory holding hub.html (passed to the hub server). */
|
|
63
62
|
wellKnownDir?: string;
|
|
64
63
|
configDir?: string;
|
|
65
64
|
port?: number;
|
|
@@ -105,11 +104,14 @@ export interface ExposeOpts {
|
|
|
105
104
|
const HUB_DEPENDENT_SHORTS = ["vault"] as const;
|
|
106
105
|
|
|
107
106
|
/**
|
|
108
|
-
* OAuth paths the hub
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
107
|
+
* OAuth paths the hub serves natively. The mount path is what clients see;
|
|
108
|
+
* the target is the hub's loopback origin (where `hub-server.ts` is
|
|
109
|
+
* listening). tailscale strips the mount before forwarding, so the target
|
|
110
|
+
* must include the same path so the hub-server router sees the full URL.
|
|
111
|
+
*
|
|
112
|
+
* Pre-cli#58 (PR (c)) these were proxied to vault's `/vault/<name>/oauth/*`
|
|
113
|
+
* handlers; after PR (c) the hub IS the OAuth IdP and vault validates
|
|
114
|
+
* hub-issued JWTs (vault#169).
|
|
113
115
|
*/
|
|
114
116
|
const OAUTH_PATHS = [
|
|
115
117
|
"/.well-known/oauth-authorization-server",
|
|
@@ -119,12 +121,17 @@ const OAUTH_PATHS = [
|
|
|
119
121
|
] as const;
|
|
120
122
|
|
|
121
123
|
/**
|
|
122
|
-
* Single
|
|
123
|
-
*
|
|
124
|
+
* Single tailscale serve mount that catches every `/vault/<name>/...` request
|
|
125
|
+
* and routes it through the hub. The hub then reads services.json on each
|
|
126
|
+
* request to pick the right vault backend (#144). Consolidating to one mount
|
|
127
|
+
* means `parachute vault create techne` is reachable on the tailnet without
|
|
128
|
+
* re-running `parachute expose` — only the hub-internal lookup needs to know
|
|
129
|
+
* about the new path.
|
|
130
|
+
*
|
|
131
|
+
* Trailing slash distinguishes the mount from `/vaults` (the create-vault
|
|
132
|
+
* POST endpoint, an exact-match on hub).
|
|
124
133
|
*/
|
|
125
|
-
|
|
126
|
-
return services.find((s) => isVaultEntry(s));
|
|
127
|
-
}
|
|
134
|
+
const VAULT_MOUNT = "/vault/";
|
|
128
135
|
|
|
129
136
|
/**
|
|
130
137
|
* Remap legacy `paths: ["/"]` entries to `/<shortname>` so they don't collide
|
|
@@ -212,7 +219,15 @@ function planEntries(services: readonly ServiceEntry[], hubPort: number): ServeE
|
|
|
212
219
|
target: serviceProxyTarget(hubPort, HUB_MOUNT),
|
|
213
220
|
service: "hub",
|
|
214
221
|
});
|
|
222
|
+
let anyVault = false;
|
|
215
223
|
for (const s of services) {
|
|
224
|
+
if (isVaultEntry(s)) {
|
|
225
|
+
// Vault paths route through the single `/vault/` → hub mount below so
|
|
226
|
+
// `parachute vault create <name>` is reachable on the tailnet without
|
|
227
|
+
// a re-expose. Hub does the per-request services.json lookup (#144).
|
|
228
|
+
anyVault = true;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
216
231
|
const mount = s.paths[0] ?? `/${shortName(s.name)}`;
|
|
217
232
|
entries.push({
|
|
218
233
|
kind: "proxy",
|
|
@@ -221,6 +236,14 @@ function planEntries(services: readonly ServiceEntry[], hubPort: number): ServeE
|
|
|
221
236
|
service: s.name,
|
|
222
237
|
});
|
|
223
238
|
}
|
|
239
|
+
if (anyVault) {
|
|
240
|
+
entries.push({
|
|
241
|
+
kind: "proxy",
|
|
242
|
+
mount: VAULT_MOUNT,
|
|
243
|
+
target: serviceProxyTarget(hubPort, VAULT_MOUNT),
|
|
244
|
+
service: "vault",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
224
247
|
entries.push({
|
|
225
248
|
kind: "proxy",
|
|
226
249
|
mount: WELL_KNOWN_MOUNT,
|
|
@@ -228,21 +251,17 @@ function planEntries(services: readonly ServiceEntry[], hubPort: number): ServeE
|
|
|
228
251
|
service: "well-known",
|
|
229
252
|
});
|
|
230
253
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
target: `http://127.0.0.1:${vault.port}${vaultBase}${oauthPath}`,
|
|
243
|
-
service: `${vault.name}:oauth`,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
254
|
+
// The hub is the OAuth IdP — mount the four endpoints at the canonical
|
|
255
|
+
// origin and proxy them to the hub's loopback. tailscale strips the mount
|
|
256
|
+
// before forwarding, so the target keeps the same path (matches the
|
|
257
|
+
// `serviceProxyTarget` rule of thumb in the doc above).
|
|
258
|
+
for (const oauthPath of OAUTH_PATHS) {
|
|
259
|
+
entries.push({
|
|
260
|
+
kind: "proxy",
|
|
261
|
+
mount: oauthPath,
|
|
262
|
+
target: serviceProxyTarget(hubPort, oauthPath),
|
|
263
|
+
service: "hub:oauth",
|
|
264
|
+
});
|
|
246
265
|
}
|
|
247
266
|
return entries;
|
|
248
267
|
}
|
|
@@ -372,12 +391,22 @@ export async function exposeUp(layer: ExposeLayer, opts: ExposeOpts = {}): Promi
|
|
|
372
391
|
);
|
|
373
392
|
}
|
|
374
393
|
|
|
394
|
+
// Kept for manual debugging / inspection only — the hub server now builds
|
|
395
|
+
// /.well-known/parachute.json dynamically from services.json at request time
|
|
396
|
+
// (#135), so this on-disk copy is no longer load-bearing for any consumer.
|
|
375
397
|
const wellKnownDoc = buildWellKnown({ services, canonicalOrigin });
|
|
376
398
|
writeWellKnownFile(wellKnownDoc, wellKnownFilePath);
|
|
377
399
|
log(`Wrote ${wellKnownFilePath}`);
|
|
378
400
|
writeHubFile(hubFilePath);
|
|
379
401
|
log(`Wrote ${hubFilePath}`);
|
|
380
402
|
|
|
403
|
+
// Resolve the public hub origin before spawning the hub server — it gets
|
|
404
|
+
// baked into the OAuth `iss` claim via the `--issuer` flag. Falling back to
|
|
405
|
+
// the request origin would put `http://127.0.0.1:<port>` in tokens, which
|
|
406
|
+
// any client following RFC 8414 would reject.
|
|
407
|
+
const hubOrigin =
|
|
408
|
+
deriveHubOrigin({ override: opts.hubOrigin, exposeFqdn: fqdn }) ?? canonicalOrigin;
|
|
409
|
+
|
|
381
410
|
let hubPort: number;
|
|
382
411
|
if (opts.skipHub) {
|
|
383
412
|
const existing = readHubPort(configDir);
|
|
@@ -391,6 +420,7 @@ export async function exposeUp(layer: ExposeLayer, opts: ExposeOpts = {}): Promi
|
|
|
391
420
|
...(opts.hubEnsureOpts ?? {}),
|
|
392
421
|
configDir,
|
|
393
422
|
wellKnownDir,
|
|
423
|
+
issuer: hubOrigin,
|
|
394
424
|
log,
|
|
395
425
|
});
|
|
396
426
|
hubPort = hub.port;
|
|
@@ -415,8 +445,6 @@ export async function exposeUp(layer: ExposeLayer, opts: ExposeOpts = {}): Promi
|
|
|
415
445
|
return code;
|
|
416
446
|
}
|
|
417
447
|
|
|
418
|
-
const hubOrigin =
|
|
419
|
-
deriveHubOrigin({ override: opts.hubOrigin, exposeFqdn: fqdn }) ?? canonicalOrigin;
|
|
420
448
|
const state: ExposeState = {
|
|
421
449
|
version: 1,
|
|
422
450
|
layer,
|
|
@@ -433,13 +461,14 @@ export async function exposeUp(layer: ExposeLayer, opts: ExposeOpts = {}): Promi
|
|
|
433
461
|
if (layer === "public") {
|
|
434
462
|
log(`✓ Public exposure active (Funnel). Open: ${canonicalOrigin}/`);
|
|
435
463
|
log(" This node is reachable from the public internet.");
|
|
464
|
+
log(
|
|
465
|
+
" Note: public is exploratory. Tailnet is the supported exposure shape today; the hub's OAuth + scope work targets tailnet first. Prefer `parachute expose tailnet` unless you specifically need a public URL.",
|
|
466
|
+
);
|
|
436
467
|
} else {
|
|
437
468
|
log(`✓ Tailnet exposure active. Open: ${canonicalOrigin}/`);
|
|
438
469
|
}
|
|
439
470
|
log(` Discovery: ${canonicalOrigin}${WELL_KNOWN_MOUNT}`);
|
|
440
|
-
|
|
441
|
-
log(` OAuth issuer: ${hubOrigin}`);
|
|
442
|
-
}
|
|
471
|
+
log(` OAuth issuer: ${hubOrigin}`);
|
|
443
472
|
|
|
444
473
|
// Auto-restart services that cache the hub origin. Aaron hit this on launch
|
|
445
474
|
// day: after `expose public` first-run, vault kept its stale (loopback)
|
|
@@ -494,6 +523,8 @@ export async function exposeOff(layer: ExposeLayer, opts: ExposeOpts = {}): Prom
|
|
|
494
523
|
}
|
|
495
524
|
|
|
496
525
|
clearExposeState(statePath);
|
|
526
|
+
// Pair to the debug-only write at expose-up — clean up the inspection artifact
|
|
527
|
+
// on teardown so it doesn't outlive the layer it described.
|
|
497
528
|
if (existsSync(wellKnownFilePath)) {
|
|
498
529
|
unlinkSync(wellKnownFilePath);
|
|
499
530
|
}
|