@substrat-run/kernel 0.138.0 → 0.140.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 (164) hide show
  1. package/dist/admin-log-ddl.d.ts +21 -0
  2. package/dist/admin-log-ddl.d.ts.map +1 -0
  3. package/dist/admin-log-ddl.js +32 -0
  4. package/dist/admin-log-ddl.js.map +1 -0
  5. package/dist/attachment-extractor.d.ts +21 -2
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +31 -6
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/attachment-text.d.ts +64 -0
  10. package/dist/attachment-text.d.ts.map +1 -1
  11. package/dist/attachment-text.js +87 -16
  12. package/dist/attachment-text.js.map +1 -1
  13. package/dist/attribution.d.ts +36 -3
  14. package/dist/attribution.d.ts.map +1 -1
  15. package/dist/attribution.js +21 -8
  16. package/dist/attribution.js.map +1 -1
  17. package/dist/audit-outcome.d.ts +149 -0
  18. package/dist/audit-outcome.d.ts.map +1 -0
  19. package/dist/audit-outcome.js +233 -0
  20. package/dist/audit-outcome.js.map +1 -0
  21. package/dist/connect-links.d.ts +91 -0
  22. package/dist/connect-links.d.ts.map +1 -0
  23. package/dist/connect-links.js +189 -0
  24. package/dist/connect-links.js.map +1 -0
  25. package/dist/connect-state.d.ts +12 -3
  26. package/dist/connect-state.d.ts.map +1 -1
  27. package/dist/connect-state.js +8 -3
  28. package/dist/connect-state.js.map +1 -1
  29. package/dist/derived-object.d.ts +14 -0
  30. package/dist/derived-object.d.ts.map +1 -0
  31. package/dist/derived-object.js +2 -0
  32. package/dist/derived-object.js.map +1 -0
  33. package/dist/derived-objects.d.ts +108 -0
  34. package/dist/derived-objects.d.ts.map +1 -0
  35. package/dist/derived-objects.js +199 -0
  36. package/dist/derived-objects.js.map +1 -0
  37. package/dist/email-intent.d.ts +56 -0
  38. package/dist/email-intent.d.ts.map +1 -0
  39. package/dist/email-intent.js +71 -0
  40. package/dist/email-intent.js.map +1 -0
  41. package/dist/entity-edges.d.ts +6 -1
  42. package/dist/entity-edges.d.ts.map +1 -1
  43. package/dist/entity-edges.js +13 -0
  44. package/dist/entity-edges.js.map +1 -1
  45. package/dist/entity-grant-shape.d.ts +94 -0
  46. package/dist/entity-grant-shape.d.ts.map +1 -0
  47. package/dist/entity-grant-shape.js +329 -0
  48. package/dist/entity-grant-shape.js.map +1 -0
  49. package/dist/entity-grant.d.ts +45 -0
  50. package/dist/entity-grant.d.ts.map +1 -0
  51. package/dist/entity-grant.js +71 -0
  52. package/dist/entity-grant.js.map +1 -0
  53. package/dist/entity-state-reads.d.ts +40 -0
  54. package/dist/entity-state-reads.d.ts.map +1 -0
  55. package/dist/entity-state-reads.js +91 -0
  56. package/dist/entity-state-reads.js.map +1 -0
  57. package/dist/entity-state.d.ts +204 -0
  58. package/dist/entity-state.d.ts.map +1 -0
  59. package/dist/entity-state.js +356 -0
  60. package/dist/entity-state.js.map +1 -0
  61. package/dist/entity-trash.d.ts +245 -0
  62. package/dist/entity-trash.d.ts.map +1 -0
  63. package/dist/entity-trash.js +380 -0
  64. package/dist/entity-trash.js.map +1 -0
  65. package/dist/findings.d.ts +143 -0
  66. package/dist/findings.d.ts.map +1 -0
  67. package/dist/findings.js +463 -0
  68. package/dist/findings.js.map +1 -0
  69. package/dist/grant-scoped-read.d.ts +41 -0
  70. package/dist/grant-scoped-read.d.ts.map +1 -0
  71. package/dist/grant-scoped-read.js +100 -0
  72. package/dist/grant-scoped-read.js.map +1 -0
  73. package/dist/index.d.ts +40 -8
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +26 -6
  76. package/dist/index.js.map +1 -1
  77. package/dist/kernel-outbox.d.ts +14 -0
  78. package/dist/kernel-outbox.d.ts.map +1 -0
  79. package/dist/kernel-outbox.js +37 -0
  80. package/dist/kernel-outbox.js.map +1 -0
  81. package/dist/list-index.d.ts +34 -3
  82. package/dist/list-index.d.ts.map +1 -1
  83. package/dist/list-index.js +83 -31
  84. package/dist/list-index.js.map +1 -1
  85. package/dist/mail-sender.d.ts +78 -0
  86. package/dist/mail-sender.d.ts.map +1 -0
  87. package/dist/mail-sender.js +2 -0
  88. package/dist/mail-sender.js.map +1 -0
  89. package/dist/membership-executor.d.ts +7 -47
  90. package/dist/membership-executor.d.ts.map +1 -1
  91. package/dist/membership-executor.js +28 -53
  92. package/dist/membership-executor.js.map +1 -1
  93. package/dist/migration-digest.d.ts +161 -0
  94. package/dist/migration-digest.d.ts.map +1 -0
  95. package/dist/migration-digest.js +237 -0
  96. package/dist/migration-digest.js.map +1 -0
  97. package/dist/module-erasure.d.ts +156 -0
  98. package/dist/module-erasure.d.ts.map +1 -0
  99. package/dist/module-erasure.js +440 -0
  100. package/dist/module-erasure.js.map +1 -0
  101. package/dist/module-migrations.d.ts +6 -3
  102. package/dist/module-migrations.d.ts.map +1 -1
  103. package/dist/module-migrations.js +12 -5
  104. package/dist/module-migrations.js.map +1 -1
  105. package/dist/peer.d.ts +9 -1
  106. package/dist/peer.d.ts.map +1 -1
  107. package/dist/peer.js +29 -0
  108. package/dist/peer.js.map +1 -1
  109. package/dist/permission-checker.d.ts +6 -0
  110. package/dist/permission-checker.d.ts.map +1 -1
  111. package/dist/permission-checker.js.map +1 -1
  112. package/dist/permission-eval.d.ts +51 -1
  113. package/dist/permission-eval.d.ts.map +1 -1
  114. package/dist/permission-eval.js +104 -2
  115. package/dist/permission-eval.js.map +1 -1
  116. package/dist/platform-sweep.d.ts +8 -2
  117. package/dist/platform-sweep.d.ts.map +1 -1
  118. package/dist/platform-sweep.js +17 -0
  119. package/dist/platform-sweep.js.map +1 -1
  120. package/dist/scope-host.d.ts +479 -24
  121. package/dist/scope-host.d.ts.map +1 -1
  122. package/dist/scope-host.js +82 -7
  123. package/dist/scope-host.js.map +1 -1
  124. package/dist/scope-role-admin.d.ts +68 -0
  125. package/dist/scope-role-admin.d.ts.map +1 -0
  126. package/dist/scope-role-admin.js +80 -0
  127. package/dist/scope-role-admin.js.map +1 -0
  128. package/dist/search-index.d.ts +21 -1
  129. package/dist/search-index.d.ts.map +1 -1
  130. package/dist/search-index.js +60 -37
  131. package/dist/search-index.js.map +1 -1
  132. package/dist/spine-guard.d.ts +71 -2
  133. package/dist/spine-guard.d.ts.map +1 -1
  134. package/dist/spine-guard.js +393 -10
  135. package/dist/spine-guard.js.map +1 -1
  136. package/dist/spine-restore.d.ts.map +1 -1
  137. package/dist/spine-restore.js +5 -0
  138. package/dist/spine-restore.js.map +1 -1
  139. package/dist/sql-identifier.d.ts +12 -0
  140. package/dist/sql-identifier.d.ts.map +1 -0
  141. package/dist/sql-identifier.js +17 -0
  142. package/dist/sql-identifier.js.map +1 -0
  143. package/dist/sql-statements.d.ts +71 -0
  144. package/dist/sql-statements.d.ts.map +1 -0
  145. package/dist/sql-statements.js +191 -0
  146. package/dist/sql-statements.js.map +1 -0
  147. package/dist/subject-redaction.d.ts +9 -1
  148. package/dist/subject-redaction.d.ts.map +1 -1
  149. package/dist/subject-redaction.js.map +1 -1
  150. package/dist/system-switch-record.d.ts +3 -0
  151. package/dist/system-switch-record.d.ts.map +1 -1
  152. package/dist/system-switch-record.js +2 -0
  153. package/dist/system-switch-record.js.map +1 -1
  154. package/dist/table-ownership.d.ts +103 -0
  155. package/dist/table-ownership.d.ts.map +1 -0
  156. package/dist/table-ownership.js +285 -0
  157. package/dist/table-ownership.js.map +1 -0
  158. package/dist/timeline.d.ts +4 -4
  159. package/dist/timeline.js +4 -4
  160. package/dist/visible-continuation.d.ts +38 -0
  161. package/dist/visible-continuation.d.ts.map +1 -0
  162. package/dist/visible-continuation.js +162 -0
  163. package/dist/visible-continuation.js.map +1 -0
  164. package/package.json +2 -2
@@ -1,12 +1,18 @@
1
1
  import type { ModuleLog } from './module-log.js';
2
2
  import type { DeliveryRefusal } from './delivery-refusal.js';
3
- import type { OnBehalfOf, ExportReadInput, ExportedBatch, ImportBatch, ImportedEvent, ImportResult, ImportState, ImportCursorMove, ImportCursorMoved, AdminAction, BecomeCapabilityInput, CapabilityExchange, CapabilityFilter, CapabilityId, CapabilityPage, MintedCapability, ListPage, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, DelegatedReadRecord, OwnerTransferAudit, CopyMarkAudit, BindHostnameInput, AdminLogEntry, OpsFailureEntry, PlatformRequestDrainTotalsEntry, SweepRunEntry, SweepRunKind, SweepRunOutcome, ModelUsageEntry, ModelUsageSummary, CapabilityGrant, CreateTenantInput, Decision, Instant, DomainEvent, DomainEventInput, PlatformRequestInput, PlatformRequestId, PlatformRequest, PlatformRequestFilter, PlatformRequestStatus, PlatformRequestFailure, EntitlementGrant, EntitlementGrantInput, EntitlementView, MeterReading, EntityRef, IdentityLink, IdentityPool, Jurisdiction, ModuleId, ModuleManifest, ScheduleSpec, SystemGrant, PeerGrantsStatusEntry, SystemGrantsStatusEntry, SystemSwitchRecord, SystemSwitch, SystemSwitchResult, PeerCoverage, PeerSwitch, PeerSwitchResult, VerticalCaller, VerticalResolution, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ExportBreak, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, BindAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, IdentityMembership, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, RefusalFilter, RefusalRecord, PermissionDenial, Coverage, BeginImpersonationInput, ImpersonationFilter, ImpersonationSession, ImpersonationSessionId, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, BlobStoreHandle, Visibility, Vertical, VerticalChannel, VerticalVersion, TenantStatus, Page, CountedPage, FreshnessSpec, EntityHistoryInput, EventFacetInput, EventCauseInput, EventEffectsInput, EffectsTree, InvocationEventsInput, InvocationEvents, DeadLettersInput, DeadLetter, LifecycleFlowInput, LifecycleFlowResult, OperationSeriesInput, OperationSeriesResult, CauseChain, EventFacetResult, HistoryEntry, ErrorCode, PlatformRequestFailureOrigin, IssueEntry, IssueStatus, IssueStatusInput, DeclaredMigration, CheckSubject } from '@substrat-run/contracts';
3
+ import type { ScopeRoleHolder } from './scope-role-admin.js';
4
+ import type { OnBehalfOf, ExportReadInput, ExportedBatch, ImportBatch, ImportedEvent, ImportResult, ImportState, ImportCursorMove, ImportCursorMoved, AdminAction, BecomeCapabilityInput, CapabilityExchange, CapabilityFilter, CapabilityId, CapabilityPage, MintedCapability, ListPage, ConnectLink, ConnectLinkConsume, ConnectLinkFilter, ConnectLinkKey, ConsumeConnectLinkInput, MintConnectLinkInput, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, DelegatedReadRecord, OwnerTransferAudit, MemberChangeAudit, PeerBindingAudit, CopyMarkAudit, BindHostnameInput, AdminLogEntry, OpsFailureEntry, PlatformRequestDrainTotalsEntry, SweepRunEntry, SweepRunKind, SweepRunOutcome, ModelUsageEntry, ModelUsageSummary, CapabilityGrant, CreateTenantInput, Decision, Instant, DomainEvent, DomainEventInput, PlatformRequestInput, PlatformRequestId, PlatformRequest, PlatformRequestFilter, PlatformRequestStatus, PlatformRequestFailure, PlatformOutcomeEvent, EntitlementGrant, EntitlementGrantInput, EntitlementView, MeterReading, EntityRef, IdentityLink, IdentityPool, Jurisdiction, ModuleId, ModuleManifest, ScheduleSpec, SystemGrant, PeerGrantsStatusEntry, SystemGrantsStatusEntry, SystemSwitchRecord, SystemSwitch, SystemSwitchResult, PeerCoverage, PeerSwitch, PeerSwitchResult, VerticalCaller, VerticalResolution, PeerBinding, PeerInstanceResolution, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ExportBreak, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, BindAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, IdentityMembership, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, RefusalFilter, RefusalRecord, PermissionDenial, Coverage, BeginImpersonationInput, ImpersonationFilter, ImpersonationSession, ImpersonationSessionId, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, BlobStoreHandle, Visibility, Vertical, VerticalChannel, VerticalVersion, TenantStatus, Page, CountedPage, FreshnessSpec, EntityHistoryInput, EventFacetInput, EventCauseInput, EventEffectsInput, EffectsTree, InvocationEventsInput, InvocationEvents, DeadLettersInput, DeadLetter, LifecycleFlowInput, LifecycleFlowResult, OperationSeriesInput, OperationSeriesResult, CauseChain, EventFacetResult, HistoryEntry, ErrorCode, PlatformRequestFailureOrigin, IssueEntry, IssueStatus, IssueStatusInput, FindingEntry, FindingFilter, FindingRuleEntry, FindingRuleInput, FindingStatusInput, DeclaredMigration, CheckSubject, EntityGrantShape } from '@substrat-run/contracts';
4
5
  import type { ConnectionUseOutcome } from './connector-calls.js';
5
6
  import type { CapabilityVerbs } from './capability.js';
7
+ import type { AuditedOperationRef, AuditedOperationRow } from './audit-outcome.js';
8
+ import { type EntityStateName } from '@substrat-run/contracts';
6
9
  import type { ModelUsageFilter, ModelUsageInput, ModelUsageWindow } from './model-usage.js';
10
+ import type { FindingPruneReport } from './findings.js';
7
11
  import type { SealedSecret } from './secret-box.js';
12
+ import type { OnSubjectErased } from './module-erasure.js';
8
13
  import type { SystemSwitchReassert, SystemSwitchReassertOptions, SystemSwitchRecordFilter } from './system-switch-record.js';
9
14
  import type { SearchHit, SearchOptions } from './search-index.js';
15
+ import type { GrantedEntitiesPage } from './grant-scoped-read.js';
10
16
  import type { EntityVersion } from './entity-version.js';
11
17
  import type { UndrainedEvents } from './outbox-event.js';
12
18
  import type { JobDriveReport, JobHandler, JobRun, JobRunFilter, StartJobRunInput } from './job-run.js';
@@ -32,6 +38,16 @@ export interface PageParams {
32
38
  readonly filters?: Readonly<Record<string, unknown>>;
33
39
  /** Also count the filtered set — the declaration's `total`, passed through. */
34
40
  readonly total?: boolean;
41
+ /**
42
+ * Which rows of an archivable entity (#119): `active` when unset, `archived` to read the
43
+ * archive. Refused for an entity that declares no archive. The bin is `ctx.pageTrashed`.
44
+ */
45
+ readonly view?: Exclude<EntityStateName, 'trashed'>;
46
+ /**
47
+ * Also return each row's own cursor, as `rowCursors` (#2073) — asked for by `pageVisible`,
48
+ * which may stop partway through a page. Off by default, so an ordinary page is unchanged.
49
+ */
50
+ readonly rowCursors?: boolean;
35
51
  }
36
52
  /**
37
53
  * The scope-host contract — the adapter seam (§5.1 of the design doc).
@@ -82,6 +98,13 @@ export interface OperationContext {
82
98
  readonly tenantId: TenantId;
83
99
  readonly scopeId: ScopeId;
84
100
  readonly principal: PrincipalId;
101
+ /** Seal positions for one filtered list under this scope, caller and operation. */
102
+ pageContinuation(list: string, query: unknown): {
103
+ /** Read-only and copy walks cannot persist a hidden budget-stop position. */
104
+ writable?: boolean;
105
+ open(cursor: string): Promise<string>;
106
+ seal(position: string, hidden: boolean): Promise<string | null>;
107
+ };
85
108
  readonly sql: ScopedSql;
86
109
  /**
87
110
  * The operation's instant (#812) — the ONLY clock module code may read.
@@ -201,6 +224,18 @@ export interface OperationContext {
201
224
  * than returning a denial, and records none (#1642, `assertPermissionKey`).
202
225
  */
203
226
  check(permission: PermissionKey, entity?: EntityRef): Promise<Decision>;
227
+ /**
228
+ * Page entity ids reachable through this caller's live grants and declared parent edges.
229
+ * `all` means a node grant makes every entity of this type visible; `incomplete` means
230
+ * this checker cannot enumerate the path, so callers must use a checked full walk.
231
+ * An `ids` page is complete only after its cursor becomes null. Ids may repeat across
232
+ * pages on multiple parent paths: accumulate a Set, and do not treat page lengths as
233
+ * a count or total. The union of all pages is complete. Every id passes `ctx.check`.
234
+ */
235
+ grantedEntities(permission: PermissionKey, entityType: string, options?: {
236
+ limit?: number;
237
+ cursor?: string;
238
+ }): Promise<GrantedEntitiesPage>;
204
239
  /**
205
240
  * Find entities of one type by what a person typed (#827) — the read a picker
206
241
  * over 40 000 customers needs and `ctx.sql` cannot express without every
@@ -257,9 +292,9 @@ export interface OperationContext {
257
292
  *
258
293
  * **This does not check permission** — nothing on `ctx` does, and a paged read
259
294
  * is not an exception. The operation's own `assertAllowed` still comes first.
260
- * A read that filters per ROW after the fact (a portal walk) cannot use this at
261
- * all: a page of 20 filtered down to 3 is not a page, and the honest shape is an
262
- * over-fetch loop the handler owns.
295
+ * A read that filters per ROW after the fact (a portal walk) cannot hand this
296
+ * page on as it is: a page of 20 filtered down to 3 is not a page, and its cursor
297
+ * may be a refused row's. It walks this read with `pageVisible` instead (#2073).
263
298
  *
264
299
  * Throws `NotListable` for an entity no operation declared `paged.over` on,
265
300
  * `SortNotDeclared` for a `?sort=` outside the vocabulary, and
@@ -326,6 +361,12 @@ export interface OperationContext {
326
361
  * revives it and records that as one `entity.linked` spine event, since access resumes.
327
362
  *
328
363
  * Checks no permission: the operation does, in its own vocabulary.
364
+ *
365
+ * A parent in the TRASH (#119) is refused `not_found`: nothing new hangs off a binned
366
+ * entity, since it is gone from everyone's view until restored. A MISSING parent of a type that
367
+ * declares trash gets the identical refusal, so the two cannot be told apart. An ARCHIVED parent
368
+ * is allowed — archived is filed away and still readable, and whether one may still be added to
369
+ * is the vertical's rule, not the kernel's.
329
370
  */
330
371
  link(child: EntityRef, parent: EntityRef): void;
331
372
  /**
@@ -338,7 +379,17 @@ export interface OperationContext {
338
379
  * move would make the child its own ancestor.
339
380
  * - `from` must be a live parent edge of `child`; `conflict` otherwise. Every other
340
381
  * parent a multi-parent entity has is left alone.
382
+ * - A `to` in the TRASH is refused `not_found`, as `link` refuses one (#119) — the same refusal a
383
+ * missing `to` of that type gets. `from` is not: moving a child out from under a binned
384
+ * parent is how it is rescued.
341
385
  * - `from` equal to `to` is a no-op: nothing written, nothing emitted.
386
+ * - A `to` that is ALREADY a live parent of `child` is the way to detach one parent of a
387
+ * multi-parent child (#2044): the `from` edge is tombstoned, the live `to` edge stays
388
+ * live — no second row, no revive, no `entity.linked` — and `entity.relinked` is still
389
+ * emitted. Like every edge `link` or `relink` leaves live, `to` is left PERMANENT: a
390
+ * future expiry on it (which only a restored dump can hold) is cleared, silently. The
391
+ * child keeps every other parent untouched, expiry included. The contract suite holds
392
+ * both adapters to it.
342
393
  * - The old edge is tombstoned (K-21), not deleted, and one `entity.relinked` spine event
343
394
  * records the move on the child's timeline, stamped like any event the operation emits.
344
395
  * - Transactional with the operation: a relink whose operation throws never happened.
@@ -354,6 +405,56 @@ export interface OperationContext {
354
405
  * ```
355
406
  */
356
407
  relink(child: EntityRef, from: EntityRef, to: EntityRef): void;
408
+ /**
409
+ * Archive an entity (#119): hide it from active views and keep it. Only an entity whose
410
+ * model declares `archive: { permission }` can be archived.
411
+ *
412
+ * - **Checks the declared archive key on the entity itself** — unlike `link`, because the
413
+ * key is declared beside the entity and an operation archiving under another key is the
414
+ * one mistake this exists to make impossible. A denial throws `PermissionDenied`.
415
+ * - Only an `active` entity: anything else is `conflict` (`reason: 'invalid_transition'`);
416
+ * a missing one is `not_found`.
417
+ * - Stamps `_substrat_archived_at` with the operation's instant and emits one kernel-authored
418
+ * `entity.archived`, with the actor and the authorization chain on its envelope.
419
+ * - Transactional with the operation, like every other write.
420
+ *
421
+ * Resolves to the state the entity is now in.
422
+ */
423
+ archive(entity: EntityRef): Promise<EntityStateName>;
424
+ /** Bring an archived entity back to active. Same key, refusals and recording as `archive`. */
425
+ unarchive(entity: EntityRef): Promise<EntityStateName>;
426
+ /**
427
+ * Move an entity to the trash (#119) — a reversible delete. Needs `trash: { permission }`
428
+ * on the entity, and checks that key on it. An `active` or an `archived` entity may be
429
+ * trashed; the archive survives the trip, so a `restore` returns it to the archive.
430
+ * Emits `entity.trashed`.
431
+ */
432
+ trash(entity: EntityRef): Promise<EntityStateName>;
433
+ /**
434
+ * Take an entity out of the trash, back to the state it was trashed from — `archived` if it
435
+ * was archived, `active` otherwise. Checks the trash key. Emits `entity.restored`, whose
436
+ * payload's `to` says which — and so does the value it resolves to.
437
+ */
438
+ restore(entity: EntityRef): Promise<EntityStateName>;
439
+ /**
440
+ * The state of one archivable/trashable entity — `active`, `archived` or `trashed` — or
441
+ * `null` when the row does not exist. What a get-by-id reads before deciding what to answer:
442
+ * the kernel filters the reads it composes, never a handler's own `SELECT`.
443
+ *
444
+ * Checks no permission, like every read on `ctx`. Throws `validation_failed` for an entity
445
+ * type that declares neither.
446
+ */
447
+ entityState(entity: EntityRef): EntityStateName | null;
448
+ /**
449
+ * One page of the TRASH of a declared entity (#119) — `ctx.page` over the trashed rows,
450
+ * with the declared trash key checked on EACH row inside the kernel, so a handler cannot
451
+ * forget it. A row the caller may not see in the bin is left out, which can make a page
452
+ * short; the walk still ends only at a null `nextCursor`. No `total`: a count over rows the
453
+ * caller cannot see would disclose them.
454
+ */
455
+ pageTrashed<T>(entityType: string, params: Omit<PageParams, 'view' | 'total'>): Promise<Page<T>>;
456
+ /** `ctx.search` over the trash, the declared trash key checked per hit (#119). */
457
+ searchTrashed(entityType: string, term: string, options?: Omit<SearchOptions, 'view'>): Promise<SearchHit[]>;
357
458
  /**
358
459
  * Narrow a permission the CALLER ALREADY HOLDS onto one entity — how an app
359
460
  * expresses user-initiated sharing.
@@ -738,9 +839,7 @@ export type ConsumerHandler = (ctx: OperationContext, event: DomainEvent) => voi
738
839
  export type ImportHandler = (ctx: OperationContext, event: ImportedEvent) => void | Promise<void>;
739
840
  /**
740
841
  * One membership change for `HostAdmin.applyMembership` (#1184): a TENANT-level role assigned
741
- * or taken away, and nothing else. No org is joined or left — what an org confers lives partly
742
- * in each scope's own store, where no directory unit can bound it, so authorizing an org
743
- * membership is its own capability.
842
+ * or taken away and, when `orgId` is given (#2047), the org joined or left in the same unit.
744
843
  */
745
844
  export type MembershipChange = {
746
845
  tenantId: TenantId;
@@ -753,6 +852,15 @@ export type MembershipChange = {
753
852
  * read inside the unit (`tenantCoverage`).
754
853
  */
755
854
  boundedBy: PrincipalId;
855
+ /**
856
+ * The org joined (an add) or left (a removal) beside the role (#2047). Bounded by membership:
857
+ * the unit applies nothing unless `boundedBy` is a live member of it, read inside the unit
858
+ * (`liveOrgMembership`). A member holds everything the org confers — its grants in each
859
+ * scope's own store included — so that is the whole bound, and no scope is read. A join's
860
+ * membership expires no later than `boundedBy`'s own (`joinedMembershipExpiry`). Absent, the
861
+ * change is the role alone.
862
+ */
863
+ orgId?: OrgId;
756
864
  } & ({
757
865
  op: 'add';
758
866
  /** Apply nothing when `principal`'s removal fence stands at or after this instant. */
@@ -761,9 +869,10 @@ export type MembershipChange = {
761
869
  op: 'remove';
762
870
  });
763
871
  /**
764
- * What the unit did: applied, or why not — fenced by a removal (an add only), or out of the
765
- * bound. A removal that applied says whether it took anything (`changed`); one that took
766
- * nothing still raised the fence.
872
+ * What the unit did: applied, or why not — fenced by a removal (an add only), out of the role's
873
+ * bound, or out of the org's (#2047: `boundedBy` is no live member of it). A removal that
874
+ * applied says whether it took anything (`changed`); one that took nothing still raised the
875
+ * fence.
767
876
  */
768
877
  export type MembershipChangeResult = {
769
878
  applied: true;
@@ -777,6 +886,12 @@ export type MembershipChangeResult = {
777
886
  } | {
778
887
  applied: false;
779
888
  unknownRole: string;
889
+ } | {
890
+ applied: false;
891
+ unknownOrg: OrgId;
892
+ } | {
893
+ applied: false;
894
+ notMember: OrgId;
780
895
  };
781
896
  /**
782
897
  * An **executor**: out-of-band host code that effects, outside a scope, what a module
@@ -795,8 +910,11 @@ export type MembershipChangeResult = {
795
910
  * consumers must.
796
911
  *
797
912
  * It receives `HostAdmin`, not `ctx`: it acts with platform authority, which is
798
- * precisely what module code must never hold. Admin writes it makes are stamped with
799
- * the causing event's id (`causedBy`), so the split trail joins.
913
+ * precisely what module code must never hold. Admin writes it makes through the `admin`
914
+ * it is handed are stamped with the causing event's id (`causedBy`), so the split trail
915
+ * joins. That `admin` is bound to this one event, never the host's own (#2055). A handler
916
+ * that writes for a person attributes from it — `admin.attributed(onBehalfOf)` — which keeps
917
+ * the event (#2069); a view of the HOST knows nothing of the event.
800
918
  *
801
919
  * A handler that decides an event must never be effected RETURNS `refuseDelivery(reason)`
802
920
  * (#1184). The delivery is journaled terminal with the reason, never retried. A return
@@ -1409,6 +1527,11 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1409
1527
  * nothing, and it reads as coverage. A bound operation with no entry is
1410
1528
  * allowed and means what it always meant — nothing was declared to parse.
1411
1529
  *
1530
+ * **It also carries the declared surface (#119).** `operationInputsOf` records, for the frozen
1531
+ * map it returns, every declared operation and the entity each addresses by id. The host DERIVES
1532
+ * its trash refusal from that — nothing else is handed over — and a module with a trashable
1533
+ * entity must pass that map as returned, with every operation it binds declared in it.
1534
+ *
1412
1535
  * Typed structurally rather than as `z.ZodType` so the kernel keeps its single
1413
1536
  * dependency and no zod version is pinned by the scope-host contract. The
1414
1537
  * shape is the whole surface the host uses: throw to refuse, return the value
@@ -1485,6 +1608,15 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1485
1608
  * contribute the same name.
1486
1609
  */
1487
1610
  predicates?: Record<string, GuardPredicate>;
1611
+ /**
1612
+ * This module's own step in a subject erasure (#2068), for the rows its declared
1613
+ * `erasure: { subjects }` cannot reach — a link the row does not hold itself. Run by
1614
+ * `shredSubject` inside the erasure's one transaction, after the declared entities and
1615
+ * before the key is destroyed; a throw refuses the whole erasure and rolls it back.
1616
+ * Synchronous, idempotent, and handed a `sql` that reaches the module's own tables only
1617
+ * (`module-erasure.ts`). Requires `manifest.erasure`, whose `tables` is that reach.
1618
+ */
1619
+ onSubjectErased?: OnSubjectErased;
1488
1620
  }
1489
1621
  /**
1490
1622
  * Admin surface for enforcement input (design doc §4; control-plane.md §4.4).
@@ -1509,6 +1641,16 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1509
1641
  * stay sync — they are code-time bookkeeping, not control-plane state.)
1510
1642
  */
1511
1643
  export interface HostAdmin {
1644
+ /**
1645
+ * This same admin, with every row written through it also naming `onBehalfOf` (#977, #2069),
1646
+ * and keeping whatever this one already carries — for the `admin` an executor handler is
1647
+ * handed, the event (`causedBy`). A handler that writes for a person attributes from that
1648
+ * `admin`, so the cause comes along by construction and there is no option to forget.
1649
+ *
1650
+ * Optional so an admin that predates it still satisfies the interface; a caller that finds it
1651
+ * absent writes through the admin it holds, unattributed to a person.
1652
+ */
1653
+ attributed?(onBehalfOf: OnBehalfOf): HostAdmin;
1512
1654
  defineRole(actor: PlatformActorId, tenantId: TenantId, role: RoleDefinition): Promise<void>;
1513
1655
  /**
1514
1656
  * Every role the directory holds, ordered by (tenantId, key).
@@ -1540,6 +1682,57 @@ export interface HostAdmin {
1540
1682
  */
1541
1683
  unassignRole(actor: PlatformActorId, assignment: RoleAssignment): Promise<void>;
1542
1684
  grant(actor: PlatformActorId, grant: CapabilityGrant): Promise<void>;
1685
+ /**
1686
+ * Give a person a vertical's declared entity-grant SHAPE on one entity (#2071): every key in
1687
+ * `permissions`, plus the marker that makes them a holder of the shape, so a key the shape
1688
+ * gains later reaches them at the next {@link reconcileEntityGrantShapes}. What a vertical
1689
+ * calls where it used to `grant` its `ENTITY_GRANTS` keys one at a time. It brings back a key
1690
+ * or marker a revoke tombstoned, as `grant` does, and leaves a live row as it is, expiry
1691
+ * included, as `ctx.grant` does. Audited as `grantEntityShape`.
1692
+ */
1693
+ grantEntityShape(actor: PlatformActorId, grant: {
1694
+ principalId: PrincipalId;
1695
+ node: {
1696
+ tenantId: TenantId;
1697
+ scopeId: ScopeId;
1698
+ };
1699
+ entity: EntityRef;
1700
+ permissions: readonly PermissionKey[];
1701
+ grantedBy: PrincipalId;
1702
+ }): Promise<void>;
1703
+ /**
1704
+ * Top every holder of each declared shape up to the shape as it is now (#2071): a key the
1705
+ * shape gained is granted to each person holding the shape's marker on an entity of that
1706
+ * type. Never a key revoked from that person there (its K-21 tombstone stays). A key merely
1707
+ * dropped from the shape is left in place; a key the shape declares `retired` (#2082) is taken
1708
+ * back from every holder instead, its row tombstoned, once per scope — including a direct grant
1709
+ * of that key to the holder on the same entity, which is the same row. Until it is done on a scope, a
1710
+ * shape that declares a `holder` is also backfilled: each person its holder relationship names
1711
+ * as the owner of an entity (`'self'`, or the vertical's own column) is marked if they hold a
1712
+ * key of the shape there. Never inferred from key sets, so a grant delegated on someone else's
1713
+ * record is never marked. Without a `holder`, there is no backfill.
1714
+ *
1715
+ * Only shapes declared `bootstrap: true` are reconciled — those given with
1716
+ * {@link grantEntityShape} on a person's own record. A SHARING shape, reached through
1717
+ * `ctx.grant` (todo's `list`), is skipped whole: reconciling one would mark every person
1718
+ * something was fully shared with as a holder.
1719
+ *
1720
+ * Bounded: `batch` rows of work (default 500, an integer from 1 to 5000, or `validation_failed`)
1721
+ * per scope transaction, backfill included, repeated until a pass finishes — so it is safe
1722
+ * on a large scope, and finishes on a re-run if interrupted. Each (person, entity) topped up is
1723
+ * an `entity.grants-topped-up` event on the entity, and each one retired keys were taken from an
1724
+ * `entity.grants-retired` event. Audited as `reconcileEntityGrantShapes` when it changed
1725
+ * anything. Returns how many (person, entity) it topped up, and how many it retired keys from.
1726
+ */
1727
+ reconcileEntityGrantShapes(actor: PlatformActorId, node: {
1728
+ tenantId: TenantId;
1729
+ scopeId: ScopeId;
1730
+ }, shapes: readonly EntityGrantShape[], opts?: {
1731
+ batch?: number;
1732
+ }): Promise<{
1733
+ toppedUp: number;
1734
+ retired: number;
1735
+ }>;
1543
1736
  /** Grant to an organization (portal customers); members reach it via membership tuples. */
1544
1737
  /**
1545
1738
  * Grant a permission to a CONNECTION (#97) — how a connector is allowed to
@@ -1776,7 +1969,14 @@ export interface HostAdmin {
1776
1969
  */
1777
1970
  revokeCapability(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, capabilityId: CapabilityId): Promise<void>;
1778
1971
  grantToOrg(actor: PlatformActorId, orgId: OrgId, permission: PermissionKey, node: Node, entity?: EntityRef): Promise<void>;
1779
- addMember(actor: PlatformActorId, tenantId: TenantId, principal: PrincipalId, orgId: OrgId): Promise<void>;
1972
+ /**
1973
+ * Make `principal` a member of the org. `opts.expiresAt` (#2047) makes the membership lapse
1974
+ * then, as any expiring tuple does; absent, it never lapses. Re-adding replaces the row,
1975
+ * clearing a tombstone and setting the expiry anew.
1976
+ */
1977
+ addMember(actor: PlatformActorId, tenantId: TenantId, principal: PrincipalId, orgId: OrgId, opts?: {
1978
+ expiresAt?: Instant;
1979
+ }): Promise<void>;
1780
1980
  /**
1781
1981
  * One membership change, applied as ONE directory unit (#1184): a single SQLite transaction or
1782
1982
  * a single synchronous ControlPlaneDO method, with no await inside it. In that unit it
@@ -1787,6 +1987,11 @@ export interface HostAdmin {
1787
1987
  * does. A removal, a grant, a role redefinition or a demotion lands wholly before the unit
1788
1988
  * (and governs it) or wholly after it, never between its check and its write.
1789
1989
  *
1990
+ * With `orgId` (#2047) the org is joined or left in the same unit, and bounded there too:
1991
+ * `boundedBy` must be a live member of it. An add writes the membership — expiring no later
1992
+ * than `boundedBy`'s own — with an `addMember` audit row; a removal tombstones it, with a
1993
+ * `removeMember` row if it took anything. The role and the org apply together or not at all.
1994
+ *
1790
1995
  * The fence is `_substrat_membership_fences`: every tenant-level `unassignRole` and every
1791
1996
  * `removeMember` raises it for the person, in the same unit as its revoke — a no-op included,
1792
1997
  * since a removal of someone whose add is still on its way must still win.
@@ -2214,6 +2419,17 @@ export interface HostAdmin {
2214
2419
  * per-call path, before any staff member is involved.
2215
2420
  */
2216
2421
  resolveVerticalInstance(tenantId: TenantId, vertical: string): Promise<VerticalResolution>;
2422
+ /** Resolve for one caller at execution time. An explicit stale binding refuses without fallback. */
2423
+ resolvePeerInstance(tenantId: TenantId, callerScopeId: ScopeId, vertical: string): Promise<PeerInstanceResolution>;
2424
+ /** Read the caller's explicit choice, if any. Tenant-scoped and recorded as an admin read. */
2425
+ peerBinding(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId, vertical: string): Promise<PeerBinding | undefined>;
2426
+ /** Every explicit target choice this calling scope currently holds. */
2427
+ peerBindings(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId): Promise<PeerBinding[]>;
2428
+ /** Bind or clear this caller's target. Both endpoints are checked in the directory at write. */
2429
+ setPeerBinding(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId, vertical: string, targetScopeId: ScopeId | null): Promise<{
2430
+ changed: boolean;
2431
+ previous: ScopeId | null;
2432
+ }>;
2217
2433
  /**
2218
2434
  * Persist a tenant. Idempotent on the id — re-creating an existing tenant is a
2219
2435
  * no-op, not an error (control-plane.md §4.1). `status` starts `active` and
@@ -2858,8 +3074,11 @@ export interface HostAdmin {
2858
3074
  * the audit metadata — the rotate-side analogue of `createdBy` on create
2859
3075
  * (§3.5.1). Omitted ⇒ the effecting actor stands alone, the platform-driven
2860
3076
  * refresh path.
3077
+ *
3078
+ * `expiresAt`: a date replaces the connection's expiry, `null` clears it (the credential
3079
+ * that had one was replaced by one that has none), and omitted keeps it.
2861
3080
  */
2862
- updateConnectionSecret(actor: PlatformActorId, id: ConnectionId, secret: ConnectionSecret, expiresAt?: string, opts?: {
3081
+ updateConnectionSecret(actor: PlatformActorId, id: ConnectionId, secret: ConnectionSecret, expiresAt?: string | null, opts?: {
2863
3082
  rotatedBy?: string;
2864
3083
  }): Promise<void>;
2865
3084
  /**
@@ -2869,6 +3088,37 @@ export interface HostAdmin {
2869
3088
  * constraint ignores revoked rows.
2870
3089
  */
2871
3090
  revokeConnection(actor: PlatformActorId, id: ConnectionId): Promise<void>;
3091
+ /**
3092
+ * A vertical's MAILED connect link (connections.md §3.5.4) — the row that makes a consent
3093
+ * round single-use and revocable when it lives days rather than minutes. Beside the
3094
+ * connections because it is their front door: the round it starts ends in
3095
+ * `createConnection` (or a rotation) for the same tenant and scope.
3096
+ *
3097
+ * Every verb below is keyed by (tenant, scope): a link of another scope or tenant is
3098
+ * `undefined` / `unknown`, never a refusal that admits it exists. Expiry is judged against
3099
+ * the host's own clock. Mint, revoke, consume and restore are each audited — metadata
3100
+ * only, though nothing here is secret: the credential does not exist until a round is
3101
+ * consumed, and it never touches this row.
3102
+ */
3103
+ mintConnectLink(actor: PlatformActorId, input: MintConnectLinkInput): Promise<ConnectLink>;
3104
+ getConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<ConnectLink | undefined>;
3105
+ /** Newest first. `outstandingOnly`: outstanding AND unexpired — the links that still open. */
3106
+ listConnectLinks(actor: PlatformActorId, filter: ConnectLinkFilter): Promise<ConnectLink[]>;
3107
+ /**
3108
+ * Outstanding → revoked. Idempotent: a revoked or used link is answered as it stands and
3109
+ * nothing is written. `undefined` when the scope holds no such link.
3110
+ */
3111
+ revokeConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<ConnectLink | undefined>;
3112
+ /**
3113
+ * Outstanding and unexpired → used, recording what the consent attached. Atomic: of two
3114
+ * concurrent consumes exactly one answers `ok`. Otherwise the typed reason.
3115
+ */
3116
+ consumeConnectLink(actor: PlatformActorId, input: ConsumeConnectLinkInput): Promise<ConnectLinkConsume>;
3117
+ /**
3118
+ * Used → outstanding, the best-effort undo after the credential store failed. Only a
3119
+ * used, unexpired link moves; `false` for anything else, a revoked link included.
3120
+ */
3121
+ restoreConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<boolean>;
2872
3122
  /**
2873
3123
  * Open the credential for one (tenant, vertical, provider[, account]) — the
2874
3124
  * connector's read, and the only path in the system that yields plaintext.
@@ -3058,6 +3308,42 @@ export interface HostAdmin {
3058
3308
  * for an unknown fingerprint. Audited with the before/after status diff (K-33).
3059
3309
  */
3060
3310
  setIssueStatus(actor: PlatformActorId, fingerprint: string, status: IssueStatusInput): Promise<IssueEntry | undefined>;
3311
+ /**
3312
+ * Findings (#1748), most recently seen first: the tenant-scoped inbox. `filter.tenantId`
3313
+ * absent is the staff fleet read; every tenant-facing caller passes it, and the HTTP surface
3314
+ * forces it from the principal. Access-logged (K-24). No cursor, for `listIssues`'s reason.
3315
+ */
3316
+ listFindings(actor: PlatformActorId, filter?: FindingFilter): Promise<FindingEntry[]>;
3317
+ /**
3318
+ * A verdict on one of `tenantId`'s findings: acknowledge, resolve, or reopen. Keyed on the
3319
+ * tenant as well as the id, so another tenant's finding reads as unknown (undefined).
3320
+ * Audited with the before/after diff (K-33).
3321
+ */
3322
+ setFindingStatus(actor: PlatformActorId, tenantId: TenantId, id: string, status: FindingStatusInput): Promise<FindingEntry | undefined>;
3323
+ /**
3324
+ * Suppress with a rule: a scope (kind / operation / code / subject) and an expiry of at most
3325
+ * `FINDING_RULE_MAX_DAYS`. The findings it covers now are suppressed at once; later
3326
+ * occurrences it covers are counted but keep their finding suppressed. Audited, naming how
3327
+ * many findings it suppressed.
3328
+ */
3329
+ createFindingRule(actor: PlatformActorId, tenantId: TenantId, input: FindingRuleInput): Promise<{
3330
+ rule: FindingRuleEntry;
3331
+ suppressed: number;
3332
+ }>;
3333
+ /** End one of `tenantId`'s rules now. Undefined for an unknown rule. Audited. */
3334
+ revokeFindingRule(actor: PlatformActorId, tenantId: TenantId, ruleId: string): Promise<FindingRuleEntry | undefined>;
3335
+ /**
3336
+ * The findings retention pass (#1748) — `pruneFindings`: a quiet open or acked finding is
3337
+ * resolved as `stale` with a `resolveStaleFinding` audit row written in the same unit, never
3338
+ * deleted; closed findings and expired rules past the window are deleted, at most `limit` of
3339
+ * each per call. Optional so a host that predates findings degrades the sweep's phase to `null`.
3340
+ */
3341
+ pruneFindings?(actor: PlatformActorId, limit: number): Promise<FindingPruneReport>;
3342
+ /** `tenantId`'s suppress rules, newest first; `active` keeps the unexpired ones. Access-logged. */
3343
+ listFindingRules(actor: PlatformActorId, tenantId: TenantId, filter?: {
3344
+ active?: boolean;
3345
+ limit?: number;
3346
+ }): Promise<FindingRuleEntry[]>;
3061
3347
  /**
3062
3348
  * Meter 3's ledger (#1054): one line per model call a vertical made through the
3063
3349
  * platform's model host, drained here as a `model-usage` intent. Idempotent on the
@@ -3125,6 +3411,35 @@ export interface HostAdmin {
3125
3411
  * cannot be written.
3126
3412
  */
3127
3413
  recordCopyMark(actor: PlatformActorId, entry: CopyMarkAudit): Promise<void>;
3414
+ /**
3415
+ * Record one phase of a dashboard member change (#1150) — an invite, a role move or a removal
3416
+ * on an installed vertical's scope — as `manageScopeMember`. The change runs in the vertical's
3417
+ * deployment, so the control plane writes these around its call: `intent` first (and must not
3418
+ * call if this throws), then the outcome. `recordOwnerTransfer`'s shape. Throws when the row
3419
+ * cannot be written.
3420
+ */
3421
+ recordMemberChange(actor: PlatformActorId, entry: MemberChangeAudit): Promise<void>;
3422
+ /** One intent or outcome of a tenant's peer-instance binding change (#1720). */
3423
+ recordPeerBindingChange(actor: PlatformActorId, entry: PeerBindingAudit): Promise<void>;
3424
+ /**
3425
+ * Close one audited change whose intent has no outcome (#2064): in ONE transaction, read the
3426
+ * intent `intentId` names, and if no outcome row exists for its operation, write an `unknown`
3427
+ * outcome carrying `error` and an ops-failure row (`audit.<action>`, stage `outcome-unknown`)
3428
+ * for the staff digest. Answers `true` when it wrote them, `false` when an outcome was already
3429
+ * there, so concurrent callers settle an operation once. Throws `not_found` for an id that is
3430
+ * not an audited-change intent. See `audit-outcome.ts` for why a later real outcome still wins.
3431
+ */
3432
+ settleUnrecordedOutcome(actor: PlatformActorId, input: {
3433
+ intentId: string;
3434
+ error: string;
3435
+ }): Promise<boolean>;
3436
+ /**
3437
+ * Every admin-log row of the given audited operations (#2064), read through the operation-id
3438
+ * index in bounded batches — one statement per `AUDITED_OPERATIONS_BATCH` ids, never a scan of
3439
+ * the log. Only rows of the exact (action, operation, tenant, scope) asked about are returned.
3440
+ * Reading the audit trail is itself recorded, once per call.
3441
+ */
3442
+ auditedOperations(actor: PlatformActorId, refs: readonly AuditedOperationRef[]): Promise<AuditedOperationRow[]>;
3128
3443
  /**
3129
3444
  * Stamp `drainedAt` on every not-yet-drained access row up to and including
3130
3445
  * `upToId`, marking them shipped to Tier 2. Returns how many rows moved.
@@ -3583,6 +3898,12 @@ export declare function telemetryRetentionStatements(nowMs: number, limit: numbe
3583
3898
  sql: string;
3584
3899
  params: [string, number];
3585
3900
  }[];
3901
+ /**
3902
+ * One bounded retention DELETE: at most `LIMIT ?` rows whose `column` is before `?`, oldest
3903
+ * first through the column's own index, `RETURNING 1` so the count is what was deleted. Takes
3904
+ * `(horizon, limit)`. The statement `telemetryRetentionStatements` and `pruneFindings` share.
3905
+ */
3906
+ export declare function boundedRetentionDelete(table: string, column: string): string;
3586
3907
  /** Filter for `listIssues` (#1233). Bounded by `limit` only — see the verb's doc. */
3587
3908
  export interface IssueFilter {
3588
3909
  status?: IssueStatus;
@@ -3763,8 +4084,24 @@ export interface ScopeHost {
3763
4084
  *
3764
4085
  * Optional so a host that predates it still satisfies the interface; a transport
3765
4086
  * that finds it absent writes unattributed rows, which is what every row was before.
4087
+ *
4088
+ * A view of the host knows nothing of any event: an executor handler attributes from the
4089
+ * `admin` it is handed instead (`HostAdmin.attributed`), which keeps its `causedBy`.
3766
4090
  */
3767
4091
  attributed?(onBehalfOf: OnBehalfOf): ScopeHost;
4092
+ /**
4093
+ * The same view, its rows also naming `options.causedBy` as the event they are the effect
4094
+ * of (K-22, #2055).
4095
+ *
4096
+ * @deprecated since #2069: an executor handler attributes from the `admin` it is handed —
4097
+ * `admin.attributed(onBehalfOf)` — which carries its event by construction. This form needs
4098
+ * the event passed again, and a handler that forgets writes `causedBy` NULL with nothing
4099
+ * failing. It stays because it shipped, and because it is how the membership executor still
4100
+ * attributes on a host whose handed admin predates `HostAdmin.attributed`.
4101
+ */
4102
+ attributed?(onBehalfOf: OnBehalfOf, options?: {
4103
+ causedBy?: string;
4104
+ }): ScopeHost;
3768
4105
  /**
3769
4106
  * Mint a capability stub for a principal. Validates the (tenantId, scopeId)
3770
4107
  * pair against the directory — a mismatched pair fails closed (K-3), it never
@@ -4190,6 +4527,31 @@ export interface ScopeHost {
4190
4527
  * `canAssign`; an unknown role throws `not_found`.
4191
4528
  */
4192
4529
  assignScopeRoleBounded(tenantId: TenantId, scopeId: ScopeId, caller: PrincipalId, assignee: PrincipalId, roleKey: string): Promise<Coverage>;
4530
+ /**
4531
+ * The scope's role roster (#1150): every live scope-level role assignment, one row per
4532
+ * (principal, role) — or one principal's, with `principal`. One scope, addressed by the caller
4533
+ * after its own (tenant, scope) check — K-3 is asserted here too — so it is a read of this
4534
+ * scope, never a walk across scopes.
4535
+ */
4536
+ listScopeRoleHolders(tenantId: TenantId, scopeId: ScopeId, principal?: PrincipalId): Promise<ScopeRoleHolder[]>;
4537
+ /**
4538
+ * Move `principal` from scope role `from` to `to` in ONE scope task (#1150): the caller's
4539
+ * bound is asked over both roles (taking `from` away is bounded like granting it, §5.1
4540
+ * consequence 1), then `from` is tombstoned and `to` granted together. Nothing is written
4541
+ * on a refusal, so nobody is left holding both roles or neither. Throws `not_found` for a
4542
+ * `to` this tenant does not define, and `conflict` when `principal` does not hold `from`.
4543
+ */
4544
+ changeScopeRoleBounded(tenantId: TenantId, scopeId: ScopeId, caller: PrincipalId, principal: PrincipalId, from: string, to: string): Promise<Coverage>;
4545
+ /**
4546
+ * Take every scope role `principal` holds, in ONE scope task (#1150), bounded by the
4547
+ * caller's authority over each (§5.1 consequence 1: you cannot strip what you could not
4548
+ * have granted). A role the tenant no longer defines confers nothing and is taken without a
4549
+ * bound. A refusal writes nothing; `revoked` names what was taken.
4550
+ */
4551
+ revokeScopeRolesBounded(tenantId: TenantId, scopeId: ScopeId, caller: PrincipalId, principal: PrincipalId): Promise<{
4552
+ coverage: Coverage;
4553
+ revoked: string[];
4554
+ }>;
4193
4555
  /**
4194
4556
  * The recurring-work declarations of every module registered on this host (#383)
4195
4557
  * — each module's id, the vertical it belongs to, and its `schedules`. Sync like
@@ -4442,6 +4804,11 @@ export interface ScopeHost {
4442
4804
  lastError?: string | null;
4443
4805
  /** WHO refused (#841). Omitted by a caller too old to attribute — stored as NULL. */
4444
4806
  failure?: PlatformRequestFailure | null;
4807
+ /**
4808
+ * #2102: an outcome event written into the scope with the settle, in its transaction,
4809
+ * and only when this settle moves the row out of `pending` (`settlePlatformRequestIn`).
4810
+ */
4811
+ event?: PlatformOutcomeEvent | null;
4445
4812
  }): Promise<void>;
4446
4813
  /**
4447
4814
  * Live reads (#938) — a subscription to this scope's change feed, or absent.
@@ -4495,10 +4862,12 @@ export interface ScopeHost {
4495
4862
  * changed at 14:02 is information about that row, so "the body was empty" is not a
4496
4863
  * defence: the filter runs whether or not there is a payload to withhold.
4497
4864
  *
4498
- * A subscription may be narrowed `within` one entity (#1853), which only removes frames;
4499
- * the one exception to the per-principal check is a `within` the vertical built with
4500
- * `vouchedWithin`, whose subscriber is sent bare `LiveNudge` frames and nothing that
4501
- * names an entity.
4865
+ * A subscription may be narrowed `within` one entity (#1853), which only removes frames.
4866
+ * Two built roots replace the per-row check with one about the ROOT, and their subscribers
4867
+ * are sent bare `LiveNudge` frames that name no entity: `checkedWithin`, whose principal must
4868
+ * pass a stated key on the root at the handshake and again on every pass that has something
4869
+ * to send (#938), and `vouchedWithin`, which the vertical asserts once and nothing re-checks
4870
+ * (#1853). Which to use is on `checkedWithin`.
4502
4871
  *
4503
4872
  * **Generic over the runtime's request and response, because the kernel names
4504
4873
  * neither.** This package has one dependency and no DOM or workers lib
@@ -4551,10 +4920,51 @@ export interface LiveReadSurface<Req extends LiveUpgradeRequest = LiveUpgradeReq
4551
4920
  * the same trust it already extends in naming `principal`, and the scope's walk is
4552
4921
  * the whole filter. Such a subscriber receives `LiveNudge` frames only, which name
4553
4922
  * no event type and no entity.
4923
+ * - **A `checkedWithin(…)` value replaces the per-row check with one on the root**
4924
+ * (#938). The principal must pass the stated key on the root, at the handshake (a
4925
+ * `403` otherwise) and again on every pass that has a row beneath it to announce,
4926
+ * so a grant withdrawn, or a root moved out from under it, closes the socket
4927
+ * instead of nudging it. `LiveNudge` frames only, as for a vouched root.
4928
+ *
4929
+ * **Freshness (Codex #2077 r5).** A decision is reused within a pass only while the
4930
+ * scope has had no write and nothing it holds has reached its `expires_at` since the
4931
+ * decision. It is never reused while the scope reads its permissions from the directory.
4932
+ * Both are checked against the clock immediately before each send. A change that lands
4933
+ * while a check is still being evaluated can let that one frame through. Frames name
4934
+ * nothing the subscriber could not read when checked, and the next frame, pass and poll
4935
+ * see the change: a push is a hint at most one evaluation stale.
4554
4936
  */
4555
- within?: EntityRef | VouchedWithin;
4937
+ within?: EntityRef | VouchedWithin | CheckedWithin;
4938
+ /**
4939
+ * When the credential that proved `principal` stops being valid (ISO 8601), so the
4940
+ * socket never outlives it (#938). Pass the session's or the bearer's expiry
4941
+ * (`AuthSubject.expiresAt`). A handshake at or past it is refused, and the scope closes
4942
+ * the socket (`1008`) on its first pass at or past it, before sending anything on that
4943
+ * pass. Absent: the socket lives until either end closes it, as before.
4944
+ */
4945
+ expiresAt?: string;
4556
4946
  }): Promise<Res>;
4557
4947
  }
4948
+ /**
4949
+ * The close codes a live socket ends with, so a client can tell "try again" from "poll".
4950
+ *
4951
+ * - `1008` (policy): the subscriber may no longer watch what it subscribed to — a
4952
+ * `checkedWithin` gate refused, or the session that opened it ended. A reconnect meets
4953
+ * the handshake's own refusal.
4954
+ * - `4429`: this principal already holds `LIVE_SOCKETS_PER_PRINCIPAL` sockets on the scope.
4955
+ * Not a reason to retry: the client should poll, which it does anyway, and stop asking.
4956
+ */
4957
+ export declare const LIVE_CLOSE: {
4958
+ readonly revoked: 1008;
4959
+ readonly tooMany: 4429;
4960
+ };
4961
+ /**
4962
+ * How many live sockets one principal may hold on one scope (#938). A tab holds one per
4963
+ * feed (the desk's, and one per open conversation in ticket0's portal), and each socket is
4964
+ * work on every post-commit pass, so a principal opening more is a cost on everybody else
4965
+ * writing to the scope. Eight covers several tabs; past it a socket is closed `4429`.
4966
+ */
4967
+ export declare const LIVE_SOCKETS_PER_PRINCIPAL = 8;
4558
4968
  /** The brand only `vouchedWithin` can apply — a literal cannot type-check as one. */
4559
4969
  declare const vouchedBrand: unique symbol;
4560
4970
  /**
@@ -4586,6 +4996,50 @@ export declare function vouchedWithin(entity: EntityRef, opts: {
4586
4996
  }): VouchedWithin;
4587
4997
  /** Was this value built by `vouchedWithin`? A host asks before it drops the principal's check. */
4588
4998
  export declare function isVouchedWithin(value: unknown): value is VouchedWithin;
4999
+ /** The brand only `checkedWithin` can apply. */
5000
+ declare const checkedBrand: unique symbol;
5001
+ /**
5002
+ * A `within` root the PRINCIPAL must be allowed `permission` on, re-checked for as long
5003
+ * as the socket is open (#938). Built only by `checkedWithin`.
5004
+ */
5005
+ export interface CheckedWithin {
5006
+ readonly [checkedBrand]: true;
5007
+ readonly entity: EntityRef;
5008
+ readonly permission: PermissionKey;
5009
+ }
5010
+ /**
5011
+ * Watch `entity` and everything beneath it, for as long as the subscriber may
5012
+ * `permission` on `entity` itself.
5013
+ *
5014
+ * For a subscriber whose grant reaches the ROOT but not each row under it the way a
5015
+ * `liveTargets` key would: ticket0's portal customer holds `conversation:read-own` on
5016
+ * their contact, which reaches the conversation's public thread, while the per-row key
5017
+ * on a message is the staff read. Rooted at an entity whose subtree is exactly what that
5018
+ * key's read returns, the walk is the row filter and the key is the gate.
5019
+ *
5020
+ * The gate is asked at the handshake (`403` if it refuses) and again, once per socket,
5021
+ * on every post-commit pass that has a row beneath the root to announce. A refusal or a
5022
+ * check that throws closes the socket (`1008`) and sends nothing on that pass, so a
5023
+ * revoked grant, a principal who is gone, or a root relinked out of the grant's reach
5024
+ * ends the subscription instead of nudging it, and the client's reconnect meets the
5025
+ * handshake's `403`. A pass with nothing beneath the root asks nothing, so an idle
5026
+ * subscription costs no checks.
5027
+ *
5028
+ * **`checkedWithin` or `vouchedWithin`?** Use `checkedWithin` whenever the subscriber is
5029
+ * a principal with a grant that reaches the root: authority leaves with the grant. Use
5030
+ * `vouchedWithin` only when there is no such grant to ask, as for ticket0's widget
5031
+ * visitor, who holds a session token rather than a principal of their own. A vouched
5032
+ * socket is asserted once, so a token revoked mid-socket keeps receiving content-free
5033
+ * nudges until the socket closes. That is known and accepted for the widget: the nudge
5034
+ * names nothing, and every re-read it causes is checked again.
5035
+ *
5036
+ * Both send `LiveNudge` frames only: the subscriber may not read each row, so it is
5037
+ * never told which one changed. Only rows of a type some module declared in
5038
+ * `liveTargets` are announced, as for every other subscriber.
5039
+ */
5040
+ export declare function checkedWithin(entity: EntityRef, permission: string): CheckedWithin;
5041
+ /** Was this value built by `checkedWithin`? */
5042
+ export declare function isCheckedWithin(value: unknown): value is CheckedWithin;
4589
5043
  /**
4590
5044
  * Is this request asking to be upgraded to a WebSocket?
4591
5045
  *
@@ -4646,12 +5100,13 @@ export interface LiveChange {
4646
5100
  readonly at: string;
4647
5101
  }
4648
5102
  /**
4649
- * What a vouched subscriber is told (#1853): something beneath its root changed, and
4650
- * nothing else.
5103
+ * What a vouched or checked subscriber is told (#1853, #938): something beneath its root
5104
+ * changed, and nothing else.
4651
5105
  *
4652
- * No event type and no entity, deliberately. A vouched subscriber holds no read on the
4653
- * entities its frames are about — the vertical vouched for the ROOT, and the scope
4654
- * cannot know which rows under it the vertical's own read would show. Naming the type
5106
+ * No event type and no entity, deliberately. Such a subscriber holds no read on the
5107
+ * entities its frames are about — the vertical vouched for the ROOT, or the principal was
5108
+ * checked on the root alone, and the scope cannot know which rows under it the vertical's
5109
+ * own read would show. Naming the type
4655
5110
  * or the id would tell it what it never asked to read. The client re-reads, as it does
4656
5111
  * on a `LiveChange`.
4657
5112
  */