@substrat-run/kernel 0.139.0 → 0.141.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 (148) 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 +12 -1
  6. package/dist/attachment-extractor.d.ts.map +1 -1
  7. package/dist/attachment-extractor.js +10 -5
  8. package/dist/attachment-extractor.js.map +1 -1
  9. package/dist/audit-outcome.d.ts +149 -0
  10. package/dist/audit-outcome.d.ts.map +1 -0
  11. package/dist/audit-outcome.js +233 -0
  12. package/dist/audit-outcome.js.map +1 -0
  13. package/dist/capability.d.ts +5 -2
  14. package/dist/capability.d.ts.map +1 -1
  15. package/dist/capability.js +17 -8
  16. package/dist/capability.js.map +1 -1
  17. package/dist/connect-links.d.ts +91 -0
  18. package/dist/connect-links.d.ts.map +1 -0
  19. package/dist/connect-links.js +189 -0
  20. package/dist/connect-links.js.map +1 -0
  21. package/dist/connect-state.d.ts +12 -3
  22. package/dist/connect-state.d.ts.map +1 -1
  23. package/dist/connect-state.js +8 -3
  24. package/dist/connect-state.js.map +1 -1
  25. package/dist/derived-object.d.ts +14 -0
  26. package/dist/derived-object.d.ts.map +1 -0
  27. package/dist/derived-object.js +2 -0
  28. package/dist/derived-object.js.map +1 -0
  29. package/dist/derived-objects.d.ts +108 -0
  30. package/dist/derived-objects.d.ts.map +1 -0
  31. package/dist/derived-objects.js +199 -0
  32. package/dist/derived-objects.js.map +1 -0
  33. package/dist/email-intent.d.ts +56 -0
  34. package/dist/email-intent.d.ts.map +1 -0
  35. package/dist/email-intent.js +71 -0
  36. package/dist/email-intent.js.map +1 -0
  37. package/dist/entity-edges.d.ts +6 -1
  38. package/dist/entity-edges.d.ts.map +1 -1
  39. package/dist/entity-edges.js +13 -0
  40. package/dist/entity-edges.js.map +1 -1
  41. package/dist/entity-grant-shape.d.ts +94 -0
  42. package/dist/entity-grant-shape.d.ts.map +1 -0
  43. package/dist/entity-grant-shape.js +329 -0
  44. package/dist/entity-grant-shape.js.map +1 -0
  45. package/dist/entity-grant.d.ts +45 -0
  46. package/dist/entity-grant.d.ts.map +1 -0
  47. package/dist/entity-grant.js +71 -0
  48. package/dist/entity-grant.js.map +1 -0
  49. package/dist/entity-state-reads.d.ts +4 -7
  50. package/dist/entity-state-reads.d.ts.map +1 -1
  51. package/dist/entity-state-reads.js +13 -57
  52. package/dist/entity-state-reads.js.map +1 -1
  53. package/dist/entity-state.d.ts +37 -10
  54. package/dist/entity-state.d.ts.map +1 -1
  55. package/dist/entity-state.js +80 -69
  56. package/dist/entity-state.js.map +1 -1
  57. package/dist/entity-trash.d.ts +245 -0
  58. package/dist/entity-trash.d.ts.map +1 -0
  59. package/dist/entity-trash.js +380 -0
  60. package/dist/entity-trash.js.map +1 -0
  61. package/dist/grant-scoped-read.d.ts +41 -0
  62. package/dist/grant-scoped-read.d.ts.map +1 -0
  63. package/dist/grant-scoped-read.js +100 -0
  64. package/dist/grant-scoped-read.js.map +1 -0
  65. package/dist/index.d.ts +35 -8
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +22 -5
  68. package/dist/index.js.map +1 -1
  69. package/dist/kernel-outbox.d.ts +14 -0
  70. package/dist/kernel-outbox.d.ts.map +1 -0
  71. package/dist/kernel-outbox.js +37 -0
  72. package/dist/kernel-outbox.js.map +1 -0
  73. package/dist/list-index.d.ts +11 -2
  74. package/dist/list-index.d.ts.map +1 -1
  75. package/dist/list-index.js +29 -21
  76. package/dist/list-index.js.map +1 -1
  77. package/dist/mail-sender.d.ts +78 -0
  78. package/dist/mail-sender.d.ts.map +1 -0
  79. package/dist/mail-sender.js +2 -0
  80. package/dist/mail-sender.js.map +1 -0
  81. package/dist/membership-executor.d.ts +0 -52
  82. package/dist/membership-executor.d.ts.map +1 -1
  83. package/dist/membership-executor.js +19 -59
  84. package/dist/membership-executor.js.map +1 -1
  85. package/dist/migration-digest.d.ts +161 -0
  86. package/dist/migration-digest.d.ts.map +1 -0
  87. package/dist/migration-digest.js +237 -0
  88. package/dist/migration-digest.js.map +1 -0
  89. package/dist/module-erasure.d.ts +156 -0
  90. package/dist/module-erasure.d.ts.map +1 -0
  91. package/dist/module-erasure.js +440 -0
  92. package/dist/module-erasure.js.map +1 -0
  93. package/dist/peer.d.ts +9 -1
  94. package/dist/peer.d.ts.map +1 -1
  95. package/dist/peer.js +29 -0
  96. package/dist/peer.js.map +1 -1
  97. package/dist/permission-checker.d.ts +6 -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 +10 -1
  101. package/dist/permission-eval.d.ts.map +1 -1
  102. package/dist/permission-eval.js +51 -2
  103. package/dist/permission-eval.js.map +1 -1
  104. package/dist/platform-sweep.d.ts +1 -1
  105. package/dist/platform-sweep.d.ts.map +1 -1
  106. package/dist/platform-sweep.js +5 -0
  107. package/dist/platform-sweep.js.map +1 -1
  108. package/dist/scope-copy-ledger.d.ts +79 -0
  109. package/dist/scope-copy-ledger.d.ts.map +1 -0
  110. package/dist/scope-copy-ledger.js +82 -0
  111. package/dist/scope-copy-ledger.js.map +1 -0
  112. package/dist/scope-host.d.ts +402 -25
  113. package/dist/scope-host.d.ts.map +1 -1
  114. package/dist/scope-host.js +74 -5
  115. package/dist/scope-host.js.map +1 -1
  116. package/dist/search-index.d.ts +8 -0
  117. package/dist/search-index.d.ts.map +1 -1
  118. package/dist/search-index.js +51 -21
  119. package/dist/search-index.js.map +1 -1
  120. package/dist/spine-guard.d.ts +7 -0
  121. package/dist/spine-guard.d.ts.map +1 -1
  122. package/dist/spine-guard.js +54 -6
  123. package/dist/spine-guard.js.map +1 -1
  124. package/dist/spine-restore.d.ts.map +1 -1
  125. package/dist/spine-restore.js +5 -0
  126. package/dist/spine-restore.js.map +1 -1
  127. package/dist/sql-statements.d.ts +71 -0
  128. package/dist/sql-statements.d.ts.map +1 -0
  129. package/dist/sql-statements.js +191 -0
  130. package/dist/sql-statements.js.map +1 -0
  131. package/dist/subject-redaction.d.ts +9 -1
  132. package/dist/subject-redaction.d.ts.map +1 -1
  133. package/dist/subject-redaction.js.map +1 -1
  134. package/dist/system-switch-record.d.ts +3 -0
  135. package/dist/system-switch-record.d.ts.map +1 -1
  136. package/dist/system-switch-record.js +2 -0
  137. package/dist/system-switch-record.js.map +1 -1
  138. package/dist/table-ownership.d.ts +103 -0
  139. package/dist/table-ownership.d.ts.map +1 -0
  140. package/dist/table-ownership.js +285 -0
  141. package/dist/table-ownership.js.map +1 -0
  142. package/dist/timeline.d.ts +4 -4
  143. package/dist/timeline.js +4 -4
  144. package/dist/visible-continuation.d.ts +38 -0
  145. package/dist/visible-continuation.d.ts.map +1 -0
  146. package/dist/visible-continuation.js +162 -0
  147. package/dist/visible-continuation.js.map +1 -0
  148. package/package.json +4 -4
@@ -1,15 +1,19 @@
1
1
  import type { ModuleLog } from './module-log.js';
2
2
  import type { DeliveryRefusal } from './delivery-refusal.js';
3
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, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, DelegatedReadRecord, OwnerTransferAudit, MemberChangeAudit, 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, FindingEntry, FindingFilter, FindingRuleEntry, FindingRuleInput, FindingStatusInput, DeclaredMigration, CheckSubject } from '@substrat-run/contracts';
4
+ import type { SubjectRedactionCounts } from './subject-redaction.js';
5
+ 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';
5
6
  import type { ConnectionUseOutcome } from './connector-calls.js';
6
7
  import type { CapabilityVerbs } from './capability.js';
8
+ import type { AuditedOperationRef, AuditedOperationRow } from './audit-outcome.js';
7
9
  import { type EntityStateName } from '@substrat-run/contracts';
8
10
  import type { ModelUsageFilter, ModelUsageInput, ModelUsageWindow } from './model-usage.js';
9
11
  import type { FindingPruneReport } from './findings.js';
10
12
  import type { SealedSecret } from './secret-box.js';
13
+ import type { OnSubjectErased } from './module-erasure.js';
11
14
  import type { SystemSwitchReassert, SystemSwitchReassertOptions, SystemSwitchRecordFilter } from './system-switch-record.js';
12
15
  import type { SearchHit, SearchOptions } from './search-index.js';
16
+ import type { GrantedEntitiesPage } from './grant-scoped-read.js';
13
17
  import type { EntityVersion } from './entity-version.js';
14
18
  import type { UndrainedEvents } from './outbox-event.js';
15
19
  import type { JobDriveReport, JobHandler, JobRun, JobRunFilter, StartJobRunInput } from './job-run.js';
@@ -40,6 +44,11 @@ export interface PageParams {
40
44
  * archive. Refused for an entity that declares no archive. The bin is `ctx.pageTrashed`.
41
45
  */
42
46
  readonly view?: Exclude<EntityStateName, 'trashed'>;
47
+ /**
48
+ * Also return each row's own cursor, as `rowCursors` (#2073) — asked for by `pageVisible`,
49
+ * which may stop partway through a page. Off by default, so an ordinary page is unchanged.
50
+ */
51
+ readonly rowCursors?: boolean;
43
52
  }
44
53
  /**
45
54
  * The scope-host contract — the adapter seam (§5.1 of the design doc).
@@ -90,6 +99,13 @@ export interface OperationContext {
90
99
  readonly tenantId: TenantId;
91
100
  readonly scopeId: ScopeId;
92
101
  readonly principal: PrincipalId;
102
+ /** Seal positions for one filtered list under this scope, caller and operation. */
103
+ pageContinuation(list: string, query: unknown): {
104
+ /** Read-only and copy walks cannot persist a hidden budget-stop position. */
105
+ writable?: boolean;
106
+ open(cursor: string): Promise<string>;
107
+ seal(position: string, hidden: boolean): Promise<string | null>;
108
+ };
93
109
  readonly sql: ScopedSql;
94
110
  /**
95
111
  * The operation's instant (#812) — the ONLY clock module code may read.
@@ -209,6 +225,18 @@ export interface OperationContext {
209
225
  * than returning a denial, and records none (#1642, `assertPermissionKey`).
210
226
  */
211
227
  check(permission: PermissionKey, entity?: EntityRef): Promise<Decision>;
228
+ /**
229
+ * Page entity ids reachable through this caller's live grants and declared parent edges.
230
+ * `all` means a node grant makes every entity of this type visible; `incomplete` means
231
+ * this checker cannot enumerate the path, so callers must use a checked full walk.
232
+ * An `ids` page is complete only after its cursor becomes null. Ids may repeat across
233
+ * pages on multiple parent paths: accumulate a Set, and do not treat page lengths as
234
+ * a count or total. The union of all pages is complete. Every id passes `ctx.check`.
235
+ */
236
+ grantedEntities(permission: PermissionKey, entityType: string, options?: {
237
+ limit?: number;
238
+ cursor?: string;
239
+ }): Promise<GrantedEntitiesPage>;
212
240
  /**
213
241
  * Find entities of one type by what a person typed (#827) — the read a picker
214
242
  * over 40 000 customers needs and `ctx.sql` cannot express without every
@@ -265,9 +293,9 @@ export interface OperationContext {
265
293
  *
266
294
  * **This does not check permission** — nothing on `ctx` does, and a paged read
267
295
  * is not an exception. The operation's own `assertAllowed` still comes first.
268
- * A read that filters per ROW after the fact (a portal walk) cannot use this at
269
- * all: a page of 20 filtered down to 3 is not a page, and the honest shape is an
270
- * over-fetch loop the handler owns.
296
+ * A read that filters per ROW after the fact (a portal walk) cannot hand this
297
+ * page on as it is: a page of 20 filtered down to 3 is not a page, and its cursor
298
+ * may be a refused row's. It walks this read with `pageVisible` instead (#2073).
271
299
  *
272
300
  * Throws `NotListable` for an entity no operation declared `paged.over` on,
273
301
  * `SortNotDeclared` for a `?sort=` outside the vocabulary, and
@@ -334,6 +362,12 @@ export interface OperationContext {
334
362
  * revives it and records that as one `entity.linked` spine event, since access resumes.
335
363
  *
336
364
  * Checks no permission: the operation does, in its own vocabulary.
365
+ *
366
+ * A parent in the TRASH (#119) is refused `not_found`: nothing new hangs off a binned
367
+ * entity, since it is gone from everyone's view until restored. A MISSING parent of a type that
368
+ * declares trash gets the identical refusal, so the two cannot be told apart. An ARCHIVED parent
369
+ * is allowed — archived is filed away and still readable, and whether one may still be added to
370
+ * is the vertical's rule, not the kernel's.
337
371
  */
338
372
  link(child: EntityRef, parent: EntityRef): void;
339
373
  /**
@@ -346,6 +380,9 @@ export interface OperationContext {
346
380
  * move would make the child its own ancestor.
347
381
  * - `from` must be a live parent edge of `child`; `conflict` otherwise. Every other
348
382
  * parent a multi-parent entity has is left alone.
383
+ * - A `to` in the TRASH is refused `not_found`, as `link` refuses one (#119) — the same refusal a
384
+ * missing `to` of that type gets. `from` is not: moving a child out from under a binned
385
+ * parent is how it is rescued.
349
386
  * - `from` equal to `to` is a no-op: nothing written, nothing emitted.
350
387
  * - A `to` that is ALREADY a live parent of `child` is the way to detach one parent of a
351
388
  * multi-parent child (#2044): the `from` edge is tombstoned, the live `to` edge stays
@@ -876,9 +913,9 @@ export type MembershipChangeResult = {
876
913
  * It receives `HostAdmin`, not `ctx`: it acts with platform authority, which is
877
914
  * precisely what module code must never hold. Admin writes it makes through the `admin`
878
915
  * it is handed are stamped with the causing event's id (`causedBy`), so the split trail
879
- * joins. That `admin` is bound to this one event, never the host's own: a handler that
880
- * writes through the host instead — `host.attributed(…)` — passes `{ causedBy: event.id }`
881
- * itself (#2055).
916
+ * joins. That `admin` is bound to this one event, never the host's own (#2055). A handler
917
+ * that writes for a person attributes from it — `admin.attributed(onBehalfOf)` — which keeps
918
+ * the event (#2069); a view of the HOST knows nothing of the event.
882
919
  *
883
920
  * A handler that decides an event must never be effected RETURNS `refuseDelivery(reason)`
884
921
  * (#1184). The delivery is journaled terminal with the reason, never retried. A return
@@ -1491,6 +1528,11 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1491
1528
  * nothing, and it reads as coverage. A bound operation with no entry is
1492
1529
  * allowed and means what it always meant — nothing was declared to parse.
1493
1530
  *
1531
+ * **It also carries the declared surface (#119).** `operationInputsOf` records, for the frozen
1532
+ * map it returns, every declared operation and the entity each addresses by id. The host DERIVES
1533
+ * its trash refusal from that — nothing else is handed over — and a module with a trashable
1534
+ * entity must pass that map as returned, with every operation it binds declared in it.
1535
+ *
1494
1536
  * Typed structurally rather than as `z.ZodType` so the kernel keeps its single
1495
1537
  * dependency and no zod version is pinned by the scope-host contract. The
1496
1538
  * shape is the whole surface the host uses: throw to refuse, return the value
@@ -1567,6 +1609,15 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1567
1609
  * contribute the same name.
1568
1610
  */
1569
1611
  predicates?: Record<string, GuardPredicate>;
1612
+ /**
1613
+ * This module's own step in a subject erasure (#2068), for the rows its declared
1614
+ * `erasure: { subjects }` cannot reach — a link the row does not hold itself. Run by
1615
+ * `shredSubject` inside the erasure's one transaction, after the declared entities and
1616
+ * before the key is destroyed; a throw refuses the whole erasure and rolls it back.
1617
+ * Synchronous, idempotent, and handed a `sql` that reaches the module's own tables only
1618
+ * (`module-erasure.ts`). Requires `manifest.erasure`, whose `tables` is that reach.
1619
+ */
1620
+ onSubjectErased?: OnSubjectErased;
1570
1621
  }
1571
1622
  /**
1572
1623
  * Admin surface for enforcement input (design doc §4; control-plane.md §4.4).
@@ -1591,6 +1642,16 @@ export interface ModuleRegistration<C extends readonly EventContract[] = []> {
1591
1642
  * stay sync — they are code-time bookkeeping, not control-plane state.)
1592
1643
  */
1593
1644
  export interface HostAdmin {
1645
+ /**
1646
+ * This same admin, with every row written through it also naming `onBehalfOf` (#977, #2069),
1647
+ * and keeping whatever this one already carries — for the `admin` an executor handler is
1648
+ * handed, the event (`causedBy`). A handler that writes for a person attributes from that
1649
+ * `admin`, so the cause comes along by construction and there is no option to forget.
1650
+ *
1651
+ * Optional so an admin that predates it still satisfies the interface; a caller that finds it
1652
+ * absent writes through the admin it holds, unattributed to a person.
1653
+ */
1654
+ attributed?(onBehalfOf: OnBehalfOf): HostAdmin;
1594
1655
  defineRole(actor: PlatformActorId, tenantId: TenantId, role: RoleDefinition): Promise<void>;
1595
1656
  /**
1596
1657
  * Every role the directory holds, ordered by (tenantId, key).
@@ -1622,6 +1683,57 @@ export interface HostAdmin {
1622
1683
  */
1623
1684
  unassignRole(actor: PlatformActorId, assignment: RoleAssignment): Promise<void>;
1624
1685
  grant(actor: PlatformActorId, grant: CapabilityGrant): Promise<void>;
1686
+ /**
1687
+ * Give a person a vertical's declared entity-grant SHAPE on one entity (#2071): every key in
1688
+ * `permissions`, plus the marker that makes them a holder of the shape, so a key the shape
1689
+ * gains later reaches them at the next {@link reconcileEntityGrantShapes}. What a vertical
1690
+ * calls where it used to `grant` its `ENTITY_GRANTS` keys one at a time. It brings back a key
1691
+ * or marker a revoke tombstoned, as `grant` does, and leaves a live row as it is, expiry
1692
+ * included, as `ctx.grant` does. Audited as `grantEntityShape`.
1693
+ */
1694
+ grantEntityShape(actor: PlatformActorId, grant: {
1695
+ principalId: PrincipalId;
1696
+ node: {
1697
+ tenantId: TenantId;
1698
+ scopeId: ScopeId;
1699
+ };
1700
+ entity: EntityRef;
1701
+ permissions: readonly PermissionKey[];
1702
+ grantedBy: PrincipalId;
1703
+ }): Promise<void>;
1704
+ /**
1705
+ * Top every holder of each declared shape up to the shape as it is now (#2071): a key the
1706
+ * shape gained is granted to each person holding the shape's marker on an entity of that
1707
+ * type. Never a key revoked from that person there (its K-21 tombstone stays). A key merely
1708
+ * dropped from the shape is left in place; a key the shape declares `retired` (#2082) is taken
1709
+ * back from every holder instead, its row tombstoned, once per scope — including a direct grant
1710
+ * of that key to the holder on the same entity, which is the same row. Until it is done on a scope, a
1711
+ * shape that declares a `holder` is also backfilled: each person its holder relationship names
1712
+ * as the owner of an entity (`'self'`, or the vertical's own column) is marked if they hold a
1713
+ * key of the shape there. Never inferred from key sets, so a grant delegated on someone else's
1714
+ * record is never marked. Without a `holder`, there is no backfill.
1715
+ *
1716
+ * Only shapes declared `bootstrap: true` are reconciled — those given with
1717
+ * {@link grantEntityShape} on a person's own record. A SHARING shape, reached through
1718
+ * `ctx.grant` (todo's `list`), is skipped whole: reconciling one would mark every person
1719
+ * something was fully shared with as a holder.
1720
+ *
1721
+ * Bounded: `batch` rows of work (default 500, an integer from 1 to 5000, or `validation_failed`)
1722
+ * per scope transaction, backfill included, repeated until a pass finishes — so it is safe
1723
+ * on a large scope, and finishes on a re-run if interrupted. Each (person, entity) topped up is
1724
+ * an `entity.grants-topped-up` event on the entity, and each one retired keys were taken from an
1725
+ * `entity.grants-retired` event. Audited as `reconcileEntityGrantShapes` when it changed
1726
+ * anything. Returns how many (person, entity) it topped up, and how many it retired keys from.
1727
+ */
1728
+ reconcileEntityGrantShapes(actor: PlatformActorId, node: {
1729
+ tenantId: TenantId;
1730
+ scopeId: ScopeId;
1731
+ }, shapes: readonly EntityGrantShape[], opts?: {
1732
+ batch?: number;
1733
+ }): Promise<{
1734
+ toppedUp: number;
1735
+ retired: number;
1736
+ }>;
1625
1737
  /** Grant to an organization (portal customers); members reach it via membership tuples. */
1626
1738
  /**
1627
1739
  * Grant a permission to a CONNECTION (#97) — how a connector is allowed to
@@ -2096,6 +2208,9 @@ export interface HostAdmin {
2096
2208
  snapshot?: boolean;
2097
2209
  acknowledge?: BindAcknowledgement;
2098
2210
  expectedVersionId?: string | null;
2211
+ expectedErasureEpoch?: number;
2212
+ /** #1722: the copy move this bind routes the scope onto, confirmed in the same write. */
2213
+ confirmMove?: ScopeCopyMoveConfirmation;
2099
2214
  }): Promise<void>;
2100
2215
  /**
2101
2216
  * Which apps in the scope's tenant binding `versionId` would break (#1756): the answer
@@ -2168,6 +2283,8 @@ export interface HostAdmin {
2168
2283
  */
2169
2284
  setScopeServingRef(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, servingRef: string | null, opts?: {
2170
2285
  acknowledge?: BindAcknowledgement;
2286
+ expectedErasureEpoch?: number;
2287
+ confirmMove?: ScopeCopyMoveConfirmation;
2171
2288
  }): Promise<void>;
2172
2289
  /**
2173
2290
  * Move a fork's GC deadline (preview-and-snapshots.md §9). The reap sweep deletes any
@@ -2185,6 +2302,54 @@ export interface HostAdmin {
2185
2302
  * write revision before and after. The directory changes nothing else. Refuses an unknown scope.
2186
2303
  */
2187
2304
  recordKeptCopyResolution(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, resolution: KeptCopyResolution): Promise<void>;
2305
+ /** A script that holds or held this scope's data. The moveId fences delayed cleanup
2306
+ * from a later rollback and carry through the same script. */
2307
+ recordScopeScriptCopy(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, scriptRef: string, moveId: string,
2308
+ /** Which end of the move this script is, and for a destination the load stamp its restore
2309
+ * will leave: what a crash-recovery sweep fences its wipe on. The entry is leased to the
2310
+ * recording move for `leaseMs` (default `SCOPE_COPY_LEASE_MS`). */
2311
+ opts?: {
2312
+ role?: ScopeCopyRole;
2313
+ loadStamp?: string | null;
2314
+ leaseMs?: number;
2315
+ }): Promise<void>;
2316
+ /**
2317
+ * Claim up to `limit` pending entries whose lease ran out before `now` (a move that crashed,
2318
+ * or a sweep that did), leasing each to `owner` until `leaseUntil`. An entry whose lease is
2319
+ * live is never returned, so a move still in flight keeps it; and a move whose lease ran out
2320
+ * can no longer confirm (`confirmMove`), so the claimant cannot lose a race to it.
2321
+ */
2322
+ claimExpiredScopeScriptCopies(actor: PlatformActorId, input: {
2323
+ now: string;
2324
+ leaseUntil: string;
2325
+ owner: string;
2326
+ limit: number;
2327
+ }): Promise<ScopeScriptCopy[]>;
2328
+ /** Reserve a scope for script cleanup only when no copy move is in flight. The claim is a
2329
+ * column on the directory row, not a ledger entry: it refuses new moves and the conditional
2330
+ * bind until the row itself is deleted, and a crashed reaper resumes it. */
2331
+ beginScopeScriptReap(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId): Promise<void>;
2332
+ /** Only a confirmed move makes a source eligible for a nonterminal wipe. */
2333
+ settleScopeScriptCopy(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, scriptRef: string, moveId: string, state: 'eligible' | 'retained' | 'kept' | 'done', marker?: {
2334
+ loadStamp: string | null;
2335
+ revision: string | null;
2336
+ },
2337
+ /** A sweep's own claim: the entry settles only while it is still pending under that owner. */
2338
+ opts?: {
2339
+ claimedBy?: string;
2340
+ }): Promise<boolean>;
2341
+ /** Move a failed retry to the back of the due queue without changing its state. */
2342
+ touchScopeScriptCopy(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, scriptRef: string, moveId: string): Promise<void>;
2343
+ listScopeScriptCopies(actor: PlatformActorId, filter: {
2344
+ tenantId?: TenantId;
2345
+ scopeId?: ScopeId;
2346
+ state?: 'pending' | 'eligible' | 'retained' | 'kept' | 'done';
2347
+ limit?: number;
2348
+ after?: {
2349
+ scriptRef: string;
2350
+ moveId: string;
2351
+ };
2352
+ }): Promise<ScopeScriptCopy[]>;
2188
2353
  /**
2189
2354
  * When each of a CO-LOCATED scope's migrations actually ran (#1236), newest
2190
2355
  * first. `_substrat_migrations.applied_at` has been written since the table
@@ -2308,6 +2473,17 @@ export interface HostAdmin {
2308
2473
  * per-call path, before any staff member is involved.
2309
2474
  */
2310
2475
  resolveVerticalInstance(tenantId: TenantId, vertical: string): Promise<VerticalResolution>;
2476
+ /** Resolve for one caller at execution time. An explicit stale binding refuses without fallback. */
2477
+ resolvePeerInstance(tenantId: TenantId, callerScopeId: ScopeId, vertical: string): Promise<PeerInstanceResolution>;
2478
+ /** Read the caller's explicit choice, if any. Tenant-scoped and recorded as an admin read. */
2479
+ peerBinding(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId, vertical: string): Promise<PeerBinding | undefined>;
2480
+ /** Every explicit target choice this calling scope currently holds. */
2481
+ peerBindings(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId): Promise<PeerBinding[]>;
2482
+ /** Bind or clear this caller's target. Both endpoints are checked in the directory at write. */
2483
+ setPeerBinding(actor: PlatformActorId, tenantId: TenantId, callerScopeId: ScopeId, vertical: string, targetScopeId: ScopeId | null): Promise<{
2484
+ changed: boolean;
2485
+ previous: ScopeId | null;
2486
+ }>;
2311
2487
  /**
2312
2488
  * Persist a tenant. Idempotent on the id — re-creating an existing tenant is a
2313
2489
  * no-op, not an error (control-plane.md §4.1). `status` starts `active` and
@@ -2801,6 +2977,17 @@ export interface HostAdmin {
2801
2977
  * undocumented gets oversold by someone who was not in the room.
2802
2978
  */
2803
2979
  shredSubject(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, subjectId: string): Promise<SubjectShredReceipt>;
2980
+ /** Monotone directory fence against a carry from before subject erasure. */
2981
+ scopeErasureEpoch(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId): Promise<number>;
2982
+ /** Finish an orchestrated erasure only after every script holding a copy has redacted it.
2983
+ * The copy count fences a move that was recorded after the coordinator listed scripts;
2984
+ * ledger rows are append-only, so a changed count means that inventory is stale. */
2985
+ finalizeSubjectShred(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, subjectId: string, redactions: readonly SubjectRedactionCounts[], expected: {
2986
+ versionId: string | null;
2987
+ servingRef: string | null;
2988
+ epoch: number;
2989
+ copyCount: number;
2990
+ }): Promise<SubjectShredReceipt>;
2804
2991
  /**
2805
2992
  * Open a session that acts as `input.principal`, and record it.
2806
2993
  *
@@ -2952,8 +3139,11 @@ export interface HostAdmin {
2952
3139
  * the audit metadata — the rotate-side analogue of `createdBy` on create
2953
3140
  * (§3.5.1). Omitted ⇒ the effecting actor stands alone, the platform-driven
2954
3141
  * refresh path.
3142
+ *
3143
+ * `expiresAt`: a date replaces the connection's expiry, `null` clears it (the credential
3144
+ * that had one was replaced by one that has none), and omitted keeps it.
2955
3145
  */
2956
- updateConnectionSecret(actor: PlatformActorId, id: ConnectionId, secret: ConnectionSecret, expiresAt?: string, opts?: {
3146
+ updateConnectionSecret(actor: PlatformActorId, id: ConnectionId, secret: ConnectionSecret, expiresAt?: string | null, opts?: {
2957
3147
  rotatedBy?: string;
2958
3148
  }): Promise<void>;
2959
3149
  /**
@@ -2963,6 +3153,37 @@ export interface HostAdmin {
2963
3153
  * constraint ignores revoked rows.
2964
3154
  */
2965
3155
  revokeConnection(actor: PlatformActorId, id: ConnectionId): Promise<void>;
3156
+ /**
3157
+ * A vertical's MAILED connect link (connections.md §3.5.4) — the row that makes a consent
3158
+ * round single-use and revocable when it lives days rather than minutes. Beside the
3159
+ * connections because it is their front door: the round it starts ends in
3160
+ * `createConnection` (or a rotation) for the same tenant and scope.
3161
+ *
3162
+ * Every verb below is keyed by (tenant, scope): a link of another scope or tenant is
3163
+ * `undefined` / `unknown`, never a refusal that admits it exists. Expiry is judged against
3164
+ * the host's own clock. Mint, revoke, consume and restore are each audited — metadata
3165
+ * only, though nothing here is secret: the credential does not exist until a round is
3166
+ * consumed, and it never touches this row.
3167
+ */
3168
+ mintConnectLink(actor: PlatformActorId, input: MintConnectLinkInput): Promise<ConnectLink>;
3169
+ getConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<ConnectLink | undefined>;
3170
+ /** Newest first. `outstandingOnly`: outstanding AND unexpired — the links that still open. */
3171
+ listConnectLinks(actor: PlatformActorId, filter: ConnectLinkFilter): Promise<ConnectLink[]>;
3172
+ /**
3173
+ * Outstanding → revoked. Idempotent: a revoked or used link is answered as it stands and
3174
+ * nothing is written. `undefined` when the scope holds no such link.
3175
+ */
3176
+ revokeConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<ConnectLink | undefined>;
3177
+ /**
3178
+ * Outstanding and unexpired → used, recording what the consent attached. Atomic: of two
3179
+ * concurrent consumes exactly one answers `ok`. Otherwise the typed reason.
3180
+ */
3181
+ consumeConnectLink(actor: PlatformActorId, input: ConsumeConnectLinkInput): Promise<ConnectLinkConsume>;
3182
+ /**
3183
+ * Used → outstanding, the best-effort undo after the credential store failed. Only a
3184
+ * used, unexpired link moves; `false` for anything else, a revoked link included.
3185
+ */
3186
+ restoreConnectLink(actor: PlatformActorId, key: ConnectLinkKey): Promise<boolean>;
2966
3187
  /**
2967
3188
  * Open the credential for one (tenant, vertical, provider[, account]) — the
2968
3189
  * connector's read, and the only path in the system that yields plaintext.
@@ -3263,6 +3484,27 @@ export interface HostAdmin {
3263
3484
  * cannot be written.
3264
3485
  */
3265
3486
  recordMemberChange(actor: PlatformActorId, entry: MemberChangeAudit): Promise<void>;
3487
+ /** One intent or outcome of a tenant's peer-instance binding change (#1720). */
3488
+ recordPeerBindingChange(actor: PlatformActorId, entry: PeerBindingAudit): Promise<void>;
3489
+ /**
3490
+ * Close one audited change whose intent has no outcome (#2064): in ONE transaction, read the
3491
+ * intent `intentId` names, and if no outcome row exists for its operation, write an `unknown`
3492
+ * outcome carrying `error` and an ops-failure row (`audit.<action>`, stage `outcome-unknown`)
3493
+ * for the staff digest. Answers `true` when it wrote them, `false` when an outcome was already
3494
+ * there, so concurrent callers settle an operation once. Throws `not_found` for an id that is
3495
+ * not an audited-change intent. See `audit-outcome.ts` for why a later real outcome still wins.
3496
+ */
3497
+ settleUnrecordedOutcome(actor: PlatformActorId, input: {
3498
+ intentId: string;
3499
+ error: string;
3500
+ }): Promise<boolean>;
3501
+ /**
3502
+ * Every admin-log row of the given audited operations (#2064), read through the operation-id
3503
+ * index in bounded batches — one statement per `AUDITED_OPERATIONS_BATCH` ids, never a scan of
3504
+ * the log. Only rows of the exact (action, operation, tenant, scope) asked about are returned.
3505
+ * Reading the audit trail is itself recorded, once per call.
3506
+ */
3507
+ auditedOperations(actor: PlatformActorId, refs: readonly AuditedOperationRef[]): Promise<AuditedOperationRow[]>;
3266
3508
  /**
3267
3509
  * Stamp `drainedAt` on every not-yet-drained access row up to and including
3268
3510
  * `upToId`, marking them shipped to Tier 2. Returns how many rows moved.
@@ -3828,6 +4070,38 @@ export interface OpsFailureInput {
3828
4070
  /** The upstream provider's trace reference, when the message carried one. */
3829
4071
  reference?: string | null;
3830
4072
  }
4073
+ /** How long a recorded copy move owns its pending entries before a sweep may settle them (#1722). */
4074
+ export declare const SCOPE_COPY_LEASE_MS: number;
4075
+ /** Which end of a copy move a script is (#1722). Null on an entry recorded without one. */
4076
+ export type ScopeCopyRole = 'source' | 'destination';
4077
+ /**
4078
+ * A copy move's confirmation, written with the bind that routes the scope onto its destination
4079
+ * (#1722). The bind lands only while every entry of the move is still pending, leased to the move
4080
+ * (no sweep has claimed it) and inside its lease; in the same write the destination entries
4081
+ * settle `done` (the route reaches them) and the source entries settle as the `source` field
4082
+ * says, with `sourceMarker` as the marker a later fenced wipe expects.
4083
+ */
4084
+ export interface ScopeCopyMoveConfirmation {
4085
+ moveId: string;
4086
+ source: 'eligible' | 'retained';
4087
+ sourceMarker?: {
4088
+ loadStamp: string | null;
4089
+ revision: string | null;
4090
+ };
4091
+ }
4092
+ /** Directory inventory of one script's copy of a scope (#1722). */
4093
+ export interface ScopeScriptCopy {
4094
+ tenantId: TenantId;
4095
+ scopeId: ScopeId;
4096
+ scriptRef: string;
4097
+ moveId: string;
4098
+ state: 'pending' | 'eligible' | 'retained' | 'kept' | 'done';
4099
+ loadStamp: string | null;
4100
+ revision: string | null;
4101
+ role: ScopeCopyRole | null;
4102
+ /** Until when the pending entry belongs to its move (or to the sweep that claimed it). */
4103
+ leaseUntil: string | null;
4104
+ }
3831
4105
  /** One staff resolution of a kept copy (#1722), as `recordKeptCopyResolution` logs it. */
3832
4106
  export interface KeptCopyResolution {
3833
4107
  /** `discard` wiped the kept copy; `restore-forward` restored it over the live store, then wiped it;
@@ -3908,10 +4182,19 @@ export interface ScopeHost {
3908
4182
  * Optional so a host that predates it still satisfies the interface; a transport
3909
4183
  * that finds it absent writes unattributed rows, which is what every row was before.
3910
4184
  *
3911
- * `options.causedBy` (#2055) is the event whose effect the view's writes are (K-22): an
3912
- * executor handler passes its own event's id, because the host stamps nothing ambiently —
3913
- * a field set around the handler's `await` would also stamp every other admin call the
3914
- * host served meanwhile. The `admin` a handler is handed already carries it.
4185
+ * A view of the host knows nothing of any event: an executor handler attributes from the
4186
+ * `admin` it is handed instead (`HostAdmin.attributed`), which keeps its `causedBy`.
4187
+ */
4188
+ attributed?(onBehalfOf: OnBehalfOf): ScopeHost;
4189
+ /**
4190
+ * The same view, its rows also naming `options.causedBy` as the event they are the effect
4191
+ * of (K-22, #2055).
4192
+ *
4193
+ * @deprecated since #2069: an executor handler attributes from the `admin` it is handed —
4194
+ * `admin.attributed(onBehalfOf)` — which carries its event by construction. This form needs
4195
+ * the event passed again, and a handler that forgets writes `causedBy` NULL with nothing
4196
+ * failing. It stays because it shipped, and because it is how the membership executor still
4197
+ * attributes on a host whose handed admin predates `HostAdmin.attributed`.
3915
4198
  */
3916
4199
  attributed?(onBehalfOf: OnBehalfOf, options?: {
3917
4200
  causedBy?: string;
@@ -4270,9 +4553,10 @@ export interface ScopeHost {
4270
4553
  * NOW. So a capability reads no attachment its minter could not read at this moment, and
4271
4554
  * none outside its subtree. The session is re-resolved on EVERY call, as on the invoke
4272
4555
  * door: a revoked or expired capability, or an expired session, refuses `unauthenticated`
4273
- * before any key is checked; a capability carrying an operation allowlist refuses
4274
- * `forbidden`, since no attachment verb is an operation it could have named. A read never
4275
- * takes a use — a use is an exchange.
4556
+ * before any key is checked. An operations-narrowed capability refuses reads by default;
4557
+ * its mint may set `attachments: 'read'` to opt into list, open and search. That field
4558
+ * does not replace the `readPermission` check above. A read never takes a use — a use is
4559
+ * an exchange.
4276
4560
  *
4277
4561
  * **Writes are refused.** `upload` and `remove` throw `capabilityAttachmentWriteRefused`
4278
4562
  * whatever keys the capability carries, recorded in the denial log against
@@ -4618,6 +4902,11 @@ export interface ScopeHost {
4618
4902
  lastError?: string | null;
4619
4903
  /** WHO refused (#841). Omitted by a caller too old to attribute — stored as NULL. */
4620
4904
  failure?: PlatformRequestFailure | null;
4905
+ /**
4906
+ * #2102: an outcome event written into the scope with the settle, in its transaction,
4907
+ * and only when this settle moves the row out of `pending` (`settlePlatformRequestIn`).
4908
+ */
4909
+ event?: PlatformOutcomeEvent | null;
4621
4910
  }): Promise<void>;
4622
4911
  /**
4623
4912
  * Live reads (#938) — a subscription to this scope's change feed, or absent.
@@ -4671,10 +4960,12 @@ export interface ScopeHost {
4671
4960
  * changed at 14:02 is information about that row, so "the body was empty" is not a
4672
4961
  * defence: the filter runs whether or not there is a payload to withhold.
4673
4962
  *
4674
- * A subscription may be narrowed `within` one entity (#1853), which only removes frames;
4675
- * the one exception to the per-principal check is a `within` the vertical built with
4676
- * `vouchedWithin`, whose subscriber is sent bare `LiveNudge` frames and nothing that
4677
- * names an entity.
4963
+ * A subscription may be narrowed `within` one entity (#1853), which only removes frames.
4964
+ * Two built roots replace the per-row check with one about the ROOT, and their subscribers
4965
+ * are sent bare `LiveNudge` frames that name no entity: `checkedWithin`, whose principal must
4966
+ * pass a stated key on the root at the handshake and again on every pass that has something
4967
+ * to send (#938), and `vouchedWithin`, which the vertical asserts once and nothing re-checks
4968
+ * (#1853). Which to use is on `checkedWithin`.
4678
4969
  *
4679
4970
  * **Generic over the runtime's request and response, because the kernel names
4680
4971
  * neither.** This package has one dependency and no DOM or workers lib
@@ -4727,10 +5018,51 @@ export interface LiveReadSurface<Req extends LiveUpgradeRequest = LiveUpgradeReq
4727
5018
  * the same trust it already extends in naming `principal`, and the scope's walk is
4728
5019
  * the whole filter. Such a subscriber receives `LiveNudge` frames only, which name
4729
5020
  * no event type and no entity.
5021
+ * - **A `checkedWithin(…)` value replaces the per-row check with one on the root**
5022
+ * (#938). The principal must pass the stated key on the root, at the handshake (a
5023
+ * `403` otherwise) and again on every pass that has a row beneath it to announce,
5024
+ * so a grant withdrawn, or a root moved out from under it, closes the socket
5025
+ * instead of nudging it. `LiveNudge` frames only, as for a vouched root.
5026
+ *
5027
+ * **Freshness (Codex #2077 r5).** A decision is reused within a pass only while the
5028
+ * scope has had no write and nothing it holds has reached its `expires_at` since the
5029
+ * decision. It is never reused while the scope reads its permissions from the directory.
5030
+ * Both are checked against the clock immediately before each send. A change that lands
5031
+ * while a check is still being evaluated can let that one frame through. Frames name
5032
+ * nothing the subscriber could not read when checked, and the next frame, pass and poll
5033
+ * see the change: a push is a hint at most one evaluation stale.
5034
+ */
5035
+ within?: EntityRef | VouchedWithin | CheckedWithin;
5036
+ /**
5037
+ * When the credential that proved `principal` stops being valid (ISO 8601), so the
5038
+ * socket never outlives it (#938). Pass the session's or the bearer's expiry
5039
+ * (`AuthSubject.expiresAt`). A handshake at or past it is refused, and the scope closes
5040
+ * the socket (`1008`) on its first pass at or past it, before sending anything on that
5041
+ * pass. Absent: the socket lives until either end closes it, as before.
4730
5042
  */
4731
- within?: EntityRef | VouchedWithin;
5043
+ expiresAt?: string;
4732
5044
  }): Promise<Res>;
4733
5045
  }
5046
+ /**
5047
+ * The close codes a live socket ends with, so a client can tell "try again" from "poll".
5048
+ *
5049
+ * - `1008` (policy): the subscriber may no longer watch what it subscribed to — a
5050
+ * `checkedWithin` gate refused, or the session that opened it ended. A reconnect meets
5051
+ * the handshake's own refusal.
5052
+ * - `4429`: this principal already holds `LIVE_SOCKETS_PER_PRINCIPAL` sockets on the scope.
5053
+ * Not a reason to retry: the client should poll, which it does anyway, and stop asking.
5054
+ */
5055
+ export declare const LIVE_CLOSE: {
5056
+ readonly revoked: 1008;
5057
+ readonly tooMany: 4429;
5058
+ };
5059
+ /**
5060
+ * How many live sockets one principal may hold on one scope (#938). A tab holds one per
5061
+ * feed (the desk's, and one per open conversation in ticket0's portal), and each socket is
5062
+ * work on every post-commit pass, so a principal opening more is a cost on everybody else
5063
+ * writing to the scope. Eight covers several tabs; past it a socket is closed `4429`.
5064
+ */
5065
+ export declare const LIVE_SOCKETS_PER_PRINCIPAL = 8;
4734
5066
  /** The brand only `vouchedWithin` can apply — a literal cannot type-check as one. */
4735
5067
  declare const vouchedBrand: unique symbol;
4736
5068
  /**
@@ -4762,6 +5094,50 @@ export declare function vouchedWithin(entity: EntityRef, opts: {
4762
5094
  }): VouchedWithin;
4763
5095
  /** Was this value built by `vouchedWithin`? A host asks before it drops the principal's check. */
4764
5096
  export declare function isVouchedWithin(value: unknown): value is VouchedWithin;
5097
+ /** The brand only `checkedWithin` can apply. */
5098
+ declare const checkedBrand: unique symbol;
5099
+ /**
5100
+ * A `within` root the PRINCIPAL must be allowed `permission` on, re-checked for as long
5101
+ * as the socket is open (#938). Built only by `checkedWithin`.
5102
+ */
5103
+ export interface CheckedWithin {
5104
+ readonly [checkedBrand]: true;
5105
+ readonly entity: EntityRef;
5106
+ readonly permission: PermissionKey;
5107
+ }
5108
+ /**
5109
+ * Watch `entity` and everything beneath it, for as long as the subscriber may
5110
+ * `permission` on `entity` itself.
5111
+ *
5112
+ * For a subscriber whose grant reaches the ROOT but not each row under it the way a
5113
+ * `liveTargets` key would: ticket0's portal customer holds `conversation:read-own` on
5114
+ * their contact, which reaches the conversation's public thread, while the per-row key
5115
+ * on a message is the staff read. Rooted at an entity whose subtree is exactly what that
5116
+ * key's read returns, the walk is the row filter and the key is the gate.
5117
+ *
5118
+ * The gate is asked at the handshake (`403` if it refuses) and again, once per socket,
5119
+ * on every post-commit pass that has a row beneath the root to announce. A refusal or a
5120
+ * check that throws closes the socket (`1008`) and sends nothing on that pass, so a
5121
+ * revoked grant, a principal who is gone, or a root relinked out of the grant's reach
5122
+ * ends the subscription instead of nudging it, and the client's reconnect meets the
5123
+ * handshake's `403`. A pass with nothing beneath the root asks nothing, so an idle
5124
+ * subscription costs no checks.
5125
+ *
5126
+ * **`checkedWithin` or `vouchedWithin`?** Use `checkedWithin` whenever the subscriber is
5127
+ * a principal with a grant that reaches the root: authority leaves with the grant. Use
5128
+ * `vouchedWithin` only when there is no such grant to ask, as for ticket0's widget
5129
+ * visitor, who holds a session token rather than a principal of their own. A vouched
5130
+ * socket is asserted once, so a token revoked mid-socket keeps receiving content-free
5131
+ * nudges until the socket closes. That is known and accepted for the widget: the nudge
5132
+ * names nothing, and every re-read it causes is checked again.
5133
+ *
5134
+ * Both send `LiveNudge` frames only: the subscriber may not read each row, so it is
5135
+ * never told which one changed. Only rows of a type some module declared in
5136
+ * `liveTargets` are announced, as for every other subscriber.
5137
+ */
5138
+ export declare function checkedWithin(entity: EntityRef, permission: string): CheckedWithin;
5139
+ /** Was this value built by `checkedWithin`? */
5140
+ export declare function isCheckedWithin(value: unknown): value is CheckedWithin;
4765
5141
  /**
4766
5142
  * Is this request asking to be upgraded to a WebSocket?
4767
5143
  *
@@ -4822,12 +5198,13 @@ export interface LiveChange {
4822
5198
  readonly at: string;
4823
5199
  }
4824
5200
  /**
4825
- * What a vouched subscriber is told (#1853): something beneath its root changed, and
4826
- * nothing else.
5201
+ * What a vouched or checked subscriber is told (#1853, #938): something beneath its root
5202
+ * changed, and nothing else.
4827
5203
  *
4828
- * No event type and no entity, deliberately. A vouched subscriber holds no read on the
4829
- * entities its frames are about — the vertical vouched for the ROOT, and the scope
4830
- * cannot know which rows under it the vertical's own read would show. Naming the type
5204
+ * No event type and no entity, deliberately. Such a subscriber holds no read on the
5205
+ * entities its frames are about — the vertical vouched for the ROOT, or the principal was
5206
+ * checked on the root alone, and the scope cannot know which rows under it the vertical's
5207
+ * own read would show. Naming the type
4831
5208
  * or the id would tell it what it never asked to read. The client re-reads, as it does
4832
5209
  * on a `LiveChange`.
4833
5210
  */