@openparachute/hub 0.5.2 → 0.5.9-rc.6
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__/admin-clients.test.ts +275 -0
- package/src/__tests__/admin-handlers.test.ts +159 -320
- package/src/__tests__/admin-host-admin-token.test.ts +52 -4
- package/src/__tests__/api-me.test.ts +149 -0
- package/src/__tests__/api-mint-token.test.ts +381 -0
- package/src/__tests__/api-revocation-list.test.ts +198 -0
- package/src/__tests__/api-revoke-token.test.ts +320 -0
- package/src/__tests__/api-tokens.test.ts +629 -0
- package/src/__tests__/auth.test.ts +680 -16
- package/src/__tests__/expose-2fa-warning.test.ts +123 -0
- package/src/__tests__/expose-cloudflare.test.ts +101 -0
- package/src/__tests__/expose.test.ts +199 -340
- package/src/__tests__/hub-server.test.ts +986 -66
- package/src/__tests__/hub.test.ts +108 -55
- package/src/__tests__/install-source.test.ts +249 -0
- package/src/__tests__/install.test.ts +50 -31
- package/src/__tests__/jwt-sign.test.ts +205 -0
- package/src/__tests__/lifecycle.test.ts +97 -2
- package/src/__tests__/module-manifest.test.ts +48 -0
- package/src/__tests__/notes-serve.test.ts +154 -2
- package/src/__tests__/oauth-handlers.test.ts +1000 -3
- package/src/__tests__/operator-token.test.ts +379 -3
- package/src/__tests__/origin-check.test.ts +220 -0
- package/src/__tests__/port-assign.test.ts +41 -52
- package/src/__tests__/rate-limit.test.ts +190 -0
- package/src/__tests__/services-manifest.test.ts +341 -0
- package/src/__tests__/setup.test.ts +12 -9
- package/src/__tests__/status.test.ts +372 -0
- package/src/__tests__/well-known.test.ts +69 -0
- package/src/admin-clients.ts +139 -0
- package/src/admin-handlers.ts +63 -260
- package/src/admin-host-admin-token.ts +25 -10
- package/src/admin-login-ui.ts +256 -0
- package/src/admin-vault-admin-token.ts +1 -1
- package/src/api-me.ts +124 -0
- package/src/api-mint-token.ts +239 -0
- package/src/api-revocation-list.ts +59 -0
- package/src/api-revoke-token.ts +153 -0
- package/src/api-tokens.ts +224 -0
- package/src/commands/auth.ts +408 -51
- package/src/commands/expose-2fa-warning.ts +82 -0
- package/src/commands/expose-cloudflare.ts +27 -0
- package/src/commands/expose-public-auto.ts +3 -7
- package/src/commands/expose.ts +88 -173
- package/src/commands/install.ts +11 -13
- package/src/commands/lifecycle.ts +53 -4
- package/src/commands/status.ts +99 -8
- package/src/csrf.ts +6 -3
- package/src/help.ts +13 -7
- package/src/hub-db.ts +63 -0
- package/src/hub-server.ts +572 -106
- package/src/hub.ts +272 -149
- package/src/install-source.ts +291 -0
- package/src/jwt-sign.ts +265 -5
- package/src/module-manifest.ts +48 -10
- package/src/notes-serve.ts +70 -9
- package/src/oauth-handlers.ts +395 -29
- package/src/oauth-ui.ts +188 -0
- package/src/operator-token.ts +272 -18
- package/src/origin-check.ts +127 -0
- package/src/port-assign.ts +28 -35
- package/src/rate-limit.ts +166 -0
- package/src/scope-explanations.ts +33 -2
- package/src/service-spec.ts +58 -13
- package/src/services-manifest.ts +62 -3
- package/src/sessions.ts +19 -0
- package/src/well-known.ts +54 -1
- package/web/ui/dist/assets/index-Bv6Bq_wx.js +60 -0
- package/web/ui/dist/assets/index-D54otIhv.css +1 -0
- package/web/ui/dist/index.html +2 -2
- package/src/__tests__/admin-config.test.ts +0 -281
- package/src/admin-config-ui.ts +0 -534
- package/src/admin-config.ts +0 -226
- package/web/ui/dist/assets/index-BKzPDdB0.js +0 -60
- package/web/ui/dist/assets/index-Dyk6g7vT.css +0 -1
package/src/module-manifest.ts
CHANGED
|
@@ -114,6 +114,27 @@ export interface ModuleManifest {
|
|
|
114
114
|
* as `hasAuth` / `init` / `urlForEntry`.
|
|
115
115
|
*/
|
|
116
116
|
readonly managementUrl?: string;
|
|
117
|
+
/**
|
|
118
|
+
* Where the module's primary user-facing UI lives. Hub renders a tile on
|
|
119
|
+
* the discovery page (`/`) Services section when set (see
|
|
120
|
+
* `parachute-patterns/patterns/module-json-extensibility.md` and the
|
|
121
|
+
* `loadUiUrls` resolver in `hub-server.ts`).
|
|
122
|
+
*
|
|
123
|
+
* Two shapes — same rules as `managementUrl`:
|
|
124
|
+
* - A relative path (e.g. `"/notes"`, `"/agent"`) — hub resolves
|
|
125
|
+
* against the canonical hub origin.
|
|
126
|
+
* - A full absolute URL — hub uses verbatim.
|
|
127
|
+
*
|
|
128
|
+
* Absent = no Services tile rendered (the module is API-only or surfaces
|
|
129
|
+
* its UI via a sibling module — e.g. vault content browses through Notes,
|
|
130
|
+
* so vault has no `uiUrl`).
|
|
131
|
+
*
|
|
132
|
+
* Read at every discovery render via `installDir/.parachute/module.json`
|
|
133
|
+
* (mirrors how `managementUrl` is sourced for vaults). Not persisted in
|
|
134
|
+
* services.json — that file's "services own the write side" semantics
|
|
135
|
+
* would clobber any install-time copy on the next service boot.
|
|
136
|
+
*/
|
|
137
|
+
readonly uiUrl?: string;
|
|
117
138
|
/**
|
|
118
139
|
* When `true`, the hub's `/<svc>/*` proxy strips the matched mount prefix
|
|
119
140
|
* before forwarding (so the backend sees `/health` rather than
|
|
@@ -374,6 +395,7 @@ export function validateModuleManifest(raw: unknown, where: string): ModuleManif
|
|
|
374
395
|
const dependencies = asDependencies(m.dependencies, where);
|
|
375
396
|
const configSchema = asConfigSchema(m.configSchema, where);
|
|
376
397
|
const managementUrl = asManagementUrl(m.managementUrl, where);
|
|
398
|
+
const uiUrl = asUiUrl(m.uiUrl, where);
|
|
377
399
|
let stripPrefix: boolean | undefined;
|
|
378
400
|
if (m.stripPrefix !== undefined) {
|
|
379
401
|
if (typeof m.stripPrefix !== "boolean") {
|
|
@@ -396,6 +418,9 @@ export function validateModuleManifest(raw: unknown, where: string): ModuleManif
|
|
|
396
418
|
if (managementUrl !== undefined) {
|
|
397
419
|
(out as { managementUrl?: string }).managementUrl = managementUrl;
|
|
398
420
|
}
|
|
421
|
+
if (uiUrl !== undefined) {
|
|
422
|
+
(out as { uiUrl?: string }).uiUrl = uiUrl;
|
|
423
|
+
}
|
|
399
424
|
if (stripPrefix !== undefined) {
|
|
400
425
|
(out as { stripPrefix?: boolean }).stripPrefix = stripPrefix;
|
|
401
426
|
}
|
|
@@ -403,26 +428,39 @@ export function validateModuleManifest(raw: unknown, where: string): ModuleManif
|
|
|
403
428
|
}
|
|
404
429
|
|
|
405
430
|
function asManagementUrl(v: unknown, where: string): string | undefined {
|
|
431
|
+
return asPathOrUrl(v, where, "managementUrl");
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function asUiUrl(v: unknown, where: string): string | undefined {
|
|
435
|
+
return asPathOrUrl(v, where, "uiUrl");
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Validate a "path or http(s) URL" field. Both `managementUrl` and `uiUrl`
|
|
440
|
+
* follow the same shape per the module-json-extensibility pattern doc;
|
|
441
|
+
* factored so the next URL-shaped field doesn't have to copy-paste.
|
|
442
|
+
*/
|
|
443
|
+
function asPathOrUrl(v: unknown, where: string, field: string): string | undefined {
|
|
406
444
|
if (v === undefined) return undefined;
|
|
407
445
|
if (typeof v !== "string" || v.length === 0) {
|
|
408
|
-
throw new ModuleManifestError(
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
//
|
|
413
|
-
|
|
446
|
+
throw new ModuleManifestError(`${where}: "${field}" must be a non-empty string if present`);
|
|
447
|
+
}
|
|
448
|
+
// Two valid shapes: an absolute path starting with a single "/" or a full
|
|
449
|
+
// http(s) URL. Reject protocol-relative forms like "//evil.com" — they
|
|
450
|
+
// start with "/" but `new URL("//evil.com", base)` resolves to the foreign
|
|
451
|
+
// origin, which would let a malicious module render an off-origin tile and
|
|
452
|
+
// turn the discovery page into an open-redirect surface.
|
|
453
|
+
if (v.startsWith("/") && !v.startsWith("//")) return v;
|
|
414
454
|
try {
|
|
415
455
|
const u = new URL(v);
|
|
416
456
|
if (u.protocol !== "http:" && u.protocol !== "https:") {
|
|
417
|
-
throw new ModuleManifestError(
|
|
418
|
-
`${where}: "managementUrl" absolute form must use http: or https:`,
|
|
419
|
-
);
|
|
457
|
+
throw new ModuleManifestError(`${where}: "${field}" absolute form must use http: or https:`);
|
|
420
458
|
}
|
|
421
459
|
return v;
|
|
422
460
|
} catch (err) {
|
|
423
461
|
if (err instanceof ModuleManifestError) throw err;
|
|
424
462
|
throw new ModuleManifestError(
|
|
425
|
-
`${where}: "
|
|
463
|
+
`${where}: "${field}" must be a path starting with "/" or a full http(s) URL`,
|
|
426
464
|
);
|
|
427
465
|
}
|
|
428
466
|
}
|
package/src/notes-serve.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { existsSync } from "node:fs";
|
|
28
|
+
import { homedir } from "node:os";
|
|
28
29
|
import { dirname, join, resolve } from "node:path";
|
|
29
30
|
|
|
30
31
|
interface Args {
|
|
@@ -67,16 +68,76 @@ export function normalizeMount(raw: string): string {
|
|
|
67
68
|
return raw.replace(/\/+$/, "");
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Candidate base directories that `Bun.resolveSync` walks from when looking
|
|
73
|
+
* for `@openparachute/notes/package.json`. Order matters:
|
|
74
|
+
*
|
|
75
|
+
* 1. `process.cwd()` — works when notes-serve is invoked from inside the
|
|
76
|
+
* notes checkout (e.g. via `installDir` cwd in lifecycle.ts) or from
|
|
77
|
+
* any project that depends on `@openparachute/notes`.
|
|
78
|
+
* 2. `~/.bun/install/global/node_modules` — modern Bun's global-install
|
|
79
|
+
* layout. This is where `bun add -g @openparachute/notes` lands the
|
|
80
|
+
* package, and where `bun link @openparachute/notes` symlinks it.
|
|
81
|
+
* 3. `~/.bun/install/global` — defensive fallback for older Bun layouts.
|
|
82
|
+
*
|
|
83
|
+
* Hub itself does NOT depend on `@openparachute/notes`, so when
|
|
84
|
+
* `parachute start notes` is run from the hub repo dir, the cwd-relative
|
|
85
|
+
* resolve walks ancestral node_modules and finds nothing. Bun does not
|
|
86
|
+
* auto-consult the global install dir, so bun-linked installs fail to
|
|
87
|
+
* resolve without (2)/(3). hub#194: Aaron hit silent 502 on tailnet
|
|
88
|
+
* `/notes/` because of this — fixed by trying the global install dirs.
|
|
89
|
+
*
|
|
90
|
+
* Exported (and parameterized via `cwd`/`home`) so tests can drive the
|
|
91
|
+
* resolution order against a real fixture install without monkey-patching
|
|
92
|
+
* `Bun.resolveSync`.
|
|
93
|
+
*/
|
|
94
|
+
export function notesDistCandidates(cwd: string, home: string): string[] {
|
|
95
|
+
return [cwd, join(home, ".bun/install/global/node_modules"), join(home, ".bun/install/global")];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ResolveNotesDistDeps {
|
|
99
|
+
cwd?: string;
|
|
100
|
+
home?: string;
|
|
101
|
+
/** Override `Bun.resolveSync` for tests. */
|
|
102
|
+
resolveSync?: (specifier: string, base: string) => string;
|
|
103
|
+
existsSync?: (path: string) => boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function resolveNotesDistFrom(deps: ResolveNotesDistDeps = {}): string {
|
|
107
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
108
|
+
const home = deps.home ?? homedir();
|
|
109
|
+
const resolveSync = deps.resolveSync ?? Bun.resolveSync;
|
|
110
|
+
const exists = deps.existsSync ?? existsSync;
|
|
111
|
+
const candidates = notesDistCandidates(cwd, home);
|
|
112
|
+
const resolveErrors: string[] = [];
|
|
113
|
+
for (const base of candidates) {
|
|
114
|
+
let pkgPath: string;
|
|
115
|
+
try {
|
|
116
|
+
pkgPath = resolveSync("@openparachute/notes/package.json", base);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
resolveErrors.push(` - ${base}: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const root = dirname(pkgPath);
|
|
122
|
+
const dist = join(root, "dist");
|
|
123
|
+
if (!exists(dist)) {
|
|
124
|
+
// Found the package but it has no dist/. This is a hard error
|
|
125
|
+
// (package shipped without a prebuilt bundle); don't fall through to
|
|
126
|
+
// other candidates — they'd resolve to the same package and report
|
|
127
|
+
// the same problem.
|
|
128
|
+
throw new Error(
|
|
129
|
+
`@openparachute/notes resolved at ${root} has no dist/ directory at ${dist}. The package may not ship a prebuilt bundle — ask the notes maintainer to add a prepublishOnly build step.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return dist;
|
|
78
133
|
}
|
|
79
|
-
|
|
134
|
+
throw new Error(
|
|
135
|
+
`Could not resolve @openparachute/notes from any of:\n${resolveErrors.join("\n")}\nIs the package installed? Try \`bun add -g @openparachute/notes\` or \`parachute install notes\`.`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveNotesDist(): string {
|
|
140
|
+
return resolveNotesDistFrom();
|
|
80
141
|
}
|
|
81
142
|
|
|
82
143
|
function mimeFor(path: string): string | undefined {
|