@substrat-run/adapter-sqlite 0.146.0 → 0.148.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,16 +1,17 @@
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, 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, 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, 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, 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, } 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, 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';
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 } 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';
8
+ import { INERT_SCOPE_REASON, isPrimaryScopeRow, UPLOAD_LINK_WRONG_TARGET } 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, 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';
12
12
  import { ScopeActor } from './actor.js';
13
13
  import { createTupleChecker, directoryTenantReader } from './checker.js';
14
+ export { scopeStoreRunnersFor } from './scope-store-runners.js';
14
15
  /**
15
16
  * A fault in the database itself (full, corrupt, I/O), as opposed to one statement failing.
16
17
  * The #1764 backfill logs and opens past a failed batch, but never past one of these.
@@ -213,6 +214,9 @@ const KERNEL_DDL = `
213
214
  -- an exchange trades that secret for. Spine (kernel-written), shared with the DO adapter
214
215
  -- from @substrat-run/kernel so the two cannot part company; the column comments are there.
215
216
  ${CAPABILITY_DDL}
217
+ -- Upload links: a short-lived, single-use URL that lets a caller with no session upload ONE
218
+ -- file to ONE entity. Spine (kernel-written), shared with the DO adapter from the kernel.
219
+ ${UPLOAD_LINK_DDL}
216
220
  -- #1686: where a copied scope's data came from, and the event id its own events start above.
217
221
  -- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
218
222
  ${COPY_ORIGIN_DDL}
@@ -620,6 +624,9 @@ export class SqliteScopeHost {
620
624
  /** predicate name → the module-contributed implementation. Names are global. */
621
625
  predicates = new Map();
622
626
  /** operation names whose default binding some manifest withdrew (K-17). */
627
+ scopeStoreRunners;
628
+ scopeStoreVertical;
629
+ scopeStoreLifecycle;
623
630
  withdrawn = new Map(); // operation → withdrawing module
624
631
  relations = new Map();
625
632
  /** entityType → its derived FTS5 index (#827). One entity type, one index. */
@@ -634,6 +641,8 @@ export class SqliteScopeHost {
634
641
  erasurePlans = [];
635
642
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
636
643
  attachmentTargets = new Map();
644
+ /** entityType → the `uploadLinks` policy its `attachmentTargets` entry declares; absent = no links. */
645
+ uploadLinkPolicies = new Map();
637
646
  /** operation name → who binds it: the owning module, its entitlementKey and its declared
638
647
  * schedules — the §4.3 gate's input, resolved per invoke by `requiredEntitlementFor` (#1654). */
639
648
  operationEntitlement = new Map();
@@ -712,6 +721,9 @@ export class SqliteScopeHost {
712
721
  this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
713
722
  this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
714
723
  this.versionId = options.versionId ?? null;
724
+ this.scopeStoreVertical = options.scopeStores?.vertical;
725
+ this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
726
+ this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
715
727
  this.logSink = options.logSink ?? consoleLogSink;
716
728
  this.invocationLineSink = options.invocationLineSink ?? consoleInvocationLineSink;
717
729
  // Every option is read above and here, BEFORE the directory is claimed: an option that throws
@@ -930,6 +942,8 @@ export class SqliteScopeHost {
930
942
  created_at TEXT NOT NULL,
931
943
  PRIMARY KEY (tenant_id, vertical, binding)
932
944
  );
945
+ -- A store kind's stored releases, rollouts and holds (D-63, #2300).
946
+ ${STORE_KIND_RELEASES_DDL}
933
947
  -- The vertical + version registry (#31). A scope binds to a VERSION, so
934
948
  -- dev/staging/prod are the same vertical pinned differently, and a preview
935
949
  -- deployment is a version nothing has been promoted to yet.
@@ -1000,6 +1014,7 @@ export class SqliteScopeHost {
1000
1014
  UNIQUE (vertical_slug, version)
1001
1015
  );
1002
1016
  ${VERSION_MIGRATIONS_DDL}
1017
+ ${VERTICAL_SCOPE_STORES_DDL}
1003
1018
  -- Channels (#31 step 2): a named pointer per vertical. Promotion moves it,
1004
1019
  -- and promotion is where the migration and permission diffs fire — the
1005
1020
  -- moment a change reaches anyone, rather than the moment it was typed.
@@ -1701,6 +1716,13 @@ export class SqliteScopeHost {
1701
1716
  `(${existing.read}/${existing.write}) vs (${gate.read}/${gate.write})`);
1702
1717
  }
1703
1718
  this.attachmentTargets.set(target.entityType, gate);
1719
+ if (target.uploadLinks) {
1720
+ const known = this.uploadLinkPolicies.get(target.entityType);
1721
+ if (known && JSON.stringify(known) !== JSON.stringify(target.uploadLinks)) {
1722
+ throw new Error(`conflicting uploadLinks policies for attachment target '${target.entityType}'`);
1723
+ }
1724
+ this.uploadLinkPolicies.set(target.entityType, target.uploadLinks);
1725
+ }
1704
1726
  }
1705
1727
  // WITHDRAWAL (K-17): suppress another module's default binding. Order
1706
1728
  // independent — a manifest may withdraw an operation whose module has not
@@ -2118,6 +2140,40 @@ export class SqliteScopeHost {
2118
2140
  const store = this.attachmentStore(tenantId, scopeRow?.vertical ?? null);
2119
2141
  return this.buildAttachments(rt, asPrincipal(principal), store);
2120
2142
  }
2143
+ /** The declared write key and link policy of an attachment target — what `ctx.attachments` mints against. */
2144
+ uploadTargetOf(entityType) {
2145
+ const gate = this.attachmentTargets.get(entityType);
2146
+ if (!gate)
2147
+ return undefined;
2148
+ const uploadLinks = this.uploadLinkPolicies.get(entityType);
2149
+ return uploadLinks ? { write: gate.write, uploadLinks } : { write: gate.write };
2150
+ }
2151
+ async inspectUploadLink(tenantId, scopeId, secret) {
2152
+ const rt = await this.openActiveScope(tenantId, scopeId);
2153
+ return rt.actor.enqueue(async () => (await inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret)).view);
2154
+ }
2155
+ async redeemUploadLink(tenantId, scopeId, secret, input) {
2156
+ 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,
2161
+ });
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
+ });
2176
+ }
2121
2177
  async getSystemAttachments(moduleId, tenantId, scopeId) {
2122
2178
  // The same registered-module and lifecycle gate as a system invoke. The
2123
2179
  // attachment surface checks the target's read key as system:<moduleId>.
@@ -2182,8 +2238,17 @@ export class SqliteScopeHost {
2182
2238
  }
2183
2239
  // #1525: null, for the reason the denial two lines up records null — this task
2184
2240
  // is the attachment RPC's own and no invoke set an id on the runtime.
2185
- await this.dispatch(rt, null);
2186
- await this.dispatchExecutors(rt, null);
2241
+ // The write has COMMITTED, so a failed drain must not reach the caller as a refusal:
2242
+ // `upload` deletes the bytes on a refusal, which would leave this row without them
2243
+ // (and a redeemed link spent) (#2311). An undelivered event has no delivery row, so
2244
+ // the next drain in this scope delivers it.
2245
+ try {
2246
+ await this.dispatch(rt, null);
2247
+ await this.dispatchExecutors(rt, null);
2248
+ }
2249
+ catch (err) {
2250
+ console.error('substrat: consumer drain failed after an attachment committed', err);
2251
+ }
2187
2252
  return result;
2188
2253
  });
2189
2254
  /**
@@ -2242,9 +2307,9 @@ export class SqliteScopeHost {
2242
2307
  createdBy: creator.id,
2243
2308
  createdAt: new Date().toISOString(),
2244
2309
  });
2245
- // Bytes first, row second: a crash between the two leaves an orphaned object
2246
- // (harmless, GC-able via list), never a row without bytes. A refused check
2247
- // compensates the object away below.
2310
+ // Bytes first, row second: a crash between the two leaves an orphaned object that no
2311
+ // row points at (#2314). A refused check compensates the object away below; a drain
2312
+ // fault after the commit is not a refusal (`serialized` keeps it from becoming one).
2248
2313
  await store.put(key, input.body, { contentType: input.contentType });
2249
2314
  try {
2250
2315
  return await guarded('attachments.upload', async (ctx) => {
@@ -2265,6 +2330,11 @@ export class SqliteScopeHost {
2265
2330
  // #1575: queue its text extraction in the same transaction — a `pending` row
2266
2331
  // and a job run. Nothing is extracted here, so nothing here can fail the upload.
2267
2332
  enqueueAttachmentText(spineSql(rt.db), record.id, ulid(), this.clock());
2333
+ // An upload-link redemption spends its link HERE, in the same transaction as the
2334
+ // 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);
2337
+ }
2268
2338
  return record;
2269
2339
  });
2270
2340
  }
@@ -2358,6 +2428,7 @@ export class SqliteScopeHost {
2358
2428
  };
2359
2429
  }
2360
2430
  async importScope(actor, input, dump) {
2431
+ this.refuseUnusableScopeStores(dump.stores, input.vertical ?? null);
2361
2432
  // Create the destination scope (directory row + storage). Its migration step
2362
2433
  // creates some tables; the dump then replaces them wholesale, so the end state
2363
2434
  // is the dump, not whatever the local module set would have built. Provenance is
@@ -2370,6 +2441,7 @@ export class SqliteScopeHost {
2370
2441
  });
2371
2442
  // A fork: its callers (snapshotScope) hand it a dump the platform exported.
2372
2443
  await this.loadDump(input.tenantId, input.scopeId, { ...dump, exact: true });
2444
+ await this.importScopeStores(input.tenantId, input.scopeId, dump.stores);
2373
2445
  await this.admin.activateScope(actor, input.tenantId, input.scopeId);
2374
2446
  this.recordAdmin(actor, 'importScope', { tenantId: input.tenantId, scopeId: input.scopeId }, null, { sourceScopeId: dump.scopeId, tables: dump.tables.length, capturedAt: dump.capturedAt });
2375
2447
  }
@@ -2522,6 +2594,7 @@ export class SqliteScopeHost {
2522
2594
  const existing = await this.admin.getScopeRecord(actor, tenantId, scopeId);
2523
2595
  if (!existing)
2524
2596
  throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
2597
+ this.refuseUnusableScopeStores(dump.stores, existing.vertical);
2525
2598
  // #1742: a dump from before a switch was pulled brings the module's grants back live, so
2526
2599
  // the directory's recorded-off modules go back off in the load's own turn, as a
2527
2600
  // provision's seat does — no schedule pass can run in between.
@@ -2530,9 +2603,17 @@ export class SqliteScopeHost {
2530
2603
  // #1869: the re-point's source is the separate hint when given; the dump is otherwise as sent.
2531
2604
  const source = { tables: dump.tables, scopeId: opts?.sourceScopeId ?? dump.scopeId };
2532
2605
  await this.loadDump(tenantId, scopeId, source, (rt) => this.reassertSwitchesInTurn(rt, actor, tenantId, scopeId, undefined, (action, after) => rows.push([action, after])));
2606
+ await this.importScopeStores(tenantId, scopeId, dump.stores);
2607
+ const replay = await this.replayShreddedStoreErasures(actor, tenantId, scopeId, (opts?.sourceScopeId ?? dump.scopeId));
2533
2608
  for (const [action, after] of rows)
2534
2609
  this.recordAdmin(actor, action, { tenantId, scopeId }, null, after);
2535
- this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, { sourceScopeId: dump.scopeId, tables: dump.tables.length, capturedAt: dump.capturedAt });
2610
+ this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, {
2611
+ sourceScopeId: dump.scopeId,
2612
+ tables: dump.tables.length,
2613
+ capturedAt: dump.capturedAt,
2614
+ ...replay,
2615
+ });
2616
+ return replay;
2536
2617
  }
2537
2618
  async snapshotScope(actor, tenantId, scopeId, opts) {
2538
2619
  const source = await this.admin.getScopeRecord(actor, tenantId, scopeId);
@@ -2554,6 +2635,7 @@ export class SqliteScopeHost {
2554
2635
  if (source.verticalVersionId) {
2555
2636
  await this.admin.bindScopeVersion(actor, tenantId, snapshotId, source.verticalVersionId);
2556
2637
  }
2638
+ await this.replayShreddedStoreErasures(actor, tenantId, snapshotId, scopeId);
2557
2639
  return snapshotId;
2558
2640
  }
2559
2641
  async deleteSnapshot(actor, tenantId, scopeId) {
@@ -2567,6 +2649,7 @@ export class SqliteScopeHost {
2567
2649
  throw substratError('forbidden', `scope ${scopeId} is not a fork or preview — only previews may be deleted; ` +
2568
2650
  `archive a primary scope instead`);
2569
2651
  }
2652
+ await this.wipeScopeStores(tenantId, scopeId);
2570
2653
  // Close and evict the runtime handle before touching the file.
2571
2654
  const rt = this.scopes.get(`${tenantId}/${scopeId}`);
2572
2655
  if (rt)
@@ -2635,6 +2718,202 @@ export class SqliteScopeHost {
2635
2718
  }
2636
2719
  return this.buildStub(tenantId, scopeId, rt, asPrincipal(principal), options);
2637
2720
  }
2721
+ async getScopeStore(principal, tenantId, scopeId, binding, options) {
2722
+ // The directory check is the same one `getScope` makes. With no runners
2723
+ // configured, the binding is refused after that check.
2724
+ await this.getScope(principal, tenantId, scopeId);
2725
+ if (this.scopeStoreRunners.size === 0) {
2726
+ throw substratError('unavailable', `no scope-store runner is configured for binding '${binding}'`);
2727
+ }
2728
+ const vertical = this.scopeStoreVerticalOf(scopeId);
2729
+ const runner = this.scopeStoreRunners.get(binding);
2730
+ if (!runner) {
2731
+ throw substratError('not_found', `no scope store is bound for '${binding}'`);
2732
+ }
2733
+ await this.scopeStoreLifecycle.start();
2734
+ return {
2735
+ tenantId,
2736
+ scopeId,
2737
+ subjectKind: 'principal',
2738
+ invoke: (operation, input, ...extra) => observeStoreCall(binding, operation, () => callScopeStore((request) => runner.invoke({ tenantId, vertical, scopeId, binding }, request), operation, input, ...extra), options?.onStoreCall),
2739
+ };
2740
+ }
2741
+ /** The scope's vertical when it has one. No vertical means this host has no store to touch. */
2742
+ scopeStoreVerticalIfPresent(scopeId) {
2743
+ const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
2744
+ const vertical = row?.vertical;
2745
+ if (!vertical)
2746
+ return undefined;
2747
+ if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
2748
+ throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and scope ${scopeId} runs '${vertical}'`);
2749
+ }
2750
+ return vertical;
2751
+ }
2752
+ /** The scope's vertical, refused when this host is limited to another one. */
2753
+ scopeStoreVerticalOf(scopeId) {
2754
+ const vertical = this.scopeStoreVerticalIfPresent(scopeId);
2755
+ if (!vertical) {
2756
+ throw substratError('not_found', `scope ${scopeId} has no vertical, so it has no scope store`);
2757
+ }
2758
+ return vertical;
2759
+ }
2760
+ /**
2761
+ * One reserved call on a declared binding. The vertical's stub never reaches
2762
+ * this; `callReservedStoreOperation` refuses a name that is not reserved.
2763
+ */
2764
+ async invokeReservedStore(address, operation, input) {
2765
+ const runner = this.scopeStoreRunners.get(address.binding);
2766
+ if (!runner)
2767
+ throw substratError('not_found', `no scope store is bound for '${address.binding}'`);
2768
+ await this.scopeStoreLifecycle.start();
2769
+ return callReservedStoreOperation((request) => runner.invoke(address, request), operation, input);
2770
+ }
2771
+ /**
2772
+ * Each declared binding's `substrat.size`. No runners means an empty list,
2773
+ * and the scope is not opened to discover that.
2774
+ */
2775
+ async readScopeStoreSizes(tenantId, scopeId) {
2776
+ const bindings = [...this.scopeStoreRunners.keys()];
2777
+ if (bindings.length === 0)
2778
+ return [];
2779
+ this.scopeReadDbFor(tenantId, scopeId);
2780
+ const vertical = this.scopeStoreVerticalOf(scopeId);
2781
+ const sizes = [];
2782
+ for (const binding of bindings) {
2783
+ const { bytes } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.size');
2784
+ sizes.push({ binding, bytes });
2785
+ }
2786
+ return sizes;
2787
+ }
2788
+ /** The vertical whose stores this scope has on this host, or nothing when it has none. */
2789
+ storeVerticalOf(scopeId) {
2790
+ if (this.scopeStoreRunners.size === 0)
2791
+ return undefined;
2792
+ const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
2793
+ const vertical = row?.vertical ?? undefined;
2794
+ if (!vertical)
2795
+ return undefined;
2796
+ if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical)
2797
+ return undefined;
2798
+ return vertical;
2799
+ }
2800
+ /** The erasure claim (`SUBJECT_ERASURE_CLAIM_SQL`); a scope that moved refuses with the key kept. */
2801
+ claimSubjectErasure(tenantId, scopeId, fence) {
2802
+ const claimed = this.directory.prepare(SUBJECT_ERASURE_CLAIM_SQL).run(...subjectErasureClaimParams(tenantId, scopeId, fence));
2803
+ if (claimed.changes === 0) {
2804
+ throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
2805
+ }
2806
+ }
2807
+ /**
2808
+ * `substrat.erase` on each declared binding. A store error throws, so the caller
2809
+ * can leave the subject key untouched. Fail closed (#2242): an empty answer means
2810
+ * this host KNOWS it holds nothing for the scope. It always can tell — the
2811
+ * directory names the scope's vertical, and the runners are this host's own — so
2812
+ * unlike a Cloudflare host with no control plane it has no "cannot tell" case.
2813
+ */
2814
+ async eraseScopeStores(tenantId, scopeId, subjectId) {
2815
+ const bindings = [...this.scopeStoreRunners.keys()];
2816
+ // Shred calls this for every scope. No vertical names no store object, and a
2817
+ // vertical this host is not pinned to is one its runners never served: both hold
2818
+ // nothing here, and the key is still destroyed.
2819
+ const vertical = this.storeVerticalOf(scopeId);
2820
+ if (!vertical)
2821
+ return [];
2822
+ const erasures = [];
2823
+ for (const binding of bindings) {
2824
+ const { erased } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.erase', { subjectId });
2825
+ erasures.push({ binding, erased });
2826
+ }
2827
+ return erasures;
2828
+ }
2829
+ /**
2830
+ * 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,
2832
+ * any store at all for a destination with no vertical (no store object can be named for
2833
+ * it, so loading would drop them), or a destination on a vertical other than the one this
2834
+ * host is pinned to.
2835
+ */
2836
+ refuseUnusableScopeStores(stores, vertical) {
2837
+ const listed = stores ?? [];
2838
+ if (listed.length === 0)
2839
+ return;
2840
+ if (vertical === null) {
2841
+ throw substratError('conflict', 'dump carries scope stores, and the destination scope has no vertical to hold them');
2842
+ }
2843
+ if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
2844
+ throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and the destination scope runs '${vertical}'`);
2845
+ }
2846
+ for (const store of listed) {
2847
+ const runner = this.scopeStoreRunners.get(store.binding);
2848
+ if (!runner) {
2849
+ throw substratError('not_found', `dump names scope store '${store.binding}', which is not bound`);
2850
+ }
2851
+ if (!store.kind) {
2852
+ throw substratError('validation_failed', `dump's scope store '${store.binding}' names no kind`);
2853
+ }
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}'`);
2856
+ }
2857
+ }
2858
+ }
2859
+ async exportScopeStores(tenantId, scopeId) {
2860
+ const bindings = [...this.scopeStoreRunners.keys()];
2861
+ if (bindings.length === 0)
2862
+ return [];
2863
+ const vertical = this.scopeStoreVerticalIfPresent(scopeId);
2864
+ if (!vertical)
2865
+ return [];
2866
+ const stores = [];
2867
+ for (const binding of bindings) {
2868
+ const runner = this.scopeStoreRunners.get(binding);
2869
+ if (!runner)
2870
+ throw substratError('not_found', `no scope store is bound for '${binding}'`);
2871
+ const exported = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.export');
2872
+ stores.push({ binding, kind: runner.kind, tables: exported.tables });
2873
+ }
2874
+ return stores;
2875
+ }
2876
+ async importScopeStores(tenantId, scopeId, stores) {
2877
+ const listed = stores ?? [];
2878
+ if (listed.length === 0)
2879
+ return;
2880
+ const vertical = this.scopeStoreVerticalIfPresent(scopeId);
2881
+ if (!vertical) {
2882
+ throw substratError('conflict', `dump carries scope stores, and scope ${scopeId} has no vertical to hold them`);
2883
+ }
2884
+ for (const store of listed) {
2885
+ await this.invokeReservedStore({ tenantId, vertical, scopeId, binding: store.binding }, 'substrat.import', { tables: store.tables });
2886
+ }
2887
+ }
2888
+ /**
2889
+ * `substrat.erase` again for every tombstoned subject (#2207), on the target's and the
2890
+ * source's marks (`replayStoreErasures`). A target this host names no store for (no
2891
+ * runner, or no vertical a store is kept for) has nothing to replay into: nothing is read.
2892
+ */
2893
+ async replayShreddedStoreErasures(actor, tenantId, target, source) {
2894
+ if (!this.storeVerticalOf(target))
2895
+ return undefined;
2896
+ return replayStoreErasures({
2897
+ admin: this.admin,
2898
+ actor,
2899
+ tenantId,
2900
+ target,
2901
+ source,
2902
+ erase: async (subjectId) => (await this.eraseScopeStores(tenantId, target, subjectId)).length,
2903
+ });
2904
+ }
2905
+ /** `substrat.wipe` on each declared binding, before the scope's own storage goes. */
2906
+ async wipeScopeStores(tenantId, scopeId) {
2907
+ const bindings = [...this.scopeStoreRunners.keys()];
2908
+ if (bindings.length === 0)
2909
+ return;
2910
+ const vertical = this.scopeStoreVerticalIfPresent(scopeId);
2911
+ if (!vertical)
2912
+ return;
2913
+ for (const binding of bindings) {
2914
+ await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.wipe');
2915
+ }
2916
+ }
2638
2917
  /**
2639
2918
  * A scope stub whose authority is a CONNECTION (#97).
2640
2919
  *
@@ -4072,10 +4351,15 @@ export class SqliteScopeHost {
4072
4351
  }
4073
4352
  }
4074
4353
  async close() {
4075
- for (const rt of [...this.scopes.values()])
4076
- this.closeRuntime(rt);
4077
- this.directory.close();
4078
- this.owner.release();
4354
+ try {
4355
+ await this.scopeStoreLifecycle.stop();
4356
+ }
4357
+ finally {
4358
+ for (const rt of [...this.scopes.values()])
4359
+ this.closeRuntime(rt);
4360
+ this.directory.close();
4361
+ this.owner.release();
4362
+ }
4079
4363
  }
4080
4364
  // -------------------------------------------------------------------------
4081
4365
  // Event dispatch (testrun spec §9.2.3): at-least-once, kernel-journaled,
@@ -6118,14 +6402,16 @@ export class SqliteScopeHost {
6118
6402
  }
6119
6403
  this.recordAdmin(actor, 'setHostnameIssuance', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status: fields.status, note: fields.note ?? null });
6120
6404
  },
6121
- unbindHostname: async (actor, raw) => {
6405
+ unbindHostname: async (actor, raw, owner) => {
6122
6406
  const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
6407
+ // One statement: the row it removes is the row it audits, and an owner is judged by the
6408
+ // delete itself, so a name rebound to another scope in between is left alone (#2285).
6123
6409
  const row = this.directory
6124
- .prepare('SELECT tenant_id, scope_id, status FROM hostnames WHERE hostname = ?')
6125
- .get(hostname);
6410
+ .prepare(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
6411
+ RETURNING tenant_id, scope_id, status`)
6412
+ .get(hostname, ...(owner ? [owner.tenantId, owner.scopeId] : []));
6126
6413
  if (!row)
6127
6414
  return; // idempotent, and a no-op is not audited
6128
- this.directory.prepare('DELETE FROM hostnames WHERE hostname = ?').run(hostname);
6129
6415
  this.recordAdmin(actor, 'unbindHostname', { tenantId: row.tenant_id, scopeId: row.scope_id }, { hostname, status: row.status }, null);
6130
6416
  },
6131
6417
  listHostnames: async (actor, filter) => {
@@ -6361,6 +6647,8 @@ export class SqliteScopeHost {
6361
6647
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`)
6362
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());
6363
6649
  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.
6651
+ indexVersionScopeStores(switchSqlOf(this.directory), audited.verticalSlug, audited.id, split.manifestJson);
6364
6652
  })();
6365
6653
  this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
6366
6654
  ...audited,
@@ -6368,6 +6656,12 @@ export class SqliteScopeHost {
6368
6656
  admission: selfAdmits ? 'admitted' : 'pending',
6369
6657
  });
6370
6658
  },
6659
+ verticalScopeStores: async (actor, verticalSlug) => {
6660
+ // One transaction: versions not indexed yet are indexed by the read (#2286).
6661
+ const stores = this.directory.transaction(() => verticalScopeStoresOf(switchSqlOf(this.directory), verticalSlug))();
6662
+ this.recordAccess(actor, 'verticalScopeStores', {}, { verticalSlug }, stores.length);
6663
+ return stores;
6664
+ },
6371
6665
  listVersions: async (actor, verticalSlug, page) => {
6372
6666
  const where = ['vertical_slug = ?'];
6373
6667
  const params = [verticalSlug];
@@ -6471,6 +6765,7 @@ export class SqliteScopeHost {
6471
6765
  .prepare(`DELETE FROM vertical_version_migrations
6472
6766
  WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`)
6473
6767
  .run(slug);
6768
+ forgetVerticalScopeStores(switchSqlOf(this.directory), slug);
6474
6769
  this.directory.prepare('DELETE FROM vertical_versions WHERE vertical_slug = ?').run(slug);
6475
6770
  this.directory.prepare('DELETE FROM verticals WHERE slug = ?').run(slug);
6476
6771
  this.recordAdmin(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.ownerTenant }, null);
@@ -7227,6 +7522,11 @@ export class SqliteScopeHost {
7227
7522
  this.recordAccess(actor, 'scopeDatabaseSize', { tenantId, scopeId }, null, 1);
7228
7523
  return bytes;
7229
7524
  },
7525
+ scopeStoreSizes: async (actor, tenantId, scopeId) => {
7526
+ const sizes = await this.readScopeStoreSizes(tenantId, scopeId);
7527
+ this.recordAccess(actor, 'scopeStoreSizes', { tenantId, scopeId }, null, sizes.length);
7528
+ return sizes;
7529
+ },
7230
7530
  rewindScope: async () => {
7231
7531
  throw new Error('point-in-time rewind is not available on this host (PITR is a Durable-Object-plane mechanism) — use the backup restore path');
7232
7532
  },
@@ -7482,6 +7782,48 @@ export class SqliteScopeHost {
7482
7782
  createdAt: r.created_at,
7483
7783
  }));
7484
7784
  },
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
7787
+ // Cloudflare control plane; each call is one transaction. A hold's expiry is judged against
7788
+ // this host's clock, so a test can let a lease lapse without waiting.
7789
+ recordStoreKindRelease: async (actor, input) => {
7790
+ const at = this.clock();
7791
+ const { record, created } = this.directory.transaction(() => recordStoreKindReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
7792
+ if (created)
7793
+ this.recordAdmin(actor, 'uploadStoreKindRelease', { tenantId: null }, null, record, at);
7794
+ return record;
7795
+ },
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);
7799
+ return record;
7800
+ },
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);
7804
+ return state;
7805
+ },
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);
7809
+ return declared;
7810
+ },
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);
7814
+ },
7815
+ beginStoreKindRollout: async (actor, input) => {
7816
+ 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);
7819
+ return row;
7820
+ },
7821
+ finishStoreKindRollout: async (actor, input) => {
7822
+ 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);
7825
+ return row;
7826
+ },
7485
7827
  getScopeRecord: async (actor, tenantId, scopeId) => {
7486
7828
  const r = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
7487
7829
  // Cross-check the pair (K-3): a scope that exists under a DIFFERENT tenant
@@ -7622,7 +7964,8 @@ export class SqliteScopeHost {
7622
7964
  return { name, ddl: sql, columns, rows };
7623
7965
  });
7624
7966
  this.recordAccess(actor, 'exportScope', { tenantId, scopeId }, null, tables.length);
7625
- return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables };
7967
+ const stores = await this.exportScopeStores(tenantId, scopeId);
7968
+ return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables, stores };
7626
7969
  },
7627
7970
  // -- directory disaster recovery (#40) ----------------------------------
7628
7971
  // The self-hosted half of the same story. A self-host has no DO point-in-time
@@ -7762,6 +8105,7 @@ export class SqliteScopeHost {
7762
8105
  const rt = this.scopes.get(`${tenantId}/${scopeId}`);
7763
8106
  if (rt)
7764
8107
  this.closeRuntime(rt);
8108
+ await this.wipeScopeStores(tenantId, scopeId);
7765
8109
  rmSync(join(this.dir, `${tenantId}__${scopeId}.sqlite`), { force: true });
7766
8110
  // The recoverable copy the caller stored first (#493), named in the audit entry so
7767
8111
  // the trail answers "was there a backup" without correlating two timestamps.
@@ -7789,13 +8133,29 @@ export class SqliteScopeHost {
7789
8133
  },
7790
8134
  // `stamp` is the erasure epoch an orchestrated erasure's claim compared (#1722). A direct shred
7791
8135
  // claims nothing and records the epoch it ran under: the backfill orders erasures by them.
7792
- shredSubject: async (actor, tenantId, scopeId, subjectId, stamp) => {
8136
+ listShreddedSubjects: async (actor, tenantId, scopeId, cursor) => {
8137
+ this.assertScope(tenantId, scopeId);
8138
+ const rows = this.directory
8139
+ .prepare(`SELECT subject_id FROM _substrat_subject_keys
8140
+ WHERE scope_id = ? AND tenant_id = ? AND shredded_at IS NOT NULL AND subject_id > ?
8141
+ ORDER BY subject_id LIMIT 101`)
8142
+ .all(scopeId, tenantId, cursor ?? '');
8143
+ const subjects = rows.slice(0, 100).map((row) => row.subject_id);
8144
+ const nextCursor = rows.length > 100 ? (subjects[99] ?? null) : null;
8145
+ this.recordAccess(actor, 'listShreddedSubjects', { tenantId, scopeId }, null, subjects.length);
8146
+ return { subjects, nextCursor };
8147
+ },
8148
+ shredSubject: async (actor, tenantId, scopeId, subjectId, fence, stamp, storeErasures) => {
7793
8149
  this.assertScope(tenantId, scopeId);
7794
8150
  const before = stamp ?? {
7795
8151
  erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
7796
8152
  ?.erasure_epoch ?? 0,
7797
8153
  path: 'direct',
7798
8154
  };
8155
+ // #2285: a direct shred with a fence claims what it read, as the finalize does; the epoch
8156
+ // it records is the one its claim compared.
8157
+ if (fence)
8158
+ before.erasureEpoch = fence.epoch;
7799
8159
  // Redact the live spine FIRST. Both halves are idempotent and a crash between them
7800
8160
  // converges on retry, so the order is decided by which half-done state harms the
7801
8161
  // person: dying after this leaves ciphertext in a backup that no key opens; dying
@@ -7864,6 +8224,13 @@ export class SqliteScopeHost {
7864
8224
  // intents, an issue's exemplar, a sweep record's error. Before the key, for the
7865
8225
  // ordering reason above.
7866
8226
  redactSubjectDirectoryText(redactionSqlOf(this.directory), { tenantId, scopeId, subjectId, intentIds });
8227
+ // Scope stores after the scope-side redaction and before the key (#2068). A store
8228
+ // that errors throws here, so the key below is untouched and a retry converges.
8229
+ // An orchestrated caller passes the serving script's result and this host does
8230
+ // not erase again.
8231
+ const erasures = storeErasures ?? (await this.eraseScopeStores(tenantId, scopeId, subjectId));
8232
+ if (fence)
8233
+ this.claimSubjectErasure(tenantId, scopeId, fence);
7867
8234
  const { existed } = await this.subjectKeysFor(tenantId, scopeId).destroy(subjectId, at);
7868
8235
  const receipt = subjectShredReceipt.parse({
7869
8236
  subjectId,
@@ -7871,6 +8238,7 @@ export class SqliteScopeHost {
7871
8238
  intentsRedacted,
7872
8239
  jobRunsRedacted,
7873
8240
  ...vertical,
8241
+ storeErasures: erasures,
7874
8242
  keyDestroyed: existed,
7875
8243
  tombstoned: true,
7876
8244
  });
@@ -7891,23 +8259,14 @@ export class SqliteScopeHost {
7891
8259
  throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
7892
8260
  return row.erasure_epoch;
7893
8261
  },
7894
- finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected) => {
8262
+ finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected, storeErasures) => {
7895
8263
  if (redactions.length === 0)
7896
8264
  throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
7897
- const claimed = this.directory.prepare(`UPDATE scopes SET erasure_epoch = COALESCE(erasure_epoch, 0) + 1
7898
- WHERE tenant_id = ? AND scope_id = ? AND vertical_version_id IS ?
7899
- AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
7900
- AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
7901
- WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
7902
- AND copy.state = 'pending')
7903
- AND (SELECT COUNT(*) FROM scope_script_copies AS copy
7904
- 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);
7905
- if (claimed.changes === 0)
7906
- throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
8265
+ this.claimSubjectErasure(tenantId, scopeId, expected);
7907
8266
  // The pure adapter has one co-located scope store. Its existing atomic shred is
7908
8267
  // the final local redaction and key destruction, after remote copies confirmed.
7909
8268
  const shred = this.admin.shredSubject;
7910
- return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' });
8269
+ return shred(actor, tenantId, scopeId, subjectId, undefined, { erasureEpoch: expected.epoch, path: 'orchestrated' }, storeErasures);
7911
8270
  },
7912
8271
  // -- impersonation (K-42, #868) ----------------------------------------
7913
8272
  beginImpersonation: async (actor, input) => {
@@ -7983,6 +8342,7 @@ export class SqliteScopeHost {
7983
8342
  '_substrat_finding_rules', // #1748: the tenant's suppress rules
7984
8343
  '_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
7985
8344
  '_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
8345
+ '_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
7986
8346
  ];
7987
8347
  const clear = this.directory.transaction(() => {
7988
8348
  for (const table of tables) {
@@ -8091,6 +8451,7 @@ export class SqliteScopeHost {
8091
8451
  // #1524: the stored gauge's latest sample per non-reaped scope. A directory read only.
8092
8452
  storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
8093
8453
  storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
8454
+ storeStorage: listScopeStoreStorageRows(redactionSqlOf(this.directory), only),
8094
8455
  });
8095
8456
  // The count that matters for K-24 is how many TENANTS this reading covered —
8096
8457
  // "read the meter for one tenant" and "metered the whole fleet" are different
@@ -8541,6 +8902,10 @@ export class SqliteScopeHost {
8541
8902
  const { tenantId, scopeId, ...after } = memberChangeAudit.parse(entry);
8542
8903
  this.recordAdmin(actor, 'manageScopeMember', { tenantId, scopeId }, null, after);
8543
8904
  },
8905
+ recordConnectionBindingChange: async (actor, entry) => {
8906
+ const { tenantId, scopeId, ...after } = connectionBindingAudit.parse(entry);
8907
+ this.recordAdmin(actor, 'bindConnectionScope', { tenantId, scopeId }, null, after);
8908
+ },
8544
8909
  recordPeerBindingChange: async (actor, entry) => {
8545
8910
  const { tenantId, scopeId, ...after } = peerBindingAudit.parse(entry);
8546
8911
  this.recordAdmin(actor, 'setPeerBinding', { tenantId, scopeId }, null, after);
@@ -9707,6 +10072,18 @@ export class SqliteScopeHost {
9707
10072
  assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
9708
10073
  minted,
9709
10074
  }),
10075
+ // Upload links: mint / revoke / list, written once in the kernel — the same four things
10076
+ // `capabilities` is handed, and the same consumer-is-system-actor rule.
10077
+ attachments: createAttachmentVerbs({
10078
+ sql: spineSql(rt.db),
10079
+ subject: overrideActor ? { kind: 'system', id: overrideActor.system } : subject,
10080
+ now: at,
10081
+ check: runCheck,
10082
+ emit: (event) => writeEvent(event, 'kernel'),
10083
+ assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
10084
+ minted,
10085
+ targetOf: (entityType) => this.uploadTargetOf(entityType),
10086
+ }),
9710
10087
  // K-16 / #1864: link and relink, written once in the kernel over the raw spine seam.
9711
10088
  ...createEntityEdgeVerbs({
9712
10089
  sql: spineSql(rt.db),