@substrat-run/adapter-sqlite 0.108.0 → 0.110.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 +30 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +156 -6
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -119,6 +119,21 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
119
119
|
* schema backwards and the next read of a newer column would fail with a bare `no such
|
|
120
120
|
* column`. Every statement is IF NOT EXISTS and `ensureDirectoryColumns` probes before
|
|
121
121
|
* it alters, so this only ever adds back what a copy did not carry.
|
|
122
|
+
*
|
|
123
|
+
* It is also one of two hand-written copies of the directory spine (#969). The other is
|
|
124
|
+
* `DIRECTORY_DDL` in `adapter-cloudflare/src/control-plane-do.ts`, and the two must
|
|
125
|
+
* describe the same schema: this side is dev, CI, self-host and escrow, that side is
|
|
126
|
+
* production. So a new directory table both adapters need, or a new column on a table
|
|
127
|
+
* they both build, is added HERE and THERE — and for a directory created before it, to
|
|
128
|
+
* both adapters' `ensureDirectoryColumns` as well.
|
|
129
|
+
* `pnpm lint:spine-ddl` (`tools/spine-ddl-drift.mjs`) refuses a divergence by executing
|
|
130
|
+
* each side's DDL plus those later ALTERs and comparing the schemas a query would
|
|
131
|
+
* actually meet — columns, indexes and foreign keys. What it does not judge: a table
|
|
132
|
+
* present on one side only, which it reports as a note because the adapters legitimately
|
|
133
|
+
* partition the spine differently, and triggers or CHECK constraints, which it does not
|
|
134
|
+
* compare at all (the spine has none).
|
|
135
|
+
* Keeping the copies and gating them, rather than moving the DDL into the kernel, is the
|
|
136
|
+
* recorded answer to #969; `docs/architecture/kernel-design.md` §8 says why.
|
|
122
137
|
*/
|
|
123
138
|
private applyDirectorySchema;
|
|
124
139
|
registerExecutor(id: string, eventType: string, handler: ExecutorHandler, retry?: ExecutorRetryPolicy): void;
|
|
@@ -434,6 +449,21 @@ export declare class SqliteScopeHost implements ScopeHost {
|
|
|
434
449
|
* DIFFERENT tenant, throws — the introspection reads never open another tenant's
|
|
435
450
|
* DB, and never CREATE one for an id that was never provisioned.
|
|
436
451
|
*/
|
|
452
|
+
/**
|
|
453
|
+
* Whether this (tenant, scope) pair may be reached at all — the DIRECTORY half of
|
|
454
|
+
* `scopeDbFor`, split out so a call that ends up touching no rows still answers the
|
|
455
|
+
* question. K-3 first: a scope under another tenant is indistinguishable from one
|
|
456
|
+
* that does not exist. Then the reap.
|
|
457
|
+
*
|
|
458
|
+
* A reaped scope keeps its directory row as a tombstone while `reapScope` deletes
|
|
459
|
+
* the file (§4.4). `runtime()` opens a database by CREATING it when absent, so
|
|
460
|
+
* reading one would not merely answer emptily — it would put the file back,
|
|
461
|
+
* resurrecting storage an irreversible reap destroyed, and every later read would
|
|
462
|
+
* report a scope with no events rather than a scope that is gone. `archived` is
|
|
463
|
+
* deliberately still reachable: its bytes exist, and that is the whole point of the
|
|
464
|
+
* state.
|
|
465
|
+
*/
|
|
466
|
+
private assertScopeReachable;
|
|
437
467
|
private scopeDbFor;
|
|
438
468
|
/**
|
|
439
469
|
* The additive spine-column migrations, shared by `runtime()` and the dump replay.
|
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,EAcL,KAAK,eAAe,EAuDpB,KAAK,YAAY,EAGjB,KAAK,QAAQ,EAWb,KAAK,WAAW,EAIhB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAI3B,KAAK,wBAAwB,EAW7B,KAAK,eAAe,EACpB,KAAK,WAAW,EAehB,KAAK,SAAS,EAEd,KAAK,OAAO,EAYZ,KAAK,sBAAsB,EAG3B,KAAK,QAAQ,EAGb,KAAK,iBAAiB,EAUvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAcL,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAc1B,KAAK,uBAAuB,EAG5B,KAAK,gBAAgB,EAGrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,mBAAmB,EAoCxB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,KAAK,EACV,KAAK,SAAS,EACd,KAAK,SAAS,EAEd,KAAK,SAAS,EACd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAErB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EAExB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EAGtB,KAAK,SAAS,EACd,KAAK,SAAS,EACd,KAAK,gBAAgB,EAGrB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -2,9 +2,29 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
4
|
import { accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, platformRequest, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, capabilityGrant, 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, assertReplayableDump, } from '@substrat-run/contracts';
|
|
5
|
-
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, guardSpine, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, 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, globalFetch, createUlid, readHistory, } from '@substrat-run/kernel';
|
|
5
|
+
import { asPrincipal, assertAllowed, assertReadOnlyQuery, attachmentBlobKey, entitlementDenial, foldMeterReading, guardSpine, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, 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, globalFetch, createUlid, facetEvents, readHistory, } from '@substrat-run/kernel';
|
|
6
6
|
import { ScopeActor } from './actor.js';
|
|
7
7
|
import { createTupleChecker } from './checker.js';
|
|
8
|
+
/**
|
|
9
|
+
* The scope spine, as this adapter builds it — one of two hand-written copies (#969).
|
|
10
|
+
*
|
|
11
|
+
* The other is `KERNEL_DDL` in `adapter-cloudflare/src/scope-do.ts`, and the two must
|
|
12
|
+
* describe the same schema: this side is dev, CI, self-host and escrow, that side is
|
|
13
|
+
* production. So a new `_substrat_*` table both hosts need, or a new column on a table
|
|
14
|
+
* they both build, is added HERE and THERE — and for a store created before it, to both
|
|
15
|
+
* column-addition lists as well (`ensureSpineColumns` below, `applySpineColumnAdditions`
|
|
16
|
+
* on the DO side).
|
|
17
|
+
*
|
|
18
|
+
* `pnpm lint:spine-ddl` (`tools/spine-ddl-drift.mjs`) is what refuses a divergence: it
|
|
19
|
+
* executes each side's DDL plus those later ALTERs and compares the schemas a query would
|
|
20
|
+
* actually meet — columns, indexes (including the ones a UNIQUE creates) and foreign keys.
|
|
21
|
+
* Two things it does NOT judge. A table present on one side only is a note, not a failure,
|
|
22
|
+
* because the adapters legitimately partition the spine differently. And triggers and CHECK
|
|
23
|
+
* constraints are not compared at all; the spine has none today, so adding one here means
|
|
24
|
+
* adding it there with nothing to catch you. Keeping the copies and gating them, rather
|
|
25
|
+
* than moving the DDL into the kernel, is the recorded answer to #969;
|
|
26
|
+
* `docs/architecture/kernel-design.md` §8 says why.
|
|
27
|
+
*/
|
|
8
28
|
const KERNEL_DDL = `
|
|
9
29
|
CREATE TABLE IF NOT EXISTS _substrat_outbox (
|
|
10
30
|
id TEXT PRIMARY KEY,
|
|
@@ -34,11 +54,28 @@ const KERNEL_DDL = `
|
|
|
34
54
|
-- event was emitted (the signals version dimension, #1231). NULL = the host was
|
|
35
55
|
-- handed no version id, or the row predates the column.
|
|
36
56
|
version TEXT,
|
|
57
|
+
-- #1237: the event this one was emitted in REACTION to — set whenever an emit
|
|
58
|
+
-- happens while this scope is delivering an event to a module consumer. The
|
|
59
|
+
-- spine already recorded what authority an operation held and what invocation
|
|
60
|
+
-- it ran under; neither is cause, and a consumer emit has no operation at all,
|
|
61
|
+
-- so a backwards walk used to stop dead at the first consumer hop. NULL =
|
|
62
|
+
-- nothing was being delivered (an operation emitted it directly), or the row
|
|
63
|
+
-- predates the column.
|
|
64
|
+
caused_by TEXT,
|
|
37
65
|
drained_at TEXT
|
|
38
66
|
);
|
|
39
67
|
-- #1232: the freshness evaluator's read — MAX(occurred_at) per type, every pass,
|
|
40
68
|
-- over a table that is never pruned. Unindexed, that is a full history scan.
|
|
41
69
|
CREATE INDEX IF NOT EXISTS _substrat_outbox_type_at ON _substrat_outbox (type, occurred_at);
|
|
70
|
+
-- #1334: the drain's read — WHERE drained_at IS NULL ORDER BY id. No existing index
|
|
71
|
+
-- starts with drained_at, so without this one SQLite walks the PRIMARY KEY from the
|
|
72
|
+
-- oldest event forward, stepping over every row a previous pass already shipped. A
|
|
73
|
+
-- drain RETAINS what it marks, so that prefix only grows: the cost of finding the next
|
|
74
|
+
-- batch would rise with the scope's lifetime event count rather than with how far
|
|
75
|
+
-- behind the drain is. Leading with drained_at makes the undrained rows a seekable
|
|
76
|
+
-- range, and the trailing id gives the ORDER BY for free. IF NOT EXISTS in
|
|
77
|
+
-- KERNEL_DDL, which every runtime() re-runs, so existing scopes get it on next wake.
|
|
78
|
+
CREATE INDEX IF NOT EXISTS _substrat_outbox_drained ON _substrat_outbox (drained_at, id);
|
|
42
79
|
-- platform-intents.md: durable intents a vertical enqueues (ctx.requestPlatform) for the platform
|
|
43
80
|
-- to drain and execute with HostAdmin authority — the sandbox-clean way a vertical asks for a
|
|
44
81
|
-- privileged action. Written by the kernel (spine), settled by the platform drain.
|
|
@@ -405,6 +442,21 @@ export class SqliteScopeHost {
|
|
|
405
442
|
* schema backwards and the next read of a newer column would fail with a bare `no such
|
|
406
443
|
* column`. Every statement is IF NOT EXISTS and `ensureDirectoryColumns` probes before
|
|
407
444
|
* it alters, so this only ever adds back what a copy did not carry.
|
|
445
|
+
*
|
|
446
|
+
* It is also one of two hand-written copies of the directory spine (#969). The other is
|
|
447
|
+
* `DIRECTORY_DDL` in `adapter-cloudflare/src/control-plane-do.ts`, and the two must
|
|
448
|
+
* describe the same schema: this side is dev, CI, self-host and escrow, that side is
|
|
449
|
+
* production. So a new directory table both adapters need, or a new column on a table
|
|
450
|
+
* they both build, is added HERE and THERE — and for a directory created before it, to
|
|
451
|
+
* both adapters' `ensureDirectoryColumns` as well.
|
|
452
|
+
* `pnpm lint:spine-ddl` (`tools/spine-ddl-drift.mjs`) refuses a divergence by executing
|
|
453
|
+
* each side's DDL plus those later ALTERs and comparing the schemas a query would
|
|
454
|
+
* actually meet — columns, indexes and foreign keys. What it does not judge: a table
|
|
455
|
+
* present on one side only, which it reports as a note because the adapters legitimately
|
|
456
|
+
* partition the spine differently, and triggers or CHECK constraints, which it does not
|
|
457
|
+
* compare at all (the spine has none).
|
|
458
|
+
* Keeping the copies and gating them, rather than moving the DDL into the kernel, is the
|
|
459
|
+
* recorded answer to #969; `docs/architecture/kernel-design.md` §8 says why.
|
|
408
460
|
*/
|
|
409
461
|
applyDirectorySchema() {
|
|
410
462
|
this.directory.exec(`
|
|
@@ -2784,6 +2836,11 @@ export class SqliteScopeHost {
|
|
|
2784
2836
|
const ctx = this.operationContext(rt, asPrincipal(this.systemPrincipal), {
|
|
2785
2837
|
system: mod.id,
|
|
2786
2838
|
});
|
|
2839
|
+
// #1237: anything this consumer emits was emitted BECAUSE of this event.
|
|
2840
|
+
// `dispatchExecutors` already did this for the admin log; the module
|
|
2841
|
+
// consumers never did, which is exactly where a backwards walk stopped.
|
|
2842
|
+
// On the runtime rather than the host — see `ScopeRuntime.causedBy`.
|
|
2843
|
+
rt.causedBy = event.id;
|
|
2787
2844
|
rt.db.exec('BEGIN IMMEDIATE');
|
|
2788
2845
|
try {
|
|
2789
2846
|
await consumer.handler(ctx, event);
|
|
@@ -2803,6 +2860,13 @@ export class SqliteScopeHost {
|
|
|
2803
2860
|
VALUES (?, ?, ?, ?)`)
|
|
2804
2861
|
.run(event.id, mod.id, new Date().toISOString(), String(err));
|
|
2805
2862
|
}
|
|
2863
|
+
finally {
|
|
2864
|
+
// Cleared on BOTH paths. Left set, the id would leak onto every later
|
|
2865
|
+
// emit in this scope — an operation's own event stamped as caused by
|
|
2866
|
+
// whatever happened to be delivered last, which is worse than no cause
|
|
2867
|
+
// at all, because it reads as a recorded fact.
|
|
2868
|
+
rt.causedBy = null;
|
|
2869
|
+
}
|
|
2806
2870
|
}
|
|
2807
2871
|
}
|
|
2808
2872
|
}
|
|
@@ -4119,6 +4183,62 @@ export class SqliteScopeHost {
|
|
|
4119
4183
|
.run(expiresAt, scopeId);
|
|
4120
4184
|
this.recordAdmin(actor, 'setScopeExpiresAt', { tenantId, scopeId }, { expiresAt: scope.expires_at }, { expiresAt });
|
|
4121
4185
|
},
|
|
4186
|
+
readUndrainedEvents: async (actor, tenantId, scopeId, limit) => {
|
|
4187
|
+
const db = this.scopeDbFor(tenantId, scopeId);
|
|
4188
|
+
const rows = db
|
|
4189
|
+
.prepare(`SELECT * FROM _substrat_outbox WHERE drained_at IS NULL ORDER BY id LIMIT ?`)
|
|
4190
|
+
.all(Math.min(Math.max(limit ?? 200, 1), 1000));
|
|
4191
|
+
this.recordAccess(actor, 'readUndrainedEvents', { tenantId, scopeId }, { limit }, rows.length);
|
|
4192
|
+
return rows.map((r) => ({
|
|
4193
|
+
...this.parseOutboxRow(r),
|
|
4194
|
+
operation: r.operation ?? null,
|
|
4195
|
+
version: r.version ?? null,
|
|
4196
|
+
}));
|
|
4197
|
+
},
|
|
4198
|
+
markEventsDrained: async (actor, tenantId, scopeId, eventIds) => {
|
|
4199
|
+
// BEFORE the empty-batch shortcut, not after. "Nothing to mark" and "you may
|
|
4200
|
+
// not address this scope" are different answers, and returning 0 for an
|
|
4201
|
+
// unknown pair would make a caller's typo look like a successful no-op —
|
|
4202
|
+
// while the Cloudflare host, which resolves the record first, throws. An
|
|
4203
|
+
// empty batch must not open the scope file either, which is why this is the
|
|
4204
|
+
// directory-only check rather than `scopeDbFor`.
|
|
4205
|
+
this.assertScopeReachable(tenantId, scopeId);
|
|
4206
|
+
if (eventIds.length === 0)
|
|
4207
|
+
return 0;
|
|
4208
|
+
const db = this.scopeDbFor(tenantId, scopeId);
|
|
4209
|
+
const at = new Date().toISOString();
|
|
4210
|
+
// Only an UNDRAINED row is stamped, so a replayed batch cannot move an
|
|
4211
|
+
// earlier drain's timestamp forward and misreport when it shipped.
|
|
4212
|
+
const stmt = db.prepare(`UPDATE _substrat_outbox SET drained_at = ? WHERE id = ? AND drained_at IS NULL`);
|
|
4213
|
+
// All-or-nothing. A PARTIAL mark is the one failure the ship→stamp order
|
|
4214
|
+
// cannot absorb: the next read resumes from the middle of a batch already
|
|
4215
|
+
// in the lake, and the object it writes overlaps the one already there —
|
|
4216
|
+
// the duplication the first-id object key exists to prevent.
|
|
4217
|
+
const drained = db.transaction(() => {
|
|
4218
|
+
let n = 0;
|
|
4219
|
+
for (const id of eventIds)
|
|
4220
|
+
n += stmt.run(at, id).changes;
|
|
4221
|
+
return n;
|
|
4222
|
+
})();
|
|
4223
|
+
// K-24's rule, one tier down (#1334): declaring domain payloads shipped is an
|
|
4224
|
+
// EGRESS, and the admin log is where "these events left the platform, at this
|
|
4225
|
+
// time, on this actor's say-so" is recorded. `drainAccessLog` audits the same
|
|
4226
|
+
// act for the smaller class of data; this one carries the larger. Only a
|
|
4227
|
+
// NONZERO change is recorded, so a retried pass that re-marks a batch it
|
|
4228
|
+
// already shipped writes no row claiming an egress that never happened.
|
|
4229
|
+
if (drained > 0) {
|
|
4230
|
+
this.recordAdmin(actor, 'drainEvents', { tenantId, scopeId }, null, { drained, requested: eventIds.length, drainedAt: at });
|
|
4231
|
+
}
|
|
4232
|
+
return drained;
|
|
4233
|
+
},
|
|
4234
|
+
facetEvents: async (actor, tenantId, scopeId, input) => {
|
|
4235
|
+
// `facetEvents` is the sanctioned read: an erased payload yields the same
|
|
4236
|
+
// NULL a missing field does, and only the helper counts them apart.
|
|
4237
|
+
const db = this.scopeDbFor(tenantId, scopeId);
|
|
4238
|
+
const result = facetEvents({ sql: scopedSql(db) }, input);
|
|
4239
|
+
this.recordAccess(actor, 'facetEvents', { tenantId, scopeId }, input, result.buckets.length);
|
|
4240
|
+
return result;
|
|
4241
|
+
},
|
|
4122
4242
|
entityHistory: async (actor, tenantId, scopeId, input) => {
|
|
4123
4243
|
// `readHistory` over this scope's own outbox — the sanctioned read, and the
|
|
4124
4244
|
// only one that decodes the envelope's nullable facts (an erased payload, an
|
|
@@ -5942,13 +6062,18 @@ export class SqliteScopeHost {
|
|
|
5942
6062
|
.prepare(`INSERT INTO _substrat_outbox
|
|
5943
6063
|
(id, type, schema_version, occurred_at, tenant_id, scope_id, actor,
|
|
5944
6064
|
entity_type, entity_id, pii_class, subject_id, authorization,
|
|
5945
|
-
impersonation, operation, version, payload)
|
|
5946
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
6065
|
+
impersonation, operation, version, caused_by, payload)
|
|
6066
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
5947
6067
|
.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.operation ?? null,
|
|
5948
6068
|
// #1242: host configuration, not envelope data — the version is a fact
|
|
5949
6069
|
// about the process, so it never rides `DomainEvent` for module code to
|
|
5950
6070
|
// branch on; it exists for the observability joins the column serves.
|
|
5951
|
-
this.versionId,
|
|
6071
|
+
this.versionId,
|
|
6072
|
+
// #1237: whatever delivery is in flight FOR THIS SCOPE, if any — read off
|
|
6073
|
+
// the runtime, not the host, because the host serves every scope at once
|
|
6074
|
+
// and this context outlives an `await`. A fact about the surrounding
|
|
6075
|
+
// dispatch, never envelope data module code could set or branch on.
|
|
6076
|
+
rt.causedBy, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
5952
6077
|
},
|
|
5953
6078
|
requestPlatform: (request) => {
|
|
5954
6079
|
assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
|
|
@@ -6228,10 +6353,30 @@ export class SqliteScopeHost {
|
|
|
6228
6353
|
* DIFFERENT tenant, throws — the introspection reads never open another tenant's
|
|
6229
6354
|
* DB, and never CREATE one for an id that was never provisioned.
|
|
6230
6355
|
*/
|
|
6231
|
-
|
|
6232
|
-
|
|
6356
|
+
/**
|
|
6357
|
+
* Whether this (tenant, scope) pair may be reached at all — the DIRECTORY half of
|
|
6358
|
+
* `scopeDbFor`, split out so a call that ends up touching no rows still answers the
|
|
6359
|
+
* question. K-3 first: a scope under another tenant is indistinguishable from one
|
|
6360
|
+
* that does not exist. Then the reap.
|
|
6361
|
+
*
|
|
6362
|
+
* A reaped scope keeps its directory row as a tombstone while `reapScope` deletes
|
|
6363
|
+
* the file (§4.4). `runtime()` opens a database by CREATING it when absent, so
|
|
6364
|
+
* reading one would not merely answer emptily — it would put the file back,
|
|
6365
|
+
* resurrecting storage an irreversible reap destroyed, and every later read would
|
|
6366
|
+
* report a scope with no events rather than a scope that is gone. `archived` is
|
|
6367
|
+
* deliberately still reachable: its bytes exist, and that is the whole point of the
|
|
6368
|
+
* state.
|
|
6369
|
+
*/
|
|
6370
|
+
assertScopeReachable(tenantId, scopeId) {
|
|
6371
|
+
const r = this.directory.prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
6233
6372
|
if (!r || r.tenant_id !== tenantId)
|
|
6234
6373
|
throw new Error(`unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
6374
|
+
if (r.status === 'reaped') {
|
|
6375
|
+
throw new Error(`scope ${scopeId} is reaped — its storage is gone and cannot be read`);
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
scopeDbFor(tenantId, scopeId) {
|
|
6379
|
+
this.assertScopeReachable(tenantId, scopeId);
|
|
6235
6380
|
return this.runtime(tenantId, scopeId).db;
|
|
6236
6381
|
}
|
|
6237
6382
|
/**
|
|
@@ -6273,6 +6418,10 @@ export class SqliteScopeHost {
|
|
|
6273
6418
|
// #1242: the signals `version` dimension on the outbox, for a scope DB created
|
|
6274
6419
|
// before the column. NULL stays honest — unstamped, not a version nobody named.
|
|
6275
6420
|
this.ensureColumn(db, '_substrat_outbox', 'version', 'version TEXT');
|
|
6421
|
+
// #1237: existing scopes get the cause column on their next wake, like the four
|
|
6422
|
+
// above. Rows already written keep NULL, which is honestly "unrecorded" — the
|
|
6423
|
+
// reason the column is nullable rather than defaulted.
|
|
6424
|
+
this.ensureColumn(db, '_substrat_outbox', 'caused_by', 'caused_by TEXT');
|
|
6276
6425
|
}
|
|
6277
6426
|
runtime(tenantId, scopeId) {
|
|
6278
6427
|
const key = `${tenantId}/${scopeId}`;
|
|
@@ -6300,6 +6449,7 @@ export class SqliteScopeHost {
|
|
|
6300
6449
|
actor: new ScopeActor(),
|
|
6301
6450
|
appliedMigrations,
|
|
6302
6451
|
mintEventId,
|
|
6452
|
+
causedBy: null,
|
|
6303
6453
|
};
|
|
6304
6454
|
this.scopes.set(key, created);
|
|
6305
6455
|
this.scopesById.set(scopeId, created);
|