@substrat-run/adapter-sqlite 0.147.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.d.ts +54 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +456 -106
- package/dist/index.js.map +1 -1
- package/dist/scope-store-runners.d.ts +15 -0
- package/dist/scope-store-runners.d.ts.map +1 -0
- package/dist/scope-store-runners.js +24 -0
- package/dist/scope-store-runners.js.map +1 -0
- package/package.json +6 -6
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, 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, 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 } 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, normalizeUploadMediaType } 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}
|
|
@@ -318,11 +322,19 @@ const KERNEL_DDL = `
|
|
|
318
322
|
-- #1575: attachment text and its FTS5 index, shared from @substrat-run/kernel. After
|
|
319
323
|
-- _substrat_attachments, whose delete trigger it adds.
|
|
320
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}
|
|
321
328
|
-- #901: an entity's version is the ULID of the last event about it (a state move
|
|
322
329
|
-- excepted, #2191), so the newest id per (entity_type, entity_id) is the read. The id
|
|
323
330
|
-- column sits last so SQLite walks the matched range backwards instead of aggregating.
|
|
324
331
|
${OUTBOX_ENTITY_INDEX}
|
|
325
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;
|
|
326
338
|
/** Row → contract shape. Never reads the secrets table — that is the point of the split. */
|
|
327
339
|
const toConnection = (r) => connection.parse({
|
|
328
340
|
id: r.id,
|
|
@@ -637,6 +649,8 @@ export class SqliteScopeHost {
|
|
|
637
649
|
erasurePlans = [];
|
|
638
650
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
639
651
|
attachmentTargets = new Map();
|
|
652
|
+
/** entityType → the `uploadLinks` policy its `attachmentTargets` entry declares; absent = no links. */
|
|
653
|
+
uploadLinkPolicies = new Map();
|
|
640
654
|
/** operation name → who binds it: the owning module, its entitlementKey and its declared
|
|
641
655
|
* schedules — the §4.3 gate's input, resolved per invoke by `requiredEntitlementFor` (#1654). */
|
|
642
656
|
operationEntitlement = new Map();
|
|
@@ -692,11 +706,13 @@ export class SqliteScopeHost {
|
|
|
692
706
|
secretBox;
|
|
693
707
|
/** The parsers attachment text is extracted with (K-43); the host's own, never imported here. */
|
|
694
708
|
attachmentExtractors;
|
|
709
|
+
wrapBlobStore;
|
|
695
710
|
attachmentTextBounds;
|
|
696
711
|
fetchImpl;
|
|
697
712
|
connectorCalls;
|
|
698
713
|
clock;
|
|
699
714
|
versionId;
|
|
715
|
+
migrationsDigest;
|
|
700
716
|
/** #1746: where `ctx.log` lines go — the console unless a caller (a test) says otherwise. */
|
|
701
717
|
logSink;
|
|
702
718
|
/** #1901: where async work's invocation lines go. */
|
|
@@ -710,11 +726,13 @@ export class SqliteScopeHost {
|
|
|
710
726
|
this.secretBox = options.secretBox ?? unconfiguredSecretBox;
|
|
711
727
|
assertAttachmentExtractors(options.attachmentExtractors ?? []);
|
|
712
728
|
this.attachmentExtractors = options.attachmentExtractors ?? [];
|
|
729
|
+
this.wrapBlobStore = options.wrapBlobStore ?? ((store) => store);
|
|
713
730
|
this.attachmentTextBounds = resolveAttachmentTextBounds(options.attachmentTextBounds);
|
|
714
731
|
this.fetchImpl = options.fetch ?? globalFetch;
|
|
715
732
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
716
733
|
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
717
734
|
this.versionId = options.versionId ?? null;
|
|
735
|
+
this.migrationsDigest = options.migrationsDigest;
|
|
718
736
|
this.scopeStoreVertical = options.scopeStores?.vertical;
|
|
719
737
|
this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
|
|
720
738
|
this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
|
|
@@ -737,17 +755,17 @@ export class SqliteScopeHost {
|
|
|
737
755
|
this.directory.pragma('journal_mode = WAL');
|
|
738
756
|
this.ensureDirectorySchema();
|
|
739
757
|
this.loadRoles();
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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());
|
|
751
769
|
this.admin = this.buildAdmin();
|
|
752
770
|
this.owner = owner;
|
|
753
771
|
}
|
|
@@ -936,6 +954,8 @@ export class SqliteScopeHost {
|
|
|
936
954
|
created_at TEXT NOT NULL,
|
|
937
955
|
PRIMARY KEY (tenant_id, vertical, binding)
|
|
938
956
|
);
|
|
957
|
+
-- A store driver's stored releases, rollouts and holds (D-63, #2300).
|
|
958
|
+
${STORE_DRIVER_RELEASES_DDL}
|
|
939
959
|
-- The vertical + version registry (#31). A scope binds to a VERSION, so
|
|
940
960
|
-- dev/staging/prod are the same vertical pinned differently, and a preview
|
|
941
961
|
-- deployment is a version nothing has been promoted to yet.
|
|
@@ -1002,10 +1022,14 @@ export class SqliteScopeHost {
|
|
|
1002
1022
|
migration_count INTEGER,
|
|
1003
1023
|
-- 1 once the SQL is out of manifest_json. NULL = stored before #1764, not yet backfilled.
|
|
1004
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,
|
|
1005
1028
|
created_at TEXT NOT NULL,
|
|
1006
1029
|
UNIQUE (vertical_slug, version)
|
|
1007
1030
|
);
|
|
1008
1031
|
${VERSION_MIGRATIONS_DDL}
|
|
1032
|
+
${VERTICAL_SCOPE_STORES_DDL}
|
|
1009
1033
|
-- Channels (#31 step 2): a named pointer per vertical. Promotion moves it,
|
|
1010
1034
|
-- and promotion is where the migration and permission diffs fire — the
|
|
1011
1035
|
-- moment a change reaches anyone, rather than the moment it was typed.
|
|
@@ -1405,13 +1429,27 @@ export class SqliteScopeHost {
|
|
|
1405
1429
|
* it inside its transaction onto an emptied directory, before loading any row.
|
|
1406
1430
|
*/
|
|
1407
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);
|
|
1408
1443
|
// #1764: `VERSION_MIGRATIONS_DDL` indexes these two columns, so a directory created
|
|
1409
1444
|
// before them needs them before `applyDirectorySchema` reaches that statement.
|
|
1410
1445
|
if (this.directory.prepare('PRAGMA table_info(vertical_versions)').all().length > 0) {
|
|
1411
1446
|
this.ensureColumn(this.directory, 'vertical_versions', 'migration_count', 'migration_count INTEGER');
|
|
1412
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');
|
|
1413
1449
|
}
|
|
1414
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');
|
|
1415
1453
|
}
|
|
1416
1454
|
/**
|
|
1417
1455
|
* `applyDirectorySchema`, plus the backfill of the schedule switch's record (#1674) from
|
|
@@ -1673,6 +1711,7 @@ export class SqliteScopeHost {
|
|
|
1673
1711
|
}
|
|
1674
1712
|
// #1705: validated whole and kept only if it passes, so a refused module leaves nothing.
|
|
1675
1713
|
this.crossVertical.register(manifest, registration.imports);
|
|
1714
|
+
this.peerUnion = undefined; // #2263: the next check reads the new module set's declarations
|
|
1676
1715
|
this.modules.set(manifest.id, {
|
|
1677
1716
|
id: manifest.id,
|
|
1678
1717
|
// The order the kernel writes once (#1677): authored, then search, then list indexes.
|
|
@@ -1707,6 +1746,13 @@ export class SqliteScopeHost {
|
|
|
1707
1746
|
`(${existing.read}/${existing.write}) vs (${gate.read}/${gate.write})`);
|
|
1708
1747
|
}
|
|
1709
1748
|
this.attachmentTargets.set(target.entityType, gate);
|
|
1749
|
+
if (target.uploadLinks) {
|
|
1750
|
+
const known = this.uploadLinkPolicies.get(target.entityType);
|
|
1751
|
+
if (known && JSON.stringify(known) !== JSON.stringify(target.uploadLinks)) {
|
|
1752
|
+
throw new Error(`conflicting uploadLinks policies for attachment target '${target.entityType}'`);
|
|
1753
|
+
}
|
|
1754
|
+
this.uploadLinkPolicies.set(target.entityType, target.uploadLinks);
|
|
1755
|
+
}
|
|
1710
1756
|
}
|
|
1711
1757
|
// WITHDRAWAL (K-17): suppress another module's default binding. Order
|
|
1712
1758
|
// independent — a manifest may withdraw an operation whose module has not
|
|
@@ -2057,7 +2103,7 @@ export class SqliteScopeHost {
|
|
|
2057
2103
|
out.push(r);
|
|
2058
2104
|
}
|
|
2059
2105
|
};
|
|
2060
|
-
return {
|
|
2106
|
+
return this.wrapBlobStore({
|
|
2061
2107
|
put: async (key, body, opts) => {
|
|
2062
2108
|
const p = resolveKey(key);
|
|
2063
2109
|
mkdirSync(dirname(p), { recursive: true });
|
|
@@ -2089,7 +2135,7 @@ export class SqliteScopeHost {
|
|
|
2089
2135
|
walk(root, '', out);
|
|
2090
2136
|
return out.filter((k) => k.startsWith(prefix)).sort();
|
|
2091
2137
|
},
|
|
2092
|
-
};
|
|
2138
|
+
}, ref);
|
|
2093
2139
|
}
|
|
2094
2140
|
/** Resolve the blob store that carries a scope's attachments (#473): the platform-minted
|
|
2095
2141
|
* store for (tenant, the scope's vertical). Fails loudly when none was provisioned —
|
|
@@ -2124,6 +2170,100 @@ export class SqliteScopeHost {
|
|
|
2124
2170
|
const store = this.attachmentStore(tenantId, scopeRow?.vertical ?? null);
|
|
2125
2171
|
return this.buildAttachments(rt, asPrincipal(principal), store);
|
|
2126
2172
|
}
|
|
2173
|
+
/** The declared write key and link policy of an attachment target — what `ctx.attachments` mints against. */
|
|
2174
|
+
uploadTargetOf(entityType) {
|
|
2175
|
+
const gate = this.attachmentTargets.get(entityType);
|
|
2176
|
+
if (!gate)
|
|
2177
|
+
return undefined;
|
|
2178
|
+
const uploadLinks = this.uploadLinkPolicies.get(entityType);
|
|
2179
|
+
return uploadLinks ? { write: gate.write, uploadLinks } : { write: gate.write };
|
|
2180
|
+
}
|
|
2181
|
+
async inspectUploadLink(tenantId, scopeId, secret, target, idempotencyKey) {
|
|
2182
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
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());
|
|
2244
|
+
}
|
|
2245
|
+
async redeemUploadLink(tenantId, scopeId, secret, input) {
|
|
2246
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
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,
|
|
2264
|
+
});
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2127
2267
|
async getSystemAttachments(moduleId, tenantId, scopeId) {
|
|
2128
2268
|
// The same registered-module and lifecycle gate as a system invoke. The
|
|
2129
2269
|
// attachment surface checks the target's read key as system:<moduleId>.
|
|
@@ -2188,8 +2328,17 @@ export class SqliteScopeHost {
|
|
|
2188
2328
|
}
|
|
2189
2329
|
// #1525: null, for the reason the denial two lines up records null — this task
|
|
2190
2330
|
// is the attachment RPC's own and no invoke set an id on the runtime.
|
|
2191
|
-
|
|
2192
|
-
|
|
2331
|
+
// The write has COMMITTED, so a failed drain must not reach the caller as a refusal:
|
|
2332
|
+
// `upload` deletes the bytes on a refusal, which would leave this row without them
|
|
2333
|
+
// (and a redeemed link spent) (#2311). An undelivered event has no delivery row, so
|
|
2334
|
+
// the next drain in this scope delivers it.
|
|
2335
|
+
try {
|
|
2336
|
+
await this.dispatch(rt, null);
|
|
2337
|
+
await this.dispatchExecutors(rt, null);
|
|
2338
|
+
}
|
|
2339
|
+
catch (err) {
|
|
2340
|
+
console.error('substrat: consumer drain failed after an attachment committed', err);
|
|
2341
|
+
}
|
|
2193
2342
|
return result;
|
|
2194
2343
|
});
|
|
2195
2344
|
/**
|
|
@@ -2248,13 +2397,15 @@ export class SqliteScopeHost {
|
|
|
2248
2397
|
createdBy: creator.id,
|
|
2249
2398
|
createdAt: new Date().toISOString(),
|
|
2250
2399
|
});
|
|
2251
|
-
// Bytes first, row second: a crash between the two leaves an orphaned object
|
|
2252
|
-
//
|
|
2253
|
-
//
|
|
2400
|
+
// Bytes first, row second: a crash between the two leaves an orphaned object that no
|
|
2401
|
+
// row points at (#2314). A refused check compensates the object away below; a drain
|
|
2402
|
+
// fault after the commit is not a refusal (`serialized` keeps it from becoming one).
|
|
2254
2403
|
await store.put(key, input.body, { contentType: input.contentType });
|
|
2255
2404
|
try {
|
|
2256
2405
|
return await guarded('attachments.upload', async (ctx) => {
|
|
2257
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);
|
|
2258
2409
|
rt.db
|
|
2259
2410
|
.prepare(`INSERT INTO _substrat_attachments
|
|
2260
2411
|
(id, entity_type, entity_id, filename, content_type, size, sha256,
|
|
@@ -2271,6 +2422,11 @@ export class SqliteScopeHost {
|
|
|
2271
2422
|
// #1575: queue its text extraction in the same transaction — a `pending` row
|
|
2272
2423
|
// and a job run. Nothing is extracted here, so nothing here can fail the upload.
|
|
2273
2424
|
enqueueAttachmentText(spineSql(rt.db), record.id, ulid(), this.clock());
|
|
2425
|
+
// An upload-link redemption spends its link HERE, in the same transaction as the
|
|
2426
|
+
// row it produced: a refusal anywhere above or below rolls the spend back.
|
|
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);
|
|
2429
|
+
}
|
|
2274
2430
|
return record;
|
|
2275
2431
|
});
|
|
2276
2432
|
}
|
|
@@ -2409,6 +2565,16 @@ export class SqliteScopeHost {
|
|
|
2409
2565
|
// rows are gone and the new ones are in. It also covers what a topological sort
|
|
2410
2566
|
// cannot express: FK cycles, and self-referencing rows within one table.
|
|
2411
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);
|
|
2412
2578
|
// Drop the current schema: a vertical's tables are rebuilt from the dump's DDL, the
|
|
2413
2579
|
// spine from KERNEL_DDL (below). Only real tables (never `sqlite_*` internals, which
|
|
2414
2580
|
// are auto-managed and un-droppable).
|
|
@@ -2423,10 +2589,6 @@ export class SqliteScopeHost {
|
|
|
2423
2589
|
continue;
|
|
2424
2590
|
db.exec(`DROP TABLE IF EXISTS "${name}"`);
|
|
2425
2591
|
}
|
|
2426
|
-
// A dump written before indexes were excluded may still carry them; skip those
|
|
2427
|
-
// too rather than failing a restore over data that is about to be recomputed.
|
|
2428
|
-
// Matched without case, as SQLite resolves a table name (#1883 review).
|
|
2429
|
-
const replayable = dumped.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
|
|
2430
2592
|
// The dump is untrusted input (#1143): its names reach SQL as identifiers, and
|
|
2431
2593
|
// `db.exec` would run every statement its `ddl` contains, not just the CREATE
|
|
2432
2594
|
// TABLE. Judged as a whole before any of it executes — a check interleaved with
|
|
@@ -2438,9 +2600,6 @@ export class SqliteScopeHost {
|
|
|
2438
2600
|
// contributes only rows, by column name (`dumpRowsInsert`). Otherwise a dump could
|
|
2439
2601
|
// declare a column the checker compares on as COLLATE NOCASE. The column pass follows,
|
|
2440
2602
|
// for the one outbox index KERNEL_DDL leaves to it.
|
|
2441
|
-
// A Durable Object's dump carries spine tables this adapter keeps in its directory, or does
|
|
2442
|
-
// not keep at all; they are skipped by name, and any other unknown spine table is refused.
|
|
2443
|
-
const loadable = replayable.filter((t) => !DO_SCOPE_ONLY_SPINE_TABLES.has(t.name.toLowerCase()));
|
|
2444
2603
|
// Comment-blanked (#2068), as the Durable Object replays it: the dump's DDL is
|
|
2445
2604
|
// `sqlite_master.sql` verbatim, and `prepare` still compiles exactly one statement.
|
|
2446
2605
|
for (const t of loadable)
|
|
@@ -2531,6 +2690,8 @@ export class SqliteScopeHost {
|
|
|
2531
2690
|
if (!existing)
|
|
2532
2691
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2533
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);
|
|
2534
2695
|
// #1742: a dump from before a switch was pulled brings the module's grants back live, so
|
|
2535
2696
|
// the directory's recorded-off modules go back off in the load's own turn, as a
|
|
2536
2697
|
// provision's seat does — no schedule pass can run in between.
|
|
@@ -2733,6 +2894,13 @@ export class SqliteScopeHost {
|
|
|
2733
2894
|
return undefined;
|
|
2734
2895
|
return vertical;
|
|
2735
2896
|
}
|
|
2897
|
+
/** The erasure claim (`SUBJECT_ERASURE_CLAIM_SQL`); a scope that moved refuses with the key kept. */
|
|
2898
|
+
claimSubjectErasure(tenantId, scopeId, fence) {
|
|
2899
|
+
const claimed = this.directory.prepare(SUBJECT_ERASURE_CLAIM_SQL).run(...subjectErasureClaimParams(tenantId, scopeId, fence));
|
|
2900
|
+
if (claimed.changes === 0) {
|
|
2901
|
+
throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2736
2904
|
/**
|
|
2737
2905
|
* `substrat.erase` on each declared binding. A store error throws, so the caller
|
|
2738
2906
|
* can leave the subject key untouched. Fail closed (#2242): an empty answer means
|
|
@@ -2757,7 +2925,7 @@ export class SqliteScopeHost {
|
|
|
2757
2925
|
}
|
|
2758
2926
|
/**
|
|
2759
2927
|
* A dump's stores this host cannot load are refused before any write (#2211, L8): a
|
|
2760
|
-
* binding that is not bound, a store with no
|
|
2928
|
+
* binding that is not bound, a store with no driver or a driver other than the bound runner's,
|
|
2761
2929
|
* any store at all for a destination with no vertical (no store object can be named for
|
|
2762
2930
|
* it, so loading would drop them), or a destination on a vertical other than the one this
|
|
2763
2931
|
* host is pinned to.
|
|
@@ -2777,11 +2945,11 @@ export class SqliteScopeHost {
|
|
|
2777
2945
|
if (!runner) {
|
|
2778
2946
|
throw substratError('not_found', `dump names scope store '${store.binding}', which is not bound`);
|
|
2779
2947
|
}
|
|
2780
|
-
if (!store.
|
|
2781
|
-
throw substratError('validation_failed', `dump's scope store '${store.binding}' names no
|
|
2948
|
+
if (!store.driver) {
|
|
2949
|
+
throw substratError('validation_failed', `dump's scope store '${store.binding}' names no driver`);
|
|
2782
2950
|
}
|
|
2783
|
-
if (store.
|
|
2784
|
-
throw substratError('conflict', `dump's scope store '${store.binding}' is
|
|
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}'`);
|
|
2785
2953
|
}
|
|
2786
2954
|
}
|
|
2787
2955
|
}
|
|
@@ -2798,7 +2966,7 @@ export class SqliteScopeHost {
|
|
|
2798
2966
|
if (!runner)
|
|
2799
2967
|
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2800
2968
|
const exported = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.export');
|
|
2801
|
-
stores.push({ binding,
|
|
2969
|
+
stores.push({ binding, driver: runner.driver, tables: exported.tables });
|
|
2802
2970
|
}
|
|
2803
2971
|
return stores;
|
|
2804
2972
|
}
|
|
@@ -3385,10 +3553,15 @@ export class SqliteScopeHost {
|
|
|
3385
3553
|
const store = this.attachmentStore(tenantId, scopeRow?.vertical ?? null);
|
|
3386
3554
|
return this.buildAttachments(rt, { kind: 'capability-session', hash }, store);
|
|
3387
3555
|
}
|
|
3388
|
-
/**
|
|
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
|
+
*/
|
|
3389
3561
|
peerDeclarations() {
|
|
3390
|
-
return collectPeers(this.modules.values());
|
|
3562
|
+
return (this.peerUnion ??= collectPeers(this.modules.values()));
|
|
3391
3563
|
}
|
|
3564
|
+
peerUnion;
|
|
3392
3565
|
/**
|
|
3393
3566
|
* The peer door (#1706) — mirror of the connection, system and capability doors. The caller is
|
|
3394
3567
|
* the local broker's word (the platform's, on the pure host); the pair and lifecycle gate run
|
|
@@ -3762,7 +3935,8 @@ export class SqliteScopeHost {
|
|
|
3762
3935
|
try {
|
|
3763
3936
|
if (schedule.purge) {
|
|
3764
3937
|
// #119: a purge horizon's schedule runs its operation once per due entity, never once.
|
|
3765
|
-
|
|
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);
|
|
3766
3940
|
stillDue = purgeStillDue(pass);
|
|
3767
3941
|
const outcome = purgeReportOf(schedule.operation, schedule.purge.entityType, pass);
|
|
3768
3942
|
report.errors.push(...outcome.errors);
|
|
@@ -3833,7 +4007,9 @@ export class SqliteScopeHost {
|
|
|
3833
4007
|
* through a purge stub (`openSystemScope`) that never leaves this method, each in its own
|
|
3834
4008
|
* transaction and under the cutoff the host re-checks inside it (`refuseTrashedTarget`).
|
|
3835
4009
|
*/
|
|
3836
|
-
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) {
|
|
3837
4013
|
const entityType = this.modules
|
|
3838
4014
|
.get(moduleId)
|
|
3839
4015
|
?.schedules.find((s) => s.operation === operation && s.purge)?.purge?.entityType;
|
|
@@ -3860,7 +4036,7 @@ export class SqliteScopeHost {
|
|
|
3860
4036
|
return heldPurgePass(due.held);
|
|
3861
4037
|
const stub = await this.openSystemScope(moduleId, tenantId, scopeId, true);
|
|
3862
4038
|
return runPurgePass(operation, due, async (entityId) => {
|
|
3863
|
-
await stub.invoke(operation, { [due.idFrom]: entityId }, { invocationId:
|
|
4039
|
+
await stub.invoke(operation, { [due.idFrom]: entityId }, { invocationId: runInvocationId });
|
|
3864
4040
|
}, (write) => rt.actor.turn(() => write(spineSql(rt.db))));
|
|
3865
4041
|
}
|
|
3866
4042
|
/**
|
|
@@ -5460,10 +5636,15 @@ export class SqliteScopeHost {
|
|
|
5460
5636
|
calls: callsOfManifestJson(r.manifest_json),
|
|
5461
5637
|
createdAt: r.created_at,
|
|
5462
5638
|
});
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
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
|
+
}
|
|
5467
5648
|
return r ? mapVersion(r) : undefined;
|
|
5468
5649
|
};
|
|
5469
5650
|
const mapOrg = (r) => orgSchema.parse({
|
|
@@ -5529,8 +5710,8 @@ export class SqliteScopeHost {
|
|
|
5529
5710
|
});
|
|
5530
5711
|
// The (version, scope) pair a bind and its impact read both start from, and the refusals
|
|
5531
5712
|
// that come before any export-break question, in the order the bind makes them (#1756).
|
|
5532
|
-
const bindTarget = (tenantId, scopeId, versionId) => {
|
|
5533
|
-
const v = readVersion(versionId);
|
|
5713
|
+
const bindTarget = async (tenantId, scopeId, versionId) => {
|
|
5714
|
+
const v = await readVersion(versionId);
|
|
5534
5715
|
if (!v)
|
|
5535
5716
|
throw substratError('not_found', `unknown version ${versionId}`);
|
|
5536
5717
|
const scope = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
@@ -5911,7 +6092,7 @@ export class SqliteScopeHost {
|
|
|
5911
6092
|
throw substratError('not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
5912
6093
|
}
|
|
5913
6094
|
const rt = this.runtime(tenantId, scopeId);
|
|
5914
|
-
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()));
|
|
5915
6096
|
const offPeers = new Set(states.filter((p) => p.calls === 'off').map((p) => p.vertical));
|
|
5916
6097
|
const explanations = offPeers.size > 0 ? lastSwitchedOffBy(tenantId, scopeId, 'revokeFromPeer', 'vertical', offPeers) : new Map();
|
|
5917
6098
|
const result = states.map((p) => ({
|
|
@@ -6331,14 +6512,16 @@ export class SqliteScopeHost {
|
|
|
6331
6512
|
}
|
|
6332
6513
|
this.recordAdmin(actor, 'setHostnameIssuance', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status: fields.status, note: fields.note ?? null });
|
|
6333
6514
|
},
|
|
6334
|
-
unbindHostname: async (actor, raw) => {
|
|
6515
|
+
unbindHostname: async (actor, raw, owner) => {
|
|
6335
6516
|
const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
|
|
6517
|
+
// One statement: the row it removes is the row it audits, and an owner is judged by the
|
|
6518
|
+
// delete itself, so a name rebound to another scope in between is left alone (#2285).
|
|
6336
6519
|
const row = this.directory
|
|
6337
|
-
.prepare(
|
|
6338
|
-
|
|
6520
|
+
.prepare(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
|
|
6521
|
+
RETURNING tenant_id, scope_id, status`)
|
|
6522
|
+
.get(hostname, ...(owner ? [owner.tenantId, owner.scopeId] : []));
|
|
6339
6523
|
if (!row)
|
|
6340
6524
|
return; // idempotent, and a no-op is not audited
|
|
6341
|
-
this.directory.prepare('DELETE FROM hostnames WHERE hostname = ?').run(hostname);
|
|
6342
6525
|
this.recordAdmin(actor, 'unbindHostname', { tenantId: row.tenant_id, scopeId: row.scope_id }, { hostname, status: row.status }, null);
|
|
6343
6526
|
},
|
|
6344
6527
|
listHostnames: async (actor, filter) => {
|
|
@@ -6565,34 +6748,54 @@ export class SqliteScopeHost {
|
|
|
6565
6748
|
// The SQL migrations are stored apart from the manifest (#1764), so no read of the
|
|
6566
6749
|
// version but the promote review's carries them.
|
|
6567
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
|
+
});
|
|
6568
6755
|
this.directory.transaction(() => {
|
|
6569
6756
|
this.directory
|
|
6570
6757
|
.prepare(`INSERT INTO vertical_versions
|
|
6571
6758
|
(id, vertical_slug, version, manifest_digest, permission_digest,
|
|
6572
6759
|
migration_digest, deployment_ref, admission, admission_note, manifest_json,
|
|
6573
|
-
origin_json, migration_count, migrations_split, created_at)
|
|
6574
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`)
|
|
6575
|
-
.run(audited.id, audited.verticalSlug, audited.version, audited.manifestDigest, audited.permissionDigest,
|
|
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());
|
|
6576
6763
|
writeVersionMigrations(switchSqlOf(this.directory), audited.id, split.migrations);
|
|
6764
|
+
// #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
|
|
6765
|
+
indexVersionScopeStores(switchSqlOf(this.directory), audited.verticalSlug, audited.id, split.manifestJson);
|
|
6577
6766
|
})();
|
|
6578
6767
|
this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
|
|
6579
6768
|
...audited,
|
|
6769
|
+
// The digest the registry holds, which differs from the pushed one only for an unheld version.
|
|
6770
|
+
migrationDigest,
|
|
6580
6771
|
...(origin ? { origin } : {}),
|
|
6581
6772
|
admission: selfAdmits ? 'admitted' : 'pending',
|
|
6582
6773
|
});
|
|
6583
6774
|
},
|
|
6775
|
+
verticalScopeStores: async (actor, verticalSlug) => {
|
|
6776
|
+
// One transaction: versions not indexed yet are indexed by the read (#2286).
|
|
6777
|
+
const stores = this.directory.transaction(() => verticalScopeStoresOf(switchSqlOf(this.directory), verticalSlug))();
|
|
6778
|
+
this.recordAccess(actor, 'verticalScopeStores', {}, { verticalSlug }, stores.length);
|
|
6779
|
+
return stores;
|
|
6780
|
+
},
|
|
6584
6781
|
listVersions: async (actor, verticalSlug, page) => {
|
|
6585
6782
|
const where = ['vertical_slug = ?'];
|
|
6586
6783
|
const params = [verticalSlug];
|
|
6587
6784
|
const tail = keysetTail(where, params, 'id', page);
|
|
6588
|
-
const
|
|
6589
|
-
|
|
6590
|
-
|
|
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
|
+
}
|
|
6591
6794
|
this.recordAccess(actor, 'listVersions', {}, { verticalSlug }, rows.length);
|
|
6592
6795
|
return rows.map(mapVersion);
|
|
6593
6796
|
},
|
|
6594
6797
|
getVersion: async (actor, versionId, verticalSlug) => {
|
|
6595
|
-
const v = readVersion(versionId);
|
|
6798
|
+
const v = await readVersion(versionId);
|
|
6596
6799
|
// A version of another vertical reads as absent when the caller named one.
|
|
6597
6800
|
const hit = v && (verticalSlug === undefined || v.verticalSlug === verticalSlug) ? v : undefined;
|
|
6598
6801
|
this.recordAccess(actor, 'getVersion', {}, { versionId, verticalSlug }, hit ? 1 : 0);
|
|
@@ -6609,7 +6812,7 @@ export class SqliteScopeHost {
|
|
|
6609
6812
|
const prod = this.directory
|
|
6610
6813
|
.prepare("SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = 'prod'")
|
|
6611
6814
|
.get(slug);
|
|
6612
|
-
const prodVersion = prod ? readVersion(prod.version_id) : undefined;
|
|
6815
|
+
const prodVersion = prod ? await readVersion(prod.version_id) : undefined;
|
|
6613
6816
|
if (prodVersion?.admissionNote === AUTO_ADMISSION_NOTE) {
|
|
6614
6817
|
throw substratError('conflict', `vertical '${slug}' prod version ${prodVersion.id} is auto-admitted (private self-serve) — ` +
|
|
6615
6818
|
`a staff admit must vouch for it before listing`);
|
|
@@ -6684,12 +6887,13 @@ export class SqliteScopeHost {
|
|
|
6684
6887
|
.prepare(`DELETE FROM vertical_version_migrations
|
|
6685
6888
|
WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`)
|
|
6686
6889
|
.run(slug);
|
|
6890
|
+
forgetVerticalScopeStores(switchSqlOf(this.directory), slug);
|
|
6687
6891
|
this.directory.prepare('DELETE FROM vertical_versions WHERE vertical_slug = ?').run(slug);
|
|
6688
6892
|
this.directory.prepare('DELETE FROM verticals WHERE slug = ?').run(slug);
|
|
6689
6893
|
this.recordAdmin(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.ownerTenant }, null);
|
|
6690
6894
|
},
|
|
6691
6895
|
admitVersion: async (actor, versionId) => {
|
|
6692
|
-
const v = readVersion(versionId);
|
|
6896
|
+
const v = await readVersion(versionId);
|
|
6693
6897
|
if (!v)
|
|
6694
6898
|
throw substratError('not_found', `unknown version ${versionId}`);
|
|
6695
6899
|
if (v.admission === 'admitted') {
|
|
@@ -6717,7 +6921,7 @@ export class SqliteScopeHost {
|
|
|
6717
6921
|
});
|
|
6718
6922
|
},
|
|
6719
6923
|
rejectVersion: async (actor, versionId, note) => {
|
|
6720
|
-
const v = readVersion(versionId);
|
|
6924
|
+
const v = await readVersion(versionId);
|
|
6721
6925
|
if (!v)
|
|
6722
6926
|
throw substratError('not_found', `unknown version ${versionId}`);
|
|
6723
6927
|
if (v.admission === 'admitted') {
|
|
@@ -6734,7 +6938,7 @@ export class SqliteScopeHost {
|
|
|
6734
6938
|
});
|
|
6735
6939
|
},
|
|
6736
6940
|
promotionImpact: async (actor, verticalSlug, channel, versionId) => {
|
|
6737
|
-
const incoming = readVersion(versionId);
|
|
6941
|
+
const incoming = await readVersion(versionId);
|
|
6738
6942
|
if (!incoming || incoming.verticalSlug !== verticalSlug) {
|
|
6739
6943
|
throw substratError('not_found', `unknown version ${versionId} for vertical '${verticalSlug}'`);
|
|
6740
6944
|
}
|
|
@@ -6746,7 +6950,7 @@ export class SqliteScopeHost {
|
|
|
6746
6950
|
return breaks;
|
|
6747
6951
|
},
|
|
6748
6952
|
promoteVersion: async (actor, verticalSlug, channel, versionId, acknowledge) => {
|
|
6749
|
-
const incoming = readVersion(versionId);
|
|
6953
|
+
const incoming = await readVersion(versionId);
|
|
6750
6954
|
if (!incoming)
|
|
6751
6955
|
throw substratError('not_found', `unknown version ${versionId}`);
|
|
6752
6956
|
if (incoming.verticalSlug !== verticalSlug) {
|
|
@@ -6758,7 +6962,7 @@ export class SqliteScopeHost {
|
|
|
6758
6962
|
const current = this.directory
|
|
6759
6963
|
.prepare('SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = ?')
|
|
6760
6964
|
.get(verticalSlug, channel);
|
|
6761
|
-
const outgoing = current ? readVersion(current.version_id) : undefined;
|
|
6965
|
+
const outgoing = current ? await readVersion(current.version_id) : undefined;
|
|
6762
6966
|
const ack = promotionAcknowledgement.parse(acknowledge ?? {});
|
|
6763
6967
|
// The owning tenant's live scopes a PRIVATE prod promote re-points — below for an embedded
|
|
6764
6968
|
// vertical, after the in-place serve for a dispatch-backed one (#321) — read before
|
|
@@ -6781,6 +6985,18 @@ export class SqliteScopeHost {
|
|
|
6781
6985
|
served?.versionId,
|
|
6782
6986
|
...owned.map((s) => runningVersionOf(mapScope(s), served)),
|
|
6783
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
|
+
});
|
|
6784
7000
|
// §4's two checkpoints, fired where the blast radius is. A FIRST promotion
|
|
6785
7001
|
// has nothing to diff against, so there is nothing to acknowledge — the
|
|
6786
7002
|
// gate is about change, not about existence.
|
|
@@ -6790,8 +7006,7 @@ export class SqliteScopeHost {
|
|
|
6790
7006
|
`${incoming.permissionDigest}) — acknowledge it explicitly to promote`);
|
|
6791
7007
|
}
|
|
6792
7008
|
if (outgoing.migrationDigest !== incoming.migrationDigest && !ack.migrationChange) {
|
|
6793
|
-
throw substratError('conflict',
|
|
6794
|
-
`${incoming.migrationDigest}) — acknowledge it explicitly to promote`);
|
|
7009
|
+
throw substratError('conflict', migrationChangeRefusal(outgoing.migrationDigest, incoming.migrationDigest));
|
|
6795
7010
|
}
|
|
6796
7011
|
// #1705 PR 3: an export an installed consumer imports, dropped or re-versioned.
|
|
6797
7012
|
if (!ack.exportBreak) {
|
|
@@ -6801,18 +7016,32 @@ export class SqliteScopeHost {
|
|
|
6801
7016
|
}
|
|
6802
7017
|
}
|
|
6803
7018
|
const promotedAt = new Date().toISOString();
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
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);
|
|
6816
7045
|
// For a PRIVATE vertical, prod IS what the owner's apps run: re-point the
|
|
6817
7046
|
// owning tenant's live scopes in the same act, so merge-to-main (push +
|
|
6818
7047
|
// promote) is a complete deploy and a rollback promote reaches the running
|
|
@@ -6832,7 +7061,7 @@ export class SqliteScopeHost {
|
|
|
6832
7061
|
for (const s of incoming.deploymentRef ? [] : owned) {
|
|
6833
7062
|
if (s.vertical_version_id === versionId)
|
|
6834
7063
|
continue;
|
|
6835
|
-
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;
|
|
6836
7065
|
if (prev && prev.migrationDigest !== incoming.migrationDigest) {
|
|
6837
7066
|
await this.snapshotScope(actor, s.tenant_id, s.scope_id);
|
|
6838
7067
|
}
|
|
@@ -6894,7 +7123,7 @@ export class SqliteScopeHost {
|
|
|
6894
7123
|
}));
|
|
6895
7124
|
},
|
|
6896
7125
|
bindingImpact: async (actor, tenantId, scopeId, versionId, opts) => {
|
|
6897
|
-
const { v, scope } = bindTarget(tenantId, scopeId, versionId);
|
|
7126
|
+
const { v, scope } = await bindTarget(tenantId, scopeId, versionId);
|
|
6898
7127
|
// #2178: a move no acknowledgement passes is refused here too, so a caller asking first
|
|
6899
7128
|
// (an adopt, an Update) stops before anything is copied.
|
|
6900
7129
|
this.assertBindingShapesKept(mapScope(scope), versionId, opts?.servingRef);
|
|
@@ -6903,22 +7132,30 @@ export class SqliteScopeHost {
|
|
|
6903
7132
|
return breaks;
|
|
6904
7133
|
},
|
|
6905
7134
|
bindScopeVersion: async (actor, tenantId, scopeId, versionId, opts) => {
|
|
6906
|
-
const { v, scope } = bindTarget(tenantId, scopeId, versionId);
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
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();
|
|
6910
7145
|
const epoch = this.directory.prepare('SELECT COALESCE(erasure_epoch, 0) AS erasure_epoch FROM scopes WHERE tenant_id = ? AND scope_id = ?')
|
|
6911
7146
|
.get(tenantId, scopeId);
|
|
6912
7147
|
if (opts?.expectedErasureEpoch !== undefined && epoch?.erasure_epoch !== opts.expectedErasureEpoch) {
|
|
6913
7148
|
throw substratError('precondition_failed', 'scope erasure changed; reload the scope and retry');
|
|
6914
7149
|
}
|
|
6915
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;
|
|
6916
7153
|
// #2178 first: no acknowledgement passes it, so none should be asked for ahead of it.
|
|
6917
|
-
this.assertBindingShapesKept(mapScope(scope), versionId);
|
|
7154
|
+
this.assertBindingShapesKept(mapScope(scope), versionId, servingRef);
|
|
6918
7155
|
// #1756: an export an app in this tenant imports, dropped or re-versioned by what this
|
|
6919
7156
|
// scope would run. Before the snapshot, so a refused bind leaves nothing behind.
|
|
6920
7157
|
if (!ack.exportBreak) {
|
|
6921
|
-
const breaks = await this.bindBreaks(actor, mapScope(scope), v);
|
|
7158
|
+
const breaks = await this.bindBreaks(actor, mapScope(scope), v, servingRef);
|
|
6922
7159
|
if (breaks.length > 0)
|
|
6923
7160
|
throw substratError('precondition_failed', bindExportBreakRefusal(breaks));
|
|
6924
7161
|
}
|
|
@@ -6926,19 +7163,29 @@ export class SqliteScopeHost {
|
|
|
6926
7163
|
// crosses a migration boundary. Gated on a real digest change — a code-only
|
|
6927
7164
|
// rebind snapshots nothing — and on the caller opting in (until GC ships).
|
|
6928
7165
|
if (opts?.snapshot && scope.vertical_version_id) {
|
|
6929
|
-
const outgoing = readVersion(scope.vertical_version_id);
|
|
7166
|
+
const outgoing = await readVersion(scope.vertical_version_id);
|
|
6930
7167
|
if (outgoing && outgoing.migrationDigest !== v.migrationDigest) {
|
|
6931
7168
|
await this.snapshotScope(actor, tenantId, scopeId);
|
|
6932
7169
|
}
|
|
6933
7170
|
}
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
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 }, {
|
|
6938
7183
|
versionId,
|
|
6939
7184
|
vertical: v.verticalSlug,
|
|
6940
7185
|
version: v.version,
|
|
7186
|
+
...(servingRef !== undefined ? { servingRef } : {}),
|
|
6941
7187
|
...(opts?.expectedVersionId !== undefined ? { expectedVersionId: opts.expectedVersionId } : {}),
|
|
7188
|
+
...(opts?.expectedServing ? { expectedServing: opts.expectedServing } : {}),
|
|
6942
7189
|
...(ack.exportBreak ? { acknowledged: ack } : {}),
|
|
6943
7190
|
});
|
|
6944
7191
|
},
|
|
@@ -7123,7 +7370,7 @@ export class SqliteScopeHost {
|
|
|
7123
7370
|
const ack = bindAcknowledgement.parse(opts?.acknowledge ?? {});
|
|
7124
7371
|
// #1756: onto (or off) a serving script, the scope runs other code before its pointer
|
|
7125
7372
|
// moves. Judged here, so no caller that routes first and binds second passes unasked.
|
|
7126
|
-
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;
|
|
7127
7374
|
if (bound)
|
|
7128
7375
|
this.assertBindingShapesKept(mapScope(scope), bound.id, servingRef);
|
|
7129
7376
|
if (!ack.exportBreak && bound) {
|
|
@@ -7700,6 +7947,74 @@ export class SqliteScopeHost {
|
|
|
7700
7947
|
createdAt: r.created_at,
|
|
7701
7948
|
}));
|
|
7702
7949
|
},
|
|
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
|
|
7952
|
+
// Cloudflare control plane; each call is one transaction. A hold's expiry is judged against
|
|
7953
|
+
// this host's clock, so a test can let a lease lapse without waiting.
|
|
7954
|
+
recordStoreDriverRelease: async (actor, input) => {
|
|
7955
|
+
const at = this.clock();
|
|
7956
|
+
const { record, created } = this.directory.transaction(() => recordStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
|
|
7957
|
+
if (created)
|
|
7958
|
+
this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, record, at);
|
|
7959
|
+
return record;
|
|
7960
|
+
},
|
|
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);
|
|
7964
|
+
return record;
|
|
7965
|
+
},
|
|
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);
|
|
7969
|
+
return state;
|
|
7970
|
+
},
|
|
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);
|
|
7974
|
+
return declared;
|
|
7975
|
+
},
|
|
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;
|
|
7985
|
+
},
|
|
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) => {
|
|
7991
|
+
const at = this.clock();
|
|
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);
|
|
7994
|
+
return row;
|
|
7995
|
+
},
|
|
7996
|
+
finishStoreDriverRollout: async (actor, input) => {
|
|
7997
|
+
const at = this.clock();
|
|
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);
|
|
8000
|
+
return row;
|
|
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
|
+
},
|
|
7703
8018
|
getScopeRecord: async (actor, tenantId, scopeId) => {
|
|
7704
8019
|
const r = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
7705
8020
|
// Cross-check the pair (K-3): a scope that exists under a DIFFERENT tenant
|
|
@@ -8021,13 +8336,17 @@ export class SqliteScopeHost {
|
|
|
8021
8336
|
this.recordAccess(actor, 'listShreddedSubjects', { tenantId, scopeId }, null, subjects.length);
|
|
8022
8337
|
return { subjects, nextCursor };
|
|
8023
8338
|
},
|
|
8024
|
-
shredSubject: async (actor, tenantId, scopeId, subjectId, stamp, storeErasures) => {
|
|
8339
|
+
shredSubject: async (actor, tenantId, scopeId, subjectId, fence, stamp, storeErasures) => {
|
|
8025
8340
|
this.assertScope(tenantId, scopeId);
|
|
8026
8341
|
const before = stamp ?? {
|
|
8027
8342
|
erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
|
|
8028
8343
|
?.erasure_epoch ?? 0,
|
|
8029
8344
|
path: 'direct',
|
|
8030
8345
|
};
|
|
8346
|
+
// #2285: a direct shred with a fence claims what it read, as the finalize does; the epoch
|
|
8347
|
+
// it records is the one its claim compared.
|
|
8348
|
+
if (fence)
|
|
8349
|
+
before.erasureEpoch = fence.epoch;
|
|
8031
8350
|
// Redact the live spine FIRST. Both halves are idempotent and a crash between them
|
|
8032
8351
|
// converges on retry, so the order is decided by which half-done state harms the
|
|
8033
8352
|
// person: dying after this leaves ciphertext in a backup that no key opens; dying
|
|
@@ -8101,6 +8420,8 @@ export class SqliteScopeHost {
|
|
|
8101
8420
|
// An orchestrated caller passes the serving script's result and this host does
|
|
8102
8421
|
// not erase again.
|
|
8103
8422
|
const erasures = storeErasures ?? (await this.eraseScopeStores(tenantId, scopeId, subjectId));
|
|
8423
|
+
if (fence)
|
|
8424
|
+
this.claimSubjectErasure(tenantId, scopeId, fence);
|
|
8104
8425
|
const { existed } = await this.subjectKeysFor(tenantId, scopeId).destroy(subjectId, at);
|
|
8105
8426
|
const receipt = subjectShredReceipt.parse({
|
|
8106
8427
|
subjectId,
|
|
@@ -8132,20 +8453,11 @@ export class SqliteScopeHost {
|
|
|
8132
8453
|
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected, storeErasures) => {
|
|
8133
8454
|
if (redactions.length === 0)
|
|
8134
8455
|
throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
|
|
8135
|
-
|
|
8136
|
-
WHERE tenant_id = ? AND scope_id = ? AND vertical_version_id IS ?
|
|
8137
|
-
AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
|
|
8138
|
-
AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
|
|
8139
|
-
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
|
|
8140
|
-
AND copy.state = 'pending')
|
|
8141
|
-
AND (SELECT COUNT(*) FROM scope_script_copies AS copy
|
|
8142
|
-
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);
|
|
8143
|
-
if (claimed.changes === 0)
|
|
8144
|
-
throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
|
|
8456
|
+
this.claimSubjectErasure(tenantId, scopeId, expected);
|
|
8145
8457
|
// The pure adapter has one co-located scope store. Its existing atomic shred is
|
|
8146
8458
|
// the final local redaction and key destruction, after remote copies confirmed.
|
|
8147
8459
|
const shred = this.admin.shredSubject;
|
|
8148
|
-
return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' }, storeErasures);
|
|
8460
|
+
return shred(actor, tenantId, scopeId, subjectId, undefined, { erasureEpoch: expected.epoch, path: 'orchestrated' }, storeErasures);
|
|
8149
8461
|
},
|
|
8150
8462
|
// -- impersonation (K-42, #868) ----------------------------------------
|
|
8151
8463
|
beginImpersonation: async (actor, input) => {
|
|
@@ -8781,6 +9093,10 @@ export class SqliteScopeHost {
|
|
|
8781
9093
|
const { tenantId, scopeId, ...after } = memberChangeAudit.parse(entry);
|
|
8782
9094
|
this.recordAdmin(actor, 'manageScopeMember', { tenantId, scopeId }, null, after);
|
|
8783
9095
|
},
|
|
9096
|
+
recordConnectionBindingChange: async (actor, entry) => {
|
|
9097
|
+
const { tenantId, scopeId, ...after } = connectionBindingAudit.parse(entry);
|
|
9098
|
+
this.recordAdmin(actor, 'bindConnectionScope', { tenantId, scopeId }, null, after);
|
|
9099
|
+
},
|
|
8784
9100
|
recordPeerBindingChange: async (actor, entry) => {
|
|
8785
9101
|
const { tenantId, scopeId, ...after } = peerBindingAudit.parse(entry);
|
|
8786
9102
|
this.recordAdmin(actor, 'setPeerBinding', { tenantId, scopeId }, null, after);
|
|
@@ -8790,6 +9106,11 @@ export class SqliteScopeHost {
|
|
|
8790
9106
|
const { tenantId, scopeId, action, ...after } = copyMarkAudit.parse(entry);
|
|
8791
9107
|
this.recordAdmin(actor, action === 'mark' ? 'markScopeCopy' : 'clearScopeCopyMark', { tenantId, scopeId }, null, after);
|
|
8792
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
|
+
},
|
|
8793
9114
|
accessLog: async (actor, filter) => {
|
|
8794
9115
|
const where = [];
|
|
8795
9116
|
const params = [];
|
|
@@ -9248,7 +9569,8 @@ export class SqliteScopeHost {
|
|
|
9248
9569
|
const moved = this.directory.prepare(`UPDATE scopes SET ${set} WHERE scope_id = ?
|
|
9249
9570
|
${cond?.expectedVersionId === undefined ? '' : 'AND vertical_version_id IS ?'}
|
|
9250
9571
|
${cond?.expectedErasureEpoch === undefined ? '' : 'AND COALESCE(erasure_epoch, 0) = ? AND reap_claimed_at IS NULL'}
|
|
9251
|
-
${cond?.confirmMove === undefined ? '' : COPY_MOVE_LIVE_PREDICATE}
|
|
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)));
|
|
9252
9574
|
if (moved.changes === 0)
|
|
9253
9575
|
return false;
|
|
9254
9576
|
if (cond?.confirmMove) {
|
|
@@ -9258,6 +9580,10 @@ export class SqliteScopeHost {
|
|
|
9258
9580
|
return true;
|
|
9259
9581
|
})();
|
|
9260
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
|
+
}
|
|
9261
9587
|
ensureColumn(db, table, column, ddl) {
|
|
9262
9588
|
// Without case, as SQLite resolves a column name: a restore may have added this column
|
|
9263
9589
|
// already, untyped and spelled as a newer kernel's dump spelled it (#1883).
|
|
@@ -9947,6 +10273,18 @@ export class SqliteScopeHost {
|
|
|
9947
10273
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
9948
10274
|
minted,
|
|
9949
10275
|
}),
|
|
10276
|
+
// Upload links: mint / revoke / list, written once in the kernel — the same four things
|
|
10277
|
+
// `capabilities` is handed, and the same consumer-is-system-actor rule.
|
|
10278
|
+
attachments: createAttachmentVerbs({
|
|
10279
|
+
sql: spineSql(rt.db),
|
|
10280
|
+
subject: overrideActor ? { kind: 'system', id: overrideActor.system } : subject,
|
|
10281
|
+
now: at,
|
|
10282
|
+
check: runCheck,
|
|
10283
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
10284
|
+
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
10285
|
+
minted,
|
|
10286
|
+
targetOf: (entityType) => this.uploadTargetOf(entityType),
|
|
10287
|
+
}),
|
|
9950
10288
|
// K-16 / #1864: link and relink, written once in the kernel over the raw spine seam.
|
|
9951
10289
|
...createEntityEdgeVerbs({
|
|
9952
10290
|
sql: spineSql(rt.db),
|
|
@@ -10073,6 +10411,14 @@ export class SqliteScopeHost {
|
|
|
10073
10411
|
await this.repairDerived(rt);
|
|
10074
10412
|
return;
|
|
10075
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
|
+
}
|
|
10076
10422
|
// The failing `module@version` and its cause, captured structurally rather than
|
|
10077
10423
|
// re-parsed out of the thrown message — the directory record has to name both.
|
|
10078
10424
|
let failure;
|
|
@@ -10106,7 +10452,8 @@ export class SqliteScopeHost {
|
|
|
10106
10452
|
assertMigrationLeavesLedgerAlone(migration.sql, `migration ${key}`);
|
|
10107
10453
|
// #2068: one statement at a time, keeping the table set either side of each — the
|
|
10108
10454
|
// same splitter the Durable Object runs, so both hosts execute identical statements.
|
|
10109
|
-
|
|
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 });
|
|
10110
10457
|
assertTablesWithinColumnLimit(rt.db);
|
|
10111
10458
|
// #2090: the state columns this migration must have left, and after the last of the
|
|
10112
10459
|
// pass, the triggers and indexes a create-copy-rename rebuild dropped.
|
|
@@ -10386,6 +10733,9 @@ export class SqliteScopeHost {
|
|
|
10386
10733
|
// #2009: the copy classification on a scope DB built before it (NULL reads as a copy; see
|
|
10387
10734
|
// `COPY_ORIGIN_DDL`).
|
|
10388
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');
|
|
10389
10739
|
// #1237: `readInvocation`'s lookup — WHERE invocation_id = ? ORDER BY id — over an outbox
|
|
10390
10740
|
// that is never pruned. No index leads with invocation_id, so without this one SQLite
|
|
10391
10741
|
// walks the PRIMARY KEY from the oldest event until it reaches the call, and reading a
|