@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 +33 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +250 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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
|
/**
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAQL,KAAK,eAAe,
|
|
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(
|
|
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(
|
|
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,
|
|
1895
|
-
result = await handler(ctx,
|
|
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
|