@substrat-run/adapter-sqlite 0.140.0 → 0.142.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 +31 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +349 -39
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, 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, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, 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, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, 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, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, 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, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, 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, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, 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, resolvePeerInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditWarningOf, 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, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, 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';
|
|
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, isSubstratError, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, 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, principalBecomeCapabilityInput, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, 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, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, 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, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, 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, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, 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, resolvePeerInstanceFrom, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditWarningOf, 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, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, 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
6
|
import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
|
|
7
7
|
import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
|
|
8
8
|
import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
|
|
9
|
+
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, scopeScriptCopyOf, } from '@substrat-run/kernel';
|
|
9
10
|
import { GRANT_CHILDREN_INDEX_DDL, grantedEntitiesForContext } from '@substrat-run/kernel';
|
|
10
11
|
import { LEGACY_SCOPE_ROWS_BACKFILL, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
|
|
11
12
|
import { ScopeActor } from './actor.js';
|
|
@@ -810,6 +811,10 @@ export class SqliteScopeHost {
|
|
|
810
811
|
status TEXT NOT NULL DEFAULT 'active',
|
|
811
812
|
schema_version TEXT NOT NULL DEFAULT '0',
|
|
812
813
|
vertical_version_id TEXT,
|
|
814
|
+
erasure_epoch INTEGER,
|
|
815
|
+
-- #1722: set once a reap has started draining this scope's script copies, and never
|
|
816
|
+
-- cleared: only deleting the row ends it. While set, no copy move is recorded or bound.
|
|
817
|
+
reap_claimed_at TEXT,
|
|
813
818
|
provisioned_version_id TEXT,
|
|
814
819
|
-- Last FAILED migration attempt (§5.3). All null / 0 = healthy. Written on
|
|
815
820
|
-- the failure path so a scope that fails closed stops rendering as active;
|
|
@@ -831,8 +836,29 @@ export class SqliteScopeHost {
|
|
|
831
836
|
-- When the scope last entered the archived state (§4.4). Drives the reap sweep's
|
|
832
837
|
-- age filter; null for scopes that never archived, cleared on unarchive.
|
|
833
838
|
archived_at TEXT,
|
|
839
|
+
-- #1524: the status it was archived FROM. 'provisioning' = it never held data, so the
|
|
840
|
+
-- storage gauge neither reads nor counts it. Written with archived_at, cleared with it.
|
|
841
|
+
archived_from_status TEXT,
|
|
834
842
|
created_at TEXT NOT NULL
|
|
835
843
|
);
|
|
844
|
+
-- #1722: additive copy inventory; historical audit backfill is a separate act.
|
|
845
|
+
CREATE TABLE IF NOT EXISTS scope_script_copies (
|
|
846
|
+
tenant_id TEXT NOT NULL,
|
|
847
|
+
scope_id TEXT NOT NULL,
|
|
848
|
+
script_ref TEXT NOT NULL,
|
|
849
|
+
move_id TEXT NOT NULL,
|
|
850
|
+
state TEXT NOT NULL,
|
|
851
|
+
load_stamp TEXT,
|
|
852
|
+
revision TEXT,
|
|
853
|
+
last_attempt_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')),
|
|
854
|
+
-- 'source' | 'destination'. A pending entry belongs to its move until lease_until; a
|
|
855
|
+
-- sweep that claims it after that sets lease_owner (kernel scope-copy-ledger.ts).
|
|
856
|
+
role TEXT,
|
|
857
|
+
lease_until TEXT,
|
|
858
|
+
lease_owner TEXT,
|
|
859
|
+
PRIMARY KEY (tenant_id, scope_id, script_ref, move_id)
|
|
860
|
+
);
|
|
861
|
+
CREATE INDEX IF NOT EXISTS scope_script_copies_state ON scope_script_copies (state, scope_id);
|
|
836
862
|
-- Private host state. Module SQL opens only the scope file, never this directory;
|
|
837
863
|
-- neither table is included in directory exports or scope dumps (#2074).
|
|
838
864
|
CREATE TABLE IF NOT EXISTS private_continuation_keys (
|
|
@@ -1358,6 +1384,8 @@ export class SqliteScopeHost {
|
|
|
1358
1384
|
);
|
|
1359
1385
|
CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant ON _substrat_model_usage (tenant_id, at);
|
|
1360
1386
|
CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
|
|
1387
|
+
-- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
|
|
1388
|
+
${SCOPE_STORAGE_DDL}
|
|
1361
1389
|
CREATE INDEX IF NOT EXISTS scopes_tenant ON scopes (tenant_id, scope_id);
|
|
1362
1390
|
`);
|
|
1363
1391
|
this.ensureDirectoryColumns();
|
|
@@ -2113,7 +2141,7 @@ export class SqliteScopeHost {
|
|
|
2113
2141
|
const actingAs = (operation) => authority.kind === 'capability-session'
|
|
2114
2142
|
? {
|
|
2115
2143
|
kind: 'capability',
|
|
2116
|
-
id: resolveCapabilitySession(spineSql(rt.db), authority.hash, this.clock(), operation),
|
|
2144
|
+
id: resolveCapabilitySession(spineSql(rt.db), authority.hash, this.clock(), operation, true),
|
|
2117
2145
|
}
|
|
2118
2146
|
: authority;
|
|
2119
2147
|
const node = { tenantId: rt.tenantId, scopeId: rt.scopeId };
|
|
@@ -2547,6 +2575,7 @@ export class SqliteScopeHost {
|
|
|
2547
2575
|
forgetSwitchesOf(switchSqlOf(this.directory), scopeId);
|
|
2548
2576
|
this.directory.prepare('DELETE FROM private_continuation_positions WHERE scope_id = ?').run(scopeId);
|
|
2549
2577
|
this.directory.prepare('DELETE FROM private_continuation_keys WHERE scope_id = ?').run(scopeId);
|
|
2578
|
+
forgetScopeStorage(redactionSqlOf(this.directory), scopeId);
|
|
2550
2579
|
this.directory.prepare('DELETE FROM scopes WHERE scope_id = ?').run(scopeId);
|
|
2551
2580
|
})();
|
|
2552
2581
|
this.recordAdmin(actor, 'deleteSnapshot', { tenantId, scopeId }, null, {
|
|
@@ -2688,14 +2717,14 @@ export class SqliteScopeHost {
|
|
|
2688
2717
|
// Same lifecycle gates as the principal door, and reached the same way: a
|
|
2689
2718
|
// suspended tenant refuses a support session exactly as it refuses a user.
|
|
2690
2719
|
const scope = this.directory
|
|
2691
|
-
.prepare('SELECT tenant_id
|
|
2720
|
+
.prepare('SELECT tenant_id FROM scopes WHERE scope_id = ?')
|
|
2692
2721
|
.get(scopeId);
|
|
2693
2722
|
if (!scope || scope.tenant_id !== tenantId) {
|
|
2694
2723
|
throw substratError('not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
2695
2724
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2725
|
+
const refusal = this.scopeWorkRefusal(tenantId, scopeId);
|
|
2726
|
+
if (refusal)
|
|
2727
|
+
throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
|
|
2699
2728
|
const rt = this.runtime(tenantId, scopeId);
|
|
2700
2729
|
await this.applyPendingMigrations(rt);
|
|
2701
2730
|
return this.buildStub(tenantId, scopeId, rt, asPrincipal(record.principal), options, record.id);
|
|
@@ -2717,14 +2746,14 @@ export class SqliteScopeHost {
|
|
|
2717
2746
|
throw substratError('not_found', `module not registered on this host: ${moduleId}`);
|
|
2718
2747
|
}
|
|
2719
2748
|
const scope = this.directory
|
|
2720
|
-
.prepare('SELECT tenant_id
|
|
2749
|
+
.prepare('SELECT tenant_id FROM scopes WHERE scope_id = ?')
|
|
2721
2750
|
.get(scopeId);
|
|
2722
2751
|
if (!scope || scope.tenant_id !== tenantId) {
|
|
2723
2752
|
throw substratError('not_found', `unknown scope: ${scopeId}`);
|
|
2724
2753
|
}
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2754
|
+
const refusal = this.scopeWorkRefusal(tenantId, scopeId);
|
|
2755
|
+
if (refusal)
|
|
2756
|
+
throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
|
|
2728
2757
|
const rt = this.runtime(tenantId, scopeId);
|
|
2729
2758
|
await this.applyPendingMigrations(rt);
|
|
2730
2759
|
return this.buildStub(tenantId, scopeId, rt, { kind: 'system', id: moduleId }, undefined, undefined, purge);
|
|
@@ -2989,6 +3018,20 @@ export class SqliteScopeHost {
|
|
|
2989
3018
|
await this.applyPendingMigrations(rt);
|
|
2990
3019
|
return rt;
|
|
2991
3020
|
}
|
|
3021
|
+
/** The directory's current hold, read again when background work is about to run. */
|
|
3022
|
+
scopeWorkRefusal(tenantId, scopeId, allowProvisioning = false) {
|
|
3023
|
+
const row = this.directory
|
|
3024
|
+
.prepare(`SELECT s.status AS scope, t.status AS tenant
|
|
3025
|
+
FROM scopes s JOIN tenants t ON t.tenant_id = s.tenant_id
|
|
3026
|
+
WHERE s.scope_id = ? AND s.tenant_id = ?`)
|
|
3027
|
+
.get(scopeId, tenantId);
|
|
3028
|
+
if (!row)
|
|
3029
|
+
return `unknown scope for tenant: (${tenantId}, ${scopeId})`;
|
|
3030
|
+
// A connector can serve provisioning work; suspension still holds it.
|
|
3031
|
+
if (allowProvisioning && row.scope === 'provisioning' && row.tenant === 'active')
|
|
3032
|
+
return null;
|
|
3033
|
+
return lifecycleRefusal(row, { tenantId, scopeId });
|
|
3034
|
+
}
|
|
2992
3035
|
/**
|
|
2993
3036
|
* The exchange (#1672) — a secret traded for a session, or for the principal a `become`
|
|
2994
3037
|
* capability yields. One serialized scope task and one transaction, so the use it takes
|
|
@@ -3011,6 +3054,7 @@ export class SqliteScopeHost {
|
|
|
3011
3054
|
// pseudo-operation name — the actor is the capability, as on every event it
|
|
3012
3055
|
// goes on to cause.
|
|
3013
3056
|
emit: (capability, event) => kernelEmit(this.operationContext(rt, { kind: 'capability', id: capability }, undefined, undefined, undefined, CAPABILITY_EXCHANGE_OPERATION, [], now), event),
|
|
3057
|
+
holdings: this.holdingsAt(tenantId, scopeId),
|
|
3014
3058
|
}, secret, options?.mode);
|
|
3015
3059
|
rt.db.exec('COMMIT');
|
|
3016
3060
|
}
|
|
@@ -3123,6 +3167,65 @@ export class SqliteScopeHost {
|
|
|
3123
3167
|
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
3124
3168
|
return rt.actor.turn(() => revokeScopeRoles(switchSqlOf(rt.db), scopeId, principalId.parse(principal), this.clock(), this.roleBound(caller, tenantId, scopeId), (run) => rt.db.transaction(run)()));
|
|
3125
3169
|
}
|
|
3170
|
+
/**
|
|
3171
|
+
* A principal's `become` mint (#1686) — a member invite's link. The kernel's `becomeMintCheck`
|
|
3172
|
+
* and the write in ONE scope turn and one transaction, so nothing the target or the caller
|
|
3173
|
+
* holds can move between the check and the mint; a refusal writes nothing. The mint is on the
|
|
3174
|
+
* spine as `capability.become-minted`, the caller its actor.
|
|
3175
|
+
*/
|
|
3176
|
+
async mintBecomeCapabilityBounded(tenantId, scopeId, caller, input) {
|
|
3177
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
3178
|
+
const minter = principalId.parse(caller);
|
|
3179
|
+
const parsed = principalBecomeCapabilityInput.parse(input);
|
|
3180
|
+
const outcome = await rt.actor.turn(async () => {
|
|
3181
|
+
const checked = await becomeMintCheck({ sql: spineSql(rt.db), checker: this.checker }, minter, parsed.principal, { tenantId, scopeId });
|
|
3182
|
+
if (!checked.ok)
|
|
3183
|
+
return checked;
|
|
3184
|
+
const now = this.clock();
|
|
3185
|
+
rt.db.exec('BEGIN IMMEDIATE');
|
|
3186
|
+
try {
|
|
3187
|
+
const minted = await mintBecomeCapabilityAsPrincipal({
|
|
3188
|
+
sql: spineSql(rt.db),
|
|
3189
|
+
now,
|
|
3190
|
+
emit: (event) => kernelEmit(this.operationContext(rt, asPrincipal(minter), undefined, undefined, undefined, CAPABILITY_BECOME_MINT_OPERATION, [], now), event),
|
|
3191
|
+
}, minter, parsed, checked.targetDigest);
|
|
3192
|
+
rt.db.exec('COMMIT');
|
|
3193
|
+
return { ok: true, minted };
|
|
3194
|
+
}
|
|
3195
|
+
catch (err) {
|
|
3196
|
+
rt.db.exec('ROLLBACK');
|
|
3197
|
+
throw err;
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
if (outcome.ok) {
|
|
3201
|
+
await this.dispatch(rt, null);
|
|
3202
|
+
await this.dispatchExecutors(rt, null);
|
|
3203
|
+
}
|
|
3204
|
+
return outcome;
|
|
3205
|
+
}
|
|
3206
|
+
/**
|
|
3207
|
+
* Revoke a `become` capability a principal minted (#1686) — withdrawing a member invite's
|
|
3208
|
+
* link, recorded with `by` as the revoker. `revoked` is false for anything else: a platform-minted
|
|
3209
|
+
* or an `act` capability is not this verb's. The kernel bounds `by` in the same turn: the link's
|
|
3210
|
+
* minter, or someone holding everything its principal holds now.
|
|
3211
|
+
*/
|
|
3212
|
+
async revokeBecomeCapability(tenantId, scopeId, capabilityId, by) {
|
|
3213
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
3214
|
+
return rt.actor.turn(() => revokeBecomeCapabilityAsPrincipal({ sql: spineSql(rt.db), checker: this.checker, node: { tenantId, scopeId }, now: this.clock() }, capabilityId, principalId.parse(by)));
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Where each named `become` link stands (#1686) — what a pending-invite list shows, so a link the
|
|
3218
|
+
* kernel revoked or that expired is never shown as open. A read in the scope's task.
|
|
3219
|
+
*/
|
|
3220
|
+
async becomeLinkStates(tenantId, scopeId, ids) {
|
|
3221
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
3222
|
+
return rt.actor.enqueue(() => readBecomeLinkStates(spineSql(rt.db), ids, this.clock(), this.holdingsAt(tenantId, scopeId)));
|
|
3223
|
+
}
|
|
3224
|
+
/** What a principal holds at this scope, for a `become` exchange (#1686); absent with a checker that cannot say. */
|
|
3225
|
+
holdingsAt(tenantId, scopeId) {
|
|
3226
|
+
const checker = this.checker;
|
|
3227
|
+
return checker.holdings ? (principal) => checker.holdings(asPrincipal(principal), { tenantId, scopeId }) : undefined;
|
|
3228
|
+
}
|
|
3126
3229
|
/** The caller's bound per role at the scope, `null` for a role this tenant does not define (#1150). */
|
|
3127
3230
|
roleBound(caller, tenantId, scopeId) {
|
|
3128
3231
|
const subject = asPrincipal(principalId.parse(caller));
|
|
@@ -3185,10 +3288,7 @@ export class SqliteScopeHost {
|
|
|
3185
3288
|
const report = { checks: [] };
|
|
3186
3289
|
if (!this.modules.has(moduleId))
|
|
3187
3290
|
return report;
|
|
3188
|
-
|
|
3189
|
-
.prepare('SELECT status FROM scopes WHERE scope_id = ? AND tenant_id = ?')
|
|
3190
|
-
.get(scopeId, tenantId);
|
|
3191
|
-
if (!scope || scope.status !== 'active')
|
|
3291
|
+
if (this.scopeWorkRefusal(tenantId, scopeId))
|
|
3192
3292
|
return report;
|
|
3193
3293
|
const rt = this.runtime(tenantId, scopeId);
|
|
3194
3294
|
await this.applyPendingMigrations(rt);
|
|
@@ -3265,10 +3365,7 @@ export class SqliteScopeHost {
|
|
|
3265
3365
|
// not this driver's problem to raise — it simply has nothing due (the sweep
|
|
3266
3366
|
// already enumerated `active` scopes; this guards a race where one archived
|
|
3267
3367
|
// between enumeration and here).
|
|
3268
|
-
|
|
3269
|
-
.prepare('SELECT status FROM scopes WHERE scope_id = ? AND tenant_id = ?')
|
|
3270
|
-
.get(scopeId, tenantId);
|
|
3271
|
-
if (!scope || scope.status !== 'active')
|
|
3368
|
+
if (this.scopeWorkRefusal(tenantId, scopeId))
|
|
3272
3369
|
return report;
|
|
3273
3370
|
const rt = this.runtime(tenantId, scopeId);
|
|
3274
3371
|
await this.applyPendingMigrations(rt);
|
|
@@ -3361,6 +3458,14 @@ export class SqliteScopeHost {
|
|
|
3361
3458
|
report.fired += 1;
|
|
3362
3459
|
}
|
|
3363
3460
|
catch (err) {
|
|
3461
|
+
// The system door can meet a suspension after the cadence gate. A refused
|
|
3462
|
+
// fire did not run, so leave its row due for the first live pass.
|
|
3463
|
+
if (isSubstratError(err) && err.code === 'conflict' && err.extensions.reason === SCOPE_GATE_REASONS.notActive) {
|
|
3464
|
+
report.lifecycleHeld = true;
|
|
3465
|
+
report.skipped += 1;
|
|
3466
|
+
report.runs.push({ operation: schedule.operation, outcome: 'skipped' });
|
|
3467
|
+
continue;
|
|
3468
|
+
}
|
|
3364
3469
|
status = 'failed';
|
|
3365
3470
|
failure = { error: err };
|
|
3366
3471
|
report.failed += 1;
|
|
@@ -3924,6 +4029,12 @@ export class SqliteScopeHost {
|
|
|
3924
4029
|
return report;
|
|
3925
4030
|
}
|
|
3926
4031
|
async dispatchExecutorsPass(rt, invocationId, report, lines, outcomes) {
|
|
4032
|
+
// A request that started while live may commit after a suspension. Its outbox
|
|
4033
|
+
// stays due without an attempt, just as a scheduled retry does.
|
|
4034
|
+
if (this.scopeWorkRefusal(rt.tenantId, rt.scopeId)) {
|
|
4035
|
+
report.lifecycleHeld = true;
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
3927
4038
|
const now = new Date().toISOString();
|
|
3928
4039
|
const scope = this.executorScope(rt);
|
|
3929
4040
|
// #2005: a non-primary scope (a fork, a snapshot, a preview of either kind) causes no
|
|
@@ -3947,6 +4058,10 @@ export class SqliteScopeHost {
|
|
|
3947
4058
|
ORDER BY o.id`)
|
|
3948
4059
|
.all(deliveryId, executor.eventType, now);
|
|
3949
4060
|
for (const row of rows) {
|
|
4061
|
+
if (this.scopeWorkRefusal(rt.tenantId, rt.scopeId)) {
|
|
4062
|
+
report.lifecycleHeld = true;
|
|
4063
|
+
return;
|
|
4064
|
+
}
|
|
3950
4065
|
report.attempted += 1;
|
|
3951
4066
|
// #1901: this attempt's line. Its number is the one the journal records, which
|
|
3952
4067
|
// `recordExecutorDelivery` answers — the Durable Object's RPC does the same.
|
|
@@ -4070,6 +4185,9 @@ export class SqliteScopeHost {
|
|
|
4070
4185
|
return isPrimaryScopeRow(this.directory.prepare('SELECT kind, forked_from FROM scopes WHERE scope_id = ?').get(scopeId));
|
|
4071
4186
|
}
|
|
4072
4187
|
async drainDue(tenantId, scopeId) {
|
|
4188
|
+
if (this.scopeWorkRefusal(tenantId, scopeId)) {
|
|
4189
|
+
return { attempted: 0, delivered: 0, retrying: 0, deadLettered: 0, lifecycleHeld: true };
|
|
4190
|
+
}
|
|
4073
4191
|
const rt = this.runtime(tenantId, scopeId);
|
|
4074
4192
|
await this.applyPendingMigrations(rt);
|
|
4075
4193
|
// #1525: null, and honestly so — a sweep is not a call. An attempt this pass makes
|
|
@@ -4254,13 +4372,11 @@ export class SqliteScopeHost {
|
|
|
4254
4372
|
this.attachmentExtractors, this.attachmentTextBounds);
|
|
4255
4373
|
}
|
|
4256
4374
|
async startJobRun(tenantId, scopeId, input) {
|
|
4257
|
-
const rt = this.
|
|
4258
|
-
await this.applyPendingMigrations(rt);
|
|
4375
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
4259
4376
|
return jobRunOf(await startJobRun(this.jobStore(rt), input, ulid, this.clock));
|
|
4260
4377
|
}
|
|
4261
4378
|
async runDueJobs(tenantId, scopeId, options) {
|
|
4262
|
-
const rt = this.
|
|
4263
|
-
await this.applyPendingMigrations(rt);
|
|
4379
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
4264
4380
|
// #1575: the kernel's extraction jobs are this host's own, bound to this scope — no
|
|
4265
4381
|
// deployment registers them, and none can shadow them. The backfill starts here, on
|
|
4266
4382
|
// the drive, so a scope's first request never pays for attachments that predate it.
|
|
@@ -4292,6 +4408,9 @@ export class SqliteScopeHost {
|
|
|
4292
4408
|
// directory, so a node control plane can drain routed intents exactly as the
|
|
4293
4409
|
// Cloudflare one does; the context build is the same one `dispatchExecutors` hands
|
|
4294
4410
|
// an in-process connector.
|
|
4411
|
+
const refusal = this.scopeWorkRefusal(tenantId, scopeId, true);
|
|
4412
|
+
if (refusal)
|
|
4413
|
+
throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
|
|
4295
4414
|
const rt = this.runtime(tenantId, scopeId);
|
|
4296
4415
|
// `false`: this path deliberately does NOT enqueue, so nothing is held and
|
|
4297
4416
|
// the connection's reads take an ordinary serialized turn. Built on a view bound to
|
|
@@ -5075,6 +5194,7 @@ export class SqliteScopeHost {
|
|
|
5075
5194
|
expiresAt: r.expires_at,
|
|
5076
5195
|
...(r.serving_ref ? { servingRef: r.serving_ref } : {}),
|
|
5077
5196
|
archivedAt: r.archived_at ?? null,
|
|
5197
|
+
archivedFromStatus: (r.archived_from_status ?? null),
|
|
5078
5198
|
createdAt: r.created_at,
|
|
5079
5199
|
});
|
|
5080
5200
|
// The (version, scope) pair a bind and its impact read both start from, and the refusals
|
|
@@ -5125,14 +5245,15 @@ export class SqliteScopeHost {
|
|
|
5125
5245
|
// Stamp/clear archived_at so the reap sweep can age scopes (§4.4). Entering
|
|
5126
5246
|
// `archived` records when; `unarchive` (→ active, a restore) clears it so a later
|
|
5127
5247
|
// re-archive dates from the new event; `reaped` keeps it as terminal history.
|
|
5248
|
+
// #1524: the status it left is recorded beside the stamp and cleared with it.
|
|
5128
5249
|
if (to === 'archived') {
|
|
5129
5250
|
this.directory
|
|
5130
|
-
.prepare('UPDATE scopes SET status = ?, archived_at = ? WHERE scope_id = ?')
|
|
5131
|
-
.run(to, new Date().toISOString(), scopeId);
|
|
5251
|
+
.prepare('UPDATE scopes SET status = ?, archived_at = ?, archived_from_status = ? WHERE scope_id = ?')
|
|
5252
|
+
.run(to, new Date().toISOString(), row.status, scopeId);
|
|
5132
5253
|
}
|
|
5133
5254
|
else if (to === 'active') {
|
|
5134
5255
|
this.directory
|
|
5135
|
-
.prepare('UPDATE scopes SET status = ?, archived_at = NULL WHERE scope_id = ?')
|
|
5256
|
+
.prepare('UPDATE scopes SET status = ?, archived_at = NULL, archived_from_status = NULL WHERE scope_id = ?')
|
|
5136
5257
|
.run(to, scopeId);
|
|
5137
5258
|
}
|
|
5138
5259
|
else {
|
|
@@ -5141,8 +5262,11 @@ export class SqliteScopeHost {
|
|
|
5141
5262
|
if (to === 'archived' || to === 'reaped') {
|
|
5142
5263
|
this.directory.prepare('UPDATE peer_bindings SET invalidated = 1 WHERE tenant_id = ? AND target_scope_id = ?').run(tenantId, scopeId);
|
|
5143
5264
|
}
|
|
5144
|
-
if (to === 'reaped')
|
|
5265
|
+
if (to === 'reaped') {
|
|
5145
5266
|
forgetSwitchesOf(switchSqlOf(this.directory), scopeId);
|
|
5267
|
+
// #1524: its storage is gone, so its samples describe nothing.
|
|
5268
|
+
forgetScopeStorage(redactionSqlOf(this.directory), scopeId);
|
|
5269
|
+
}
|
|
5146
5270
|
// The audit target carries the scope's vertical (control-plane.md §4.4:
|
|
5147
5271
|
// "vertical stays null until §4.2 lifecycle actions that name one"). It is
|
|
5148
5272
|
// read from the scope rather than passed in, so the trail cannot disagree
|
|
@@ -6449,6 +6573,11 @@ export class SqliteScopeHost {
|
|
|
6449
6573
|
if (opts?.expectedVersionId !== undefined && scope.vertical_version_id !== opts.expectedVersionId) {
|
|
6450
6574
|
throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
|
|
6451
6575
|
}
|
|
6576
|
+
const epoch = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
|
|
6577
|
+
.get(tenantId, scopeId);
|
|
6578
|
+
if (opts?.expectedErasureEpoch !== undefined && epoch?.erasure_epoch !== opts.expectedErasureEpoch) {
|
|
6579
|
+
throw substratError('precondition_failed', 'scope erasure changed; reload the scope and retry');
|
|
6580
|
+
}
|
|
6452
6581
|
const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
|
|
6453
6582
|
// #1756: an export an app in this tenant imports, dropped or re-versioned by what this
|
|
6454
6583
|
// scope would run. Before the snapshot, so a refused bind leaves nothing behind.
|
|
@@ -6466,12 +6595,7 @@ export class SqliteScopeHost {
|
|
|
6466
6595
|
await this.snapshotScope(actor, tenantId, scopeId);
|
|
6467
6596
|
}
|
|
6468
6597
|
}
|
|
6469
|
-
|
|
6470
|
-
? this.directory.prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ?')
|
|
6471
|
-
.run(versionId, v.verticalSlug, scopeId)
|
|
6472
|
-
: this.directory.prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ? AND vertical_version_id IS ?')
|
|
6473
|
-
.run(versionId, v.verticalSlug, scopeId, opts.expectedVersionId);
|
|
6474
|
-
if (update.changes === 0) {
|
|
6598
|
+
if (!this.moveScopeRow('vertical_version_id = ?, vertical = ?', [versionId, v.verticalSlug], tenantId, scopeId, opts)) {
|
|
6475
6599
|
throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
|
|
6476
6600
|
}
|
|
6477
6601
|
this.recordAdmin(actor, 'bindScopeVersion', { tenantId, scopeId }, null, {
|
|
@@ -6482,6 +6606,106 @@ export class SqliteScopeHost {
|
|
|
6482
6606
|
...(ack.exportBreak ? { acknowledged: ack } : {}),
|
|
6483
6607
|
});
|
|
6484
6608
|
},
|
|
6609
|
+
recordScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId, opts) => {
|
|
6610
|
+
if (!scriptRef || !moveId)
|
|
6611
|
+
throw substratError('conflict', 'copy script and move must name a real script');
|
|
6612
|
+
const result = this.directory.prepare(`INSERT INTO scope_script_copies (tenant_id, scope_id, script_ref, move_id, state, role, load_stamp, lease_until)
|
|
6613
|
+
SELECT tenant_id, scope_id, ?, ?, 'pending', ?, ?, ? FROM scopes
|
|
6614
|
+
WHERE tenant_id = ? AND scope_id = ? AND reap_claimed_at IS NULL
|
|
6615
|
+
ON CONFLICT (tenant_id, scope_id, script_ref, move_id) DO NOTHING`).run(scriptRef, moveId, opts?.role ?? null, opts?.loadStamp ?? null, new Date(Date.now() + (opts?.leaseMs ?? SCOPE_COPY_LEASE_MS)).toISOString(), tenantId, scopeId);
|
|
6616
|
+
if (result.changes > 0)
|
|
6617
|
+
return;
|
|
6618
|
+
const scope = this.directory.prepare('SELECT reap_claimed_at FROM scopes WHERE tenant_id = ? AND scope_id = ?')
|
|
6619
|
+
.get(tenantId, scopeId);
|
|
6620
|
+
if (!scope)
|
|
6621
|
+
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
6622
|
+
if (scope.reap_claimed_at !== null)
|
|
6623
|
+
throw substratError('precondition_failed', `scope ${scopeId} is being reaped`);
|
|
6624
|
+
},
|
|
6625
|
+
backfillScopeScriptCopy: async (actor, tenantId, scopeId, scriptRef, opts) => {
|
|
6626
|
+
if (!scriptRef)
|
|
6627
|
+
throw substratError('conflict', 'a backfilled copy must name a real script');
|
|
6628
|
+
// The audit row commits with the entry, stamped with the erasure epoch read in the same unit.
|
|
6629
|
+
return this.directory.transaction(() => {
|
|
6630
|
+
const written = this.directory.prepare(COPY_BACKFILL_SQL).run(...copyBackfillParams(tenantId, scopeId, scriptRef)).changes > 0;
|
|
6631
|
+
const scope = this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId);
|
|
6632
|
+
if (!written)
|
|
6633
|
+
return copyBackfillRefusal(scope);
|
|
6634
|
+
this.recordAdmin(actor, 'backfillScopeCopy', { tenantId, scopeId }, { erasureEpoch: scope.erasure_epoch }, { scriptRef, fromLogId: opts?.fromLogId ?? null });
|
|
6635
|
+
return 'recorded';
|
|
6636
|
+
})();
|
|
6637
|
+
},
|
|
6638
|
+
claimExpiredScopeScriptCopies: async (_actor, input) => {
|
|
6639
|
+
const limit = assertRowLimit('limit', input.limit);
|
|
6640
|
+
return this.directory.transaction(() => {
|
|
6641
|
+
const due = this.directory.prepare(COPY_EXPIRED_SQL).all(input.now, limit);
|
|
6642
|
+
const claimed = [];
|
|
6643
|
+
for (const r of due) {
|
|
6644
|
+
if (this.directory.prepare(COPY_CLAIM_SQL).run(input.owner, input.leaseUntil, r.tenant_id, r.scope_id, r.script_ref, r.move_id, input.now).changes === 0)
|
|
6645
|
+
continue;
|
|
6646
|
+
claimed.push(this.directory.prepare(`SELECT ${SCOPE_SCRIPT_COPY_COLUMNS} FROM scope_script_copies
|
|
6647
|
+
WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ?`).get(r.tenant_id, r.scope_id, r.script_ref, r.move_id));
|
|
6648
|
+
}
|
|
6649
|
+
return claimed.map(scopeScriptCopyOf);
|
|
6650
|
+
})();
|
|
6651
|
+
},
|
|
6652
|
+
beginScopeScriptReap: async (_actor, tenantId, scopeId) => {
|
|
6653
|
+
const result = this.directory.prepare(`UPDATE scopes SET reap_claimed_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
6654
|
+
WHERE tenant_id = ? AND scope_id = ? AND reap_claimed_at IS NULL
|
|
6655
|
+
AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS move
|
|
6656
|
+
WHERE move.tenant_id = scopes.tenant_id AND move.scope_id = scopes.scope_id
|
|
6657
|
+
AND move.state = 'pending')`).run(tenantId, scopeId);
|
|
6658
|
+
if (result.changes > 0)
|
|
6659
|
+
return;
|
|
6660
|
+
const scope = this.directory.prepare('SELECT reap_claimed_at FROM scopes WHERE tenant_id = ? AND scope_id = ?')
|
|
6661
|
+
.get(tenantId, scopeId);
|
|
6662
|
+
if (!scope)
|
|
6663
|
+
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
6664
|
+
if (scope.reap_claimed_at !== null)
|
|
6665
|
+
return; // a reaper resumes its earlier claim
|
|
6666
|
+
throw substratError('precondition_failed', `scope ${scopeId} has a copy move in flight; retry reap after it settles`);
|
|
6667
|
+
},
|
|
6668
|
+
settleScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId, state, marker, opts) => this.directory.transaction(() => {
|
|
6669
|
+
const settled = this.directory.prepare(`UPDATE scope_script_copies SET state = ?, load_stamp = ?, revision = ?,
|
|
6670
|
+
last_attempt_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
6671
|
+
WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ?
|
|
6672
|
+
AND (state <> 'done' OR ? = 'done')
|
|
6673
|
+
${opts?.claimedBy === undefined ? '' : "AND state = 'pending' AND lease_owner = ?"}`).run(state, marker?.loadStamp ?? null, marker?.revision ?? null, tenantId, scopeId, scriptRef, moveId, state, ...(opts?.claimedBy === undefined ? [] : [opts.claimedBy])).changes > 0;
|
|
6674
|
+
if (settled && state === 'done')
|
|
6675
|
+
this.directory.prepare(COPY_BACKFILL_SUPERSEDE_SQL).run(tenantId, scopeId);
|
|
6676
|
+
return settled;
|
|
6677
|
+
})(),
|
|
6678
|
+
touchScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId) => {
|
|
6679
|
+
this.directory.prepare(`UPDATE scope_script_copies SET last_attempt_at = strftime('%Y-%m-%dT%H:%M:%fZ','now')
|
|
6680
|
+
WHERE tenant_id = ? AND scope_id = ? AND script_ref = ? AND move_id = ? AND state = 'eligible'`).run(tenantId, scopeId, scriptRef, moveId);
|
|
6681
|
+
},
|
|
6682
|
+
listScopeScriptCopies: async (_actor, filter) => {
|
|
6683
|
+
const where = [];
|
|
6684
|
+
const args = [];
|
|
6685
|
+
if (filter.tenantId) {
|
|
6686
|
+
where.push('tenant_id = ?');
|
|
6687
|
+
args.push(filter.tenantId);
|
|
6688
|
+
}
|
|
6689
|
+
if (filter.scopeId) {
|
|
6690
|
+
where.push('scope_id = ?');
|
|
6691
|
+
args.push(filter.scopeId);
|
|
6692
|
+
}
|
|
6693
|
+
if (filter.state) {
|
|
6694
|
+
where.push('state = ?');
|
|
6695
|
+
args.push(filter.state);
|
|
6696
|
+
}
|
|
6697
|
+
if (filter.after) {
|
|
6698
|
+
if (!filter.tenantId || !filter.scopeId)
|
|
6699
|
+
throw substratError('validation_failed', 'copy cursor requires tenant and scope');
|
|
6700
|
+
where.push('(script_ref > ? OR (script_ref = ? AND move_id > ?))');
|
|
6701
|
+
args.push(filter.after.scriptRef, filter.after.scriptRef, filter.after.moveId);
|
|
6702
|
+
}
|
|
6703
|
+
const limit = assertRowLimit('limit', filter.limit ?? 100);
|
|
6704
|
+
const order = filter.scopeId ? 'script_ref, move_id' : 'last_attempt_at, tenant_id, scope_id, script_ref';
|
|
6705
|
+
const rows = this.directory.prepare(`SELECT ${SCOPE_SCRIPT_COPY_COLUMNS} FROM scope_script_copies ${where.length ? `WHERE ${where.join(' AND ')}` : ''}
|
|
6706
|
+
ORDER BY ${order} LIMIT ?`).all(...args, limit);
|
|
6707
|
+
return rows.map(scopeScriptCopyOf);
|
|
6708
|
+
},
|
|
6485
6709
|
/**
|
|
6486
6710
|
* Record that this scope's provision has now run against `versionId` (#1172).
|
|
6487
6711
|
* Written only after a provision or reconcile succeeded — a receipt, never a hope.
|
|
@@ -6559,9 +6783,9 @@ export class SqliteScopeHost {
|
|
|
6559
6783
|
if (breaks.length > 0)
|
|
6560
6784
|
throw substratError('precondition_failed', bindExportBreakRefusal(breaks));
|
|
6561
6785
|
}
|
|
6562
|
-
this.
|
|
6563
|
-
|
|
6564
|
-
|
|
6786
|
+
if (!this.moveScopeRow('serving_ref = ?', [servingRef], tenantId, scopeId, opts)) {
|
|
6787
|
+
throw substratError('precondition_failed', 'scope erasure or reap changed; reload and retry');
|
|
6788
|
+
}
|
|
6565
6789
|
this.recordAdmin(actor, 'setScopeServingRef', { tenantId, scopeId }, { servingRef: scope.serving_ref }, { servingRef, ...(ack.exportBreak ? { acknowledged: ack } : {}) });
|
|
6566
6790
|
},
|
|
6567
6791
|
recordKeptCopyResolution: async (actor, tenantId, scopeId, r) => {
|
|
@@ -7428,8 +7652,15 @@ export class SqliteScopeHost {
|
|
|
7428
7652
|
this.recordAccess(actor, 'openSubjectPayloads', { tenantId, scopeId }, { subjects: new Set(items.map((i) => i.subjectId)).size }, opened.filter((o) => o !== null).length);
|
|
7429
7653
|
return opened;
|
|
7430
7654
|
},
|
|
7431
|
-
|
|
7655
|
+
// `stamp` is the erasure epoch an orchestrated erasure's claim compared (#1722). A direct shred
|
|
7656
|
+
// claims nothing and records the epoch it ran under: the backfill orders erasures by them.
|
|
7657
|
+
shredSubject: async (actor, tenantId, scopeId, subjectId, stamp) => {
|
|
7432
7658
|
this.assertScope(tenantId, scopeId);
|
|
7659
|
+
const before = stamp ?? {
|
|
7660
|
+
erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
|
|
7661
|
+
?.erasure_epoch ?? 0,
|
|
7662
|
+
path: 'direct',
|
|
7663
|
+
};
|
|
7433
7664
|
// Redact the live spine FIRST. Both halves are idempotent and a crash between them
|
|
7434
7665
|
// converges on retry, so the order is decided by which half-done state harms the
|
|
7435
7666
|
// person: dying after this leaves ciphertext in a backup that no key opens; dying
|
|
@@ -7511,12 +7742,38 @@ export class SqliteScopeHost {
|
|
|
7511
7742
|
// BOTH logs, which is unusual and deliberate: the admin log because this is a
|
|
7512
7743
|
// mutation, the access log because it destroys evidence. An erasure is the one
|
|
7513
7744
|
// action where "who asked for this to disappear" is itself the record.
|
|
7514
|
-
this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId },
|
|
7745
|
+
this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, before, receipt);
|
|
7515
7746
|
// BOTH counts: the access log's number is "how much evidence this destroyed", and
|
|
7516
7747
|
// an intent payload is a whole event's worth of it.
|
|
7517
7748
|
this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, redacted.changes + intentsRedacted + jobRunsRedacted + idempotencyResults + moduleRowsErased(vertical));
|
|
7518
7749
|
return receipt;
|
|
7519
7750
|
},
|
|
7751
|
+
scopeErasureEpoch: async (_actor, tenantId, scopeId) => {
|
|
7752
|
+
// NULL on every row from before the column (#1722): the same 0 every CAS compares against.
|
|
7753
|
+
const row = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
|
|
7754
|
+
.get(tenantId, scopeId);
|
|
7755
|
+
if (!row)
|
|
7756
|
+
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
7757
|
+
return row.erasure_epoch;
|
|
7758
|
+
},
|
|
7759
|
+
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected) => {
|
|
7760
|
+
if (redactions.length === 0)
|
|
7761
|
+
throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
|
|
7762
|
+
const claimed = this.directory.prepare(`UPDATE scopes SET erasure_epoch = COALESCE(erasure_epoch, 0) + 1
|
|
7763
|
+
WHERE tenant_id = ? AND scope_id = ? AND vertical_version_id IS ?
|
|
7764
|
+
AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
|
|
7765
|
+
AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
|
|
7766
|
+
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
|
|
7767
|
+
AND copy.state = 'pending')
|
|
7768
|
+
AND (SELECT COUNT(*) FROM scope_script_copies AS copy
|
|
7769
|
+
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id) = ?`).run(tenantId, scopeId, expected.versionId, expected.servingRef, expected.epoch, expected.copyCount);
|
|
7770
|
+
if (claimed.changes === 0)
|
|
7771
|
+
throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
|
|
7772
|
+
// The pure adapter has one co-located scope store. Its existing atomic shred is
|
|
7773
|
+
// the final local redaction and key destruction, after remote copies confirmed.
|
|
7774
|
+
const shred = this.admin.shredSubject;
|
|
7775
|
+
return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' });
|
|
7776
|
+
},
|
|
7520
7777
|
// -- impersonation (K-42, #868) ----------------------------------------
|
|
7521
7778
|
beginImpersonation: async (actor, input) => {
|
|
7522
7779
|
// Fail closed on the scope before minting anything, exactly as the
|
|
@@ -7589,6 +7846,8 @@ export class SqliteScopeHost {
|
|
|
7589
7846
|
'_substrat_switch_owed', // #2045: subjects owed a re-assert, per scope
|
|
7590
7847
|
'_substrat_findings', // #1748: the tenant's findings
|
|
7591
7848
|
'_substrat_finding_rules', // #1748: the tenant's suppress rules
|
|
7849
|
+
'_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
|
|
7850
|
+
'_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
|
|
7592
7851
|
];
|
|
7593
7852
|
const clear = this.directory.transaction(() => {
|
|
7594
7853
|
for (const table of tables) {
|
|
@@ -7681,7 +7940,11 @@ export class SqliteScopeHost {
|
|
|
7681
7940
|
const reading = foldMeterReading({
|
|
7682
7941
|
readAt: instant.parse(new Date().toISOString()),
|
|
7683
7942
|
tenants: this.directory.prepare(`SELECT tenant_id, slug, status FROM tenants${where}`).all(...args).map((r) => ({ tenantId: r.tenant_id, slug: r.slug, status: r.status })),
|
|
7684
|
-
scopes: this.directory.prepare(`SELECT tenant_id, status FROM scopes${where}`).all(...args).map((r) => ({
|
|
7943
|
+
scopes: this.directory.prepare(`SELECT tenant_id, status, archived_from_status FROM scopes${where}`).all(...args).map((r) => ({
|
|
7944
|
+
tenantId: r.tenant_id,
|
|
7945
|
+
status: r.status,
|
|
7946
|
+
archivedFromStatus: r.archived_from_status,
|
|
7947
|
+
})),
|
|
7685
7948
|
entitlements: this.directory
|
|
7686
7949
|
.prepare(`SELECT tenant_id, entitlement_key, plan, expires_at FROM _substrat_entitlements${where}`)
|
|
7687
7950
|
.all(...args).map((r) => ({
|
|
@@ -7690,6 +7953,9 @@ export class SqliteScopeHost {
|
|
|
7690
7953
|
plan: r.plan,
|
|
7691
7954
|
expiresAt: r.expires_at,
|
|
7692
7955
|
})),
|
|
7956
|
+
// #1524: the stored gauge's latest sample per non-reaped scope. A directory read only.
|
|
7957
|
+
storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
|
|
7958
|
+
storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
|
|
7693
7959
|
});
|
|
7694
7960
|
// The count that matters for K-24 is how many TENANTS this reading covered —
|
|
7695
7961
|
// "read the meter for one tenant" and "metered the whole fleet" are different
|
|
@@ -8554,6 +8820,20 @@ export class SqliteScopeHost {
|
|
|
8554
8820
|
this.directory.prepare('DELETE FROM _substrat_model_usage WHERE at < ?').run(horizon);
|
|
8555
8821
|
return { recorded: res.changes > 0 };
|
|
8556
8822
|
},
|
|
8823
|
+
recordScopeStorage: async (_actor, readings) => ({
|
|
8824
|
+
recorded: this.directory.transaction(() => recordScopeStorageRows(redactionSqlOf(this.directory), readings))(),
|
|
8825
|
+
}),
|
|
8826
|
+
listScopeStorage: async (actor, filter) => {
|
|
8827
|
+
const rows = listScopeStorageRows(redactionSqlOf(this.directory), filter);
|
|
8828
|
+
this.recordAccess(actor, 'listScopeStorage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter ?? null, rows.length);
|
|
8829
|
+
return rows;
|
|
8830
|
+
},
|
|
8831
|
+
listScopeStorageAttempts: async (actor) => {
|
|
8832
|
+
const rows = listScopeStorageAttemptRows(redactionSqlOf(this.directory));
|
|
8833
|
+
this.recordAccess(actor, 'listScopeStorageAttempts', { tenantId: null }, null, rows.length);
|
|
8834
|
+
return rows;
|
|
8835
|
+
},
|
|
8836
|
+
pruneScopeStorage: async (_actor, limit) => pruneScopeStorageRows(redactionSqlOf(this.directory), Date.now(), limit),
|
|
8557
8837
|
listModelUsage: async (actor, filter) => {
|
|
8558
8838
|
const rows = this.selectModelUsage(filter);
|
|
8559
8839
|
this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
@@ -8581,6 +8861,28 @@ export class SqliteScopeHost {
|
|
|
8581
8861
|
* directory. `PRAGMA table_info` is available in the pure adapter (the DO adapter
|
|
8582
8862
|
* has to attempt-and-tolerate instead — see its `ensureDirectoryColumns`).
|
|
8583
8863
|
*/
|
|
8864
|
+
/**
|
|
8865
|
+
* Move a scope's binding or route (#1722), conditionally: on the binding the caller read, on
|
|
8866
|
+
* the erasure epoch its carry read (a NULL epoch, from before the column, is 0) with no reap
|
|
8867
|
+
* claimed, and on its copy move still owning a live lease. The move's confirmation commits in
|
|
8868
|
+
* the same transaction as the row. False when any condition failed and nothing was written.
|
|
8869
|
+
*/
|
|
8870
|
+
moveScopeRow(set, setArgs, tenantId, scopeId, cond) {
|
|
8871
|
+
const now = new Date().toISOString();
|
|
8872
|
+
return this.directory.transaction(() => {
|
|
8873
|
+
const moved = this.directory.prepare(`UPDATE scopes SET ${set} WHERE scope_id = ?
|
|
8874
|
+
${cond?.expectedVersionId === undefined ? '' : 'AND vertical_version_id IS ?'}
|
|
8875
|
+
${cond?.expectedErasureEpoch === undefined ? '' : 'AND COALESCE(erasure_epoch, 0) = ? AND reap_claimed_at IS NULL'}
|
|
8876
|
+
${cond?.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}`).run(...setArgs, scopeId, ...(cond?.expectedVersionId === undefined ? [] : [cond.expectedVersionId]), ...(cond?.expectedErasureEpoch === undefined ? [] : [cond.expectedErasureEpoch]), ...(cond?.confirmMove === undefined ? [] : copyMoveLiveParams(cond.confirmMove, now)));
|
|
8877
|
+
if (moved.changes === 0)
|
|
8878
|
+
return false;
|
|
8879
|
+
if (cond?.confirmMove) {
|
|
8880
|
+
this.directory.prepare(COPY_MOVE_CONFIRM_SQL).run(...copyMoveConfirmParams(cond.confirmMove, tenantId, scopeId, now));
|
|
8881
|
+
this.directory.prepare(COPY_BACKFILL_SUPERSEDE_SQL).run(tenantId, scopeId);
|
|
8882
|
+
}
|
|
8883
|
+
return true;
|
|
8884
|
+
})();
|
|
8885
|
+
}
|
|
8584
8886
|
ensureColumn(db, table, column, ddl) {
|
|
8585
8887
|
// Without case, as SQLite resolves a column name: a restore may have added this column
|
|
8586
8888
|
// already, untyped and spelled as a newer kernel's dump spelled it (#1883).
|
|
@@ -8767,6 +9069,8 @@ export class SqliteScopeHost {
|
|
|
8767
9069
|
// #1172: which version this scope's provision hook last ran against. Null on every
|
|
8768
9070
|
// existing row, which reads as "unknown, reconcile once" rather than "up to date".
|
|
8769
9071
|
this.ensureColumn(this.directory, 'scopes', 'provisioned_version_id', 'provisioned_version_id TEXT');
|
|
9072
|
+
this.ensureColumn(this.directory, 'scopes', 'erasure_epoch', 'erasure_epoch INTEGER');
|
|
9073
|
+
this.ensureColumn(this.directory, 'scopes', 'reap_claimed_at', 'reap_claimed_at TEXT');
|
|
8770
9074
|
// §4.8's grace-window timestamp on tenants (mirrors scopes' archived_at).
|
|
8771
9075
|
this.ensureColumn(this.directory, 'tenants', 'deleting_at', 'deleting_at TEXT');
|
|
8772
9076
|
this.ensureColumn(this.directory, 'tenants', 'provisioned_by_tenant', 'provisioned_by_tenant TEXT REFERENCES tenants(tenant_id)');
|
|
@@ -8853,6 +9157,7 @@ export class SqliteScopeHost {
|
|
|
8853
9157
|
['expires_at', 'expires_at TEXT'],
|
|
8854
9158
|
['serving_ref', 'serving_ref TEXT'],
|
|
8855
9159
|
['archived_at', 'archived_at TEXT'],
|
|
9160
|
+
['archived_from_status', 'archived_from_status TEXT'],
|
|
8856
9161
|
]) {
|
|
8857
9162
|
if (!existing.has(column))
|
|
8858
9163
|
this.directory.exec(`ALTER TABLE scopes ADD COLUMN ${ddl}`);
|
|
@@ -9617,6 +9922,11 @@ export class SqliteScopeHost {
|
|
|
9617
9922
|
// KERNEL_DDL is all IF NOT EXISTS, so a scope DB created before K-21 keeps the
|
|
9618
9923
|
// old shape — ALTER the tombstone in.
|
|
9619
9924
|
this.ensureColumn(db, '_substrat_tuples', 'revoked_at', 'revoked_at TEXT');
|
|
9925
|
+
// #2126: NULL keeps every existing capability's attachment behavior unchanged.
|
|
9926
|
+
this.ensureColumn(db, '_substrat_capabilities', 'attachments', 'attachments TEXT');
|
|
9927
|
+
// #1686: NULL on every row already there — none is a principal-minted become.
|
|
9928
|
+
this.ensureColumn(db, '_substrat_capabilities', 'target_digest', 'target_digest TEXT');
|
|
9929
|
+
this.ensureColumn(db, '_substrat_capabilities', 'revoked_reason', 'revoked_reason TEXT');
|
|
9620
9930
|
db.exec(GRANT_CHILDREN_INDEX_DDL);
|
|
9621
9931
|
// Executor retry state (#100), same reasoning: scopes provisioned before it
|
|
9622
9932
|
// already have the table. Defaults read as "terminal", which is exactly right
|