@substrat-run/adapter-sqlite 0.148.0 → 0.149.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.js CHANGED
@@ -1,11 +1,11 @@
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, assertNoBindingShapeBreak, 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, connectionBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
- import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, UPLOAD_LINK_DDL, createAttachmentVerbs, inspectUploadLink, redeemUploadLinkInTransaction, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, 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, listScopeStoreStorageRows, 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, peerCallerAt, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerVerticalSpellings, samePeerVertical, 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, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, forgetVerticalScopeStores, indexVersionScopeStores, kindDeclarationsOf, verticalScopeStoresOf, STORE_KIND_RELEASES_DDL, beginStoreKindRolloutIn, finishStoreKindRolloutIn, holdStoreKindIn, recordStoreKindReleaseIn, releaseStoreKindHoldIn, storeKindReleaseIn, storeKindStateIn, 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, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, 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, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf, } 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, assertNoBindingShapeBreak, 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, connectionBindingAudit, copyMarkAudit, orphanSweepAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
5
+ import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, UPLOAD_LINK_DDL, createAttachmentVerbs, inspectUploadLink, redeemUploadLinkInTransaction, redeemOrReplayUploadLink, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, 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, listScopeStoreStorageRows, 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, boundToPeerDeclarations, peerCallerAt, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerVerticalSpellings, samePeerVertical, 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, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, forgetVerticalScopeStores, indexVersionScopeStores, driverDeclarationsOf, verticalScopeStoresOf, STORE_DRIVER_RELEASES_DDL, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, recordStoreDriverReleaseIn, releaseStoreDriverHoldIn, storeDriverReleaseIn, storeDriverStateIn, storeDriverRolloutPendingIn, storeDriverRolloutWalkedIn, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, deriveVersionMigrationDigest, publishedMigrationDigest, migrationChangeRefusal, 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, migrationSetDivergence, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, ATTACHMENT_CONDEMNED_DDL, condemnOrphanAttachments, fenceAttachmentSweep, refuseCondemnedAttachment, refuseCondemnedInDump, rowlessAttachmentIds, sweepOrphanAttachments, missingAttachmentStore, SETTLED_COPY_STATES, unsettledCopyRefusal, 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, assertPromoteReviewCurrent, promoteRaceRefusal, rebindRaceRefusal, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, 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, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf, } 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
- import { INERT_SCOPE_REASON, isPrimaryScopeRow, UPLOAD_LINK_WRONG_TARGET } from '@substrat-run/kernel';
8
+ import { INERT_SCOPE_REASON, isPrimaryScopeRow, normalizeUploadMediaType } from '@substrat-run/kernel';
9
9
  import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, SUBJECT_ERASURE_CLAIM_SQL, subjectErasureClaimParams, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, scopeScriptCopyOf, } from '@substrat-run/kernel';
10
10
  import { GRANT_CHILDREN_INDEX_DDL, SHAPE_MARKER_INDEX_DDL, grantedEntitiesForContext } from '@substrat-run/kernel';
11
11
  import { LEGACY_SCOPE_ROWS_BACKFILL, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
@@ -322,11 +322,19 @@ const KERNEL_DDL = `
322
322
  -- #1575: attachment text and its FTS5 index, shared from @substrat-run/kernel. After
323
323
  -- _substrat_attachments, whose delete trigger it adds.
324
324
  ${ATTACHMENT_TEXT_DDL}
325
+ -- #2314: the orphan sweep's tombstones, shared from @substrat-run/kernel. An id here
326
+ -- never takes an attachment row again: the insert path refuses it.
327
+ ${ATTACHMENT_CONDEMNED_DDL}
325
328
  -- #901: an entity's version is the ULID of the last event about it (a state move
326
329
  -- excepted, #2191), so the newest id per (entity_type, entity_id) is the read. The id
327
330
  -- column sits last so SQLite walks the matched range backwards instead of aggregating.
328
331
  ${OUTBOX_ENTITY_INDEX}
329
332
  `;
333
+ /**
334
+ * The scope spine a fresh scope gets, for a tool that replays a vertical's migrations outside a
335
+ * host — `pnpm lint:migration-fence` (#115), whose reviewed spine migrations read and write it.
336
+ */
337
+ export const SCOPE_SPINE_DDL = KERNEL_DDL;
330
338
  /** Row → contract shape. Never reads the secrets table — that is the point of the split. */
331
339
  const toConnection = (r) => connection.parse({
332
340
  id: r.id,
@@ -698,11 +706,13 @@ export class SqliteScopeHost {
698
706
  secretBox;
699
707
  /** The parsers attachment text is extracted with (K-43); the host's own, never imported here. */
700
708
  attachmentExtractors;
709
+ wrapBlobStore;
701
710
  attachmentTextBounds;
702
711
  fetchImpl;
703
712
  connectorCalls;
704
713
  clock;
705
714
  versionId;
715
+ migrationsDigest;
706
716
  /** #1746: where `ctx.log` lines go — the console unless a caller (a test) says otherwise. */
707
717
  logSink;
708
718
  /** #1901: where async work's invocation lines go. */
@@ -716,11 +726,13 @@ export class SqliteScopeHost {
716
726
  this.secretBox = options.secretBox ?? unconfiguredSecretBox;
717
727
  assertAttachmentExtractors(options.attachmentExtractors ?? []);
718
728
  this.attachmentExtractors = options.attachmentExtractors ?? [];
729
+ this.wrapBlobStore = options.wrapBlobStore ?? ((store) => store);
719
730
  this.attachmentTextBounds = resolveAttachmentTextBounds(options.attachmentTextBounds);
720
731
  this.fetchImpl = options.fetch ?? globalFetch;
721
732
  this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
722
733
  this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
723
734
  this.versionId = options.versionId ?? null;
735
+ this.migrationsDigest = options.migrationsDigest;
724
736
  this.scopeStoreVertical = options.scopeStores?.vertical;
725
737
  this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
726
738
  this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
@@ -743,17 +755,17 @@ export class SqliteScopeHost {
743
755
  this.directory.pragma('journal_mode = WAL');
744
756
  this.ensureDirectorySchema();
745
757
  this.loadRoles();
746
- this.checker =
747
- customChecker ??
748
- createTupleChecker({
749
- directory: this.directory,
750
- scopeDb: (scopeId) => this.scopesById.get(scopeId)?.db,
751
- getRole: (tenantId, key) => this.roles.get(`${tenantId}/${key}`),
752
- // #956: the evaluator judges `expires_at` against the host's clock, so a
753
- // scripted one can actually expire a grant. A caller-supplied `checker`
754
- // keeps its own time source — this only binds the built-in one.
755
- clock: () => this.clock(),
756
- });
758
+ // #2263: whichever checker evaluates, a peer holds only what the running declaration names.
759
+ this.checker = boundToPeerDeclarations(customChecker ??
760
+ createTupleChecker({
761
+ directory: this.directory,
762
+ scopeDb: (scopeId) => this.scopesById.get(scopeId)?.db,
763
+ getRole: (tenantId, key) => this.roles.get(`${tenantId}/${key}`),
764
+ // #956: the evaluator judges `expires_at` against the host's clock, so a
765
+ // scripted one can actually expire a grant. A caller-supplied `checker`
766
+ // keeps its own time source — this only binds the built-in one.
767
+ clock: () => this.clock(),
768
+ }), () => this.peerDeclarations());
757
769
  this.admin = this.buildAdmin();
758
770
  this.owner = owner;
759
771
  }
@@ -942,8 +954,8 @@ export class SqliteScopeHost {
942
954
  created_at TEXT NOT NULL,
943
955
  PRIMARY KEY (tenant_id, vertical, binding)
944
956
  );
945
- -- A store kind's stored releases, rollouts and holds (D-63, #2300).
946
- ${STORE_KIND_RELEASES_DDL}
957
+ -- A store driver's stored releases, rollouts and holds (D-63, #2300).
958
+ ${STORE_DRIVER_RELEASES_DDL}
947
959
  -- The vertical + version registry (#31). A scope binds to a VERSION, so
948
960
  -- dev/staging/prod are the same vertical pinned differently, and a preview
949
961
  -- deployment is a version nothing has been promoted to yet.
@@ -1010,6 +1022,9 @@ export class SqliteScopeHost {
1010
1022
  migration_count INTEGER,
1011
1023
  -- 1 once the SQL is out of manifest_json. NULL = stored before #1764, not yet backfilled.
1012
1024
  migrations_split INTEGER,
1025
+ -- 1 once migration_digest is the one derived from the stored migrations (#2339).
1026
+ -- NULL = stored before, still the pushed digest; derived on its next read.
1027
+ migration_digest_derived INTEGER,
1013
1028
  created_at TEXT NOT NULL,
1014
1029
  UNIQUE (vertical_slug, version)
1015
1030
  );
@@ -1414,13 +1429,27 @@ export class SqliteScopeHost {
1414
1429
  * it inside its transaction onto an emptied directory, before loading any row.
1415
1430
  */
1416
1431
  buildDirectorySchema() {
1432
+ // A directory built before the store driver rename keeps its rows under the old names (and one
1433
+ // that ran pre-rename code again, a rollback, holds both names), and
1434
+ // `applyDirectorySchema` would only create empty tables beside them.
1435
+ const tables = this.directory.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`).all().map((t) => t.name);
1436
+ const hasRows = (table) => this.directory.prepare(`SELECT 1 FROM "${table}" LIMIT 1`).get() !== undefined;
1437
+ // Asked before the moves below, which drop a name with no rows.
1438
+ const bothNames = legacyStoreDriverConflict(tables, hasRows);
1439
+ if (bothNames)
1440
+ console.error(bothNames);
1441
+ for (const stmt of legacyStoreDriverRenames(tables, (table) => builtColumnsOf(this.directory, table), hasRows))
1442
+ this.directory.exec(stmt);
1417
1443
  // #1764: `VERSION_MIGRATIONS_DDL` indexes these two columns, so a directory created
1418
1444
  // before them needs them before `applyDirectorySchema` reaches that statement.
1419
1445
  if (this.directory.prepare('PRAGMA table_info(vertical_versions)').all().length > 0) {
1420
1446
  this.ensureColumn(this.directory, 'vertical_versions', 'migration_count', 'migration_count INTEGER');
1421
1447
  this.ensureColumn(this.directory, 'vertical_versions', 'migrations_split', 'migrations_split INTEGER');
1448
+ this.ensureColumn(this.directory, 'vertical_versions', 'migration_digest_derived', 'migration_digest_derived INTEGER');
1422
1449
  }
1423
1450
  this.applyDirectorySchema();
1451
+ // #2322: a migration's declared moves and discards, on a table created before they were carried.
1452
+ this.ensureColumn(this.directory, 'vertical_version_migrations', 'declarations', 'declarations TEXT');
1424
1453
  }
1425
1454
  /**
1426
1455
  * `applyDirectorySchema`, plus the backfill of the schedule switch's record (#1674) from
@@ -1682,6 +1711,7 @@ export class SqliteScopeHost {
1682
1711
  }
1683
1712
  // #1705: validated whole and kept only if it passes, so a refused module leaves nothing.
1684
1713
  this.crossVertical.register(manifest, registration.imports);
1714
+ this.peerUnion = undefined; // #2263: the next check reads the new module set's declarations
1685
1715
  this.modules.set(manifest.id, {
1686
1716
  id: manifest.id,
1687
1717
  // The order the kernel writes once (#1677): authored, then search, then list indexes.
@@ -2073,7 +2103,7 @@ export class SqliteScopeHost {
2073
2103
  out.push(r);
2074
2104
  }
2075
2105
  };
2076
- return {
2106
+ return this.wrapBlobStore({
2077
2107
  put: async (key, body, opts) => {
2078
2108
  const p = resolveKey(key);
2079
2109
  mkdirSync(dirname(p), { recursive: true });
@@ -2105,7 +2135,7 @@ export class SqliteScopeHost {
2105
2135
  walk(root, '', out);
2106
2136
  return out.filter((k) => k.startsWith(prefix)).sort();
2107
2137
  },
2108
- };
2138
+ }, ref);
2109
2139
  }
2110
2140
  /** Resolve the blob store that carries a scope's attachments (#473): the platform-minted
2111
2141
  * store for (tenant, the scope's vertical). Fails loudly when none was provisioned —
@@ -2148,30 +2178,90 @@ export class SqliteScopeHost {
2148
2178
  const uploadLinks = this.uploadLinkPolicies.get(entityType);
2149
2179
  return uploadLinks ? { write: gate.write, uploadLinks } : { write: gate.write };
2150
2180
  }
2151
- async inspectUploadLink(tenantId, scopeId, secret) {
2181
+ async inspectUploadLink(tenantId, scopeId, secret, target, idempotencyKey) {
2152
2182
  const rt = await this.openActiveScope(tenantId, scopeId);
2153
- return rt.actor.enqueue(async () => (await inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret)).view);
2183
+ return rt.actor.enqueue(async () => (await inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret, target, idempotencyKey)).view);
2184
+ }
2185
+ /**
2186
+ * #2314: one orphan-sweep pass over a scope's attachment bytes (`sweepOrphanAttachments` in the
2187
+ * kernel has the guards). Only an active PRIMARY scope of `tenantId` is judged: a fork or a
2188
+ * preview holds another scope's rows, never this one's.
2189
+ */
2190
+ async sweepOrphanAttachments(tenantId, scopeId, request = {}) {
2191
+ const row = this.directory
2192
+ .prepare('SELECT vertical, kind, forked_from FROM scopes WHERE scope_id = ? AND tenant_id = ?')
2193
+ .get(scopeId, tenantId);
2194
+ if (row && !isPrimaryScopeRow(row)) {
2195
+ throw substratError('conflict', `refused: scope ${scopeId} is a copy, and a copy may not judge orphan attachments`);
2196
+ }
2197
+ const copies = this.directory
2198
+ .prepare(`SELECT script_ref AS scriptRef, state FROM scope_script_copies
2199
+ WHERE tenant_id = ? AND scope_id = ? AND state NOT IN (${SETTLED_COPY_STATES.map(() => '?').join(', ')})`)
2200
+ .all(tenantId, scopeId, ...SETTLED_COPY_STATES);
2201
+ const unsettled = unsettledCopyRefusal(scopeId, copies);
2202
+ if (unsettled)
2203
+ throw unsettled;
2204
+ // Only an active scope: an archived or reaped one is not this pass's to judge.
2205
+ const rt = await this.openActiveScope(tenantId, scopeId);
2206
+ // A scope with no blob store holds no bytes: answered as that, marked, after every guard above,
2207
+ // so the platform's pass tells it from a failure and from a store that held nothing.
2208
+ const stores = row?.vertical
2209
+ ? this.directory.prepare('SELECT 1 FROM blob_stores WHERE tenant_id = ? AND vertical = ? LIMIT 1').get(tenantId, row.vertical)
2210
+ : undefined;
2211
+ if (!stores) {
2212
+ // A scope that holds an attachment row does keep files: its missing store is a misconfiguration.
2213
+ const holds = await rt.actor.enqueue(() => {
2214
+ const table = rt.db.prepare(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = '_substrat_attachments'`).get();
2215
+ return table !== undefined && rt.db.prepare('SELECT 1 FROM _substrat_attachments LIMIT 1').get() !== undefined;
2216
+ });
2217
+ if (holds)
2218
+ throw missingAttachmentStore(tenantId, scopeId);
2219
+ return { listed: 0, candidates: 0, orphans: 0, deleted: 0, keys: [], dryRun: request.dryRun === true, bucket: 'none' };
2220
+ }
2221
+ const store = this.attachmentStore(tenantId, row?.vertical ?? null);
2222
+ return sweepOrphanAttachments(store, scopeId, {
2223
+ rowless: async (ids) => rt.actor.enqueue(() => rowlessAttachmentIds(spineSql(rt.db), ids)),
2224
+ condemn: async (ids, now) => rt.actor.enqueue(() => rt.db.transaction(() => condemnOrphanAttachments(spineSql(rt.db), ids, now))()),
2225
+ }, {
2226
+ // The host's own clock, never a caller's: no request can step past the floor or the fence.
2227
+ now: this.clock(),
2228
+ ...(request.dryRun ? { dryRun: true } : {}),
2229
+ ...(request.limit !== undefined ? { limit: request.limit } : {}),
2230
+ });
2231
+ }
2232
+ /**
2233
+ * #2314: the orphan sweep's restore fence for a scope whose rows are about to be replaced. A
2234
+ * scope whose vertical has no blob store has no bytes, so nothing is fenced; a fence that cannot
2235
+ * be written throws, and the caller refuses the replacement.
2236
+ */
2237
+ fenceOrphanSweep(tenantId, scopeId, vertical) {
2238
+ const held = vertical
2239
+ ? this.directory.prepare('SELECT 1 FROM blob_stores WHERE tenant_id = ? AND vertical = ? LIMIT 1').get(tenantId, vertical)
2240
+ : undefined;
2241
+ if (!held)
2242
+ return Promise.resolve();
2243
+ return fenceAttachmentSweep(this.attachmentStore(tenantId, vertical), scopeId, this.clock());
2154
2244
  }
2155
2245
  async redeemUploadLink(tenantId, scopeId, secret, input) {
2156
2246
  const rt = await this.openActiveScope(tenantId, scopeId);
2157
- const { view, mintedBy } = await rt.actor.enqueue(() => inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret));
2158
- if (view.entity.entityType !== input.entity.entityType || view.entity.entityId !== input.entity.entityId) {
2159
- throw substratError('not_found', 'This upload link is not valid for that target.', {
2160
- reason: UPLOAD_LINK_WRONG_TARGET,
2247
+ // The target first (the kernel judges it before the state), so a wrong path never reads as
2248
+ // gone; a replay (#2315) is answered from the inspection and never reaches the write.
2249
+ const inspect = () => rt.actor.enqueue(() => inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret, input.entity, input.idempotencyKey));
2250
+ return redeemOrReplayUploadLink(inspect, input, async ({ view, mintedBy }) => {
2251
+ // The write is made AS the minter: the same gates `attachments()` puts in front of a person.
2252
+ await this.getScope(mintedBy, tenantId, scopeId);
2253
+ const scopeRow = this.directory
2254
+ .prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
2255
+ .get(scopeId);
2256
+ const files = this.buildAttachments(rt, asPrincipal(mintedBy), this.attachmentStore(tenantId, scopeRow?.vertical ?? null), { uploadLink: { secret, idempotencyKey: input.idempotencyKey } });
2257
+ return files.upload({
2258
+ entity: view.entity,
2259
+ filename: view.filename ?? input.filename,
2260
+ // Recorded as it is judged: the essence, never a parameter or a stray space.
2261
+ contentType: normalizeUploadMediaType(input.contentType),
2262
+ visibility: view.visibility,
2263
+ body: input.body,
2161
2264
  });
2162
- }
2163
- // The write is made AS the minter: the same gates `attachments()` puts in front of a person.
2164
- await this.getScope(mintedBy, tenantId, scopeId);
2165
- const scopeRow = this.directory
2166
- .prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
2167
- .get(scopeId);
2168
- const files = this.buildAttachments(rt, asPrincipal(mintedBy), this.attachmentStore(tenantId, scopeRow?.vertical ?? null), { uploadLinkSecret: secret });
2169
- return files.upload({
2170
- entity: view.entity,
2171
- filename: view.filename ?? input.filename,
2172
- contentType: input.contentType,
2173
- visibility: view.visibility,
2174
- body: input.body,
2175
2265
  });
2176
2266
  }
2177
2267
  async getSystemAttachments(moduleId, tenantId, scopeId) {
@@ -2314,6 +2404,8 @@ export class SqliteScopeHost {
2314
2404
  try {
2315
2405
  return await guarded('attachments.upload', async (ctx) => {
2316
2406
  assertAllowed(await ctx.check(gate.write, input.entity));
2407
+ // #2314: an id the orphan sweep condemned takes no row, in this same transaction.
2408
+ refuseCondemnedAttachment(spineSql(rt.db), record.id);
2317
2409
  rt.db
2318
2410
  .prepare(`INSERT INTO _substrat_attachments
2319
2411
  (id, entity_type, entity_id, filename, content_type, size, sha256,
@@ -2332,8 +2424,8 @@ export class SqliteScopeHost {
2332
2424
  enqueueAttachmentText(spineSql(rt.db), record.id, ulid(), this.clock());
2333
2425
  // An upload-link redemption spends its link HERE, in the same transaction as the
2334
2426
  // row it produced: a refusal anywhere above or below rolls the spend back.
2335
- if (opts.uploadLinkSecret !== undefined) {
2336
- await redeemUploadLinkInTransaction({ sql: spineSql(rt.db), now: this.clock(), emit: (event) => kernelEmit(ctx, event) }, opts.uploadLinkSecret, record);
2427
+ if (opts.uploadLink !== undefined) {
2428
+ await redeemUploadLinkInTransaction({ sql: spineSql(rt.db), now: this.clock(), emit: (event) => kernelEmit(ctx, event) }, opts.uploadLink.secret, record, opts.uploadLink.idempotencyKey);
2337
2429
  }
2338
2430
  return record;
2339
2431
  });
@@ -2473,6 +2565,16 @@ export class SqliteScopeHost {
2473
2565
  // rows are gone and the new ones are in. It also covers what a topological sort
2474
2566
  // cannot express: FK cycles, and self-referencing rows within one table.
2475
2567
  db.pragma('defer_foreign_keys = ON');
2568
+ // A dump written before indexes were excluded may still carry them; skip those
2569
+ // too rather than failing a restore over data that is about to be recomputed.
2570
+ // Matched without case, as SQLite resolves a table name (#1883 review).
2571
+ const replayable = dumped.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
2572
+ // A Durable Object's dump carries spine tables this adapter keeps in its directory, or does
2573
+ // not keep at all; they are skipped by name, and any other unknown spine table is refused.
2574
+ const loadable = replayable.filter((t) => !DO_SCOPE_ONLY_SPINE_TABLES.has(t.name.toLowerCase()));
2575
+ // #2314: a row for an id the orphan sweep condemned here is refused, as the insert refuses it.
2576
+ // Judged on `loadable`, the very tables whose rows the replay below inserts, before any drop.
2577
+ refuseCondemnedInDump(spineSql(db), loadable);
2476
2578
  // Drop the current schema: a vertical's tables are rebuilt from the dump's DDL, the
2477
2579
  // spine from KERNEL_DDL (below). Only real tables (never `sqlite_*` internals, which
2478
2580
  // are auto-managed and un-droppable).
@@ -2487,10 +2589,6 @@ export class SqliteScopeHost {
2487
2589
  continue;
2488
2590
  db.exec(`DROP TABLE IF EXISTS "${name}"`);
2489
2591
  }
2490
- // A dump written before indexes were excluded may still carry them; skip those
2491
- // too rather than failing a restore over data that is about to be recomputed.
2492
- // Matched without case, as SQLite resolves a table name (#1883 review).
2493
- const replayable = dumped.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
2494
2592
  // The dump is untrusted input (#1143): its names reach SQL as identifiers, and
2495
2593
  // `db.exec` would run every statement its `ddl` contains, not just the CREATE
2496
2594
  // TABLE. Judged as a whole before any of it executes — a check interleaved with
@@ -2502,9 +2600,6 @@ export class SqliteScopeHost {
2502
2600
  // contributes only rows, by column name (`dumpRowsInsert`). Otherwise a dump could
2503
2601
  // declare a column the checker compares on as COLLATE NOCASE. The column pass follows,
2504
2602
  // for the one outbox index KERNEL_DDL leaves to it.
2505
- // A Durable Object's dump carries spine tables this adapter keeps in its directory, or does
2506
- // not keep at all; they are skipped by name, and any other unknown spine table is refused.
2507
- const loadable = replayable.filter((t) => !DO_SCOPE_ONLY_SPINE_TABLES.has(t.name.toLowerCase()));
2508
2603
  // Comment-blanked (#2068), as the Durable Object replays it: the dump's DDL is
2509
2604
  // `sqlite_master.sql` verbatim, and `prepare` still compiles exactly one statement.
2510
2605
  for (const t of loadable)
@@ -2595,6 +2690,8 @@ export class SqliteScopeHost {
2595
2690
  if (!existing)
2596
2691
  throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
2597
2692
  this.refuseUnusableScopeStores(dump.stores, existing.vertical);
2693
+ // #2314: the load replaces the scope's rows, so the orphan sweep is fenced first.
2694
+ await this.fenceOrphanSweep(tenantId, scopeId, existing.vertical ?? null);
2598
2695
  // #1742: a dump from before a switch was pulled brings the module's grants back live, so
2599
2696
  // the directory's recorded-off modules go back off in the load's own turn, as a
2600
2697
  // provision's seat does — no schedule pass can run in between.
@@ -2828,7 +2925,7 @@ export class SqliteScopeHost {
2828
2925
  }
2829
2926
  /**
2830
2927
  * A dump's stores this host cannot load are refused before any write (#2211, L8): a
2831
- * binding that is not bound, a store with no kind or a kind other than the bound runner's,
2928
+ * binding that is not bound, a store with no driver or a driver other than the bound runner's,
2832
2929
  * any store at all for a destination with no vertical (no store object can be named for
2833
2930
  * it, so loading would drop them), or a destination on a vertical other than the one this
2834
2931
  * host is pinned to.
@@ -2848,11 +2945,11 @@ export class SqliteScopeHost {
2848
2945
  if (!runner) {
2849
2946
  throw substratError('not_found', `dump names scope store '${store.binding}', which is not bound`);
2850
2947
  }
2851
- if (!store.kind) {
2852
- throw substratError('validation_failed', `dump's scope store '${store.binding}' names no kind`);
2948
+ if (!store.driver) {
2949
+ throw substratError('validation_failed', `dump's scope store '${store.binding}' names no driver`);
2853
2950
  }
2854
- if (store.kind !== runner.kind) {
2855
- throw substratError('conflict', `dump's scope store '${store.binding}' is kind '${store.kind}', and this host binds kind '${runner.kind}'`);
2951
+ if (store.driver !== runner.driver) {
2952
+ throw substratError('conflict', `dump's scope store '${store.binding}' is driver '${store.driver}', and this host binds driver '${runner.driver}'`);
2856
2953
  }
2857
2954
  }
2858
2955
  }
@@ -2869,7 +2966,7 @@ export class SqliteScopeHost {
2869
2966
  if (!runner)
2870
2967
  throw substratError('not_found', `no scope store is bound for '${binding}'`);
2871
2968
  const exported = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.export');
2872
- stores.push({ binding, kind: runner.kind, tables: exported.tables });
2969
+ stores.push({ binding, driver: runner.driver, tables: exported.tables });
2873
2970
  }
2874
2971
  return stores;
2875
2972
  }
@@ -3456,10 +3553,15 @@ export class SqliteScopeHost {
3456
3553
  const store = this.attachmentStore(tenantId, scopeRow?.vertical ?? null);
3457
3554
  return this.buildAttachments(rt, { kind: 'capability-session', hash }, store);
3458
3555
  }
3459
- /** Every registered module's `peers`, as one declaration per peer vertical (#1706). */
3556
+ /**
3557
+ * Every registered module's `peers`, as one declaration per peer vertical (#1706). Built once
3558
+ * per registered module set and dropped by `registerModule` (#2263): the checker reads it on
3559
+ * every check, so it must be a lookup, never a rebuild.
3560
+ */
3460
3561
  peerDeclarations() {
3461
- return collectPeers(this.modules.values());
3562
+ return (this.peerUnion ??= collectPeers(this.modules.values()));
3462
3563
  }
3564
+ peerUnion;
3463
3565
  /**
3464
3566
  * The peer door (#1706) — mirror of the connection, system and capability doors. The caller is
3465
3567
  * the local broker's word (the platform's, on the pure host); the pair and lifecycle gate run
@@ -3833,7 +3935,8 @@ export class SqliteScopeHost {
3833
3935
  try {
3834
3936
  if (schedule.purge) {
3835
3937
  // #119: a purge horizon's schedule runs its operation once per due entity, never once.
3836
- const pass = await this.runPurgeSweep(moduleId, tenantId, scopeId, schedule.operation);
3938
+ // #2192: under the run's own id, so the purges and their deliveries join this run's line.
3939
+ const pass = await this.runPurgeSweep(moduleId, tenantId, scopeId, schedule.operation, invocationId);
3837
3940
  stillDue = purgeStillDue(pass);
3838
3941
  const outcome = purgeReportOf(schedule.operation, schedule.purge.entityType, pass);
3839
3942
  report.errors.push(...outcome.errors);
@@ -3904,7 +4007,9 @@ export class SqliteScopeHost {
3904
4007
  * through a purge stub (`openSystemScope`) that never leaves this method, each in its own
3905
4008
  * transaction and under the cutoff the host re-checks inside it (`refuseTrashedTarget`).
3906
4009
  */
3907
- async runPurgeSweep(moduleId, tenantId, scopeId, operation) {
4010
+ async runPurgeSweep(moduleId, tenantId, scopeId, operation,
4011
+ /** #2192: the schedule run this pass belongs to; every purge in it runs under that id. */
4012
+ runInvocationId) {
3908
4013
  const entityType = this.modules
3909
4014
  .get(moduleId)
3910
4015
  ?.schedules.find((s) => s.operation === operation && s.purge)?.purge?.entityType;
@@ -3931,7 +4036,7 @@ export class SqliteScopeHost {
3931
4036
  return heldPurgePass(due.held);
3932
4037
  const stub = await this.openSystemScope(moduleId, tenantId, scopeId, true);
3933
4038
  return runPurgePass(operation, due, async (entityId) => {
3934
- await stub.invoke(operation, { [due.idFrom]: entityId }, { invocationId: ulid() });
4039
+ await stub.invoke(operation, { [due.idFrom]: entityId }, { invocationId: runInvocationId });
3935
4040
  }, (write) => rt.actor.turn(() => write(spineSql(rt.db))));
3936
4041
  }
3937
4042
  /**
@@ -5531,10 +5636,15 @@ export class SqliteScopeHost {
5531
5636
  calls: callsOfManifestJson(r.manifest_json),
5532
5637
  createdAt: r.created_at,
5533
5638
  });
5534
- const readVersion = (id) => {
5535
- const r = this.directory
5536
- .prepare('SELECT * FROM vertical_versions WHERE id = ?')
5537
- .get(id);
5639
+ // #2339: a version stored before the server derived migration digests is derived on its
5640
+ // first read, so no gate here ever compares the digest its push sent.
5641
+ const readVersion = async (id) => {
5642
+ const read = () => this.directory.prepare('SELECT * FROM vertical_versions WHERE id = ?').get(id);
5643
+ let r = read();
5644
+ if (r && r.migration_digest_derived === null) {
5645
+ await deriveVersionMigrationDigest(switchSqlOf(this.directory), id);
5646
+ r = read();
5647
+ }
5538
5648
  return r ? mapVersion(r) : undefined;
5539
5649
  };
5540
5650
  const mapOrg = (r) => orgSchema.parse({
@@ -5600,8 +5710,8 @@ export class SqliteScopeHost {
5600
5710
  });
5601
5711
  // The (version, scope) pair a bind and its impact read both start from, and the refusals
5602
5712
  // that come before any export-break question, in the order the bind makes them (#1756).
5603
- const bindTarget = (tenantId, scopeId, versionId) => {
5604
- const v = readVersion(versionId);
5713
+ const bindTarget = async (tenantId, scopeId, versionId) => {
5714
+ const v = await readVersion(versionId);
5605
5715
  if (!v)
5606
5716
  throw substratError('not_found', `unknown version ${versionId}`);
5607
5717
  const scope = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
@@ -5982,7 +6092,7 @@ export class SqliteScopeHost {
5982
6092
  throw substratError('not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
5983
6093
  }
5984
6094
  const rt = this.runtime(tenantId, scopeId);
5985
- const states = await rt.actor.turn(() => peerGrantsStatus(switchSqlOf(rt.db), new Date().toISOString()));
6095
+ const states = await rt.actor.turn(() => peerGrantsStatus(switchSqlOf(rt.db), new Date().toISOString(), this.peerDeclarations()));
5986
6096
  const offPeers = new Set(states.filter((p) => p.calls === 'off').map((p) => p.vertical));
5987
6097
  const explanations = offPeers.size > 0 ? lastSwitchedOffBy(tenantId, scopeId, 'revokeFromPeer', 'vertical', offPeers) : new Map();
5988
6098
  const result = states.map((p) => ({
@@ -6638,20 +6748,26 @@ export class SqliteScopeHost {
6638
6748
  // The SQL migrations are stored apart from the manifest (#1764), so no read of the
6639
6749
  // version but the promote review's carries them.
6640
6750
  const split = splitManifestMigrations(manifestJson ?? null, 'push');
6751
+ // #2339: the digest a promote compares is the one derived from what is stored, never the pushed one.
6752
+ const migrationDigest = await publishedMigrationDigest({
6753
+ id: audited.id, migrationDigest: audited.migrationDigest, manifestJson: manifestJson ?? null, migrations: split.migrations,
6754
+ });
6641
6755
  this.directory.transaction(() => {
6642
6756
  this.directory
6643
6757
  .prepare(`INSERT INTO vertical_versions
6644
6758
  (id, vertical_slug, version, manifest_digest, permission_digest,
6645
6759
  migration_digest, deployment_ref, admission, admission_note, manifest_json,
6646
- origin_json, migration_count, migrations_split, created_at)
6647
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`)
6648
- .run(audited.id, audited.verticalSlug, audited.version, audited.manifestDigest, audited.permissionDigest, audited.migrationDigest, audited.deploymentRef, selfAdmits ? 'admitted' : 'pending', selfAdmits ? AUTO_ADMISSION_NOTE : null, split.manifestJson, origin ? JSON.stringify(origin) : null, split.migrations?.length ?? null, new Date().toISOString());
6760
+ origin_json, migration_count, migrations_split, migration_digest_derived, created_at)
6761
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, 1, ?)`)
6762
+ .run(audited.id, audited.verticalSlug, audited.version, audited.manifestDigest, audited.permissionDigest, migrationDigest, audited.deploymentRef, selfAdmits ? 'admitted' : 'pending', selfAdmits ? AUTO_ADMISSION_NOTE : null, split.manifestJson, origin ? JSON.stringify(origin) : null, split.migrations?.length ?? null, new Date().toISOString());
6649
6763
  writeVersionMigrations(switchSqlOf(this.directory), audited.id, split.migrations);
6650
- // #2286: the version's scope-store bindings, refusing one re-kinded, in the insert's transaction.
6764
+ // #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
6651
6765
  indexVersionScopeStores(switchSqlOf(this.directory), audited.verticalSlug, audited.id, split.manifestJson);
6652
6766
  })();
6653
6767
  this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
6654
6768
  ...audited,
6769
+ // The digest the registry holds, which differs from the pushed one only for an unheld version.
6770
+ migrationDigest,
6655
6771
  ...(origin ? { origin } : {}),
6656
6772
  admission: selfAdmits ? 'admitted' : 'pending',
6657
6773
  });
@@ -6666,14 +6782,20 @@ export class SqliteScopeHost {
6666
6782
  const where = ['vertical_slug = ?'];
6667
6783
  const params = [verticalSlug];
6668
6784
  const tail = keysetTail(where, params, 'id', page);
6669
- const rows = this.directory
6670
- .prepare(`SELECT * FROM vertical_versions WHERE ${where.join(' AND ')}${tail}`)
6671
- .all(...params);
6785
+ const sql = `SELECT * FROM vertical_versions WHERE ${where.join(' AND ')}${tail}`;
6786
+ let rows = this.directory.prepare(sql).all(...params);
6787
+ // #2339: a page holding versions whose digest is still the pushed one derives them first.
6788
+ const underived = rows.filter((r) => r.migration_digest_derived === null);
6789
+ if (underived.length > 0) {
6790
+ for (const r of underived)
6791
+ await deriveVersionMigrationDigest(switchSqlOf(this.directory), r.id);
6792
+ rows = this.directory.prepare(sql).all(...params);
6793
+ }
6672
6794
  this.recordAccess(actor, 'listVersions', {}, { verticalSlug }, rows.length);
6673
6795
  return rows.map(mapVersion);
6674
6796
  },
6675
6797
  getVersion: async (actor, versionId, verticalSlug) => {
6676
- const v = readVersion(versionId);
6798
+ const v = await readVersion(versionId);
6677
6799
  // A version of another vertical reads as absent when the caller named one.
6678
6800
  const hit = v && (verticalSlug === undefined || v.verticalSlug === verticalSlug) ? v : undefined;
6679
6801
  this.recordAccess(actor, 'getVersion', {}, { versionId, verticalSlug }, hit ? 1 : 0);
@@ -6690,7 +6812,7 @@ export class SqliteScopeHost {
6690
6812
  const prod = this.directory
6691
6813
  .prepare("SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = 'prod'")
6692
6814
  .get(slug);
6693
- const prodVersion = prod ? readVersion(prod.version_id) : undefined;
6815
+ const prodVersion = prod ? await readVersion(prod.version_id) : undefined;
6694
6816
  if (prodVersion?.admissionNote === AUTO_ADMISSION_NOTE) {
6695
6817
  throw substratError('conflict', `vertical '${slug}' prod version ${prodVersion.id} is auto-admitted (private self-serve) — ` +
6696
6818
  `a staff admit must vouch for it before listing`);
@@ -6771,7 +6893,7 @@ export class SqliteScopeHost {
6771
6893
  this.recordAdmin(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.ownerTenant }, null);
6772
6894
  },
6773
6895
  admitVersion: async (actor, versionId) => {
6774
- const v = readVersion(versionId);
6896
+ const v = await readVersion(versionId);
6775
6897
  if (!v)
6776
6898
  throw substratError('not_found', `unknown version ${versionId}`);
6777
6899
  if (v.admission === 'admitted') {
@@ -6799,7 +6921,7 @@ export class SqliteScopeHost {
6799
6921
  });
6800
6922
  },
6801
6923
  rejectVersion: async (actor, versionId, note) => {
6802
- const v = readVersion(versionId);
6924
+ const v = await readVersion(versionId);
6803
6925
  if (!v)
6804
6926
  throw substratError('not_found', `unknown version ${versionId}`);
6805
6927
  if (v.admission === 'admitted') {
@@ -6816,7 +6938,7 @@ export class SqliteScopeHost {
6816
6938
  });
6817
6939
  },
6818
6940
  promotionImpact: async (actor, verticalSlug, channel, versionId) => {
6819
- const incoming = readVersion(versionId);
6941
+ const incoming = await readVersion(versionId);
6820
6942
  if (!incoming || incoming.verticalSlug !== verticalSlug) {
6821
6943
  throw substratError('not_found', `unknown version ${versionId} for vertical '${verticalSlug}'`);
6822
6944
  }
@@ -6828,7 +6950,7 @@ export class SqliteScopeHost {
6828
6950
  return breaks;
6829
6951
  },
6830
6952
  promoteVersion: async (actor, verticalSlug, channel, versionId, acknowledge) => {
6831
- const incoming = readVersion(versionId);
6953
+ const incoming = await readVersion(versionId);
6832
6954
  if (!incoming)
6833
6955
  throw substratError('not_found', `unknown version ${versionId}`);
6834
6956
  if (incoming.verticalSlug !== verticalSlug) {
@@ -6840,7 +6962,7 @@ export class SqliteScopeHost {
6840
6962
  const current = this.directory
6841
6963
  .prepare('SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = ?')
6842
6964
  .get(verticalSlug, channel);
6843
- const outgoing = current ? readVersion(current.version_id) : undefined;
6965
+ const outgoing = current ? await readVersion(current.version_id) : undefined;
6844
6966
  const ack = promotionAcknowledgement.parse(acknowledge ?? {});
6845
6967
  // The owning tenant's live scopes a PRIVATE prod promote re-points — below for an embedded
6846
6968
  // vertical, after the in-place serve for a dispatch-backed one (#321) — read before
@@ -6863,6 +6985,18 @@ export class SqliteScopeHost {
6863
6985
  served?.versionId,
6864
6986
  ...owned.map((s) => runningVersionOf(mapScope(s), served)),
6865
6987
  ], versionId, served || owned.length > 0 ? this.provisionedVersionsOf(verticalSlug, served, owned.length > 0 ? owning.ownerTenant : null) : []);
6988
+ // #2333: the acknowledgements answer the review they were given against, or none at all.
6989
+ const reviewed = (v) => ({
6990
+ ...v,
6991
+ migrations: versionMigrationsOf(switchSqlOf(this.directory), v.id)?.migrations ?? null,
6992
+ });
6993
+ await assertPromoteReviewCurrent({
6994
+ ack,
6995
+ channel,
6996
+ baseId: outgoing?.id ?? null,
6997
+ migrationsChange: outgoing !== undefined && outgoing.migrationDigest !== incoming.migrationDigest,
6998
+ read: async () => ({ base: outgoing ? reviewed(outgoing) : null, incoming: reviewed(incoming) }),
6999
+ });
6866
7000
  // §4's two checkpoints, fired where the blast radius is. A FIRST promotion
6867
7001
  // has nothing to diff against, so there is nothing to acknowledge — the
6868
7002
  // gate is about change, not about existence.
@@ -6872,8 +7006,7 @@ export class SqliteScopeHost {
6872
7006
  `${incoming.permissionDigest}) — acknowledge it explicitly to promote`);
6873
7007
  }
6874
7008
  if (outgoing.migrationDigest !== incoming.migrationDigest && !ack.migrationChange) {
6875
- throw substratError('conflict', `promotion changes migrations (${outgoing.migrationDigest} → ` +
6876
- `${incoming.migrationDigest}) — acknowledge it explicitly to promote`);
7009
+ throw substratError('conflict', migrationChangeRefusal(outgoing.migrationDigest, incoming.migrationDigest));
6877
7010
  }
6878
7011
  // #1705 PR 3: an export an installed consumer imports, dropped or re-versioned.
6879
7012
  if (!ack.exportBreak) {
@@ -6883,18 +7016,32 @@ export class SqliteScopeHost {
6883
7016
  }
6884
7017
  }
6885
7018
  const promotedAt = new Date().toISOString();
6886
- this.directory
6887
- .prepare(`INSERT INTO vertical_channels (vertical_slug, channel, version_id, updated_at)
6888
- VALUES (?, ?, ?, ?)
6889
- ON CONFLICT (vertical_slug, channel) DO UPDATE SET version_id = ?, updated_at = ?`)
6890
- .run(verticalSlug, channel, versionId, promotedAt, versionId, promotedAt);
6891
- // The timeline row: what makes rollback a choice among recorded moments, and
6892
- // `at` the PITR anchor a data rollback would rewind to.
6893
- this.directory
6894
- .prepare(`INSERT INTO vertical_channel_history
6895
- (id, vertical_slug, channel, version_id, from_version_id, actor, at)
6896
- VALUES (?, ?, ?, ?, ?, ?, ?)`)
6897
- .run(ulid(), verticalSlug, channel, versionId, outgoing?.id ?? null, actor, promotedAt);
7019
+ // #2333: a compare-and-set. Every gate above — the review binding, the digest checkpoints,
7020
+ // the export breaks — was judged against `outgoing`, across awaits another promote can run
7021
+ // in. So the pointer moves only if it still names `outgoing`, read and written in one
7022
+ // transaction; the promote that lost the race is refused as stale and writes nothing.
7023
+ const moved = this.directory.transaction(() => {
7024
+ const now = this.directory
7025
+ .prepare('SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = ?')
7026
+ .get(verticalSlug, channel);
7027
+ if ((now?.version_id ?? null) !== (outgoing?.id ?? null))
7028
+ return false;
7029
+ this.directory
7030
+ .prepare(`INSERT INTO vertical_channels (vertical_slug, channel, version_id, updated_at)
7031
+ VALUES (?, ?, ?, ?)
7032
+ ON CONFLICT (vertical_slug, channel) DO UPDATE SET version_id = ?, updated_at = ?`)
7033
+ .run(verticalSlug, channel, versionId, promotedAt, versionId, promotedAt);
7034
+ // The timeline row: what makes rollback a choice among recorded moments, and
7035
+ // `at` the PITR anchor a data rollback would rewind to.
7036
+ this.directory
7037
+ .prepare(`INSERT INTO vertical_channel_history
7038
+ (id, vertical_slug, channel, version_id, from_version_id, actor, at)
7039
+ VALUES (?, ?, ?, ?, ?, ?, ?)`)
7040
+ .run(ulid(), verticalSlug, channel, versionId, outgoing?.id ?? null, actor, promotedAt);
7041
+ return true;
7042
+ })();
7043
+ if (!moved)
7044
+ throw promoteRaceRefusal(channel);
6898
7045
  // For a PRIVATE vertical, prod IS what the owner's apps run: re-point the
6899
7046
  // owning tenant's live scopes in the same act, so merge-to-main (push +
6900
7047
  // promote) is a complete deploy and a rollback promote reaches the running
@@ -6914,7 +7061,7 @@ export class SqliteScopeHost {
6914
7061
  for (const s of incoming.deploymentRef ? [] : owned) {
6915
7062
  if (s.vertical_version_id === versionId)
6916
7063
  continue;
6917
- const prev = s.vertical_version_id ? readVersion(s.vertical_version_id) : undefined;
7064
+ const prev = s.vertical_version_id ? await readVersion(s.vertical_version_id) : undefined;
6918
7065
  if (prev && prev.migrationDigest !== incoming.migrationDigest) {
6919
7066
  await this.snapshotScope(actor, s.tenant_id, s.scope_id);
6920
7067
  }
@@ -6976,7 +7123,7 @@ export class SqliteScopeHost {
6976
7123
  }));
6977
7124
  },
6978
7125
  bindingImpact: async (actor, tenantId, scopeId, versionId, opts) => {
6979
- const { v, scope } = bindTarget(tenantId, scopeId, versionId);
7126
+ const { v, scope } = await bindTarget(tenantId, scopeId, versionId);
6980
7127
  // #2178: a move no acknowledgement passes is refused here too, so a caller asking first
6981
7128
  // (an adopt, an Update) stops before anything is copied.
6982
7129
  this.assertBindingShapesKept(mapScope(scope), versionId, opts?.servingRef);
@@ -6985,22 +7132,30 @@ export class SqliteScopeHost {
6985
7132
  return breaks;
6986
7133
  },
6987
7134
  bindScopeVersion: async (actor, tenantId, scopeId, versionId, opts) => {
6988
- const { v, scope } = bindTarget(tenantId, scopeId, versionId);
6989
- if (opts?.expectedVersionId !== undefined && scope.vertical_version_id !== opts.expectedVersionId) {
6990
- throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
6991
- }
7135
+ const { v, scope } = await bindTarget(tenantId, scopeId, versionId);
7136
+ // #2347: a rebind judged against both lineages loses its race as stale, so its client reads
7137
+ // the review again; any other conditional bind keeps the refusal it always had.
7138
+ const lost = () => opts?.expectedServing
7139
+ ? rebindRaceRefusal(opts.expectedServing.vertical)
7140
+ : substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
7141
+ if (opts?.expectedVersionId !== undefined && scope.vertical_version_id !== opts.expectedVersionId)
7142
+ throw lost();
7143
+ if (opts?.expectedServing && !this.servesNow(opts.expectedServing))
7144
+ throw lost();
6992
7145
  const epoch = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
6993
7146
  .get(tenantId, scopeId);
6994
7147
  if (opts?.expectedErasureEpoch !== undefined && epoch?.erasure_epoch !== opts.expectedErasureEpoch) {
6995
7148
  throw substratError('precondition_failed', 'scope erasure changed; reload the scope and retry');
6996
7149
  }
6997
7150
  const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
7151
+ // #2347: a route moving in the same write is judged with it, as `bindingImpact` is.
7152
+ const servingRef = opts?.servingRef;
6998
7153
  // #2178 first: no acknowledgement passes it, so none should be asked for ahead of it.
6999
- this.assertBindingShapesKept(mapScope(scope), versionId);
7154
+ this.assertBindingShapesKept(mapScope(scope), versionId, servingRef);
7000
7155
  // #1756: an export an app in this tenant imports, dropped or re-versioned by what this
7001
7156
  // scope would run. Before the snapshot, so a refused bind leaves nothing behind.
7002
7157
  if (!ack.exportBreak) {
7003
- const breaks = await this.bindBreaks(actor, mapScope(scope), v);
7158
+ const breaks = await this.bindBreaks(actor, mapScope(scope), v, servingRef);
7004
7159
  if (breaks.length > 0)
7005
7160
  throw substratError('precondition_failed', bindExportBreakRefusal(breaks));
7006
7161
  }
@@ -7008,19 +7163,29 @@ export class SqliteScopeHost {
7008
7163
  // crosses a migration boundary. Gated on a real digest change — a code-only
7009
7164
  // rebind snapshots nothing — and on the caller opting in (until GC ships).
7010
7165
  if (opts?.snapshot && scope.vertical_version_id) {
7011
- const outgoing = readVersion(scope.vertical_version_id);
7166
+ const outgoing = await readVersion(scope.vertical_version_id);
7012
7167
  if (outgoing && outgoing.migrationDigest !== v.migrationDigest) {
7013
7168
  await this.snapshotScope(actor, tenantId, scopeId);
7014
7169
  }
7015
7170
  }
7016
- if (!this.moveScopeRow('vertical_version_id = ?, vertical = ?', [versionId, v.verticalSlug], tenantId, scopeId, opts)) {
7017
- throw substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
7018
- }
7019
- this.recordAdmin(actor, 'bindScopeVersion', { tenantId, scopeId }, null, {
7171
+ const moved = servingRef === undefined
7172
+ ? this.moveScopeRow('vertical_version_id = ?, vertical = ?', [versionId, v.verticalSlug], tenantId, scopeId, opts)
7173
+ : this.moveScopeRow('vertical_version_id = ?, vertical = ?, serving_ref = ?', [versionId, v.verticalSlug, servingRef], tenantId, scopeId, opts);
7174
+ if (!moved) {
7175
+ // The erasure and copy-move conditions keep their own refusal: only a moved binding or a
7176
+ // moved serving version is the race a rebind's review answers.
7177
+ const now = this.directory.prepare('SELECT vertical_version_id FROM scopes WHERE scope_id = ?').get(scopeId);
7178
+ const raced = (opts?.expectedVersionId !== undefined && now?.vertical_version_id !== opts.expectedVersionId)
7179
+ || (opts?.expectedServing !== undefined && !this.servesNow(opts.expectedServing));
7180
+ throw raced ? lost() : substratError('precondition_failed', 'scope binding changed; reload the scope and retry');
7181
+ }
7182
+ this.recordAdmin(actor, 'bindScopeVersion', { tenantId, scopeId }, servingRef === undefined ? null : { servingRef: scope.serving_ref }, {
7020
7183
  versionId,
7021
7184
  vertical: v.verticalSlug,
7022
7185
  version: v.version,
7186
+ ...(servingRef !== undefined ? { servingRef } : {}),
7023
7187
  ...(opts?.expectedVersionId !== undefined ? { expectedVersionId: opts.expectedVersionId } : {}),
7188
+ ...(opts?.expectedServing ? { expectedServing: opts.expectedServing } : {}),
7024
7189
  ...(ack.exportBreak ? { acknowledged: ack } : {}),
7025
7190
  });
7026
7191
  },
@@ -7205,7 +7370,7 @@ export class SqliteScopeHost {
7205
7370
  const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
7206
7371
  // #1756: onto (or off) a serving script, the scope runs other code before its pointer
7207
7372
  // moves. Judged here, so no caller that routes first and binds second passes unasked.
7208
- const bound = scope.vertical_version_id ? readVersion(scope.vertical_version_id) : undefined;
7373
+ const bound = scope.vertical_version_id ? await readVersion(scope.vertical_version_id) : undefined;
7209
7374
  if (bound)
7210
7375
  this.assertBindingShapesKept(mapScope(scope), bound.id, servingRef);
7211
7376
  if (!ack.exportBreak && bound) {
@@ -7782,48 +7947,74 @@ export class SqliteScopeHost {
7782
7947
  createdAt: r.created_at,
7783
7948
  }));
7784
7949
  },
7785
- // -- store-kind releases, rollouts and holds (D-63, #2300) -------------------
7786
- // The state is kernel code over the directory (store-kind-releases.ts), shared with the
7950
+ // -- store-driver releases, rollouts and holds (D-63, #2300) -------------------
7951
+ // The state is kernel code over the directory (store-driver-releases.ts), shared with the
7787
7952
  // Cloudflare control plane; each call is one transaction. A hold's expiry is judged against
7788
7953
  // this host's clock, so a test can let a lease lapse without waiting.
7789
- recordStoreKindRelease: async (actor, input) => {
7954
+ recordStoreDriverRelease: async (actor, input) => {
7790
7955
  const at = this.clock();
7791
- const { record, created } = this.directory.transaction(() => recordStoreKindReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
7956
+ const { record, created } = this.directory.transaction(() => recordStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
7792
7957
  if (created)
7793
- this.recordAdmin(actor, 'uploadStoreKindRelease', { tenantId: null }, null, record, at);
7958
+ this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, record, at);
7794
7959
  return record;
7795
7960
  },
7796
- getStoreKindRelease: async (actor, kind, sha256) => {
7797
- const record = storeKindReleaseIn(switchSqlOf(this.directory), kind, sha256);
7798
- this.recordAccess(actor, 'getStoreKindRelease', { tenantId: null }, { kind, sha256 }, record ? 1 : 0);
7961
+ getStoreDriverRelease: async (actor, driver, sha256) => {
7962
+ const record = storeDriverReleaseIn(switchSqlOf(this.directory), driver, sha256);
7963
+ this.recordAccess(actor, 'getStoreDriverRelease', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
7799
7964
  return record;
7800
7965
  },
7801
- storeKindState: async (actor, kind) => {
7802
- const state = storeKindStateIn(switchSqlOf(this.directory), kind, Date.parse(this.clock()));
7803
- this.recordAccess(actor, 'storeKindState', { tenantId: null }, { kind }, state.rollouts.length);
7966
+ storeDriverState: async (actor, driver) => {
7967
+ const state = storeDriverStateIn(switchSqlOf(this.directory), driver, Date.parse(this.clock()));
7968
+ this.recordAccess(actor, 'storeDriverState', { tenantId: null }, { driver }, state.rollouts.length);
7804
7969
  return state;
7805
7970
  },
7806
- storeKindDeclarations: async (actor, kind) => {
7807
- const declared = this.directory.transaction(() => kindDeclarationsOf(switchSqlOf(this.directory), kind))();
7808
- this.recordAccess(actor, 'storeKindDeclarations', { tenantId: null }, { kind }, declared.verticals.length);
7971
+ storeDriverDeclarations: async (actor, driver) => {
7972
+ const declared = this.directory.transaction(() => driverDeclarationsOf(switchSqlOf(this.directory), driver))();
7973
+ this.recordAccess(actor, 'storeDriverDeclarations', { tenantId: null }, { driver }, declared.verticals.length);
7809
7974
  return declared;
7810
7975
  },
7811
- holdStoreKind: async (_actor, input) => this.directory.transaction(() => holdStoreKindIn(switchSqlOf(this.directory), { ...input, nowMs: Date.parse(this.clock()) }))(),
7812
- releaseStoreKindHold: async (_actor, input) => {
7813
- releaseStoreKindHoldIn(switchSqlOf(this.directory), input);
7976
+ storeDriverRolloutPending: async (actor, input) => {
7977
+ const page = this.directory.transaction(() => storeDriverRolloutPendingIn(switchSqlOf(this.directory), input))();
7978
+ this.recordAccess(actor, 'storeDriverRolloutPending', { tenantId: null }, { driver: input.driver, sha256: input.sha256, action: input.action }, page.verticals.length);
7979
+ return page;
7980
+ },
7981
+ storeDriverRolloutWalked: async (actor, input) => {
7982
+ const walked = this.directory.transaction(() => storeDriverRolloutWalkedIn(switchSqlOf(this.directory), input))();
7983
+ this.recordAccess(actor, 'storeDriverRolloutWalked', { tenantId: null }, { driver: input.driver, seq: input.seq }, walked.verticals.length);
7984
+ return walked;
7814
7985
  },
7815
- beginStoreKindRollout: async (actor, input) => {
7986
+ holdStoreDriver: async (_actor, input) => this.directory.transaction(() => holdStoreDriverIn(switchSqlOf(this.directory), { ...input, nowMs: Date.parse(this.clock()) }))(),
7987
+ releaseStoreDriverHold: async (_actor, input) => {
7988
+ releaseStoreDriverHoldIn(switchSqlOf(this.directory), input);
7989
+ },
7990
+ beginStoreDriverRollout: async (actor, input) => {
7816
7991
  const at = this.clock();
7817
- const row = this.directory.transaction(() => beginStoreKindRolloutIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) }))();
7818
- this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreKind' : 'rollOutStoreKind', { tenantId: null }, null, row, at);
7992
+ const row = this.directory.transaction(() => beginStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) }))();
7993
+ this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at);
7819
7994
  return row;
7820
7995
  },
7821
- finishStoreKindRollout: async (actor, input) => {
7996
+ finishStoreDriverRollout: async (actor, input) => {
7822
7997
  const at = this.clock();
7823
- const row = this.directory.transaction(() => finishStoreKindRolloutIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
7824
- this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreKind' : 'rollOutStoreKind', { tenantId: null }, null, row, at);
7998
+ const row = this.directory.transaction(() => finishStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
7999
+ this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at);
7825
8000
  return row;
7826
8001
  },
8002
+ storeDriverRebuild: async (actor, driver) => {
8003
+ const rebuild = storeDriverRebuildIn(switchSqlOf(this.directory), driver);
8004
+ this.recordAccess(actor, 'storeDriverRebuild', { tenantId: null }, { driver }, rebuild ? 1 : 0);
8005
+ return rebuild;
8006
+ },
8007
+ claimStoreDriverRebuild: async (_actor, input) => {
8008
+ const at = this.clock();
8009
+ return this.directory.transaction(() => claimStoreDriverRebuildIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
8010
+ },
8011
+ advanceStoreDriverRebuild: async (_actor, input) => {
8012
+ const at = this.clock();
8013
+ return this.directory.transaction(() => advanceStoreDriverRebuildIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
8014
+ },
8015
+ recordStoreDriverRebuildRun: async (actor, entry) => {
8016
+ this.recordAdmin(actor, 'rebuildStoreDriver', { tenantId: null }, null, entry, this.clock());
8017
+ },
7827
8018
  getScopeRecord: async (actor, tenantId, scopeId) => {
7828
8019
  const r = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
7829
8020
  // Cross-check the pair (K-3): a scope that exists under a DIFFERENT tenant
@@ -8915,6 +9106,11 @@ export class SqliteScopeHost {
8915
9106
  const { tenantId, scopeId, action, ...after } = copyMarkAudit.parse(entry);
8916
9107
  this.recordAdmin(actor, action === 'mark' ? 'markScopeCopy' : 'clearScopeCopyMark', { tenantId, scopeId }, null, after);
8917
9108
  },
9109
+ /** #2314: one orphan-attachment pass over a scope, written after the vertical's own pass. */
9110
+ recordOrphanSweep: async (actor, entry) => {
9111
+ const { tenantId, scopeId, ...after } = orphanSweepAudit.parse(entry);
9112
+ this.recordAdmin(actor, 'sweepOrphanAttachments', { tenantId, scopeId }, null, after);
9113
+ },
8918
9114
  accessLog: async (actor, filter) => {
8919
9115
  const where = [];
8920
9116
  const params = [];
@@ -9373,7 +9569,8 @@ export class SqliteScopeHost {
9373
9569
  const moved = this.directory.prepare(`UPDATE scopes SET ${set} WHERE scope_id = ?
9374
9570
  ${cond?.expectedVersionId === undefined ? '' : 'AND vertical_version_id IS ?'}
9375
9571
  ${cond?.expectedErasureEpoch === undefined ? '' : 'AND COALESCE(erasure_epoch, 0) = ? AND reap_claimed_at IS NULL'}
9376
- ${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)));
9572
+ ${cond?.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}
9573
+ ${cond?.expectedServing === undefined ? '' : SERVES_NOW_PREDICATE}`).run(...setArgs, scopeId, ...(cond?.expectedVersionId === undefined ? [] : [cond.expectedVersionId]), ...(cond?.expectedErasureEpoch === undefined ? [] : [cond.expectedErasureEpoch]), ...(cond?.confirmMove === undefined ? [] : copyMoveLiveParams(cond.confirmMove, now)), ...(cond?.expectedServing === undefined ? [] : servesNowParams(cond.expectedServing)));
9377
9574
  if (moved.changes === 0)
9378
9575
  return false;
9379
9576
  if (cond?.confirmMove) {
@@ -9383,6 +9580,10 @@ export class SqliteScopeHost {
9383
9580
  return true;
9384
9581
  })();
9385
9582
  }
9583
+ /** #2347: whether a lineage's serving script still runs the version a rebind was judged against. */
9584
+ servesNow(expected) {
9585
+ return this.directory.prepare(`SELECT 1 FROM verticals WHERE ${SERVES_NOW_CONDITION}`).get(...servesNowParams(expected)) !== undefined;
9586
+ }
9386
9587
  ensureColumn(db, table, column, ddl) {
9387
9588
  // Without case, as SQLite resolves a column name: a restore may have added this column
9388
9589
  // already, untyped and spelled as a newer kernel's dump spelled it (#1883).
@@ -10210,6 +10411,14 @@ export class SqliteScopeHost {
10210
10411
  await this.repairDerived(rt);
10211
10412
  return;
10212
10413
  }
10414
+ // #2349: before any SQL, the registered set is held to the one stored for this version.
10415
+ // Only here, so a scope with nothing pending never computes it.
10416
+ const unstored = await migrationSetDivergence(this.modules.values(), this.migrationsDigest);
10417
+ if (unstored) {
10418
+ const refused = { version: `${pending[0].moduleId}@${pending[0].migration.version}`, error: unstored };
10419
+ this.recordMigrationState(rt, refused);
10420
+ throw migrationFailedError(refused.version, refused.error);
10421
+ }
10213
10422
  // The failing `module@version` and its cause, captured structurally rather than
10214
10423
  // re-parsed out of the thrown message — the directory record has to name both.
10215
10424
  let failure;
@@ -10243,7 +10452,8 @@ export class SqliteScopeHost {
10243
10452
  assertMigrationLeavesLedgerAlone(migration.sql, `migration ${key}`);
10244
10453
  // #2068: one statement at a time, keeping the table set either side of each — the
10245
10454
  // same splitter the Durable Object runs, so both hosts execute identical statements.
10246
- const steps = runMigrationStatements(spineSql(rt.db), migration.sql, (stmt) => rt.db.exec(stmt));
10455
+ // #115: an authored one is also held to the data-loss fence, inside this transaction.
10456
+ const steps = runMigrationStatements(spineSql(rt.db), migration, (stmt) => rt.db.exec(stmt), { key, authored });
10247
10457
  assertTablesWithinColumnLimit(rt.db);
10248
10458
  // #2090: the state columns this migration must have left, and after the last of the
10249
10459
  // pass, the triggers and indexes a create-copy-rename rebuild dropped.
@@ -10523,6 +10733,9 @@ export class SqliteScopeHost {
10523
10733
  // #2009: the copy classification on a scope DB built before it (NULL reads as a copy; see
10524
10734
  // `COPY_ORIGIN_DDL`).
10525
10735
  this.ensureColumn(db, '_substrat_copy_origin', 'is_copy', 'is_copy INTEGER');
10736
+ // #2315: the hash of the key a redemption carried, on a scope DB built before it. NULL = the
10737
+ // spending request carried none, the honest reading of every link already spent.
10738
+ this.ensureColumn(db, '_substrat_upload_links', 'redeemed_key_hash', 'redeemed_key_hash TEXT');
10526
10739
  // #1237: `readInvocation`'s lookup — WHERE invocation_id = ? ORDER BY id — over an outbox
10527
10740
  // that is never pruned. No index leads with invocation_id, so without this one SQLite
10528
10741
  // walks the PRIMARY KEY from the oldest event until it reaches the call, and reading a