@openparachute/hub 0.7.1 → 0.7.2-rc.1

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 (73) hide show
  1. package/README.md +13 -14
  2. package/package.json +1 -1
  3. package/src/__tests__/admin-agent-grants.test.ts +1547 -0
  4. package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
  5. package/src/__tests__/admin-connections-credentials.test.ts +8 -4
  6. package/src/__tests__/admin-connections.test.ts +211 -57
  7. package/src/__tests__/admin-csrf-belt.test.ts +7 -7
  8. package/src/__tests__/admin-lock.test.ts +600 -0
  9. package/src/__tests__/admin-module-token.test.ts +36 -8
  10. package/src/__tests__/admin-vaults.test.ts +8 -8
  11. package/src/__tests__/api-modules-ops.test.ts +17 -16
  12. package/src/__tests__/api-modules.test.ts +35 -36
  13. package/src/__tests__/api-ready.test.ts +2 -2
  14. package/src/__tests__/clients.test.ts +91 -0
  15. package/src/__tests__/grants-store.test.ts +219 -0
  16. package/src/__tests__/hub-server.test.ts +9 -5
  17. package/src/__tests__/migrate.test.ts +1 -1
  18. package/src/__tests__/module-manifest.test.ts +11 -11
  19. package/src/__tests__/oauth-client.test.ts +446 -0
  20. package/src/__tests__/oauth-flows-store.test.ts +141 -0
  21. package/src/__tests__/oauth-handlers.test.ts +124 -26
  22. package/src/__tests__/operator-token.test.ts +2 -2
  23. package/src/__tests__/scope-explanations.test.ts +3 -3
  24. package/src/__tests__/serve-boot.test.ts +14 -14
  25. package/src/__tests__/serve.test.ts +26 -0
  26. package/src/__tests__/service-spec-discovery.test.ts +26 -18
  27. package/src/__tests__/services-manifest.test.ts +60 -48
  28. package/src/__tests__/setup-gate.test.ts +52 -3
  29. package/src/__tests__/setup-wizard.test.ts +86 -280
  30. package/src/__tests__/setup.test.ts +1 -1
  31. package/src/__tests__/upgrade.test.ts +276 -0
  32. package/src/__tests__/vault-remove.test.ts +393 -0
  33. package/src/admin-agent-grants.ts +1365 -0
  34. package/src/admin-agent-token.ts +147 -0
  35. package/src/admin-connections.ts +67 -50
  36. package/src/admin-host-admin-token.ts +14 -1
  37. package/src/admin-lock.ts +281 -0
  38. package/src/admin-module-token.ts +15 -7
  39. package/src/admin-vault-admin-token.ts +8 -1
  40. package/src/admin-vaults.ts +12 -12
  41. package/src/api-admin-lock.ts +335 -0
  42. package/src/api-modules-ops.ts +3 -2
  43. package/src/api-modules.ts +9 -9
  44. package/src/cli.ts +13 -1
  45. package/src/clients.ts +88 -0
  46. package/src/commands/install.ts +7 -0
  47. package/src/commands/serve-boot.ts +5 -4
  48. package/src/commands/serve.ts +45 -19
  49. package/src/commands/setup.ts +4 -3
  50. package/src/commands/upgrade.ts +118 -2
  51. package/src/commands/vault-remove.ts +361 -0
  52. package/src/commands/wizard.ts +4 -4
  53. package/src/connections-store.ts +3 -3
  54. package/src/grants-store.ts +272 -0
  55. package/src/help.ts +4 -1
  56. package/src/hub-server.ts +209 -27
  57. package/src/hub-settings.ts +23 -8
  58. package/src/jwt-sign.ts +5 -1
  59. package/src/module-manifest.ts +2 -2
  60. package/src/oauth-client.ts +497 -0
  61. package/src/oauth-flows-store.ts +163 -0
  62. package/src/oauth-handlers.ts +40 -13
  63. package/src/operator-token.ts +1 -1
  64. package/src/origin-check.ts +7 -2
  65. package/src/resource-binding.ts +4 -4
  66. package/src/scope-explanations.ts +3 -3
  67. package/src/service-spec.ts +56 -43
  68. package/src/setup-wizard.ts +56 -240
  69. package/web/ui/dist/assets/index-B5AUE359.js +61 -0
  70. package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
  71. package/web/ui/dist/index.html +2 -2
  72. package/src/admin-channel-token.ts +0 -135
  73. package/web/ui/dist/assets/index-Cxtod68O.js +0 -61
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Tests for the channel UI session→bearer mint endpoint. Mirrors
3
- * `admin-host-admin-token.test.ts` shape (channel has a single bare audience,
2
+ * Tests for the agent UI session→bearer mint endpoint. Mirrors
3
+ * `admin-host-admin-token.test.ts` shape (agent has a single bare audience,
4
4
  * no per-vault name). Covers:
5
5
  * - 401 when no admin session cookie is present.
6
6
  * - 401 when the cookie names a deleted session.
7
7
  * - 405 on POST.
8
- * - 200 + JWT carrying `aud: "channel"` and `channel:read channel:send channel:admin`.
8
+ * - 200 + JWT carrying `aud: "agent"` and `agent:read agent:send agent:admin`.
9
9
  * - First-admin gate: 403 for a signed-in non-first-admin (friend); the
10
10
  * admin's happy path still mints when a friend exists alongside.
11
11
  */
@@ -14,7 +14,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
14
14
  import { mkdtempSync, rmSync } from "node:fs";
15
15
  import { tmpdir } from "node:os";
16
16
  import { join } from "node:path";
17
- import { CHANNEL_TOKEN_TTL_SECONDS, handleChannelToken } from "../admin-channel-token.ts";
17
+ import { AGENT_TOKEN_TTL_SECONDS, handleAgentToken } from "../admin-agent-token.ts";
18
18
  import { hubDbPath, openHubDb } from "../hub-db.ts";
19
19
  import { validateAccessToken } from "../jwt-sign.ts";
20
20
  import { SESSION_TTL_MS, buildSessionCookie, createSession, deleteSession } from "../sessions.ts";
@@ -29,7 +29,7 @@ interface Harness {
29
29
  }
30
30
 
31
31
  function makeHarness(): Harness {
32
- const dir = mkdtempSync(join(tmpdir(), "phub-channel-token-"));
32
+ const dir = mkdtempSync(join(tmpdir(), "phub-agent-token-"));
33
33
  const db = openHubDb(hubDbPath(dir));
34
34
  return {
35
35
  db,
@@ -79,10 +79,10 @@ async function withAdminAndFriend(): Promise<{
79
79
  };
80
80
  }
81
81
 
82
- describe("handleChannelToken", () => {
82
+ describe("handleAgentToken", () => {
83
83
  test("401 when no session cookie is present", async () => {
84
- const req = new Request(`${ISSUER}/admin/channel-token`);
85
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
84
+ const req = new Request(`${ISSUER}/admin/agent-token`);
85
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
86
86
  expect(res.status).toBe(401);
87
87
  const body = (await res.json()) as { error: string };
88
88
  expect(body.error).toBe("unauthenticated");
@@ -92,54 +92,54 @@ describe("handleChannelToken", () => {
92
92
  const { cookie } = await withSession();
93
93
  const sid = cookie.match(/parachute_hub_session=([^;]+)/)?.[1] ?? "";
94
94
  deleteSession(harness.db, sid);
95
- const req = new Request(`${ISSUER}/admin/channel-token`, { headers: { cookie } });
96
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
95
+ const req = new Request(`${ISSUER}/admin/agent-token`, { headers: { cookie } });
96
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
97
97
  expect(res.status).toBe(401);
98
98
  });
99
99
 
100
100
  test("405 on POST", async () => {
101
101
  const { cookie } = await withSession();
102
- const req = new Request(`${ISSUER}/admin/channel-token`, {
102
+ const req = new Request(`${ISSUER}/admin/agent-token`, {
103
103
  method: "POST",
104
104
  headers: { cookie },
105
105
  });
106
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
106
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
107
107
  expect(res.status).toBe(405);
108
108
  });
109
109
 
110
- test("200 mints a JWT carrying aud:channel + channel:read channel:send channel:admin", async () => {
110
+ test("200 mints a JWT carrying aud:agent + agent:read agent:send agent:admin", async () => {
111
111
  const { cookie, userId } = await withSession();
112
112
  rotateSigningKey(harness.db);
113
- const req = new Request(`${ISSUER}/admin/channel-token`, { headers: { cookie } });
114
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
113
+ const req = new Request(`${ISSUER}/admin/agent-token`, { headers: { cookie } });
114
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
115
115
  expect(res.status).toBe(200);
116
116
  expect(res.headers.get("cache-control")).toBe("no-store");
117
117
 
118
118
  const body = (await res.json()) as { token: string; expires_at: string; scopes: string[] };
119
- // `channel:send` (post) + `channel:read` (SSE replies) + `channel:admin`
119
+ // `agent:send` (post) + `agent:read` (SSE replies) + `agent:admin`
120
120
  // (config UI list/edit — 2026-06-09 modular-UI architecture P3). Deliberately
121
- // NOT `channel:write` — that's the session-reply scope a UI token must not hold.
122
- expect(body.scopes).toEqual(["channel:read", "channel:send", "channel:admin"]);
123
- expect(body.scopes).not.toContain("channel:write");
121
+ // NOT `agent:write` — that's the session-reply scope a UI token must not hold.
122
+ expect(body.scopes).toEqual(["agent:read", "agent:send", "agent:admin"]);
123
+ expect(body.scopes).not.toContain("agent:write");
124
124
  expect(body.token.length).toBeGreaterThan(20);
125
125
 
126
126
  const expMs = new Date(body.expires_at).getTime();
127
127
  const skew = expMs - Date.now();
128
- expect(skew).toBeGreaterThan((CHANNEL_TOKEN_TTL_SECONDS - 30) * 1000);
129
- expect(skew).toBeLessThan((CHANNEL_TOKEN_TTL_SECONDS + 30) * 1000);
128
+ expect(skew).toBeGreaterThan((AGENT_TOKEN_TTL_SECONDS - 30) * 1000);
129
+ expect(skew).toBeLessThan((AGENT_TOKEN_TTL_SECONDS + 30) * 1000);
130
130
 
131
131
  const validated = await validateAccessToken(harness.db, body.token, ISSUER);
132
132
  expect(validated.payload.sub).toBe(userId);
133
133
  expect(validated.payload.iss).toBe(ISSUER);
134
- // Bare service audience — channel validates `aud === "channel"`
135
- // (parachute-channel src/hub-jwt.ts CHANNEL_AUDIENCE).
136
- expect(validated.payload.aud).toBe("channel");
134
+ // Bare service audience — agent validates `aud === "agent"`
135
+ // (parachute-agent src/hub-jwt.ts).
136
+ expect(validated.payload.aud).toBe("agent");
137
137
  const scopeClaim = (validated.payload as { scope?: string }).scope ?? "";
138
138
  const scopes = scopeClaim.split(/\s+/);
139
- expect(scopes).toContain("channel:read");
140
- expect(scopes).toContain("channel:send");
141
- expect(scopes).toContain("channel:admin");
142
- expect(scopes).not.toContain("channel:write");
139
+ expect(scopes).toContain("agent:read");
140
+ expect(scopes).toContain("agent:send");
141
+ expect(scopes).toContain("agent:admin");
142
+ expect(scopes).not.toContain("agent:write");
143
143
  });
144
144
 
145
145
  test("403 not_admin when a signed-in non-first-admin (friend) hits the endpoint", async () => {
@@ -148,10 +148,10 @@ describe("handleChannelToken", () => {
148
148
  // first-admin row.
149
149
  const { friendCookie } = await withAdminAndFriend();
150
150
  rotateSigningKey(harness.db);
151
- const req = new Request(`${ISSUER}/admin/channel-token`, {
151
+ const req = new Request(`${ISSUER}/admin/agent-token`, {
152
152
  headers: { cookie: friendCookie },
153
153
  });
154
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
154
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
155
155
  expect(res.status).toBe(403);
156
156
  const body = (await res.json()) as { error: string; error_description: string };
157
157
  expect(body.error).toBe("not_admin");
@@ -161,10 +161,10 @@ describe("handleChannelToken", () => {
161
161
  test("first-admin path still succeeds when a friend exists alongside", async () => {
162
162
  const { adminCookie, adminId } = await withAdminAndFriend();
163
163
  rotateSigningKey(harness.db);
164
- const req = new Request(`${ISSUER}/admin/channel-token`, {
164
+ const req = new Request(`${ISSUER}/admin/agent-token`, {
165
165
  headers: { cookie: adminCookie },
166
166
  });
167
- const res = await handleChannelToken(req, { db: harness.db, issuer: ISSUER });
167
+ const res = await handleAgentToken(req, { db: harness.db, issuer: ISSUER });
168
168
  expect(res.status).toBe(200);
169
169
  const body = (await res.json()) as { token: string };
170
170
  const validated = await validateAccessToken(harness.db, body.token, ISSUER);
@@ -168,7 +168,7 @@ function credDeps(fetchImpl: typeof fetch, modules: InstalledModuleInfo[]): Conn
168
168
  modules,
169
169
  resolveVaultOrigin: (v) => (v === "default" ? VAULT_ORIGIN : null),
170
170
  resolveModuleOrigin: (short) => (short === "surface" ? SURFACE_ORIGIN : null),
171
- channelOrigin: null,
171
+ agentOrigin: null,
172
172
  storePath: harness.storePath,
173
173
  fetchImpl,
174
174
  };
@@ -1121,7 +1121,11 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
1121
1121
  test("CLI-shape claim: vault_scope [] accepted when scope+aud pin the vault (the live surface#113 credentials)", async () => {
1122
1122
  const { fetchImpl } = mockFetch({});
1123
1123
  const deps = credDeps(fetchImpl, modulesOf(SURFACE_MANIFEST));
1124
- const cli = await mintDirectDelivered({ vault: "default", verb: "read", emptyVaultScope: true });
1124
+ const cli = await mintDirectDelivered({
1125
+ vault: "default",
1126
+ verb: "read",
1127
+ emptyVaultScope: true,
1128
+ });
1125
1129
  const res = await handleConnections(
1126
1130
  claimReq(CLAIM_ID, SURFACE_CLAIM, cli.token),
1127
1131
  `/${CLAIM_ID}/claim`,
@@ -1233,7 +1237,7 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
1233
1237
  putConnection(harness.storePath, {
1234
1238
  id: CLAIM_ID,
1235
1239
  source: { module: "vault", vault: "default", event: "note.created" },
1236
- sink: { module: "channel", action: "message.deliver" },
1240
+ sink: { module: "agent", action: "message.deliver" },
1237
1241
  provisioned: { type: "vault-trigger", vault: "default", triggerName: "t", mintedJtis: [] },
1238
1242
  createdAt: new Date().toISOString(),
1239
1243
  });
@@ -1260,7 +1264,7 @@ describe("credential connection — claim/reconcile (surface#113)", () => {
1260
1264
  putConnection(harness.storePath, {
1261
1265
  id: "ev-conn",
1262
1266
  source: { module: "vault", vault: "default", event: "note.created" },
1263
- sink: { module: "channel", action: "message.deliver" },
1267
+ sink: { module: "agent", action: "message.deliver" },
1264
1268
  provisioned: { type: "vault-trigger", vault: "default", triggerName: "t", mintedJtis: [] },
1265
1269
  createdAt: new Date().toISOString(),
1266
1270
  });