@substrat-run/adapter-sqlite 0.82.0 → 0.84.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/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type BlobStoreHandle, type ConnectionId, type ModuleId, type DomainEvent, type PlatformRequestId, type PlatformRequest, type PlatformRequestFilter, type PlatformRequestStatus, type PlatformActorId, type PrincipalId, type ScopeDump, type ScopeId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
2
- import { type BlobStoreProvisionInput, type ScopeAttachments, type ExecutorDeadLetter, type ExecutorDrainReport, type MigrateScopeOutcome, type MigrationFrontier, type ExecutorHandler, type ExecutorRetryPolicy, type ConnectorHandler, type ConnectorOptions, type FetchLike, type SecretBox, type HostAdmin, type ModuleRegistration, type OperationHandler, type PermissionChecker, type ProvisionScopeInput, type ScheduleRegistration, type ScheduleRunReport, type ScopeHost, type ScopeStub, type ScopeStubOptions, type TenantRelationalStore, type TenantStoreProvisionInput } from '@substrat-run/kernel';
1
+ import { type BlobStoreHandle, type ConnectionId, type ModuleId, type DomainEvent, type PlatformRequestId, type PlatformRequest, type PlatformRequestFilter, type PlatformRequestStatus, type PlatformRequestFailure, type ProjectedConnectionGrant, type PlatformActorId, type PrincipalId, type ScopeDump, type ScopeId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
2
+ import { type BlobStoreProvisionInput, type ScopeAttachments, type ExecutorDeadLetter, type ExecutorDrainReport, type MigrateScopeOutcome, type MigrationFrontier, type ExecutorHandler, type ExecutorRetryPolicy, type ConnectorHandler, type ConnectorOptions, type Clock, type FetchLike, type SecretBox, type HostAdmin, type ModuleRegistration, type OperationHandler, type PermissionChecker, type ProvisionScopeInput, type ScheduleRegistration, type ScheduleRunReport, type ScopeHost, type ScopeStub, type ScopeStubOptions, type TenantRelationalStore, type TenantStoreProvisionInput } from '@substrat-run/kernel';
3
3
  export interface SqliteScopeHostOptions {
4
4
  /** Directory holding one SQLite file per scope plus the directory database. */
5
5
  dir: string;
@@ -20,6 +20,15 @@ export interface SqliteScopeHostOptions {
20
20
  * provider will not produce on demand.
21
21
  */
22
22
  fetch?: FetchLike;
23
+ /**
24
+ * What `ctx.now()` reads (#812). Defaults to the wall clock.
25
+ *
26
+ * The same seam as `fetch`, for the same reason: a scenario that needs an
27
+ * absence window to lapse, a metering period to roll, or a booking hold to
28
+ * expire cannot get there by waiting. Hand in a frozen or scripted clock and
29
+ * the interesting case becomes assertable.
30
+ */
31
+ clock?: Clock;
23
32
  }
24
33
  export declare class SqliteScopeHost implements ScopeHost {
25
34
  readonly admin: HostAdmin;
@@ -43,6 +52,8 @@ export declare class SqliteScopeHost implements ScopeHost {
43
52
  private readonly relations;
44
53
  /** entityType → its derived FTS5 index (#827). One entity type, one index. */
45
54
  private readonly searchPlans;
55
+ /** #811: the paged lists modules declare, by entity type. Same one-owner rule as search. */
56
+ private readonly listPlans;
46
57
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
47
58
  private readonly attachmentTargets;
48
59
  /** operation name → its owning module's entitlementKey (§4.3 gate). */
@@ -60,6 +71,7 @@ export declare class SqliteScopeHost implements ScopeHost {
60
71
  private readonly systemPrincipal;
61
72
  private readonly secretBox;
62
73
  private readonly fetchImpl;
74
+ private readonly clock;
63
75
  constructor(options: SqliteScopeHostOptions);
64
76
  /**
65
77
  * The directory schema, applied on every open — and again after a restore (#40).
@@ -181,6 +193,15 @@ export declare class SqliteScopeHost implements ScopeHost {
181
193
  dispatchConnector(tenantId: TenantId, scopeId: ScopeId, handler: ConnectorHandler, event: DomainEvent, options?: {
182
194
  timeoutMs?: number;
183
195
  }): Promise<void>;
196
+ /**
197
+ * The scope's own answer to "what may this connection do here" (#726 gap 1) — read
198
+ * from the delivered `connection:<id>` tuples, which is the same row the checker
199
+ * walks, so this cannot disagree with what would be enforced.
200
+ *
201
+ * Live only: a tombstoned tuple (K-21) and one past `expires_at` are both unenforced,
202
+ * and reporting either would make the read a worse answer than no read at all.
203
+ */
204
+ connectionGrantsInScope(tenantId: TenantId, scopeId: ScopeId): Promise<ProjectedConnectionGrant[]>;
184
205
  migrationFrontier(): MigrationFrontier;
185
206
  /**
186
207
  * The reconciliation sweep's wake + retry (kernel-design §5.3, #49). Reuses
@@ -199,6 +220,7 @@ export declare class SqliteScopeHost implements ScopeHost {
199
220
  status: PlatformRequestStatus;
200
221
  result?: unknown;
201
222
  lastError?: string | null;
223
+ failure?: PlatformRequestFailure | null;
202
224
  }): Promise<void>;
203
225
  private dispatch;
204
226
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import Database from 'better-sqlite3';
4
- import { accessLogEntry, adminLogEntry, opsFailureEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, platformRequest, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, subjectRef, createConnectionInput, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, outboundOfManifestJson, } from '@substrat-run/contracts';
5
- import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, } from '@substrat-run/kernel';
4
+ import { accessLogEntry, adminLogEntry, opsFailureEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, platformRequest, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, subjectRef, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, outboundOfManifestJson, listLimitOf, } from '@substrat-run/contracts';
5
+ import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, PLATFORM_REQUEST_COLUMNS, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, } from '@substrat-run/kernel';
6
6
  import { ScopeActor } from './actor.js';
7
7
  import { createTupleChecker } from './checker.js';
8
8
  const KERNEL_DDL = `
@@ -35,6 +35,12 @@ const KERNEL_DDL = `
35
35
  status TEXT NOT NULL DEFAULT 'pending',
36
36
  attempts INTEGER NOT NULL DEFAULT 0,
37
37
  last_error TEXT,
38
+ -- #841: WHO refused, as JSON {origin, code, permission}. last_error says WHAT
39
+ -- happened and always did; this says whether it was the provider's answer or our
40
+ -- own refusal before egress -- the distinction the dashboard was guessing wrong.
41
+ -- NULL = never classified (a row settled before this column, or one that never
42
+ -- failed), which is not the same fact as an origin of 'unknown'.
43
+ last_failure TEXT,
38
44
  result TEXT,
39
45
  requested_at TEXT NOT NULL,
40
46
  settled_at TEXT
@@ -187,11 +193,49 @@ function rowToPlatformRequest(r) {
187
193
  status: r.status,
188
194
  attempts: r.attempts,
189
195
  lastError: r.last_error,
196
+ failure: r.last_failure == null ? null : JSON.parse(r.last_failure),
190
197
  result: r.result === null ? null : JSON.parse(r.result),
191
198
  requestedAt: r.requested_at,
192
199
  settledAt: r.settled_at,
193
200
  });
194
201
  }
202
+ /**
203
+ * The dispatch capability (#726 remedy B): a connector delivery may read the attachments
204
+ * of the entity the DELIVERED EVENT names, and nothing else.
205
+ *
206
+ * The read a signing connector needs is inherently per-dispatch — the event names one
207
+ * `documentAttachmentId`, `bindDocument` already refused to bind an attachment owned by
208
+ * anything but the instance being signed, and `openAttachment` takes an id rather than a
209
+ * search. Modelling that as a standing scope-wide `protocol:read` grant was the mismatch
210
+ * this closes: the grant site has no entity leg, so the narrow check was being asked a
211
+ * question the grant model could only answer broadly — and `protocol:read` also gates
212
+ * `protocol/get`, `list-templates` and `list-for-entity`, none of which a connector
213
+ * sending one named document has any business reaching.
214
+ *
215
+ * **Derived here, never asserted by the caller.** The entity comes from this scope's own
216
+ * kernel-stamped spine row, looked up by event id. On the hosted path the platform runs
217
+ * the connector, so an entity passed across the seam would be the platform's word for
218
+ * what it is allowed to read; an event id it must resolve against the deployment's own
219
+ * outbox is not. The platform can name any delivery; it cannot name an entity.
220
+ *
221
+ * **No fallback to the permission check.** Inside a dispatch this IS the authority. A
222
+ * grant would only re-widen what the whole point is to narrow, and there is no read a
223
+ * connector legitimately makes during a delivery that the delivery does not name.
224
+ */
225
+ const admitByDelivery = (rt, eventId, row) => {
226
+ const event = rt.db
227
+ .prepare('SELECT entity_type, entity_id FROM _substrat_outbox WHERE id = ?')
228
+ .get(eventId);
229
+ if (!event) {
230
+ throw new PermissionDenied(`attachment ${row.id}: delivery ${eventId} is not an event of this scope, so it ` +
231
+ `carries no authority to read anything here`);
232
+ }
233
+ if (event.entity_type !== row.entity_type || event.entity_id !== row.entity_id) {
234
+ throw new PermissionDenied(`attachment ${row.id} belongs to ${row.entity_type}/${row.entity_id}, and this ` +
235
+ `delivery is for ${event.entity_type}/${event.entity_id} — a connector may read ` +
236
+ `the attachments of the entity its event names, and no others`);
237
+ }
238
+ };
195
239
  export class SqliteScopeHost {
196
240
  admin;
197
241
  dir;
@@ -214,6 +258,8 @@ export class SqliteScopeHost {
214
258
  relations = new Map();
215
259
  /** entityType → its derived FTS5 index (#827). One entity type, one index. */
216
260
  searchPlans = new Map();
261
+ /** #811: the paged lists modules declare, by entity type. Same one-owner rule as search. */
262
+ listPlans = new Map();
217
263
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
218
264
  attachmentTargets = new Map();
219
265
  /** operation name → its owning module's entitlementKey (§4.3 gate). */
@@ -231,9 +277,11 @@ export class SqliteScopeHost {
231
277
  systemPrincipal = principalId.parse(ulid());
232
278
  secretBox;
233
279
  fetchImpl;
280
+ clock;
234
281
  constructor(options) {
235
282
  this.secretBox = options.secretBox ?? unconfiguredSecretBox;
236
283
  this.fetchImpl = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
284
+ this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
237
285
  this.dir = options.dir;
238
286
  mkdirSync(this.dir, { recursive: true });
239
287
  this.directory = new Database(join(this.dir, '_directory.sqlite'));
@@ -756,7 +804,9 @@ export class SqliteScopeHost {
756
804
  * than assuming the worse case is what keeps the platform-dispatch path under the
757
805
  * K-6 serialization the adapter promises.
758
806
  */
759
- connectorContext(rt, timeoutMs, holdsActor) {
807
+ connectorContext(rt, timeoutMs, holdsActor,
808
+ /** The delivery this context is FOR (#726) — the dispatch capability's whole basis. */
809
+ eventId) {
760
810
  const vertical = this.directory
761
811
  .prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
762
812
  .get(rt.scopeId)?.vertical ?? null;
@@ -789,10 +839,19 @@ export class SqliteScopeHost {
789
839
  // serialized turn and stays under K-6 like every other reader. Assuming
790
840
  // the reentrant case everywhere would have dropped serialization on a path
791
841
  // that never needed it.
842
+ // #726 gap 1: this connection's live grants IN THIS SCOPE, so a connector can
843
+ // assert its preconditions at the top of a dispatch rather than discovering a
844
+ // missing grant as a refusal several calls later.
845
+ grants: async () => (await this.connectionGrantsInScope(rt.tenantId, rt.scopeId))
846
+ .filter((g) => g.connectionId === open.id)
847
+ .map((g) => g.permission),
792
848
  openAttachment: async (attachmentId) => {
793
849
  const store = this.attachmentStore(rt.tenantId, vertical);
794
850
  return this.buildAttachments(rt, { kind: 'connection', id: open.id }, store, {
795
851
  reentrant: holdsActor,
852
+ // #726 remedy (B): inside a dispatch the authority to read is the DELIVERY,
853
+ // not a standing grant. See `admitByEvent` on `buildAttachments`.
854
+ admitByEvent: eventId,
796
855
  }).open(attachmentId);
797
856
  },
798
857
  // #687: open a cell the scope sealed TO this connection. The keyId-indexed
@@ -896,9 +955,27 @@ export class SqliteScopeHost {
896
955
  }
897
956
  this.searchPlans.set(plan.entityType, plan);
898
957
  }
958
+ // #811: the list indexes this module's `lists` declare — same placement and
959
+ // the same reason as the search indexes above: appended AFTER the module's
960
+ // own migrations, so `CREATE INDEX` names a table that exists.
961
+ const listMigrations = listIndexMigrations(manifest.id, manifest.lists);
962
+ for (const m of listMigrations) {
963
+ if (seen.has(m.version)) {
964
+ throw new Error(`duplicate migration version in ${manifest.id}: ${m.version}`);
965
+ }
966
+ seen.add(m.version);
967
+ }
968
+ for (const plan of listIndexPlans(manifest.id, manifest.lists)) {
969
+ const existing = this.listPlans.get(plan.entityType);
970
+ if (existing) {
971
+ throw new Error(`list: '${plan.entityType}' declares a paged list in both '${existing.moduleId}' and ` +
972
+ `'${plan.moduleId}' — one entity type, one walk; rename one`);
973
+ }
974
+ this.listPlans.set(plan.entityType, plan);
975
+ }
899
976
  this.modules.set(manifest.id, {
900
977
  id: manifest.id,
901
- migrations: [...migrations, ...searchMigrations],
978
+ migrations: [...migrations, ...searchMigrations, ...listMigrations],
902
979
  consumers,
903
980
  schedules: manifest.schedules ?? [],
904
981
  });
@@ -1362,6 +1439,10 @@ export class SqliteScopeHost {
1362
1439
  .get(attachmentId);
1363
1440
  if (!row)
1364
1441
  return null;
1442
+ if (opts.admitByEvent !== undefined) {
1443
+ admitByDelivery(rt, opts.admitByEvent, row);
1444
+ return rowToRecord(row);
1445
+ }
1365
1446
  const gate = targetGate(row.entity_type);
1366
1447
  assertAllowed(await ctx.check(gate.read, { entityType: row.entity_type, entityId: row.entity_id }));
1367
1448
  return rowToRecord(row);
@@ -1919,7 +2000,7 @@ export class SqliteScopeHost {
1919
2000
  if (executor.kind === 'connector') {
1920
2001
  // `true`: dispatchExecutors is only ever reached from inside
1921
2002
  // `rt.actor.enqueue` (invoke's post-commit tail, or drainDue).
1922
- await executor.handler(this.connectorContext(rt, executor.timeoutMs, true), event);
2003
+ await executor.handler(this.connectorContext(rt, executor.timeoutMs, true, event.id), event);
1923
2004
  }
1924
2005
  else {
1925
2006
  await executor.handler(this.admin, event);
@@ -1986,12 +2067,47 @@ export class SqliteScopeHost {
1986
2067
  try {
1987
2068
  // `false`: this path deliberately does NOT enqueue, so nothing is held and
1988
2069
  // the connection's reads take an ordinary serialized turn.
1989
- await handler(this.connectorContext(rt, options?.timeoutMs ?? 30_000, false), event);
2070
+ await handler(this.connectorContext(rt, options?.timeoutMs ?? 30_000, false, event.id), event);
1990
2071
  }
1991
2072
  finally {
1992
2073
  this.causedBy = null;
1993
2074
  }
1994
2075
  }
2076
+ /**
2077
+ * The scope's own answer to "what may this connection do here" (#726 gap 1) — read
2078
+ * from the delivered `connection:<id>` tuples, which is the same row the checker
2079
+ * walks, so this cannot disagree with what would be enforced.
2080
+ *
2081
+ * Live only: a tombstoned tuple (K-21) and one past `expires_at` are both unenforced,
2082
+ * and reporting either would make the read a worse answer than no read at all.
2083
+ */
2084
+ async connectionGrantsInScope(tenantId, scopeId) {
2085
+ const rt = this.runtime(tenantId, scopeId);
2086
+ await this.applyPendingMigrations(rt);
2087
+ const now = new Date().toISOString();
2088
+ // BOTH tuple stores, because a scope check consults both: rule 2 inheritance means
2089
+ // a tenant-level grant (`grantToConnection` with `scopeId: null`) is enforced here
2090
+ // exactly as a scope-level one is. Reading only the scope's own table would answer
2091
+ // "not granted" where the checker answers allow — a read-back that disagrees with
2092
+ // enforcement is worse than none, since it is the read an operator would trust.
2093
+ const rows = [
2094
+ ...rt.db
2095
+ .prepare(`SELECT subject, relation, expires_at FROM _substrat_tuples
2096
+ WHERE subject LIKE 'connection:%' AND relation LIKE 'granted:%'
2097
+ AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`)
2098
+ .all(now),
2099
+ ...this.directory
2100
+ .prepare(`SELECT subject, relation, expires_at FROM _substrat_tenant_tuples
2101
+ WHERE tenant_id = ? AND subject LIKE 'connection:%' AND relation LIKE 'granted:%'
2102
+ AND revoked_at IS NULL AND (expires_at IS NULL OR expires_at > ?)`)
2103
+ .all(tenantId, now),
2104
+ ].sort((a, b) => a.subject.localeCompare(b.subject) || a.relation.localeCompare(b.relation));
2105
+ return rows.map((r) => projectedConnectionGrant.parse({
2106
+ connectionId: r.subject.slice('connection:'.length),
2107
+ permission: r.relation.slice('granted:'.length),
2108
+ ...(r.expires_at ? { expiresAt: r.expires_at } : {}),
2109
+ }));
2110
+ }
1995
2111
  migrationFrontier() {
1996
2112
  let total = 0;
1997
2113
  for (const mod of this.modules.values())
@@ -2077,7 +2193,7 @@ export class SqliteScopeHost {
2077
2193
  const rt = this.runtime(tenantId, scopeId);
2078
2194
  await this.applyPendingMigrations(rt);
2079
2195
  const rows = rt.db
2080
- .prepare(`SELECT id, kind, payload, requested_by, status, attempts, last_error, result, requested_at, settled_at
2196
+ .prepare(`SELECT ${PLATFORM_REQUEST_COLUMNS}
2081
2197
  FROM _substrat_platform_requests WHERE status = 'pending' ORDER BY id`)
2082
2198
  .all();
2083
2199
  return rows.map(rowToPlatformRequest);
@@ -2094,10 +2210,10 @@ export class SqliteScopeHost {
2094
2210
  await rt.actor.enqueue(() => {
2095
2211
  rt.db
2096
2212
  .prepare(`UPDATE _substrat_platform_requests
2097
- SET status = ?, result = COALESCE(?, result), last_error = ?,
2213
+ SET status = ?, result = COALESCE(?, result), last_error = ?, last_failure = ?,
2098
2214
  attempts = attempts + 1, settled_at = ?
2099
2215
  WHERE id = ?`)
2100
- .run(outcome.status, outcome.result === undefined ? null : JSON.stringify(outcome.result), outcome.lastError ?? null, outcome.status === 'pending' ? null : new Date().toISOString(), id);
2216
+ .run(outcome.status, outcome.result === undefined ? null : JSON.stringify(outcome.result), outcome.lastError ?? null, outcome.failure == null ? null : JSON.stringify(outcome.failure), outcome.status === 'pending' ? null : new Date().toISOString(), id);
2101
2217
  });
2102
2218
  }
2103
2219
  async dispatch(rt) {
@@ -4767,11 +4883,23 @@ export class SqliteScopeHost {
4767
4883
  const checker = this.checker;
4768
4884
  const relations = this.relations;
4769
4885
  const searchPlans = this.searchPlans;
4886
+ const listPlans = this.listPlans;
4770
4887
  // K-34: the checks that passed in THIS operation. The context is created per invoke
4771
4888
  // (see buildStub), so this accumulates one operation's authorizations; `emit` snapshots
4772
4889
  // whatever has passed up to that point. A system/override actor is unconditionally
4773
4890
  // allowed, so its checks are not authorizations and are not recorded.
4774
4891
  const passed = [];
4892
+ /**
4893
+ * The operation's instant (#812), read ONCE when the context is built.
4894
+ *
4895
+ * Everything the operation stamps — its own rows via `ctx.now()`, the
4896
+ * `occurredAt` on every event it emits, the `requested_at` on every platform
4897
+ * intent it enqueues — comes from here, so a row and the event announcing it
4898
+ * can never disagree about when. Reading the clock per-call would have made
4899
+ * `ctx.now()` deterministic only in the sense that each reading is separately
4900
+ * unpredictable.
4901
+ */
4902
+ const at = this.clock();
4775
4903
  /**
4776
4904
  * The scope host's half of `ctx.atomic` (#770) — everything else is the
4777
4905
  * kernel's (`createAtomic`). SQLite gives us savepoints, which nest inside
@@ -4835,12 +4963,13 @@ export class SqliteScopeHost {
4835
4963
  scopeId: rt.scopeId,
4836
4964
  principal,
4837
4965
  sql: scopedSql(rt.db),
4966
+ now: () => at,
4838
4967
  emit: (event) => {
4839
4968
  const input = domainEventInput.parse(event);
4840
4969
  const full = domainEvent.parse({
4841
4970
  ...input,
4842
4971
  id: eventId.parse(ulid()),
4843
- occurredAt: instant.parse(new Date().toISOString()),
4972
+ occurredAt: at,
4844
4973
  tenantId: rt.tenantId,
4845
4974
  scopeId: rt.scopeId,
4846
4975
  actor: overrideActor ??
@@ -4879,7 +5008,7 @@ export class SqliteScopeHost {
4879
5008
  .prepare(`INSERT INTO _substrat_platform_requests
4880
5009
  (id, kind, payload, requested_by, status, attempts, requested_at)
4881
5010
  VALUES (?, ?, ?, ?, 'pending', 0, ?)`)
4882
- .run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), instant.parse(new Date().toISOString()));
5011
+ .run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), at);
4883
5012
  if (signals)
4884
5013
  signals.platformRequests += 1;
4885
5014
  return id;
@@ -4904,6 +5033,37 @@ export class SqliteScopeHost {
4904
5033
  const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit));
4905
5034
  return rt.db.prepare(q.sql).all(...q.params).map((row) => ({ entityType, id: row.id, rank: row.rank }));
4906
5035
  },
5036
+ /**
5037
+ * #811. The plan is registration state, the indexes are scope state — same
5038
+ * split as search, and the same consequence: an entity nobody declared is
5039
+ * `NotListable` (a wiring mistake), while a declared one whose indexes are
5040
+ * missing in THIS scope cannot happen, because the DDL rides the same
5041
+ * journal as the module's own migrations.
5042
+ */
5043
+ page: (entityType, params) => {
5044
+ const plan = listPlans.get(entityType);
5045
+ if (!plan)
5046
+ throw new NotListable(entityType);
5047
+ const limit = listLimitOf(params.limit);
5048
+ const q = listQuery(plan, {
5049
+ limit,
5050
+ sort: params.sort,
5051
+ order: params.order,
5052
+ cursor: params.cursor,
5053
+ filters: params.filters,
5054
+ });
5055
+ const rows = rt.db.prepare(q.sql).all(...q.params);
5056
+ // A FULL page may have more; a short one is the end of the walk. Same rule
5057
+ // as `pageOf`, applied here because the cursor is a COLUMN's value (plus
5058
+ // the tie-break) rather than a field the caller could name.
5059
+ const last = rows.length >= limit ? rows[rows.length - 1] : undefined;
5060
+ const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn);
5061
+ const page = { entries: rows, nextCursor };
5062
+ if (!params.total)
5063
+ return page;
5064
+ const counted = rt.db.prepare(q.countSql).all(...q.countParams);
5065
+ return { ...page, total: counted[0]?.n ?? 0 };
5066
+ },
4907
5067
  /**
4908
5068
  * Narrow a permission this caller already holds onto one entity (#K-sharing).
4909
5069
  *
@@ -5119,6 +5279,10 @@ export class SqliteScopeHost {
5119
5279
  // so legacy outbox rows read as "unrecorded" — the honest value. (_substrat_denials
5120
5280
  // is a whole new table, so KERNEL_DDL's IF NOT EXISTS covers it with no ALTER.)
5121
5281
  this.ensureColumn(db, '_substrat_outbox', 'authorization', 'authorization TEXT');
5282
+ // #841: refusal attribution on a scope DB that predates it. Nullable, so every row
5283
+ // already settled reads as "nobody classified this" rather than claiming an origin
5284
+ // the drain never actually decided.
5285
+ this.ensureColumn(db, '_substrat_platform_requests', 'last_failure', 'last_failure TEXT');
5122
5286
  const appliedMigrations = new Set(db.prepare('SELECT module_id, version FROM _substrat_migrations').all().map((r) => `${r.module_id}@${r.version}`));
5123
5287
  const created = { tenantId, scopeId, db, actor: new ScopeActor(), appliedMigrations };
5124
5288
  this.scopes.set(key, created);