@substrat-run/adapter-sqlite 0.88.0 → 0.90.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,4 +1,4 @@
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';
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 ImpersonationSessionId, type TenantId, type TenantStoreHandle } from '@substrat-run/contracts';
2
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. */
@@ -71,6 +71,13 @@ export declare class SqliteScopeHost implements ScopeHost {
71
71
  * a bare `defineOperation` carries no declaration to read one off.
72
72
  */
73
73
  private readonly operationConcurrency;
74
+ /**
75
+ * #116: the operations that declared `idempotency: false` — a set of refusals,
76
+ * because that is what the declaration is. Every name NOT in here honours an
77
+ * `Idempotency-Key`, which is the default and the reason it is a set of
78
+ * exceptions rather than a map of participants.
79
+ */
80
+ private readonly operationIdempotencyOptOut;
74
81
  private readonly roles;
75
82
  /** Executor id → {eventType, handler} (K-22 §4.2). Host code, not module code. */
76
83
  private readonly executors;
@@ -164,10 +171,21 @@ export declare class SqliteScopeHost implements ScopeHost {
164
171
  */
165
172
  getConnectorScope(connectionId: ConnectionId, scopeId: ScopeId): Promise<ScopeStub>;
166
173
  getConnectorAttachments(connectionId: ConnectionId, scopeId: ScopeId): Promise<ScopeAttachments>;
174
+ /** Read one session, or say so. The door and every invoke through it come here. */
175
+ private readImpersonation;
176
+ /**
177
+ * The impersonation door (K-42) — mirror of `getConnectorScope`/`getSystemScope`.
178
+ *
179
+ * The session is the authority and it is read from the directory, never from
180
+ * the caller: a stub cannot be minted by describing a session, only by naming
181
+ * one somebody opened and the admin log recorded. Everything after that is an
182
+ * ordinary invoke against the impersonated principal.
183
+ */
184
+ getImpersonatedScope(session: ImpersonationSessionId, tenantId: TenantId, scopeId: ScopeId, options?: ScopeStubOptions): Promise<ScopeStub>;
167
185
  getSystemScope(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScopeStub>;
168
186
  registeredSchedules(): ScheduleRegistration[];
169
187
  runDueSchedules(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScheduleRunReport>;
170
- /** The stub body, shared by the principal and connection doors. */
188
+ /** The stub body, shared by the principal, connection, system and impersonation doors. */
171
189
  private buildStub;
172
190
  /**
173
191
  * The entity a guarded operation's precondition is about (#129).
@@ -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, 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, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialBucketRow, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, } 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, requestFingerprint, 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, substratError, } from '@substrat-run/contracts';
5
+ import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, OPS_FAILURE_RETENTION_DAYS, backoffAt, platformRequestHistoryQuery, PLATFORM_REQUEST_COLUMNS, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialBucketRow, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, IDEMPOTENCY_DDL, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, } from '@substrat-run/kernel';
6
6
  import { ScopeActor } from './actor.js';
7
7
  import { createTupleChecker } from './checker.js';
8
8
  const KERNEL_DDL = `
@@ -22,6 +22,10 @@ const KERNEL_DDL = `
22
22
  -- K-34: the checks the emitting operation passed (JSON [{permission, grant?}]).
23
23
  -- NULL on rows written before the field existed — honestly unrecorded, not empty.
24
24
  authorization TEXT,
25
+ -- K-42: the staff actor + session an impersonated operation ran under (JSON
26
+ -- session/by). NULL is the ordinary case — nobody was impersonating — and the
27
+ -- actor column above stays the principal the permission model answered about.
28
+ impersonation TEXT,
25
29
  drained_at TEXT
26
30
  );
27
31
  -- platform-intents.md: durable intents a vertical enqueues (ctx.requestPlatform) for the platform
@@ -32,6 +36,8 @@ const KERNEL_DDL = `
32
36
  kind TEXT NOT NULL,
33
37
  payload TEXT NOT NULL,
34
38
  requested_by TEXT NOT NULL,
39
+ -- K-42: the staff actor + session, when the intent was raised under one.
40
+ impersonation TEXT,
35
41
  status TEXT NOT NULL DEFAULT 'pending',
36
42
  attempts INTEGER NOT NULL DEFAULT 0,
37
43
  last_error TEXT,
@@ -57,9 +63,12 @@ const KERNEL_DDL = `
57
63
  tenant_id TEXT NOT NULL,
58
64
  scope_id TEXT,
59
65
  operation TEXT,
66
+ -- K-42: WHICH of the two actors was refused is exactly what this log is for.
67
+ impersonation TEXT,
60
68
  at TEXT NOT NULL,
61
69
  drained_at TEXT
62
70
  );
71
+ ${IDEMPOTENCY_DDL}
63
72
  CREATE TABLE IF NOT EXISTS _substrat_migrations (
64
73
  module_id TEXT NOT NULL,
65
74
  version TEXT NOT NULL,
@@ -194,6 +203,7 @@ function rowToPlatformRequest(r) {
194
203
  kind: r.kind,
195
204
  payload: JSON.parse(r.payload),
196
205
  requestedBy: JSON.parse(r.requested_by),
206
+ impersonation: r.impersonation == null ? null : JSON.parse(r.impersonation),
197
207
  status: r.status,
198
208
  attempts: r.attempts,
199
209
  lastError: r.last_error,
@@ -281,6 +291,13 @@ export class SqliteScopeHost {
281
291
  * a bare `defineOperation` carries no declaration to read one off.
282
292
  */
283
293
  operationConcurrency = new Map();
294
+ /**
295
+ * #116: the operations that declared `idempotency: false` — a set of refusals,
296
+ * because that is what the declaration is. Every name NOT in here honours an
297
+ * `Idempotency-Key`, which is the default and the reason it is a set of
298
+ * exceptions rather than a map of participants.
299
+ */
300
+ operationIdempotencyOptOut = new Set();
284
301
  roles = new Map(); // 'tenantId/roleKey'
285
302
  /** Executor id → {eventType, handler} (K-22 §4.2). Host code, not module code. */
286
303
  executors = new Map();
@@ -738,6 +755,7 @@ export class SqliteScopeHost {
738
755
  shredded_at TEXT,
739
756
  PRIMARY KEY (scope_id, subject_id)
740
757
  );
758
+ ${IMPERSONATION_DDL}
741
759
  CREATE TABLE IF NOT EXISTS _substrat_admin_log (
742
760
  id TEXT PRIMARY KEY,
743
761
  actor TEXT NOT NULL,
@@ -1051,6 +1069,16 @@ export class SqliteScopeHost {
1051
1069
  throw new Error(`${manifest.id} declares operationConcurrency for unbound operation(s): ` +
1052
1070
  `${unguarded.sort().join(', ')} — a precondition on nothing reads as a guard that is not there`);
1053
1071
  }
1072
+ // #116: an opt-out on an unbound name is the same defect one paragraph up,
1073
+ // and reads worse — it looks like a deliberate exclusion of something that is
1074
+ // not there, so nobody goes looking for what actually honours the header.
1075
+ const declaredOptOuts = registration.operationIdempotencyOptOuts ?? [];
1076
+ const unboundOptOuts = declaredOptOuts.filter((name) => !ownOperations.has(name));
1077
+ if (unboundOptOuts.length > 0) {
1078
+ throw new Error(`${manifest.id} declares idempotency: false for unbound operation(s): ` +
1079
+ `${[...unboundOptOuts].sort().join(', ')} — an opt-out on nothing reads as an ` +
1080
+ 'exclusion someone decided, of an operation that does not exist');
1081
+ }
1054
1082
  for (const [name, handler] of Object.entries(registration.operations ?? {})) {
1055
1083
  this.defineOperation(name, handler);
1056
1084
  // Record which SKU flag gates this operation (§4.3). Bare defineOperation
@@ -1064,6 +1092,9 @@ export class SqliteScopeHost {
1064
1092
  const guarded = declaredConcurrency[name];
1065
1093
  if (guarded && this.operations.has(name))
1066
1094
  this.operationConcurrency.set(name, guarded);
1095
+ if (declaredOptOuts.includes(name) && this.operations.has(name)) {
1096
+ this.operationIdempotencyOptOut.add(name);
1097
+ }
1067
1098
  }
1068
1099
  }
1069
1100
  defineOperation(name, handler) {
@@ -1800,6 +1831,40 @@ export class SqliteScopeHost {
1800
1831
  const store = this.attachmentStore(conn.tenant_id, scope.vertical);
1801
1832
  return this.buildAttachments(rt, { kind: 'connection', id: connectionId }, store);
1802
1833
  }
1834
+ /** Read one session, or say so. The door and every invoke through it come here. */
1835
+ readImpersonation(session) {
1836
+ const q = impersonationByIdQuery(session);
1837
+ const row = this.directory.prepare(q.sql).get(...q.params);
1838
+ if (!row)
1839
+ throw new ImpersonationRefused(`unknown impersonation session: ${session}`);
1840
+ return mapImpersonationRow(row);
1841
+ }
1842
+ /**
1843
+ * The impersonation door (K-42) — mirror of `getConnectorScope`/`getSystemScope`.
1844
+ *
1845
+ * The session is the authority and it is read from the directory, never from
1846
+ * the caller: a stub cannot be minted by describing a session, only by naming
1847
+ * one somebody opened and the admin log recorded. Everything after that is an
1848
+ * ordinary invoke against the impersonated principal.
1849
+ */
1850
+ async getImpersonatedScope(session, tenantId, scopeId, options) {
1851
+ const record = this.readImpersonation(session);
1852
+ assertSessionUsable(record, this.clock(), { tenantId, scopeId });
1853
+ // Same lifecycle gates as the principal door, and reached the same way: a
1854
+ // suspended tenant refuses a support session exactly as it refuses a user.
1855
+ const scope = this.directory
1856
+ .prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?')
1857
+ .get(scopeId);
1858
+ if (!scope || scope.tenant_id !== tenantId) {
1859
+ throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
1860
+ }
1861
+ if (scope.status !== 'active') {
1862
+ throw new Error(`scope not active (status: ${scope.status}): ${scopeId}`);
1863
+ }
1864
+ const rt = this.runtime(tenantId, scopeId);
1865
+ await this.applyPendingMigrations(rt);
1866
+ return this.buildStub(tenantId, scopeId, rt, asPrincipal(record.principal), options, record.id);
1867
+ }
1803
1868
  async getSystemScope(moduleId, tenantId, scopeId) {
1804
1869
  // The scheduler's door (#383) — mirror of getConnectorScope. The module must be
1805
1870
  // registered on this host (a schedule can only invoke its own vertical's ops),
@@ -1901,16 +1966,39 @@ export class SqliteScopeHost {
1901
1966
  }
1902
1967
  return report;
1903
1968
  }
1904
- /** The stub body, shared by the principal and connection doors. */
1905
- buildStub(tenantId, scopeId, rt, subject, options) {
1969
+ /** The stub body, shared by the principal, connection, system and impersonation doors. */
1970
+ buildStub(tenantId, scopeId, rt, subject, options,
1971
+ /**
1972
+ * K-42: the session this stub acts under. `subject` is already the
1973
+ * IMPERSONATED principal — that is what makes the permission model answer
1974
+ * about them with no override branch anywhere — and this is what every
1975
+ * record additionally keeps, plus the bound a read-only session is held to.
1976
+ */
1977
+ sessionId) {
1906
1978
  const operations = this.operations;
1907
1979
  return {
1908
1980
  tenantId,
1909
1981
  scopeId,
1910
1982
  invoke: async (operation, input, invokeOptions) => {
1911
1983
  const handler = operations.get(operation);
1984
+ // `not_found`, not a bare throw (#113) — the same code `adapter-cloudflare`
1985
+ // gives it, so a demo and a hosted vertical answer 404 for the same reason.
1912
1986
  if (!handler)
1913
- return Promise.reject(new Error(`unknown operation: ${operation}`));
1987
+ return Promise.reject(substratError('not_found', `unknown operation: ${operation}`));
1988
+ // K-42: re-read the session on EVERY invoke, not once when the stub was
1989
+ // minted. A stub is a capability and nothing takes it away, so a session
1990
+ // checked only at the door would be a time box the one caller holding it
1991
+ // never runs out of — and an `endImpersonation` would stop nothing.
1992
+ let session;
1993
+ if (sessionId !== undefined) {
1994
+ try {
1995
+ session = this.readImpersonation(sessionId);
1996
+ assertSessionUsable(session, this.clock(), { tenantId, scopeId });
1997
+ }
1998
+ catch (err) {
1999
+ return Promise.reject(err);
2000
+ }
2001
+ }
1914
2002
  // #129. A precondition the operation never declared is REFUSED, not
1915
2003
  // ignored. A caller sending `If-Match` believes its write is serialised,
1916
2004
  // and quietly dropping the header leaves that belief intact while the
@@ -1921,13 +2009,27 @@ export class SqliteScopeHost {
1921
2009
  return Promise.reject(new Error(`${operation} was called with If-Match but declares no \`concurrency\` — ` +
1922
2010
  'nothing would have been compared. Declare it, or drop the header'));
1923
2011
  }
2012
+ // #116. An operation that declared `idempotency: false` REFUSES a key
2013
+ // rather than ignoring one, and the reason is the reason every branch of
2014
+ // this feature is written the way it is: a caller who sent a key and got a
2015
+ // 200 believes the retry is safe, and here it is not — the response was
2016
+ // never recorded, so the second request would execute the operation again.
2017
+ const idempotencyKey = invokeOptions?.idempotencyKey;
2018
+ if (idempotencyKey !== undefined) {
2019
+ if (this.operationIdempotencyOptOut.has(operation)) {
2020
+ return Promise.reject(new Error(idempotencyOptedOutMessage(operation)));
2021
+ }
2022
+ // Refused BEFORE the queue: a key we cannot store must not take a turn
2023
+ // ahead of work that can.
2024
+ assertIdempotencyKey(idempotencyKey);
2025
+ }
1924
2026
  // Entitlement gate (control-plane.md §4.3): a module loads for a tenant
1925
2027
  // only if the tenant holds its SKU flag. Checked per invoke — the simple,
1926
2028
  // uncached path (K-OQ5); a DO-cached variant is a later benchmark call.
1927
2029
  // Fails closed the same way withdrawal does: the operation is unavailable.
1928
2030
  const requiredKey = this.operationEntitlement.get(operation);
1929
2031
  if (requiredKey && !this.tenantHoldsEntitlement(tenantId, requiredKey)) {
1930
- return Promise.reject(new Error(entitlementDenial(operation, requiredKey, this.heldEntitlements(tenantId))));
2032
+ return Promise.reject(substratError('not_found', entitlementDenial(operation, requiredKey, this.heldEntitlements(tenantId))));
1931
2033
  }
1932
2034
  // #458: per-invoke tally of `ctx.requestPlatform` calls. Reported to the
1933
2035
  // harness only after the enqueue task resolves — i.e. after COMMIT — so a
@@ -1936,10 +2038,13 @@ export class SqliteScopeHost {
1936
2038
  // #129: read inside the transaction, reported after it commits — a tag
1937
2039
  // handed out for writes that were rolled back is a tag nobody may hold.
1938
2040
  let committedVersion;
2041
+ // #116: reported after the task, like the version — a replay is a fact
2042
+ // about the response, and the response is not one until it is returned.
2043
+ let replayed = false;
1939
2044
  const invoked = await rt.actor.enqueue(async () => {
1940
2045
  // Fresh per operation: the context carries the K-34 authorization accumulator,
1941
2046
  // which must not leak across operations (invokes are serialized per scope).
1942
- const ctx = this.operationContext(rt, subject, undefined, signals);
2047
+ const ctx = this.operationContext(rt, subject, undefined, signals, session);
1943
2048
  const clonedInput = structuredClone(input);
1944
2049
  // #893: parse, don't trust — at the scope door, from the operation's own
1945
2050
  // declaration. BEFORE `BEGIN`, so a malformed call never opens a
@@ -1953,9 +2058,35 @@ export class SqliteScopeHost {
1953
2058
  const parsed = this.operationInput.has(operation)
1954
2059
  ? this.operationInput.get(operation).parse(clonedInput)
1955
2060
  : clonedInput;
2061
+ // #116: fingerprinted from the PARSED input, so a retry that omits an
2062
+ // optional field the original sent at its default value is still the
2063
+ // same request. Computed before `BEGIN` — it is a pure hash of what the
2064
+ // caller sent and has no business inside a transaction.
2065
+ const fingerprint = idempotencyKey === undefined
2066
+ ? undefined
2067
+ : await requestFingerprint(operation, parsed);
1956
2068
  rt.db.exec('BEGIN IMMEDIATE');
1957
2069
  let result;
1958
2070
  try {
2071
+ // #116: a retry is answered from the recording, and nothing else runs
2072
+ // — not the guards, not the handler, not the permission check inside
2073
+ // it. The row is keyed by SUBJECT, so a caller can only ever reach a
2074
+ // response it received itself; `idempotency.ts` states what that does
2075
+ // and does not promise about authorization.
2076
+ if (idempotencyKey !== undefined && fingerprint !== undefined) {
2077
+ const lookup = idempotencyLookupQuery(subject, idempotencyKey);
2078
+ const prior = rt.db.prepare(lookup.sql).all(...lookup.params)[0];
2079
+ if (prior) {
2080
+ const replay = replayFor(idempotencyKey, fingerprint, prior);
2081
+ rt.db.exec('COMMIT');
2082
+ // Only a guarded operation may report a tag (#129); an unguarded
2083
+ // one recorded null and must stay silent rather than announce it.
2084
+ if (guarded)
2085
+ committedVersion = replay.entityVersion;
2086
+ replayed = true;
2087
+ return replay.result;
2088
+ }
2089
+ }
1959
2090
  // #129, and the SPLIT here is the whole subtlety: the version is
1960
2091
  // snapshotted before the handler, and compared after it.
1961
2092
  //
@@ -1992,28 +2123,56 @@ export class SqliteScopeHost {
1992
2123
  // it, so it is read after the handler and still under `BEGIN`.
1993
2124
  if (ref)
1994
2125
  committedVersion = this.versionAt(rt, ref);
1995
- rt.db.exec('COMMIT');
2126
+ // #116: recorded INSIDE the transaction, which is what makes a failed
2127
+ // request retried rather than replayed — the operation threw, this row
2128
+ // rolls back with the writes it describes, and there is nothing for the
2129
+ // retry to find. The prune rides along: bounded work, on the only path
2130
+ // that adds a row.
2131
+ if (idempotencyKey !== undefined && fingerprint !== undefined) {
2132
+ const at = this.clock();
2133
+ const record = idempotencyRecordStatement(subject, idempotencyKey, operation, fingerprint, result, committedVersion ?? null, at);
2134
+ rt.db.prepare(record.sql).run(...record.params);
2135
+ const prune = idempotencyPruneStatement(at);
2136
+ rt.db.prepare(prune.sql).run(...prune.params);
2137
+ }
2138
+ // K-42: a read-only session's transaction is ROLLED BACK, never
2139
+ // committed — the half of read-only that is a mechanism rather than a
2140
+ // promise. `ctx.emit` and the other effecting verbs refuse outright,
2141
+ // which is what a support engineer sees; this is what holds when a
2142
+ // handler writes a row with plain `ctx.sql.exec` and calls none of
2143
+ // them. Same shape as the scope console's read-only query on the DO.
2144
+ if (session?.mode === 'read-only')
2145
+ rt.db.exec('ROLLBACK');
2146
+ else
2147
+ rt.db.exec('COMMIT');
1996
2148
  }
1997
2149
  catch (err) {
1998
2150
  rt.db.exec('ROLLBACK');
1999
2151
  // K-35: a refused check rolled the operation back. Record it now — a fresh
2000
2152
  // statement in autocommit, AFTER the rollback, so the denial survives it.
2001
2153
  if (err instanceof PermissionDenied)
2002
- this.recordDenial(rt, subject, operation, err);
2154
+ this.recordDenial(rt, subject, operation, err, session);
2003
2155
  throw err;
2004
2156
  }
2005
2157
  // Post-commit, still inside the actor task: drain outbox → consumers,
2006
2158
  // then → executors. Prompt dispatch (K-22 §4.2): the common case
2007
2159
  // completes inside this request, with the outbox as the retry backstop
2008
2160
  // if it does not.
2009
- await this.dispatch(rt);
2010
- await this.dispatchExecutors(rt);
2161
+ // A read-only session committed nothing, so there is nothing of its to
2162
+ // drain — and draining anyway would run consumers as a side effect of a
2163
+ // session that may not have side effects.
2164
+ if (session?.mode !== 'read-only') {
2165
+ await this.dispatch(rt);
2166
+ await this.dispatchExecutors(rt);
2167
+ }
2011
2168
  return structuredClone(result);
2012
2169
  });
2013
2170
  if (signals.platformRequests > 0)
2014
2171
  options?.onPlatformRequests?.(signals.platformRequests);
2015
2172
  if (committedVersion !== undefined)
2016
2173
  invokeOptions?.onEntityVersion?.(committedVersion);
2174
+ if (replayed)
2175
+ invokeOptions?.onIdempotentReplay?.();
2017
2176
  return invoked;
2018
2177
  },
2019
2178
  };
@@ -2391,7 +2550,9 @@ export class SqliteScopeHost {
2391
2550
  * catch AFTER `ROLLBACK`, so this INSERT runs in autocommit and survives — the whole
2392
2551
  * point, since the denial is exactly the write the rolled-back operation could not make.
2393
2552
  */
2394
- recordDenial(rt, subject, operation, err) {
2553
+ recordDenial(rt, subject, operation, err,
2554
+ /** K-42: the session the refused call ran under, when it ran under one. */
2555
+ impersonation) {
2395
2556
  // Only an ENFORCED denial (assertAllowed, which attaches the checked permission +
2396
2557
  // node) is recorded. A module's own hand-thrown `new PermissionDenied('…')` is its
2397
2558
  // policy, carries no permission key, and is left to the module.
@@ -2404,9 +2565,9 @@ export class SqliteScopeHost {
2404
2565
  : subject.id;
2405
2566
  rt.db
2406
2567
  .prepare(`INSERT INTO _substrat_denials
2407
- (id, actor, permission, tenant_id, scope_id, operation, at)
2408
- VALUES (?, ?, ?, ?, ?, ?, ?)`)
2409
- .run(ulid(), JSON.stringify(actor), err.permission, err.node.tenantId, err.node.scopeId ?? null, operation, new Date().toISOString());
2568
+ (id, actor, permission, tenant_id, scope_id, operation, impersonation, at)
2569
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
2570
+ .run(ulid(), JSON.stringify(actor), err.permission, err.node.tenantId, err.node.scopeId ?? null, operation, impersonation ? JSON.stringify(impersonationStampOf(impersonation)) : null, new Date().toISOString());
2410
2571
  }
2411
2572
  parseOutboxRow(row) {
2412
2573
  return domainEvent.parse({
@@ -2421,6 +2582,11 @@ export class SqliteScopeHost {
2421
2582
  piiClass: row.pii_class,
2422
2583
  ...(row.subject_id ? { subjectId: row.subject_id } : {}),
2423
2584
  ...(row.authorization ? { authorization: JSON.parse(row.authorization) } : {}),
2585
+ // K-42: the stamp survives the read, so a consumer's event and an executor's
2586
+ // are the same fact the stored row is. Absent rather than null when nobody
2587
+ // was impersonating, because `DomainEvent.impersonation` is optional — the
2588
+ // shape module code never sees is also the shape it cannot branch on.
2589
+ ...(row.impersonation ? { impersonation: JSON.parse(row.impersonation) } : {}),
2424
2590
  payload: row.payload === null ? undefined : JSON.parse(row.payload),
2425
2591
  });
2426
2592
  }
@@ -4162,6 +4328,48 @@ export class SqliteScopeHost {
4162
4328
  this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, redacted.changes);
4163
4329
  return receipt;
4164
4330
  },
4331
+ // -- impersonation (K-42, #868) ----------------------------------------
4332
+ beginImpersonation: async (actor, input) => {
4333
+ // Fail closed on the scope before minting anything, exactly as the
4334
+ // principal door does: a session naming a scope that is not there is a
4335
+ // credential for nothing, and issuing one anyway means the admin log
4336
+ // records an access that could never have happened.
4337
+ this.assertScope(input.tenantId, input.scopeId);
4338
+ const session = newImpersonationSession(ulid(), actor, input, this.clock());
4339
+ this.directory
4340
+ .prepare(`INSERT INTO _substrat_impersonations
4341
+ (${IMPERSONATION_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
4342
+ .run(...impersonationRowValues(session));
4343
+ // BEFORE the session is returned, so the log entry precedes every row it
4344
+ // will ever touch. The reason rides in `after` — it is the field a review
4345
+ // reads first, and a log saying a session opened but not why is half a
4346
+ // record.
4347
+ this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session);
4348
+ return session;
4349
+ },
4350
+ endImpersonation: async (actor, id) => {
4351
+ const before = this.readImpersonation(id);
4352
+ // Idempotent: "stop that session" must not fail because somebody else
4353
+ // already stopped it, and it must not move the moment it stopped.
4354
+ if (before.endedAt !== null)
4355
+ return before;
4356
+ this.directory
4357
+ .prepare('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ?')
4358
+ .run(this.clock(), id);
4359
+ const after = this.readImpersonation(id);
4360
+ this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after);
4361
+ return after;
4362
+ },
4363
+ listImpersonations: async (actor, filter) => {
4364
+ const q = impersonationListQuery(filter, this.clock());
4365
+ const rows = this.directory.prepare(q.sql).all(...q.params);
4366
+ const sessions = rows.map(mapImpersonationRow);
4367
+ // A staff READ, logged like every other one (K-24) — including the
4368
+ // `result_count`, which is what separates "checked one session" from
4369
+ // "enumerated every support session on the platform".
4370
+ this.recordAccess(actor, 'listImpersonations', { tenantId: (filter?.tenantId ?? null) }, filter ?? null, sessions.length);
4371
+ return sessions;
4372
+ },
4165
4373
  reapTenant: async (actor, tenantId) => {
4166
4374
  // The terminal tenant reap (§4.8), the tenant analogue of reapScope. The
4167
4375
  // caller (the reap route / grace-window sweep) has already reaped every scope's
@@ -5033,7 +5241,14 @@ export class SqliteScopeHost {
5033
5241
  */
5034
5242
  operationContext(rt, subject, overrideActor,
5035
5243
  /** #458: per-invoke tally of `ctx.requestPlatform` calls; absent for consumer dispatch. */
5036
- signals) {
5244
+ signals,
5245
+ /**
5246
+ * K-42: the session this operation is running under, if any. Read here and
5247
+ * nowhere module code can reach — a vertical that could see it could branch
5248
+ * on it, and "hide this row when support is looking" is the one behaviour
5249
+ * this whole feature exists to make impossible.
5250
+ */
5251
+ impersonation) {
5037
5252
  const principal = subject.id;
5038
5253
  const checker = this.checker;
5039
5254
  const relations = this.relations;
@@ -5044,6 +5259,10 @@ export class SqliteScopeHost {
5044
5259
  // whatever has passed up to that point. A system/override actor is unconditionally
5045
5260
  // allowed, so its checks are not authorizations and are not recorded.
5046
5261
  const passed = [];
5262
+ // K-42: the two-actor stamp, computed once per operation. Every record this
5263
+ // operation writes about who did what carries it; nothing module code passes
5264
+ // can add it, change it or drop it.
5265
+ const stamp = impersonation ? impersonationStampOf(impersonation) : undefined;
5047
5266
  /**
5048
5267
  * The operation's instant (#812), read ONCE when the context is built.
5049
5268
  *
@@ -5120,6 +5339,7 @@ export class SqliteScopeHost {
5120
5339
  sql: scopedSql(rt.db),
5121
5340
  now: () => at,
5122
5341
  emit: (event) => {
5342
+ assertImpersonationWrites(impersonation, 'ctx.emit');
5123
5343
  const input = domainEventInput.parse(event);
5124
5344
  const full = domainEvent.parse({
5125
5345
  ...input,
@@ -5134,15 +5354,18 @@ export class SqliteScopeHost {
5134
5354
  ? { connection: subject.id }
5135
5355
  : principal),
5136
5356
  ...(passed.length ? { authorization: passed.map((p) => ({ ...p })) } : {}),
5357
+ ...(stamp ? { impersonation: stamp } : {}),
5137
5358
  });
5138
5359
  rt.db
5139
5360
  .prepare(`INSERT INTO _substrat_outbox
5140
5361
  (id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
5141
- entity_type, entity_id, pii_class, subject_id, authorization, payload)
5142
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
5143
- .run(full.id, full.type, full.schemaVersion, full.occurredAt, full.tenantId, full.scopeId, JSON.stringify(full.actor), full.entity.entityType, full.entity.entityId, full.piiClass, full.subjectId ?? null, full.authorization ? JSON.stringify(full.authorization) : null, full.payload === undefined ? null : JSON.stringify(full.payload));
5362
+ entity_type, entity_id, pii_class, subject_id, authorization,
5363
+ impersonation, payload)
5364
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
5365
+ .run(full.id, full.type, full.schemaVersion, full.occurredAt, full.tenantId, full.scopeId, JSON.stringify(full.actor), full.entity.entityType, full.entity.entityId, full.piiClass, full.subjectId ?? null, full.authorization ? JSON.stringify(full.authorization) : null, full.impersonation ? JSON.stringify(full.impersonation) : null, full.payload === undefined ? null : JSON.stringify(full.payload));
5144
5366
  },
5145
5367
  requestPlatform: (request) => {
5368
+ assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
5146
5369
  const input = platformRequestInput.parse(request);
5147
5370
  // Backpressure (platform-intents.md): refuse when the scope already holds too many pending
5148
5371
  // intents, so a stuck or runaway vertical cannot flood the platform drain.
@@ -5161,9 +5384,9 @@ export class SqliteScopeHost {
5161
5384
  : principal);
5162
5385
  rt.db
5163
5386
  .prepare(`INSERT INTO _substrat_platform_requests
5164
- (id, kind, payload, requested_by, status, attempts, requested_at)
5165
- VALUES (?, ?, ?, ?, 'pending', 0, ?)`)
5166
- .run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), at);
5387
+ (id, kind, payload, requested_by, impersonation, status, attempts, requested_at)
5388
+ VALUES (?, ?, ?, ?, ?, 'pending', 0, ?)`)
5389
+ .run(id, input.kind, JSON.stringify(input.payload ?? null), JSON.stringify(requestedBy), stamp ? JSON.stringify(stamp) : null, at);
5167
5390
  if (signals)
5168
5391
  signals.platformRequests += 1;
5169
5392
  return id;
@@ -5247,6 +5470,7 @@ export class SqliteScopeHost {
5247
5470
  * guardrail 2 the same way its `check` does.
5248
5471
  */
5249
5472
  grant: async (principal, permission, entity) => {
5473
+ assertImpersonationWrites(impersonation, 'ctx.grant');
5250
5474
  const held = await runCheck(permission, entity);
5251
5475
  if (!held.allowed) {
5252
5476
  throw new PermissionDenied(`cannot grant '${permission}' on ${entity.entityType}:${entity.entityId} — ` +
@@ -5259,6 +5483,7 @@ export class SqliteScopeHost {
5259
5483
  },
5260
5484
  /** Withdraw a grant this caller could have made. Same guardrails, same reason. */
5261
5485
  revoke: async (principal, permission, entity) => {
5486
+ assertImpersonationWrites(impersonation, 'ctx.revoke');
5262
5487
  const held = await runCheck(permission, entity);
5263
5488
  if (!held.allowed) {
5264
5489
  throw new PermissionDenied(`cannot revoke '${permission}' on ${entity.entityType}:${entity.entityId} — ` +
@@ -5270,6 +5495,7 @@ export class SqliteScopeHost {
5270
5495
  },
5271
5496
  atomic: createAtomic(runSub, { passed, signals }),
5272
5497
  link: (child, parent) => {
5498
+ assertImpersonationWrites(impersonation, 'ctx.link');
5273
5499
  const allowed = relations.get(child.entityType);
5274
5500
  if (!allowed?.has(parent.entityType)) {
5275
5501
  throw new Error(`undeclared entity relation: ${child.entityType} → ${parent.entityType} ` +
@@ -5446,6 +5672,13 @@ export class SqliteScopeHost {
5446
5672
  // already settled reads as "nobody classified this" rather than claiming an origin
5447
5673
  // the drain never actually decided.
5448
5674
  this.ensureColumn(db, '_substrat_platform_requests', 'last_failure', 'last_failure TEXT');
5675
+ // K-42: the two-actor stamp on the three spine tables that record who did what,
5676
+ // for a scope DB created before impersonation existed. Nullable everywhere, and
5677
+ // the null means "nobody was impersonating" rather than "unrecorded" — every one
5678
+ // of those rows was written when there was no other possibility.
5679
+ this.ensureColumn(db, '_substrat_outbox', 'impersonation', 'impersonation TEXT');
5680
+ this.ensureColumn(db, '_substrat_platform_requests', 'impersonation', 'impersonation TEXT');
5681
+ this.ensureColumn(db, '_substrat_denials', 'impersonation', 'impersonation TEXT');
5449
5682
  const appliedMigrations = new Set(db.prepare('SELECT module_id, version FROM _substrat_migrations').all().map((r) => `${r.module_id}@${r.version}`));
5450
5683
  const created = { tenantId, scopeId, db, actor: new ScopeActor(), appliedMigrations };
5451
5684
  this.scopes.set(key, created);