@substrat-run/adapter-sqlite 0.138.0 → 0.139.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 +35 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +277 -132
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
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, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
-
import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText,
|
|
6
|
-
import {
|
|
4
|
+
import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
+
import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, entityStateTriggerDdl, statefulTablesOf, assertEntityStateIntact, stateListIndexNames, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexDdl, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, isSearchIndexTable, searchIndexDdl, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
|
|
6
|
+
import { attributedView } from '@substrat-run/kernel';
|
|
7
7
|
import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
|
|
8
8
|
import { LEGACY_SCOPE_ROWS_BACKFILL, assertNoSpineReference, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
|
|
9
9
|
import { ScopeActor } from './actor.js';
|
|
@@ -213,6 +213,9 @@ const KERNEL_DDL = `
|
|
|
213
213
|
-- #1686: where a copied scope's data came from, and the event id its own events start above.
|
|
214
214
|
-- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
|
|
215
215
|
${COPY_ORIGIN_DDL}
|
|
216
|
+
-- #119: the kernel's authorization for one archive/trash move, read by the derived update
|
|
217
|
+
-- trigger. Shared with the other adapter from @substrat-run/kernel; the comments are there.
|
|
218
|
+
${ENTITY_STATE_MOVES_DDL}
|
|
216
219
|
-- #383 / #1232 / #1288: the platform sweep's per-scope gating state, holding two
|
|
217
220
|
-- families of row that the kind COLUMN — not the spelling of a key — tells apart.
|
|
218
221
|
-- Spine (kernel-written), never a module migration. Shared with the DO adapter from
|
|
@@ -521,6 +524,8 @@ export class SqliteScopeHost {
|
|
|
521
524
|
searchPlans = new Map();
|
|
522
525
|
/** #811: the paged lists modules declare, by entity type. Same one-owner rule as search. */
|
|
523
526
|
listPlans = new Map();
|
|
527
|
+
/** #119: entity type → its archive/trash plan, from every registered module. */
|
|
528
|
+
statePlans = new Map();
|
|
524
529
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
525
530
|
attachmentTargets = new Map();
|
|
526
531
|
/** operation name → who binds it: the owning module, its entitlementKey and its declared
|
|
@@ -557,10 +562,10 @@ export class SqliteScopeHost {
|
|
|
557
562
|
*/
|
|
558
563
|
jobs = new Map();
|
|
559
564
|
/**
|
|
560
|
-
* The event
|
|
561
|
-
*
|
|
562
|
-
*
|
|
563
|
-
*
|
|
565
|
+
* The event an executor is effecting, stamped onto the admin rows it writes (K-22). Like
|
|
566
|
+
* `onBehalfOf`, only ever set on a VIEW (`causedByView`, or `attributed` with a
|
|
567
|
+
* `causedBy`), never on the host: a field set around the handler's `await` stamped every
|
|
568
|
+
* admin call the host served meanwhile — a staff call included — with that event (#2055).
|
|
564
569
|
*/
|
|
565
570
|
causedBy = null;
|
|
566
571
|
/**
|
|
@@ -573,6 +578,7 @@ export class SqliteScopeHost {
|
|
|
573
578
|
secretBox;
|
|
574
579
|
/** The parsers attachment text is extracted with (K-43); the host's own, never imported here. */
|
|
575
580
|
attachmentExtractors;
|
|
581
|
+
attachmentTextBounds;
|
|
576
582
|
fetchImpl;
|
|
577
583
|
connectorCalls;
|
|
578
584
|
clock;
|
|
@@ -590,6 +596,7 @@ export class SqliteScopeHost {
|
|
|
590
596
|
this.secretBox = options.secretBox ?? unconfiguredSecretBox;
|
|
591
597
|
assertAttachmentExtractors(options.attachmentExtractors ?? []);
|
|
592
598
|
this.attachmentExtractors = options.attachmentExtractors ?? [];
|
|
599
|
+
this.attachmentTextBounds = resolveAttachmentTextBounds(options.attachmentTextBounds);
|
|
593
600
|
this.fetchImpl = options.fetch ?? globalFetch;
|
|
594
601
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
595
602
|
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
@@ -1146,6 +1153,8 @@ export class SqliteScopeHost {
|
|
|
1146
1153
|
resolved_at TEXT
|
|
1147
1154
|
);
|
|
1148
1155
|
CREATE INDEX IF NOT EXISTS _substrat_issues_seen ON _substrat_issues (last_seen);
|
|
1156
|
+
-- #1748: tenant findings and their suppress rules — kernel-owned DDL (findings.ts).
|
|
1157
|
+
${FINDINGS_DDL}
|
|
1149
1158
|
-- The durable sweep record (#1232): one row per unit outcome per pass —
|
|
1150
1159
|
-- a connection swept/skipped/failed, a schedule fired/skipped/failed. What
|
|
1151
1160
|
-- makes "when was this last swept" answerable after the log line rolls off.
|
|
@@ -1458,14 +1467,17 @@ export class SqliteScopeHost {
|
|
|
1458
1467
|
// #811: the list indexes this module's `lists` declare — same placement and
|
|
1459
1468
|
// the same reason as the search indexes above: appended AFTER the module's
|
|
1460
1469
|
// own migrations, so `CREATE INDEX` names a table that exists.
|
|
1461
|
-
|
|
1462
|
-
|
|
1470
|
+
// #119: the archive/trash columns, and the plans the verbs and the reads consult.
|
|
1471
|
+
const stateMigrations = entityStateMigrations(manifest.id, manifest.entityStates);
|
|
1472
|
+
const listMigrations = listIndexMigrations(manifest.id, manifest.lists, manifest.entityStates);
|
|
1473
|
+
for (const m of [...stateMigrations, ...listMigrations]) {
|
|
1463
1474
|
if (seen.has(m.version)) {
|
|
1464
1475
|
throw new Error(`duplicate migration version in ${manifest.id}: ${m.version}`);
|
|
1465
1476
|
}
|
|
1466
1477
|
seen.add(m.version);
|
|
1467
1478
|
}
|
|
1468
|
-
|
|
1479
|
+
addStatePlans(this.statePlans, manifest.id, manifest.entityStates, manifest.permissions);
|
|
1480
|
+
for (const plan of listIndexPlans(manifest.id, manifest.lists, manifest.entityStates)) {
|
|
1469
1481
|
const existing = this.listPlans.get(plan.entityType);
|
|
1470
1482
|
if (existing) {
|
|
1471
1483
|
throw new Error(`list: '${plan.entityType}' declares a paged list in both '${existing.moduleId}' and ` +
|
|
@@ -2266,6 +2278,18 @@ export class SqliteScopeHost {
|
|
|
2266
2278
|
if (present.has(plan.table))
|
|
2267
2279
|
db.exec(searchIndexDdl(plan));
|
|
2268
2280
|
}
|
|
2281
|
+
// #119: the guard triggers went with the dropped table. Put back AFTER the rows, which
|
|
2282
|
+
// may legitimately arrive archived or trashed.
|
|
2283
|
+
for (const plan of this.statePlans.values()) {
|
|
2284
|
+
if (present.has(plan.table))
|
|
2285
|
+
db.exec(entityStateTriggerDdl(plan));
|
|
2286
|
+
}
|
|
2287
|
+
// #811 / #119: the derived list indexes went with it too, and a load never put them back —
|
|
2288
|
+
// an archivable entity's partial indexes are part of what the kernel checks after DDL.
|
|
2289
|
+
for (const plan of this.listPlans.values()) {
|
|
2290
|
+
if (present.has(plan.table))
|
|
2291
|
+
db.exec(listIndexDdl(plan));
|
|
2292
|
+
}
|
|
2269
2293
|
// #1575: attachment text is not in a dump, so a load left it as it was. Drop the
|
|
2270
2294
|
// text of attachments the dump did not bring back, and queue extraction for those
|
|
2271
2295
|
// it brought back without text — the bytes decide what that run finds.
|
|
@@ -2685,16 +2709,14 @@ export class SqliteScopeHost {
|
|
|
2685
2709
|
const { hops: _hops, ...fact } = e;
|
|
2686
2710
|
const event = structuredClone({ ...fact, source });
|
|
2687
2711
|
const unit = unitOf(imp.moduleId, e.id, e.type);
|
|
2688
|
-
// What the handler emits was emitted BECAUSE of the producer's event (#1237). The id
|
|
2689
|
-
// resolves through `_substrat_imports`, which the row above has just written.
|
|
2690
|
-
rt.causedBy = e.id;
|
|
2691
|
-
rt.unitInvocationId = unit.invocationId;
|
|
2692
2712
|
rt.db.exec('BEGIN IMMEDIATE');
|
|
2693
2713
|
try {
|
|
2694
2714
|
// As the producer's principal, admitted above: the handler's checks are real ones,
|
|
2695
2715
|
// against the grants this vertical's `peers` gave it, and its emits carry the actor
|
|
2696
|
-
// `{ vertical, scope }` and the authorization they passed (K-34).
|
|
2697
|
-
|
|
2716
|
+
// `{ vertical, scope }` and the authorization they passed (K-34). What it emits was
|
|
2717
|
+
// emitted BECAUSE of the producer's event (#1237); the id resolves through
|
|
2718
|
+
// `_substrat_imports`, which the row above has just written.
|
|
2719
|
+
await imp.handler(this.deliveryContext(rt, subject, undefined, { causedBy: e.id, invocationId: unit.invocationId }), event);
|
|
2698
2720
|
rt.db
|
|
2699
2721
|
.prepare(`INSERT INTO _substrat_deliveries (event_id, consumer_module, delivered_at, invocation_id)
|
|
2700
2722
|
VALUES (?, ?, ?, NULL)`)
|
|
@@ -2711,10 +2733,6 @@ export class SqliteScopeHost {
|
|
|
2711
2733
|
result.deadLettered += 1;
|
|
2712
2734
|
lines.write({ ...unit, outcome: 'dead-lettered', error: err });
|
|
2713
2735
|
}
|
|
2714
|
-
finally {
|
|
2715
|
-
rt.causedBy = null;
|
|
2716
|
-
rt.unitInvocationId = null;
|
|
2717
|
-
}
|
|
2718
2736
|
}
|
|
2719
2737
|
if (!ran)
|
|
2720
2738
|
result.duplicates += 1;
|
|
@@ -2925,13 +2943,35 @@ export class SqliteScopeHost {
|
|
|
2925
2943
|
const target = principalId.parse(assignee);
|
|
2926
2944
|
return rt.actor.turn(async () => {
|
|
2927
2945
|
const bound = await this.assignmentBound(subject, tenantId, scopeId, roleKey);
|
|
2928
|
-
if (bound.covered)
|
|
2929
|
-
rt.db
|
|
2930
|
-
VALUES (?, ?, ?, NULL)`).run(`principal:${target}`, `role:${roleKey}`, `scope:${scopeId}`);
|
|
2931
|
-
}
|
|
2946
|
+
if (bound.covered)
|
|
2947
|
+
applyScopeRoleChange(switchSqlOf(rt.db), scopeId, target, { revoke: [], grant: roleKey }, this.clock());
|
|
2932
2948
|
return bound;
|
|
2933
2949
|
});
|
|
2934
2950
|
}
|
|
2951
|
+
async listScopeRoleHolders(tenantId, scopeId, principal) {
|
|
2952
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
2953
|
+
const who = principal === undefined ? undefined : principalId.parse(principal);
|
|
2954
|
+
return rt.actor.enqueue(() => scopeRoleHolders(switchSqlOf(rt.db), scopeId, this.clock(), who));
|
|
2955
|
+
}
|
|
2956
|
+
async changeScopeRoleBounded(tenantId, scopeId, caller, principal, from, to) {
|
|
2957
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
2958
|
+
const target = principalId.parse(principal);
|
|
2959
|
+
const answer = await rt.actor.turn(() => changeScopeRole(switchSqlOf(rt.db), scopeId, target, from, to, this.clock(), this.roleBound(caller, tenantId, scopeId), (run) => rt.db.transaction(run)()));
|
|
2960
|
+
if (answer === 'not-held')
|
|
2961
|
+
throw substratError('conflict', `${target} does not hold '${from}' at this scope`);
|
|
2962
|
+
if (answer === 'unknown-to')
|
|
2963
|
+
throw unknownRoleError(to);
|
|
2964
|
+
return answer;
|
|
2965
|
+
}
|
|
2966
|
+
async revokeScopeRolesBounded(tenantId, scopeId, caller, principal) {
|
|
2967
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
2968
|
+
return rt.actor.turn(() => revokeScopeRoles(switchSqlOf(rt.db), scopeId, principalId.parse(principal), this.clock(), this.roleBound(caller, tenantId, scopeId), (run) => rt.db.transaction(run)()));
|
|
2969
|
+
}
|
|
2970
|
+
/** The caller's bound per role at the scope, `null` for a role this tenant does not define (#1150). */
|
|
2971
|
+
roleBound(caller, tenantId, scopeId) {
|
|
2972
|
+
const subject = asPrincipal(principalId.parse(caller));
|
|
2973
|
+
return async (roleKey) => this.roles.has(`${tenantId}/${roleKey}`) ? this.assignmentBound(subject, tenantId, scopeId, roleKey) : null;
|
|
2974
|
+
}
|
|
2935
2975
|
/**
|
|
2936
2976
|
* §5.1's assignment bound, resolved against the SAME role table the checker expands
|
|
2937
2977
|
* — not a vertical's compile-time `ROLES` array. A tenant's projected role is what
|
|
@@ -3683,16 +3723,17 @@ export class SqliteScopeHost {
|
|
|
3683
3723
|
lines.write({ ...unit, attempt: attempts, outcome: 'inert' });
|
|
3684
3724
|
continue;
|
|
3685
3725
|
}
|
|
3686
|
-
|
|
3726
|
+
// #2055: the handler writes through a view bound to its event, never through the host.
|
|
3727
|
+
const caused = this.causedByView(event.id);
|
|
3687
3728
|
try {
|
|
3688
3729
|
let result;
|
|
3689
3730
|
if (executor.kind === 'connector') {
|
|
3690
3731
|
// `true`: dispatchExecutors is only ever reached from inside
|
|
3691
3732
|
// `rt.actor.enqueue` (invoke's post-commit tail, or drainDue).
|
|
3692
|
-
await executor.handler(
|
|
3733
|
+
await executor.handler(caused.connectorContext(rt, executor.timeoutMs, true, event.id), event);
|
|
3693
3734
|
}
|
|
3694
3735
|
else {
|
|
3695
|
-
result = await executor.handler(
|
|
3736
|
+
result = await executor.handler(caused.admin, event, scope);
|
|
3696
3737
|
}
|
|
3697
3738
|
// #1184: a refusal is the handler's own terminal decision — journaled with its
|
|
3698
3739
|
// reason, never retried, and listed by `executorDeadLetters` beside an exhausted one.
|
|
@@ -3719,9 +3760,6 @@ export class SqliteScopeHost {
|
|
|
3719
3760
|
outcomeOf(dead ? 'dead-lettered' : 'retrying', err);
|
|
3720
3761
|
lines.write({ ...unit, attempt: attempts, outcome: dead ? 'dead-lettered' : 'retrying', error: err });
|
|
3721
3762
|
}
|
|
3722
|
-
finally {
|
|
3723
|
-
this.causedBy = null;
|
|
3724
|
-
}
|
|
3725
3763
|
}
|
|
3726
3764
|
}
|
|
3727
3765
|
}
|
|
@@ -3948,7 +3986,7 @@ export class SqliteScopeHost {
|
|
|
3948
3986
|
write: (attachmentId, outcome) => rt.actor.enqueue(() => recordAttachmentText(spineSql(rt.db), attachmentId, outcome, this.clock())),
|
|
3949
3987
|
},
|
|
3950
3988
|
// K-43: the host's parsers, handed in — this adapter imports none.
|
|
3951
|
-
this.attachmentExtractors);
|
|
3989
|
+
this.attachmentExtractors, this.attachmentTextBounds);
|
|
3952
3990
|
}
|
|
3953
3991
|
async startJobRun(tenantId, scopeId, input) {
|
|
3954
3992
|
const rt = this.runtime(tenantId, scopeId);
|
|
@@ -3958,12 +3996,19 @@ export class SqliteScopeHost {
|
|
|
3958
3996
|
async runDueJobs(tenantId, scopeId, options) {
|
|
3959
3997
|
const rt = this.runtime(tenantId, scopeId);
|
|
3960
3998
|
await this.applyPendingMigrations(rt);
|
|
3961
|
-
// #1575: the kernel's extraction
|
|
3962
|
-
// deployment registers
|
|
3963
|
-
|
|
3999
|
+
// #1575: the kernel's extraction jobs are this host's own, bound to this scope — no
|
|
4000
|
+
// deployment registers them, and none can shadow them. The backfill starts here, on
|
|
4001
|
+
// the drive, so a scope's first request never pays for attachments that predate it.
|
|
4002
|
+
await rt.actor.enqueue(() => startAttachmentTextBackfill(spineSql(rt.db), ulid(), this.clock()));
|
|
4003
|
+
const kernelJobs = {
|
|
4004
|
+
[ATTACHMENT_TEXT_JOB]: this.attachmentTextHandler(rt),
|
|
4005
|
+
[ATTACHMENT_TEXT_BACKFILL_JOB]: attachmentTextBackfillJob({
|
|
4006
|
+
queueBatch: (after) => rt.actor.enqueue(() => rt.db.transaction(() => queueAttachmentTextBackfill(spineSql(rt.db), after, ulid, this.clock()))()),
|
|
4007
|
+
}),
|
|
4008
|
+
};
|
|
3964
4009
|
return runDueJobRuns({
|
|
3965
4010
|
store: this.jobStore(rt),
|
|
3966
|
-
handlerFor: (run) =>
|
|
4011
|
+
handlerFor: (run) => kernelJobFor(run, kernelJobs) ?? this.jobs.get(`${run.module_id}/${run.job}`),
|
|
3967
4012
|
now: this.clock,
|
|
3968
4013
|
openScope: (run) => this.getSystemScope(run.module_id, tenantId, scopeId),
|
|
3969
4014
|
maxPasses: options?.maxPasses,
|
|
@@ -3983,15 +4028,10 @@ export class SqliteScopeHost {
|
|
|
3983
4028
|
// Cloudflare one does; the context build is the same one `dispatchExecutors` hands
|
|
3984
4029
|
// an in-process connector.
|
|
3985
4030
|
const rt = this.runtime(tenantId, scopeId);
|
|
3986
|
-
this
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
await handler(this.connectorContext(rt, options?.timeoutMs ?? 30_000, false, event.id), event);
|
|
3991
|
-
}
|
|
3992
|
-
finally {
|
|
3993
|
-
this.causedBy = null;
|
|
3994
|
-
}
|
|
4031
|
+
// `false`: this path deliberately does NOT enqueue, so nothing is held and
|
|
4032
|
+
// the connection's reads take an ordinary serialized turn. Built on a view bound to
|
|
4033
|
+
// the event, so its admin rows carry it and nothing else the host serves does (#2055).
|
|
4034
|
+
await handler(this.causedByView(event.id).connectorContext(rt, options?.timeoutMs ?? 30_000, false, event.id), event);
|
|
3995
4035
|
}
|
|
3996
4036
|
/**
|
|
3997
4037
|
* The scope's own answer to "what may this connection do here" (#726 gap 1) — read
|
|
@@ -4211,16 +4251,12 @@ export class SqliteScopeHost {
|
|
|
4211
4251
|
lines.write({ ...unit, outcome: 'dead-lettered' });
|
|
4212
4252
|
continue;
|
|
4213
4253
|
}
|
|
4214
|
-
|
|
4215
|
-
|
|
4254
|
+
// #1237: anything this consumer emits was emitted BECAUSE of this event — where a
|
|
4255
|
+
// backwards walk used to stop. #1901: its `ctx.log` lines join this delivery's line.
|
|
4256
|
+
const ctx = this.deliveryContext(rt, asPrincipal(this.systemPrincipal), { system: mod.id }, {
|
|
4257
|
+
causedBy: event.id,
|
|
4258
|
+
invocationId: unit.invocationId,
|
|
4216
4259
|
});
|
|
4217
|
-
// #1237: anything this consumer emits was emitted BECAUSE of this event.
|
|
4218
|
-
// `dispatchExecutors` already did this for the admin log; the module
|
|
4219
|
-
// consumers never did, which is exactly where a backwards walk stopped.
|
|
4220
|
-
// On the runtime rather than the host — see `ScopeRuntime.causedBy`.
|
|
4221
|
-
rt.causedBy = event.id;
|
|
4222
|
-
// #1901: the handler's `ctx.log` lines join this delivery's line.
|
|
4223
|
-
rt.unitInvocationId = unit.invocationId;
|
|
4224
4260
|
rt.db.exec('BEGIN IMMEDIATE');
|
|
4225
4261
|
try {
|
|
4226
4262
|
await consumer.handler(ctx, event);
|
|
@@ -4244,14 +4280,6 @@ export class SqliteScopeHost {
|
|
|
4244
4280
|
.run(event.id, mod.id, new Date().toISOString(), String(err), invocationId);
|
|
4245
4281
|
lines.write({ ...unit, outcome: 'dead-lettered', error: err });
|
|
4246
4282
|
}
|
|
4247
|
-
finally {
|
|
4248
|
-
rt.unitInvocationId = null;
|
|
4249
|
-
// Cleared on BOTH paths. Left set, the id would leak onto every later
|
|
4250
|
-
// emit in this scope — an operation's own event stamped as caused by
|
|
4251
|
-
// whatever happened to be delivered last, which is worse than no cause
|
|
4252
|
-
// at all, because it reads as a recorded fact.
|
|
4253
|
-
rt.causedBy = null;
|
|
4254
|
-
}
|
|
4255
4283
|
}
|
|
4256
4284
|
}
|
|
4257
4285
|
}
|
|
@@ -4532,6 +4560,14 @@ export class SqliteScopeHost {
|
|
|
4532
4560
|
throw new Error(`connection not found: ${id}`);
|
|
4533
4561
|
return row;
|
|
4534
4562
|
}
|
|
4563
|
+
/**
|
|
4564
|
+
* A findings mutation (#1748) and the audit rows it hands back, in ONE directory transaction:
|
|
4565
|
+
* an audit write that fails rolls the mutation back, so no verdict, rule or stale resolution
|
|
4566
|
+
* stands without its row.
|
|
4567
|
+
*/
|
|
4568
|
+
auditedFindings(actor, run) {
|
|
4569
|
+
return this.directory.transaction(() => run(redactionSqlOf(this.directory), (a) => this.recordAdmin(actor, a.action, a.target, a.before, a.after)))();
|
|
4570
|
+
}
|
|
4535
4571
|
recordAdmin(actor, action, target, before, after,
|
|
4536
4572
|
/** When the row says it happened; the wall clock unless the write took its time elsewhere (#1184). */
|
|
4537
4573
|
at = new Date().toISOString()) {
|
|
@@ -4539,7 +4575,9 @@ export class SqliteScopeHost {
|
|
|
4539
4575
|
.prepare(`INSERT INTO _substrat_admin_log
|
|
4540
4576
|
(id, actor, action, tenant_id, scope_id, vertical, before, after, caused_by, on_behalf_of, at)
|
|
4541
4577
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
4542
|
-
.run(ulid(), actor, action, target.tenantId ?? null, target.scopeId ?? null, target.vertical ?? null, before == null ? null : JSON.stringify(before), after == null ? null : JSON.stringify(after),
|
|
4578
|
+
.run(ulid(), actor, action, target.tenantId ?? null, target.scopeId ?? null, target.vertical ?? null, before == null ? null : JSON.stringify(before), after == null ? null : JSON.stringify(after),
|
|
4579
|
+
// #2055: like `onBehalfOf`, set only on a view, never on the host itself.
|
|
4580
|
+
this.causedBy,
|
|
4543
4581
|
// #977: set only on an attributed view (`attributed`), never on the host itself.
|
|
4544
4582
|
this.onBehalfOf === null ? null : JSON.stringify(this.onBehalfOf), at);
|
|
4545
4583
|
}
|
|
@@ -4562,8 +4600,12 @@ export class SqliteScopeHost {
|
|
|
4562
4600
|
.all(tenantId).map((r) => ({ key: r.entitlement_key, expired: r.expires_at !== null && r.expires_at <= now }));
|
|
4563
4601
|
}
|
|
4564
4602
|
/** #977: this host, with every admin row it writes naming who the actor acted for. */
|
|
4565
|
-
attributed(onBehalfOf) {
|
|
4566
|
-
return
|
|
4603
|
+
attributed(onBehalfOf, options) {
|
|
4604
|
+
return attributedView(this, { onBehalfOf, causedBy: options?.causedBy }, this.buildAdmin);
|
|
4605
|
+
}
|
|
4606
|
+
/** #2055: this host, with every admin row it writes naming `eventId` as its cause. */
|
|
4607
|
+
causedByView(eventId) {
|
|
4608
|
+
return attributedView(this, { causedBy: eventId }, this.buildAdmin);
|
|
4567
4609
|
}
|
|
4568
4610
|
buildAdmin() {
|
|
4569
4611
|
// #977: an admin whose audit rows say on whose behalf it acts. Built over a VIEW of this
|
|
@@ -4808,6 +4850,13 @@ export class SqliteScopeHost {
|
|
|
4808
4850
|
const { tenantId } = assignment.node;
|
|
4809
4851
|
return revokeAndFence(tenantId, assignment.principalId, `role:${assignment.roleKey}`, `tenant:${tenantId}`, (at) => this.recordAdmin(actor, 'unassignRole', { tenantId, scopeId: null }, assignment, null, at));
|
|
4810
4852
|
};
|
|
4853
|
+
// An org membership written or tombstoned with its audit row — `addMember` and `removeMember`,
|
|
4854
|
+
// and the membership executor's unit (#2047), one spelling each. Leaving raises the fence too.
|
|
4855
|
+
const joinOrg = (actor, tenantId, principal, orgId, expiresAt) => {
|
|
4856
|
+
writeTenantTuple(tenantId, `principal:${principal}`, 'member', `org:${orgId}`, expiresAt);
|
|
4857
|
+
this.recordAdmin(actor, 'addMember', { tenantId }, null, memberAddedAudit(principal, orgId, expiresAt));
|
|
4858
|
+
};
|
|
4859
|
+
const leaveOrgAndFence = (actor, tenantId, principal, orgId) => revokeAndFence(tenantId, principal, 'member', `org:${orgId}`, (at) => this.recordAdmin(actor, 'removeMember', { tenantId }, { principal, orgId }, null, at));
|
|
4811
4860
|
// The tenant-level reads the checker makes, synchronous, for the bound inside a unit.
|
|
4812
4861
|
const directoryReader = directoryTenantReader({
|
|
4813
4862
|
directory: this.directory,
|
|
@@ -6444,14 +6493,13 @@ export class SqliteScopeHost {
|
|
|
6444
6493
|
this.recordAccess(actor, 'getOrg', { tenantId }, { orgId }, o ? 1 : 0);
|
|
6445
6494
|
return o;
|
|
6446
6495
|
},
|
|
6447
|
-
addMember: async (actor, tenantId, principal, orgId) => {
|
|
6496
|
+
addMember: async (actor, tenantId, principal, orgId, opts) => {
|
|
6448
6497
|
requireOrg(tenantId, orgId);
|
|
6449
6498
|
// INSERT OR REPLACE, so re-adding a revoked member clears the tombstone —
|
|
6450
6499
|
// they are a member again. The add/revoke history is not lost: it lives in
|
|
6451
6500
|
// the append-only admin log, which is where "what happened" belongs. The
|
|
6452
6501
|
// tuple carries "what is true now" plus enough to explain a live proof.
|
|
6453
|
-
|
|
6454
|
-
this.recordAdmin(actor, 'addMember', { tenantId }, null, { principal, orgId });
|
|
6502
|
+
joinOrg(actor, tenantId, principal, orgId, opts?.expiresAt ?? null);
|
|
6455
6503
|
},
|
|
6456
6504
|
applyMembership: async (actor, change) => {
|
|
6457
6505
|
// ONE directory transaction, synchronous throughout (#1184): the fence (an add's), the
|
|
@@ -6473,10 +6521,24 @@ export class SqliteScopeHost {
|
|
|
6473
6521
|
const bound = tenantCoverage(directoryReader, tenantId, boundedBy, role.permissions);
|
|
6474
6522
|
if (!bound.covered)
|
|
6475
6523
|
return { applied: false, missing: bound.missing };
|
|
6476
|
-
|
|
6477
|
-
|
|
6524
|
+
// #2047: the org, bounded by `boundedBy`'s own live membership of it.
|
|
6525
|
+
const { orgId } = change;
|
|
6526
|
+
const org = orgId ? { orgId, ...orgChangeBound(directoryReader, tenantId, { ...change, orgId }) } : undefined;
|
|
6527
|
+
if (org) {
|
|
6528
|
+
if (!readOrg(tenantId, org.orgId))
|
|
6529
|
+
return { applied: false, unknownOrg: org.orgId };
|
|
6530
|
+
if (!org.bounded)
|
|
6531
|
+
return { applied: false, notMember: org.orgId };
|
|
6532
|
+
}
|
|
6533
|
+
if (change.op === 'remove') {
|
|
6534
|
+
const roleTaken = revokeTenantRoleAndFence(actor, assignment);
|
|
6535
|
+
const orgLeft = !!org && leaveOrgAndFence(actor, tenantId, principal, org.orgId);
|
|
6536
|
+
return { applied: true, changed: roleTaken || orgLeft };
|
|
6537
|
+
}
|
|
6478
6538
|
writeTenantTuple(tenantId, `principal:${principal}`, `role:${roleKey}`, `tenant:${tenantId}`);
|
|
6479
6539
|
this.recordAdmin(actor, 'assignRole', { tenantId, scopeId: null }, null, assignment);
|
|
6540
|
+
if (org?.bounded)
|
|
6541
|
+
joinOrg(actor, tenantId, principal, org.orgId, org.expiresAt);
|
|
6480
6542
|
return { applied: true };
|
|
6481
6543
|
})();
|
|
6482
6544
|
},
|
|
@@ -6485,12 +6547,12 @@ export class SqliteScopeHost {
|
|
|
6485
6547
|
// Tombstone (K-21), never DELETE: a repeat revoke neither moves the timestamp nor writes
|
|
6486
6548
|
// a second audit row. One transaction with the removal fence (#1184), which a no-op
|
|
6487
6549
|
// raises too.
|
|
6488
|
-
this.directory.transaction(() =>
|
|
6550
|
+
this.directory.transaction(() => leaveOrgAndFence(actor, tenantId, principal, orgId))();
|
|
6489
6551
|
},
|
|
6490
6552
|
listMembers: async (actor, tenantId, orgId, options) => {
|
|
6491
6553
|
requireOrg(tenantId, orgId);
|
|
6492
6554
|
const rows = this.directory
|
|
6493
|
-
.prepare(`SELECT subject, revoked_at FROM _substrat_tenant_tuples
|
|
6555
|
+
.prepare(`SELECT subject, revoked_at, expires_at FROM _substrat_tenant_tuples
|
|
6494
6556
|
WHERE tenant_id = ? AND relation = 'member' AND object = ?
|
|
6495
6557
|
${options?.includeRevoked ? '' : 'AND revoked_at IS NULL'}
|
|
6496
6558
|
ORDER BY subject`)
|
|
@@ -6500,6 +6562,7 @@ export class SqliteScopeHost {
|
|
|
6500
6562
|
principal: r.subject.slice('principal:'.length),
|
|
6501
6563
|
orgId,
|
|
6502
6564
|
revokedAt: r.revoked_at,
|
|
6565
|
+
expiresAt: r.expires_at,
|
|
6503
6566
|
}));
|
|
6504
6567
|
},
|
|
6505
6568
|
createTenant: async (actor, input) => {
|
|
@@ -7089,6 +7152,8 @@ export class SqliteScopeHost {
|
|
|
7089
7152
|
'_substrat_membership_fences', // #1184: the latest removal, per principal
|
|
7090
7153
|
'_substrat_peer_switches', // #2029: the peer switch's record, per scope
|
|
7091
7154
|
'_substrat_switch_owed', // #2045: subjects owed a re-assert, per scope
|
|
7155
|
+
'_substrat_findings', // #1748: the tenant's findings
|
|
7156
|
+
'_substrat_finding_rules', // #1748: the tenant's suppress rules
|
|
7092
7157
|
];
|
|
7093
7158
|
const clear = this.directory.transaction(() => {
|
|
7094
7159
|
for (const table of tables) {
|
|
@@ -7590,6 +7655,11 @@ export class SqliteScopeHost {
|
|
|
7590
7655
|
const { tenantId, scopeId, ...after } = ownerTransferAudit.parse(entry);
|
|
7591
7656
|
this.recordAdmin(actor, 'transferOwner', { tenantId, scopeId }, null, after);
|
|
7592
7657
|
},
|
|
7658
|
+
/** #1150: one phase of a dashboard member change, written around the vertical's own. */
|
|
7659
|
+
recordMemberChange: async (actor, entry) => {
|
|
7660
|
+
const { tenantId, scopeId, ...after } = memberChangeAudit.parse(entry);
|
|
7661
|
+
this.recordAdmin(actor, 'manageScopeMember', { tenantId, scopeId }, null, after);
|
|
7662
|
+
},
|
|
7593
7663
|
/** #2005: one change to a scope's copy marker, written around the vertical's own change. */
|
|
7594
7664
|
recordCopyMark: async (actor, entry) => {
|
|
7595
7665
|
const { tenantId, scopeId, action, ...after } = copyMarkAudit.parse(entry);
|
|
@@ -7732,40 +7802,49 @@ export class SqliteScopeHost {
|
|
|
7732
7802
|
recordOpsFailure: async (entry) => {
|
|
7733
7803
|
const at = new Date().toISOString();
|
|
7734
7804
|
const fingerprint = opsFailureFingerprint(entry);
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
.
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
|
|
7805
|
+
// #1748: the evidence, its issue and its finding in ONE transaction — a detector that
|
|
7806
|
+
// throws takes the evidence with it, so the caller's retry observes it rather than finding
|
|
7807
|
+
// the row already there and the observation lost.
|
|
7808
|
+
this.directory.transaction(() => {
|
|
7809
|
+
this.directory
|
|
7810
|
+
.prepare(`INSERT INTO _substrat_ops_failures
|
|
7811
|
+
(id, actor, operation, stage, tenant_id, scope_id, vertical, version, status, message, reference, origin, code, fingerprint, at)
|
|
7812
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
7813
|
+
.run(ulid(), entry.actor, entry.operation, entry.stage ?? null, entry.tenantId ?? null, entry.scopeId ?? null, entry.vertical ?? null, entry.version ?? null, entry.status ?? null,
|
|
7814
|
+
// Bounded here, not trusted from the catch site: one runaway upstream
|
|
7815
|
+
// body must not become a runaway directory row (#559).
|
|
7816
|
+
entry.message.slice(0, 2000), entry.reference ?? null, entry.origin ?? null, entry.code ?? null, fingerprint, at);
|
|
7817
|
+
// Prune-on-write (#559): retention lives here, not in a cron — every insert
|
|
7818
|
+
// pays for its own housekeeping, so the table stays bounded even where no
|
|
7819
|
+
// scheduled pass runs (this adapter has none).
|
|
7820
|
+
const horizon = new Date(Date.now() - OPS_FAILURE_RETENTION_DAYS * 86_400_000).toISOString();
|
|
7821
|
+
this.directory.prepare('DELETE FROM _substrat_ops_failures WHERE at < ?').run(horizon);
|
|
7822
|
+
// The issues materialization (#1233): the group's counters live on their
|
|
7823
|
+
// own row, bumped in the same call, because the evidence self-prunes above
|
|
7824
|
+
// and a count must survive its own exemplars. A fresh arrival regresses a
|
|
7825
|
+
// resolved issue; an ignored one stays ignored — that is what ignoring means.
|
|
7826
|
+
this.directory
|
|
7827
|
+
.prepare(`INSERT INTO _substrat_issues
|
|
7828
|
+
(fingerprint, operation, stage, origin, code, status, seen_count, first_seen, last_seen, last_message, last_tenant_id, last_owner_kind, last_vertical, last_version, resolved_version, resolved_at)
|
|
7829
|
+
VALUES (?, ?, ?, ?, ?, 'new', 1, ?, ?, ?, ?, ?, ?, ?, NULL, NULL)
|
|
7830
|
+
ON CONFLICT (fingerprint) DO UPDATE SET
|
|
7831
|
+
seen_count = seen_count + 1,
|
|
7832
|
+
last_seen = excluded.last_seen,
|
|
7833
|
+
last_message = excluded.last_message,
|
|
7834
|
+
last_tenant_id = excluded.last_tenant_id,
|
|
7835
|
+
last_owner_kind = excluded.last_owner_kind,
|
|
7836
|
+
last_vertical = COALESCE(excluded.last_vertical, last_vertical),
|
|
7837
|
+
last_version = COALESCE(excluded.last_version, last_version),
|
|
7838
|
+
origin = COALESCE(excluded.origin, origin),
|
|
7839
|
+
status = CASE WHEN status = 'resolved' THEN 'regressed' ELSE status END`)
|
|
7840
|
+
.run(fingerprint, entry.operation, entry.stage ?? null, entry.origin ?? null, entry.code ?? null, at, at, entry.message.slice(0, 2000), entry.tenantId ?? null, issueExemplarOwner(entry.tenantId ?? null), entry.vertical ?? null, entry.version ?? null);
|
|
7841
|
+
const issueHorizon = new Date(Date.now() - ISSUE_RETENTION_DAYS * 86_400_000).toISOString();
|
|
7842
|
+
this.directory.prepare('DELETE FROM _substrat_issues WHERE last_seen < ?').run(issueHorizon);
|
|
7843
|
+
// #1748: the tenant's Recurring finding, over the tenant's own occurrences only.
|
|
7844
|
+
const finding = findingOfOpsFailure(entry);
|
|
7845
|
+
if (finding)
|
|
7846
|
+
observeFinding(redactionSqlOf(this.directory), finding, at);
|
|
7847
|
+
})();
|
|
7769
7848
|
},
|
|
7770
7849
|
listOpsFailures: async (actor, filter) => {
|
|
7771
7850
|
const where = [];
|
|
@@ -7847,19 +7926,30 @@ export class SqliteScopeHost {
|
|
|
7847
7926
|
}));
|
|
7848
7927
|
},
|
|
7849
7928
|
recordSweepRun: async (entry) => {
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
|
|
7853
|
-
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
|
|
7858
|
-
|
|
7859
|
-
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7929
|
+
const at = entry.at ?? new Date().toISOString();
|
|
7930
|
+
// #1748: the row and its finding in ONE transaction. A replayed drain the unique index
|
|
7931
|
+
// ignores observes nothing, which is right only because the first arrival's observation
|
|
7932
|
+
// committed with it, or did not commit at all.
|
|
7933
|
+
this.directory.transaction(() => {
|
|
7934
|
+
const { changes } = this.directory
|
|
7935
|
+
.prepare(`INSERT OR IGNORE INTO _substrat_sweep_runs
|
|
7936
|
+
(id, kind, unit, outcome, tenant_id, scope_id, vertical, version, operation,
|
|
7937
|
+
connection_id, error, elapsed_ms, request_id, event_type, observed_at, platform_requests, at)
|
|
7938
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
7939
|
+
.run(ulid(), entry.kind, entry.unit, entry.outcome, entry.tenantId ?? null, entry.scopeId ?? null, entry.vertical ?? null, entry.version ?? null, entry.operation ?? null, entry.connectionId ?? null,
|
|
7940
|
+
// Bounded here, not trusted from the sweep: one runaway provider body
|
|
7941
|
+
// must not become a runaway directory row (the #559 rule).
|
|
7942
|
+
entry.error == null ? null : entry.error.slice(0, 2000), entry.elapsedMs ?? null, entry.requestId ?? null, entry.eventType ?? null, entry.observedAt ?? null, entry.platformRequests == null ? null : JSON.stringify(entry.platformRequests), at);
|
|
7943
|
+
// Prune-on-write, like ops failures and for the same reason: the table stays
|
|
7944
|
+
// bounded even on a deployment whose scheduled pass is broken.
|
|
7945
|
+
const horizon = new Date(Date.now() - SWEEP_RUN_RETENTION_DAYS * 86_400_000).toISOString();
|
|
7946
|
+
this.directory.prepare('DELETE FROM _substrat_sweep_runs WHERE at < ?').run(horizon);
|
|
7947
|
+
// #1748: a failed schedule (Invariant) or a stale freshness verdict (Drift). Only when the
|
|
7948
|
+
// row was written: a replayed drain the unique index ignored is not a second occurrence.
|
|
7949
|
+
const finding = changes > 0 ? findingOfSweepRun(entry) : null;
|
|
7950
|
+
if (finding)
|
|
7951
|
+
observeFinding(redactionSqlOf(this.directory), finding, at);
|
|
7952
|
+
})();
|
|
7863
7953
|
},
|
|
7864
7954
|
listSweepRuns: async (actor, filter) => {
|
|
7865
7955
|
const where = [];
|
|
@@ -7929,6 +8019,8 @@ export class SqliteScopeHost {
|
|
|
7929
8019
|
}
|
|
7930
8020
|
return pruned;
|
|
7931
8021
|
},
|
|
8022
|
+
// #1748: one transaction, so a stale resolution never lands without its audit row.
|
|
8023
|
+
pruneFindings: async (actor, limit) => this.auditedFindings(actor, (sql, audit) => pruneFindings(sql, Date.now(), limit, audit)),
|
|
7932
8024
|
listIssues: async (actor, filter) => {
|
|
7933
8025
|
const where = [];
|
|
7934
8026
|
const params = [];
|
|
@@ -7973,6 +8065,25 @@ export class SqliteScopeHost {
|
|
|
7973
8065
|
this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: existing.status }, { fingerprint, status: after.status });
|
|
7974
8066
|
return after;
|
|
7975
8067
|
},
|
|
8068
|
+
listFindings: async (actor, filter) => {
|
|
8069
|
+
const rows = listFindings(redactionSqlOf(this.directory), filter);
|
|
8070
|
+
this.recordAccess(actor, 'listFindings', { tenantId: filter?.tenantId ?? null }, filter, rows.length);
|
|
8071
|
+
return rows;
|
|
8072
|
+
},
|
|
8073
|
+
// #1748: every findings mutation runs in ONE transaction with its audit row (`audited`).
|
|
8074
|
+
setFindingStatus: async (actor, tenantId, id, status) => this.auditedFindings(actor, (sql, audit) => setFindingStatus(sql, tenantId, id, status, new Date().toISOString(), audit))
|
|
8075
|
+
?.after,
|
|
8076
|
+
createFindingRule: async (actor, tenantId, input) => {
|
|
8077
|
+
const { rule, suppressed } = this.auditedFindings(actor, (sql, audit) => createFindingRule(sql, tenantId, input, actor, new Date().toISOString(), audit));
|
|
8078
|
+
return { rule, suppressed: suppressed.length };
|
|
8079
|
+
},
|
|
8080
|
+
revokeFindingRule: async (actor, tenantId, ruleId) => this.auditedFindings(actor, (sql, audit) => revokeFindingRule(sql, tenantId, ruleId, new Date().toISOString(), audit))
|
|
8081
|
+
?.after,
|
|
8082
|
+
listFindingRules: async (actor, tenantId, filter) => {
|
|
8083
|
+
const rows = listFindingRules(redactionSqlOf(this.directory), tenantId, filter?.active ? new Date().toISOString() : undefined, filter?.limit);
|
|
8084
|
+
this.recordAccess(actor, 'listFindingRules', { tenantId }, filter, rows.length);
|
|
8085
|
+
return rows;
|
|
8086
|
+
},
|
|
7976
8087
|
recordModelUsage: async (input) => {
|
|
7977
8088
|
const l = input.line;
|
|
7978
8089
|
// INSERT OR IGNORE on the UNIQUE request id: the drain may replay a settled-then-
|
|
@@ -8314,6 +8425,10 @@ export class SqliteScopeHost {
|
|
|
8314
8425
|
* can never disagree about who acted (#97). `overrideActor` remains for the
|
|
8315
8426
|
* system-actor path, where the acting module is the honest answer.
|
|
8316
8427
|
*/
|
|
8428
|
+
/** The context a consumer or an import runs in: no operation, and the delivery it is for (#2055). */
|
|
8429
|
+
deliveryContext(rt, subject, overrideActor, delivery) {
|
|
8430
|
+
return this.operationContext(rt, subject, overrideActor, undefined, undefined, undefined, [], undefined, delivery);
|
|
8431
|
+
}
|
|
8317
8432
|
operationContext(rt, subject, overrideActor,
|
|
8318
8433
|
/** #458: per-invoke tally of `ctx.requestPlatform` calls; absent for consumer dispatch. */
|
|
8319
8434
|
signals,
|
|
@@ -8343,7 +8458,9 @@ export class SqliteScopeHost {
|
|
|
8343
8458
|
* instant its row write used, so the `capability.exercised` event's `occurredAt` and the
|
|
8344
8459
|
* row's `last_used_at` are the same value — a second clock read could disagree.
|
|
8345
8460
|
*/
|
|
8346
|
-
at = this.clock()
|
|
8461
|
+
at = this.clock(),
|
|
8462
|
+
/** #2055: the delivery a consumer's or an import's context runs for; absent otherwise. */
|
|
8463
|
+
delivery) {
|
|
8347
8464
|
// For a connection, system, capability or peer-vertical subject this carries THAT id so
|
|
8348
8465
|
// the type holds — it is not a person, and the event actor below says what it is instead.
|
|
8349
8466
|
const principal = subject.id;
|
|
@@ -8351,6 +8468,7 @@ export class SqliteScopeHost {
|
|
|
8351
8468
|
const relations = this.relations;
|
|
8352
8469
|
const searchPlans = this.searchPlans;
|
|
8353
8470
|
const listPlans = this.listPlans;
|
|
8471
|
+
const statePlans = this.statePlans;
|
|
8354
8472
|
// K-34: the checks that passed in THIS operation. The context is created per invoke
|
|
8355
8473
|
// (see buildStub), so this accumulates one operation's authorizations; `emit` snapshots
|
|
8356
8474
|
// whatever has passed up to that point. A system/override actor is unconditionally
|
|
@@ -8472,11 +8590,10 @@ export class SqliteScopeHost {
|
|
|
8472
8590
|
// about the process, so it never rides `DomainEvent` for module code to
|
|
8473
8591
|
// branch on; it exists for the observability joins the column serves.
|
|
8474
8592
|
this.versionId,
|
|
8475
|
-
// #1237:
|
|
8476
|
-
// the runtime
|
|
8477
|
-
//
|
|
8478
|
-
|
|
8479
|
-
rt.causedBy,
|
|
8593
|
+
// #1237: the delivery this context was built for, if any — passed in, never read
|
|
8594
|
+
// off the runtime or the host (#2055). A fact about the surrounding dispatch,
|
|
8595
|
+
// never envelope data module code could set or branch on.
|
|
8596
|
+
delivery?.causedBy ?? null,
|
|
8480
8597
|
// #1237: a fact about the surrounding CALL, like the version above — never
|
|
8481
8598
|
// envelope data module code could set or branch on.
|
|
8482
8599
|
rt.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
@@ -8485,7 +8602,11 @@ export class SqliteScopeHost {
|
|
|
8485
8602
|
tenantId: rt.tenantId,
|
|
8486
8603
|
scopeId: rt.scopeId,
|
|
8487
8604
|
principal,
|
|
8488
|
-
sql: guardSecrets(guardSqlLimits(scopedSql(rt.db, true
|
|
8605
|
+
sql: guardSecrets(guardSqlLimits(scopedSql(rt.db, true, statefulTablesOf(statePlans),
|
|
8606
|
+
// #119: after runtime DDL, the stateful tables must still carry what the kernel derived.
|
|
8607
|
+
statePlans.size
|
|
8608
|
+
? () => assertEntityStateIntact(spineSql(rt.db), statePlans, stateListIndexNames(listPlans.values()))
|
|
8609
|
+
: undefined)), minted),
|
|
8489
8610
|
now: () => at,
|
|
8490
8611
|
// #1746/#1747: the host stamps who and where; module code supplies only the template
|
|
8491
8612
|
// and its fields. The invocation id is read per call — it is set for the duration of
|
|
@@ -8494,7 +8615,7 @@ export class SqliteScopeHost {
|
|
|
8494
8615
|
tenantId: rt.tenantId,
|
|
8495
8616
|
scopeId: rt.scopeId,
|
|
8496
8617
|
operation: operation ?? null,
|
|
8497
|
-
invocationId: () => rt.invocationId ??
|
|
8618
|
+
invocationId: () => rt.invocationId ?? delivery?.invocationId ?? null,
|
|
8498
8619
|
// A consumer runs under the system override, so it logs as `system`.
|
|
8499
8620
|
principalKind: overrideActor ? 'system' : subject.kind,
|
|
8500
8621
|
// The string-safe redaction: `redactSecrets` parses its serialization back, and a
|
|
@@ -8555,7 +8676,9 @@ export class SqliteScopeHost {
|
|
|
8555
8676
|
const plan = searchPlans.get(entityType);
|
|
8556
8677
|
if (!plan)
|
|
8557
8678
|
throw new NotSearchable(entityType);
|
|
8558
|
-
const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit)
|
|
8679
|
+
const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit),
|
|
8680
|
+
// #119: active rows unless the archive is asked for.
|
|
8681
|
+
searchStateWhere(statePlans, entityType, options?.view));
|
|
8559
8682
|
return rt.db.prepare(q.sql).all(...q.params).map((row) => ({ entityType, id: row.id, rank: row.rank }));
|
|
8560
8683
|
},
|
|
8561
8684
|
/**
|
|
@@ -8576,13 +8699,15 @@ export class SqliteScopeHost {
|
|
|
8576
8699
|
order: params.order,
|
|
8577
8700
|
cursor: params.cursor,
|
|
8578
8701
|
filters: params.filters,
|
|
8702
|
+
// #119: the bin is the checked reader's, never this one's.
|
|
8703
|
+
view: uncheckedView('ctx.page', entityType, params.view),
|
|
8579
8704
|
});
|
|
8580
8705
|
const rows = rt.db.prepare(q.sql).all(...q.params);
|
|
8581
8706
|
// A FULL page may have more; a short one is the end of the walk. Same rule
|
|
8582
8707
|
// as `pageOf`, applied here because the cursor is a COLUMN's value (plus
|
|
8583
8708
|
// the tie-break) rather than a field the caller could name.
|
|
8584
8709
|
const last = rows.length >= limit ? rows[rows.length - 1] : undefined;
|
|
8585
|
-
const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order);
|
|
8710
|
+
const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order, q.view);
|
|
8586
8711
|
const page = { entries: rows, nextCursor };
|
|
8587
8712
|
if (!params.total)
|
|
8588
8713
|
return page;
|
|
@@ -8661,6 +8786,24 @@ export class SqliteScopeHost {
|
|
|
8661
8786
|
emit: (event) => writeEvent(event, 'kernel'),
|
|
8662
8787
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
8663
8788
|
}),
|
|
8789
|
+
// #119: archive and trash, written once in the kernel. The raw seam, because the guarded
|
|
8790
|
+
// `ctx.sql` refuses the very columns these write; the operation's own check, so the
|
|
8791
|
+
// declared key's pass is one of its authorizations.
|
|
8792
|
+
...createEntityStateVerbs({
|
|
8793
|
+
sql: spineSql(rt.db),
|
|
8794
|
+
plans: statePlans,
|
|
8795
|
+
now: at,
|
|
8796
|
+
check: runCheck,
|
|
8797
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
8798
|
+
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
8799
|
+
}),
|
|
8800
|
+
...createTrashedReads({
|
|
8801
|
+
query: (q, params) => rt.db.prepare(q).all(...params),
|
|
8802
|
+
listPlans,
|
|
8803
|
+
searchPlans,
|
|
8804
|
+
statePlans,
|
|
8805
|
+
check: runCheck,
|
|
8806
|
+
}),
|
|
8664
8807
|
// #304: the request-time entitlement read. The pure adapter is single-process, so the
|
|
8665
8808
|
// directory is local — no projection needed; it reads `_substrat_entitlements` straight,
|
|
8666
8809
|
// with the same expiry-at-read filter as the gate (#33), and the SAME contract the DO
|
|
@@ -9049,9 +9192,7 @@ export class SqliteScopeHost {
|
|
|
9049
9192
|
actor: new ScopeActor(),
|
|
9050
9193
|
appliedMigrations,
|
|
9051
9194
|
mintEventId,
|
|
9052
|
-
causedBy: null,
|
|
9053
9195
|
invocationId: null,
|
|
9054
|
-
unitInvocationId: null,
|
|
9055
9196
|
};
|
|
9056
9197
|
this.scopes.set(key, created);
|
|
9057
9198
|
this.scopesById.set(scopeId, created);
|
|
@@ -9116,7 +9257,11 @@ function assertTablesWithinColumnLimit(db) {
|
|
|
9116
9257
|
if (wide)
|
|
9117
9258
|
throw new Error(tooManyTableColumns(wide.name, wide.width));
|
|
9118
9259
|
}
|
|
9119
|
-
function scopedSql(db, judgeWidth = false
|
|
9260
|
+
function scopedSql(db, judgeWidth = false,
|
|
9261
|
+
/** #119: the module tables carrying archive/trash columns — `guardSpine` refuses positional writes to them. */
|
|
9262
|
+
statefulTables,
|
|
9263
|
+
/** #119: the kernel's integrity check, run after any runtime DDL — see `guardSpine`. */
|
|
9264
|
+
afterDdl) {
|
|
9120
9265
|
// #1811: a Durable Object refuses a result set over its column cap, `exec` of a SELECT included.
|
|
9121
9266
|
// The driver knows the width, so it is read off the prepared statement rather than parsed out
|
|
9122
9267
|
// of the text (a `SELECT *` is as wide as the tables under it). Module SQL only, like the
|
|
@@ -9143,7 +9288,7 @@ function scopedSql(db, judgeWidth = false) {
|
|
|
9143
9288
|
assertTablesWithinColumnLimit(db);
|
|
9144
9289
|
return { changes: info.changes };
|
|
9145
9290
|
},
|
|
9146
|
-
});
|
|
9291
|
+
}, statefulTables, afterDdl);
|
|
9147
9292
|
}
|
|
9148
9293
|
const schemaVersion = (db) => db.pragma('schema_version', { simple: true });
|
|
9149
9294
|
//# sourceMappingURL=index.js.map
|