@substrat-run/adapter-sqlite 0.87.0 → 0.89.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
@@ -58,6 +58,26 @@ export declare class SqliteScopeHost implements ScopeHost {
58
58
  private readonly attachmentTargets;
59
59
  /** operation name → its owning module's entitlementKey (§4.3 gate). */
60
60
  private readonly operationEntitlement;
61
+ /**
62
+ * #893: name → the declared input schema, parsed before guards and handler.
63
+ * Populated only by `registerModule` — a bare `defineOperation` (tests, glue)
64
+ * carries no declaration and stays unparsed, exactly as it carries no manifest
65
+ * and stays ungated.
66
+ */
67
+ private readonly operationInput;
68
+ /**
69
+ * #129: name → the entity whose version an `If-Match` is compared against.
70
+ * Populated only by `registerModule`, for the same reason `operationInput` is:
71
+ * a bare `defineOperation` carries no declaration to read one off.
72
+ */
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;
61
81
  private readonly roles;
62
82
  /** Executor id → {eventType, handler} (K-22 §4.2). Host code, not module code. */
63
83
  private readonly executors;
@@ -156,6 +176,19 @@ export declare class SqliteScopeHost implements ScopeHost {
156
176
  runDueSchedules(moduleId: ModuleId, tenantId: TenantId, scopeId: ScopeId): Promise<ScheduleRunReport>;
157
177
  /** The stub body, shared by the principal and connection doors. */
158
178
  private buildStub;
179
+ /**
180
+ * The entity a guarded operation's precondition is about (#129).
181
+ *
182
+ * `idFrom` is compile-checked to name an input field, and the host has already
183
+ * parsed that input — so the field exists and has its declared type. What is
184
+ * still possible is a field the schema lets the caller OMIT, and a precondition
185
+ * with no row to read is not a weaker check but an absent one. Refused rather
186
+ * than skipped: the caller sent `If-Match` and must not receive a 200 that
187
+ * compared nothing.
188
+ */
189
+ private concurrencyRef;
190
+ /** This scope's spine, read under whatever transaction the caller already opened. */
191
+ private versionAt;
159
192
  private runGuards;
160
193
  close(): Promise<void>;
161
194
  /**
@@ -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, 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';
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, 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 = `
@@ -60,6 +60,7 @@ const KERNEL_DDL = `
60
60
  at TEXT NOT NULL,
61
61
  drained_at TEXT
62
62
  );
63
+ ${IDEMPOTENCY_DDL}
63
64
  CREATE TABLE IF NOT EXISTS _substrat_migrations (
64
65
  module_id TEXT NOT NULL,
65
66
  version TEXT NOT NULL,
@@ -112,6 +113,10 @@ const KERNEL_DDL = `
112
113
  );
113
114
  CREATE INDEX IF NOT EXISTS _substrat_attachments_entity
114
115
  ON _substrat_attachments (entity_type, entity_id);
116
+ -- #901: an entity's version is the ULID of the last event about it, so
117
+ -- MAX(id) per (entity_type, entity_id) is the read. The id column sits last so
118
+ -- SQLite walks to the end of the matched range instead of aggregating over it.
119
+ ${OUTBOX_ENTITY_INDEX}
115
120
  `;
116
121
  /** Row → contract shape. Never reads the secrets table — that is the point of the split. */
117
122
  const toConnection = (r) => connection.parse({
@@ -264,6 +269,26 @@ export class SqliteScopeHost {
264
269
  attachmentTargets = new Map();
265
270
  /** operation name → its owning module's entitlementKey (§4.3 gate). */
266
271
  operationEntitlement = new Map();
272
+ /**
273
+ * #893: name → the declared input schema, parsed before guards and handler.
274
+ * Populated only by `registerModule` — a bare `defineOperation` (tests, glue)
275
+ * carries no declaration and stays unparsed, exactly as it carries no manifest
276
+ * and stays ungated.
277
+ */
278
+ operationInput = new Map();
279
+ /**
280
+ * #129: name → the entity whose version an `If-Match` is compared against.
281
+ * Populated only by `registerModule`, for the same reason `operationInput` is:
282
+ * a bare `defineOperation` carries no declaration to read one off.
283
+ */
284
+ operationConcurrency = new Map();
285
+ /**
286
+ * #116: the operations that declared `idempotency: false` — a set of refusals,
287
+ * because that is what the declaration is. Every name NOT in here honours an
288
+ * `Idempotency-Key`, which is the default and the reason it is a set of
289
+ * exceptions rather than a map of participants.
290
+ */
291
+ operationIdempotencyOptOut = new Set();
267
292
  roles = new Map(); // 'tenantId/roleKey'
268
293
  /** Executor id → {eventType, handler} (K-22 §4.2). Host code, not module code. */
269
294
  executors = new Map();
@@ -1016,11 +1041,50 @@ export class SqliteScopeHost {
1016
1041
  this.withdrawn.set(name, manifest.id);
1017
1042
  this.operations.delete(name);
1018
1043
  }
1044
+ // A declared input schema for an operation this module does not bind is a
1045
+ // schema that enforces nothing while reading as coverage — the same reason
1046
+ // `checksDeclaredElsewhere` refuses a stale exemption.
1047
+ const declaredInputs = registration.operationInputs ?? {};
1048
+ const unbound = Object.keys(declaredInputs).filter((name) => !ownOperations.has(name));
1049
+ if (unbound.length > 0) {
1050
+ throw new Error(`${manifest.id} declares operationInputs for unbound operation(s): ` +
1051
+ `${unbound.sort().join(', ')} — a schema on nothing reads as a parse that is not there`);
1052
+ }
1053
+ // Same rule for a declared precondition, and it matters more: a `concurrency`
1054
+ // on an unbound name is a guarantee nothing enforces, which is exactly the
1055
+ // belief #129 exists to stop anyone holding.
1056
+ const declaredConcurrency = registration.operationConcurrency ?? {};
1057
+ const unguarded = Object.keys(declaredConcurrency).filter((name) => !ownOperations.has(name));
1058
+ if (unguarded.length > 0) {
1059
+ throw new Error(`${manifest.id} declares operationConcurrency for unbound operation(s): ` +
1060
+ `${unguarded.sort().join(', ')} — a precondition on nothing reads as a guard that is not there`);
1061
+ }
1062
+ // #116: an opt-out on an unbound name is the same defect one paragraph up,
1063
+ // and reads worse — it looks like a deliberate exclusion of something that is
1064
+ // not there, so nobody goes looking for what actually honours the header.
1065
+ const declaredOptOuts = registration.operationIdempotencyOptOuts ?? [];
1066
+ const unboundOptOuts = declaredOptOuts.filter((name) => !ownOperations.has(name));
1067
+ if (unboundOptOuts.length > 0) {
1068
+ throw new Error(`${manifest.id} declares idempotency: false for unbound operation(s): ` +
1069
+ `${[...unboundOptOuts].sort().join(', ')} — an opt-out on nothing reads as an ` +
1070
+ 'exclusion someone decided, of an operation that does not exist');
1071
+ }
1019
1072
  for (const [name, handler] of Object.entries(registration.operations ?? {})) {
1020
1073
  this.defineOperation(name, handler);
1021
1074
  // Record which SKU flag gates this operation (§4.3). Bare defineOperation
1022
1075
  // bindings (tests, glue) carry no manifest and stay ungated.
1023
1076
  this.operationEntitlement.set(name, manifest.entitlementKey);
1077
+ // #893: withdrawal removes the binding, so the schema follows the handler
1078
+ // rather than the name — a withdrawn operation has nothing to parse for.
1079
+ const schema = declaredInputs[name];
1080
+ if (schema && this.operations.has(name))
1081
+ this.operationInput.set(name, schema);
1082
+ const guarded = declaredConcurrency[name];
1083
+ if (guarded && this.operations.has(name))
1084
+ this.operationConcurrency.set(name, guarded);
1085
+ if (declaredOptOuts.includes(name) && this.operations.has(name)) {
1086
+ this.operationIdempotencyOptOut.add(name);
1087
+ }
1024
1088
  }
1025
1089
  }
1026
1090
  defineOperation(name, handler) {
@@ -1864,35 +1928,148 @@ export class SqliteScopeHost {
1864
1928
  return {
1865
1929
  tenantId,
1866
1930
  scopeId,
1867
- invoke: async (operation, input) => {
1931
+ invoke: async (operation, input, invokeOptions) => {
1868
1932
  const handler = operations.get(operation);
1933
+ // `not_found`, not a bare throw (#113) — the same code `adapter-cloudflare`
1934
+ // gives it, so a demo and a hosted vertical answer 404 for the same reason.
1869
1935
  if (!handler)
1870
- return Promise.reject(new Error(`unknown operation: ${operation}`));
1936
+ return Promise.reject(substratError('not_found', `unknown operation: ${operation}`));
1937
+ // #129. A precondition the operation never declared is REFUSED, not
1938
+ // ignored. A caller sending `If-Match` believes its write is serialised,
1939
+ // and quietly dropping the header leaves that belief intact while the
1940
+ // protection is absent — the one failure mode this mechanism exists to
1941
+ // prevent, arrived at through the mechanism itself.
1942
+ const guarded = this.operationConcurrency.get(operation);
1943
+ if (invokeOptions?.ifMatch !== undefined && !guarded) {
1944
+ return Promise.reject(new Error(`${operation} was called with If-Match but declares no \`concurrency\` — ` +
1945
+ 'nothing would have been compared. Declare it, or drop the header'));
1946
+ }
1947
+ // #116. An operation that declared `idempotency: false` REFUSES a key
1948
+ // rather than ignoring one, and the reason is the reason every branch of
1949
+ // this feature is written the way it is: a caller who sent a key and got a
1950
+ // 200 believes the retry is safe, and here it is not — the response was
1951
+ // never recorded, so the second request would execute the operation again.
1952
+ const idempotencyKey = invokeOptions?.idempotencyKey;
1953
+ if (idempotencyKey !== undefined) {
1954
+ if (this.operationIdempotencyOptOut.has(operation)) {
1955
+ return Promise.reject(new Error(idempotencyOptedOutMessage(operation)));
1956
+ }
1957
+ // Refused BEFORE the queue: a key we cannot store must not take a turn
1958
+ // ahead of work that can.
1959
+ assertIdempotencyKey(idempotencyKey);
1960
+ }
1871
1961
  // Entitlement gate (control-plane.md §4.3): a module loads for a tenant
1872
1962
  // only if the tenant holds its SKU flag. Checked per invoke — the simple,
1873
1963
  // uncached path (K-OQ5); a DO-cached variant is a later benchmark call.
1874
1964
  // Fails closed the same way withdrawal does: the operation is unavailable.
1875
1965
  const requiredKey = this.operationEntitlement.get(operation);
1876
1966
  if (requiredKey && !this.tenantHoldsEntitlement(tenantId, requiredKey)) {
1877
- return Promise.reject(new Error(entitlementDenial(operation, requiredKey, this.heldEntitlements(tenantId))));
1967
+ return Promise.reject(substratError('not_found', entitlementDenial(operation, requiredKey, this.heldEntitlements(tenantId))));
1878
1968
  }
1879
1969
  // #458: per-invoke tally of `ctx.requestPlatform` calls. Reported to the
1880
1970
  // harness only after the enqueue task resolves — i.e. after COMMIT — so a
1881
1971
  // rolled-back intent never signals.
1882
1972
  const signals = { platformRequests: 0 };
1973
+ // #129: read inside the transaction, reported after it commits — a tag
1974
+ // handed out for writes that were rolled back is a tag nobody may hold.
1975
+ let committedVersion;
1976
+ // #116: reported after the task, like the version — a replay is a fact
1977
+ // about the response, and the response is not one until it is returned.
1978
+ let replayed = false;
1883
1979
  const invoked = await rt.actor.enqueue(async () => {
1884
1980
  // Fresh per operation: the context carries the K-34 authorization accumulator,
1885
1981
  // which must not leak across operations (invokes are serialized per scope).
1886
1982
  const ctx = this.operationContext(rt, subject, undefined, signals);
1887
1983
  const clonedInput = structuredClone(input);
1984
+ // #893: parse, don't trust — at the scope door, from the operation's own
1985
+ // declaration. BEFORE `BEGIN`, so a malformed call never opens a
1986
+ // transaction, and before the guards, so a K-17 pre-condition reads the
1987
+ // same typed input the handler will.
1988
+ //
1989
+ // Every caller passes here: the HTTP mount, a scenario test, a seed, a
1990
+ // schedule. That is the point — parsing at the mount alone would leave
1991
+ // the demos' own suites exercising an unparsed path, and the wire is
1992
+ // where the untrusted input actually arrives.
1993
+ const parsed = this.operationInput.has(operation)
1994
+ ? this.operationInput.get(operation).parse(clonedInput)
1995
+ : clonedInput;
1996
+ // #116: fingerprinted from the PARSED input, so a retry that omits an
1997
+ // optional field the original sent at its default value is still the
1998
+ // same request. Computed before `BEGIN` — it is a pure hash of what the
1999
+ // caller sent and has no business inside a transaction.
2000
+ const fingerprint = idempotencyKey === undefined
2001
+ ? undefined
2002
+ : await requestFingerprint(operation, parsed);
1888
2003
  rt.db.exec('BEGIN IMMEDIATE');
1889
2004
  let result;
1890
2005
  try {
2006
+ // #116: a retry is answered from the recording, and nothing else runs
2007
+ // — not the guards, not the handler, not the permission check inside
2008
+ // it. The row is keyed by SUBJECT, so a caller can only ever reach a
2009
+ // response it received itself; `idempotency.ts` states what that does
2010
+ // and does not promise about authorization.
2011
+ if (idempotencyKey !== undefined && fingerprint !== undefined) {
2012
+ const lookup = idempotencyLookupQuery(subject, idempotencyKey);
2013
+ const prior = rt.db.prepare(lookup.sql).all(...lookup.params)[0];
2014
+ if (prior) {
2015
+ const replay = replayFor(idempotencyKey, fingerprint, prior);
2016
+ rt.db.exec('COMMIT');
2017
+ // Only a guarded operation may report a tag (#129); an unguarded
2018
+ // one recorded null and must stay silent rather than announce it.
2019
+ if (guarded)
2020
+ committedVersion = replay.entityVersion;
2021
+ replayed = true;
2022
+ return replay.result;
2023
+ }
2024
+ }
2025
+ // #129, and the SPLIT here is the whole subtlety: the version is
2026
+ // snapshotted before the handler, and compared after it.
2027
+ //
2028
+ // **Read before**, because the handler's own `emit` moves the version —
2029
+ // comparing afterwards against a value the operation itself just
2030
+ // changed would refuse every write. Inside the transaction, because a
2031
+ // version read outside the write's own transaction is
2032
+ // time-of-check/time-of-use: the value can move between the comparison
2033
+ // and the commit, and the write lands anyway.
2034
+ //
2035
+ // **Compare after**, because the permission check lives INSIDE the
2036
+ // handler (`assertAllowed(await ctx.check(…))`) and must be what
2037
+ // answers first. Refusing on the version before the handler runs makes
2038
+ // this operation a version oracle: a principal with no permission on
2039
+ // the entity sends `If-Match: *` and learns whether it exists, or sends
2040
+ // a tag and learns whether it has changed. Found by driving Callout's
2041
+ // two-dispatcher scenario over HTTP with the wrong principal, which
2042
+ // answered 412 where it owed 403.
2043
+ //
2044
+ // The cost is a refused write having done the handler's work first. It
2045
+ // is discarded with everything else — same transaction, same rollback —
2046
+ // so the only loss is effort on a path that was already failing.
2047
+ const ref = guarded ? this.concurrencyRef(operation, guarded, parsed) : undefined;
2048
+ const seen = ref && invokeOptions?.ifMatch !== undefined ? this.versionAt(rt, ref) : undefined;
1891
2049
  // Manifest guards (K-17): pre-conditions, inside the operation's own
1892
2050
  // transaction, before the handler. A throw here blocks the operation
1893
2051
  // and rolls back exactly like a handler throw — fail closed.
1894
- await this.runGuards(operation, ctx, clonedInput);
1895
- result = await handler(ctx, clonedInput);
2052
+ await this.runGuards(operation, ctx, parsed);
2053
+ result = await handler(ctx, parsed);
2054
+ if (ref && invokeOptions?.ifMatch !== undefined) {
2055
+ assertIfMatch(ref, invokeOptions.ifMatch, seen ?? null);
2056
+ }
2057
+ // The tag the caller gets back describes the row as THIS write left
2058
+ // it, so it is read after the handler and still under `BEGIN`.
2059
+ if (ref)
2060
+ committedVersion = this.versionAt(rt, ref);
2061
+ // #116: recorded INSIDE the transaction, which is what makes a failed
2062
+ // request retried rather than replayed — the operation threw, this row
2063
+ // rolls back with the writes it describes, and there is nothing for the
2064
+ // retry to find. The prune rides along: bounded work, on the only path
2065
+ // that adds a row.
2066
+ if (idempotencyKey !== undefined && fingerprint !== undefined) {
2067
+ const at = this.clock();
2068
+ const record = idempotencyRecordStatement(subject, idempotencyKey, operation, fingerprint, result, committedVersion ?? null, at);
2069
+ rt.db.prepare(record.sql).run(...record.params);
2070
+ const prune = idempotencyPruneStatement(at);
2071
+ rt.db.prepare(prune.sql).run(...prune.params);
2072
+ }
1896
2073
  rt.db.exec('COMMIT');
1897
2074
  }
1898
2075
  catch (err) {
@@ -1913,6 +2090,10 @@ export class SqliteScopeHost {
1913
2090
  });
1914
2091
  if (signals.platformRequests > 0)
1915
2092
  options?.onPlatformRequests?.(signals.platformRequests);
2093
+ if (committedVersion !== undefined)
2094
+ invokeOptions?.onEntityVersion?.(committedVersion);
2095
+ if (replayed)
2096
+ invokeOptions?.onIdempotentReplay?.();
1916
2097
  return invoked;
1917
2098
  },
1918
2099
  };
@@ -1924,6 +2105,30 @@ export class SqliteScopeHost {
1924
2105
  // internals. They are UNCONDITIONAL gates — policy that depends on vertical
1925
2106
  // data stays vertical-composed glue inside the operation handler.
1926
2107
  // -------------------------------------------------------------------------
2108
+ /**
2109
+ * The entity a guarded operation's precondition is about (#129).
2110
+ *
2111
+ * `idFrom` is compile-checked to name an input field, and the host has already
2112
+ * parsed that input — so the field exists and has its declared type. What is
2113
+ * still possible is a field the schema lets the caller OMIT, and a precondition
2114
+ * with no row to read is not a weaker check but an absent one. Refused rather
2115
+ * than skipped: the caller sent `If-Match` and must not receive a 200 that
2116
+ * compared nothing.
2117
+ */
2118
+ concurrencyRef(operation, guarded, parsed) {
2119
+ const id = parsed?.[guarded.idFrom];
2120
+ if (typeof id !== 'string' || id.length === 0) {
2121
+ throw new Error(`${operation} declares concurrency over '${guarded.entity}' keyed by ` +
2122
+ `'${guarded.idFrom}', but the parsed input carries no such id — there is no ` +
2123
+ 'row whose version could be compared');
2124
+ }
2125
+ return { entityType: guarded.entity, entityId: id };
2126
+ }
2127
+ /** This scope's spine, read under whatever transaction the caller already opened. */
2128
+ versionAt(rt, ref) {
2129
+ const q = entityVersionQuery(ref);
2130
+ return entityVersionOf(rt.db.prepare(q.sql).all(...q.params));
2131
+ }
1927
2132
  async runGuards(operation, ctx, input) {
1928
2133
  const declared = this.guards.get(operation);
1929
2134
  if (!declared)
@@ -3800,6 +4005,36 @@ export class SqliteScopeHost {
3800
4005
  this.recordAccess(actor, 'queryScope', { tenantId, scopeId }, { sql }, rows.length);
3801
4006
  return { columns, rows, truncated };
3802
4007
  },
4008
+ // -- the denial log (K-35, #867) -------------------------------------
4009
+ // Same shape as the introspection reads above: `scopeDbFor` is the K-3
4010
+ // cross-check (a pair that does not resolve never opens a database), and every
4011
+ // read records to the K-24 access log. What differs is that these two answer a
4012
+ // question about the permission model rather than about the data.
4013
+ listDenials: async (actor, tenantId, scopeId, filter) => {
4014
+ const db = this.scopeDbFor(tenantId, scopeId);
4015
+ const q = denialListQuery(filter);
4016
+ const rows = db.prepare(q.sql).all(...q.params).map(mapDenialRow);
4017
+ this.recordAccess(actor, 'listDenials', { tenantId, scopeId }, filter ?? null, rows.length);
4018
+ return rows;
4019
+ },
4020
+ summarizeDenials: async (actor, tenantId, scopeId, filter) => {
4021
+ const db = this.scopeDbFor(tenantId, scopeId);
4022
+ const b = denialSummaryQuery(filter);
4023
+ const buckets = db.prepare(b.sql).all(...b.params).map(mapDenialBucketRow);
4024
+ const t = denialTotalsQuery(filter);
4025
+ const totals = db.prepare(t.sql).get(...t.params);
4026
+ // Unfiltered on purpose — these describe the log, not the query (denial-query.ts).
4027
+ const w = db.prepare(DENIAL_WINDOW_QUERY).get();
4028
+ this.recordAccess(actor, 'summarizeDenials', { tenantId, scopeId }, filter ?? null, buckets.length);
4029
+ return {
4030
+ buckets,
4031
+ total: Number(totals.total),
4032
+ actors: Number(totals.actors),
4033
+ windowOldestAt: w.oldest_at ?? null,
4034
+ windowNewestAt: w.newest_at ?? null,
4035
+ drained: Number(w.drained ?? 0),
4036
+ };
4037
+ },
3803
4038
  exportScope: async (actor, tenantId, scopeId) => {
3804
4039
  // K-3: cross-check the pair before opening anything (same as the introspection reads).
3805
4040
  const db = this.scopeDbFor(tenantId, scopeId);
@@ -5019,6 +5254,14 @@ export class SqliteScopeHost {
5019
5254
  const q = platformRequestHistoryQuery(filter);
5020
5255
  return rt.db.prepare(q.sql).all(...q.params).map(rowToPlatformRequest);
5021
5256
  },
5257
+ // #901. This scope's own spine, so no tenancy predicate is needed or
5258
+ // possible — the runtime IS the scope. The version is the last event's
5259
+ // ULID; `null` means nothing has ever been emitted about the entity,
5260
+ // which is distinct from the entity not existing (nothing here knows).
5261
+ versionOf: (entity) => {
5262
+ const q = entityVersionQuery(entity);
5263
+ return entityVersionOf(rt.db.prepare(q.sql).all(...q.params));
5264
+ },
5022
5265
  check: runCheck,
5023
5266
  /**
5024
5267
  * #827. The plan is registration state, the index is scope state: an entity