@substrat-run/kernel 0.163.0 → 0.164.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 (76) hide show
  1. package/dist/attachment-bytes.d.ts +121 -0
  2. package/dist/attachment-bytes.d.ts.map +1 -0
  3. package/dist/attachment-bytes.js +94 -0
  4. package/dist/attachment-bytes.js.map +1 -0
  5. package/dist/attachment-sweep.d.ts +2 -1
  6. package/dist/attachment-sweep.d.ts.map +1 -1
  7. package/dist/attachment-sweep.js +1 -1
  8. package/dist/attachment-sweep.js.map +1 -1
  9. package/dist/attachment-text.d.ts +1 -1
  10. package/dist/attachment-text.d.ts.map +1 -1
  11. package/dist/attachment-text.js +1 -1
  12. package/dist/attachment-text.js.map +1 -1
  13. package/dist/billing-retention.js +1 -1
  14. package/dist/billing-retention.js.map +1 -1
  15. package/dist/connection-context.d.ts +69 -0
  16. package/dist/connection-context.d.ts.map +1 -0
  17. package/dist/connection-context.js +19 -0
  18. package/dist/connection-context.js.map +1 -0
  19. package/dist/connection-landings.d.ts +8 -0
  20. package/dist/connection-landings.d.ts.map +1 -1
  21. package/dist/connection-landings.js +5 -1
  22. package/dist/connection-landings.js.map +1 -1
  23. package/dist/connector-transport.d.ts +313 -0
  24. package/dist/connector-transport.d.ts.map +1 -0
  25. package/dist/connector-transport.js +66 -0
  26. package/dist/connector-transport.js.map +1 -0
  27. package/dist/deadline-fetch.d.ts +1 -1
  28. package/dist/deadline-fetch.d.ts.map +1 -1
  29. package/dist/findings.js +1 -1
  30. package/dist/findings.js.map +1 -1
  31. package/dist/index.d.ts +9 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +7 -2
  34. package/dist/index.js.map +1 -1
  35. package/dist/job-run.d.ts +2 -1
  36. package/dist/job-run.d.ts.map +1 -1
  37. package/dist/job-run.js +2 -1
  38. package/dist/job-run.js.map +1 -1
  39. package/dist/live-read.d.ts +258 -0
  40. package/dist/live-read.d.ts.map +1 -0
  41. package/dist/live-read.js +101 -0
  42. package/dist/live-read.js.map +1 -0
  43. package/dist/membership-executor.d.ts +2 -1
  44. package/dist/membership-executor.d.ts.map +1 -1
  45. package/dist/membership-executor.js.map +1 -1
  46. package/dist/meter-days.js +1 -1
  47. package/dist/meter-days.js.map +1 -1
  48. package/dist/migration-digest.d.ts +1 -1
  49. package/dist/migration-digest.d.ts.map +1 -1
  50. package/dist/migration-digest.js +1 -1
  51. package/dist/migration-digest.js.map +1 -1
  52. package/dist/platform-sweep.d.ts +22 -4
  53. package/dist/platform-sweep.d.ts.map +1 -1
  54. package/dist/platform-sweep.js +69 -26
  55. package/dist/platform-sweep.js.map +1 -1
  56. package/dist/promote-review.d.ts +35 -4
  57. package/dist/promote-review.d.ts.map +1 -1
  58. package/dist/promote-review.js +31 -15
  59. package/dist/promote-review.js.map +1 -1
  60. package/dist/request-rollup.js +1 -1
  61. package/dist/request-rollup.js.map +1 -1
  62. package/dist/retention.d.ts +106 -0
  63. package/dist/retention.d.ts.map +1 -0
  64. package/dist/retention.js +131 -0
  65. package/dist/retention.js.map +1 -0
  66. package/dist/scope-host.d.ts +16 -789
  67. package/dist/scope-host.d.ts.map +1 -1
  68. package/dist/scope-host.js +1 -385
  69. package/dist/scope-host.js.map +1 -1
  70. package/dist/storage-gauge.js +1 -1
  71. package/dist/storage-gauge.js.map +1 -1
  72. package/dist/upload-link.d.ts +1 -1
  73. package/dist/upload-link.d.ts.map +1 -1
  74. package/dist/upload-link.js +1 -1
  75. package/dist/upload-link.js.map +1 -1
  76. package/package.json +2 -2
@@ -11,11 +11,15 @@ import type { StoreDriverHoldMode, StoreDriverRebuildFailure, StoreDriverRebuild
11
11
  import type { SubjectRedactionCounts } from './subject-redaction.js';
12
12
  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, ConnectionBindingAudit, CopyMarkAudit, OrphanSweepAudit, BindHostnameInput, AdminLogEntry, OpsFailureEntry, PlatformRequestDrainTotalsEntry, SweepRunEntry, SweepRunKind, SweepRunOutcome, ModelUsageEntry, ModelUsageSummary, CapabilityGrant, CreateTenantInput, Decision, Instant, DomainEvent, DomainEventInput, PlatformRequestInput, PlatformRequestId, PlatformRequestEntry, PlatformRequestFilter, PlatformRequestStatus, PlatformRequestFailure, PlatformOutcomeEvent, EntitlementGrant, EntitlementGrantInput, EntitlementView, MeterReading, MeterDay, MeterDayTake, VerticalOwnerPeriod, ScopeStorageSample, RequestDay, RequestRollupDay, 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, DeploymentWireAuth, RouteTarget, DirectoryDump, IdentityMembership, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, ScopeDumpInput, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, RefusalFilter, RefusalRecord, PermissionDenial, Coverage, BeginImpersonationInput, ImpersonationFilter, ImpersonationSession, ImpersonationSessionId, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, UploadLinkView, 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';
13
13
  import type { ConnectionUseOutcome } from './connector-calls.js';
14
- import { type ByteBody, type ByteStream, type IncrementalSha256, type SizedByteStream } from './byte-stream.js';
14
+ import type { ByteBody } from './byte-stream.js';
15
+ import type { BlobStoreProvisionInput, BlobStoreRecord, OpenedAttachment, OpenedAttachmentStream } from './attachment-bytes.js';
16
+ import type { ConnectorOptions, ExecutorDeadLetter, ExecutorRetryPolicy, ScopedConnectorConnection } from './connector-transport.js';
17
+ import type { TelemetryPruneReport } from './retention.js';
18
+ import type { LiveReadSurface } from './live-read.js';
15
19
  import type { CapabilityVerbs } from './capability.js';
16
20
  import type { AttachmentVerbs } from './upload-link.js';
17
21
  import type { AuditedOperationRef, AuditedOperationRow } from './audit-outcome.js';
18
- import { type AttachmentRecordFields, type EntityStateName } from '@substrat-run/contracts';
22
+ import { type EntityStateName } from '@substrat-run/contracts';
19
23
  import type { ModelUsageFilter, ModelUsageInput, ModelUsageWindow } from './model-usage.js';
20
24
  import type { FindingPruneReport } from './findings.js';
21
25
  import type { ScopeStorageAttempt, ScopeStorageFilter, ScopeStorageReadingInput } from './storage-gauge.js';
@@ -1056,21 +1060,6 @@ export interface ExecutorOutcome {
1056
1060
  }
1057
1061
  /** One delivery's `ExecutorOutcome` — the one builder both adapters report through. */
1058
1062
  export declare function executorOutcomeOf(executorId: string, event: DomainEvent, outcome: ExecutorOutcome['outcome'], error?: unknown): ExecutorOutcome;
1059
- /**
1060
- * How hard the host tries before it gives up on one delivery (#100).
1061
- *
1062
- * Defaults suit a directory write. A connector making an outbound HTTP call
1063
- * wants a longer tail — that is the whole reason this is per-executor rather
1064
- * than a host-wide constant.
1065
- */
1066
- export interface ExecutorRetryPolicy {
1067
- /** Total attempts including the first. Reaching it dead-letters. Default 5. */
1068
- maxAttempts?: number;
1069
- /** First backoff step; doubles per attempt. Default 1000ms. `0` retries at once. */
1070
- baseDelayMs?: number;
1071
- /** Ceiling on the doubling. Default 300_000ms (5 min). */
1072
- maxDelayMs?: number;
1073
- }
1074
1063
  /**
1075
1064
  * What one drain pass did. `retrying` and `deadLettered` are the numbers a
1076
1065
  * health surface reports; a caller that ignores them learns nothing, which is
@@ -1225,51 +1214,6 @@ export interface ScheduleTick {
1225
1214
  /** Platform requests the purge runs of the tick's earlier calls on this scope enqueued (their `purgeRequests`). */
1226
1215
  earlierPurgeRequests?: number;
1227
1216
  }
1228
- /**
1229
- * The web-standard fetch surface, structurally typed.
1230
- *
1231
- * Declared rather than imported: the kernel depends on no platform typings, and
1232
- * `RequestInit`/`Response` come from DOM lib in Node and from workers-types in
1233
- * Workers. Structural typing means both satisfy this without either being
1234
- * required — the same reason `crypto` and `TextEncoder` are declared locally.
1235
- */
1236
- export interface FetchLike {
1237
- (input: string, init?: ConnectorRequestInit): Promise<ConnectorResponse>;
1238
- }
1239
- /**
1240
- * The runtime's own `fetch`, as a `FetchLike` — the ONE place the structural cast
1241
- * lives. A host that takes a `fetch` defaults to this; a caller that hands one to a
1242
- * connector passes this. Nobody else spells `globalThis.fetch` (`lint:bound-fetch`).
1243
- *
1244
- * The cast is needed at all because a real `fetch` is NOT assignable to `FetchLike`
1245
- * under strict TypeScript: `ConnectorRequestInit.body` is a `Uint8Array` the DOM lib's
1246
- * `BodyInit` no longer accepts unparameterized, and `signal` is `unknown`. Structurally
1247
- * the runtime satisfies it — every field is one `fetch` reads — and this is where that
1248
- * claim is made, once. Code typed against the DOM `fetch` itself (a `Response` in hand)
1249
- * does not want this; `globalThis.fetch.bind(globalThis)` is that type with no cast.
1250
- *
1251
- * An arrow, deliberately, rather than the global handed on as a value: workerd
1252
- * refuses the bare global invoked with any other receiver — `TypeError: Illegal
1253
- * invocation` — and a callee is free to call what it was given as `input.fetch(…)`.
1254
- * Node's fetch accepts any `this`, and so does the wrapper the workers vitest pool
1255
- * installs, so no suite in this repo sees the difference; the Fortnox consent
1256
- * callback shipped green and failed every hosted round that way (#1291). Closing
1257
- * over the global here means the receiver is never in play, anywhere downstream.
1258
- *
1259
- * A `SizedByteStream` body (#2498) is converted by `fetchLikeOf`, which this is built from.
1260
- */
1261
- export declare const globalFetch: FetchLike;
1262
- /**
1263
- * A runtime `fetch` (the global, a service binding's, a test's stand-in) as a `FetchLike`
1264
- * (#2498): the one place a `SizedByteStream` body becomes a request the runtime can send, as
1265
- * `knownLengthInit` builds it. The runtime takes no such value: handed one as it is, a `fetch`
1266
- * sends its string form, `"[object Object]"`, and answers as if nothing were wrong. So
1267
- * `globalFetch` is built from this, and both hosts pass the `fetch` they are given through it,
1268
- * as `runPlatformSweep` does, so a sized body reaches no runtime `fetch` unconverted whichever
1269
- * one was injected. Wrapping a `FetchLike` that is already one is harmless: a converted body is
1270
- * no longer a `SizedByteStream`.
1271
- */
1272
- export declare function fetchLikeOf(runtimeFetch: (input: string, init?: never) => Promise<ConnectorResponse>): FetchLike;
1273
1217
  /**
1274
1218
  * The host's clock (#812) — what `ctx.now()` reads.
1275
1219
  *
@@ -1300,210 +1244,6 @@ export declare function fetchLikeOf(runtimeFetch: (input: string, init?: never)
1300
1244
  export interface Clock {
1301
1245
  (): Instant;
1302
1246
  }
1303
- export interface ConnectorRequestInit {
1304
- method?: string;
1305
- headers?: Record<string, string>;
1306
- /**
1307
- * `Uint8Array` as well as `string` because a real provider upload is binary:
1308
- * Scrive's `setfile` is `multipart/form-data`, whose body is a byte sequence a
1309
- * string cannot carry without corrupting the file. Web `fetch` accepts both, so
1310
- * this only widens the declared surface — the adapter passes it straight
1311
- * through.
1312
- *
1313
- * A {@link SizedByteStream} as well (#2498), for a body too large to hold in memory. The
1314
- * length is part of the value because a provider wants a `Content-Length`, and a stream
1315
- * carries none. The runtime cannot take one as it is, so it is converted by `fetchLikeOf`:
1316
- * `globalFetch` is built from it, and both hosts and `runPlatformSweep` pass the `fetch` they
1317
- * are given through it. A runtime `fetch` handed straight to a connector is made a
1318
- * `FetchLike` with `fetchLikeOf` too, never by a cast.
1319
- */
1320
- body?: string | Uint8Array | SizedByteStream;
1321
- signal?: unknown;
1322
- }
1323
- export interface ConnectorResponse {
1324
- readonly ok: boolean;
1325
- readonly status: number;
1326
- text(): Promise<string>;
1327
- json(): Promise<unknown>;
1328
- /**
1329
- * The raw bytes — for provider responses that are a file, not JSON. Scrive's
1330
- * sealed signed PDF (`documents/{id}/files/main`) comes back as `application/pdf`,
1331
- * which `text()` would corrupt. Web `Response` already has this, so declaring it
1332
- * only widens the structural surface the adapter passes straight through.
1333
- */
1334
- arrayBuffer(): Promise<ArrayBuffer>;
1335
- /**
1336
- * The body as a stream (#2498), for a provider file too large to hold: read it chunk by chunk,
1337
- * or hand it on to `upload` as a `SizedByteStream`. `null` for a response with no body, as on
1338
- * a web `Response`, which every adapter's `fetch` returns.
1339
- *
1340
- * The stream carries the body as `fetch` delivers it: DECODED. A response sent with a
1341
- * `Content-Encoding` (gzip, br) is decompressed on the way, while its `Content-Length` still
1342
- * counts the compressed bytes, so that header is the stream's length only when the response
1343
- * has no `Content-Encoding`. Otherwise take the length from the provider's own record of the
1344
- * file (its item metadata); a stream sized by the wrong number is refused, not landed.
1345
- *
1346
- * The stream is bounded by IDLENESS, not by total transfer time (#2518). A connection's `fetch`
1347
- * holds the connector's `timeoutMs` (default 30 s) over the call up to the headers and over a
1348
- * buffered read, and still holds it while `body` is only looked at or never read. From the first
1349
- * read of the stream the deadline hands over: it errors (`TimeoutError`) when the provider sends
1350
- * no byte for `STREAM_IDLE_TIMEOUT_MS` (an empty chunk is not a byte), or when
1351
- * `STREAM_MAX_DURATION_MS` has passed in all. A transfer that keeps flowing completes within
1352
- * that. A stalled or endless one errors, the connection is released, and an upload reading it
1353
- * lands no object and no row.
1354
- *
1355
- * OPTIONAL for the reason `headers` is: a hand-built stand-in is an object literal, and most
1356
- * connectors never read a stream. A caller that needs one falls back to `arrayBuffer()` when it
1357
- * is absent. Read a body once, by one of these.
1358
- */
1359
- readonly body?: ByteStream | null;
1360
- /**
1361
- * The response headers, when the runtime behind this seam has them.
1362
- *
1363
- * OPTIONAL, and the optionality is the whole design. Every adapter's `fetch`
1364
- * returns a real web `Response`, which always carries headers — so a connector
1365
- * reading one gets a value in production, always. What is not guaranteed is a
1366
- * hand-built stand-in: a mock or an in-memory provider is a plain object
1367
- * literal, and requiring `headers` would break every one of them for a field
1368
- * most connectors never read.
1369
- *
1370
- * So a caller must handle absence, and the callers that need it do. It exists
1371
- * because some provider instructions live ONLY in a header and a connector that
1372
- * cannot read them has to invent a substitute: Planima answers a rate limit with
1373
- * `Retry-After`, and a client that cannot see it backs off on a schedule of its
1374
- * own guessing — either too soon (spending another request to be told the same
1375
- * thing) or too late. Declaring it here is what keeps that read inside the
1376
- * sanctioned seam instead of a structural cast in a connector.
1377
- */
1378
- readonly headers?: {
1379
- get(name: string): string | null;
1380
- };
1381
- }
1382
- /**
1383
- * A connection with its credential opened, plus egress bound to it.
1384
- *
1385
- * `fetch` is bound to the connection rather than sitting on the context because
1386
- * health has to land on the right row by construction. An ambient
1387
- * `ctx.fetch` would need the runtime to guess which connection a call belonged
1388
- * to, and it would guess wrong the first time a connector talked to two.
1389
- */
1390
- export interface ConnectorConnection extends OpenConnection {
1391
- /**
1392
- * Sanctioned egress: a timeout, and success/failure recorded against THIS
1393
- * connection.
1394
- *
1395
- * The connector is handed its `fetch` rather than importing one — the same
1396
- * move `ctx.sql` makes for module code, and for the same reason. Egress
1397
- * policy, timeouts and health become properties of the seam instead of
1398
- * conventions a connector author has to remember. Module code still cannot
1399
- * reach any of this: boundary-lint R3 bans `fetch` outright, and a connector
1400
- * is host code.
1401
- */
1402
- fetch(input: string, init?: ConnectorRequestInit): Promise<ConnectorResponse>;
1403
- }
1404
- /**
1405
- * A connection opened INSIDE a dispatch (#711) — `ctx.connection(provider)`.
1406
- *
1407
- * Strictly more than a `ConnectorConnection`, and the difference is a scope. A
1408
- * dispatch happens *for* one scope, which is ambient in the context, so a
1409
- * connection opened there can reach that scope's attachments. A connection
1410
- * reopened top-level — a probe of a candidate credential, a poll driver
1411
- * rebuilding egress from the directory — has no such scope, and the type says so
1412
- * rather than handing out a method that would have to throw.
1413
- */
1414
- export interface ScopedConnectorConnection extends ConnectorConnection {
1415
- /**
1416
- * Read ONE attachment's bytes from the scope this delivery is for (#711) — the
1417
- * outbound mirror of `getConnectorAttachments`, which is the return path's door.
1418
- *
1419
- * A signing connector sends a document. Until this existed it could only send a
1420
- * document it RENDERED ITSELF, because `create` had no way to be handed the
1421
- * vertical's own file: the bytes were in the attachment store, and the store was
1422
- * unreachable from inside a dispatch. So a Swedish counterparty was asked to sign
1423
- * a page of identifiers with BankID rather than the contract.
1424
- *
1425
- * **On the connection, for the same reason `fetch` is.** The read is authorized as
1426
- * a connection, and the only connection it can correctly be authorized as is the
1427
- * one the handler opened — this object. An ambient "the provider this connector is
1428
- * registered under" would be a second name for the same thing, and two names for
1429
- * one fact is how they come to disagree: `registerScriveConnector({ id: 'scrive-eu' })`
1430
- * opens its credential as `'scrive'` and would have read as `'scrive-eu'`, so the
1431
- * egress half kept working while the document half failed on every contract.
1432
- * Handing the door to whoever holds the credential makes that unrepresentable.
1433
- *
1434
- * **Why not `getConnectorAttachments` from in here.** On the pure adapter a
1435
- * connector runs INSIDE the scope's actor task, and every verb of that surface
1436
- * re-enqueues on the same actor — the nested task waits on the task holding it,
1437
- * and the invoke never returns. (Pinned in `connector-reads.test.ts`.) The adapter
1438
- * builds this read to suit where it is running: reentrant when the caller already
1439
- * holds the actor, ordinary and serialized when it does not.
1440
- *
1441
- * **Reads only, and only by id.** No `list`, deliberately. A connector that
1442
- * SEARCHES for the document to send has to have a rule for picking among several
1443
- * — and the return path lands the sealed signed copy on the same entity, so a
1444
- * wrong rule mails the counterparty their own signed contract to sign again.
1445
- * Naming the id makes that unrepresentable: the caller says which bytes, the
1446
- * event carries it, and nothing has to be disambiguated. Writes stay top-level
1447
- * (`getConnectorAttachments().upload`), where a spine event and its consumers
1448
- * have a transaction to live in.
1449
- *
1450
- * Gated by the target's `readPermission` checked against this connection's
1451
- * `connection:<id>` grants. `null` for an id the scope does not know — a caller
1452
- * falls back rather than failing a dispatch over a missing file.
1453
- */
1454
- openAttachment(attachmentId: string): Promise<OpenedAttachment | null>;
1455
- /**
1456
- * `openAttachment`, with the bytes as a stream that is never held whole (#2514), for a connector
1457
- * that sends a file on to a provider. Gated the same way, before any byte is read. The sha256 is
1458
- * checked as the bytes flow, and a mismatch errors the stream before its last chunk is handed on
1459
- * (see {@link OpenedAttachmentStream}). The bytes delivered before a stream error are a prefix of
1460
- * an unverified file, never a shorter file: a connector that sends what it read discards them and
1461
- * undoes anything it wrote from them (an upload session cancelled, a request aborted before it
1462
- * completes), as the hosted read's refusal says (`STREAMED_ATTACHMENT_UNVERIFIED` in
1463
- * `@substrat-run/control-plane-api`). A stream it decides not to read is
1464
- * cancelled (`body.stream.getReader().cancel()`), which releases the stored object at once.
1465
- */
1466
- openAttachmentStream(attachmentId: string): Promise<OpenedAttachmentStream | null>;
1467
- /**
1468
- * What this connection may do in the scope this delivery is for (#726 gap 1) —
1469
- * `connectionGrantsInScope`, narrowed to this connection.
1470
- *
1471
- * Here so a connector can check its own preconditions at the top of a dispatch and
1472
- * fail saying which grant is missing, instead of discovering it three calls later as
1473
- * a refusal the drain then captions as the provider's (#841). A connector that needs
1474
- * a standing grant for its RETURN path — where there is no delivered event to carry
1475
- * authority — can say so on the way out, which is the only moment it is cheap to fix.
1476
- *
1477
- * Deliberately the permission keys and nothing else: a connector asks whether it may
1478
- * act, never who granted it or when. Both are the operator's question, and both are
1479
- * on the drawer that already renders them.
1480
- */
1481
- grants(): Promise<PermissionKey[]>;
1482
- /**
1483
- * Open a cell the scope sealed TO this connection (#687) — the egress half of
1484
- * `ctx.sealToConnection`.
1485
- *
1486
- * **Here rather than on `ConnectorConnection`, and the probe path is why.** A
1487
- * connection rebuilt top-level may have no directory row at all — the credential
1488
- * probe builds one from a candidate secret and a zero id — so it holds no
1489
- * keypair and could only ever throw. A sealed cell arrives on a delivered event,
1490
- * which is to say inside a dispatch, which is exactly the shape that has one.
1491
- *
1492
- * **On the connection, for the same reason `fetch` and `openAttachment` are.**
1493
- * The private half belongs to one connection, and the only connection it can
1494
- * correctly be is the one this handler opened. An ambient `ctx.unseal` would
1495
- * have to guess, and it would guess wrong the first time a tenant held two
1496
- * credentials for one provider.
1497
- *
1498
- * Key material never crosses this seam: the adapter holds the keyId-indexed map
1499
- * and picks by the cell's own `keyId`, so a connector cannot mislay a private
1500
- * key it never had. Throws `SealedKeyUnavailableError` when the named key is not
1501
- * held — which after a rotation-as-erasure (D-5) is the correct and permanent
1502
- * answer, not a mystery. A scope restored from backup can resurrect a pending
1503
- * request whose key is gone, and that delivery should dead-letter saying so.
1504
- */
1505
- unseal(sealed: SealedSecret): Promise<string>;
1506
- }
1507
1247
  /**
1508
1248
  * What a connector sees. Strictly more than an executor: an executor effects
1509
1249
  * something in the DIRECTORY, a connector effects something in the OUTSIDE
@@ -1527,50 +1267,6 @@ export interface ConnectorContext {
1527
1267
  connection(provider: string): Promise<ScopedConnectorConnection>;
1528
1268
  }
1529
1269
  export type ConnectorHandler = (ctx: ConnectorContext, event: DomainEvent) => void | Promise<void>;
1530
- /** Tuning for one connector's egress. */
1531
- export interface ConnectorOptions extends ExecutorRetryPolicy {
1532
- /**
1533
- * Per-request timeout. Default 30s. It bounds the call up to the response headers and a
1534
- * buffered read of the body. A streamed body (`ConnectorResponse.body`) is bounded by an idle
1535
- * window instead (#2518, `STREAM_IDLE_TIMEOUT_MS`) and an absolute cap (`STREAM_MAX_DURATION_MS`):
1536
- * it may take longer than this, as long as bytes keep arriving.
1537
- */
1538
- timeoutMs?: number;
1539
- /**
1540
- * The provider slug this connector operates (#574 phase 3) — the routing key a
1541
- * CP-less host uses when it cannot run the handler itself: the delivery becomes a
1542
- * `connector:<provider>` platform intent, and the platform's drain dispatches it to
1543
- * the handler registered for that same slug. Defaults to the registration id, which
1544
- * for the shipped connectors is already the provider name ('scrive'). Irrelevant on
1545
- * a host that reaches the connection directory (self-host, the control plane): there
1546
- * the handler runs in-process and this key is never consulted.
1547
- */
1548
- provider?: string;
1549
- }
1550
- /** A delivery that exhausted its attempts. The evidence, not a silent drop. */
1551
- export interface ExecutorDeadLetter {
1552
- eventId: string;
1553
- executorId: string;
1554
- eventType: string;
1555
- attempts: number;
1556
- error: string;
1557
- lastAttemptAt: string;
1558
- }
1559
- /**
1560
- * Retry defaults (#100). Tuned for the directory write the first executor does;
1561
- * a connector making an outbound call should raise `maxAttempts` explicitly,
1562
- * which is why the policy is per-executor rather than a host constant.
1563
- */
1564
- export declare function resolveRetryPolicy(retry?: ExecutorRetryPolicy): Required<ExecutorRetryPolicy>;
1565
- /**
1566
- * When attempt `attempts` should next be tried: exponential, capped, jittered.
1567
- *
1568
- * Jitter is ±20% and is skipped entirely at zero delay, so a test setting
1569
- * `baseDelayMs: 0` gets deterministic immediate retries rather than a race. It
1570
- * matters at real delays because every scope in a fleet retries a downed
1571
- * provider on the same schedule otherwise.
1572
- */
1573
- export declare function backoffAt(attempts: number, retry: Required<ExecutorRetryPolicy>, from: Date): string;
1574
1270
  /**
1575
1271
  * A named, manifest-wired pre-condition on an operation (K-17; engine-protocol
1576
1272
  * §6, open question 11). One module CONTRIBUTES a predicate under a name; a
@@ -2480,8 +2176,17 @@ export interface HostAdmin {
2480
2176
  * and the DO-class/migration-tag base the NEXT upload diffs against. Written only
2481
2177
  * AFTER the upload succeeded — a failed serve leaves `servingVersionId` trailing
2482
2178
  * the prod channel, which is the visible, retryable state. Audited.
2179
+ *
2180
+ * A compare-and-set (#2528): the write lands only if the row still holds the serving state this
2181
+ * method read, its ref, version, class set and migration tag, judged in the unit that writes.
2182
+ * `expected` (#2577) is the state the caller computed its upload from (`verticalServing`'s answer
2183
+ * before the upload, null for nothing served); a row whose class set or tag differs from it is
2184
+ * refused before the gate, so the record never names a migration base diffed from one that is
2185
+ * gone. Its ref and version are not compared: a serve recorded meanwhile that moved only those
2186
+ * left the upload's migration right, and the record lands over it as it did before. Either
2187
+ * refusal is `precondition_failed` (`servingRaceRefusal`) and writes nothing.
2483
2188
  */
2484
- setVerticalServing(actor: PlatformActorId, verticalSlug: string, state: VerticalServingState): Promise<void>;
2189
+ setVerticalServing(actor: PlatformActorId, verticalSlug: string, state: VerticalServingState, expected?: VerticalServingState | null): Promise<void>;
2485
2190
  /**
2486
2191
  * The pushed DeployManifest (JSON) of one version — what a serve rebuilds upload
2487
2192
  * metadata from. Null for a version pushed before manifests were retained; such a
@@ -4214,106 +3919,6 @@ export interface TenantStoreRecord {
4214
3919
  ref: string;
4215
3920
  createdAt: string;
4216
3921
  }
4217
- /** What `provisionBlobStore` needs to mint (or idempotently re-resolve) a per-tenant
4218
- * blob store (#473). Keyed by (tenant, vertical, binding), exactly like tenant stores. */
4219
- export interface BlobStoreProvisionInput {
4220
- tenantId: TenantId;
4221
- /** The vertical the store belongs to — its `blobStoreNeed` binding is scoped to it. */
4222
- vertical: string;
4223
- /** The declared `blobStoreNeed.binding` this store satisfies (SCREAMING_SNAKE). */
4224
- binding: string;
4225
- }
4226
- /**
4227
- * A live per-tenant blob store (#473) — the byte side of the attachment surface. The
4228
- * contract is async and byte-shaped (Uint8Array + web-standard types only) because the
4229
- * store is only reachable asynchronously on Cloudflare (an `R2Bucket` binding); the pure
4230
- * adapter backs it with a per-tenant directory and resolves immediately.
4231
- *
4232
- * Keys are PLATFORM-DERIVED (`attachmentBlobKey`), never caller-supplied strings — the
4233
- * per-scope prefix inside a per-tenant store is constructed in kernel/adapter code, which
4234
- * is what turns "scope/<id>/ is a convention" into "cross-scope keys are unwritable".
4235
- */
4236
- export interface TenantBlobStore {
4237
- /**
4238
- * Write `body` under `key`. A `SizedByteStream` (#2498) is written as it is read, never
4239
- * collected first, and refused if it carries other than its declared length. The put
4240
- * resolves only once the whole stream has been read.
4241
- */
4242
- put(key: string, body: ByteBody, opts?: {
4243
- contentType?: string;
4244
- }): Promise<void>;
4245
- get(key: string): Promise<{
4246
- body: Uint8Array;
4247
- contentType?: string;
4248
- } | null>;
4249
- /**
4250
- * The object under `key` as a stream (#2514), with the size the store holds for it, read as the
4251
- * consumer reads it and never collected first. Optional, so a store written before it still
4252
- * satisfies this interface: an open over such a store reads the object with `get` and hands it on
4253
- * as a stream, which works but holds the object whole.
4254
- */
4255
- getStream?(key: string): Promise<{
4256
- stream: ByteStream;
4257
- size: number;
4258
- contentType?: string;
4259
- } | null>;
4260
- delete(key: string): Promise<void>;
4261
- /** Keys under `prefix` — the GC/ops walk. */
4262
- list(prefix: string): Promise<string[]>;
4263
- }
4264
- /**
4265
- * One row of the blob-store ledger (#473) — same idempotency/deploy/reap roles as
4266
- * {@link TenantStoreRecord}: a retried provision re-resolves the same `ref`, the deploy
4267
- * path derives which `r2_bucket` bindings must ride every serving-script upload, and a
4268
- * tenant reap knows what to tear down.
4269
- */
4270
- export interface BlobStoreRecord {
4271
- tenantId: TenantId;
4272
- vertical: string;
4273
- binding: string;
4274
- kind: 'blob';
4275
- ref: string;
4276
- createdAt: string;
4277
- }
4278
- /**
4279
- * The blob key an attachment's bytes live under inside the per-tenant store (#473).
4280
- * Scope-prefixed by construction: every key this platform ever writes for a scope sits
4281
- * under `scope/<scopeId>/`, and the attachment id (a fresh ULID per upload) makes keys
4282
- * write-once — the two properties the attachment integrity story rests on. Exported so
4283
- * both adapters (and an ops GC walk) derive the same key; module and route code never do.
4284
- */
4285
- export declare function attachmentBlobKey(scopeId: string, attachmentId: string): string;
4286
- /**
4287
- * An attachment's integrity witness: the lowercase hex SHA-256 of its bytes (Web Crypto).
4288
- * One definition for the hash an upload records and every read that holds bytes to it —
4289
- * both adapters' `open`, and the extraction job (#1575).
4290
- */
4291
- export declare function attachmentSha256(body: Uint8Array): Promise<string>;
4292
- /**
4293
- * Write an upload's bytes to the blob store and answer the record they make (#2498) — the "bytes
4294
- * first" half of every attachment write, one path for both adapters. The record's fields are
4295
- * judged before a byte moves, so a record the surface would refuse lands no object. A buffer is
4296
- * then hashed and written, as before. A stream is hashed and counted while the store reads it, so
4297
- * it is never held whole; `sha256` is the adapter's running digest, since the kernel has none of
4298
- * its own. The caller writes the row.
4299
- *
4300
- * A streamed write that fails, or whose store answers without reading the stream to its end, has
4301
- * its object deleted and is refused: no record may witness bytes nobody saw.
4302
- */
4303
- export declare function putAttachmentBytes(store: TenantBlobStore, key: string, fields: AttachmentRecordFields, body: ByteBody, sha256: () => IncrementalSha256): Promise<AttachmentRecord>;
4304
- /**
4305
- * The bytes of an attachment the scope has already AUTHORIZED, as a stream held to its record (#2514):
4306
- * the "bytes" half of every streamed open, one path for both adapters, as {@link putAttachmentBytes}
4307
- * is for the write. The caller checks the read permission first and calls this only once it passed,
4308
- * so a refused read never reaches the store.
4309
- *
4310
- * The length is the record's `size`, which the upload took from the bytes themselves. The store's own
4311
- * size for the object is held to it before the first byte, so an object that is not the recorded one
4312
- * is refused up front. The sha256 is checked as the bytes flow ({@link verifiedStream}): a mismatch
4313
- * errors the stream before its last chunk is handed on. A store without `getStream` is read whole with
4314
- * `get` and handed on the same way.
4315
- */
4316
- export declare function openAttachmentByteStream(store: TenantBlobStore, key: string, record: AttachmentRecord, sha256: () => IncrementalSha256): Promise<OpenedAttachmentStream>;
4317
3922
  /**
4318
3923
  * Who binds an operation, as the §4.3 entitlement gate needs to know it: the owning
4319
3924
  * module, the SKU flag its manifest declares, and the operations that module declares
@@ -4445,24 +4050,6 @@ export type RedeemedUploadLink = AttachmentRecord & {
4445
4050
  /** Present only on a replay (#2315): the file was stored by the request that spent the link, and nothing was written now. */
4446
4051
  replayed?: true;
4447
4052
  };
4448
- /** An opened attachment: the metadata fact plus the bytes it witnesses. */
4449
- export interface OpenedAttachment {
4450
- record: AttachmentRecord;
4451
- body: Uint8Array;
4452
- contentType: string;
4453
- }
4454
- /**
4455
- * An opened attachment whose bytes are a stream (#2514): the metadata fact, and its bytes at the
4456
- * record's `size`, read as the consumer reads them. The sha256 is checked as they pass, so a
4457
- * mismatch errors the stream before its last chunk instead of refusing up front: a consumer that
4458
- * has received all `size` bytes has received bytes that matched the record. One-shot: read it once,
4459
- * or cancel it, which releases the stored object; a stream that is neither holds it until collected.
4460
- */
4461
- export interface OpenedAttachmentStream {
4462
- record: AttachmentRecord;
4463
- body: SizedByteStream;
4464
- contentType: string;
4465
- }
4466
4053
  /**
4467
4054
  * The attachment surface a host mints per (principal, scope) — `attachmentTargets`
4468
4055
  * finally consumed (#473). Every method is gated INSIDE the platform by the declared
@@ -4569,111 +4156,6 @@ export interface AccessLogFilter extends ListPage {
4569
4156
  */
4570
4157
  drained?: boolean;
4571
4158
  }
4572
- /**
4573
- * How long a recorded operational failure is kept (#559). Telemetry, not evidence:
4574
- * unlike the never-swept admin log, the ops-failures table self-prunes on write in
4575
- * every adapter, so it can never grow without bound and needs no cron wiring or
4576
- * operator decision. 90 days comfortably outlives any incident follow-up (a
4577
- * Cloudflare ticket round-trip) while keeping directory storage flat.
4578
- */
4579
- export declare const OPS_FAILURE_RETENTION_DAYS = 90;
4580
- /**
4581
- * How long a sweep-run row is kept (#1232). Much shorter than ops failures: this
4582
- * is high-frequency telemetry (a pass every few minutes times every connection
4583
- * and schedule), and its job is the recent-runs strip plus an incident window —
4584
- * 14 days covers both. Pruned on write in every adapter, like ops failures and
4585
- * for the same reason: the table stays bounded even where no scheduled pass runs.
4586
- */
4587
- export declare const SWEEP_RUN_RETENTION_DAYS = 14;
4588
- /**
4589
- * The drained batch's dedupe key on the directory's `_substrat_sweep_runs` (#1232), as
4590
- * both adapters build it: `recordSweepRun` is an `INSERT OR IGNORE` against this index,
4591
- * which is what makes a replayed drain write nothing twice.
4592
- *
4593
- * `kind` is in it since #1572, and that is the fix rather than a tidy-up. Every entry of
4594
- * one drained batch shares its `request_id`, and `unit` is built from two namespaces that
4595
- * nothing keeps apart — `<scopeId>:<operation>` for a schedule, `<scopeId>:<eventType>`
4596
- * for a freshness verdict — while `scheduleSpec.operation` is `z.string().min(1)`. A
4597
- * schedule named exactly like an event type its scope expects freshness on collided on
4598
- * `(request_id, unit)`, and the ignore-on-conflict write discarded the second row with no
4599
- * error anywhere. Both rows are signals by their absence (a stopped evaluator, a missed
4600
- * run), so losing one read as the very thing it was meant to rule out. It is #1288's
4601
- * collision, in the record table beside the state table.
4602
- *
4603
- * Shared rather than spelled in each adapter because a directory that predates it
4604
- * re-creates it (`ensureSweepRunsIntentKind`, in both), and `lint:spine-ddl` sees only
4605
- * the created copy. One definition keeps the re-created shape and the created shape the
4606
- * same shape; the gate then holds each adapter's DDL to including it.
4607
- */
4608
- export declare const SWEEP_RUNS_INTENT_INDEX = "CREATE UNIQUE INDEX IF NOT EXISTS _substrat_sweep_runs_intent ON _substrat_sweep_runs (request_id, kind, unit);";
4609
- /**
4610
- * Whether a directory's `_substrat_sweep_runs_intent` already has `kind` among its
4611
- * columns, read off the index's `sql` in `sqlite_master` — which DO SQLite serves and
4612
- * `PRAGMA` does not, so both adapters detect by one strategy. `false` means the index
4613
- * must be DROPPED and created again: `CREATE … IF NOT EXISTS` matches on the name alone,
4614
- * so on every existing directory the old `(request_id, unit)` index would otherwise
4615
- * stand for good, while a fresh one — and any test that starts from one — gets the new.
4616
- */
4617
- export declare function sweepRunsIntentHasKind(indexSql: string): boolean;
4618
- /**
4619
- * How long an issue row outlives its last occurrence (#1233). Deliberately longer
4620
- * than the 90-day evidence beneath it: an issue is the compressed memory of a
4621
- * failure class, and "we saw this five months ago" is exactly what a regression
4622
- * needs to be recognizable. Pruned on write like everything else here, and on the
4623
- * scheduled pass (`pruneTelemetry`, #1632), so a quiet directory sheds it on time too.
4624
- */
4625
- export declare const ISSUE_RETENTION_DAYS = 180;
4626
- /** What one `pruneTelemetry` call deleted, per table. */
4627
- export interface TelemetryPruneReport {
4628
- opsFailures: number;
4629
- issues: number;
4630
- sweepRuns: number;
4631
- }
4632
- /**
4633
- * A caller-supplied row bound, checked before it reaches a SQL `LIMIT` (#1632). SQLite reads a
4634
- * negative `LIMIT` as no limit at all, so `-1` turns a bounded batch into an unbounded one; a
4635
- * fractional or non-finite value is refused by SQLite itself, as a failure every later call
4636
- * repeats. Neither is a value anyone means, so it is a configuration error, refused here with the
4637
- * option's name, and never silently normalized into something the caller did not ask for.
4638
- * Returns the value, so a call site can bind it in place.
4639
- */
4640
- export declare function assertRowLimit(name: string, value: number): number;
4641
- /**
4642
- * A caller-supplied row offset, checked before it reaches a SQL `OFFSET` — `assertRowLimit`'s
4643
- * sibling. Zero is a valid offset; a negative, fractional or non-finite one is refused rather
4644
- * than passed to SQLite, which fails the statement on it.
4645
- */
4646
- export declare function assertRowOffset(name: string, value: number): number;
4647
- /** How many rows of EACH telemetry table one `pruneTelemetry` call deletes, by default. */
4648
- export declare const TELEMETRY_PRUNE_BATCH = 500;
4649
- /**
4650
- * The three telemetry retentions as statements, for `pruneTelemetry` on both adapters (#1632).
4651
- *
4652
- * Each table already prunes on write, and that bound holds only while something is written:
4653
- * a quiet directory keeps a row past its retention for as long as nothing new arrives in
4654
- * that table. An issue is the case that matters. Erasure skips an issue whose exemplar owner
4655
- * is unknown, on the promise that it ages out within `ISSUE_RETENTION_DAYS`, and on a
4656
- * directory with no new failures nothing ever deleted it. The scheduled pass runs these
4657
- * statements, which makes the stated retention a bound in time rather than in traffic.
4658
- *
4659
- * **Bounded per call, oldest first.** After a long pause or a restore the backlog can be a
4660
- * year of sweep runs, and one unbounded DELETE over it is a pass that fails on the Durable
4661
- * Object every tick. Each statement deletes at most `limit` rows, chosen through the table's
4662
- * own retention index (`… WHERE rowid IN (SELECT rowid … ORDER BY <ts> LIMIT ?)`), so the
4663
- * backlog drains over passes. `RETURNING` makes the count what was actually deleted on both
4664
- * adapters, rather than a rows-written figure that would also count index writes.
4665
- */
4666
- export declare function telemetryRetentionStatements(nowMs: number, limit: number): {
4667
- table: keyof TelemetryPruneReport;
4668
- sql: string;
4669
- params: [string, number];
4670
- }[];
4671
- /**
4672
- * One bounded retention DELETE: at most `LIMIT ?` rows whose `column` is before `?`, oldest
4673
- * first through the column's own index, `RETURNING 1` so the count is what was deleted. Takes
4674
- * `(horizon, limit)`. The statement `telemetryRetentionStatements` and `pruneFindings` share.
4675
- */
4676
- export declare function boundedRetentionDelete(table: string, column: string): string;
4677
4159
  /** Filter for `listIssues` (#1233). Bounded by `limit` only — see the verb's doc. */
4678
4160
  export interface IssueFilter {
4679
4161
  status?: IssueStatus;
@@ -5742,261 +5224,6 @@ export interface ScopeHost {
5742
5224
  defineOperation<I, O>(name: string, handler: OperationHandler<I, O>): void;
5743
5225
  close(): Promise<void>;
5744
5226
  }
5745
- /**
5746
- * The live-read surface (#938): one `Upgrade` in, one long-lived subscription out.
5747
- *
5748
- * **What crosses it is an invalidation, never a payload.** A frame names the event
5749
- * type and the `(entityType, entityId)` it was about, and the client re-reads that
5750
- * entity through the ordinary operation it already calls. That is what keeps the
5751
- * change feed from quietly becoming a second, ungoverned read API: every field a
5752
- * subscriber ever sees came back through the declared read surface, with that read's
5753
- * own permission check, its own field-level omissions and its own PII handling.
5754
- *
5755
- * **Every frame is filtered per subscriber, after the commit and after the check.**
5756
- * The host offers each committed event to each subscriber and delivers it only if
5757
- * that subscriber passes the entity type's declared `liveTargets.readPermission` ON
5758
- * THAT ENTITY — the same walk `ctx.check(key, entityRef)` makes, through the same
5759
- * evaluator, so an entity-narrowed grant decides a push exactly as it decides a read.
5760
- * An entity type no module declared reaches nobody. Knowing that a row exists and
5761
- * changed at 14:02 is information about that row, so "the body was empty" is not a
5762
- * defence: the filter runs whether or not there is a payload to withhold.
5763
- *
5764
- * A subscription may be narrowed `within` one entity (#1853), which only removes frames.
5765
- * Two built roots replace the per-row check with one about the ROOT, and their subscribers
5766
- * are sent bare `LiveNudge` frames that name no entity: `checkedWithin`, whose principal must
5767
- * pass a stated key on the root at the handshake and again on every pass that has something
5768
- * to send (#938), and `vouchedWithin`, which the vertical asserts once and nothing re-checks
5769
- * (#1853). Which to use is on `checkedWithin`.
5770
- *
5771
- * **Generic over the runtime's request and response, because the kernel names
5772
- * neither.** This package has one dependency and no DOM or workers lib
5773
- * (`docs/architecture/dependency-policy.md`), which is why `FetchLike` above describes
5774
- * a `fetch` structurally rather than importing one. The same rule applies here, and it
5775
- * is load-bearing in the other direction too: what a caller gets back must be the
5776
- * runtime's OWN response object, because a Worker's handler has to return one. A
5777
- * structural stand-in would describe it correctly and still not be returnable. So the
5778
- * type travels through instead of being restated — `CloudflareScopeHost` binds it to
5779
- * the real `Request`/`Response`, and code holding the bare contract gets `unknown` back
5780
- * and narrows at its own mount point, where it knows which host it is on.
5781
- */
5782
- export interface LiveReadSurface<Req extends LiveUpgradeRequest = LiveUpgradeRequest, Res = unknown> {
5783
- /**
5784
- * Accept a WebSocket `Upgrade` and subscribe the principal to the scope's changes.
5785
- *
5786
- * The caller is the vertical, and it has already done the one thing this surface
5787
- * cannot do for itself: resolved WHO is asking. The principal is an authenticated
5788
- * fact the vertical's session carries, exactly as it is for an `invoke` — this
5789
- * surface trusts it the same way and no further, which is why every frame is still
5790
- * checked against it individually rather than a subscription being authorized once.
5791
- *
5792
- * Returns the 101 to hand back to the client, or a refusal to return as-is: a
5793
- * request that is not an upgrade gets 426, and one arriving over a hop that cannot
5794
- * carry a socket gets a refusal naming that, so the client can fall back to polling
5795
- * KNOWINGLY. Neither is thrown, because both are answers to a client rather than
5796
- * faults of the caller.
5797
- */
5798
- subscribe(input: {
5799
- tenantId: TenantId;
5800
- scopeId: ScopeId;
5801
- /** WHO is subscribing — resolved by the vertical from its own session, never by the client. */
5802
- principal: PrincipalId;
5803
- /** The upgrade request as it arrived, carried whole so the host reads its own headers. */
5804
- request: Req;
5805
- /**
5806
- * Narrow the feed to one entity and what hangs beneath it (#1853).
5807
- *
5808
- * A frame is then delivered only if its entity IS this one or reaches it upward
5809
- * through live declared `parent` edges (what `ctx.link` / `ctx.relink` write) — the
5810
- * walk `ctx.check` makes, rooted here instead of at a grant.
5811
- *
5812
- * - **An `EntityRef` narrows and nothing else.** The principal's own per-frame
5813
- * `liveTargets` check still runs; `within` is ANDed with it, so it can only take
5814
- * frames away. A screen watching one conversation passes it to stop hearing the
5815
- * rest of the desk.
5816
- * - **A `vouchedWithin(…)` value replaces the principal's check.** For a subscriber
5817
- * confined by something other than a grant — a widget visitor holding a session
5818
- * token the vertical has just redeemed. The vertical asserts access to the root,
5819
- * the same trust it already extends in naming `principal`, and the scope's walk is
5820
- * the whole filter. Such a subscriber receives `LiveNudge` frames only, which name
5821
- * no event type and no entity.
5822
- * - **A `checkedWithin(…)` value replaces the per-row check with one on the root**
5823
- * (#938). The principal must pass the stated key on the root, at the handshake (a
5824
- * `403` otherwise) and again on every pass that has a row beneath it to announce,
5825
- * so a grant withdrawn, or a root moved out from under it, closes the socket
5826
- * instead of nudging it. `LiveNudge` frames only, as for a vouched root.
5827
- *
5828
- * **Freshness (Codex #2077 r5).** A decision is reused within a pass only while the
5829
- * scope has had no write and nothing it holds has reached its `expires_at` since the
5830
- * decision. It is never reused while the scope reads its permissions from the directory.
5831
- * Both are checked against the clock immediately before each send. A change that lands
5832
- * while a check is still being evaluated can let that one frame through. Frames name
5833
- * nothing the subscriber could not read when checked, and the next frame, pass and poll
5834
- * see the change: a push is a hint at most one evaluation stale.
5835
- */
5836
- within?: EntityRef | VouchedWithin | CheckedWithin;
5837
- /**
5838
- * When the credential that proved `principal` stops being valid (ISO 8601), so the
5839
- * socket never outlives it (#938). Pass the session's or the bearer's expiry
5840
- * (`AuthSubject.expiresAt`). A handshake at or past it is refused, and the scope closes
5841
- * the socket (`1008`) on its first pass at or past it, before sending anything on that
5842
- * pass. Absent: the socket lives until either end closes it, as before.
5843
- */
5844
- expiresAt?: string;
5845
- }): Promise<Res>;
5846
- }
5847
- /** The brand only `vouchedWithin` can apply — a literal cannot type-check as one. */
5848
- declare const vouchedBrand: unique symbol;
5849
- /**
5850
- * A `within` root the VERTICAL vouches the subscriber may watch, in place of the
5851
- * principal's own grants (#1853). Built only by `vouchedWithin`.
5852
- */
5853
- export interface VouchedWithin {
5854
- readonly [vouchedBrand]: true;
5855
- readonly entity: EntityRef;
5856
- /** Why the vertical vouches — what it checked. Required, and kept on the subscription. */
5857
- readonly because: string;
5858
- }
5859
- /**
5860
- * Vouch that the subscriber may watch `entity` and everything beneath it, though the
5861
- * principal it subscribes as holds no read on it.
5862
- *
5863
- * The one way to reach the replacing mode, so every use is greppable and none is an
5864
- * accident: an object literal is refused by the type checker, and anything not built
5865
- * here is refused by the host at run time (`isVouchedWithin`). Call it only after the
5866
- * vertical itself has proven access — for ticket0's widget, by redeeming the session
5867
- * token for the session it names.
5868
- *
5869
- * What it costs the subscriber is detail: a vouched feed carries `LiveNudge` frames, so
5870
- * the scope never tells it which entity changed or how. Root it at an entity whose
5871
- * subtree holds only what the subscriber may see — the walk is the whole filter.
5872
- */
5873
- export declare function vouchedWithin(entity: EntityRef, opts: {
5874
- because: string;
5875
- }): VouchedWithin;
5876
- /** Was this value built by `vouchedWithin`? A host asks before it drops the principal's check. */
5877
- export declare function isVouchedWithin(value: unknown): value is VouchedWithin;
5878
- /** The brand only `checkedWithin` can apply. */
5879
- declare const checkedBrand: unique symbol;
5880
- /**
5881
- * A `within` root the PRINCIPAL must be allowed `permission` on, re-checked for as long
5882
- * as the socket is open (#938). Built only by `checkedWithin`.
5883
- */
5884
- export interface CheckedWithin {
5885
- readonly [checkedBrand]: true;
5886
- readonly entity: EntityRef;
5887
- readonly permission: PermissionKey;
5888
- }
5889
- /**
5890
- * Watch `entity` and everything beneath it, for as long as the subscriber may
5891
- * `permission` on `entity` itself.
5892
- *
5893
- * For a subscriber whose grant reaches the ROOT but not each row under it the way a
5894
- * `liveTargets` key would: ticket0's portal customer holds `conversation:read-own` on
5895
- * their contact, which reaches the conversation's public thread, while the per-row key
5896
- * on a message is the staff read. Rooted at an entity whose subtree is exactly what that
5897
- * key's read returns, the walk is the row filter and the key is the gate.
5898
- *
5899
- * The gate is asked at the handshake (`403` if it refuses) and again, once per socket,
5900
- * on every post-commit pass that has a row beneath the root to announce. A refusal or a
5901
- * check that throws closes the socket (`1008`) and sends nothing on that pass, so a
5902
- * revoked grant, a principal who is gone, or a root relinked out of the grant's reach
5903
- * ends the subscription instead of nudging it, and the client's reconnect meets the
5904
- * handshake's `403`. A pass with nothing beneath the root asks nothing, so an idle
5905
- * subscription costs no checks.
5906
- *
5907
- * **`checkedWithin` or `vouchedWithin`?** Use `checkedWithin` whenever the subscriber is
5908
- * a principal with a grant that reaches the root: authority leaves with the grant. Use
5909
- * `vouchedWithin` only when there is no such grant to ask, as for ticket0's widget
5910
- * visitor, who holds a session token rather than a principal of their own. A vouched
5911
- * socket is asserted once, so a token revoked mid-socket keeps receiving content-free
5912
- * nudges until the socket closes. That is known and accepted for the widget: the nudge
5913
- * names nothing, and every re-read it causes is checked again.
5914
- *
5915
- * Both send `LiveNudge` frames only: the subscriber may not read each row, so it is
5916
- * never told which one changed. Only rows of a type some module declared in
5917
- * `liveTargets` are announced, as for every other subscriber.
5918
- */
5919
- export declare function checkedWithin(entity: EntityRef, permission: string): CheckedWithin;
5920
- /** Was this value built by `checkedWithin`? */
5921
- export declare function isCheckedWithin(value: unknown): value is CheckedWithin;
5922
- /**
5923
- * Is this request asking to be upgraded to a WebSocket?
5924
- *
5925
- * Paired with `LIVE_MODE_HEADER` (`@substrat-run/contracts`) because a vertical's live
5926
- * route asks it first, before anything else it decides: only a WebSocket handshake is a
5927
- * live read, and a browser always sends `Origin` on one, so everything past this check
5928
- * can trust that a missing `Origin` did not come from a browser page. A plain GET with a
5929
- * cookie on it cannot.
5930
- */
5931
- export declare function isUpgradeRequest(request: LiveUpgradeRequest): boolean;
5932
- /**
5933
- * The only thing this surface needs of an incoming request: its headers.
5934
- *
5935
- * Narrow on purpose. A live-read door reads `Upgrade` to know what is being asked for
5936
- * and `cf-connecting-o2o` to know whether this hop can carry it, and nothing else — it
5937
- * does not route on the path, read the body, or care about the method. Describing
5938
- * exactly that much is what lets a test drive the door with a two-line object instead
5939
- * of constructing a runtime request.
5940
- */
5941
- export interface LiveUpgradeRequest {
5942
- readonly headers: {
5943
- get(name: string): string | null;
5944
- };
5945
- }
5946
- /**
5947
- * One frame on a live read (#938) — the wire shape, so both ends name it once.
5948
- *
5949
- * Deliberately not the outbox envelope. An envelope carries the payload, the
5950
- * authorization chain, the impersonation stamp and the PII class, and none of those
5951
- * belong on a channel whose contract is "re-read it yourself". What a subscriber
5952
- * needs in order to act is what is here: which entity changed, and what happened to it.
5953
- */
5954
- export interface LiveChange {
5955
- /** Always `'change'` today; a field rather than an assumption, so a second kind can be added. */
5956
- readonly kind: 'change';
5957
- /**
5958
- * The event id — a ULID, so frames sort and a repeat is recognisable.
5959
- *
5960
- * **Not a gap detector, and a client must not use it as one.** ULIDs are ordered but
5961
- * not contiguous, so "the next id is not the one after this" says nothing on its own.
5962
- * More to the point, gaps here are the NORMAL case and the deliberate one: the
5963
- * permission filter withholds every event this subscriber may not read, so the ids it
5964
- * receives are a sparse subset of what the scope emitted, by design. A client
5965
- * inferring missed updates from the spacing would be reading someone else's
5966
- * entitlements as its own packet loss.
5967
- *
5968
- * What it is good for: ordering frames that arrive out of order, and discarding one
5969
- * it has already acted on. Detecting a genuinely missed update would need an
5970
- * authorized contiguous cursor — a per-subscriber sequence, counted after the filter —
5971
- * which this protocol does not have and should not grow by accident.
5972
- */
5973
- readonly id: string;
5974
- /** The emitted event type, e.g. `'ticket0/message-posted'`. */
5975
- readonly type: string;
5976
- readonly entityType: string;
5977
- readonly entityId: string;
5978
- /** When the event was emitted (ISO 8601), i.e. the emitting operation's instant. */
5979
- readonly at: string;
5980
- }
5981
- /**
5982
- * What a vouched or checked subscriber is told (#1853, #938): something beneath its root
5983
- * changed, and nothing else.
5984
- *
5985
- * No event type and no entity, deliberately. Such a subscriber holds no read on the
5986
- * entities its frames are about — the vertical vouched for the ROOT, or the principal was
5987
- * checked on the root alone, and the scope cannot know which rows under it the vertical's
5988
- * own read would show. Naming the type
5989
- * or the id would tell it what it never asked to read. The client re-reads, as it does
5990
- * on a `LiveChange`.
5991
- */
5992
- export interface LiveNudge {
5993
- readonly kind: 'nudge';
5994
- /** The event id, as on `LiveChange` — for ordering and de-duplication only. */
5995
- readonly id: string;
5996
- readonly at: string;
5997
- }
5998
- /** Any frame a live read sends. */
5999
- export type LiveFrame = LiveChange | LiveNudge;
6000
5227
  /**
6001
5228
  * Refuse a cutoff in the future — at the HostAdmin boundary, not only at the HTTP door.
6002
5229
  *