@openparachute/hub 0.7.6 → 0.7.7-rc.12
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 -7
- package/package.json +8 -13
- package/src/__tests__/account-api.test.ts +798 -0
- package/src/__tests__/account-session.test.ts +252 -0
- package/src/__tests__/account-token.test.ts +316 -0
- package/src/__tests__/admin-connections-credentials.test.ts +41 -0
- package/src/__tests__/admin-handlers.test.ts +25 -0
- package/src/__tests__/admin-lock.test.ts +3 -14
- package/src/__tests__/admin-module-token.test.ts +10 -30
- package/src/__tests__/admin-surfaces.test.ts +21 -0
- package/src/__tests__/api-hub-upgrade.test.ts +11 -0
- package/src/__tests__/api-mint-token.test.ts +25 -0
- package/src/__tests__/api-modules-ops.test.ts +34 -29
- package/src/__tests__/api-modules.test.ts +50 -58
- package/src/__tests__/api-revoke-token.test.ts +23 -0
- package/src/__tests__/api-settings-hub-origin.test.ts +12 -0
- package/src/__tests__/api-settings-root-redirect.test.ts +12 -0
- package/src/__tests__/api-tokens.test.ts +44 -0
- package/src/__tests__/audience-gate.test.ts +24 -0
- package/src/__tests__/bearer-scheme-casing.test.ts +110 -0
- package/src/__tests__/chrome-strip.test.ts +18 -1
- package/src/__tests__/doctor.test.ts +10 -17
- package/src/__tests__/door-contract-parity.test.ts +46 -0
- package/src/__tests__/hub-server.test.ts +37 -0
- package/src/__tests__/hub.test.ts +29 -0
- package/src/__tests__/install.test.ts +279 -5
- package/src/__tests__/migrate.test.ts +3 -1
- package/src/__tests__/notes-serve.test.ts +216 -0
- package/src/__tests__/oauth-handlers.test.ts +19 -8
- package/src/__tests__/operator-token.test.ts +1 -2
- package/src/__tests__/port-assign.test.ts +37 -17
- package/src/__tests__/scope-explanations.test.ts +22 -2
- package/src/__tests__/serve-boot.test.ts +25 -36
- package/src/__tests__/service-spec-discovery.test.ts +30 -35
- package/src/__tests__/services-manifest.test.ts +372 -132
- package/src/__tests__/sessions.test.ts +75 -28
- package/src/__tests__/setup-wizard.test.ts +7 -10
- package/src/__tests__/setup.test.ts +13 -14
- package/src/__tests__/status.test.ts +0 -5
- package/src/__tests__/surface-notes-alias.test.ts +296 -0
- package/src/account-api.ts +677 -0
- package/src/account-session.ts +132 -0
- package/src/account-token.ts +200 -0
- package/src/admin-connections.ts +4 -2
- package/src/admin-lock.ts +1 -2
- package/src/admin-module-token.ts +13 -8
- package/src/admin-surfaces.ts +2 -1
- package/src/api-hub-upgrade.ts +2 -1
- package/src/api-invites.ts +19 -0
- package/src/api-mint-token.ts +2 -1
- package/src/api-modules-ops.ts +2 -1
- package/src/api-modules.ts +10 -8
- package/src/api-revoke-token.ts +2 -1
- package/src/api-settings-hub-origin.ts +2 -1
- package/src/api-settings-root-redirect.ts +2 -1
- package/src/api-tokens.ts +2 -1
- package/src/audience-gate.ts +2 -1
- package/src/chrome-strip.ts +16 -4
- package/src/commands/install.ts +86 -8
- package/src/commands/migrate.ts +5 -1
- package/src/commands/setup.ts +9 -6
- package/src/help.ts +6 -6
- package/src/hub-server.ts +247 -52
- package/src/hub-settings.ts +25 -1
- package/src/hub.ts +64 -31
- package/src/invites.ts +42 -0
- package/src/module-ops-client.ts +2 -1
- package/src/notes-serve.ts +73 -31
- package/src/oauth-handlers.ts +1 -11
- package/src/operator-token.ts +0 -1
- package/src/origin-check.ts +2 -2
- package/src/scope-explanations.ts +35 -5
- package/src/service-spec.ts +128 -74
- package/src/services-manifest.ts +112 -52
- package/src/sessions.ts +66 -30
- package/src/surface-notes-alias.ts +126 -0
- package/src/__tests__/admin-agent-token.test.ts +0 -173
- package/src/admin-agent-token.ts +0 -147
package/src/notes-serve.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Tiny static-file server for
|
|
4
|
+
* Tiny static-file server for a Parachute PWA bundle — originally written
|
|
5
|
+
* for @openparachute/notes, generalized in hub-parity P5 (2026-07-11) to
|
|
6
|
+
* also serve @openparachute/parachute-app (the super-surface front door,
|
|
7
|
+
* port 1944, mount `/app`) via the same shim. Any package matching this
|
|
8
|
+
* shape (a prebuilt SPA `dist/` with no server of its own) can reuse it.
|
|
5
9
|
*
|
|
6
|
-
*
|
|
7
|
-
* this shim with the installed `dist/` path so the PWA is
|
|
8
|
-
* known port and can be reverse-proxied by `parachute expose`
|
|
9
|
-
* the other services.
|
|
10
|
+
* A served bundle is a SPA — no backend of its own. `parachute start
|
|
11
|
+
* <svc>` invokes this shim with the installed `dist/` path so the PWA is
|
|
12
|
+
* served at a known port and can be reverse-proxied by `parachute expose`
|
|
13
|
+
* alongside the other services.
|
|
10
14
|
*
|
|
11
15
|
* Invoked as:
|
|
12
|
-
* bun <this-file> --port <n> [--dist <path>] [--mount <prefix>]
|
|
16
|
+
* bun <this-file> --port <n> [--dist <path>] [--mount <prefix>] [--package <npmName>]
|
|
13
17
|
*
|
|
14
18
|
* `--mount` (default `/notes`) is the path prefix the reverse proxy hands
|
|
15
19
|
* us. We strip it before resolving against `dist/` so a request for
|
|
@@ -17,27 +21,43 @@
|
|
|
17
21
|
* `{dist}/notes/sw.js`. Without the strip, the SW + .webmanifest both
|
|
18
22
|
* SPA-fall-back to index.html with content-type text/html, and the PWA
|
|
19
23
|
* install prompt never fires. Pass `--mount ""` (or `--mount /`) when the
|
|
20
|
-
* bundle is served at the origin root.
|
|
24
|
+
* bundle is served at the origin root. THIS IS LOAD-BEARING for every
|
|
25
|
+
* package served by this shim, not just notes — keep it exactly as-is.
|
|
21
26
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
27
|
+
* `--package` (default `@openparachute/notes`, back-compat) names the npm
|
|
28
|
+
* package whose `dist/` we resolve when `--dist` is omitted. Passed by
|
|
29
|
+
* FIRST_PARTY_FALLBACKS entries whose startCmd composes this shim for a
|
|
30
|
+
* package other than notes (e.g. `app`'s `--package @openparachute/parachute-app`).
|
|
31
|
+
*
|
|
32
|
+
* If --dist is omitted, we resolve the package's dist directory via
|
|
33
|
+
* Bun.resolveSync. If that fails (package not installed globally, or
|
|
24
34
|
* package doesn't ship dist/), exit 1 with a clear error.
|
|
35
|
+
*
|
|
36
|
+
* `/health` (post-mount-strip) always answers 2xx — the doctor/status
|
|
37
|
+
* probe (`probeModuleHealth`) only cares about the status code, but we
|
|
38
|
+
* answer explicitly rather than relying on the SPA-shell catch-all so a
|
|
39
|
+
* missing/corrupt `dist/index.html` can't take the health check down with it.
|
|
25
40
|
*/
|
|
26
41
|
|
|
27
42
|
import { existsSync } from "node:fs";
|
|
28
43
|
import { homedir } from "node:os";
|
|
29
44
|
import { dirname, join, resolve } from "node:path";
|
|
30
45
|
|
|
46
|
+
/** Back-compat default — the shim's original (and still primary) consumer. */
|
|
47
|
+
const DEFAULT_PACKAGE = "@openparachute/notes";
|
|
48
|
+
|
|
31
49
|
interface Args {
|
|
32
50
|
port: number;
|
|
33
51
|
dist?: string;
|
|
34
52
|
mount: string;
|
|
53
|
+
pkg: string;
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
function parseArgs(argv: string[]): Args {
|
|
38
57
|
let port = 5173;
|
|
39
58
|
let dist: string | undefined;
|
|
40
59
|
let mount = "/notes";
|
|
60
|
+
let pkg = DEFAULT_PACKAGE;
|
|
41
61
|
for (let i = 0; i < argv.length; i++) {
|
|
42
62
|
const a = argv[i];
|
|
43
63
|
if (a === "--port") {
|
|
@@ -56,11 +76,15 @@ function parseArgs(argv: string[]): Args {
|
|
|
56
76
|
const v = argv[++i];
|
|
57
77
|
if (v === undefined) throw new Error("--mount requires a value");
|
|
58
78
|
mount = normalizeMount(v);
|
|
79
|
+
} else if (a === "--package") {
|
|
80
|
+
const v = argv[++i];
|
|
81
|
+
if (!v) throw new Error("--package requires a value");
|
|
82
|
+
pkg = v;
|
|
59
83
|
} else {
|
|
60
84
|
throw new Error(`unknown argument: ${a}`);
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
|
-
return { port, dist, mount };
|
|
87
|
+
return { port, dist, mount, pkg };
|
|
64
88
|
}
|
|
65
89
|
|
|
66
90
|
export function normalizeMount(raw: string): string {
|
|
@@ -70,22 +94,22 @@ export function normalizeMount(raw: string): string {
|
|
|
70
94
|
|
|
71
95
|
/**
|
|
72
96
|
* Candidate base directories that `Bun.resolveSync` walks from when looking
|
|
73
|
-
* for
|
|
97
|
+
* for `<package>/package.json`. Order matters:
|
|
74
98
|
*
|
|
75
|
-
* 1. `process.cwd()` — works when
|
|
76
|
-
*
|
|
77
|
-
* any project that depends on
|
|
99
|
+
* 1. `process.cwd()` — works when the shim is invoked from inside the
|
|
100
|
+
* package's own checkout (e.g. via `installDir` cwd in lifecycle.ts) or
|
|
101
|
+
* from any project that depends on the package.
|
|
78
102
|
* 2. `~/.bun/install/global/node_modules` — modern Bun's global-install
|
|
79
|
-
* layout. This is where `bun add -g
|
|
80
|
-
*
|
|
103
|
+
* layout. This is where `bun add -g <package>` lands the package, and
|
|
104
|
+
* where `bun link <package>` symlinks it.
|
|
81
105
|
* 3. `~/.bun/install/global` — defensive fallback for older Bun layouts.
|
|
82
106
|
*
|
|
83
|
-
* Hub itself does NOT depend on
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
107
|
+
* Hub itself does NOT depend on the served package, so when `parachute
|
|
108
|
+
* start <svc>` is run from the hub repo dir, the cwd-relative resolve walks
|
|
109
|
+
* ancestral node_modules and finds nothing. Bun does not auto-consult the
|
|
110
|
+
* global install dir, so bun-linked installs fail to resolve without
|
|
111
|
+
* (2)/(3). hub#194: Aaron hit silent 502 on tailnet `/notes/` because of
|
|
112
|
+
* this — fixed by trying the global install dirs.
|
|
89
113
|
*
|
|
90
114
|
* Exported (and parameterized via `cwd`/`home`) so tests can drive the
|
|
91
115
|
* resolution order against a real fixture install without monkey-patching
|
|
@@ -98,6 +122,8 @@ export function notesDistCandidates(cwd: string, home: string): string[] {
|
|
|
98
122
|
export interface ResolveNotesDistDeps {
|
|
99
123
|
cwd?: string;
|
|
100
124
|
home?: string;
|
|
125
|
+
/** npm package name to resolve. Defaults to `@openparachute/notes` (back-compat). */
|
|
126
|
+
pkg?: string;
|
|
101
127
|
/** Override `Bun.resolveSync` for tests. */
|
|
102
128
|
resolveSync?: (specifier: string, base: string) => string;
|
|
103
129
|
existsSync?: (path: string) => boolean;
|
|
@@ -106,6 +132,7 @@ export interface ResolveNotesDistDeps {
|
|
|
106
132
|
export function resolveNotesDistFrom(deps: ResolveNotesDistDeps = {}): string {
|
|
107
133
|
const cwd = deps.cwd ?? process.cwd();
|
|
108
134
|
const home = deps.home ?? homedir();
|
|
135
|
+
const pkg = deps.pkg ?? DEFAULT_PACKAGE;
|
|
109
136
|
const resolveSync = deps.resolveSync ?? Bun.resolveSync;
|
|
110
137
|
const exists = deps.existsSync ?? existsSync;
|
|
111
138
|
const candidates = notesDistCandidates(cwd, home);
|
|
@@ -113,7 +140,7 @@ export function resolveNotesDistFrom(deps: ResolveNotesDistDeps = {}): string {
|
|
|
113
140
|
for (const base of candidates) {
|
|
114
141
|
let pkgPath: string;
|
|
115
142
|
try {
|
|
116
|
-
pkgPath = resolveSync(
|
|
143
|
+
pkgPath = resolveSync(`${pkg}/package.json`, base);
|
|
117
144
|
} catch (err) {
|
|
118
145
|
resolveErrors.push(` - ${base}: ${err instanceof Error ? err.message : String(err)}`);
|
|
119
146
|
continue;
|
|
@@ -126,18 +153,18 @@ export function resolveNotesDistFrom(deps: ResolveNotesDistDeps = {}): string {
|
|
|
126
153
|
// other candidates — they'd resolve to the same package and report
|
|
127
154
|
// the same problem.
|
|
128
155
|
throw new Error(
|
|
129
|
-
|
|
156
|
+
`${pkg} resolved at ${root} has no dist/ directory at ${dist}. The package may not ship a prebuilt bundle — ask the ${pkg} maintainer to add a prepublishOnly build step.`,
|
|
130
157
|
);
|
|
131
158
|
}
|
|
132
159
|
return dist;
|
|
133
160
|
}
|
|
134
161
|
throw new Error(
|
|
135
|
-
`Could not resolve
|
|
162
|
+
`Could not resolve ${pkg} from any of:\n${resolveErrors.join("\n")}\nIs the package installed? Try \`bun add -g ${pkg}\` or the matching \`parachute install <short>\`.`,
|
|
136
163
|
);
|
|
137
164
|
}
|
|
138
165
|
|
|
139
|
-
function resolveNotesDist(): string {
|
|
140
|
-
return resolveNotesDistFrom();
|
|
166
|
+
function resolveNotesDist(pkg: string): string {
|
|
167
|
+
return resolveNotesDistFrom({ pkg });
|
|
141
168
|
}
|
|
142
169
|
|
|
143
170
|
function mimeFor(path: string): string | undefined {
|
|
@@ -160,6 +187,17 @@ export function notesFetch(dist: string, mount: string): (req: Request) => Respo
|
|
|
160
187
|
if (mount && (pathname === mount || pathname.startsWith(`${mount}/`))) {
|
|
161
188
|
pathname = pathname.slice(mount.length) || "/";
|
|
162
189
|
}
|
|
190
|
+
if (pathname === "/health") {
|
|
191
|
+
// Explicit rather than falling through to the SPA shell: the doctor /
|
|
192
|
+
// status probe (`probeModuleHealth`) only checks for a 2xx status, but
|
|
193
|
+
// answering directly means a missing/corrupt `dist/index.html` can't
|
|
194
|
+
// take the health check down with it. Every FIRST_PARTY_FALLBACKS
|
|
195
|
+
// entry served by this shim declares `health` under its own mount
|
|
196
|
+
// (e.g. `/notes/health`, `/app/health`) — this answers all of them.
|
|
197
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
198
|
+
headers: { "content-type": "application/json" },
|
|
199
|
+
});
|
|
200
|
+
}
|
|
163
201
|
if (pathname === "/" || pathname.endsWith("/")) {
|
|
164
202
|
return spaShell();
|
|
165
203
|
}
|
|
@@ -200,17 +238,21 @@ export function notesServeOptions(
|
|
|
200
238
|
}
|
|
201
239
|
|
|
202
240
|
if (import.meta.main) {
|
|
203
|
-
const { port, dist: distArg, mount } = parseArgs(process.argv.slice(2));
|
|
241
|
+
const { port, dist: distArg, mount, pkg } = parseArgs(process.argv.slice(2));
|
|
204
242
|
|
|
205
243
|
let dist: string;
|
|
206
244
|
try {
|
|
207
|
-
dist = distArg ?? resolveNotesDist();
|
|
245
|
+
dist = distArg ?? resolveNotesDist(pkg);
|
|
208
246
|
} catch (err) {
|
|
209
|
-
console.error(
|
|
247
|
+
console.error(
|
|
248
|
+
`parachute-static-serve (${pkg}): ${err instanceof Error ? err.message : String(err)}`,
|
|
249
|
+
);
|
|
210
250
|
process.exit(1);
|
|
211
251
|
}
|
|
212
252
|
|
|
213
253
|
Bun.serve(notesServeOptions(port, dist, mount));
|
|
214
254
|
|
|
215
|
-
console.log(
|
|
255
|
+
console.log(
|
|
256
|
+
`static-serve listening on :${port} (pkg=${pkg}, dist=${dist}, mount=${mount || "/"})`,
|
|
257
|
+
);
|
|
216
258
|
}
|
package/src/oauth-handlers.ts
CHANGED
|
@@ -418,13 +418,9 @@ function oauthErrorRedirect(
|
|
|
418
418
|
* Keyed on the SHORT name (resolved from each services.json row via
|
|
419
419
|
* `shortNameForManifest`) rather than the raw manifest name so a row written
|
|
420
420
|
* under a legacy manifest name still counts as installed — e.g. an un-upgraded
|
|
421
|
-
* `parachute-channel` row aliases to short `agent` (channel→agent rename
|
|
422
|
-
* 2026-06-17), so `agent:send` is still correctly advertised until the daemon
|
|
423
|
-
* re-registers under `parachute-agent`.
|
|
424
421
|
*/
|
|
425
422
|
const OPTIONAL_MODULE_SCOPES: ReadonlyArray<readonly [prefix: string, short: string]> = [
|
|
426
423
|
["scribe:", "scribe"],
|
|
427
|
-
["agent:", "agent"],
|
|
428
424
|
["surface:", "surface"],
|
|
429
425
|
];
|
|
430
426
|
|
|
@@ -1903,13 +1899,7 @@ export async function handleApproveClientPost(
|
|
|
1903
1899
|
// Headers logged are non-sensitive (Origin/Referer/Host are public);
|
|
1904
1900
|
// the bound set is hub's own configuration. Body content not logged.
|
|
1905
1901
|
console.warn(
|
|
1906
|
-
`[oauth] approve POST same-origin check failed. headers:
|
|
1907
|
-
`origin=${JSON.stringify(req.headers.get("origin"))} ` +
|
|
1908
|
-
`referer=${JSON.stringify(req.headers.get("referer"))} ` +
|
|
1909
|
-
`host=${JSON.stringify(req.headers.get("host"))} ` +
|
|
1910
|
-
`xff-host=${JSON.stringify(req.headers.get("x-forwarded-host"))} ` +
|
|
1911
|
-
`xff-proto=${JSON.stringify(req.headers.get("x-forwarded-proto"))}. ` +
|
|
1912
|
-
`bound origins: ${JSON.stringify(bound)}`,
|
|
1902
|
+
`[oauth] approve POST same-origin check failed. headers: origin=${JSON.stringify(req.headers.get("origin"))} referer=${JSON.stringify(req.headers.get("referer"))} host=${JSON.stringify(req.headers.get("host"))} xff-host=${JSON.stringify(req.headers.get("x-forwarded-host"))} xff-proto=${JSON.stringify(req.headers.get("x-forwarded-proto"))}. bound origins: ${JSON.stringify(bound)}`,
|
|
1913
1903
|
);
|
|
1914
1904
|
return htmlError(
|
|
1915
1905
|
"Cross-origin request rejected",
|
package/src/operator-token.ts
CHANGED
package/src/origin-check.ts
CHANGED
|
@@ -198,8 +198,8 @@ const MUTATION_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
|
198
198
|
*
|
|
199
199
|
* NOT belted, and why:
|
|
200
200
|
* - GET/HEAD/OPTIONS — read-shaped; the mint GETs
|
|
201
|
-
* (`/admin/host-admin-token`, `/admin/
|
|
202
|
-
* `/admin/
|
|
201
|
+
* (`/admin/host-admin-token`, `/admin/module-token/<short>`,
|
|
202
|
+
* `/admin/vault-admin-token/<name>`)
|
|
203
203
|
* enforce GET-only with a 405 and their response bodies are unreadable
|
|
204
204
|
* cross-origin (no CORS on these routes).
|
|
205
205
|
* - Bearer-authed requests — a cross-site page cannot attach an
|
|
@@ -46,7 +46,7 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
46
46
|
"Read and write everything, plus admin: config & settings, triggers & automation, GitHub backup, and minting access tokens.",
|
|
47
47
|
level: "admin",
|
|
48
48
|
},
|
|
49
|
-
// Optional-module scopes (scribe /
|
|
49
|
+
// Optional-module scopes (scribe / surface). These are in
|
|
50
50
|
// FIRST_PARTY_SCOPES (= Object.keys(this map)) but the modules may not be
|
|
51
51
|
// installed — so they're GATED in `OPTIONAL_MODULE_SCOPES` (oauth-handlers.ts)
|
|
52
52
|
// and only advertised in `scopes_supported` when the service is in
|
|
@@ -61,10 +61,7 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
61
61
|
label: "Manage Scribe configuration (provider keys, models, quotas).",
|
|
62
62
|
level: "admin",
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
label: "Post messages to your Agent.",
|
|
66
|
-
level: "send",
|
|
67
|
-
},
|
|
64
|
+
|
|
68
65
|
// Surface Git Transport scopes (surface-host). `surface:read` = clone/fetch a
|
|
69
66
|
// surface's hub-hosted git repo; `surface:write` = push to it. Named forms
|
|
70
67
|
// (`surface:<name>:<verb>`) collapse to these via the 3→2-segment rule in
|
|
@@ -112,8 +109,34 @@ export const SCOPE_EXPLANATIONS: Record<string, ScopeExplanation> = {
|
|
|
112
109
|
label: "Administer vaults on this host (create, configure, delete).",
|
|
113
110
|
level: "admin",
|
|
114
111
|
},
|
|
112
|
+
// Account scopes (Parachute App campaign, Phase 2 — the `/account/*` door
|
|
113
|
+
// contract). `account:<id>:admin` authorizes every account mutation
|
|
114
|
+
// (create/delete/import vaults, mint per-vault tokens, set caps); `:read`
|
|
115
|
+
// authorizes the read side (list vaults, read caps/usage). On a self-host
|
|
116
|
+
// hub `<id>` is the sentinel `self` (account ≡ box). These are minted ONLY
|
|
117
|
+
// by the cookie→bearer `POST /account/token` exchange, NEVER via the public
|
|
118
|
+
// OAuth flow — they're in NON_REQUESTABLE_SCOPES below, like the host scopes.
|
|
119
|
+
// Listed here (a) so `NON_REQUESTABLE_SCOPES ⊆ FIRST_PARTY_SCOPES` holds and
|
|
120
|
+
// (b) so scope-guard's `admin ⊇ read` inheritance recognizes the grammar
|
|
121
|
+
// (`account:self:admin` satisfies `account:self:read`).
|
|
122
|
+
"account:self:admin": {
|
|
123
|
+
label:
|
|
124
|
+
"Manage your Parachute account — create, delete, and configure your vaults, and mint access tokens for them.",
|
|
125
|
+
level: "admin",
|
|
126
|
+
},
|
|
127
|
+
"account:self:read": {
|
|
128
|
+
label: "View your Parachute account — list your vaults and read their settings and usage.",
|
|
129
|
+
level: "read",
|
|
130
|
+
},
|
|
115
131
|
};
|
|
116
132
|
|
|
133
|
+
/** Account scope strings (Parachute App campaign, Phase 2). Exported so the
|
|
134
|
+
* `POST /account/token` mint and the `/account/*` route gates reference the
|
|
135
|
+
* canonical constants rather than re-literaling the strings. `self` is the
|
|
136
|
+
* self-host account sentinel (account ≡ box). */
|
|
137
|
+
export const ACCOUNT_SELF_ADMIN_SCOPE = "account:self:admin";
|
|
138
|
+
export const ACCOUNT_SELF_READ_SCOPE = "account:self:read";
|
|
139
|
+
|
|
117
140
|
/**
|
|
118
141
|
* Sorted list of every first-party scope the hub recognizes. Used as the
|
|
119
142
|
* baseline for `scopes_supported` in the OAuth-AS metadata; module-declared
|
|
@@ -170,6 +193,13 @@ export const NON_REQUESTABLE_SCOPES: ReadonlySet<string> = new Set([
|
|
|
170
193
|
// Service-admin scopes: operator-only, never requestable via /oauth/authorize.
|
|
171
194
|
"hub:admin",
|
|
172
195
|
"scribe:admin",
|
|
196
|
+
// Account scopes (Parachute App campaign, Phase 2): cookie-minted only via
|
|
197
|
+
// `POST /account/token`, NEVER OAuth-requestable. A third-party app pointed
|
|
198
|
+
// at the hub AS must not be able to consent its way to account authority
|
|
199
|
+
// (create/delete vaults, mint per-vault tokens) — the account credential is
|
|
200
|
+
// exclusively the same-origin app's cookie→bearer exchange.
|
|
201
|
+
ACCOUNT_SELF_ADMIN_SCOPE,
|
|
202
|
+
ACCOUNT_SELF_READ_SCOPE,
|
|
173
203
|
]);
|
|
174
204
|
|
|
175
205
|
/**
|
package/src/service-spec.ts
CHANGED
|
@@ -8,10 +8,16 @@ import type { ServiceEntry } from "./services-manifest.ts";
|
|
|
8
8
|
*
|
|
9
9
|
* 1939 parachute-hub internal static + proxy, CLI-managed
|
|
10
10
|
* 1940 parachute-vault committed core
|
|
11
|
-
* 1941
|
|
11
|
+
* 1941 unassigned
|
|
12
12
|
* 1942 parachute-notes committed core (PWA bundle)
|
|
13
13
|
* 1943 parachute-scribe committed core
|
|
14
|
-
* 1944
|
|
14
|
+
* 1944 parachute-app the super-surface front door (PWA bundle,
|
|
15
|
+
* hub-parity P5) — see the PORT_RESERVATIONS
|
|
16
|
+
* comment below for why this is a DIFFERENT
|
|
17
|
+
* module from the pre-2026-05-27 `app`
|
|
18
|
+
* 1945 unassigned
|
|
19
|
+
* 1946 parachute-surface UI host (renamed from `app` 2026-05-27)
|
|
20
|
+
* 1947–1949 unassigned
|
|
15
21
|
*
|
|
16
22
|
* Hub pins 1939: `parachute expose` composes hub targets as
|
|
17
23
|
* `http://127.0.0.1:1939/` and that URL has to be stable across machines for
|
|
@@ -64,15 +70,27 @@ export interface PortReservation {
|
|
|
64
70
|
export const PORT_RESERVATIONS: readonly PortReservation[] = [
|
|
65
71
|
{ port: 1939, name: "parachute-hub", status: "assigned" },
|
|
66
72
|
{ port: 1940, name: "parachute-vault", status: "assigned" },
|
|
67
|
-
{ port: 1941, name: "
|
|
73
|
+
{ port: 1941, name: "unassigned", status: "reserved" },
|
|
68
74
|
{ port: 1942, name: "parachute-notes", status: "assigned" },
|
|
69
75
|
{ port: 1943, name: "parachute-scribe", status: "assigned" },
|
|
70
|
-
|
|
76
|
+
// hub-parity P5 (2026-07-11): parachute-app's canonical slot — the NEW
|
|
77
|
+
// super-surface front door (@openparachute/parachute-app), landing as a
|
|
78
|
+
// FIRST_PARTY_FALLBACKS entry (short "app") with its own hub-side static-
|
|
79
|
+
// serve shim (notes-serve.ts --package). This is a DIFFERENT, unrelated
|
|
80
|
+
// module from the pre-2026-05-27 `app` package described in the 1946
|
|
81
|
+
// comment below — the two just happen to share the name across a rename.
|
|
82
|
+
// Status `assigned` keeps the fallback-port walker (`assignPort` in
|
|
83
|
+
// port-assign.ts) from handing this port to a colliding third-party
|
|
84
|
+
// module.
|
|
85
|
+
{ port: 1944, name: "parachute-app", status: "assigned" },
|
|
71
86
|
{ port: 1945, name: "unassigned", status: "reserved" },
|
|
72
|
-
// hub#323: parachute-app's canonical slot
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
87
|
+
// hub#323 (historical): this WAS `parachute-app`'s canonical slot back
|
|
88
|
+
// when "app" meant the UI-host module. That module renamed to
|
|
89
|
+
// `parachute-surface` 2026-05-27 (patterns#102); port 1944 above is the
|
|
90
|
+
// different, new parachute-app (super-surface) from hub-parity P5. Status
|
|
91
|
+
// `assigned` keeps the fallback-port walker from handing 1946 to a
|
|
92
|
+
// colliding third-party module. The matching KNOWN_MODULES row carries
|
|
93
|
+
// the canonicalPort + paths for status/expose surfaces.
|
|
76
94
|
{ port: 1946, name: "parachute-surface", status: "assigned" },
|
|
77
95
|
{ port: 1947, name: "unassigned", status: "reserved" },
|
|
78
96
|
{ port: 1948, name: "unassigned", status: "reserved" },
|
|
@@ -278,9 +296,15 @@ export function composeServiceSpec(opts: {
|
|
|
278
296
|
// — its startCmd is composed from the services.json entry's port + mount,
|
|
279
297
|
// which is hub-side logic, not something notes itself runs. (The archive
|
|
280
298
|
// isn't done — notes-daemon Phase 3 retirement hasn't landed.)
|
|
299
|
+
// - app: the NEW super-surface front door (@openparachute/parachute-app,
|
|
300
|
+
// hub-parity P5, 2026-07-11) — same shape as notes: a frontend bundle
|
|
301
|
+
// with no server of its own, served by the SAME hub-side shim
|
|
302
|
+
// (`notes-serve.ts --package @openparachute/parachute-app`). Unrelated
|
|
303
|
+
// to the pre-2026-05-27 `app` package that renamed to `surface` (see
|
|
304
|
+
// the KNOWN_MODULES.surface tagline + the RETIRED_MODULES note below).
|
|
281
305
|
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
306
|
+
// Each entry keeps its "FALLBACK: Delete when …" marker so the next cleanup
|
|
307
|
+
// pass is a one-grep operation.
|
|
284
308
|
// ---------------------------------------------------------------------------
|
|
285
309
|
|
|
286
310
|
// FALLBACK: Delete when @openparachute/notes ships .parachute/module.json AND
|
|
@@ -317,11 +341,58 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
317
341
|
},
|
|
318
342
|
};
|
|
319
343
|
|
|
344
|
+
// FALLBACK: Delete when @openparachute/parachute-app ships
|
|
345
|
+
// .parachute/module.json AND self-registers its services.json row at boot.
|
|
346
|
+
// As of hub-parity P5 the app repo doesn't carry the real module.json yet —
|
|
347
|
+
// only a stale `dist/.parachute/info` artifact (name: "parachute-notes")
|
|
348
|
+
// left over from seeding the app off notes-ui, which is a DIFFERENT file
|
|
349
|
+
// (module-manifest.ts's runtime `/.parachute/info`, not the static
|
|
350
|
+
// `.parachute/module.json` contract this fallback stands in for) and
|
|
351
|
+
// doesn't block this fallback path. parachute-app is a frontend bundle (the
|
|
352
|
+
// super-surface front door) with no server of its own, so — same as notes —
|
|
353
|
+
// its startCmd is hub-side logic composed from the services.json entry
|
|
354
|
+
// (port + mount), running through the SAME `notes-serve.ts` shim
|
|
355
|
+
// generalized in hub-parity P5 via `--package`.
|
|
356
|
+
const APP_FALLBACK: FirstPartyFallback = {
|
|
357
|
+
package: "@openparachute/parachute-app",
|
|
358
|
+
manifest: {
|
|
359
|
+
name: "app",
|
|
360
|
+
manifestName: "parachute-app",
|
|
361
|
+
displayName: "Parachute",
|
|
362
|
+
tagline: "The Parachute app — your parachute's front door.",
|
|
363
|
+
port: 1944,
|
|
364
|
+
paths: ["/app"],
|
|
365
|
+
health: "/app/health",
|
|
366
|
+
},
|
|
367
|
+
extras: {
|
|
368
|
+
startCmd: (entry) => {
|
|
369
|
+
const first = entry.paths[0] ?? "/app";
|
|
370
|
+
const mount = first === "/" ? "" : first.replace(/\/+$/, "");
|
|
371
|
+
return [
|
|
372
|
+
"bun",
|
|
373
|
+
NOTES_SERVE_PATH,
|
|
374
|
+
"--port",
|
|
375
|
+
String(entry.port),
|
|
376
|
+
"--mount",
|
|
377
|
+
mount,
|
|
378
|
+
"--package",
|
|
379
|
+
"@openparachute/parachute-app",
|
|
380
|
+
];
|
|
381
|
+
},
|
|
382
|
+
postInstallFooter: () => [
|
|
383
|
+
"",
|
|
384
|
+
"Open your Parachute at <origin>/app — it signs in with your hub account.",
|
|
385
|
+
"The hub's front page (`/`) now opens the app too, unless you've already",
|
|
386
|
+
"set a custom root redirect (`parachute hub set-root-redirect` or the admin SPA).",
|
|
387
|
+
],
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
320
391
|
/**
|
|
321
392
|
* Vendored manifests + extras for first-party modules that still need them.
|
|
322
393
|
* Indexed by short name (the `parachute install <X>` token).
|
|
323
394
|
*
|
|
324
|
-
*
|
|
395
|
+
* notes + app remain — see the block comment above for the rationale
|
|
325
396
|
* (vault/scribe/agent now self-register and ship their own
|
|
326
397
|
* module.json). Other code paths consult both this table AND `KNOWN_MODULES`
|
|
327
398
|
* (which carries the post-self-register-retirement entries) via the helpers
|
|
@@ -329,6 +400,7 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
329
400
|
*/
|
|
330
401
|
export const FIRST_PARTY_FALLBACKS: Record<string, FirstPartyFallback> = {
|
|
331
402
|
notes: NOTES_FALLBACK,
|
|
403
|
+
app: APP_FALLBACK,
|
|
332
404
|
};
|
|
333
405
|
|
|
334
406
|
/**
|
|
@@ -438,8 +510,8 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
438
510
|
},
|
|
439
511
|
},
|
|
440
512
|
// NOTE (2026-07-01): `runner` was REMOVED from this registry (decision:
|
|
441
|
-
// Aaron 2026-07-01 — the module set of record is vault / hub /
|
|
442
|
-
//
|
|
513
|
+
// Aaron 2026-07-01 — the module set of record is vault / hub / scribe /
|
|
514
|
+
// surface). Runner is no longer offered, installable, or
|
|
443
515
|
// lifecycle-addressable by short name from the hub's bootstrap registries.
|
|
444
516
|
// Existing installs stay GRACEFUL: a legacy `parachute-runner` services.json
|
|
445
517
|
// row is handled exactly like any unknown/third-party row — `parachute
|
|
@@ -448,29 +520,7 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
448
520
|
// stamped and logs-and-skips otherwise. Deliberately NOT added to
|
|
449
521
|
// RETIRED_MODULES: that registry GC-drops rows on load, which would break
|
|
450
522
|
// routing for operators still running the runner daemon.
|
|
451
|
-
|
|
452
|
-
short: "agent",
|
|
453
|
-
package: "@openparachute/agent",
|
|
454
|
-
manifestName: "parachute-agent",
|
|
455
|
-
canonicalPort: 1941,
|
|
456
|
-
displayName: "Agent",
|
|
457
|
-
// Mirrors agent's own module.json (the canonical fields below do too —
|
|
458
|
-
// keep in sync if agent's declaration changes). Renamed from `channel`
|
|
459
|
-
// 2026-06-17 (parachute-channel → parachute-agent); the module is the
|
|
460
|
-
// same webhook-fan-out + MCP bridge daemon, on the same port 1941.
|
|
461
|
-
tagline: "Chat with your Claude Code sessions — a channel per session.",
|
|
462
|
-
canonicalPaths: ["/agent"],
|
|
463
|
-
canonicalHealth: "/health",
|
|
464
|
-
canonicalStripPrefix: true,
|
|
465
|
-
extras: {
|
|
466
|
-
// Backward-compat startCmd for rows without installDir — same rationale
|
|
467
|
-
// as scribe / vault. The bare binary IS the daemon (agent's
|
|
468
|
-
// package.json bin maps `parachute-agent` → src/daemon.ts).
|
|
469
|
-
startCmd: () => ["parachute-agent"],
|
|
470
|
-
// Agent gates its endpoints behind hub-issued JWTs (agent:* scopes).
|
|
471
|
-
hasAuth: true,
|
|
472
|
-
},
|
|
473
|
-
},
|
|
523
|
+
|
|
474
524
|
surface: {
|
|
475
525
|
short: "surface",
|
|
476
526
|
package: "@openparachute/surface",
|
|
@@ -520,39 +570,43 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
520
570
|
* legacy operators. `notes` (the daemon) is the canonical
|
|
521
571
|
* "deprecating-but-not-retired" case as of 2026-05-22: do not add
|
|
522
572
|
* until its Phase 3 retirement lands.
|
|
523
|
-
* - Entries stay forever
|
|
524
|
-
*
|
|
573
|
+
* - Entries stay forever — UNLESS the retired name is later reclaimed by a
|
|
574
|
+
* genuinely new, unrelated module that needs to self-register under it
|
|
575
|
+
* for real (see the `app` / `parachute-app` un-retirement note below).
|
|
576
|
+
* That's the one case where removal is correct: keeping a retirement
|
|
577
|
+
* entry alongside a live FIRST_PARTY_FALLBACKS/KNOWN_MODULES entry of
|
|
578
|
+
* the SAME name would make `dropRetiredModuleRows` GC the live module's
|
|
579
|
+
* own services.json row on every read — an outright regression, not a
|
|
580
|
+
* conservative default. Ordinary "this module is genuinely gone"
|
|
581
|
+
* entries (no successor reusing the name) still stay forever.
|
|
525
582
|
*/
|
|
526
583
|
export const RETIRED_MODULES: Record<string, { retiredAt: string; replacement?: string }> = {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
//
|
|
546
|
-
//
|
|
547
|
-
//
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
retiredAt: "2026-05-27",
|
|
554
|
-
replacement: "parachute-surface (renamed from parachute-app — `parachute install surface`)",
|
|
555
|
-
},
|
|
584
|
+
agent: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
585
|
+
channel: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
586
|
+
"parachute-agent": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
587
|
+
"parachute-channel": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
588
|
+
// Agent was removed from Hub on 2026-07-15. All historical service-row
|
|
589
|
+
// identities are retired so normal manifest reads stop routing or supervising
|
|
590
|
+
// the deprecated daemon. Generic AI-agent authorization grants/tokens are a
|
|
591
|
+
// separate Hub capability and remain supported.
|
|
592
|
+
// NOTE (2026-07-11, hub-parity P5 — `app` / `parachute-app` UN-RETIRED):
|
|
593
|
+
// this table carried `app` + `parachute-app` (retired 2026-05-27, the
|
|
594
|
+
// app → surface rename, patterns#102) from hub#219 through hub-parity P4.
|
|
595
|
+
// They are REMOVED as of this note: a NEW, unrelated module — the real
|
|
596
|
+
// `@openparachute/parachute-app` super-surface front door (see
|
|
597
|
+
// FIRST_PARTY_FALLBACKS.app + PORT_RESERVATIONS' port-1944 comment) —
|
|
598
|
+
// claims the `parachute-app` manifestName (and the bare `app` short) for
|
|
599
|
+
// real. Keeping these entries would make `dropRetiredModuleRows`
|
|
600
|
+
// unconditionally GC the new module's services.json row on every read,
|
|
601
|
+
// breaking it outright — see the curation-rule exception above. An
|
|
602
|
+
// operator with a genuinely stale PRE-2026-05-27 `app`/`parachute-app` row
|
|
603
|
+
// (from the old, now-defunct @openparachute/app package) is handled the
|
|
604
|
+
// same as any other unrecognized row once un-retired: `parachute status`
|
|
605
|
+
// renders it, and `parachute serve` boots it via installDir's
|
|
606
|
+
// module.json if present and logs-and-skips otherwise — the same
|
|
607
|
+
// graceful fallback the `runner` registry removal (2026-07-01, see
|
|
608
|
+
// KNOWN_MODULES below) relies on. In practice the real parachute-app
|
|
609
|
+
// module's own first boot overwrites any stale row sharing its name.
|
|
556
610
|
};
|
|
557
611
|
|
|
558
612
|
/**
|
|
@@ -632,9 +686,9 @@ export function knownServices(): string[] {
|
|
|
632
686
|
* manifest-declared `focus`; this map is the fallback when `module.json` omits
|
|
633
687
|
* it (and the bootstrap value before a module is installed).
|
|
634
688
|
*
|
|
635
|
-
* - `core` — vault / scribe / hub / surface (the product surface
|
|
636
|
-
*
|
|
637
|
-
*
|
|
689
|
+
* - `core` — vault / scribe / hub / surface / app (the product surface;
|
|
690
|
+
* `app` joined 2026-07-11, hub-parity P5 — the super-surface front door).
|
|
691
|
+
* - `experimental` — any unlisted third-party short.
|
|
638
692
|
* - `deprecated` — notes (notes-daemon deprecated 2026-05-22; notes-ui moved
|
|
639
693
|
* into parachute-surface). Still RESOLVABLE (discoverableShorts unchanged)
|
|
640
694
|
* and SHOWN-IF-INSTALLED so an existing operator can manage/uninstall, but
|
|
@@ -650,7 +704,8 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
650
704
|
scribe: "core",
|
|
651
705
|
hub: "core",
|
|
652
706
|
surface: "core",
|
|
653
|
-
|
|
707
|
+
app: "core",
|
|
708
|
+
|
|
654
709
|
notes: "deprecated",
|
|
655
710
|
};
|
|
656
711
|
|
|
@@ -738,7 +793,7 @@ export function canonicalPortForManifest(manifestName: string): number | undefin
|
|
|
738
793
|
/**
|
|
739
794
|
* Resolve the runtime spec for a known short name.
|
|
740
795
|
*
|
|
741
|
-
* FIRST_PARTY_FALLBACKS shorts (notes) return a fully-composed
|
|
796
|
+
* FIRST_PARTY_FALLBACKS shorts (notes / app) return a fully-composed
|
|
742
797
|
* spec with embedded manifest + extras — the vendored manifest is the
|
|
743
798
|
* source of truth pre-install and the install path preserves it through.
|
|
744
799
|
*
|
|
@@ -847,11 +902,10 @@ export async function getSpecFromInstallDir(
|
|
|
847
902
|
*/
|
|
848
903
|
const LEGACY_MANIFEST_ALIASES: Record<string, string> = {
|
|
849
904
|
"parachute-lens": "notes",
|
|
850
|
-
"parachute-channel": "agent",
|
|
851
905
|
};
|
|
852
906
|
|
|
853
907
|
/** Short name for a given manifest name, e.g. `parachute-vault` → `vault`.
|
|
854
|
-
* Consults both FIRST_PARTY_FALLBACKS (notes) and KNOWN_MODULES
|
|
908
|
+
* Consults both FIRST_PARTY_FALLBACKS (notes / app) and KNOWN_MODULES
|
|
855
909
|
* (vault / scribe / agent / surface — post-FALLBACK-retirement).
|
|
856
910
|
* Returns undefined for unknown manifests. */
|
|
857
911
|
export function shortNameForManifest(manifestName: string): string | undefined {
|