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