@substrat-run/kernel 0.114.0 → 0.117.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.
- package/dist/capability.d.ts +220 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +537 -0
- package/dist/capability.js.map +1 -0
- package/dist/check-key.d.ts +30 -0
- package/dist/check-key.d.ts.map +1 -0
- package/dist/check-key.js +37 -0
- package/dist/check-key.js.map +1 -0
- package/dist/denial-query.d.ts +35 -2
- package/dist/denial-query.d.ts.map +1 -1
- package/dist/denial-query.js +70 -28
- package/dist/denial-query.js.map +1 -1
- package/dist/index.d.ts +23 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -4
- package/dist/index.js.map +1 -1
- package/dist/job-run.d.ts +493 -0
- package/dist/job-run.d.ts.map +1 -0
- package/dist/job-run.js +655 -0
- package/dist/job-run.js.map +1 -0
- package/dist/outbox-event.d.ts +131 -0
- package/dist/outbox-event.d.ts.map +1 -0
- package/dist/outbox-event.js +185 -0
- package/dist/outbox-event.js.map +1 -0
- package/dist/permission-checker.d.ts +8 -1
- package/dist/permission-checker.d.ts.map +1 -1
- package/dist/permission-checker.js +18 -0
- package/dist/permission-checker.js.map +1 -1
- package/dist/permission-eval.d.ts +8 -0
- package/dist/permission-eval.d.ts.map +1 -1
- package/dist/permission-eval.js +164 -91
- package/dist/permission-eval.js.map +1 -1
- package/dist/platform-request-query.d.ts +58 -1
- package/dist/platform-request-query.d.ts.map +1 -1
- package/dist/platform-request-query.js +61 -1
- package/dist/platform-request-query.js.map +1 -1
- package/dist/platform-sweep.d.ts +180 -8
- package/dist/platform-sweep.d.ts.map +1 -1
- package/dist/platform-sweep.js +243 -34
- package/dist/platform-sweep.js.map +1 -1
- package/dist/row-decode.d.ts +107 -0
- package/dist/row-decode.d.ts.map +1 -0
- package/dist/row-decode.js +93 -0
- package/dist/row-decode.js.map +1 -0
- package/dist/scope-host.d.ts +444 -9
- package/dist/scope-host.d.ts.map +1 -1
- package/dist/scope-host.js +33 -0
- package/dist/scope-host.js.map +1 -1
- package/dist/scope-tuple-seat.d.ts +72 -0
- package/dist/scope-tuple-seat.d.ts.map +1 -0
- package/dist/scope-tuple-seat.js +93 -0
- package/dist/scope-tuple-seat.js.map +1 -0
- package/dist/subject-redaction.d.ts +160 -0
- package/dist/subject-redaction.d.ts.map +1 -0
- package/dist/subject-redaction.js +210 -0
- package/dist/subject-redaction.js.map +1 -0
- package/dist/system-switch.d.ts +108 -0
- package/dist/system-switch.d.ts.map +1 -0
- package/dist/system-switch.js +145 -0
- package/dist/system-switch.js.map +1 -0
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +115 -52
- package/dist/timeline.js.map +1 -1
- package/package.json +2 -2
package/dist/scope-host.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import type { AdminAction, ListPage, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, DelegatedReadRecord, BindHostnameInput, AdminLogEntry, OpsFailureEntry, 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, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, PermissionDenial, Coverage, BeginImpersonationInput, ImpersonationFilter, ImpersonationSession, ImpersonationSessionId, ScopeTablePage, StorageShape, Tenant, TenantId, TenantRole, TenantStoreHandle, AttachmentRecord, BlobStoreHandle, Visibility, Vertical, VerticalChannel, VerticalVersion, TenantStatus, Page, CountedPage, FreshnessSpec,
|
|
1
|
+
import type { AdminAction, BecomeCapabilityInput, CapabilityExchange, CapabilityId, MintedCapability, ListPage, Connection, ConnectionFilter, ConnectionId, ConnectionGrant, ConnectionGrantRecord, ConnectionSecret, CreateConnectionInput, OpenConnection, ProjectedConnectionGrant, ProjectedConnectionKey, AccessLogEntry, DelegatedReadRecord, BindHostnameInput, AdminLogEntry, OpsFailureEntry, 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, SystemSwitch, SystemSwitchResult, CreateOrgInput, Node, Org, OrgId, OrgMembership, PermissionKey, PlatformActorId, ChannelName, ChannelHistoryEntry, DnsRecord, HostnameBinding, HostnameStatus, PromotionAcknowledgement, PublishVersionInput, RegisterVerticalInput, VerticalServingState, RouteTarget, DirectoryDump, IdentityMembership, PrincipalId, ResolvedIdentity, RoleAssignment, RoleDefinition, QueryScopeInput, ReadScopeTableInput, Scope, ScopeDump, SubjectShredReceipt, ScopeQueryResult, ScopeId, ScopeStatus, ScopeTable, DenialFilter, DenialSummary, 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, CauseChain, EventFacetResult, HistoryEntry, ErrorCode, PlatformRequestFailureOrigin, IssueEntry, IssueStatus, IssueStatusInput } from '@substrat-run/contracts';
|
|
2
|
+
import type { CapabilityVerbs } from './capability.js';
|
|
2
3
|
import type { ModelUsageFilter, ModelUsageInput, ModelUsageWindow } from './model-usage.js';
|
|
3
4
|
import type { SealedSecret } from './secret-box.js';
|
|
4
5
|
import type { SearchHit, SearchOptions } from './search-index.js';
|
|
5
6
|
import type { EntityVersion } from './entity-version.js';
|
|
7
|
+
import type { UndrainedEvents } from './outbox-event.js';
|
|
8
|
+
import type { JobDriveReport, JobHandler, JobRun, JobRunFilter, StartJobRunInput } from './job-run.js';
|
|
6
9
|
/**
|
|
7
10
|
* What a caller asks a paged read for (#811).
|
|
8
11
|
*
|
|
@@ -152,7 +155,11 @@ export interface OperationContext {
|
|
|
152
155
|
* one to an untrusted caller does its own `assertAllowed` first.
|
|
153
156
|
*/
|
|
154
157
|
versionOf(entity: EntityRef): EntityVersion | null;
|
|
155
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Node-level check; pass `entity` for per-entity checks (portal access, §4.2 rule 3).
|
|
160
|
+
* A `permission` that is not a key (a cast past the brand) throws `internal` rather
|
|
161
|
+
* than returning a denial, and records none (#1642, `assertPermissionKey`).
|
|
162
|
+
*/
|
|
156
163
|
check(permission: PermissionKey, entity?: EntityRef): Promise<Decision>;
|
|
157
164
|
/**
|
|
158
165
|
* Find entities of one type by what a person typed (#827) — the read a picker
|
|
@@ -326,6 +333,41 @@ export interface OperationContext {
|
|
|
326
333
|
* permission problem.
|
|
327
334
|
*/
|
|
328
335
|
canAssign(roleKey: string): Promise<Coverage>;
|
|
336
|
+
/**
|
|
337
|
+
* Capabilities (#1672) — authority carried by a SECRET rather than held by a principal:
|
|
338
|
+
* "anyone with this link may read this folder until Friday".
|
|
339
|
+
*
|
|
340
|
+
* `mint` narrows authority the CALLER holds onto one entity (and what lies beneath it
|
|
341
|
+
* through declared parent edges), specific keys, an optional operation allowlist, an
|
|
342
|
+
* optional expiry and use limit, and returns the secret once. Whoever later exchanges
|
|
343
|
+
* that secret (`ScopeHost.exchangeCapability`) acts as `{ capability: <id> }` — resolved
|
|
344
|
+
* by the checker against the capability's own row, stamped on every event it emits,
|
|
345
|
+
* refused into the denial log like any other actor.
|
|
346
|
+
*
|
|
347
|
+
* Non-escalating on EVERY use, not only at mint: each key is re-checked here on the
|
|
348
|
+
* entity with this operation's own check (`ctx.grant`'s predicate), and the checker
|
|
349
|
+
* re-checks the minter every time the capability acts — so revoking the minter's access
|
|
350
|
+
* ends what their links can do. Only a principal may mint or revoke; a capability,
|
|
351
|
+
* connection or schedule cannot delegate.
|
|
352
|
+
*
|
|
353
|
+
* ```ts
|
|
354
|
+
* assertAllowed(await ctx.check('folder:share', folderRef));
|
|
355
|
+
* const { secret } = await ctx.capabilities.mint({
|
|
356
|
+
* entity: folderRef,
|
|
357
|
+
* permissions: ['folder:read'],
|
|
358
|
+
* expiresAt: input.expiresAt,
|
|
359
|
+
* });
|
|
360
|
+
* return { link: `${origin}/#share=${secret}` }; // a FRAGMENT: never sent to a server
|
|
361
|
+
* ```
|
|
362
|
+
*
|
|
363
|
+
* The kernel stores only the secret's hash, and an idempotency recording withholds the
|
|
364
|
+
* secret. Beyond that, a TRIPWIRE: while this invocation runs, `ctx.emit`,
|
|
365
|
+
* `ctx.requestPlatform` and `ctx.sql` refuse any record that carries the secret verbatim,
|
|
366
|
+
* which catches a module persisting it by accident. It is not a boundary against a module
|
|
367
|
+
* that means to leak it (`assertNoSecret` says why). Transactional with the operation,
|
|
368
|
+
* like `ctx.grant`.
|
|
369
|
+
*/
|
|
370
|
+
readonly capabilities: CapabilityVerbs;
|
|
329
371
|
/**
|
|
330
372
|
* Run `fn` as a SUB-TRANSACTION of this operation (#770,
|
|
331
373
|
* docs/architecture/sub-transactions.md) — the boundary that makes catching an
|
|
@@ -632,11 +674,23 @@ export interface FreshnessReport {
|
|
|
632
674
|
* What `runDueSchedules` did for one scope in one pass (#383). A schedule inside its
|
|
633
675
|
* cadence window is `skipped`; a due one is `fired` (its operation ran) or `failed`
|
|
634
676
|
* (the operation threw — recorded, never allowed to stop the others).
|
|
677
|
+
*
|
|
678
|
+
* A module switched off on the scope (#1666, `revokeFromSystem`) reports every schedule
|
|
679
|
+
* `skipped` with `switchedOff: true`, due or not: nothing ran, and that was decided, so
|
|
680
|
+
* it is neither a failure nor an absence. `skipped` rather than a new outcome because
|
|
681
|
+
* the durable sweep record parses outcomes on the platform side, and a vertical newer
|
|
682
|
+
* than its control plane must not emit a value that refuses the whole batch.
|
|
635
683
|
*/
|
|
636
684
|
export interface ScheduleRunReport {
|
|
637
685
|
fired: number;
|
|
638
686
|
skipped: number;
|
|
639
687
|
failed: number;
|
|
688
|
+
/**
|
|
689
|
+
* The module is switched off on this scope (#1666) — every `skipped` above is that,
|
|
690
|
+
* not a cadence window. Optional and absent otherwise, so a stored or pre-widening
|
|
691
|
+
* report stays valid.
|
|
692
|
+
*/
|
|
693
|
+
switchedOff?: true;
|
|
640
694
|
/** Per-schedule failures on this scope: the operation name and the error. */
|
|
641
695
|
errors: {
|
|
642
696
|
operation: string;
|
|
@@ -1232,10 +1286,75 @@ export interface HostAdmin {
|
|
|
1232
1286
|
* reason: one grant mechanism, tuples tombstoned on revoke (K-21), visible to the
|
|
1233
1287
|
* permission diff. It is what makes `ctx.check` resolve for a schedule — the gate
|
|
1234
1288
|
* stays `ctx.check`, not a bypass. Projected at scope provisioning from the
|
|
1235
|
-
* module's declared `schedules[].permissions
|
|
1236
|
-
*
|
|
1289
|
+
* module's declared `schedules[].permissions`.
|
|
1290
|
+
*
|
|
1291
|
+
* **Refused while the module's schedule kill switch is off on that scope** (#1666),
|
|
1292
|
+
* with `conflict`: restore is the lever; a grant is not. A grant that went through would
|
|
1293
|
+
* hand the module's system authority back to a job run or a `getSystemScope` invoke
|
|
1294
|
+
* while its schedules stayed off. Only `restoreToSystem` turns the switch back on.
|
|
1295
|
+
* (A TENANT-level grant — `node.scopeId` null — is not checked: the switch lives in each
|
|
1296
|
+
* scope's storage and the directory cannot see it. Nothing in the platform writes one.)
|
|
1237
1297
|
*/
|
|
1238
1298
|
grantToSystem(actor: PlatformActorId, grant: SystemGrant): Promise<void>;
|
|
1299
|
+
/**
|
|
1300
|
+
* The schedule kill switch (#1666): turn ONE module's scheduled work off on ONE scope.
|
|
1301
|
+
*
|
|
1302
|
+
* Module-wide on the scope, by design — see `systemSwitch` for why a per-permission or
|
|
1303
|
+
* per-schedule switch is either inexpressible or noisy. It tombstones every live
|
|
1304
|
+
* `system:<module>` grant the scope holds (K-21) and makes the scope's OFF marker live
|
|
1305
|
+
* (`system-switch.ts`). While it is off:
|
|
1306
|
+
*
|
|
1307
|
+
* - `runDueSchedules` fires nothing for the module and reports each schedule `skipped`
|
|
1308
|
+
* with `switchedOff: true` — never `failed`, so a switched-off scope makes no noise,
|
|
1309
|
+
* and its cadence clock is untouched, so a due schedule fires on the first pass after
|
|
1310
|
+
* `restoreToSystem`;
|
|
1311
|
+
* - anything acting with the module's system authority is denied by its own
|
|
1312
|
+
* `ctx.check` — a resumable job run (#1577) fails its step, a `getSystemScope` invoke
|
|
1313
|
+
* is refused;
|
|
1314
|
+
* - and nothing can hand that authority back: `grantToSystem` refuses, and a reconcile
|
|
1315
|
+
* seats no `system:` grant for the module, not even one a newer version declares.
|
|
1316
|
+
* `restoreToSystem` is the only way back.
|
|
1317
|
+
*
|
|
1318
|
+
* **Audited first, on every attempt**: an intent row (with the `reason`) before anything
|
|
1319
|
+
* moves, then an outcome row (`applied`, `refused`, or `failed`), paired by the
|
|
1320
|
+
* `operationId` the call answers with. A repeat — including the retry after a crash
|
|
1321
|
+
* between the move and its outcome row — answers `changed: false` and is audited all the
|
|
1322
|
+
* same. The scope's storage and the admin log are separate stores, so the pair is not
|
|
1323
|
+
* atomic: this order fails toward "an intent with no outcome", never toward "a switch
|
|
1324
|
+
* that moved with no row".
|
|
1325
|
+
*
|
|
1326
|
+
* Throws `not_found` when the scope holds no `system:<module>` grant at all — a typo in
|
|
1327
|
+
* an emergency must not answer "done". On a host that delegates to the deployment
|
|
1328
|
+
* serving the scope, the write happens THERE and the audit rows HERE.
|
|
1329
|
+
*/
|
|
1330
|
+
revokeFromSystem(actor: PlatformActorId, input: SystemSwitch): Promise<SystemSwitchResult>;
|
|
1331
|
+
/**
|
|
1332
|
+
* The inverse of `revokeFromSystem` (#1666), and the ONLY way to turn a switched-off
|
|
1333
|
+
* module's schedules back on: tombstones the OFF marker and restores exactly the grants
|
|
1334
|
+
* the OFF took — a grant revoked independently before the switch was pulled stays
|
|
1335
|
+
* revoked, so ON never widens the system principal. Same shape, same `reason`, same
|
|
1336
|
+
* audit-first rows, same idempotence and `not_found`.
|
|
1337
|
+
*/
|
|
1338
|
+
restoreToSystem(actor: PlatformActorId, input: SystemSwitch): Promise<SystemSwitchResult>;
|
|
1339
|
+
/**
|
|
1340
|
+
* Mint a `become` capability on a scope (#1672): whoever exchanges its secret yields
|
|
1341
|
+
* `input.principal` rather than a session — the shape an owner claim link and a member
|
|
1342
|
+
* invite are, so both can move onto this primitive later. The secret is returned once;
|
|
1343
|
+
* the scope keeps its hash. Expiry and a use limit are required.
|
|
1344
|
+
*
|
|
1345
|
+
* Platform-only in this first cut, deliberately: `become` is impersonation by another
|
|
1346
|
+
* name, and the bound on who may mint one from module code is designed with the claim
|
|
1347
|
+
* and invite migrations. Audited in the admin log (never the secret, never its hash).
|
|
1348
|
+
* Refuses an unknown or inactive scope, as `getScope` does.
|
|
1349
|
+
*/
|
|
1350
|
+
mintCapability(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, input: BecomeCapabilityInput): Promise<MintedCapability>;
|
|
1351
|
+
/**
|
|
1352
|
+
* Revoke any capability on a scope (#1672) — the operator's lever for a leaked link, and
|
|
1353
|
+
* how a re-minted claim link retires the last one. Takes effect on the next check, for
|
|
1354
|
+
* every session it handed out. Idempotent; `not_found` for a capability the scope does
|
|
1355
|
+
* not hold. Audited in the admin log.
|
|
1356
|
+
*/
|
|
1357
|
+
revokeCapability(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, capabilityId: CapabilityId): Promise<void>;
|
|
1239
1358
|
grantToOrg(actor: PlatformActorId, orgId: OrgId, permission: PermissionKey, node: Node, entity?: EntityRef): Promise<void>;
|
|
1240
1359
|
addMember(actor: PlatformActorId, tenantId: TenantId, principal: PrincipalId, orgId: OrgId): Promise<void>;
|
|
1241
1360
|
/**
|
|
@@ -1489,6 +1608,15 @@ export interface HostAdmin {
|
|
|
1489
1608
|
takenAt: string;
|
|
1490
1609
|
pending: string[];
|
|
1491
1610
|
}[]>;
|
|
1611
|
+
/**
|
|
1612
|
+
* The size in bytes of a CO-LOCATED scope's database (#1524): `SqlStorage.databaseSize`
|
|
1613
|
+
* on a Durable Object, `page_count × page_size` on SQLite. It covers the scope database
|
|
1614
|
+
* only. Attachment blobs, per-tenant D1 stores and the lake are elsewhere and are not
|
|
1615
|
+
* counted. It wakes the scope, so the one caller is an on-demand storage reading, never
|
|
1616
|
+
* a sweep. For a dispatch vertical the route reads it through the vertical's
|
|
1617
|
+
* `/internal/database-size` instead, and this is the co-located fallback. Access-logged.
|
|
1618
|
+
*/
|
|
1619
|
+
scopeDatabaseSize(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId): Promise<number>;
|
|
1492
1620
|
/**
|
|
1493
1621
|
* #286's backout: PITR-rewind a scope to a pre-migration bookmark — schema AND
|
|
1494
1622
|
* data, discarding every write since. Audited (destructive by design). The scope
|
|
@@ -1664,8 +1792,14 @@ export interface HostAdmin {
|
|
|
1664
1792
|
* repeat is harmless — the lake is keyed by event id and every consumer in this
|
|
1665
1793
|
* platform is already required-idempotent. At-least-once is the only one of the
|
|
1666
1794
|
* two that cannot silently lose exact history, which is the point of the tier.
|
|
1795
|
+
*
|
|
1796
|
+
* A row that does not decode is SKIPPED (#1636): never returned, so no sink receives an
|
|
1797
|
+
* event built from stand-ins, and never stamped, so the stamp stays exact. The read steps
|
|
1798
|
+
* past it so the rows behind it still ship, and says what it stepped over in the answer's
|
|
1799
|
+
* optional `skipped` — absent on a clean read, so every caller that reads an array reads
|
|
1800
|
+
* the one it always did. See `readUndrainedOutbox` for the bound, and for the cost.
|
|
1667
1801
|
*/
|
|
1668
|
-
readUndrainedEvents(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, limit?: number): Promise<
|
|
1802
|
+
readUndrainedEvents(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, limit?: number): Promise<UndrainedEvents>;
|
|
1669
1803
|
/**
|
|
1670
1804
|
* Stamp `drained_at` on events the sink accepted (#1334). Idempotent: marking a
|
|
1671
1805
|
* row already marked changes nothing, so a retry after a partial ship is safe.
|
|
@@ -1710,9 +1844,17 @@ export interface HostAdmin {
|
|
|
1710
1844
|
*
|
|
1711
1845
|
* A cutoff in the FUTURE is refused here, at the boundary, not only by the control-plane
|
|
1712
1846
|
* route: this is a public verb and in-process callers never pass that door.
|
|
1847
|
+
*
|
|
1848
|
+
* `countOnly` (#1545) answers the same question read-only: how many rows the window holds,
|
|
1849
|
+
* with nothing reopened and no receipt written — a count egresses nothing, so there is no
|
|
1850
|
+
* second egress for K-24 to record, and an intent row for a mutation that never happens is
|
|
1851
|
+
* a claim about the tenant's data that is not true. It is UNBOUNDED, unlike the reopen:
|
|
1852
|
+
* an aggregate materialises no rows, so it answers for the whole window in one call rather
|
|
1853
|
+
* than a batch of it. Absent, the verb reopens exactly as it always has.
|
|
1713
1854
|
*/
|
|
1714
1855
|
redrainEvents(actor: PlatformActorId, tenantId: TenantId, scopeId: ScopeId, input: {
|
|
1715
1856
|
drainedBefore: string;
|
|
1857
|
+
countOnly?: boolean;
|
|
1716
1858
|
}): Promise<number>;
|
|
1717
1859
|
/**
|
|
1718
1860
|
* Facet a CO-LOCATED scope's outbox (#1239) — narrow, group, count. For a
|
|
@@ -2214,6 +2356,19 @@ export interface HostAdmin {
|
|
|
2214
2356
|
* because asking at all is a category error the caller should see.
|
|
2215
2357
|
*/
|
|
2216
2358
|
listIdentityTenants(actor: PlatformActorId, provider: string, externalId: string): Promise<TenantId[]>;
|
|
2359
|
+
/**
|
|
2360
|
+
* `listIdentityTenants`, with what a caller does next already attached: each tenant
|
|
2361
|
+
* row (status included), the login's principal in it, and the scope the link was
|
|
2362
|
+
* made in. One read and ONE access-log row, where composing the same answer out of
|
|
2363
|
+
* `listIdentityTenants` + a `getTenant`/`resolveIdentity` per tenant is 2N+ reads and
|
|
2364
|
+
* as many log rows — on a hosted directory each of those is a round trip, and a
|
|
2365
|
+
* request that resolves its caller this way pays them before it does any work.
|
|
2366
|
+
*
|
|
2367
|
+
* Same safety condition as `listIdentityTenants`, for the same reason: **central
|
|
2368
|
+
* pools only**, and it throws on a tenant-bound or unregistered pool. Ordered by
|
|
2369
|
+
* tenant id. Non-active tenants are returned, not filtered — see `IdentityMembership`.
|
|
2370
|
+
*/
|
|
2371
|
+
listIdentityMemberships(actor: PlatformActorId, provider: string, externalId: string): Promise<IdentityMembership[]>;
|
|
2217
2372
|
/**
|
|
2218
2373
|
* Every identity link in one tenant — the projection read (#406). This is what the
|
|
2219
2374
|
* platform gathers (authoritatively, never from a caller's body) to deliver a
|
|
@@ -2629,6 +2784,36 @@ export declare const OPS_FAILURE_RETENTION_DAYS = 90;
|
|
|
2629
2784
|
* for the same reason: the table stays bounded even where no scheduled pass runs.
|
|
2630
2785
|
*/
|
|
2631
2786
|
export declare const SWEEP_RUN_RETENTION_DAYS = 14;
|
|
2787
|
+
/**
|
|
2788
|
+
* The drained batch's dedupe key on the directory's `_substrat_sweep_runs` (#1232), as
|
|
2789
|
+
* both adapters build it: `recordSweepRun` is an `INSERT OR IGNORE` against this index,
|
|
2790
|
+
* which is what makes a replayed drain write nothing twice.
|
|
2791
|
+
*
|
|
2792
|
+
* `kind` is in it since #1572, and that is the fix rather than a tidy-up. Every entry of
|
|
2793
|
+
* one drained batch shares its `request_id`, and `unit` is built from two namespaces that
|
|
2794
|
+
* nothing keeps apart — `<scopeId>:<operation>` for a schedule, `<scopeId>:<eventType>`
|
|
2795
|
+
* for a freshness verdict — while `scheduleSpec.operation` is `z.string().min(1)`. A
|
|
2796
|
+
* schedule named exactly like an event type its scope expects freshness on collided on
|
|
2797
|
+
* `(request_id, unit)`, and the ignore-on-conflict write discarded the second row with no
|
|
2798
|
+
* error anywhere. Both rows are signals by their absence (a stopped evaluator, a missed
|
|
2799
|
+
* run), so losing one read as the very thing it was meant to rule out. It is #1288's
|
|
2800
|
+
* collision, in the record table beside the state table.
|
|
2801
|
+
*
|
|
2802
|
+
* Shared rather than spelled in each adapter because a directory that predates it
|
|
2803
|
+
* re-creates it (`ensureSweepRunsIntentKind`, in both), and `lint:spine-ddl` sees only
|
|
2804
|
+
* the created copy. One definition keeps the re-created shape and the created shape the
|
|
2805
|
+
* same shape; the gate then holds each adapter's DDL to including it.
|
|
2806
|
+
*/
|
|
2807
|
+
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);";
|
|
2808
|
+
/**
|
|
2809
|
+
* Whether a directory's `_substrat_sweep_runs_intent` already has `kind` among its
|
|
2810
|
+
* columns, read off the index's `sql` in `sqlite_master` — which DO SQLite serves and
|
|
2811
|
+
* `PRAGMA` does not, so both adapters detect by one strategy. `false` means the index
|
|
2812
|
+
* must be DROPPED and created again: `CREATE … IF NOT EXISTS` matches on the name alone,
|
|
2813
|
+
* so on every existing directory the old `(request_id, unit)` index would otherwise
|
|
2814
|
+
* stand for good, while a fresh one — and any test that starts from one — gets the new.
|
|
2815
|
+
*/
|
|
2816
|
+
export declare function sweepRunsIntentHasKind(indexSql: string): boolean;
|
|
2632
2817
|
/**
|
|
2633
2818
|
* How long an issue row outlives its last occurrence (#1233). Deliberately longer
|
|
2634
2819
|
* than the 90-day evidence beneath it: an issue is the compressed memory of a
|
|
@@ -2669,10 +2854,12 @@ export interface SweepRunInput {
|
|
|
2669
2854
|
at?: string | null;
|
|
2670
2855
|
/**
|
|
2671
2856
|
* The platform-intent id a drained batch arrived under — the dedupe key. The
|
|
2672
|
-
* adapters enforce UNIQUE (requestId, unit) with an ignore-on-conflict
|
|
2673
|
-
* so a replayed drain (a settle that failed in
|
|
2674
|
-
* re-run) writes nothing twice
|
|
2675
|
-
*
|
|
2857
|
+
* adapters enforce UNIQUE (requestId, kind, unit) with an ignore-on-conflict
|
|
2858
|
+
* write (`SWEEP_RUNS_INTENT_INDEX`), so a replayed drain (a settle that failed in
|
|
2859
|
+
* transport, a partial batch re-run) writes nothing twice — while a schedule and
|
|
2860
|
+
* a freshness entry of one batch that derive the same unit are two rows (#1572).
|
|
2861
|
+
* Unset (the direct sweep path) dedupes nothing: NULLs are distinct under the
|
|
2862
|
+
* unique index, exactly as intended.
|
|
2676
2863
|
*/
|
|
2677
2864
|
requestId?: string | null;
|
|
2678
2865
|
}
|
|
@@ -3040,6 +3227,43 @@ export interface ScopeHost {
|
|
|
3040
3227
|
* so the type holds, but it is **not a person**.
|
|
3041
3228
|
*/
|
|
3042
3229
|
getSystemScope(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScopeStub>;
|
|
3230
|
+
/**
|
|
3231
|
+
* Trade a capability's SECRET for what it grants (#1672) — one counted use.
|
|
3232
|
+
*
|
|
3233
|
+
* `act`: a session token to present to `getCapabilityScope`, valid until the earlier of
|
|
3234
|
+
* `CAPABILITY_SESSION_TTL_MS` and the capability's own expiry. The harness sets it as an
|
|
3235
|
+
* HttpOnly cookie, so the secret itself is presented exactly once and never lives in
|
|
3236
|
+
* browser history or a `Referer` (`@substrat-run/vertical-host`'s
|
|
3237
|
+
* `mountCapabilityExchange`). `become`: the principal the holder becomes, for the
|
|
3238
|
+
* harness's identity directory to bind.
|
|
3239
|
+
*
|
|
3240
|
+
* `null` for an unknown, expired, revoked or used-up secret — one answer, so a probe
|
|
3241
|
+
* learns nothing. The use is atomic: a single-use secret exchanged twice at once admits
|
|
3242
|
+
* one. The exchange is on the spine as `capability.exercised`, actor `{ capability }`.
|
|
3243
|
+
* Same fail-closed (tenant, scope) and lifecycle gate as `getScope`.
|
|
3244
|
+
*
|
|
3245
|
+
* `options.mode` names the only kind the caller can handle; a secret of the other kind
|
|
3246
|
+
* answers `null` WITHOUT taking a use — so a claim link pasted into a share-link route is
|
|
3247
|
+
* refused rather than spent.
|
|
3248
|
+
*/
|
|
3249
|
+
exchangeCapability(tenantId: TenantId, scopeId: ScopeId, secret: string, options?: {
|
|
3250
|
+
mode?: 'act' | 'become';
|
|
3251
|
+
}): Promise<CapabilityExchange | null>;
|
|
3252
|
+
/**
|
|
3253
|
+
* A scope stub whose authority is a CAPABILITY (#1672) — the fifth door, beside the
|
|
3254
|
+
* principal, connection, system and impersonation ones, and a door rather than a flag for
|
|
3255
|
+
* the reason each of those is: what differs is the authority.
|
|
3256
|
+
*
|
|
3257
|
+
* The session is re-resolved on EVERY invoke, inside the operation's transaction: a
|
|
3258
|
+
* revoked or expired capability, or an expired session, is refused `unauthenticated`
|
|
3259
|
+
* before the handler runs, and an operation outside the capability's allowlist is
|
|
3260
|
+
* refused `forbidden`. Neither is a K-35 denial — no key was checked. Inside, the
|
|
3261
|
+
* operation is ordinary: `ctx.check` resolves the capability's grant (its entity subtree,
|
|
3262
|
+
* its keys, its minter's authority now), refusals ARE recorded with actor
|
|
3263
|
+
* `{ capability }`, and so is every event it emits. `ctx.principal` carries the
|
|
3264
|
+
* capability id so the type holds; it is **not a person**.
|
|
3265
|
+
*/
|
|
3266
|
+
getCapabilityScope(sessionToken: string, tenantId: TenantId, scopeId: ScopeId, options?: ScopeStubOptions): Promise<ScopeStub>;
|
|
3043
3267
|
/**
|
|
3044
3268
|
* The recurring-work declarations of every module registered on this host (#383)
|
|
3045
3269
|
* — each module's id, the vertical it belongs to, and its `schedules`. Sync like
|
|
@@ -3110,6 +3334,84 @@ export interface ScopeHost {
|
|
|
3110
3334
|
* nothing is due.
|
|
3111
3335
|
*/
|
|
3112
3336
|
drainDue(tenantId: TenantId, scopeId: ScopeId): Promise<ExecutorDrainReport>;
|
|
3337
|
+
/**
|
|
3338
|
+
* Register a JOB — long, resumable work (#1577). Host code, like
|
|
3339
|
+
* `registerExecutor`, and for the same reason: a walk of an external system
|
|
3340
|
+
* holds credentials and makes network calls, which module code may not.
|
|
3341
|
+
*
|
|
3342
|
+
* `moduleId` and `name` are two thirds of the coalescing key; the third is the
|
|
3343
|
+
* `instance` a run names. The handler is the body of ONE PASS — see `JobHandler`
|
|
3344
|
+
* and this driver's contract in `job-run.ts`. `retry` is the DEFAULT policy for
|
|
3345
|
+
* the job's steps; a step may pass its own.
|
|
3346
|
+
*
|
|
3347
|
+
* The fourth driver, and a SIBLING of the three that already exist rather than a
|
|
3348
|
+
* widening of any of them. An executor retries one delivery whole; a schedule
|
|
3349
|
+
* fires one operation that must finish; the platform sweep does a pass of
|
|
3350
|
+
* maintenance. None of them can stop halfway through an hour and carry on.
|
|
3351
|
+
*/
|
|
3352
|
+
registerJob(moduleId: ModuleId, name: string, handler: JobHandler, retry?: ExecutorRetryPolicy): void;
|
|
3353
|
+
/**
|
|
3354
|
+
* Start a run, or JOIN the one already in flight for the same
|
|
3355
|
+
* `(module, job, instance)` — the coalescing half of the driver.
|
|
3356
|
+
*
|
|
3357
|
+
* A start against a live key returns THAT run: its id, its cursor, its counters.
|
|
3358
|
+
* Not a second row, and not a refusal — asking for a re-index while one is
|
|
3359
|
+
* running is a reasonable thing to do, and the useful answer is the walk that is
|
|
3360
|
+
* already happening. Coalescing is the driver's decision, never a unique index:
|
|
3361
|
+
* a run whose worker was evicted is still `running` and MUST be restartable, so
|
|
3362
|
+
* the constraint cannot be "one row ever" (which is what `_substrat_sweep_runs`
|
|
3363
|
+
* legitimately carries, being a receipt rather than a cursor).
|
|
3364
|
+
*
|
|
3365
|
+
* The payload is held to the queue-safety rule at this boundary and refused with
|
|
3366
|
+
* the offending path named, the way an operation's input failure already is: ids
|
|
3367
|
+
* and configuration, never bytes, class instances or functions.
|
|
3368
|
+
*
|
|
3369
|
+
* Fleet maintenance, no actor — the same class as `drainDue`. What the run may DO
|
|
3370
|
+
* is decided inside it, by an ordinary `ctx.check` against `system:<moduleId>`.
|
|
3371
|
+
*/
|
|
3372
|
+
startJobRun(tenantId: TenantId, scopeId: ScopeId, input: StartJobRunInput): Promise<JobRun>;
|
|
3373
|
+
/**
|
|
3374
|
+
* Advance every DUE run on this scope — the driver proper, the resumable sibling
|
|
3375
|
+
* of `drainDue`.
|
|
3376
|
+
*
|
|
3377
|
+
* One pass per run by default: a pass does a bounded chunk, commits a cursor, and
|
|
3378
|
+
* the next call resumes from it, so an hour-long walk is never one call anybody
|
|
3379
|
+
* has to keep alive. `maxPasses` raises that for a caller with a budget; `limit`
|
|
3380
|
+
* caps how many runs one call picks up.
|
|
3381
|
+
*
|
|
3382
|
+
* A step that exhausts its retries fails ITS run and is reported — never thrown,
|
|
3383
|
+
* so one bad run cannot stop the ones behind it. Idempotent and safe when nothing
|
|
3384
|
+
* is due. A run whose job this host does not register is left untouched: it may
|
|
3385
|
+
* belong to another deployment, and failing it would destroy a resumable run
|
|
3386
|
+
* because the wrong process looked at it.
|
|
3387
|
+
*
|
|
3388
|
+
* Deliberately NOT gated on the module's `system:<moduleId>` grant the way
|
|
3389
|
+
* `runDueSchedules` is. A schedule is started by the platform and the grant is
|
|
3390
|
+
* the switch that says whether it should be; a run was started explicitly, and a
|
|
3391
|
+
* grant gate here would STALL it silently rather than refuse it. The authority it
|
|
3392
|
+
* exercises is checked where it is used, inside the operations its steps invoke.
|
|
3393
|
+
*
|
|
3394
|
+
* **One driver per scope at a time**, and that is a bound the caller holds, not one
|
|
3395
|
+
* this enforces — there is no lease. Coalescing stops duplicate RUNS; two concurrent
|
|
3396
|
+
* calls of THIS would advance the same run together. Every topology the driver is
|
|
3397
|
+
* built for gives a scope one tick (the platform sweep does one call per scope, a
|
|
3398
|
+
* scope DO's alarm fires for its own), so the bound holds by construction; the full
|
|
3399
|
+
* argument, and what an overlap would actually cost, is in `job-run.ts`.
|
|
3400
|
+
*/
|
|
3401
|
+
runDueJobs(tenantId: TenantId, scopeId: ScopeId, options?: {
|
|
3402
|
+
maxPasses?: number;
|
|
3403
|
+
limit?: number;
|
|
3404
|
+
}): Promise<JobDriveReport>;
|
|
3405
|
+
/**
|
|
3406
|
+
* The scope's run records, newest first — "the outcome has to be legible to an
|
|
3407
|
+
* operator afterwards", as a read rather than as a promise.
|
|
3408
|
+
*
|
|
3409
|
+
* Every run, in whatever state it settled, with the cursor it reached, the
|
|
3410
|
+
* counters it accumulated and the error that stopped it. Fleet maintenance, no
|
|
3411
|
+
* actor — the same class as `listPlatformRequestHistory`, which it is shaped
|
|
3412
|
+
* after for the same reason: a record nobody can read is not evidence.
|
|
3413
|
+
*/
|
|
3414
|
+
jobRuns(tenantId: TenantId, scopeId: ScopeId, filter?: JobRunFilter): Promise<JobRun[]>;
|
|
3113
3415
|
/**
|
|
3114
3416
|
* Execute ONE connector delivery with this host's directory, credentials and egress —
|
|
3115
3417
|
* the platform half of #574 phase 3. A CP-less host routes each connector delivery
|
|
@@ -3191,10 +3493,143 @@ export interface ScopeHost {
|
|
|
3191
3493
|
/** WHO refused (#841). Omitted by a caller too old to attribute — stored as NULL. */
|
|
3192
3494
|
failure?: PlatformRequestFailure | null;
|
|
3193
3495
|
}): Promise<void>;
|
|
3496
|
+
/**
|
|
3497
|
+
* Live reads (#938) — a subscription to this scope's change feed, or absent.
|
|
3498
|
+
*
|
|
3499
|
+
* **Optional on the contract, and that is the whole design.** A live read needs a
|
|
3500
|
+
* transport that can hold a connection open for longer than a request, and only one
|
|
3501
|
+
* host has one: on Cloudflare a scope IS a Durable Object, which the runtime keeps
|
|
3502
|
+
* addressable between requests and can hand a `WebSocketPair` to. The pure host is a
|
|
3503
|
+
* function call inside somebody else's process — there is no connection for it to
|
|
3504
|
+
* hold and nothing to wake when an event lands — so `SqliteScopeHost` declares
|
|
3505
|
+
* `liveReads?: never`, the `clock?: never` precedent (`CloudflareScopeHostOptions`)
|
|
3506
|
+
* applied in the other direction: there, the hosted adapter refuses an option it
|
|
3507
|
+
* cannot honour; here, the pure adapter refuses a surface it cannot honour. A
|
|
3508
|
+
* mistaken `host.liveReads!.subscribe(…)` against SQLite is a compile error rather
|
|
3509
|
+
* than a promise that never resolves.
|
|
3510
|
+
*
|
|
3511
|
+
* What this costs, stated rather than hidden, exactly as the clock seam states it:
|
|
3512
|
+
* live-read behaviour is held to its tests on the Cloudflare host ONLY. There is no
|
|
3513
|
+
* contract suite for it, because a contract suite is a claim that both adapters
|
|
3514
|
+
* answer the same way, and on this one they deliberately do not.
|
|
3515
|
+
*
|
|
3516
|
+
* A caller therefore asks rather than assumes:
|
|
3517
|
+
*
|
|
3518
|
+
* ```ts
|
|
3519
|
+
* const live = host.liveReads;
|
|
3520
|
+
* if (!live) return new Response('live reads are not available here', { status: 501 });
|
|
3521
|
+
* return live.subscribe({ tenantId, scopeId, principal, request });
|
|
3522
|
+
* ```
|
|
3523
|
+
*/
|
|
3524
|
+
readonly liveReads?: LiveReadSurface;
|
|
3194
3525
|
/** Bare operation registration (tests, glue). Names are module-namespaced: 'workorder/create'. */
|
|
3195
3526
|
defineOperation<I, O>(name: string, handler: OperationHandler<I, O>): void;
|
|
3196
3527
|
close(): Promise<void>;
|
|
3197
3528
|
}
|
|
3529
|
+
/**
|
|
3530
|
+
* The live-read surface (#938): one `Upgrade` in, one long-lived subscription out.
|
|
3531
|
+
*
|
|
3532
|
+
* **What crosses it is an invalidation, never a payload.** A frame names the event
|
|
3533
|
+
* type and the `(entityType, entityId)` it was about, and the client re-reads that
|
|
3534
|
+
* entity through the ordinary operation it already calls. That is what keeps the
|
|
3535
|
+
* change feed from quietly becoming a second, ungoverned read API: every field a
|
|
3536
|
+
* subscriber ever sees came back through the declared read surface, with that read's
|
|
3537
|
+
* own permission check, its own field-level omissions and its own PII handling.
|
|
3538
|
+
*
|
|
3539
|
+
* **Every frame is filtered per subscriber, after the commit and after the check.**
|
|
3540
|
+
* The host offers each committed event to each subscriber and delivers it only if
|
|
3541
|
+
* that subscriber passes the entity type's declared `liveTargets.readPermission` ON
|
|
3542
|
+
* THAT ENTITY — the same walk `ctx.check(key, entityRef)` makes, through the same
|
|
3543
|
+
* evaluator, so an entity-narrowed grant decides a push exactly as it decides a read.
|
|
3544
|
+
* An entity type no module declared reaches nobody. Knowing that a row exists and
|
|
3545
|
+
* changed at 14:02 is information about that row, so "the body was empty" is not a
|
|
3546
|
+
* defence: the filter runs whether or not there is a payload to withhold.
|
|
3547
|
+
*
|
|
3548
|
+
* **Generic over the runtime's request and response, because the kernel names
|
|
3549
|
+
* neither.** This package has one dependency and no DOM or workers lib
|
|
3550
|
+
* (`docs/architecture/dependency-policy.md`), which is why `FetchLike` above describes
|
|
3551
|
+
* a `fetch` structurally rather than importing one. The same rule applies here, and it
|
|
3552
|
+
* is load-bearing in the other direction too: what a caller gets back must be the
|
|
3553
|
+
* runtime's OWN response object, because a Worker's handler has to return one. A
|
|
3554
|
+
* structural stand-in would describe it correctly and still not be returnable. So the
|
|
3555
|
+
* type travels through instead of being restated — `CloudflareScopeHost` binds it to
|
|
3556
|
+
* the real `Request`/`Response`, and code holding the bare contract gets `unknown` back
|
|
3557
|
+
* and narrows at its own mount point, where it knows which host it is on.
|
|
3558
|
+
*/
|
|
3559
|
+
export interface LiveReadSurface<Req extends LiveUpgradeRequest = LiveUpgradeRequest, Res = unknown> {
|
|
3560
|
+
/**
|
|
3561
|
+
* Accept a WebSocket `Upgrade` and subscribe the principal to the scope's changes.
|
|
3562
|
+
*
|
|
3563
|
+
* The caller is the vertical, and it has already done the one thing this surface
|
|
3564
|
+
* cannot do for itself: resolved WHO is asking. The principal is an authenticated
|
|
3565
|
+
* fact the vertical's session carries, exactly as it is for an `invoke` — this
|
|
3566
|
+
* surface trusts it the same way and no further, which is why every frame is still
|
|
3567
|
+
* checked against it individually rather than a subscription being authorized once.
|
|
3568
|
+
*
|
|
3569
|
+
* Returns the 101 to hand back to the client, or a refusal to return as-is: a
|
|
3570
|
+
* request that is not an upgrade gets 426, and one arriving over a hop that cannot
|
|
3571
|
+
* carry a socket gets a refusal naming that, so the client can fall back to polling
|
|
3572
|
+
* KNOWINGLY. Neither is thrown, because both are answers to a client rather than
|
|
3573
|
+
* faults of the caller.
|
|
3574
|
+
*/
|
|
3575
|
+
subscribe(input: {
|
|
3576
|
+
tenantId: TenantId;
|
|
3577
|
+
scopeId: ScopeId;
|
|
3578
|
+
/** WHO is subscribing — resolved by the vertical from its own session, never by the client. */
|
|
3579
|
+
principal: PrincipalId;
|
|
3580
|
+
/** The upgrade request as it arrived, carried whole so the host reads its own headers. */
|
|
3581
|
+
request: Req;
|
|
3582
|
+
}): Promise<Res>;
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* The only thing this surface needs of an incoming request: its headers.
|
|
3586
|
+
*
|
|
3587
|
+
* Narrow on purpose. A live-read door reads `Upgrade` to know what is being asked for
|
|
3588
|
+
* and `cf-connecting-o2o` to know whether this hop can carry it, and nothing else — it
|
|
3589
|
+
* does not route on the path, read the body, or care about the method. Describing
|
|
3590
|
+
* exactly that much is what lets a test drive the door with a two-line object instead
|
|
3591
|
+
* of constructing a runtime request.
|
|
3592
|
+
*/
|
|
3593
|
+
export interface LiveUpgradeRequest {
|
|
3594
|
+
readonly headers: {
|
|
3595
|
+
get(name: string): string | null;
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
/**
|
|
3599
|
+
* One frame on a live read (#938) — the wire shape, so both ends name it once.
|
|
3600
|
+
*
|
|
3601
|
+
* Deliberately not the outbox envelope. An envelope carries the payload, the
|
|
3602
|
+
* authorization chain, the impersonation stamp and the PII class, and none of those
|
|
3603
|
+
* belong on a channel whose contract is "re-read it yourself". What a subscriber
|
|
3604
|
+
* needs in order to act is what is here: which entity changed, and what happened to it.
|
|
3605
|
+
*/
|
|
3606
|
+
export interface LiveChange {
|
|
3607
|
+
/** Always `'change'` today; a field rather than an assumption, so a second kind can be added. */
|
|
3608
|
+
readonly kind: 'change';
|
|
3609
|
+
/**
|
|
3610
|
+
* The event id — a ULID, so frames sort and a repeat is recognisable.
|
|
3611
|
+
*
|
|
3612
|
+
* **Not a gap detector, and a client must not use it as one.** ULIDs are ordered but
|
|
3613
|
+
* not contiguous, so "the next id is not the one after this" says nothing on its own.
|
|
3614
|
+
* More to the point, gaps here are the NORMAL case and the deliberate one: the
|
|
3615
|
+
* permission filter withholds every event this subscriber may not read, so the ids it
|
|
3616
|
+
* receives are a sparse subset of what the scope emitted, by design. A client
|
|
3617
|
+
* inferring missed updates from the spacing would be reading someone else's
|
|
3618
|
+
* entitlements as its own packet loss.
|
|
3619
|
+
*
|
|
3620
|
+
* What it is good for: ordering frames that arrive out of order, and discarding one
|
|
3621
|
+
* it has already acted on. Detecting a genuinely missed update would need an
|
|
3622
|
+
* authorized contiguous cursor — a per-subscriber sequence, counted after the filter —
|
|
3623
|
+
* which this protocol does not have and should not grow by accident.
|
|
3624
|
+
*/
|
|
3625
|
+
readonly id: string;
|
|
3626
|
+
/** The emitted event type, e.g. `'ticket0/message-posted'`. */
|
|
3627
|
+
readonly type: string;
|
|
3628
|
+
readonly entityType: string;
|
|
3629
|
+
readonly entityId: string;
|
|
3630
|
+
/** When the event was emitted (ISO 8601), i.e. the emitting operation's instant. */
|
|
3631
|
+
readonly at: string;
|
|
3632
|
+
}
|
|
3198
3633
|
/**
|
|
3199
3634
|
* Refuse a cutoff in the future — at the HostAdmin boundary, not only at the HTTP door.
|
|
3200
3635
|
*
|