@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,147 +0,0 @@
1
- /**
2
- * `GET /admin/agent-token` — exchange a valid admin session cookie for a
3
- * short-lived JWT carrying `agent:read agent:send agent:admin`.
4
- *
5
- * Renamed from `/admin/channel-token` 2026-06-17 (parachute-channel →
6
- * parachute-agent). The old path keeps working via a 301 redirect in
7
- * hub-server.ts for one release cycle.
8
- *
9
- * Why this exists: two agent-owned UIs, both served behind hub's proxy to a
10
- * logged-in portal operator, need a Bearer to talk to agent's API the same
11
- * way the vault-management and scribe-config SPAs do, without running the
12
- * public `/oauth/authorize` flow:
13
- * - The **chat UI** (`/agent/ui`) receives replies over SSE
14
- * (`agent:read`) and posts a message (`agent:send`).
15
- * - The **config/admin UI** (`/agent/admin`, the 2026-06-09 modular-UI
16
- * architecture P3/P4 config surface) lists + edits configured channels via
17
- * `agent:admin`-gated endpoints (`requireScope(SCOPE_ADMIN)` in agent's
18
- * daemon).
19
- *
20
- * Both UIs fetch this single endpoint (`fetchToken()` against
21
- * `/admin/agent-token`), so the minted token carries the union of the scopes
22
- * either UI needs. The chat UI simply ignores the extra `agent:admin` scope;
23
- * `requireScope` checks for the *presence* of a specific scope, so extra
24
- * scopes never break a read/send call. This is what makes the agent config
25
- * UI work without re-touching the agent repo — the hub endpoint the config
26
- * UI already calls now mints the admin scope it needs (2026-06-09 modular-UI
27
- * architecture, P3).
28
- *
29
- * Scope choice — `agent:read agent:send agent:admin`, deliberately NOT
30
- * `agent:write`:
31
- * - `agent:read` — receive replies over SSE.
32
- * - `agent:send` — post a message into the channel.
33
- * - `agent:admin` — list + edit channel config (the config UI).
34
- * - `agent:write` is the *session-reply* scope (a connected Claude Code
35
- * session replying on a channel). A UI token must not be able to
36
- * impersonate a session, so we never mint `agent:write` here.
37
- *
38
- * Audience: `agent` (the bare service prefix). Agent validates the JWT's
39
- * `aud` claim against the literal string `"agent"` (parachute-agent
40
- * `src/hub-jwt.ts`), the same shape `inferAudience` in oauth-handlers.ts
41
- * stamps for the public OAuth flow — so hub-minted and OAuth-minted agent
42
- * tokens are indistinguishable to agent. Unlike the per-vault admin token
43
- * (`vault.<name>`), agent has a single bare audience. (During the rename
44
- * transition window the agent daemon dual-accepts both `aud: "channel"` and
45
- * `aud: "agent"`, so pre-rename tokens keep validating until re-minted — see
46
- * the channel→agent migration doc; that back-compat lives in the daemon, not
47
- * here.)
48
- *
49
- * Multi-user Phase 1 gate: the session must belong to the first admin (the
50
- * single hub admin under the Phase 1 model — see `users.ts:isFirstAdmin`),
51
- * mirroring host-admin-token and vault-admin-token. Friends pinned to a vault
52
- * use the OAuth flow for their assigned scopes; they don't get an agent
53
- * Bearer via this endpoint.
54
- *
55
- * Tokens minted here are short-lived (10 min — matches host/vault admin
56
- * tokens); the UI re-fetches on near-expiry.
57
- */
58
- import type { Database } from "bun:sqlite";
59
- import { lockedResponse, requireUnlocked } from "./admin-lock.ts";
60
- import { signAccessToken } from "./jwt-sign.ts";
61
- import { findSession, parseSessionCookie } from "./sessions.ts";
62
- import { isFirstAdmin } from "./users.ts";
63
-
64
- /** Short TTL — matches host/vault admin-token. UI re-fetches on near-expiry. */
65
- export const AGENT_TOKEN_TTL_SECONDS = 10 * 60;
66
- const AGENT_AUDIENCE = "agent";
67
- const AGENT_CLIENT_ID = "parachute-hub-spa";
68
- /**
69
- * `agent:read` (SSE replies) + `agent:send` (post a message) +
70
- * `agent:admin` (list + edit channel config — the config UI). Deliberately
71
- * NOT `agent:write` — that's the session-reply scope, and a UI token must
72
- * not be able to impersonate a connected session. The chat UI ignores the
73
- * extra `agent:admin`; the config UI needs it (2026-06-09 modular-UI
74
- * architecture, P3 — the hub endpoint the agent config UI already calls
75
- * mints the admin scope so the agent repo doesn't have to change).
76
- */
77
- export const AGENT_TOKEN_SCOPES = ["agent:read", "agent:send", "agent:admin"] as const;
78
-
79
- export interface MintAgentTokenDeps {
80
- db: Database;
81
- /** Hub origin — written into JWT `iss`. */
82
- issuer: string;
83
- }
84
-
85
- export async function handleAgentToken(req: Request, deps: MintAgentTokenDeps): Promise<Response> {
86
- if (req.method !== "GET") {
87
- return jsonError(405, "method_not_allowed", "use GET");
88
- }
89
- const sid = parseSessionCookie(req.headers.get("cookie"));
90
- const session = sid ? findSession(deps.db, sid) : null;
91
- if (!session || !sid) {
92
- return jsonError(401, "unauthenticated", "no admin session — sign in at /login first");
93
- }
94
- // First-admin gate (mirrors host/vault-admin-token). A friend account
95
- // (non-first-admin user created via `/api/users`) holds a valid session but
96
- // must not mint an agent Bearer. Without this check, any signed-in friend
97
- // hitting `GET /admin/agent-token` would walk away with a token carrying
98
- // `agent:read agent:send`.
99
- if (!isFirstAdmin(deps.db, session.userId)) {
100
- return jsonError(
101
- 403,
102
- "not_admin",
103
- "agent token mint is restricted to the hub admin — your account home is at /account/",
104
- );
105
- }
106
- // Admin screen-lock gate (see admin-host-admin-token.ts). A locked admin
107
- // session can't mint an agent Bearer, so agent's config + chat UIs show
108
- // the lock screen / fail closed until the operator unlocks. Off by default.
109
- if (!requireUnlocked(deps.db, sid).ok) {
110
- return lockedResponse();
111
- }
112
- const minted = await signAccessToken(deps.db, {
113
- sub: session.userId,
114
- scopes: [...AGENT_TOKEN_SCOPES],
115
- audience: AGENT_AUDIENCE,
116
- clientId: AGENT_CLIENT_ID,
117
- issuer: deps.issuer,
118
- ttlSeconds: AGENT_TOKEN_TTL_SECONDS,
119
- // Agent tokens carry no per-user vault pin — the UI Bearer talks to an
120
- // agent-scoped endpoint, not to a single vault. Empty `vault_scope` is
121
- // the "no per-user restriction" sentinel matching host-admin tokens.
122
- vaultScope: [],
123
- });
124
- return new Response(
125
- JSON.stringify({
126
- token: minted.token,
127
- expires_at: minted.expiresAt,
128
- scopes: AGENT_TOKEN_SCOPES,
129
- }),
130
- {
131
- status: 200,
132
- headers: {
133
- "content-type": "application/json",
134
- // No browser cache — token rotates per-fetch, and a stale 200 from a
135
- // back/forward navigation could hand the UI a long-expired JWT.
136
- "cache-control": "no-store",
137
- },
138
- },
139
- );
140
- }
141
-
142
- function jsonError(status: number, error: string, description: string): Response {
143
- return new Response(JSON.stringify({ error, error_description: description }), {
144
- status,
145
- headers: { "content-type": "application/json" },
146
- });
147
- }