@openparachute/hub 0.7.6 → 0.7.7-rc.13
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 +159 -4
- 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-command.test.ts +70 -2
- package/src/__tests__/hub-server.test.ts +322 -1
- package/src/__tests__/hub-settings.test.ts +110 -6
- package/src/__tests__/hub.test.ts +29 -0
- package/src/__tests__/install.test.ts +359 -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__/root-serve.test.ts +139 -0
- 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 +97 -20
- package/src/api-tokens.ts +2 -1
- package/src/audience-gate.ts +2 -1
- package/src/chrome-strip.ts +16 -4
- package/src/commands/hub.ts +104 -0
- package/src/commands/install.ts +97 -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 +318 -55
- package/src/hub-settings.ts +162 -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/root-serve.ts +156 -0
- package/src/scope-explanations.ts +35 -5
- package/src/service-spec.ts +129 -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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serving the Parachute app AT the hub's origin root (`root_mode = serve-app`).
|
|
5
|
+
*
|
|
6
|
+
* When an operator flips the root mode to `serve-app` (or a fresh hub installs
|
|
7
|
+
* the app during setup), the hub answers its own origin root with the installed
|
|
8
|
+
* `@openparachute/parachute-app` bundle instead of 302-ing to `/admin`. This is
|
|
9
|
+
* the self-hosted mirror of the hosted door: hit your box's URL, land in the
|
|
10
|
+
* app — no redirect hop.
|
|
11
|
+
*
|
|
12
|
+
* Why this is asset-correct with no rebuild: the app's Vite build is ROOT-based
|
|
13
|
+
* (absolute `/assets/*`, PWA scope `/`, OAuth redirect URIs at the origin root).
|
|
14
|
+
* The `/app` service mount serves the SAME dist through the `notes-serve.ts`
|
|
15
|
+
* shim with a `/app` prefix-strip; here we serve that identical dist verbatim at
|
|
16
|
+
* `/`, which is exactly the origin the build assumes. So `serveAppAtRoot` and
|
|
17
|
+
* the `/app` mount hand back byte-identical bundle files — the only difference
|
|
18
|
+
* is the URL prefix.
|
|
19
|
+
*
|
|
20
|
+
* Precedence is the caller's job (hub-server.ts): the fresh-hub setup wizard
|
|
21
|
+
* funnel and the pre-admin 503 lockout run BEFORE the root `/` handler, and
|
|
22
|
+
* every hub-owned route (/admin, /oauth, /.well-known, /vault, /git, /api,
|
|
23
|
+
* service mounts) dispatches BEFORE the SPA-fallback tail. This module only
|
|
24
|
+
* decides "given an already-unclaimed GET, does the app dist answer it?".
|
|
25
|
+
*
|
|
26
|
+
* NO chrome injection ever rides a root-served response — the app owns its whole
|
|
27
|
+
* page (same posture as the public/surface chrome opt-out). We build the
|
|
28
|
+
* responses here directly rather than routing through `decorateWithChrome`.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { existsSync, statSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { resolveNotesDistFrom } from "./notes-serve.ts";
|
|
34
|
+
|
|
35
|
+
/** The npm package whose built `dist/` is the app front door. */
|
|
36
|
+
export const APP_PACKAGE = "@openparachute/parachute-app";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Namespaces that keep the hub's branded 404 even in serve-app mode. These are
|
|
40
|
+
* hub / protocol surfaces, never app client-side routes, so an SPA shell there
|
|
41
|
+
* would be wrong — and would mask a genuine 404 for an API/OAuth/well-known
|
|
42
|
+
* typo. (Real dist files never live under these prefixes anyway; this makes the
|
|
43
|
+
* "don't shell it" decision explicit rather than incidental.)
|
|
44
|
+
*/
|
|
45
|
+
export const ROOT_SERVE_RESERVED_PREFIXES: readonly string[] = [
|
|
46
|
+
"/api/",
|
|
47
|
+
"/oauth/",
|
|
48
|
+
"/.well-known/",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build a resolver for the installed app's `dist/` directory.
|
|
53
|
+
*
|
|
54
|
+
* Resolution goes through the SAME `resolveNotesDistFrom` machinery the `/app`
|
|
55
|
+
* mount's static-serve shim uses (so root-serve and `/app` resolve to one dist).
|
|
56
|
+
* A SUCCESSFUL resolution is memoized for the process lifetime — a resolved dist
|
|
57
|
+
* path doesn't move while the hub runs, and re-walking `Bun.resolveSync` on every
|
|
58
|
+
* asset request would be needless work. A FAILURE is NOT cached: an operator who
|
|
59
|
+
* set `serve-app` before installing the app, then runs `parachute install app`,
|
|
60
|
+
* is picked up on the next request without a hub restart (dynamic recovery).
|
|
61
|
+
*
|
|
62
|
+
* `resolve` is the test seam (defaults to the real package resolution). Return
|
|
63
|
+
* value is the dist dir, or `null` when the app can't be resolved (not installed
|
|
64
|
+
* / ships no `dist/`).
|
|
65
|
+
*/
|
|
66
|
+
export function makeAppDistResolver(
|
|
67
|
+
resolve: () => string = () => resolveNotesDistFrom({ pkg: APP_PACKAGE }),
|
|
68
|
+
): () => string | null {
|
|
69
|
+
let cached: string | null = null;
|
|
70
|
+
return () => {
|
|
71
|
+
if (cached !== null) return cached;
|
|
72
|
+
try {
|
|
73
|
+
cached = resolve();
|
|
74
|
+
return cached;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** MIME overrides Bun.file doesn't infer (mirrors notes-serve.ts). */
|
|
82
|
+
function mimeFor(path: string): string | undefined {
|
|
83
|
+
// Without this the PWA install prompt sees text/html for the manifest + bails.
|
|
84
|
+
if (path.endsWith(".webmanifest")) return "application/manifest+json";
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Answer an already-unclaimed request from the app's root-based `dist/`, or
|
|
90
|
+
* return `null` to tell the caller to fall through to its normal handling
|
|
91
|
+
* (the `/` 302 fallback, or the branded 404 tail).
|
|
92
|
+
*
|
|
93
|
+
* Contract:
|
|
94
|
+
* - non-GET → null (fall through)
|
|
95
|
+
* - a reserved hub/protocol prefix → null (keep the branded 404)
|
|
96
|
+
* - GET + an existing file under dist → that file (asset-correct MIME)
|
|
97
|
+
* - GET `/` or a trailing-slash "dir" request → dist/index.html (SPA shell)
|
|
98
|
+
* - GET + no file + Accept: text/html → dist/index.html (SPA deep link)
|
|
99
|
+
* - GET + no file + non-HTML (API probe, etc.) → null (branded 404)
|
|
100
|
+
*
|
|
101
|
+
* `dist` is a resolved directory (from `makeAppDistResolver`); if its
|
|
102
|
+
* `index.html` has since vanished (app uninstalled after a successful resolve)
|
|
103
|
+
* we return `null` rather than serving a broken shell.
|
|
104
|
+
*/
|
|
105
|
+
export function serveAppAtRoot(dist: string, req: Request, pathname: string): Response | null {
|
|
106
|
+
// Only GET is served (HEAD/POST/etc. keep the caller's default — a non-GET to
|
|
107
|
+
// an unclaimed path stays a 404, and a non-GET `/` keeps its 302).
|
|
108
|
+
if (req.method !== "GET") return null;
|
|
109
|
+
|
|
110
|
+
// Hub/protocol namespaces are never the app's — leave their 404 branded.
|
|
111
|
+
for (const prefix of ROOT_SERVE_RESERVED_PREFIXES) {
|
|
112
|
+
if (pathname.startsWith(prefix)) return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const indexHtml = join(dist, "index.html");
|
|
116
|
+
// The resolved bundle lost its shell (uninstalled mid-flight) → fall through.
|
|
117
|
+
if (!existsSync(indexHtml)) return null;
|
|
118
|
+
|
|
119
|
+
const spaShell = () =>
|
|
120
|
+
new Response(Bun.file(indexHtml), {
|
|
121
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Bare root or a trailing-slash "directory" request → the SPA shell.
|
|
125
|
+
if (pathname === "/" || pathname.endsWith("/")) return spaShell();
|
|
126
|
+
|
|
127
|
+
// An existing dist file: /assets/*, /icon.svg, /manifest.webmanifest, /sw.js …
|
|
128
|
+
// Malformed percent-encoding (e.g. `/foo%`, `/assets/%ZZ`) makes
|
|
129
|
+
// `decodeURIComponent` throw a URIError. Catch it and fall through to the
|
|
130
|
+
// branded 404 (redirect-mode parity) — WITHOUT the guard the throw escapes to
|
|
131
|
+
// the dispatch outer catch, which classifies a non-DB error as "other" and
|
|
132
|
+
// re-throws → a bare 500. This path is reachable pre-auth on a public expose,
|
|
133
|
+
// so a garbage-encoded asset URL must 404, never 500.
|
|
134
|
+
let decodedPath: string;
|
|
135
|
+
try {
|
|
136
|
+
decodedPath = decodeURIComponent(pathname);
|
|
137
|
+
} catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
// Path-traversal guard: the joined path must stay strictly under dist/.
|
|
141
|
+
const filePath = join(dist, decodedPath);
|
|
142
|
+
if (filePath.startsWith(`${dist}/`) && existsSync(filePath) && statSync(filePath).isFile()) {
|
|
143
|
+
const mime = mimeFor(filePath);
|
|
144
|
+
return new Response(
|
|
145
|
+
Bun.file(filePath),
|
|
146
|
+
mime ? { headers: { "content-type": mime } } : undefined,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// No matching file. Serve the SPA shell ONLY for HTML navigations (a deep link
|
|
151
|
+
// like /some-note the app routes client-side). A browser navigation sends
|
|
152
|
+
// `Accept: text/html`; a non-HTML unclaimed request (API probe, a missing
|
|
153
|
+
// asset fetched with `Accept: */*`) falls through to the branded 404.
|
|
154
|
+
const wantsHtml = (req.headers.get("accept") ?? "").includes("text/html");
|
|
155
|
+
return wantsHtml ? spaShell() : null;
|
|
156
|
+
}
|
|
@@ -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,59 @@ 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 serves the app directly too, unless you've",
|
|
386
|
+
"already customized the root (`parachute hub set-root-mode` / `set-root-redirect`",
|
|
387
|
+
"or the admin SPA).",
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
320
392
|
/**
|
|
321
393
|
* Vendored manifests + extras for first-party modules that still need them.
|
|
322
394
|
* Indexed by short name (the `parachute install <X>` token).
|
|
323
395
|
*
|
|
324
|
-
*
|
|
396
|
+
* notes + app remain — see the block comment above for the rationale
|
|
325
397
|
* (vault/scribe/agent now self-register and ship their own
|
|
326
398
|
* module.json). Other code paths consult both this table AND `KNOWN_MODULES`
|
|
327
399
|
* (which carries the post-self-register-retirement entries) via the helpers
|
|
@@ -329,6 +401,7 @@ const NOTES_FALLBACK: FirstPartyFallback = {
|
|
|
329
401
|
*/
|
|
330
402
|
export const FIRST_PARTY_FALLBACKS: Record<string, FirstPartyFallback> = {
|
|
331
403
|
notes: NOTES_FALLBACK,
|
|
404
|
+
app: APP_FALLBACK,
|
|
332
405
|
};
|
|
333
406
|
|
|
334
407
|
/**
|
|
@@ -438,8 +511,8 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
438
511
|
},
|
|
439
512
|
},
|
|
440
513
|
// 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
|
-
//
|
|
514
|
+
// Aaron 2026-07-01 — the module set of record is vault / hub / scribe /
|
|
515
|
+
// surface). Runner is no longer offered, installable, or
|
|
443
516
|
// lifecycle-addressable by short name from the hub's bootstrap registries.
|
|
444
517
|
// Existing installs stay GRACEFUL: a legacy `parachute-runner` services.json
|
|
445
518
|
// row is handled exactly like any unknown/third-party row — `parachute
|
|
@@ -448,29 +521,7 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
448
521
|
// stamped and logs-and-skips otherwise. Deliberately NOT added to
|
|
449
522
|
// RETIRED_MODULES: that registry GC-drops rows on load, which would break
|
|
450
523
|
// 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
|
-
},
|
|
524
|
+
|
|
474
525
|
surface: {
|
|
475
526
|
short: "surface",
|
|
476
527
|
package: "@openparachute/surface",
|
|
@@ -520,39 +571,43 @@ export const KNOWN_MODULES: Record<string, KnownModule> = {
|
|
|
520
571
|
* legacy operators. `notes` (the daemon) is the canonical
|
|
521
572
|
* "deprecating-but-not-retired" case as of 2026-05-22: do not add
|
|
522
573
|
* until its Phase 3 retirement lands.
|
|
523
|
-
* - Entries stay forever
|
|
524
|
-
*
|
|
574
|
+
* - Entries stay forever — UNLESS the retired name is later reclaimed by a
|
|
575
|
+
* genuinely new, unrelated module that needs to self-register under it
|
|
576
|
+
* for real (see the `app` / `parachute-app` un-retirement note below).
|
|
577
|
+
* That's the one case where removal is correct: keeping a retirement
|
|
578
|
+
* entry alongside a live FIRST_PARTY_FALLBACKS/KNOWN_MODULES entry of
|
|
579
|
+
* the SAME name would make `dropRetiredModuleRows` GC the live module's
|
|
580
|
+
* own services.json row on every read — an outright regression, not a
|
|
581
|
+
* conservative default. Ordinary "this module is genuinely gone"
|
|
582
|
+
* entries (no successor reusing the name) still stay forever.
|
|
525
583
|
*/
|
|
526
584
|
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
|
-
},
|
|
585
|
+
agent: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
586
|
+
channel: { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
587
|
+
"parachute-agent": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
588
|
+
"parachute-channel": { retiredAt: "2026-07-15", replacement: "Vault and Surface" },
|
|
589
|
+
// Agent was removed from Hub on 2026-07-15. All historical service-row
|
|
590
|
+
// identities are retired so normal manifest reads stop routing or supervising
|
|
591
|
+
// the deprecated daemon. Generic AI-agent authorization grants/tokens are a
|
|
592
|
+
// separate Hub capability and remain supported.
|
|
593
|
+
// NOTE (2026-07-11, hub-parity P5 — `app` / `parachute-app` UN-RETIRED):
|
|
594
|
+
// this table carried `app` + `parachute-app` (retired 2026-05-27, the
|
|
595
|
+
// app → surface rename, patterns#102) from hub#219 through hub-parity P4.
|
|
596
|
+
// They are REMOVED as of this note: a NEW, unrelated module — the real
|
|
597
|
+
// `@openparachute/parachute-app` super-surface front door (see
|
|
598
|
+
// FIRST_PARTY_FALLBACKS.app + PORT_RESERVATIONS' port-1944 comment) —
|
|
599
|
+
// claims the `parachute-app` manifestName (and the bare `app` short) for
|
|
600
|
+
// real. Keeping these entries would make `dropRetiredModuleRows`
|
|
601
|
+
// unconditionally GC the new module's services.json row on every read,
|
|
602
|
+
// breaking it outright — see the curation-rule exception above. An
|
|
603
|
+
// operator with a genuinely stale PRE-2026-05-27 `app`/`parachute-app` row
|
|
604
|
+
// (from the old, now-defunct @openparachute/app package) is handled the
|
|
605
|
+
// same as any other unrecognized row once un-retired: `parachute status`
|
|
606
|
+
// renders it, and `parachute serve` boots it via installDir's
|
|
607
|
+
// module.json if present and logs-and-skips otherwise — the same
|
|
608
|
+
// graceful fallback the `runner` registry removal (2026-07-01, see
|
|
609
|
+
// KNOWN_MODULES below) relies on. In practice the real parachute-app
|
|
610
|
+
// module's own first boot overwrites any stale row sharing its name.
|
|
556
611
|
};
|
|
557
612
|
|
|
558
613
|
/**
|
|
@@ -632,9 +687,9 @@ export function knownServices(): string[] {
|
|
|
632
687
|
* manifest-declared `focus`; this map is the fallback when `module.json` omits
|
|
633
688
|
* it (and the bootstrap value before a module is installed).
|
|
634
689
|
*
|
|
635
|
-
* - `core` — vault / scribe / hub / surface (the product surface
|
|
636
|
-
*
|
|
637
|
-
*
|
|
690
|
+
* - `core` — vault / scribe / hub / surface / app (the product surface;
|
|
691
|
+
* `app` joined 2026-07-11, hub-parity P5 — the super-surface front door).
|
|
692
|
+
* - `experimental` — any unlisted third-party short.
|
|
638
693
|
* - `deprecated` — notes (notes-daemon deprecated 2026-05-22; notes-ui moved
|
|
639
694
|
* into parachute-surface). Still RESOLVABLE (discoverableShorts unchanged)
|
|
640
695
|
* and SHOWN-IF-INSTALLED so an existing operator can manage/uninstall, but
|
|
@@ -650,7 +705,8 @@ const FOCUS_DEFAULTS: Record<string, ModuleFocus> = {
|
|
|
650
705
|
scribe: "core",
|
|
651
706
|
hub: "core",
|
|
652
707
|
surface: "core",
|
|
653
|
-
|
|
708
|
+
app: "core",
|
|
709
|
+
|
|
654
710
|
notes: "deprecated",
|
|
655
711
|
};
|
|
656
712
|
|
|
@@ -738,7 +794,7 @@ export function canonicalPortForManifest(manifestName: string): number | undefin
|
|
|
738
794
|
/**
|
|
739
795
|
* Resolve the runtime spec for a known short name.
|
|
740
796
|
*
|
|
741
|
-
* FIRST_PARTY_FALLBACKS shorts (notes) return a fully-composed
|
|
797
|
+
* FIRST_PARTY_FALLBACKS shorts (notes / app) return a fully-composed
|
|
742
798
|
* spec with embedded manifest + extras — the vendored manifest is the
|
|
743
799
|
* source of truth pre-install and the install path preserves it through.
|
|
744
800
|
*
|
|
@@ -847,11 +903,10 @@ export async function getSpecFromInstallDir(
|
|
|
847
903
|
*/
|
|
848
904
|
const LEGACY_MANIFEST_ALIASES: Record<string, string> = {
|
|
849
905
|
"parachute-lens": "notes",
|
|
850
|
-
"parachute-channel": "agent",
|
|
851
906
|
};
|
|
852
907
|
|
|
853
908
|
/** Short name for a given manifest name, e.g. `parachute-vault` → `vault`.
|
|
854
|
-
* Consults both FIRST_PARTY_FALLBACKS (notes) and KNOWN_MODULES
|
|
909
|
+
* Consults both FIRST_PARTY_FALLBACKS (notes / app) and KNOWN_MODULES
|
|
855
910
|
* (vault / scribe / agent / surface — post-FALLBACK-retirement).
|
|
856
911
|
* Returns undefined for unknown manifests. */
|
|
857
912
|
export function shortNameForManifest(manifestName: string): string | undefined {
|