@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.
Files changed (78) hide show
  1. package/README.md +7 -7
  2. package/package.json +8 -13
  3. package/src/__tests__/account-api.test.ts +798 -0
  4. package/src/__tests__/account-session.test.ts +252 -0
  5. package/src/__tests__/account-token.test.ts +316 -0
  6. package/src/__tests__/admin-connections-credentials.test.ts +41 -0
  7. package/src/__tests__/admin-handlers.test.ts +25 -0
  8. package/src/__tests__/admin-lock.test.ts +3 -14
  9. package/src/__tests__/admin-module-token.test.ts +10 -30
  10. package/src/__tests__/admin-surfaces.test.ts +21 -0
  11. package/src/__tests__/api-hub-upgrade.test.ts +11 -0
  12. package/src/__tests__/api-mint-token.test.ts +25 -0
  13. package/src/__tests__/api-modules-ops.test.ts +34 -29
  14. package/src/__tests__/api-modules.test.ts +50 -58
  15. package/src/__tests__/api-revoke-token.test.ts +23 -0
  16. package/src/__tests__/api-settings-hub-origin.test.ts +12 -0
  17. package/src/__tests__/api-settings-root-redirect.test.ts +12 -0
  18. package/src/__tests__/api-tokens.test.ts +44 -0
  19. package/src/__tests__/audience-gate.test.ts +24 -0
  20. package/src/__tests__/bearer-scheme-casing.test.ts +110 -0
  21. package/src/__tests__/chrome-strip.test.ts +18 -1
  22. package/src/__tests__/doctor.test.ts +10 -17
  23. package/src/__tests__/door-contract-parity.test.ts +46 -0
  24. package/src/__tests__/hub-server.test.ts +37 -0
  25. package/src/__tests__/hub.test.ts +29 -0
  26. package/src/__tests__/install.test.ts +279 -5
  27. package/src/__tests__/migrate.test.ts +3 -1
  28. package/src/__tests__/notes-serve.test.ts +216 -0
  29. package/src/__tests__/oauth-handlers.test.ts +19 -8
  30. package/src/__tests__/operator-token.test.ts +1 -2
  31. package/src/__tests__/port-assign.test.ts +37 -17
  32. package/src/__tests__/scope-explanations.test.ts +22 -2
  33. package/src/__tests__/serve-boot.test.ts +25 -36
  34. package/src/__tests__/service-spec-discovery.test.ts +30 -35
  35. package/src/__tests__/services-manifest.test.ts +372 -132
  36. package/src/__tests__/sessions.test.ts +75 -28
  37. package/src/__tests__/setup-wizard.test.ts +7 -10
  38. package/src/__tests__/setup.test.ts +13 -14
  39. package/src/__tests__/status.test.ts +0 -5
  40. package/src/__tests__/surface-notes-alias.test.ts +296 -0
  41. package/src/account-api.ts +677 -0
  42. package/src/account-session.ts +132 -0
  43. package/src/account-token.ts +200 -0
  44. package/src/admin-connections.ts +4 -2
  45. package/src/admin-lock.ts +1 -2
  46. package/src/admin-module-token.ts +13 -8
  47. package/src/admin-surfaces.ts +2 -1
  48. package/src/api-hub-upgrade.ts +2 -1
  49. package/src/api-invites.ts +19 -0
  50. package/src/api-mint-token.ts +2 -1
  51. package/src/api-modules-ops.ts +2 -1
  52. package/src/api-modules.ts +10 -8
  53. package/src/api-revoke-token.ts +2 -1
  54. package/src/api-settings-hub-origin.ts +2 -1
  55. package/src/api-settings-root-redirect.ts +2 -1
  56. package/src/api-tokens.ts +2 -1
  57. package/src/audience-gate.ts +2 -1
  58. package/src/chrome-strip.ts +16 -4
  59. package/src/commands/install.ts +86 -8
  60. package/src/commands/migrate.ts +5 -1
  61. package/src/commands/setup.ts +9 -6
  62. package/src/help.ts +6 -6
  63. package/src/hub-server.ts +247 -52
  64. package/src/hub-settings.ts +25 -1
  65. package/src/hub.ts +64 -31
  66. package/src/invites.ts +42 -0
  67. package/src/module-ops-client.ts +2 -1
  68. package/src/notes-serve.ts +73 -31
  69. package/src/oauth-handlers.ts +1 -11
  70. package/src/operator-token.ts +0 -1
  71. package/src/origin-check.ts +2 -2
  72. package/src/scope-explanations.ts +35 -5
  73. package/src/service-spec.ts +128 -74
  74. package/src/services-manifest.ts +112 -52
  75. package/src/sessions.ts +66 -30
  76. package/src/surface-notes-alias.ts +126 -0
  77. package/src/__tests__/admin-agent-token.test.ts +0 -173
  78. package/src/admin-agent-token.ts +0 -147
@@ -1,3 +1,4 @@
1
+ import type { Database } from "bun:sqlite";
1
2
  import { existsSync, readFileSync, realpathSync } from "node:fs";
2
3
  import { createConnection } from "node:net";
3
4
  import { dirname, join } from "node:path";
@@ -15,6 +16,8 @@ import {
15
16
  defaultSleep,
16
17
  readHubPort,
17
18
  } from "../hub-control.ts";
19
+ import { hubDbPath, openHubDb } from "../hub-db.ts";
20
+ import { resolveRootRedirectDetailed, setRootRedirect } from "../hub-settings.ts";
18
21
  import { type HubUnitDeps, defaultHubUnitDeps, isHubUnitInstalled } from "../hub-unit.ts";
19
22
  import {
20
23
  type ModuleManifest,
@@ -33,6 +36,7 @@ import {
33
36
  type FirstPartyFallback,
34
37
  KNOWN_MODULES,
35
38
  type KnownModule,
39
+ RETIRED_MODULES,
36
40
  type ServiceSpec,
37
41
  composeServiceSpec,
38
42
  isCanonicalPort,
@@ -126,15 +130,9 @@ export function resolveInstallChannel(opts: {
126
130
  * launch sinks in and `parachute install lens` has stopped appearing
127
131
  * in support threads.
128
132
  *
129
- * `channel → agent` (2026-06-17): parachute-channel was renamed to
130
- * parachute-agent. `parachute install channel` keeps resolving to the
131
- * agent module for one release cycle so operators mid-upgrade aren't
132
- * stranded. Remove alongside the `parachute-channel → agent` legacy
133
- * manifest alias in service-spec.ts.
134
133
  */
135
134
  const SERVICE_ALIASES: Record<string, string> = {
136
135
  lens: "notes",
137
- channel: "agent",
138
136
  };
139
137
 
140
138
  /**
@@ -145,14 +143,41 @@ const SERVICE_ALIASES: Record<string, string> = {
145
143
  * real, non-Parachute package on npm). Install refuses with the message
146
144
  * instead.
147
145
  */
146
+ const AGENT_RETIRED_MESSAGE =
147
+ "parachute-agent was retired from the hub on 2026-07-15. " +
148
+ "Parachute product development is focused on Vault and Surface.";
149
+
148
150
  const RETIRED_INSTALL_SHORTS: Record<string, string> = {
151
+ agent: AGENT_RETIRED_MESSAGE,
152
+ channel:
153
+ "parachute-channel (later parachute-agent) was retired from the hub on 2026-07-15. " +
154
+ "Parachute product development is focused on Vault and Surface.",
149
155
  runner:
150
156
  "parachute-runner was retired from the hub's module registry on 2026-07-01 " +
151
- "(the module set of record is vault, hub, agent, scribe, surface). " +
157
+ "(the module set of record is vault, hub, scribe, surface, app). " +
152
158
  "An existing install keeps running under `parachute serve`; to install it anyway, " +
153
159
  "pass the explicit npm package name (@openparachute/runner) or a local checkout path.",
154
160
  };
155
161
 
162
+ const RETIRED_AGENT_PACKAGES = [
163
+ "@openparachute/agent",
164
+ "@openparachute/parachute-agent",
165
+ "@openparachute/channel",
166
+ "@openparachute/parachute-channel",
167
+ ] as const;
168
+
169
+ function isRetiredAgentPackage(packageName: string): boolean {
170
+ return RETIRED_AGENT_PACKAGES.some(
171
+ (retired) => packageName === retired || packageName.startsWith(`${retired}@`),
172
+ );
173
+ }
174
+
175
+ function isRetiredAgentManifest(manifest: ModuleManifest): boolean {
176
+ if (RETIRED_MODULES[manifest.name] !== undefined) return true;
177
+ if (manifest.manifestName && RETIRED_MODULES[manifest.manifestName] !== undefined) return true;
178
+ return manifest.name === "claw" && manifest.paths[0] === "/claw";
179
+ }
180
+
156
181
  export interface InstallOpts {
157
182
  runner?: Runner;
158
183
  manifestPath?: string;
@@ -408,6 +433,16 @@ export interface InstallOpts {
408
433
  * origin for stable assertions.
409
434
  */
410
435
  wellKnownOrigin?: string;
436
+ /**
437
+ * Test seam for the `app`-only set-if-unset root-redirect write (hub-parity
438
+ * P5). Production opens `openHubDb(hubDbPath(configDir))` fresh and closes
439
+ * it after the write; passing this injects an already-open DB (e.g. a
440
+ * tempdir `hub.db`) so a test never touches the real `~/.parachute/hub.db`.
441
+ * Passing this ALSO opts a tempdir-manifestPath test INTO the write path —
442
+ * mirrors `guidanceCtx`'s discriminant: without it, the production gate
443
+ * (`manifestPath === SERVICES_MANIFEST_PATH`) skips the write entirely.
444
+ */
445
+ rootRedirectDb?: Database;
411
446
  }
412
447
 
413
448
  async function defaultRunner(cmd: readonly string[]): Promise<number> {
@@ -734,12 +769,20 @@ function resolveInstallTarget(
734
769
  log(`✗ ${input} has no readable package.json — can't install as a Parachute module.`);
735
770
  return null;
736
771
  }
772
+ if (isRetiredAgentPackage(packageName)) {
773
+ log(`✗ ${AGENT_RETIRED_MESSAGE}`);
774
+ return null;
775
+ }
737
776
  return { kind: "local-path", absPath: input, packageName };
738
777
  }
739
778
 
740
779
  // Anything else is treated as an npm package (bare or @scope/name). The
741
780
  // module.json contract gates this — third-party packages without a
742
781
  // manifest fail post-install with a clear error, not silently.
782
+ if (isRetiredAgentPackage(input)) {
783
+ log(`✗ ${AGENT_RETIRED_MESSAGE}`);
784
+ return null;
785
+ }
743
786
  return { kind: "npm", packageName: input };
744
787
  }
745
788
 
@@ -962,6 +1005,10 @@ export async function install(input: string, opts: InstallOpts = {}): Promise<nu
962
1005
  log,
963
1006
  });
964
1007
  if (installedManifest === "error") return 1;
1008
+ if (installedManifest && isRetiredAgentManifest(installedManifest)) {
1009
+ log(`✗ ${target.packageName}: ${AGENT_RETIRED_MESSAGE}`);
1010
+ return 1;
1011
+ }
965
1012
 
966
1013
  let manifest: ModuleManifest;
967
1014
  let extras: FirstPartyExtras | undefined;
@@ -969,7 +1016,7 @@ export async function install(input: string, opts: InstallOpts = {}): Promise<nu
969
1016
  manifest = installedManifest ?? target.fallback.manifest;
970
1017
  extras = target.fallback.extras;
971
1018
  } else if (target.kind === "known-module") {
972
- // KNOWN_MODULES shorts (vault / scribe / agent / surface) carry no vendored
1019
+ // KNOWN_MODULES shorts (vault / scribe / surface) carry no vendored
973
1020
  // manifest (hub#310). The module's own `.parachute/module.json` is the
974
1021
  // canonical source. When it's unreadable (legacy installs from before
975
1022
  // module.json shipped, or test fixtures that mock the disk path without
@@ -1351,6 +1398,37 @@ export async function install(input: string, opts: InstallOpts = {}): Promise<nu
1351
1398
  }
1352
1399
  }
1353
1400
 
1401
+ // App-only: default the hub's bare `/` redirect to the app's front door on
1402
+ // first install (hub-parity P5, 2026-07-11) — SET-IF-UNSET ONLY, never
1403
+ // clobbering an operator's existing choice (`parachute hub
1404
+ // set-root-redirect` or the admin SPA PUT both still win on any later run,
1405
+ // and either can change it back). "Unset" means the resolved redirect is
1406
+ // still the built-in `/admin` DEFAULT — i.e. NEITHER the DB row NOR the
1407
+ // `PARACHUTE_HUB_ROOT_REDIRECT` env var (container deploys pin their landing
1408
+ // page there) has set it. Gating on `getRootRedirect(db) === null` would
1409
+ // miss the env tier and silently override an env-configured operator, since
1410
+ // the DB row we'd write wins over env on read (`resolveRootRedirectDetailed`
1411
+ // is DB-first). Gated on the same production-vs-test discriminant the
1412
+ // guidance probe below uses: a test driving install against a tempdir
1413
+ // manifestPath never opens the real `~/.parachute/hub.db` unless it opts in
1414
+ // via `opts.rootRedirectDb`.
1415
+ if (short === "app") {
1416
+ const dbProbeAllowed =
1417
+ opts.rootRedirectDb !== undefined || manifestPath === SERVICES_MANIFEST_PATH;
1418
+ if (dbProbeAllowed) {
1419
+ const db = opts.rootRedirectDb ?? openHubDb(hubDbPath(configDir));
1420
+ try {
1421
+ if (resolveRootRedirectDetailed(db).source === "default") {
1422
+ setRootRedirect(db, "/app/");
1423
+ log("✓ The hub's front page (`/`) now opens the app at /app/.");
1424
+ log(" Change it any time: `parachute hub set-root-redirect <path>` or the admin SPA.");
1425
+ }
1426
+ } finally {
1427
+ if (!opts.rootRedirectDb) db.close();
1428
+ }
1429
+ }
1430
+ }
1431
+
1354
1432
  // Per-service install footer — canonical next-step URLs and configuration
1355
1433
  // hints. Vault prints its own (richer) footer from `parachute-vault init`
1356
1434
  // (PR #166), so the spec leaves vault out and we don't double up here.
@@ -106,7 +106,11 @@ export function safelistEntries(): Set<string> {
106
106
  * list once enough operator cycles have passed to be confident no
107
107
  * install is still on the Lens-era code path.
108
108
  */
109
- export const KNOWN_ARCHIVABLE_DIRS: ReadonlySet<string> = new Set<string>(["lens"]);
109
+ export const KNOWN_ARCHIVABLE_DIRS: ReadonlySet<string> = new Set<string>([
110
+ "lens",
111
+ "agent",
112
+ "channel",
113
+ ]);
110
114
 
111
115
  /**
112
116
  * Known-cruft rules. Each rule names a label (the friendly description in
@@ -110,7 +110,7 @@ function defaultAvailability(): InteractiveAvailability {
110
110
 
111
111
  /**
112
112
  * Survey ALL known first-party shortnames (vault / notes / scribe / agent /
113
- * surface) regardless of tier — `installed` is true when the service
113
+ * surface / app) regardless of tier — `installed` is true when the service
114
114
  * has a row in services.json. The fresh-install OFFER is narrowed downstream
115
115
  * by `isOfferable` (drops already-installed + `deprecated`-tier shorts —
116
116
  * notes); agent (`experimental`) is flagged exploratory in its blurb
@@ -121,7 +121,7 @@ function defaultAvailability(): InteractiveAvailability {
121
121
  * like any unknown row it neither blocks setup nor appears in the offer.
122
122
  *
123
123
  * The full ServiceSpec is only available pre-install for FIRST_PARTY_FALLBACKS
124
- * shorts (notes — it carries a vendored manifest). KNOWN_MODULES shorts
124
+ * shorts (notes / app both carry a vendored manifest). KNOWN_MODULES shorts
125
125
  * (vault / scribe / agent / surface) ship `.parachute/module.json`
126
126
  * and self-register; pre-install we know manifestName + the urlForEntry quirk
127
127
  * from `KNOWN_MODULES[short].extras`, which is all the survey/summary needs.
@@ -175,14 +175,17 @@ export function isOfferable(choice: { short: string; installed: boolean }): bool
175
175
  const BLURBS: Record<string, string> = {
176
176
  vault: "knowledge graph (MCP) — your owner-authenticated note + tag store",
177
177
  surface: "Parachute UI host — auto-installs Notes on first boot (the recommended UI path)",
178
- // `app` is the pre-2026-05-27 name for `surface`; kept for any legacy survey row.
179
- app: "Parachute UI host auto-installs Notes on first boot (recommended over notes-daemon)",
178
+ // hub-parity P5 (2026-07-11): `app` is now the NEW super-surface front
179
+ // door (@openparachute/parachute-app)a real FIRST_PARTY_FALLBACKS
180
+ // entry, not a legacy survey row. It is UNRELATED to the pre-2026-05-27
181
+ // `app` package that renamed to `surface` (that blurb lives under the
182
+ // `surface` key above); the two just happen to share the short name
183
+ // across a rename.
184
+ app: "the super-surface front door — one app to sign in, browse your vault, and manage your hub/cloud",
180
185
  // notes is `deprecated` (not offered on a fresh setup) — this blurb only
181
186
  // renders if a legacy install surfaces it in the survey.
182
187
  notes: "Notes PWA — web/mobile UI on top of vault (notes-daemon; superseded by `surface`)",
183
188
  scribe: "audio transcription for dictation + recordings",
184
- agent:
185
- "(exploratory) chat with your Claude Code sessions — a channel per session (renamed from channel)",
186
189
  };
187
190
 
188
191
  function blurbFor(choice: ServiceChoice): string {
package/src/help.ts CHANGED
@@ -362,11 +362,11 @@ Exit codes:
362
362
 
363
363
  Example:
364
364
  $ parachute status
365
- SERVICE PORT VERSION STATE PID UPTIME LATENCY SOURCE
366
- parachute-vault 1940 0.2.4 active 12345 2h 13m 2ms bun-linked → parachute-vault @ 8aa167b
365
+ SERVICE PORT VERSION STATE PID UPTIME LATENCY SOURCE
366
+ parachute-vault 1940 0.2.4 active 12345 2h 13m 2ms bun-linked → parachute-vault @ 8aa167b
367
367
  → http://127.0.0.1:1940/vault/default/mcp
368
- parachute-app 1946 0.2.0 active 12346 2h 12m 3ms npm (0.2.0-rc.4)
369
- → http://127.0.0.1:1946/surface/notes
368
+ parachute-surface 1946 0.2.0 active 12346 2h 12m 3ms npm (0.2.0-rc.4)
369
+ → http://127.0.0.1:1946/surface
370
370
  `;
371
371
  }
372
372
 
@@ -551,8 +551,8 @@ Examples:
551
551
  Module start commands (run by the supervisor under \`serve\`):
552
552
  vault parachute-vault serve
553
553
  scribe parachute-scribe serve
554
- app parachute-app serve
555
- agent parachute-agent
554
+ surface parachute-surface serve
555
+ app bun <cli>/notes-serve.ts --port <configured> --mount <paths[0]> --package @openparachute/parachute-app
556
556
  notes bun <cli>/notes-serve.ts --port <configured> --mount <paths[0]> # back-compat: legacy notes-daemon
557
557
  `;
558
558
  }