@openparachute/hub 0.7.0 → 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 (93) hide show
  1. package/README.md +13 -14
  2. package/package.json +1 -1
  3. package/src/__tests__/account-setup.test.ts +276 -6
  4. package/src/__tests__/admin-agent-grants.test.ts +1547 -0
  5. package/src/__tests__/{admin-channel-token.test.ts → admin-agent-token.test.ts} +32 -32
  6. package/src/__tests__/admin-connections-credentials.test.ts +1324 -0
  7. package/src/__tests__/admin-connections.test.ts +211 -57
  8. package/src/__tests__/admin-csrf-belt.test.ts +7 -7
  9. package/src/__tests__/admin-lock.test.ts +600 -0
  10. package/src/__tests__/admin-module-token.test.ts +36 -8
  11. package/src/__tests__/admin-vaults.test.ts +8 -8
  12. package/src/__tests__/api-invites.test.ts +166 -6
  13. package/src/__tests__/api-modules-ops.test.ts +17 -16
  14. package/src/__tests__/api-modules.test.ts +35 -36
  15. package/src/__tests__/api-ready.test.ts +2 -2
  16. package/src/__tests__/audience-gate.test.ts +752 -0
  17. package/src/__tests__/clients.test.ts +91 -0
  18. package/src/__tests__/grants-store.test.ts +219 -0
  19. package/src/__tests__/hub-db.test.ts +36 -0
  20. package/src/__tests__/hub-server.test.ts +275 -5
  21. package/src/__tests__/invites.test.ts +64 -1
  22. package/src/__tests__/lifecycle.test.ts +238 -3
  23. package/src/__tests__/migrate.test.ts +1 -1
  24. package/src/__tests__/module-manifest.test.ts +11 -11
  25. package/src/__tests__/oauth-client.test.ts +446 -0
  26. package/src/__tests__/oauth-flows-store.test.ts +141 -0
  27. package/src/__tests__/oauth-handlers.test.ts +124 -26
  28. package/src/__tests__/operator-token.test.ts +2 -2
  29. package/src/__tests__/scope-explanations.test.ts +3 -3
  30. package/src/__tests__/serve-boot.test.ts +14 -14
  31. package/src/__tests__/serve.test.ts +26 -0
  32. package/src/__tests__/service-spec-discovery.test.ts +26 -18
  33. package/src/__tests__/services-manifest.test.ts +60 -48
  34. package/src/__tests__/setup-gate.test.ts +52 -3
  35. package/src/__tests__/setup-wizard.test.ts +86 -280
  36. package/src/__tests__/setup.test.ts +1 -1
  37. package/src/__tests__/upgrade.test.ts +276 -0
  38. package/src/__tests__/vault-remove.test.ts +393 -0
  39. package/src/__tests__/ws-bridge.test.ts +573 -0
  40. package/src/__tests__/ws-connection-caps.test.ts +456 -0
  41. package/src/account-setup.ts +94 -23
  42. package/src/admin-agent-grants.ts +1365 -0
  43. package/src/admin-agent-token.ts +147 -0
  44. package/src/admin-connections.ts +980 -61
  45. package/src/admin-host-admin-token.ts +14 -1
  46. package/src/admin-lock.ts +281 -0
  47. package/src/admin-login-ui.ts +64 -15
  48. package/src/admin-module-token.ts +15 -7
  49. package/src/admin-vault-admin-token.ts +8 -1
  50. package/src/admin-vaults.ts +21 -12
  51. package/src/api-admin-lock.ts +335 -0
  52. package/src/api-invites.ts +92 -12
  53. package/src/api-modules-ops.ts +3 -2
  54. package/src/api-modules.ts +9 -9
  55. package/src/audience-gate.ts +268 -0
  56. package/src/chrome-strip.ts +8 -1
  57. package/src/cli.ts +13 -1
  58. package/src/clients.ts +88 -0
  59. package/src/commands/install.ts +7 -0
  60. package/src/commands/lifecycle.ts +187 -47
  61. package/src/commands/serve-boot.ts +5 -4
  62. package/src/commands/serve.ts +45 -19
  63. package/src/commands/setup.ts +4 -3
  64. package/src/commands/upgrade.ts +118 -2
  65. package/src/commands/vault-remove.ts +361 -0
  66. package/src/commands/wizard.ts +4 -4
  67. package/src/connections-store.ts +35 -5
  68. package/src/grants-store.ts +272 -0
  69. package/src/help.ts +17 -7
  70. package/src/host-admin-token-validation.ts +6 -2
  71. package/src/hub-db.ts +26 -1
  72. package/src/hub-server.ts +710 -39
  73. package/src/hub-settings.ts +23 -8
  74. package/src/invites.ts +69 -2
  75. package/src/jwt-sign.ts +11 -1
  76. package/src/module-manifest.ts +109 -2
  77. package/src/oauth-client.ts +497 -0
  78. package/src/oauth-flows-store.ts +163 -0
  79. package/src/oauth-handlers.ts +40 -13
  80. package/src/operator-token.ts +1 -1
  81. package/src/origin-check.ts +13 -5
  82. package/src/resource-binding.ts +4 -4
  83. package/src/scope-explanations.ts +3 -3
  84. package/src/service-spec.ts +56 -43
  85. package/src/services-manifest.ts +97 -0
  86. package/src/setup-wizard.ts +56 -240
  87. package/src/ws-bridge.ts +256 -0
  88. package/src/ws-connection-caps.ts +170 -0
  89. package/web/ui/dist/assets/index-B5AUE359.js +61 -0
  90. package/web/ui/dist/assets/{index-E_9wqjEm.css → index-DR6R8EFf.css} +1 -1
  91. package/web/ui/dist/index.html +2 -2
  92. package/src/admin-channel-token.ts +0 -135
  93. package/web/ui/dist/assets/index-C-XzMVqN.js +0 -61
@@ -0,0 +1,361 @@
1
+ /**
2
+ * `parachute vault remove <name>` — route the CLI vault-delete verb through the
3
+ * hub's identity cascade instead of the mechanics-only `parachute-vault remove`.
4
+ *
5
+ * ## Why this command exists (B3)
6
+ *
7
+ * The transparent `parachute vault <args>` passthrough (`commands/vault.ts`)
8
+ * forwards `remove <name>` verbatim to `parachute-vault remove` — which only
9
+ * does the MECHANICS of destruction (`rmSync` the vault dir + rewrite the vault
10
+ * module's own config). That path BYPASSES every hub-side identity artifact tied
11
+ * to the vault: live `vault:<name>:*` access tokens stay valid (and reachable
12
+ * via the polled revocation list), `user_vaults` rows linger, grants keep their
13
+ * `vault:<name>:*` entries, unredeemed invites pinned to the vault stay
14
+ * redeemable, and Connections that source/provision the vault keep their
15
+ * long-lived mints. Deleting a vault that way orphans all of it.
16
+ *
17
+ * The hub ALREADY ships the correct path: `DELETE /vaults/<name>`
18
+ * (`admin-vaults.ts:handleDeleteVault`) runs the full 7-step identity cascade
19
+ * (revoke tokens → rewrite grants → drop user_vaults → revoke invites → tear
20
+ * down connections → shell `parachute-vault remove` → restart vault) and is
21
+ * exactly what the vault-admin SPA drives. This command routes the CLI verb
22
+ * through that SAME endpoint over loopback, so `parachute vault remove` and the
23
+ * SPA delete are one code path.
24
+ *
25
+ * ## Credential + transport (reused from module-ops-client.ts)
26
+ *
27
+ * We drive the RUNNING hub over loopback — never open hub.db's vault registry
28
+ * directly (the daemon holds it). The bearer is the on-disk
29
+ * `~/.parachute/operator.token`, read (never minted) via
30
+ * `useOperatorTokenWithAutoRotate`. Its default `admin` scope-set carries
31
+ * `parachute:host:admin` — exactly the scope the endpoint gates on. This is the
32
+ * same read-never-mint credential path `parachute start/stop/restart <svc>` use.
33
+ *
34
+ * ## The 409 guardrail (load-bearing)
35
+ *
36
+ * On a `409 last_vault` the endpoint refuses (deleting the last vault would let
37
+ * vault's boot silently resurrect a fresh `default`). We print the endpoint's
38
+ * message + note the raw escape hatch (`parachute-vault remove <name> --yes`)
39
+ * which SKIPS the cascade, then return NON-ZERO. We MUST NOT fall through to
40
+ * spawning `parachute-vault` ourselves: a "helpful" fall-through would re-open
41
+ * the exact orphaning bug B3 closes. The test locks this invariant.
42
+ */
43
+
44
+ import { CONFIG_DIR } from "../config.ts";
45
+ import { readExposeState } from "../expose-state.ts";
46
+ import { readHubPort } from "../hub-control.ts";
47
+ import { hubDbPath, openHubDb } from "../hub-db.ts";
48
+ import { HUB_UNIT_DEFAULT_PORT } from "../hub-unit.ts";
49
+ import {
50
+ DEFAULT_HUB_BASE_URL,
51
+ ModuleOpHttpError,
52
+ NoOperatorTokenError,
53
+ OperatorTokenExpiredError,
54
+ resolveOperatorBearer,
55
+ } from "../module-ops-client.ts";
56
+
57
+ /**
58
+ * Injectable seams. Production wires the real operator-token bearer resolver +
59
+ * the global `fetch`; tests inject fakes to assert the request shape + lock the
60
+ * 409 guardrail without a live hub or a real socket.
61
+ */
62
+ export interface VaultRemoveDeps {
63
+ /**
64
+ * Resolve the operator-token bearer to present to the loopback hub. Default
65
+ * opens hub.db, reads `~/.parachute/operator.token` (auto-rotating if near
66
+ * expiry), and returns the JWT. Throws {@link NoOperatorTokenError} /
67
+ * {@link OperatorTokenExpiredError} with already-actionable messages.
68
+ */
69
+ readonly resolveBearer?: () => Promise<string>;
70
+ /** fetch seam — `globalThis.fetch` in production; a recorder in tests. */
71
+ readonly fetch?: typeof fetch;
72
+ /** Loopback hub base URL. Defaults to {@link DEFAULT_HUB_BASE_URL}. */
73
+ readonly baseUrl?: string;
74
+ /** Output sink. Defaults to `console.log`. */
75
+ readonly log?: (line: string) => void;
76
+ /** Error sink. Defaults to `console.error`. */
77
+ readonly logError?: (line: string) => void;
78
+ }
79
+
80
+ /** Wire shape of the cascade summary nested under `cascade` in the 200 body. */
81
+ interface CascadeSummaryWire {
82
+ tokens_revoked?: number;
83
+ grants_rewritten?: number;
84
+ grants_dropped?: number;
85
+ user_vaults_removed?: number;
86
+ invites_invalidated?: number;
87
+ connections_torn_down?: number;
88
+ orphaned_channels?: unknown;
89
+ vault_removed?: boolean;
90
+ module_restarted?: boolean;
91
+ }
92
+
93
+ interface DeleteVaultSuccessWire {
94
+ ok?: boolean;
95
+ name?: string;
96
+ cascade?: CascadeSummaryWire;
97
+ warnings?: unknown;
98
+ }
99
+
100
+ /**
101
+ * Resolve the hub origin the operator token's `iss` is validated against —
102
+ * mirrors `lifecycle.ts:resolveOperatorTokenIssuer`. Unlike the spawn-env
103
+ * derivation, the operator token always carries an `iss`, so we fall back to
104
+ * the canonical loopback origin (never `undefined`). The known-issuer SET inside
105
+ * `useOperatorTokenWithAutoRotate` also accepts the public `iss` from
106
+ * expose-state, so a seed of loopback still validates an exposed-origin token.
107
+ */
108
+ function resolveOperatorTokenIssuer(configDir: string): string {
109
+ const state = readExposeState(`${configDir}/expose-state.json`);
110
+ if (state?.hubOrigin) return state.hubOrigin;
111
+ const port = readHubPort(configDir) ?? HUB_UNIT_DEFAULT_PORT;
112
+ return `http://127.0.0.1:${port}`;
113
+ }
114
+
115
+ /**
116
+ * Default bearer resolver: open hub.db, read+auto-rotate the operator token,
117
+ * return the JWT. Closes the db before returning. Read-never-mint — no second
118
+ * SQLite writer racing the running hub.
119
+ */
120
+ async function defaultResolveBearer(configDir: string): Promise<string> {
121
+ const issuer = resolveOperatorTokenIssuer(configDir);
122
+ const db = openHubDb(hubDbPath(configDir));
123
+ try {
124
+ return await resolveOperatorBearer({ db, issuer, configDir });
125
+ } finally {
126
+ db.close();
127
+ }
128
+ }
129
+
130
+ function n(v: number | undefined): number {
131
+ return typeof v === "number" ? v : 0;
132
+ }
133
+
134
+ function asStringArray(v: unknown): string[] {
135
+ return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
136
+ }
137
+
138
+ /**
139
+ * Render the structured cascade summary the endpoint returns — the operator's
140
+ * proof every identity artifact was revoked, not just the vault dir removed.
141
+ */
142
+ function renderCascadeSummary(
143
+ name: string,
144
+ body: DeleteVaultSuccessWire,
145
+ log: (line: string) => void,
146
+ ): void {
147
+ const c = body.cascade ?? {};
148
+ log(`Removed vault "${name}" with the full identity cascade:`);
149
+ log(` tokens revoked: ${n(c.tokens_revoked)}`);
150
+ log(` grants rewritten: ${n(c.grants_rewritten)}`);
151
+ log(` grants dropped: ${n(c.grants_dropped)}`);
152
+ log(` user_vaults removed: ${n(c.user_vaults_removed)}`);
153
+ log(` invites invalidated: ${n(c.invites_invalidated)}`);
154
+ log(` connections torn down: ${n(c.connections_torn_down)}`);
155
+ log(` vault removed: ${c.vault_removed === true ? "yes" : "no"}`);
156
+ log(` vault module restarted:${c.module_restarted === true ? " yes" : " no"}`);
157
+
158
+ const orphaned = asStringArray(c.orphaned_channels);
159
+ if (orphaned.length > 0) {
160
+ log("");
161
+ log(`WARNING: ${orphaned.length} vault-backed agent channel(s) still reference "${name}":`);
162
+ for (const ch of orphaned) log(` - ${ch}`);
163
+ log("Remove them in the agent UI — the hub does not delete the agent's config.");
164
+ }
165
+
166
+ // Top-level warnings[] — each a { step, detail } the cascade recorded
167
+ // (e.g. daemon-restart skipped, a partial connection teardown).
168
+ const warnings = Array.isArray(body.warnings) ? body.warnings : [];
169
+ if (warnings.length > 0) {
170
+ log("");
171
+ log("Warnings:");
172
+ for (const w of warnings) {
173
+ const rec = (w ?? {}) as { step?: unknown; detail?: unknown };
174
+ const step = typeof rec.step === "string" ? rec.step : "warning";
175
+ const detail = typeof rec.detail === "string" ? rec.detail : JSON.stringify(w);
176
+ log(` - [${step}] ${detail}`);
177
+ }
178
+ }
179
+ }
180
+
181
+ const USAGE = "usage: parachute vault remove <name> [--yes] [--hub-origin <url>]";
182
+
183
+ /**
184
+ * Route `parachute vault remove <name>` (and the `rm` alias) through the hub's
185
+ * `DELETE /vaults/<name>` identity cascade. Returns the process exit code.
186
+ */
187
+ export async function vaultRemove(args: string[], deps: VaultRemoveDeps = {}): Promise<number> {
188
+ const log = deps.log ?? ((line: string) => console.log(line));
189
+ const logError = deps.logError ?? ((line: string) => console.error(line));
190
+
191
+ // --- Parse args: positional <name> + flags (--yes/-y, --hub-origin <url>). --
192
+ let name: string | undefined;
193
+ let baseUrlOverride: string | undefined = deps.baseUrl;
194
+ // `--yes`/`-y` is accepted for parity with `parachute-vault remove --yes` and
195
+ // to telegraph non-interactive intent; the endpoint's confirm guard is
196
+ // satisfied from the name arg, so no extra prompt is shown either way. We
197
+ // parse it so it isn't mistaken for a positional, but the behaviour is the
198
+ // same with or without it (this is an admin op that already requires the
199
+ // operator token + a deliberate name retype on the wire).
200
+ for (let i = 0; i < args.length; i++) {
201
+ const a = args[i];
202
+ if (a === "--yes" || a === "-y") continue;
203
+ if (a === "--hub-origin") {
204
+ const v = args[i + 1];
205
+ if (!v) {
206
+ logError("parachute vault remove: --hub-origin requires a URL argument");
207
+ return 1;
208
+ }
209
+ baseUrlOverride = v;
210
+ i++;
211
+ continue;
212
+ }
213
+ if (a?.startsWith("--hub-origin=")) {
214
+ baseUrlOverride = a.slice("--hub-origin=".length);
215
+ continue;
216
+ }
217
+ if (a?.startsWith("-")) {
218
+ logError(`parachute vault remove: unknown flag "${a}"`);
219
+ logError(USAGE);
220
+ return 1;
221
+ }
222
+ if (name === undefined) {
223
+ name = a;
224
+ continue;
225
+ }
226
+ logError(`parachute vault remove: unexpected argument "${a}"`);
227
+ logError(USAGE);
228
+ return 1;
229
+ }
230
+
231
+ if (!name) {
232
+ logError("parachute vault remove: a vault name is required");
233
+ logError(USAGE);
234
+ return 1;
235
+ }
236
+
237
+ // --- Resolve the operator-token bearer (read, never mint). ------------------
238
+ let bearer: string;
239
+ try {
240
+ bearer = deps.resolveBearer
241
+ ? await deps.resolveBearer()
242
+ : await defaultResolveBearer(CONFIG_DIR);
243
+ } catch (err) {
244
+ if (err instanceof NoOperatorTokenError || err instanceof OperatorTokenExpiredError) {
245
+ // Already-actionable ("run `parachute auth rotate-operator`") — surface
246
+ // verbatim, never a raw 401.
247
+ logError(`parachute vault remove: ${err.message}`);
248
+ return 1;
249
+ }
250
+ logError(`parachute vault remove: ${err instanceof Error ? err.message : String(err)}`);
251
+ return 1;
252
+ }
253
+
254
+ // --- DELETE /vaults/<name> with the confirm body. --------------------------
255
+ const doFetch = deps.fetch ?? fetch;
256
+ const baseUrl = (baseUrlOverride ?? DEFAULT_HUB_BASE_URL).replace(/\/+$/, "");
257
+ const url = `${baseUrl}/vaults/${encodeURIComponent(name)}`;
258
+
259
+ let res: Response;
260
+ try {
261
+ res = await doFetch(url, {
262
+ method: "DELETE",
263
+ headers: {
264
+ authorization: `Bearer ${bearer}`,
265
+ "content-type": "application/json",
266
+ },
267
+ body: JSON.stringify({ confirm: name }),
268
+ });
269
+ } catch (err) {
270
+ // Loopback connection refused → the hub isn't running. The cascade needs
271
+ // the live hub; there's no DB-side fallback we'd take (that's the bug).
272
+ const msg = err instanceof Error ? err.message : String(err);
273
+ const refused = /econnrefused|connection refused|failed to fetch|unable to connect/i.test(msg);
274
+ if (refused) {
275
+ logError(
276
+ "parachute vault remove: the hub must be running to delete a vault with the identity cascade.",
277
+ );
278
+ logError("Run `parachute start`, then retry.");
279
+ logError(
280
+ `(The raw escape hatch \`parachute-vault remove ${name} --yes\` deletes the vault dir but SKIPS the cascade, leaving orphaned tokens.)`,
281
+ );
282
+ return 1;
283
+ }
284
+ logError(`parachute vault remove: request failed: ${msg}`);
285
+ return 1;
286
+ }
287
+
288
+ const body = await parseJsonSafe(res);
289
+
290
+ // --- Success: render the cascade summary. ----------------------------------
291
+ if (res.status === 200) {
292
+ renderCascadeSummary(name, (body ?? {}) as DeleteVaultSuccessWire, log);
293
+ return 0;
294
+ }
295
+
296
+ // --- Error mapping (actionable, never a raw status dump). -------------------
297
+ const { error, error_description } = asErrorBody(body, res.status);
298
+
299
+ if (res.status === 404 && error === "not_found") {
300
+ // Idempotent: a re-run after a successful delete lands here. Not scary.
301
+ log(`Vault "${name}" does not exist on this hub (already removed). Nothing to do.`);
302
+ return 0;
303
+ }
304
+
305
+ if (res.status === 409 && error === "last_vault") {
306
+ // CRITICAL GUARDRAIL: print + exit non-zero. Do NOT fall through to spawning
307
+ // `parachute-vault` — that would re-open the orphaned-identity bug B3 closes.
308
+ logError(`parachute vault remove: ${error_description}`);
309
+ logError("");
310
+ logError(
311
+ `The raw mechanics-only path \`parachute-vault remove ${name} --yes\` can delete the last vault,`,
312
+ );
313
+ logError(
314
+ "but it SKIPS the identity cascade — live tokens, grants, and user_vaults rows for that",
315
+ );
316
+ logError("vault would be left orphaned. Create another vault first if you can.");
317
+ return 1;
318
+ }
319
+
320
+ if (res.status === 400 && error === "confirm_mismatch") {
321
+ // Pass the hub's confirm message through.
322
+ logError(`parachute vault remove: ${error_description}`);
323
+ return 1;
324
+ }
325
+
326
+ if (res.status === 401 || res.status === 403) {
327
+ // The operator token was rejected by the hub — guide to re-mint rather than
328
+ // dumping the raw status (matches module-ops-client's posture).
329
+ logError(`parachute vault remove: the hub rejected the operator token (${error_description}).`);
330
+ logError("Run `parachute auth rotate-operator` to mint a fresh one, then retry.");
331
+ return 1;
332
+ }
333
+
334
+ // Any other non-2xx — name the failure class without a raw dump.
335
+ logError(`parachute vault remove: ${error}: ${error_description}`);
336
+ return 1;
337
+ }
338
+
339
+ async function parseJsonSafe(res: Response): Promise<unknown> {
340
+ try {
341
+ return await res.json();
342
+ } catch {
343
+ return undefined;
344
+ }
345
+ }
346
+
347
+ function asErrorBody(body: unknown, status: number): { error: string; error_description: string } {
348
+ const fallback = `hub returned HTTP ${status} with no error detail`;
349
+ if (body && typeof body === "object") {
350
+ const b = body as Record<string, unknown>;
351
+ const error = typeof b.error === "string" ? b.error : "error";
352
+ const error_description =
353
+ typeof b.error_description === "string" ? b.error_description : fallback;
354
+ return { error, error_description };
355
+ }
356
+ return { error: "error", error_description: fallback };
357
+ }
358
+
359
+ // Re-export so the test (and future CLI callers) can catch the typed errors
360
+ // without a second import path.
361
+ export { ModuleOpHttpError, NoOperatorTokenError, OperatorTokenExpiredError };
@@ -749,10 +749,10 @@ export async function runCliWizard(opts: RunCliWizardOpts): Promise<number> {
749
749
  state = await fetchWizardState(hubUrl, jar, fetchImpl);
750
750
  }
751
751
  // Done screen — fetch + show a brief summary. The browser wizard's
752
- // done screen surfaces the MCP install command + auto-minted token;
753
- // we mirror that by reading the same `setup_minted_token` row via
754
- // the GET endpoint's JSON envelope. Best-effort: a missing token
755
- // just means we point the operator at /admin/tokens instead.
752
+ // done screen surfaces the bare OAuth `claude mcp add` command (no
753
+ // token, no header vault is OAuth-default per parachute-vault #491);
754
+ // the CLI path never minted a token, so we just point the operator at
755
+ // the admin SPA + /admin/tokens for the headless-client case.
756
756
  log("");
757
757
  log("✓ Setup complete.");
758
758
  log(` Visit ${hubUrl}/admin/ to open the admin SPA.`);
@@ -35,7 +35,7 @@ export interface ConnectionSource {
35
35
 
36
36
  /** The sink side — an action a module accepts (the sink is ALWAYS an action). */
37
37
  export interface ConnectionSink {
38
- /** Sink module short name, e.g. `channel`. */
38
+ /** Sink module short name, e.g. `agent`. */
39
39
  readonly module: string;
40
40
  /** Action key declared in the sink module's `module.json`, e.g. `message.deliver`. */
41
41
  readonly action: string;
@@ -45,12 +45,26 @@ export interface ConnectionSink {
45
45
 
46
46
  /** What the provisioning engine actually wired, for teardown + display. */
47
47
  export interface ConnectionProvisioned {
48
- /** How the action was provisioned, e.g. `vault-trigger`. */
48
+ /** How the action was provisioned, e.g. `vault-trigger` or `credential`. */
49
49
  readonly type: string;
50
- /** The vault instance the trigger was registered on (vault-trigger). */
50
+ /** The vault instance the trigger was registered on (vault-trigger), or
51
+ * the vault a credential connection grants access to (credential). Either
52
+ * way it's the field the vault-delete cascade matches on. */
51
53
  readonly vault?: string;
52
54
  /** The exact vault trigger name registered — DELETE removes this. */
53
55
  readonly triggerName?: string;
56
+ /** Credential connections (H4): the exact scope minted, e.g.
57
+ * `vault:default:read` — renewal re-mints THIS, never request input. */
58
+ readonly scope?: string;
59
+ /** Credential connections (H4): the tag allowlist baked into the minted
60
+ * token's `permissions.scoped_tags`. Empty/absent = vault-wide (read
61
+ * scopes only — writes always carry tags). */
62
+ readonly scopedTags?: readonly string[];
63
+ /** Credential connections (H4): the declared credential key. */
64
+ readonly credentialKey?: string;
65
+ /** Credential connections (H4): the module's daemon-root-relative delivery
66
+ * endpoint — also the best-effort removal-notification target. */
67
+ readonly endpoint?: string;
54
68
  /**
55
69
  * jtis of the LONG-LIVED tokens minted for this connection (the webhook
56
70
  * bearer, and for a channel sink the vault-write reply token). Each is
@@ -66,6 +80,22 @@ export interface ConnectionProvisioned {
66
80
 
67
81
  export interface ConnectionRecord {
68
82
  readonly id: string;
83
+ /**
84
+ * Connection kind discriminator (H4). Absent = the original event→action
85
+ * shape; `"credential"` = a standing tag-scoped vault credential held by a
86
+ * module (the source is the granting vault, the sink is the holding
87
+ * module). Optional for back-compat: pre-H4 records read back undefined.
88
+ */
89
+ readonly kind?: "credential";
90
+ /**
91
+ * Approval state (surface#113 claim/reconcile). Absent = active (every
92
+ * operator-provisioned record, and pre-claim records, read back undefined
93
+ * = active). `"pending"` = a module-initiated CLAIM for a directly-delivered
94
+ * credential, awaiting operator approval in the hub admin Connections view.
95
+ * A pending record grants nothing: renewal refuses it, and only the
96
+ * operator-gated approve endpoint flips it to active.
97
+ */
98
+ readonly status?: "pending";
69
99
  readonly source: ConnectionSource;
70
100
  readonly sink: ConnectionSink;
71
101
  readonly provisioned: ConnectionProvisioned;
@@ -73,8 +103,8 @@ export interface ConnectionRecord {
73
103
  /**
74
104
  * Provenance — WHO requested this connection (modular-UI R2, module-initiated
75
105
  * connections). A module-owned config UI that creates a connection on the
76
- * operator's behalf (e.g. the channel admin page's "link to a vault" flow)
77
- * labels itself here (e.g. `"channel"`); a connection built by hand in the
106
+ * operator's behalf (e.g. the agent module's admin page "link to a vault" flow)
107
+ * labels itself here (e.g. `"agent"`); a connection built by hand in the
78
108
  * hub's own Connections builder is `"custom"`. Lets the operator see which
79
109
  * connections a module initiated vs which they wired themselves. Optional for
80
110
  * back-compat: records written before R2 read back as `undefined`, which the