@substrat-run/kernel 0.141.0 → 0.143.0

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 (195) hide show
  1. package/dist/async-invocation-log.d.ts +1 -1
  2. package/dist/async-invocation-log.d.ts.map +1 -1
  3. package/dist/async-invocation-log.js +1 -1
  4. package/dist/async-invocation-log.js.map +1 -1
  5. package/dist/attachment-extractor.d.ts +0 -9
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +2 -2
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/attachment-text.d.ts +0 -5
  10. package/dist/attachment-text.d.ts.map +1 -1
  11. package/dist/attachment-text.js +0 -4
  12. package/dist/attachment-text.js.map +1 -1
  13. package/dist/attribution.d.ts +0 -7
  14. package/dist/attribution.d.ts.map +1 -1
  15. package/dist/attribution.js +0 -7
  16. package/dist/attribution.js.map +1 -1
  17. package/dist/audit-outcome.d.ts +60 -2
  18. package/dist/audit-outcome.d.ts.map +1 -1
  19. package/dist/audit-outcome.js +63 -3
  20. package/dist/audit-outcome.js.map +1 -1
  21. package/dist/capability.d.ts +123 -14
  22. package/dist/capability.d.ts.map +1 -1
  23. package/dist/capability.js +266 -16
  24. package/dist/capability.js.map +1 -1
  25. package/dist/connector-calls.d.ts +3 -77
  26. package/dist/connector-calls.d.ts.map +1 -1
  27. package/dist/connector-calls.js +4 -63
  28. package/dist/connector-calls.js.map +1 -1
  29. package/dist/denial-query.d.ts +0 -14
  30. package/dist/denial-query.d.ts.map +1 -1
  31. package/dist/denial-query.js +1 -1
  32. package/dist/denial-query.js.map +1 -1
  33. package/dist/derived-handlers.d.ts +17 -0
  34. package/dist/derived-handlers.d.ts.map +1 -0
  35. package/dist/derived-handlers.js +106 -0
  36. package/dist/derived-handlers.js.map +1 -0
  37. package/dist/entity-grant-shape.d.ts +79 -7
  38. package/dist/entity-grant-shape.d.ts.map +1 -1
  39. package/dist/entity-grant-shape.js +177 -74
  40. package/dist/entity-grant-shape.js.map +1 -1
  41. package/dist/entity-grant.d.ts +0 -5
  42. package/dist/entity-grant.d.ts.map +1 -1
  43. package/dist/entity-grant.js +0 -4
  44. package/dist/entity-grant.js.map +1 -1
  45. package/dist/entity-state.d.ts +0 -17
  46. package/dist/entity-state.d.ts.map +1 -1
  47. package/dist/entity-state.js +2 -2
  48. package/dist/entity-state.js.map +1 -1
  49. package/dist/entity-trash.d.ts +2 -2
  50. package/dist/entity-trash.d.ts.map +1 -1
  51. package/dist/entity-trash.js +6 -6
  52. package/dist/entity-trash.js.map +1 -1
  53. package/dist/grant-scoped-read.d.ts +0 -2
  54. package/dist/grant-scoped-read.d.ts.map +1 -1
  55. package/dist/grant-scoped-read.js +2 -2
  56. package/dist/grant-scoped-read.js.map +1 -1
  57. package/dist/index.d.ts +58 -139
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +53 -107
  60. package/dist/index.js.map +1 -1
  61. package/dist/invocation-line.d.ts +211 -0
  62. package/dist/invocation-line.d.ts.map +1 -0
  63. package/dist/invocation-line.js +33 -0
  64. package/dist/invocation-line.js.map +1 -0
  65. package/dist/job-run.d.ts +0 -39
  66. package/dist/job-run.d.ts.map +1 -1
  67. package/dist/job-run.js +6 -6
  68. package/dist/job-run.js.map +1 -1
  69. package/dist/list-index.d.ts +0 -45
  70. package/dist/list-index.d.ts.map +1 -1
  71. package/dist/list-index.js +2 -39
  72. package/dist/list-index.js.map +1 -1
  73. package/dist/membership-executor.d.ts +2 -1
  74. package/dist/membership-executor.d.ts.map +1 -1
  75. package/dist/membership-executor.js +1 -1
  76. package/dist/membership-executor.js.map +1 -1
  77. package/dist/meters.d.ts +21 -1
  78. package/dist/meters.d.ts.map +1 -1
  79. package/dist/meters.js +0 -0
  80. package/dist/meters.js.map +1 -1
  81. package/dist/migration-digest.d.ts +0 -12
  82. package/dist/migration-digest.d.ts.map +1 -1
  83. package/dist/migration-digest.js +1 -1
  84. package/dist/migration-digest.js.map +1 -1
  85. package/dist/module-log.d.ts +5 -4
  86. package/dist/module-log.d.ts.map +1 -1
  87. package/dist/module-log.js +5 -4
  88. package/dist/module-log.js.map +1 -1
  89. package/dist/module-operations.d.ts +146 -0
  90. package/dist/module-operations.d.ts.map +1 -0
  91. package/dist/module-operations.js +174 -0
  92. package/dist/module-operations.js.map +1 -0
  93. package/dist/peer.d.ts +0 -2
  94. package/dist/peer.d.ts.map +1 -1
  95. package/dist/peer.js +0 -4
  96. package/dist/peer.js.map +1 -1
  97. package/dist/permission-checker.d.ts +23 -0
  98. package/dist/permission-checker.d.ts.map +1 -1
  99. package/dist/permission-checker.js.map +1 -1
  100. package/dist/permission-eval.d.ts.map +1 -1
  101. package/dist/permission-eval.js +77 -21
  102. package/dist/permission-eval.js.map +1 -1
  103. package/dist/platform-request-query.d.ts +6 -6
  104. package/dist/platform-request-query.d.ts.map +1 -1
  105. package/dist/platform-request-query.js +23 -7
  106. package/dist/platform-request-query.js.map +1 -1
  107. package/dist/platform-sweep.d.ts +107 -3
  108. package/dist/platform-sweep.d.ts.map +1 -1
  109. package/dist/platform-sweep.js +131 -2
  110. package/dist/platform-sweep.js.map +1 -1
  111. package/dist/refusal-query.d.ts +0 -11
  112. package/dist/refusal-query.d.ts.map +1 -1
  113. package/dist/refusal-query.js +1 -1
  114. package/dist/refusal-query.js.map +1 -1
  115. package/dist/refusals.d.ts +1 -3
  116. package/dist/refusals.d.ts.map +1 -1
  117. package/dist/refusals.js +0 -2
  118. package/dist/refusals.js.map +1 -1
  119. package/dist/row-decode.d.ts +16 -1
  120. package/dist/row-decode.d.ts.map +1 -1
  121. package/dist/row-decode.js +18 -6
  122. package/dist/row-decode.js.map +1 -1
  123. package/dist/scope-copy-ledger.d.ts +49 -2
  124. package/dist/scope-copy-ledger.d.ts.map +1 -1
  125. package/dist/scope-copy-ledger.js +44 -1
  126. package/dist/scope-copy-ledger.js.map +1 -1
  127. package/dist/scope-host.d.ts +100 -110
  128. package/dist/scope-host.d.ts.map +1 -1
  129. package/dist/scope-host.js +0 -17
  130. package/dist/scope-host.js.map +1 -1
  131. package/dist/scope-lifecycle.d.ts +0 -21
  132. package/dist/scope-lifecycle.d.ts.map +1 -1
  133. package/dist/scope-lifecycle.js +1 -1
  134. package/dist/scope-lifecycle.js.map +1 -1
  135. package/dist/scope-role-admin.d.ts +0 -6
  136. package/dist/scope-role-admin.d.ts.map +1 -1
  137. package/dist/scope-role-admin.js +1 -1
  138. package/dist/scope-role-admin.js.map +1 -1
  139. package/dist/search-index.d.ts +0 -23
  140. package/dist/search-index.d.ts.map +1 -1
  141. package/dist/search-index.js +2 -24
  142. package/dist/search-index.js.map +1 -1
  143. package/dist/spine-restore.d.ts +0 -22
  144. package/dist/spine-restore.d.ts.map +1 -1
  145. package/dist/spine-restore.js +2 -2
  146. package/dist/spine-restore.js.map +1 -1
  147. package/dist/sql-limits.d.ts +4 -0
  148. package/dist/sql-limits.d.ts.map +1 -1
  149. package/dist/sql-limits.js +4 -0
  150. package/dist/sql-limits.js.map +1 -1
  151. package/dist/storage-gauge.d.ts +125 -0
  152. package/dist/storage-gauge.d.ts.map +1 -0
  153. package/dist/storage-gauge.js +234 -0
  154. package/dist/storage-gauge.js.map +1 -0
  155. package/dist/subject-redaction.d.ts +0 -38
  156. package/dist/subject-redaction.d.ts.map +1 -1
  157. package/dist/subject-redaction.js +4 -4
  158. package/dist/subject-redaction.js.map +1 -1
  159. package/dist/system-switch-record.d.ts +0 -4
  160. package/dist/system-switch-record.d.ts.map +1 -1
  161. package/dist/system-switch-record.js +2 -2
  162. package/dist/system-switch-record.js.map +1 -1
  163. package/dist/system-switch.d.ts +0 -23
  164. package/dist/system-switch.d.ts.map +1 -1
  165. package/dist/system-switch.js +1 -1
  166. package/dist/system-switch.js.map +1 -1
  167. package/dist/table-ownership.d.ts +0 -32
  168. package/dist/table-ownership.d.ts.map +1 -1
  169. package/dist/table-ownership.js +5 -5
  170. package/dist/table-ownership.js.map +1 -1
  171. package/dist/testing.d.ts +17 -0
  172. package/dist/testing.d.ts.map +1 -0
  173. package/dist/testing.js +15 -0
  174. package/dist/testing.js.map +1 -0
  175. package/dist/vertical-events.d.ts +1 -7
  176. package/dist/vertical-events.d.ts.map +1 -1
  177. package/dist/vertical-events.js +2 -10
  178. package/dist/vertical-events.js.map +1 -1
  179. package/package.json +14 -2
  180. package/dist/invocation-log.d.ts +0 -447
  181. package/dist/invocation-log.d.ts.map +0 -1
  182. package/dist/invocation-log.js +0 -410
  183. package/dist/invocation-log.js.map +0 -1
  184. package/dist/platform-call.d.ts +0 -45
  185. package/dist/platform-call.d.ts.map +0 -1
  186. package/dist/platform-call.js +0 -63
  187. package/dist/platform-call.js.map +0 -1
  188. package/dist/provider-error.d.ts +0 -38
  189. package/dist/provider-error.d.ts.map +0 -1
  190. package/dist/provider-error.js +0 -65
  191. package/dist/provider-error.js.map +0 -1
  192. package/dist/routed-node.d.ts +0 -87
  193. package/dist/routed-node.d.ts.map +0 -1
  194. package/dist/routed-node.js +0 -67
  195. package/dist/routed-node.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { type BecomeCapabilityInput, type CapabilityExchange, type CapabilityFilter, type CapabilityId, type CapabilityMintInput, type CapabilityPage, type CapabilityRecord, type CheckSubject, type Decision, type DomainEventInput, type EntityRef, type Instant, type MintedCapability, type Node, type PermissionKey, type PlatformActorId, type PrincipalId } from '@substrat-run/contracts';
2
- import { PermissionDenied } from './permission-checker.js';
1
+ import { type BecomeCapabilityInput, type BecomeLinkState, type BecomeMintRefusal, type BoundedBecomeRevoke, type CapabilityExchange, type CapabilityFilter, type CapabilityId, type CapabilityMintInput, type CapabilityPage, type CapabilityRecord, type CheckSubject, type Decision, type DomainEventInput, type EntityRef, type Instant, type MintedCapability, type Node, type PermissionKey, type PlatformActorId, type PrincipalBecomeCapabilityInput, type PrincipalId } from '@substrat-run/contracts';
2
+ import { PermissionDenied, type Holdings, type PermissionChecker } from './permission-checker.js';
3
3
  import type { ScopedSql, SqlValue } from './scope-host.js';
4
4
  /**
5
5
  * The two spine tables, kernel-owned so no vertical carries a migration for them. Shared by
6
6
  * both adapters' `KERNEL_DDL` (interpolated, like `IDEMPOTENCY_DDL`), so the shape
7
7
  * self-host builds and the shape production builds cannot part company.
8
8
  */
9
- export declare const CAPABILITY_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_capabilities (\n id TEXT PRIMARY KEY,\n -- #1672: SHA-256 hex of the secret. The secret itself is stored NOWHERE \u2014 it leaves\n -- the kernel once, in the mint's return value, and a lookup is this indexed equality.\n token_hash TEXT NOT NULL UNIQUE,\n -- 'act': the holder acts AS the capability ({ capability } on the spine), bounded by\n -- entity + permissions (+ operations + attachments). 'become': exchanging it yields `principal`.\n mode TEXT NOT NULL,\n label TEXT,\n entity_type TEXT,\n entity_id TEXT,\n -- JSON array of permission keys (act). The capability's whole authority: no\n -- `capability:` tuple is ever written, so none can be forged through the tuple table.\n permissions TEXT,\n -- JSON array of operation names, or NULL = any operation the keys allow (act).\n operations TEXT,\n -- NULL = no attachment read opt-in; 'read' explicitly allows attachment readers.\n attachments TEXT,\n principal TEXT,\n -- JSON capabilityAuthor: a principal id (a module minted it, and its authority is\n -- re-checked on every use) or {\"platform\": \u2026} (HostAdmin minted it).\n minted_by TEXT NOT NULL,\n minted_at TEXT NOT NULL,\n expires_at TEXT,\n -- NULL = unlimited. A use is an EXCHANGE of the secret, never an invocation.\n max_uses INTEGER,\n uses INTEGER NOT NULL DEFAULT 0,\n last_used_at TEXT,\n -- Tombstone, never a delete: a capability that once admitted someone is evidence\n -- of why they were admitted (K-4, K-21).\n revoked_at TEXT,\n revoked_by TEXT\n );\n CREATE INDEX IF NOT EXISTS _substrat_capabilities_entity\n ON _substrat_capabilities (entity_type, entity_id);\n -- #1672: what an exchange hands out instead of the secret \u2014 a session token, of which\n -- again only the hash is kept. Each session acts as its capability until the earlier of\n -- its own expiry and the capability's; the capability is re-read on every invoke, so\n -- revoking it ends every session at once \u2014 a revoke touches only the capability row,\n -- and it is that per-invoke read, not a cascade here, that refuses the next call.\n -- Expired rows are pruned on exchange, at most CAPABILITY_SESSION_PRUNE_BATCH at a time,\n -- on the only path that adds one \u2014 so an exchange's write is bounded however many expired.\n CREATE TABLE IF NOT EXISTS _substrat_capability_sessions (\n token_hash TEXT PRIMARY KEY,\n capability_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _substrat_capability_sessions_expiry\n ON _substrat_capability_sessions (expires_at);\n";
9
+ export declare const CAPABILITY_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_capabilities (\n id TEXT PRIMARY KEY,\n -- #1672: SHA-256 hex of the secret. The secret itself is stored NOWHERE \u2014 it leaves\n -- the kernel once, in the mint's return value, and a lookup is this indexed equality.\n token_hash TEXT NOT NULL UNIQUE,\n -- 'act': the holder acts AS the capability ({ capability } on the spine), bounded by\n -- entity + permissions (+ operations + attachments). 'become': exchanging it yields `principal`.\n mode TEXT NOT NULL,\n label TEXT,\n entity_type TEXT,\n entity_id TEXT,\n -- JSON array of permission keys (act). The capability's whole authority: no\n -- `capability:` tuple is ever written, so none can be forged through the tuple table.\n permissions TEXT,\n -- JSON array of operation names, or NULL = any operation the keys allow (act).\n operations TEXT,\n -- NULL = no attachment read opt-in; 'read' explicitly allows attachment readers.\n attachments TEXT,\n principal TEXT,\n -- #1686: a principal-minted 'become' only \u2014 the digest of what `principal` held when it was\n -- minted (`holdingsDigest`). The exchange recomputes it, and a link whose principal's\n -- holdings have changed since is revoked rather than exchanged. NULL on every other row.\n target_digest TEXT,\n -- #1686: why the kernel itself revoked it ('holdings-changed'); NULL for any other revoke.\n revoked_reason TEXT,\n -- JSON capabilityAuthor: a principal id (a module minted it, and its authority is\n -- re-checked on every use) or {\"platform\": \u2026} (HostAdmin minted it).\n minted_by TEXT NOT NULL,\n minted_at TEXT NOT NULL,\n expires_at TEXT,\n -- NULL = unlimited. A use is an EXCHANGE of the secret, never an invocation.\n max_uses INTEGER,\n uses INTEGER NOT NULL DEFAULT 0,\n last_used_at TEXT,\n -- Tombstone, never a delete: a capability that once admitted someone is evidence\n -- of why they were admitted (K-4, K-21).\n revoked_at TEXT,\n revoked_by TEXT\n );\n CREATE INDEX IF NOT EXISTS _substrat_capabilities_entity\n ON _substrat_capabilities (entity_type, entity_id);\n -- #1672: what an exchange hands out instead of the secret \u2014 a session token, of which\n -- again only the hash is kept. Each session acts as its capability until the earlier of\n -- its own expiry and the capability's; the capability is re-read on every invoke, so\n -- revoking it ends every session at once \u2014 a revoke touches only the capability row,\n -- and it is that per-invoke read, not a cascade here, that refuses the next call.\n -- Expired rows are pruned on exchange, at most CAPABILITY_SESSION_PRUNE_BATCH at a time,\n -- on the only path that adds one \u2014 so an exchange's write is bounded however many expired.\n CREATE TABLE IF NOT EXISTS _substrat_capability_sessions (\n token_hash TEXT PRIMARY KEY,\n capability_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _substrat_capability_sessions_expiry\n ON _substrat_capability_sessions (expires_at);\n";
10
10
  /** The tables `CAPABILITY_DDL` builds — what a copy of a scope leaves behind (`capabilitiesForLoad`, #1686). */
11
11
  export declare const CAPABILITY_TABLE_NAMES: readonly ['_substrat_capabilities', '_substrat_capability_sessions'];
12
12
  /** One capability row as the spine stores it — snake_case, because that is what both adapters `SELECT`. */
@@ -28,9 +28,11 @@ export interface CapabilityRow {
28
28
  last_used_at: string | null;
29
29
  revoked_at: string | null;
30
30
  revoked_by: string | null;
31
+ /** #1686: a principal-minted `become`'s holdings digest at mint; NULL elsewhere. */
32
+ target_digest?: string | null;
33
+ /** #1686: why the kernel itself revoked it; NULL otherwise. */
34
+ revoked_reason?: string | null;
31
35
  }
32
- /** Every column a read returns — everything but `token_hash`, which no reader has a use for. */
33
- export declare const CAPABILITY_COLUMNS: string;
34
36
  /** The row the permission checker reads for a capability subject (`ScopeTupleReader.capability`). */
35
37
  export declare function capabilityByIdQuery(id: string): {
36
38
  sql: string;
@@ -55,13 +57,6 @@ export declare function capabilityListQuery(raw?: CapabilityFilter, extra?: numb
55
57
  };
56
58
  /** A read that names no `limit` gets this many records. */
57
59
  export declare const DEFAULT_CAPABILITY_LIMIT = 50;
58
- /**
59
- * The directory read, decoded — the ONE function behind `ctx.capabilities.list`, and (as
60
- * `readCapabilityPage`) `HostAdmin.listCapabilities` on both adapters, so "a reader returns
61
- * records and never a hash" is held in one place: the query selects no `token_hash`, and the
62
- * decode keeps only the record schema's fields.
63
- */
64
- export declare function readCapabilities(sql: ScopedSql, filter?: CapabilityFilter): CapabilityRecord[];
65
60
  /**
66
61
  * One page of the operator's read. Reads one row past the page: `nextCursor` is the last
67
62
  * returned record's id when that extra row exists, and `null` when the walk is complete — so
@@ -98,8 +93,6 @@ export declare function capabilityGrantOf(row: CapabilityRow): CapabilityGrantVi
98
93
  export declare function capabilityRecordOf(row: CapabilityRow): CapabilityRecord;
99
94
  /** A fresh capability secret: 256 bits, base64url, prefixed. Only its hash is ever stored. */
100
95
  export declare function mintCapabilitySecret(): string;
101
- /** A fresh session token — the same construction, its own prefix. */
102
- export declare function mintCapabilitySessionToken(): string;
103
96
  /**
104
97
  * SHA-256 hex of a secret or session token (Web Crypto — the same call in workerd, node and
105
98
  * browsers). Unkeyed on purpose: with 256 bits of entropy there is no dictionary to defend
@@ -221,6 +214,12 @@ export declare function exchangeCapability(deps: {
221
214
  sql: ScopedSql;
222
215
  now: Instant;
223
216
  emit: (capability: CapabilityId, event: DomainEventInput) => void;
217
+ /**
218
+ * What a principal holds now (#1686) — the checker's `holdings` at this scope. A
219
+ * principal-minted `become` is exchanged only while its principal's holdings digest is the
220
+ * one recorded at mint; absent, such a link is refused.
221
+ */
222
+ holdings?: (principal: PrincipalId) => Promise<Holdings>;
224
223
  }, secret: unknown,
225
224
  /**
226
225
  * The only mode the caller can handle. A secret of the OTHER mode answers `null` without
@@ -281,10 +280,120 @@ export declare const plausibleCapabilitySecret: (secret: unknown) => secret is s
281
280
  */
282
281
  export declare function checkBecomeInput(raw: BecomeCapabilityInput, now: Instant): BecomeCapabilityInput;
283
282
  export declare function mintBecomeCapability(sql: ScopedSql, raw: BecomeCapabilityInput, actor: PlatformActorId, now: Instant): Promise<MintedCapability>;
283
+ /**
284
+ * Where each named `become` link stands (#1686), in the order asked — what a pending-invite list
285
+ * shows beside each invite. A capability the scope does not hold reads as `revoked`, never `open`.
286
+ *
287
+ * A principal-minted link that would otherwise read `open` is judged as its exchange would judge
288
+ * it: its principal's holdings are read now (`holdings`) and compared with the digest taken at
289
+ * mint, and a link whose principal changed since reads `revoked` / `holdings-changed` — before
290
+ * anybody has tried it. Nothing is written on a read: the exchange is what revokes it. One with no
291
+ * digest, or with no `holdings` to judge it by, reads `revoked`, as its exchange refuses it.
292
+ */
293
+ export declare function readBecomeLinkStates(sql: ScopedSql, ids: readonly string[], now: Instant, holdings?: (principal: PrincipalId) => Promise<Holdings>): Promise<BecomeLinkState[]>;
294
+ /**
295
+ * `becomeLinkStates`' bound on how many links one call names (`BECOME_LINK_STATES_MAX_IDS`):
296
+ * refused over it, never truncated — a caller asks in pages. Its own function so a host whose
297
+ * storage sits across an RPC boundary checks on the near side, where the typed refusal survives.
298
+ */
299
+ export declare function assertBecomeLinkStateIds(ids: readonly string[]): void;
300
+ /**
301
+ * A canonical digest of what a principal holds (#1686): the role KEYS it holds at the node, as
302
+ * assigned, the permissions granted to it directly at the node, and its entity-narrowed grants —
303
+ * at scope and tenant level, through its orgs — each sorted and deduplicated, so two reads of the
304
+ * same holdings in any order give the same digest. Taken when a principal mints a `become` for it,
305
+ * and again at the exchange: a role assigned or taken away, a grant added or removed, an org joined
306
+ * or left, all change it, and the link dies.
307
+ *
308
+ * Role KEYS, not their expansions, deliberately: a role's definition is the vertical's code,
309
+ * re-projected on a push and reviewed at the permission-diff checkpoint — nothing an inviter
310
+ * controls. Expanding it would revoke every open invite at a role whenever a release adds a key to
311
+ * that role. The price, by design: a REDEFINITION can make the invitee hold more than the minter
312
+ * held when it minted — a release that widens the role widens what the link becomes. That is a
313
+ * change only code or staff make (`defineRole` has no HTTP route), never an inviter.
314
+ *
315
+ * What the digest deliberately does not see, so nobody relies on it as a reach check:
316
+ * - the LEVEL a grant is held at — a key moved between the scope and the tenant node leaves it
317
+ * unchanged, as the mint bound compares the same flattened set (`holdingsOf` merges the levels);
318
+ * - reach that grows under a narrowed grant's entity through `ctx.link` / `ctx.relink` — the
319
+ * minter's coverage of that entity was checked through `check`, whose reach grows the same way;
320
+ * - module authority decided from a principal column (an assignee, an author) rather than a tuple
321
+ * — neither the bound nor the digest sees it.
322
+ * None of these three lets the link yield more than its minter held (a role redefinition can, by
323
+ * design — see above): the identity a link binds is per
324
+ * (scope, sub), so what it becomes is this principal in this scope only. The first two are judged
325
+ * by the same checker, over the same flattened set and the same walk, that bounded the minter;
326
+ * the third is a row the scope itself points at this principal, as it would for any member it
327
+ * names — and a member invite's principal is minted fresh, so no row names it when its link is.
328
+ */
329
+ export declare function holdingsDigest(held: Holdings): Promise<string>;
284
330
  /**
285
331
  * The platform's revoke (`HostAdmin.revokeCapability`) — of any capability in the scope,
286
332
  * the operator's lever for a leaked link. Returns the row as it stood before, for the admin
287
333
  * log, or `undefined` when there is no such capability. Idempotent.
288
334
  */
289
335
  export declare function revokeCapabilityAsPlatform(sql: ScopedSql, rawId: string, actor: PlatformActorId, now: Instant): CapabilityRecord | undefined;
336
+ /** The pseudo-operation a principal's `become` mint names on its spine event. */
337
+ export declare const CAPABILITY_BECOME_MINT_OPERATION = "capabilities.mint-become";
338
+ /**
339
+ * May `caller` mint a `become` capability for `target` at `node`? (#1686) Becoming someone
340
+ * acquires everything they hold, so the bound is the assignment bound (K-21) applied to the
341
+ * whole of it: the caller must already hold, at the node, every permission the target holds
342
+ * there, and must be able to exercise every entity-narrowed grant the target holds on that same
343
+ * entity. Anything less mints impersonation upward — a lead inviting a stranger in as the owner.
344
+ *
345
+ * The bound is evaluated at mint, against what the target holds then; the exchange does not
346
+ * re-check the minter. It compares the target's holdings instead: a link whose principal's
347
+ * holdings have changed since the mint is revoked rather than exchanged (`exchangeCapability`). So a target holding nothing at the node is refused outright: its empty set would
348
+ * cover trivially, and the link would yield whatever that principal is granted later. A member
349
+ * invite never meets this — it grants the role before it mints.
350
+ *
351
+ * It is also refused for a target some `become` capability has already been exchanged into in
352
+ * this scope (`target-already-claimed`): a principal-minted `become` is for a seat nobody has
353
+ * taken, never a second key to someone who already exists. Whether a login is bound to the
354
+ * target outside the scope (an identity directory) is not visible here; the one caller today,
355
+ * a member invite, always targets a principal it has just minted.
356
+ *
357
+ * When the mint may go ahead, the target's `holdingsDigest` — what the mint records, and what the
358
+ * exchange compares against. Otherwise the refusal, as the host answers it — a `Coverage` naming
359
+ * what the caller lacks, as `canAssign`'s does, `target-holds-nothing`, or
360
+ * `target-already-claimed`. A checker with
361
+ * no `holdings` cannot say what the target holds, so the bound refuses rather than guess.
362
+ */
363
+ export declare function becomeMintCheck(deps: {
364
+ sql: ScopedSql;
365
+ checker: Pick<PermissionChecker, 'check' | 'covers' | 'holdings'>;
366
+ }, caller: PrincipalId, target: PrincipalId, node: Node): Promise<BecomeMintRefusal | {
367
+ ok: true;
368
+ targetDigest: string;
369
+ }>;
370
+ /**
371
+ * A principal's `become` mint (#1686) — the write a member invite makes once `becomeMintCheck`
372
+ * has said yes, in the same scope task. Recorded with the principal as its minter, and on the
373
+ * spine as `capability.become-minted` through `emit`, whose actor the host stamps as `minter`.
374
+ * No expiry unless one is given, as an invite has never had one; the use limit is required.
375
+ */
376
+ export declare function mintBecomeCapabilityAsPrincipal(deps: {
377
+ sql: ScopedSql;
378
+ now: Instant;
379
+ emit: (event: DomainEventInput) => void;
380
+ }, minter: PrincipalId, raw: PrincipalBecomeCapabilityInput,
381
+ /** `becomeMintCheck`'s answer: the target's holdings now, which the exchange compares against. */
382
+ targetDigest: string): Promise<MintedCapability>;
383
+ /**
384
+ * Revoke a `become` capability a PRINCIPAL minted (#1686) — what withdrawing a member invite
385
+ * does to its link, recorded with `by` as the revoker. Only such a row: a platform-minted one
386
+ * (an owner claim link) is the platform's to revoke, and an `act` one is `ctx.capabilities`'.
387
+ *
388
+ * Bounded here, not only by the caller: `by` must be the link's minter, or hold everything its
389
+ * principal holds now — whoever could have minted it, as a module's revoke of an `act` link is
390
+ * open to whoever could have minted that. A refusal writes nothing. `revoked` is true when this
391
+ * call revoked it; false when it was already revoked or is not such a row.
392
+ */
393
+ export declare function revokeBecomeCapabilityAsPrincipal(deps: {
394
+ sql: ScopedSql;
395
+ checker: Pick<PermissionChecker, 'check' | 'covers' | 'holdings'>;
396
+ node: Node;
397
+ now: Instant;
398
+ }, rawId: string, by: PrincipalId): Promise<BoundedBecomeRevoke>;
290
399
  //# sourceMappingURL=capability.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { CAPABILITY_EXERCISED, CAPABILITY_MINTED, CAPABILITY_REVOKED, CAPABILITY_SECRET_PREFIX, CAPABILITY_SESSION_PREFIX, CAPABILITY_SESSION_TTL_MS, becomeCapabilityInput, capabilityAuthor, capabilityFilter, capabilityId as capabilityIdSchema, capabilityMintInput, capabilityRecord, permissionKey, principalId as principalIdSchema, substratError, entityObjectRef, } from '@substrat-run/contracts';
1
+ import { BECOME_LINK_STATES_MAX_IDS, CAPABILITY_BECOME_MINTED, CAPABILITY_EXERCISED, CAPABILITY_MINTED, CAPABILITY_REVOKED, CAPABILITY_SECRET_PREFIX, CAPABILITY_SESSION_PREFIX, CAPABILITY_SESSION_TTL_MS, becomeCapabilityInput, capabilityAuthor, capabilityFilter, capabilityId as capabilityIdSchema, capabilityMintInput, capabilityRecord, permissionKey, principalBecomeCapabilityInput, principalId as principalIdSchema, substratError, entityObjectRef, } from '@substrat-run/contracts';
2
2
  import { toBase64url } from './base64url.js';
3
3
  import { PermissionDenied } from './permission-checker.js';
4
4
  import { ulid } from './ulid.js';
@@ -27,6 +27,12 @@ export const CAPABILITY_DDL = `
27
27
  -- NULL = no attachment read opt-in; 'read' explicitly allows attachment readers.
28
28
  attachments TEXT,
29
29
  principal TEXT,
30
+ -- #1686: a principal-minted 'become' only — the digest of what \`principal\` held when it was
31
+ -- minted (\`holdingsDigest\`). The exchange recomputes it, and a link whose principal's
32
+ -- holdings have changed since is revoked rather than exchanged. NULL on every other row.
33
+ target_digest TEXT,
34
+ -- #1686: why the kernel itself revoked it ('holdings-changed'); NULL for any other revoke.
35
+ revoked_reason TEXT,
30
36
  -- JSON capabilityAuthor: a principal id (a module minted it, and its authority is
31
37
  -- re-checked on every use) or {"platform": …} (HostAdmin minted it).
32
38
  minted_by TEXT NOT NULL,
@@ -62,8 +68,8 @@ export const CAPABILITY_DDL = `
62
68
  /** The tables `CAPABILITY_DDL` builds — what a copy of a scope leaves behind (`capabilitiesForLoad`, #1686). */
63
69
  export const CAPABILITY_TABLE_NAMES = ['_substrat_capabilities', '_substrat_capability_sessions'];
64
70
  /** Every column a read returns — everything but `token_hash`, which no reader has a use for. */
65
- export const CAPABILITY_COLUMNS = 'id, mode, label, entity_type, entity_id, permissions, operations, attachments, principal, minted_by, ' +
66
- 'minted_at, expires_at, max_uses, uses, last_used_at, revoked_at, revoked_by';
71
+ const CAPABILITY_COLUMNS = 'id, mode, label, entity_type, entity_id, permissions, operations, attachments, principal, minted_by, ' +
72
+ 'minted_at, expires_at, max_uses, uses, last_used_at, revoked_at, revoked_by, target_digest, revoked_reason';
67
73
  /** The row the permission checker reads for a capability subject (`ScopeTupleReader.capability`). */
68
74
  export function capabilityByIdQuery(id) {
69
75
  return {
@@ -113,7 +119,7 @@ export const DEFAULT_CAPABILITY_LIMIT = 50;
113
119
  * records and never a hash" is held in one place: the query selects no `token_hash`, and the
114
120
  * decode keeps only the record schema's fields.
115
121
  */
116
- export function readCapabilities(sql, filter) {
122
+ function readCapabilities(sql, filter) {
117
123
  const q = capabilityListQuery(filter);
118
124
  return sql.query(q.sql, q.params).map(capabilityRecordOf);
119
125
  }
@@ -174,6 +180,7 @@ export function capabilityRecordOf(row) {
174
180
  lastUsedAt: row.last_used_at,
175
181
  revokedAt: row.revoked_at,
176
182
  revokedBy: row.revoked_by === null ? null : JSON.parse(row.revoked_by),
183
+ revokedReason: row.revoked_reason ?? null,
177
184
  };
178
185
  return capabilityRecord.parse(row.mode === 'become'
179
186
  ? { mode: 'become', ...common, principal: row.principal }
@@ -191,7 +198,7 @@ export function mintCapabilitySecret() {
191
198
  return CAPABILITY_SECRET_PREFIX + toBase64url(crypto.getRandomValues(new Uint8Array(32)));
192
199
  }
193
200
  /** A fresh session token — the same construction, its own prefix. */
194
- export function mintCapabilitySessionToken() {
201
+ function mintCapabilitySessionToken() {
195
202
  return CAPABILITY_SESSION_PREFIX + toBase64url(crypto.getRandomValues(new Uint8Array(32)));
196
203
  }
197
204
  /**
@@ -391,7 +398,11 @@ export function createCapabilityVerbs(deps) {
391
398
  throw substratError('not_found', `no capability ${id} in this scope`);
392
399
  const grant = capabilityGrantOf(row);
393
400
  if (!grant) {
394
- throw substratError('forbidden', `capability ${id} was minted by the platform and is revoked through HostAdmin`, { reason: 'capability_platform_minted' });
401
+ // A `become` row: the platform's (HostAdmin) or a member invite's (the host's
402
+ // principal revoke, #1686) — never module code's.
403
+ throw substratError('forbidden', `capability ${id} is a become capability and is revoked by the host that minted it, not ctx.capabilities`,
404
+ // The reason predates principal-minted `become` rows and is kept: a caller may match on it.
405
+ { reason: 'capability_platform_minted' });
395
406
  }
396
407
  if (row.revoked_at !== null)
397
408
  return;
@@ -402,8 +413,7 @@ export function createCapabilityVerbs(deps) {
402
413
  `${refToString(grant.entity)} (a revoke is open to whoever could have minted it)`);
403
414
  }
404
415
  }
405
- deps.sql.exec(`UPDATE _substrat_capabilities SET revoked_at = ?, revoked_by = ?
406
- WHERE id = ? AND revoked_at IS NULL`, [deps.now, JSON.stringify(revoker), id]);
416
+ markRevoked(deps.sql, id, revoker, deps.now);
407
417
  const payload = {
408
418
  capabilityId: id,
409
419
  entity: grant.entity,
@@ -455,6 +465,19 @@ mode) {
455
465
  return null;
456
466
  if (mode !== undefined && row.mode !== mode)
457
467
  return null;
468
+ if (row.mode === 'become' && typeof JSON.parse(row.minted_by) === 'string') {
469
+ // A principal minted it (#1686): the bound held against what its principal held THEN. If
470
+ // that has changed in any way since — a role raised or lowered, a grant added, an org joined,
471
+ // a role redefined — the link is revoked here, with no revoker, and refused: the one answer
472
+ // a refusal always gets, and nothing written beyond the revoke. Its use is never taken.
473
+ if (!deps.holdings || !row.target_digest)
474
+ return null;
475
+ const held = await deps.holdings(principalIdSchema.parse(row.principal));
476
+ if ((await holdingsDigest(held)) !== row.target_digest) {
477
+ markRevoked(deps.sql, capabilityIdSchema.parse(row.id), null, deps.now, 'holdings-changed');
478
+ return null;
479
+ }
480
+ }
458
481
  const taken = deps.sql.exec(`UPDATE _substrat_capabilities SET uses = uses + 1, last_used_at = ?
459
482
  WHERE id = ? AND uses = ?`, [deps.now, row.id, row.uses]);
460
483
  if (taken.changes !== 1)
@@ -591,23 +614,130 @@ export function checkBecomeInput(raw, now) {
591
614
  }
592
615
  export async function mintBecomeCapability(sql, raw, actor, now) {
593
616
  const input = checkBecomeInput(raw, now);
617
+ return insertBecome(sql, input, { platform: actor }, now);
618
+ }
619
+ /**
620
+ * The one `become` row write, for the platform's mint and a principal's alike: a fresh id and
621
+ * secret, the secret's hash stored, the secret returned once.
622
+ */
623
+ async function insertBecome(sql, input, author, now,
624
+ /** A principal's mint only: the target's `holdingsDigest` now. */
625
+ targetDigest = null) {
594
626
  const id = capabilityIdSchema.parse(ulid());
595
627
  const secret = mintCapabilitySecret();
596
- const author = { platform: actor };
597
628
  sql.exec(`INSERT INTO _substrat_capabilities
598
629
  (id, token_hash, mode, label, entity_type, entity_id, permissions, operations, attachments,
599
- principal, minted_by, minted_at, expires_at, max_uses, uses)
600
- VALUES (?, ?, 'become', ?, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, 0)`, [
630
+ principal, minted_by, minted_at, expires_at, max_uses, uses, target_digest)
631
+ VALUES (?, ?, 'become', ?, NULL, NULL, NULL, NULL, NULL, ?, ?, ?, ?, ?, 0, ?)`, [
601
632
  id,
602
633
  await capabilityTokenHash(secret),
603
634
  input.label ?? null,
604
635
  input.principal,
605
636
  JSON.stringify(author),
606
637
  now,
607
- input.expiresAt,
638
+ input.expiresAt ?? null,
608
639
  input.maxUses,
640
+ targetDigest,
609
641
  ]);
610
- return { id, secret, expiresAt: input.expiresAt };
642
+ return { id, secret, expiresAt: input.expiresAt ?? null };
643
+ }
644
+ /**
645
+ * The one revoke write: stamps `author` once, on a row not yet revoked. True when this call did.
646
+ * `null` is the kernel's own revoke — a link whose principal's holdings changed (#1686) — which no
647
+ * person or platform actor made, and is recorded with no revoker.
648
+ */
649
+ function markRevoked(sql, id, author, now,
650
+ /** The kernel's own revoke says why. */
651
+ reason = null) {
652
+ return (sql.exec(`UPDATE _substrat_capabilities SET revoked_at = ?, revoked_by = ?, revoked_reason = ?
653
+ WHERE id = ? AND revoked_at IS NULL`, [now, author === null ? null : JSON.stringify(author), reason, id]).changes === 1);
654
+ }
655
+ /**
656
+ * Where each named `become` link stands (#1686), in the order asked — what a pending-invite list
657
+ * shows beside each invite. A capability the scope does not hold reads as `revoked`, never `open`.
658
+ *
659
+ * A principal-minted link that would otherwise read `open` is judged as its exchange would judge
660
+ * it: its principal's holdings are read now (`holdings`) and compared with the digest taken at
661
+ * mint, and a link whose principal changed since reads `revoked` / `holdings-changed` — before
662
+ * anybody has tried it. Nothing is written on a read: the exchange is what revokes it. One with no
663
+ * digest, or with no `holdings` to judge it by, reads `revoked`, as its exchange refuses it.
664
+ */
665
+ export async function readBecomeLinkStates(sql, ids, now, holdings) {
666
+ assertBecomeLinkStateIds(ids);
667
+ const states = [];
668
+ for (const raw of ids) {
669
+ const q = capabilityByIdQuery(capabilityIdSchema.parse(raw));
670
+ const row = sql.query(q.sql, q.params)[0];
671
+ states.push(await becomeLinkStateOf(row, now, holdings));
672
+ }
673
+ return states;
674
+ }
675
+ /**
676
+ * `becomeLinkStates`' bound on how many links one call names (`BECOME_LINK_STATES_MAX_IDS`):
677
+ * refused over it, never truncated — a caller asks in pages. Its own function so a host whose
678
+ * storage sits across an RPC boundary checks on the near side, where the typed refusal survives.
679
+ */
680
+ export function assertBecomeLinkStateIds(ids) {
681
+ if (ids.length > BECOME_LINK_STATES_MAX_IDS) {
682
+ throw substratError('validation_failed', `becomeLinkStates: ${ids.length} ids is more than ${BECOME_LINK_STATES_MAX_IDS} — ask in pages`);
683
+ }
684
+ }
685
+ async function becomeLinkStateOf(row, now, holdings) {
686
+ if (!row || row.mode !== 'become')
687
+ return { state: 'revoked', reason: null };
688
+ if (row.revoked_at !== null) {
689
+ return { state: 'revoked', reason: row.revoked_reason === 'holdings-changed' ? 'holdings-changed' : null };
690
+ }
691
+ if (row.expires_at !== null && row.expires_at <= now)
692
+ return { state: 'expired', reason: null };
693
+ if (row.max_uses !== null && row.uses >= row.max_uses)
694
+ return { state: 'used', reason: null };
695
+ if (typeof JSON.parse(row.minted_by) === 'string') {
696
+ if (!holdings || !row.target_digest)
697
+ return { state: 'revoked', reason: null };
698
+ const held = await holdings(principalIdSchema.parse(row.principal));
699
+ if ((await holdingsDigest(held)) !== row.target_digest)
700
+ return { state: 'revoked', reason: 'holdings-changed' };
701
+ }
702
+ return { state: 'open', reason: null };
703
+ }
704
+ /**
705
+ * A canonical digest of what a principal holds (#1686): the role KEYS it holds at the node, as
706
+ * assigned, the permissions granted to it directly at the node, and its entity-narrowed grants —
707
+ * at scope and tenant level, through its orgs — each sorted and deduplicated, so two reads of the
708
+ * same holdings in any order give the same digest. Taken when a principal mints a `become` for it,
709
+ * and again at the exchange: a role assigned or taken away, a grant added or removed, an org joined
710
+ * or left, all change it, and the link dies.
711
+ *
712
+ * Role KEYS, not their expansions, deliberately: a role's definition is the vertical's code,
713
+ * re-projected on a push and reviewed at the permission-diff checkpoint — nothing an inviter
714
+ * controls. Expanding it would revoke every open invite at a role whenever a release adds a key to
715
+ * that role. The price, by design: a REDEFINITION can make the invitee hold more than the minter
716
+ * held when it minted — a release that widens the role widens what the link becomes. That is a
717
+ * change only code or staff make (`defineRole` has no HTTP route), never an inviter.
718
+ *
719
+ * What the digest deliberately does not see, so nobody relies on it as a reach check:
720
+ * - the LEVEL a grant is held at — a key moved between the scope and the tenant node leaves it
721
+ * unchanged, as the mint bound compares the same flattened set (`holdingsOf` merges the levels);
722
+ * - reach that grows under a narrowed grant's entity through `ctx.link` / `ctx.relink` — the
723
+ * minter's coverage of that entity was checked through `check`, whose reach grows the same way;
724
+ * - module authority decided from a principal column (an assignee, an author) rather than a tuple
725
+ * — neither the bound nor the digest sees it.
726
+ * None of these three lets the link yield more than its minter held (a role redefinition can, by
727
+ * design — see above): the identity a link binds is per
728
+ * (scope, sub), so what it becomes is this principal in this scope only. The first two are judged
729
+ * by the same checker, over the same flattened set and the same walk, that bounded the minter;
730
+ * the third is a row the scope itself points at this principal, as it would for any member it
731
+ * names — and a member invite's principal is minted fresh, so no row names it when its link is.
732
+ */
733
+ export async function holdingsDigest(held) {
734
+ const sorted = (xs) => [...new Set(xs)].sort();
735
+ const canonical = JSON.stringify({
736
+ roles: sorted(held.roles),
737
+ granted: sorted(held.granted),
738
+ narrowed: sorted(held.narrowed.map((n) => `${n.permission} ${n.entity.entityType}:${n.entity.entityId}`)),
739
+ });
740
+ return capabilityTokenHash(canonical);
611
741
  }
612
742
  /**
613
743
  * The platform's revoke (`HostAdmin.revokeCapability`) — of any capability in the scope,
@@ -620,9 +750,129 @@ export function revokeCapabilityAsPlatform(sql, rawId, actor, now) {
620
750
  const row = sql.query(q.sql, q.params)[0];
621
751
  if (!row)
622
752
  return undefined;
623
- const author = { platform: actor };
624
- sql.exec(`UPDATE _substrat_capabilities SET revoked_at = ?, revoked_by = ?
625
- WHERE id = ? AND revoked_at IS NULL`, [now, JSON.stringify(author), id]);
753
+ markRevoked(sql, id, { platform: actor }, now);
626
754
  return capabilityRecordOf(row);
627
755
  }
756
+ // ---------------------------------------------------------------------------
757
+ // A principal's `become` (#1686): the bound, the mint, and the revoke a member invite runs.
758
+ // ---------------------------------------------------------------------------
759
+ /** The pseudo-operation a principal's `become` mint names on its spine event. */
760
+ export const CAPABILITY_BECOME_MINT_OPERATION = 'capabilities.mint-become';
761
+ /**
762
+ * May `caller` mint a `become` capability for `target` at `node`? (#1686) Becoming someone
763
+ * acquires everything they hold, so the bound is the assignment bound (K-21) applied to the
764
+ * whole of it: the caller must already hold, at the node, every permission the target holds
765
+ * there, and must be able to exercise every entity-narrowed grant the target holds on that same
766
+ * entity. Anything less mints impersonation upward — a lead inviting a stranger in as the owner.
767
+ *
768
+ * The bound is evaluated at mint, against what the target holds then; the exchange does not
769
+ * re-check the minter. It compares the target's holdings instead: a link whose principal's
770
+ * holdings have changed since the mint is revoked rather than exchanged (`exchangeCapability`). So a target holding nothing at the node is refused outright: its empty set would
771
+ * cover trivially, and the link would yield whatever that principal is granted later. A member
772
+ * invite never meets this — it grants the role before it mints.
773
+ *
774
+ * It is also refused for a target some `become` capability has already been exchanged into in
775
+ * this scope (`target-already-claimed`): a principal-minted `become` is for a seat nobody has
776
+ * taken, never a second key to someone who already exists. Whether a login is bound to the
777
+ * target outside the scope (an identity directory) is not visible here; the one caller today,
778
+ * a member invite, always targets a principal it has just minted.
779
+ *
780
+ * When the mint may go ahead, the target's `holdingsDigest` — what the mint records, and what the
781
+ * exchange compares against. Otherwise the refusal, as the host answers it — a `Coverage` naming
782
+ * what the caller lacks, as `canAssign`'s does, `target-holds-nothing`, or
783
+ * `target-already-claimed`. A checker with
784
+ * no `holdings` cannot say what the target holds, so the bound refuses rather than guess.
785
+ */
786
+ export async function becomeMintCheck(deps, caller, target, node) {
787
+ const { checker } = deps;
788
+ if (becomeClaimed(deps.sql, target))
789
+ return { ok: false, refused: 'target-already-claimed' };
790
+ if (!checker.holdings) {
791
+ throw substratError('unavailable', 'this permission checker cannot read what a principal holds — refusing to mint a become capability');
792
+ }
793
+ const held = await checker.holdings({ kind: 'principal', id: target }, node);
794
+ if (held.permissions.length === 0)
795
+ return { ok: false, refused: 'target-holds-nothing' };
796
+ const coverage = await coversHoldings(checker, caller, held, node);
797
+ return coverage ? { ok: false, coverage } : { ok: true, targetDigest: await holdingsDigest(held) };
798
+ }
799
+ /** Has any `become` capability for `principal` been exchanged in this scope — platform-minted or not? */
800
+ function becomeClaimed(sql, principal) {
801
+ return (sql.query(`SELECT 1 AS one FROM _substrat_capabilities WHERE mode = 'become' AND principal = ? AND uses > 0 LIMIT 1`, [principal]).length > 0);
802
+ }
803
+ /**
804
+ * Does `caller` hold everything in `held` at `node`? Node-level keys compared as `covers`
805
+ * compares, and each entity-narrowed grant re-asked through `check` on its entity. Null when it
806
+ * does; otherwise the refusal naming what is lacked.
807
+ */
808
+ async function coversHoldings(checker, caller, held, node) {
809
+ const nodeLevel = await checker.covers({ kind: 'principal', id: caller }, held.permissions, node);
810
+ const missing = new Set(nodeLevel.covered ? [] : nodeLevel.missing);
811
+ for (const { permission, entity } of held.narrowed) {
812
+ if (missing.has(permission))
813
+ continue;
814
+ if (!(await checker.check({ kind: 'principal', id: caller }, permission, node, entity)).allowed)
815
+ missing.add(permission);
816
+ }
817
+ return missing.size === 0 ? null : { covered: false, missing: [...missing] };
818
+ }
819
+ /**
820
+ * A principal's `become` mint (#1686) — the write a member invite makes once `becomeMintCheck`
821
+ * has said yes, in the same scope task. Recorded with the principal as its minter, and on the
822
+ * spine as `capability.become-minted` through `emit`, whose actor the host stamps as `minter`.
823
+ * No expiry unless one is given, as an invite has never had one; the use limit is required.
824
+ */
825
+ export async function mintBecomeCapabilityAsPrincipal(deps, minter, raw,
826
+ /** `becomeMintCheck`'s answer: the target's holdings now, which the exchange compares against. */
827
+ targetDigest) {
828
+ const input = principalBecomeCapabilityInput.parse(raw);
829
+ if (input.expiresAt !== undefined && input.expiresAt <= deps.now) {
830
+ throw substratError('validation_failed', `mintBecomeCapability: expiresAt ${input.expiresAt} is not in the future`);
831
+ }
832
+ const minted = await insertBecome(deps.sql, input, minter, deps.now, targetDigest);
833
+ const payload = {
834
+ capabilityId: minted.id,
835
+ principal: input.principal,
836
+ expiresAt: input.expiresAt ?? null,
837
+ maxUses: input.maxUses,
838
+ label: input.label ?? null,
839
+ mintedBy: minter,
840
+ };
841
+ deps.emit({
842
+ type: CAPABILITY_BECOME_MINTED,
843
+ schemaVersion: 1,
844
+ entity: { entityType: 'capability', entityId: minted.id },
845
+ piiClass: 'none',
846
+ payload,
847
+ });
848
+ return minted;
849
+ }
850
+ /**
851
+ * Revoke a `become` capability a PRINCIPAL minted (#1686) — what withdrawing a member invite
852
+ * does to its link, recorded with `by` as the revoker. Only such a row: a platform-minted one
853
+ * (an owner claim link) is the platform's to revoke, and an `act` one is `ctx.capabilities`'.
854
+ *
855
+ * Bounded here, not only by the caller: `by` must be the link's minter, or hold everything its
856
+ * principal holds now — whoever could have minted it, as a module's revoke of an `act` link is
857
+ * open to whoever could have minted that. A refusal writes nothing. `revoked` is true when this
858
+ * call revoked it; false when it was already revoked or is not such a row.
859
+ */
860
+ export async function revokeBecomeCapabilityAsPrincipal(deps, rawId, by) {
861
+ const id = capabilityIdSchema.parse(rawId);
862
+ const q = capabilityByIdQuery(id);
863
+ const row = deps.sql.query(q.sql, q.params)[0];
864
+ const minter = row ? JSON.parse(row.minted_by) : null;
865
+ if (!row || row.mode !== 'become' || typeof minter !== 'string')
866
+ return { ok: true, revoked: false };
867
+ if (minter !== by) {
868
+ if (!deps.checker.holdings) {
869
+ throw substratError('unavailable', 'this permission checker cannot read what a principal holds — refusing to revoke a become capability');
870
+ }
871
+ const held = await deps.checker.holdings({ kind: 'principal', id: principalIdSchema.parse(row.principal) }, deps.node);
872
+ const coverage = await coversHoldings(deps.checker, by, held, deps.node);
873
+ if (coverage)
874
+ return { ok: false, coverage };
875
+ }
876
+ return { ok: true, revoked: markRevoked(deps.sql, id, by, deps.now) };
877
+ }
628
878
  //# sourceMappingURL=capability.js.map