@substrat-run/adapter-sqlite 0.146.0 → 0.148.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +67 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +409 -32
- 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, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, peerCallerAt, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerVerticalSpellings, samePeerVertical, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
|
|
4
|
+
import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, assertNoBindingShapeBreak, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, identityLink, identityPool, instant, isSubstratError, connection, capabilityGrant, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchRecord, peerSwitch, verticalCaller, verticalSlug as verticalSlugOf, subjectRef, requestFingerprint, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, routeTarget, ROUTE_SCOPE_HOSTNAMES_MAX, registerVerticalInput, vertical as verticalSchema, verticalChannel, verticalVersion, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, grantRefFromProof, org as orgSchema, orgMembership, principalId, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, principalBecomeCapabilityInput, subjectShredReceipt, SCOPE_TABLE_PAGE_DEFAULT, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, verticalServingState, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, connectionBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
+
import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, UPLOAD_LINK_DDL, createAttachmentVerbs, inspectUploadLink, redeemUploadLinkInTransaction, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, peerCallerAt, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, peerVerticalSpellings, samePeerVertical, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, PEER_SWITCHES_DDL, PEER_BINDINGS_DDL, SWITCH_OWED_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, executableSqlStatements, blankSqlComments, recordWriteSuperseded, switchFencesOf, switchSupersededMessage, SWITCH_KINDS, SYSTEM_SWITCHES_DDL, forgetSwitchesOf, listSystemSwitchRecords, moveSwitch, reassertActionOf, reassertEntry, reassertOffRow, recordSwitchedOff, recordSwitchedOn, restoreSwitchRecord, scopesSwitchedOffFor, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, switchRecordsOf, switchedOffOf, switchesBackfillSqlOf, switchesTableExists, tenantHeldOf, tenantHoldsGrant, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, withRecorded, VERSION_MIGRATIONS_DDL, VERTICAL_SCOPE_STORES_DDL, forgetVerticalScopeStores, indexVersionScopeStores, kindDeclarationsOf, verticalScopeStoresOf, STORE_KIND_RELEASES_DDL, beginStoreKindRolloutIn, finishStoreKindRolloutIn, holdStoreKindIn, recordStoreKindReleaseIn, releaseStoreKindHoldIn, storeKindReleaseIn, storeKindStateIn, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, writeVersionMigrations, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, IMPERSONATION_COLUMNS, IMPERSONATION_DDL, ImpersonationRefused, assertImpersonationWrites, assertModuleEnqueueableKind, assertSessionUsable, impersonationByIdQuery, impersonationListQuery, impersonationRowValues, impersonationStampOf, mapImpersonationRow, newImpersonationSession, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, membershipFencesBackfillSql, MEMBERSHIP_FENCES_DDL, membershipFencesTableExists, MEMBERSHIP_FENCE_SINCE_SQL, RAISE_MEMBERSHIP_FENCE_SQL, memberAddedAudit, orgChangeBound, tenantCoverage, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, noSealingKeyMessage, openSealed, sealTo, ConnectionSealingKeyUnavailableError, PermissionDenied, createAtomic, NotSearchable, NotListable, listIndexMigrations, listIndexPlans, listQuery, cursorOf, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, attachmentRecordOfRow, attachmentTextJob, enqueueAttachmentText, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, queueAttachmentTextBackfill, startAttachmentTextBackfill, attachmentTextBackfillJob, reconcileAttachmentText, recordAttachmentText, searchAttachments, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, emptyImportResult, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, exportedSinceQuery, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf, } from '@substrat-run/kernel';
|
|
6
6
|
import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
|
|
7
7
|
import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
|
|
8
|
-
import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
|
|
9
|
-
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, scopeScriptCopyOf, } from '@substrat-run/kernel';
|
|
8
|
+
import { INERT_SCOPE_REASON, isPrimaryScopeRow, UPLOAD_LINK_WRONG_TARGET } from '@substrat-run/kernel';
|
|
9
|
+
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, copyBackfillParams, copyBackfillRefusal, SUBJECT_ERASURE_CLAIM_SQL, subjectErasureClaimParams, COPY_MOVE_CONFIRM_SQL, COPY_MOVE_LIVE_PREDICATE, SCOPE_COPY_LEASE_MS, SCOPE_SCRIPT_COPY_COLUMNS, copyMoveConfirmParams, copyMoveLiveParams, scopeScriptCopyOf, } from '@substrat-run/kernel';
|
|
10
10
|
import { GRANT_CHILDREN_INDEX_DDL, SHAPE_MARKER_INDEX_DDL, grantedEntitiesForContext } from '@substrat-run/kernel';
|
|
11
11
|
import { LEGACY_SCOPE_ROWS_BACKFILL, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
|
|
12
12
|
import { ScopeActor } from './actor.js';
|
|
13
13
|
import { createTupleChecker, directoryTenantReader } from './checker.js';
|
|
14
|
+
export { scopeStoreRunnersFor } from './scope-store-runners.js';
|
|
14
15
|
/**
|
|
15
16
|
* A fault in the database itself (full, corrupt, I/O), as opposed to one statement failing.
|
|
16
17
|
* The #1764 backfill logs and opens past a failed batch, but never past one of these.
|
|
@@ -213,6 +214,9 @@ const KERNEL_DDL = `
|
|
|
213
214
|
-- an exchange trades that secret for. Spine (kernel-written), shared with the DO adapter
|
|
214
215
|
-- from @substrat-run/kernel so the two cannot part company; the column comments are there.
|
|
215
216
|
${CAPABILITY_DDL}
|
|
217
|
+
-- Upload links: a short-lived, single-use URL that lets a caller with no session upload ONE
|
|
218
|
+
-- file to ONE entity. Spine (kernel-written), shared with the DO adapter from the kernel.
|
|
219
|
+
${UPLOAD_LINK_DDL}
|
|
216
220
|
-- #1686: where a copied scope's data came from, and the event id its own events start above.
|
|
217
221
|
-- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
|
|
218
222
|
${COPY_ORIGIN_DDL}
|
|
@@ -620,6 +624,9 @@ export class SqliteScopeHost {
|
|
|
620
624
|
/** predicate name → the module-contributed implementation. Names are global. */
|
|
621
625
|
predicates = new Map();
|
|
622
626
|
/** operation names whose default binding some manifest withdrew (K-17). */
|
|
627
|
+
scopeStoreRunners;
|
|
628
|
+
scopeStoreVertical;
|
|
629
|
+
scopeStoreLifecycle;
|
|
623
630
|
withdrawn = new Map(); // operation → withdrawing module
|
|
624
631
|
relations = new Map();
|
|
625
632
|
/** entityType → its derived FTS5 index (#827). One entity type, one index. */
|
|
@@ -634,6 +641,8 @@ export class SqliteScopeHost {
|
|
|
634
641
|
erasurePlans = [];
|
|
635
642
|
/** entityType → the declared attachment gate (#473): read key + write key (default: read). */
|
|
636
643
|
attachmentTargets = new Map();
|
|
644
|
+
/** entityType → the `uploadLinks` policy its `attachmentTargets` entry declares; absent = no links. */
|
|
645
|
+
uploadLinkPolicies = new Map();
|
|
637
646
|
/** operation name → who binds it: the owning module, its entitlementKey and its declared
|
|
638
647
|
* schedules — the §4.3 gate's input, resolved per invoke by `requiredEntitlementFor` (#1654). */
|
|
639
648
|
operationEntitlement = new Map();
|
|
@@ -712,6 +721,9 @@ export class SqliteScopeHost {
|
|
|
712
721
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
713
722
|
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
714
723
|
this.versionId = options.versionId ?? null;
|
|
724
|
+
this.scopeStoreVertical = options.scopeStores?.vertical;
|
|
725
|
+
this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
|
|
726
|
+
this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
|
|
715
727
|
this.logSink = options.logSink ?? consoleLogSink;
|
|
716
728
|
this.invocationLineSink = options.invocationLineSink ?? consoleInvocationLineSink;
|
|
717
729
|
// Every option is read above and here, BEFORE the directory is claimed: an option that throws
|
|
@@ -930,6 +942,8 @@ export class SqliteScopeHost {
|
|
|
930
942
|
created_at TEXT NOT NULL,
|
|
931
943
|
PRIMARY KEY (tenant_id, vertical, binding)
|
|
932
944
|
);
|
|
945
|
+
-- A store kind's stored releases, rollouts and holds (D-63, #2300).
|
|
946
|
+
${STORE_KIND_RELEASES_DDL}
|
|
933
947
|
-- The vertical + version registry (#31). A scope binds to a VERSION, so
|
|
934
948
|
-- dev/staging/prod are the same vertical pinned differently, and a preview
|
|
935
949
|
-- deployment is a version nothing has been promoted to yet.
|
|
@@ -1000,6 +1014,7 @@ export class SqliteScopeHost {
|
|
|
1000
1014
|
UNIQUE (vertical_slug, version)
|
|
1001
1015
|
);
|
|
1002
1016
|
${VERSION_MIGRATIONS_DDL}
|
|
1017
|
+
${VERTICAL_SCOPE_STORES_DDL}
|
|
1003
1018
|
-- Channels (#31 step 2): a named pointer per vertical. Promotion moves it,
|
|
1004
1019
|
-- and promotion is where the migration and permission diffs fire — the
|
|
1005
1020
|
-- moment a change reaches anyone, rather than the moment it was typed.
|
|
@@ -1701,6 +1716,13 @@ export class SqliteScopeHost {
|
|
|
1701
1716
|
`(${existing.read}/${existing.write}) vs (${gate.read}/${gate.write})`);
|
|
1702
1717
|
}
|
|
1703
1718
|
this.attachmentTargets.set(target.entityType, gate);
|
|
1719
|
+
if (target.uploadLinks) {
|
|
1720
|
+
const known = this.uploadLinkPolicies.get(target.entityType);
|
|
1721
|
+
if (known && JSON.stringify(known) !== JSON.stringify(target.uploadLinks)) {
|
|
1722
|
+
throw new Error(`conflicting uploadLinks policies for attachment target '${target.entityType}'`);
|
|
1723
|
+
}
|
|
1724
|
+
this.uploadLinkPolicies.set(target.entityType, target.uploadLinks);
|
|
1725
|
+
}
|
|
1704
1726
|
}
|
|
1705
1727
|
// WITHDRAWAL (K-17): suppress another module's default binding. Order
|
|
1706
1728
|
// independent — a manifest may withdraw an operation whose module has not
|
|
@@ -2118,6 +2140,40 @@ export class SqliteScopeHost {
|
|
|
2118
2140
|
const store = this.attachmentStore(tenantId, scopeRow?.vertical ?? null);
|
|
2119
2141
|
return this.buildAttachments(rt, asPrincipal(principal), store);
|
|
2120
2142
|
}
|
|
2143
|
+
/** The declared write key and link policy of an attachment target — what `ctx.attachments` mints against. */
|
|
2144
|
+
uploadTargetOf(entityType) {
|
|
2145
|
+
const gate = this.attachmentTargets.get(entityType);
|
|
2146
|
+
if (!gate)
|
|
2147
|
+
return undefined;
|
|
2148
|
+
const uploadLinks = this.uploadLinkPolicies.get(entityType);
|
|
2149
|
+
return uploadLinks ? { write: gate.write, uploadLinks } : { write: gate.write };
|
|
2150
|
+
}
|
|
2151
|
+
async inspectUploadLink(tenantId, scopeId, secret) {
|
|
2152
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
2153
|
+
return rt.actor.enqueue(async () => (await inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret)).view);
|
|
2154
|
+
}
|
|
2155
|
+
async redeemUploadLink(tenantId, scopeId, secret, input) {
|
|
2156
|
+
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
2157
|
+
const { view, mintedBy } = await rt.actor.enqueue(() => inspectUploadLink({ sql: spineSql(rt.db), now: this.clock() }, secret));
|
|
2158
|
+
if (view.entity.entityType !== input.entity.entityType || view.entity.entityId !== input.entity.entityId) {
|
|
2159
|
+
throw substratError('not_found', 'This upload link is not valid for that target.', {
|
|
2160
|
+
reason: UPLOAD_LINK_WRONG_TARGET,
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
// The write is made AS the minter: the same gates `attachments()` puts in front of a person.
|
|
2164
|
+
await this.getScope(mintedBy, tenantId, scopeId);
|
|
2165
|
+
const scopeRow = this.directory
|
|
2166
|
+
.prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
|
|
2167
|
+
.get(scopeId);
|
|
2168
|
+
const files = this.buildAttachments(rt, asPrincipal(mintedBy), this.attachmentStore(tenantId, scopeRow?.vertical ?? null), { uploadLinkSecret: secret });
|
|
2169
|
+
return files.upload({
|
|
2170
|
+
entity: view.entity,
|
|
2171
|
+
filename: view.filename ?? input.filename,
|
|
2172
|
+
contentType: input.contentType,
|
|
2173
|
+
visibility: view.visibility,
|
|
2174
|
+
body: input.body,
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2121
2177
|
async getSystemAttachments(moduleId, tenantId, scopeId) {
|
|
2122
2178
|
// The same registered-module and lifecycle gate as a system invoke. The
|
|
2123
2179
|
// attachment surface checks the target's read key as system:<moduleId>.
|
|
@@ -2182,8 +2238,17 @@ export class SqliteScopeHost {
|
|
|
2182
2238
|
}
|
|
2183
2239
|
// #1525: null, for the reason the denial two lines up records null — this task
|
|
2184
2240
|
// is the attachment RPC's own and no invoke set an id on the runtime.
|
|
2185
|
-
|
|
2186
|
-
|
|
2241
|
+
// The write has COMMITTED, so a failed drain must not reach the caller as a refusal:
|
|
2242
|
+
// `upload` deletes the bytes on a refusal, which would leave this row without them
|
|
2243
|
+
// (and a redeemed link spent) (#2311). An undelivered event has no delivery row, so
|
|
2244
|
+
// the next drain in this scope delivers it.
|
|
2245
|
+
try {
|
|
2246
|
+
await this.dispatch(rt, null);
|
|
2247
|
+
await this.dispatchExecutors(rt, null);
|
|
2248
|
+
}
|
|
2249
|
+
catch (err) {
|
|
2250
|
+
console.error('substrat: consumer drain failed after an attachment committed', err);
|
|
2251
|
+
}
|
|
2187
2252
|
return result;
|
|
2188
2253
|
});
|
|
2189
2254
|
/**
|
|
@@ -2242,9 +2307,9 @@ export class SqliteScopeHost {
|
|
|
2242
2307
|
createdBy: creator.id,
|
|
2243
2308
|
createdAt: new Date().toISOString(),
|
|
2244
2309
|
});
|
|
2245
|
-
// Bytes first, row second: a crash between the two leaves an orphaned object
|
|
2246
|
-
//
|
|
2247
|
-
//
|
|
2310
|
+
// Bytes first, row second: a crash between the two leaves an orphaned object that no
|
|
2311
|
+
// row points at (#2314). A refused check compensates the object away below; a drain
|
|
2312
|
+
// fault after the commit is not a refusal (`serialized` keeps it from becoming one).
|
|
2248
2313
|
await store.put(key, input.body, { contentType: input.contentType });
|
|
2249
2314
|
try {
|
|
2250
2315
|
return await guarded('attachments.upload', async (ctx) => {
|
|
@@ -2265,6 +2330,11 @@ export class SqliteScopeHost {
|
|
|
2265
2330
|
// #1575: queue its text extraction in the same transaction — a `pending` row
|
|
2266
2331
|
// and a job run. Nothing is extracted here, so nothing here can fail the upload.
|
|
2267
2332
|
enqueueAttachmentText(spineSql(rt.db), record.id, ulid(), this.clock());
|
|
2333
|
+
// An upload-link redemption spends its link HERE, in the same transaction as the
|
|
2334
|
+
// row it produced: a refusal anywhere above or below rolls the spend back.
|
|
2335
|
+
if (opts.uploadLinkSecret !== undefined) {
|
|
2336
|
+
await redeemUploadLinkInTransaction({ sql: spineSql(rt.db), now: this.clock(), emit: (event) => kernelEmit(ctx, event) }, opts.uploadLinkSecret, record);
|
|
2337
|
+
}
|
|
2268
2338
|
return record;
|
|
2269
2339
|
});
|
|
2270
2340
|
}
|
|
@@ -2358,6 +2428,7 @@ export class SqliteScopeHost {
|
|
|
2358
2428
|
};
|
|
2359
2429
|
}
|
|
2360
2430
|
async importScope(actor, input, dump) {
|
|
2431
|
+
this.refuseUnusableScopeStores(dump.stores, input.vertical ?? null);
|
|
2361
2432
|
// Create the destination scope (directory row + storage). Its migration step
|
|
2362
2433
|
// creates some tables; the dump then replaces them wholesale, so the end state
|
|
2363
2434
|
// is the dump, not whatever the local module set would have built. Provenance is
|
|
@@ -2370,6 +2441,7 @@ export class SqliteScopeHost {
|
|
|
2370
2441
|
});
|
|
2371
2442
|
// A fork: its callers (snapshotScope) hand it a dump the platform exported.
|
|
2372
2443
|
await this.loadDump(input.tenantId, input.scopeId, { ...dump, exact: true });
|
|
2444
|
+
await this.importScopeStores(input.tenantId, input.scopeId, dump.stores);
|
|
2373
2445
|
await this.admin.activateScope(actor, input.tenantId, input.scopeId);
|
|
2374
2446
|
this.recordAdmin(actor, 'importScope', { tenantId: input.tenantId, scopeId: input.scopeId }, null, { sourceScopeId: dump.scopeId, tables: dump.tables.length, capturedAt: dump.capturedAt });
|
|
2375
2447
|
}
|
|
@@ -2522,6 +2594,7 @@ export class SqliteScopeHost {
|
|
|
2522
2594
|
const existing = await this.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2523
2595
|
if (!existing)
|
|
2524
2596
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2597
|
+
this.refuseUnusableScopeStores(dump.stores, existing.vertical);
|
|
2525
2598
|
// #1742: a dump from before a switch was pulled brings the module's grants back live, so
|
|
2526
2599
|
// the directory's recorded-off modules go back off in the load's own turn, as a
|
|
2527
2600
|
// provision's seat does — no schedule pass can run in between.
|
|
@@ -2530,9 +2603,17 @@ export class SqliteScopeHost {
|
|
|
2530
2603
|
// #1869: the re-point's source is the separate hint when given; the dump is otherwise as sent.
|
|
2531
2604
|
const source = { tables: dump.tables, scopeId: opts?.sourceScopeId ?? dump.scopeId };
|
|
2532
2605
|
await this.loadDump(tenantId, scopeId, source, (rt) => this.reassertSwitchesInTurn(rt, actor, tenantId, scopeId, undefined, (action, after) => rows.push([action, after])));
|
|
2606
|
+
await this.importScopeStores(tenantId, scopeId, dump.stores);
|
|
2607
|
+
const replay = await this.replayShreddedStoreErasures(actor, tenantId, scopeId, (opts?.sourceScopeId ?? dump.scopeId));
|
|
2533
2608
|
for (const [action, after] of rows)
|
|
2534
2609
|
this.recordAdmin(actor, action, { tenantId, scopeId }, null, after);
|
|
2535
|
-
this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, {
|
|
2610
|
+
this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, {
|
|
2611
|
+
sourceScopeId: dump.scopeId,
|
|
2612
|
+
tables: dump.tables.length,
|
|
2613
|
+
capturedAt: dump.capturedAt,
|
|
2614
|
+
...replay,
|
|
2615
|
+
});
|
|
2616
|
+
return replay;
|
|
2536
2617
|
}
|
|
2537
2618
|
async snapshotScope(actor, tenantId, scopeId, opts) {
|
|
2538
2619
|
const source = await this.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
@@ -2554,6 +2635,7 @@ export class SqliteScopeHost {
|
|
|
2554
2635
|
if (source.verticalVersionId) {
|
|
2555
2636
|
await this.admin.bindScopeVersion(actor, tenantId, snapshotId, source.verticalVersionId);
|
|
2556
2637
|
}
|
|
2638
|
+
await this.replayShreddedStoreErasures(actor, tenantId, snapshotId, scopeId);
|
|
2557
2639
|
return snapshotId;
|
|
2558
2640
|
}
|
|
2559
2641
|
async deleteSnapshot(actor, tenantId, scopeId) {
|
|
@@ -2567,6 +2649,7 @@ export class SqliteScopeHost {
|
|
|
2567
2649
|
throw substratError('forbidden', `scope ${scopeId} is not a fork or preview — only previews may be deleted; ` +
|
|
2568
2650
|
`archive a primary scope instead`);
|
|
2569
2651
|
}
|
|
2652
|
+
await this.wipeScopeStores(tenantId, scopeId);
|
|
2570
2653
|
// Close and evict the runtime handle before touching the file.
|
|
2571
2654
|
const rt = this.scopes.get(`${tenantId}/${scopeId}`);
|
|
2572
2655
|
if (rt)
|
|
@@ -2635,6 +2718,202 @@ export class SqliteScopeHost {
|
|
|
2635
2718
|
}
|
|
2636
2719
|
return this.buildStub(tenantId, scopeId, rt, asPrincipal(principal), options);
|
|
2637
2720
|
}
|
|
2721
|
+
async getScopeStore(principal, tenantId, scopeId, binding, options) {
|
|
2722
|
+
// The directory check is the same one `getScope` makes. With no runners
|
|
2723
|
+
// configured, the binding is refused after that check.
|
|
2724
|
+
await this.getScope(principal, tenantId, scopeId);
|
|
2725
|
+
if (this.scopeStoreRunners.size === 0) {
|
|
2726
|
+
throw substratError('unavailable', `no scope-store runner is configured for binding '${binding}'`);
|
|
2727
|
+
}
|
|
2728
|
+
const vertical = this.scopeStoreVerticalOf(scopeId);
|
|
2729
|
+
const runner = this.scopeStoreRunners.get(binding);
|
|
2730
|
+
if (!runner) {
|
|
2731
|
+
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2732
|
+
}
|
|
2733
|
+
await this.scopeStoreLifecycle.start();
|
|
2734
|
+
return {
|
|
2735
|
+
tenantId,
|
|
2736
|
+
scopeId,
|
|
2737
|
+
subjectKind: 'principal',
|
|
2738
|
+
invoke: (operation, input, ...extra) => observeStoreCall(binding, operation, () => callScopeStore((request) => runner.invoke({ tenantId, vertical, scopeId, binding }, request), operation, input, ...extra), options?.onStoreCall),
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
/** The scope's vertical when it has one. No vertical means this host has no store to touch. */
|
|
2742
|
+
scopeStoreVerticalIfPresent(scopeId) {
|
|
2743
|
+
const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
2744
|
+
const vertical = row?.vertical;
|
|
2745
|
+
if (!vertical)
|
|
2746
|
+
return undefined;
|
|
2747
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
|
|
2748
|
+
throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and scope ${scopeId} runs '${vertical}'`);
|
|
2749
|
+
}
|
|
2750
|
+
return vertical;
|
|
2751
|
+
}
|
|
2752
|
+
/** The scope's vertical, refused when this host is limited to another one. */
|
|
2753
|
+
scopeStoreVerticalOf(scopeId) {
|
|
2754
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2755
|
+
if (!vertical) {
|
|
2756
|
+
throw substratError('not_found', `scope ${scopeId} has no vertical, so it has no scope store`);
|
|
2757
|
+
}
|
|
2758
|
+
return vertical;
|
|
2759
|
+
}
|
|
2760
|
+
/**
|
|
2761
|
+
* One reserved call on a declared binding. The vertical's stub never reaches
|
|
2762
|
+
* this; `callReservedStoreOperation` refuses a name that is not reserved.
|
|
2763
|
+
*/
|
|
2764
|
+
async invokeReservedStore(address, operation, input) {
|
|
2765
|
+
const runner = this.scopeStoreRunners.get(address.binding);
|
|
2766
|
+
if (!runner)
|
|
2767
|
+
throw substratError('not_found', `no scope store is bound for '${address.binding}'`);
|
|
2768
|
+
await this.scopeStoreLifecycle.start();
|
|
2769
|
+
return callReservedStoreOperation((request) => runner.invoke(address, request), operation, input);
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* Each declared binding's `substrat.size`. No runners means an empty list,
|
|
2773
|
+
* and the scope is not opened to discover that.
|
|
2774
|
+
*/
|
|
2775
|
+
async readScopeStoreSizes(tenantId, scopeId) {
|
|
2776
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2777
|
+
if (bindings.length === 0)
|
|
2778
|
+
return [];
|
|
2779
|
+
this.scopeReadDbFor(tenantId, scopeId);
|
|
2780
|
+
const vertical = this.scopeStoreVerticalOf(scopeId);
|
|
2781
|
+
const sizes = [];
|
|
2782
|
+
for (const binding of bindings) {
|
|
2783
|
+
const { bytes } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.size');
|
|
2784
|
+
sizes.push({ binding, bytes });
|
|
2785
|
+
}
|
|
2786
|
+
return sizes;
|
|
2787
|
+
}
|
|
2788
|
+
/** The vertical whose stores this scope has on this host, or nothing when it has none. */
|
|
2789
|
+
storeVerticalOf(scopeId) {
|
|
2790
|
+
if (this.scopeStoreRunners.size === 0)
|
|
2791
|
+
return undefined;
|
|
2792
|
+
const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
2793
|
+
const vertical = row?.vertical ?? undefined;
|
|
2794
|
+
if (!vertical)
|
|
2795
|
+
return undefined;
|
|
2796
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical)
|
|
2797
|
+
return undefined;
|
|
2798
|
+
return vertical;
|
|
2799
|
+
}
|
|
2800
|
+
/** The erasure claim (`SUBJECT_ERASURE_CLAIM_SQL`); a scope that moved refuses with the key kept. */
|
|
2801
|
+
claimSubjectErasure(tenantId, scopeId, fence) {
|
|
2802
|
+
const claimed = this.directory.prepare(SUBJECT_ERASURE_CLAIM_SQL).run(...subjectErasureClaimParams(tenantId, scopeId, fence));
|
|
2803
|
+
if (claimed.changes === 0) {
|
|
2804
|
+
throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
/**
|
|
2808
|
+
* `substrat.erase` on each declared binding. A store error throws, so the caller
|
|
2809
|
+
* can leave the subject key untouched. Fail closed (#2242): an empty answer means
|
|
2810
|
+
* this host KNOWS it holds nothing for the scope. It always can tell — the
|
|
2811
|
+
* directory names the scope's vertical, and the runners are this host's own — so
|
|
2812
|
+
* unlike a Cloudflare host with no control plane it has no "cannot tell" case.
|
|
2813
|
+
*/
|
|
2814
|
+
async eraseScopeStores(tenantId, scopeId, subjectId) {
|
|
2815
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2816
|
+
// Shred calls this for every scope. No vertical names no store object, and a
|
|
2817
|
+
// vertical this host is not pinned to is one its runners never served: both hold
|
|
2818
|
+
// nothing here, and the key is still destroyed.
|
|
2819
|
+
const vertical = this.storeVerticalOf(scopeId);
|
|
2820
|
+
if (!vertical)
|
|
2821
|
+
return [];
|
|
2822
|
+
const erasures = [];
|
|
2823
|
+
for (const binding of bindings) {
|
|
2824
|
+
const { erased } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.erase', { subjectId });
|
|
2825
|
+
erasures.push({ binding, erased });
|
|
2826
|
+
}
|
|
2827
|
+
return erasures;
|
|
2828
|
+
}
|
|
2829
|
+
/**
|
|
2830
|
+
* A dump's stores this host cannot load are refused before any write (#2211, L8): a
|
|
2831
|
+
* binding that is not bound, a store with no kind or a kind other than the bound runner's,
|
|
2832
|
+
* any store at all for a destination with no vertical (no store object can be named for
|
|
2833
|
+
* it, so loading would drop them), or a destination on a vertical other than the one this
|
|
2834
|
+
* host is pinned to.
|
|
2835
|
+
*/
|
|
2836
|
+
refuseUnusableScopeStores(stores, vertical) {
|
|
2837
|
+
const listed = stores ?? [];
|
|
2838
|
+
if (listed.length === 0)
|
|
2839
|
+
return;
|
|
2840
|
+
if (vertical === null) {
|
|
2841
|
+
throw substratError('conflict', 'dump carries scope stores, and the destination scope has no vertical to hold them');
|
|
2842
|
+
}
|
|
2843
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
|
|
2844
|
+
throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and the destination scope runs '${vertical}'`);
|
|
2845
|
+
}
|
|
2846
|
+
for (const store of listed) {
|
|
2847
|
+
const runner = this.scopeStoreRunners.get(store.binding);
|
|
2848
|
+
if (!runner) {
|
|
2849
|
+
throw substratError('not_found', `dump names scope store '${store.binding}', which is not bound`);
|
|
2850
|
+
}
|
|
2851
|
+
if (!store.kind) {
|
|
2852
|
+
throw substratError('validation_failed', `dump's scope store '${store.binding}' names no kind`);
|
|
2853
|
+
}
|
|
2854
|
+
if (store.kind !== runner.kind) {
|
|
2855
|
+
throw substratError('conflict', `dump's scope store '${store.binding}' is kind '${store.kind}', and this host binds kind '${runner.kind}'`);
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
async exportScopeStores(tenantId, scopeId) {
|
|
2860
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2861
|
+
if (bindings.length === 0)
|
|
2862
|
+
return [];
|
|
2863
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2864
|
+
if (!vertical)
|
|
2865
|
+
return [];
|
|
2866
|
+
const stores = [];
|
|
2867
|
+
for (const binding of bindings) {
|
|
2868
|
+
const runner = this.scopeStoreRunners.get(binding);
|
|
2869
|
+
if (!runner)
|
|
2870
|
+
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2871
|
+
const exported = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.export');
|
|
2872
|
+
stores.push({ binding, kind: runner.kind, tables: exported.tables });
|
|
2873
|
+
}
|
|
2874
|
+
return stores;
|
|
2875
|
+
}
|
|
2876
|
+
async importScopeStores(tenantId, scopeId, stores) {
|
|
2877
|
+
const listed = stores ?? [];
|
|
2878
|
+
if (listed.length === 0)
|
|
2879
|
+
return;
|
|
2880
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2881
|
+
if (!vertical) {
|
|
2882
|
+
throw substratError('conflict', `dump carries scope stores, and scope ${scopeId} has no vertical to hold them`);
|
|
2883
|
+
}
|
|
2884
|
+
for (const store of listed) {
|
|
2885
|
+
await this.invokeReservedStore({ tenantId, vertical, scopeId, binding: store.binding }, 'substrat.import', { tables: store.tables });
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
/**
|
|
2889
|
+
* `substrat.erase` again for every tombstoned subject (#2207), on the target's and the
|
|
2890
|
+
* source's marks (`replayStoreErasures`). A target this host names no store for (no
|
|
2891
|
+
* runner, or no vertical a store is kept for) has nothing to replay into: nothing is read.
|
|
2892
|
+
*/
|
|
2893
|
+
async replayShreddedStoreErasures(actor, tenantId, target, source) {
|
|
2894
|
+
if (!this.storeVerticalOf(target))
|
|
2895
|
+
return undefined;
|
|
2896
|
+
return replayStoreErasures({
|
|
2897
|
+
admin: this.admin,
|
|
2898
|
+
actor,
|
|
2899
|
+
tenantId,
|
|
2900
|
+
target,
|
|
2901
|
+
source,
|
|
2902
|
+
erase: async (subjectId) => (await this.eraseScopeStores(tenantId, target, subjectId)).length,
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
/** `substrat.wipe` on each declared binding, before the scope's own storage goes. */
|
|
2906
|
+
async wipeScopeStores(tenantId, scopeId) {
|
|
2907
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2908
|
+
if (bindings.length === 0)
|
|
2909
|
+
return;
|
|
2910
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2911
|
+
if (!vertical)
|
|
2912
|
+
return;
|
|
2913
|
+
for (const binding of bindings) {
|
|
2914
|
+
await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.wipe');
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2638
2917
|
/**
|
|
2639
2918
|
* A scope stub whose authority is a CONNECTION (#97).
|
|
2640
2919
|
*
|
|
@@ -4072,10 +4351,15 @@ export class SqliteScopeHost {
|
|
|
4072
4351
|
}
|
|
4073
4352
|
}
|
|
4074
4353
|
async close() {
|
|
4075
|
-
|
|
4076
|
-
this.
|
|
4077
|
-
|
|
4078
|
-
|
|
4354
|
+
try {
|
|
4355
|
+
await this.scopeStoreLifecycle.stop();
|
|
4356
|
+
}
|
|
4357
|
+
finally {
|
|
4358
|
+
for (const rt of [...this.scopes.values()])
|
|
4359
|
+
this.closeRuntime(rt);
|
|
4360
|
+
this.directory.close();
|
|
4361
|
+
this.owner.release();
|
|
4362
|
+
}
|
|
4079
4363
|
}
|
|
4080
4364
|
// -------------------------------------------------------------------------
|
|
4081
4365
|
// Event dispatch (testrun spec §9.2.3): at-least-once, kernel-journaled,
|
|
@@ -6118,14 +6402,16 @@ export class SqliteScopeHost {
|
|
|
6118
6402
|
}
|
|
6119
6403
|
this.recordAdmin(actor, 'setHostnameIssuance', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status: fields.status, note: fields.note ?? null });
|
|
6120
6404
|
},
|
|
6121
|
-
unbindHostname: async (actor, raw) => {
|
|
6405
|
+
unbindHostname: async (actor, raw, owner) => {
|
|
6122
6406
|
const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
|
|
6407
|
+
// One statement: the row it removes is the row it audits, and an owner is judged by the
|
|
6408
|
+
// delete itself, so a name rebound to another scope in between is left alone (#2285).
|
|
6123
6409
|
const row = this.directory
|
|
6124
|
-
.prepare(
|
|
6125
|
-
|
|
6410
|
+
.prepare(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
|
|
6411
|
+
RETURNING tenant_id, scope_id, status`)
|
|
6412
|
+
.get(hostname, ...(owner ? [owner.tenantId, owner.scopeId] : []));
|
|
6126
6413
|
if (!row)
|
|
6127
6414
|
return; // idempotent, and a no-op is not audited
|
|
6128
|
-
this.directory.prepare('DELETE FROM hostnames WHERE hostname = ?').run(hostname);
|
|
6129
6415
|
this.recordAdmin(actor, 'unbindHostname', { tenantId: row.tenant_id, scopeId: row.scope_id }, { hostname, status: row.status }, null);
|
|
6130
6416
|
},
|
|
6131
6417
|
listHostnames: async (actor, filter) => {
|
|
@@ -6361,6 +6647,8 @@ export class SqliteScopeHost {
|
|
|
6361
6647
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`)
|
|
6362
6648
|
.run(audited.id, audited.verticalSlug, audited.version, audited.manifestDigest, audited.permissionDigest, audited.migrationDigest, audited.deploymentRef, selfAdmits ? 'admitted' : 'pending', selfAdmits ? AUTO_ADMISSION_NOTE : null, split.manifestJson, origin ? JSON.stringify(origin) : null, split.migrations?.length ?? null, new Date().toISOString());
|
|
6363
6649
|
writeVersionMigrations(switchSqlOf(this.directory), audited.id, split.migrations);
|
|
6650
|
+
// #2286: the version's scope-store bindings, refusing one re-kinded, in the insert's transaction.
|
|
6651
|
+
indexVersionScopeStores(switchSqlOf(this.directory), audited.verticalSlug, audited.id, split.manifestJson);
|
|
6364
6652
|
})();
|
|
6365
6653
|
this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
|
|
6366
6654
|
...audited,
|
|
@@ -6368,6 +6656,12 @@ export class SqliteScopeHost {
|
|
|
6368
6656
|
admission: selfAdmits ? 'admitted' : 'pending',
|
|
6369
6657
|
});
|
|
6370
6658
|
},
|
|
6659
|
+
verticalScopeStores: async (actor, verticalSlug) => {
|
|
6660
|
+
// One transaction: versions not indexed yet are indexed by the read (#2286).
|
|
6661
|
+
const stores = this.directory.transaction(() => verticalScopeStoresOf(switchSqlOf(this.directory), verticalSlug))();
|
|
6662
|
+
this.recordAccess(actor, 'verticalScopeStores', {}, { verticalSlug }, stores.length);
|
|
6663
|
+
return stores;
|
|
6664
|
+
},
|
|
6371
6665
|
listVersions: async (actor, verticalSlug, page) => {
|
|
6372
6666
|
const where = ['vertical_slug = ?'];
|
|
6373
6667
|
const params = [verticalSlug];
|
|
@@ -6471,6 +6765,7 @@ export class SqliteScopeHost {
|
|
|
6471
6765
|
.prepare(`DELETE FROM vertical_version_migrations
|
|
6472
6766
|
WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`)
|
|
6473
6767
|
.run(slug);
|
|
6768
|
+
forgetVerticalScopeStores(switchSqlOf(this.directory), slug);
|
|
6474
6769
|
this.directory.prepare('DELETE FROM vertical_versions WHERE vertical_slug = ?').run(slug);
|
|
6475
6770
|
this.directory.prepare('DELETE FROM verticals WHERE slug = ?').run(slug);
|
|
6476
6771
|
this.recordAdmin(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.ownerTenant }, null);
|
|
@@ -7227,6 +7522,11 @@ export class SqliteScopeHost {
|
|
|
7227
7522
|
this.recordAccess(actor, 'scopeDatabaseSize', { tenantId, scopeId }, null, 1);
|
|
7228
7523
|
return bytes;
|
|
7229
7524
|
},
|
|
7525
|
+
scopeStoreSizes: async (actor, tenantId, scopeId) => {
|
|
7526
|
+
const sizes = await this.readScopeStoreSizes(tenantId, scopeId);
|
|
7527
|
+
this.recordAccess(actor, 'scopeStoreSizes', { tenantId, scopeId }, null, sizes.length);
|
|
7528
|
+
return sizes;
|
|
7529
|
+
},
|
|
7230
7530
|
rewindScope: async () => {
|
|
7231
7531
|
throw new Error('point-in-time rewind is not available on this host (PITR is a Durable-Object-plane mechanism) — use the backup restore path');
|
|
7232
7532
|
},
|
|
@@ -7482,6 +7782,48 @@ export class SqliteScopeHost {
|
|
|
7482
7782
|
createdAt: r.created_at,
|
|
7483
7783
|
}));
|
|
7484
7784
|
},
|
|
7785
|
+
// -- store-kind releases, rollouts and holds (D-63, #2300) -------------------
|
|
7786
|
+
// The state is kernel code over the directory (store-kind-releases.ts), shared with the
|
|
7787
|
+
// Cloudflare control plane; each call is one transaction. A hold's expiry is judged against
|
|
7788
|
+
// this host's clock, so a test can let a lease lapse without waiting.
|
|
7789
|
+
recordStoreKindRelease: async (actor, input) => {
|
|
7790
|
+
const at = this.clock();
|
|
7791
|
+
const { record, created } = this.directory.transaction(() => recordStoreKindReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
|
|
7792
|
+
if (created)
|
|
7793
|
+
this.recordAdmin(actor, 'uploadStoreKindRelease', { tenantId: null }, null, record, at);
|
|
7794
|
+
return record;
|
|
7795
|
+
},
|
|
7796
|
+
getStoreKindRelease: async (actor, kind, sha256) => {
|
|
7797
|
+
const record = storeKindReleaseIn(switchSqlOf(this.directory), kind, sha256);
|
|
7798
|
+
this.recordAccess(actor, 'getStoreKindRelease', { tenantId: null }, { kind, sha256 }, record ? 1 : 0);
|
|
7799
|
+
return record;
|
|
7800
|
+
},
|
|
7801
|
+
storeKindState: async (actor, kind) => {
|
|
7802
|
+
const state = storeKindStateIn(switchSqlOf(this.directory), kind, Date.parse(this.clock()));
|
|
7803
|
+
this.recordAccess(actor, 'storeKindState', { tenantId: null }, { kind }, state.rollouts.length);
|
|
7804
|
+
return state;
|
|
7805
|
+
},
|
|
7806
|
+
storeKindDeclarations: async (actor, kind) => {
|
|
7807
|
+
const declared = this.directory.transaction(() => kindDeclarationsOf(switchSqlOf(this.directory), kind))();
|
|
7808
|
+
this.recordAccess(actor, 'storeKindDeclarations', { tenantId: null }, { kind }, declared.verticals.length);
|
|
7809
|
+
return declared;
|
|
7810
|
+
},
|
|
7811
|
+
holdStoreKind: async (_actor, input) => this.directory.transaction(() => holdStoreKindIn(switchSqlOf(this.directory), { ...input, nowMs: Date.parse(this.clock()) }))(),
|
|
7812
|
+
releaseStoreKindHold: async (_actor, input) => {
|
|
7813
|
+
releaseStoreKindHoldIn(switchSqlOf(this.directory), input);
|
|
7814
|
+
},
|
|
7815
|
+
beginStoreKindRollout: async (actor, input) => {
|
|
7816
|
+
const at = this.clock();
|
|
7817
|
+
const row = this.directory.transaction(() => beginStoreKindRolloutIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) }))();
|
|
7818
|
+
this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreKind' : 'rollOutStoreKind', { tenantId: null }, null, row, at);
|
|
7819
|
+
return row;
|
|
7820
|
+
},
|
|
7821
|
+
finishStoreKindRollout: async (actor, input) => {
|
|
7822
|
+
const at = this.clock();
|
|
7823
|
+
const row = this.directory.transaction(() => finishStoreKindRolloutIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
|
|
7824
|
+
this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreKind' : 'rollOutStoreKind', { tenantId: null }, null, row, at);
|
|
7825
|
+
return row;
|
|
7826
|
+
},
|
|
7485
7827
|
getScopeRecord: async (actor, tenantId, scopeId) => {
|
|
7486
7828
|
const r = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
7487
7829
|
// Cross-check the pair (K-3): a scope that exists under a DIFFERENT tenant
|
|
@@ -7622,7 +7964,8 @@ export class SqliteScopeHost {
|
|
|
7622
7964
|
return { name, ddl: sql, columns, rows };
|
|
7623
7965
|
});
|
|
7624
7966
|
this.recordAccess(actor, 'exportScope', { tenantId, scopeId }, null, tables.length);
|
|
7625
|
-
|
|
7967
|
+
const stores = await this.exportScopeStores(tenantId, scopeId);
|
|
7968
|
+
return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables, stores };
|
|
7626
7969
|
},
|
|
7627
7970
|
// -- directory disaster recovery (#40) ----------------------------------
|
|
7628
7971
|
// The self-hosted half of the same story. A self-host has no DO point-in-time
|
|
@@ -7762,6 +8105,7 @@ export class SqliteScopeHost {
|
|
|
7762
8105
|
const rt = this.scopes.get(`${tenantId}/${scopeId}`);
|
|
7763
8106
|
if (rt)
|
|
7764
8107
|
this.closeRuntime(rt);
|
|
8108
|
+
await this.wipeScopeStores(tenantId, scopeId);
|
|
7765
8109
|
rmSync(join(this.dir, `${tenantId}__${scopeId}.sqlite`), { force: true });
|
|
7766
8110
|
// The recoverable copy the caller stored first (#493), named in the audit entry so
|
|
7767
8111
|
// the trail answers "was there a backup" without correlating two timestamps.
|
|
@@ -7789,13 +8133,29 @@ export class SqliteScopeHost {
|
|
|
7789
8133
|
},
|
|
7790
8134
|
// `stamp` is the erasure epoch an orchestrated erasure's claim compared (#1722). A direct shred
|
|
7791
8135
|
// claims nothing and records the epoch it ran under: the backfill orders erasures by them.
|
|
7792
|
-
|
|
8136
|
+
listShreddedSubjects: async (actor, tenantId, scopeId, cursor) => {
|
|
8137
|
+
this.assertScope(tenantId, scopeId);
|
|
8138
|
+
const rows = this.directory
|
|
8139
|
+
.prepare(`SELECT subject_id FROM _substrat_subject_keys
|
|
8140
|
+
WHERE scope_id = ? AND tenant_id = ? AND shredded_at IS NOT NULL AND subject_id > ?
|
|
8141
|
+
ORDER BY subject_id LIMIT 101`)
|
|
8142
|
+
.all(scopeId, tenantId, cursor ?? '');
|
|
8143
|
+
const subjects = rows.slice(0, 100).map((row) => row.subject_id);
|
|
8144
|
+
const nextCursor = rows.length > 100 ? (subjects[99] ?? null) : null;
|
|
8145
|
+
this.recordAccess(actor, 'listShreddedSubjects', { tenantId, scopeId }, null, subjects.length);
|
|
8146
|
+
return { subjects, nextCursor };
|
|
8147
|
+
},
|
|
8148
|
+
shredSubject: async (actor, tenantId, scopeId, subjectId, fence, stamp, storeErasures) => {
|
|
7793
8149
|
this.assertScope(tenantId, scopeId);
|
|
7794
8150
|
const before = stamp ?? {
|
|
7795
8151
|
erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
|
|
7796
8152
|
?.erasure_epoch ?? 0,
|
|
7797
8153
|
path: 'direct',
|
|
7798
8154
|
};
|
|
8155
|
+
// #2285: a direct shred with a fence claims what it read, as the finalize does; the epoch
|
|
8156
|
+
// it records is the one its claim compared.
|
|
8157
|
+
if (fence)
|
|
8158
|
+
before.erasureEpoch = fence.epoch;
|
|
7799
8159
|
// Redact the live spine FIRST. Both halves are idempotent and a crash between them
|
|
7800
8160
|
// converges on retry, so the order is decided by which half-done state harms the
|
|
7801
8161
|
// person: dying after this leaves ciphertext in a backup that no key opens; dying
|
|
@@ -7864,6 +8224,13 @@ export class SqliteScopeHost {
|
|
|
7864
8224
|
// intents, an issue's exemplar, a sweep record's error. Before the key, for the
|
|
7865
8225
|
// ordering reason above.
|
|
7866
8226
|
redactSubjectDirectoryText(redactionSqlOf(this.directory), { tenantId, scopeId, subjectId, intentIds });
|
|
8227
|
+
// Scope stores after the scope-side redaction and before the key (#2068). A store
|
|
8228
|
+
// that errors throws here, so the key below is untouched and a retry converges.
|
|
8229
|
+
// An orchestrated caller passes the serving script's result and this host does
|
|
8230
|
+
// not erase again.
|
|
8231
|
+
const erasures = storeErasures ?? (await this.eraseScopeStores(tenantId, scopeId, subjectId));
|
|
8232
|
+
if (fence)
|
|
8233
|
+
this.claimSubjectErasure(tenantId, scopeId, fence);
|
|
7867
8234
|
const { existed } = await this.subjectKeysFor(tenantId, scopeId).destroy(subjectId, at);
|
|
7868
8235
|
const receipt = subjectShredReceipt.parse({
|
|
7869
8236
|
subjectId,
|
|
@@ -7871,6 +8238,7 @@ export class SqliteScopeHost {
|
|
|
7871
8238
|
intentsRedacted,
|
|
7872
8239
|
jobRunsRedacted,
|
|
7873
8240
|
...vertical,
|
|
8241
|
+
storeErasures: erasures,
|
|
7874
8242
|
keyDestroyed: existed,
|
|
7875
8243
|
tombstoned: true,
|
|
7876
8244
|
});
|
|
@@ -7891,23 +8259,14 @@ export class SqliteScopeHost {
|
|
|
7891
8259
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
7892
8260
|
return row.erasure_epoch;
|
|
7893
8261
|
},
|
|
7894
|
-
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected) => {
|
|
8262
|
+
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected, storeErasures) => {
|
|
7895
8263
|
if (redactions.length === 0)
|
|
7896
8264
|
throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
|
|
7897
|
-
|
|
7898
|
-
WHERE tenant_id = ? AND scope_id = ? AND vertical_version_id IS ?
|
|
7899
|
-
AND serving_ref IS ? AND COALESCE(erasure_epoch, 0) = ?
|
|
7900
|
-
AND NOT EXISTS (SELECT 1 FROM scope_script_copies AS copy
|
|
7901
|
-
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id
|
|
7902
|
-
AND copy.state = 'pending')
|
|
7903
|
-
AND (SELECT COUNT(*) FROM scope_script_copies AS copy
|
|
7904
|
-
WHERE copy.tenant_id = scopes.tenant_id AND copy.scope_id = scopes.scope_id) = ?`).run(tenantId, scopeId, expected.versionId, expected.servingRef, expected.epoch, expected.copyCount);
|
|
7905
|
-
if (claimed.changes === 0)
|
|
7906
|
-
throw substratError('precondition_failed', 'scope route or copy inventory changed during subject erasure; retry after it settles');
|
|
8265
|
+
this.claimSubjectErasure(tenantId, scopeId, expected);
|
|
7907
8266
|
// The pure adapter has one co-located scope store. Its existing atomic shred is
|
|
7908
8267
|
// the final local redaction and key destruction, after remote copies confirmed.
|
|
7909
8268
|
const shred = this.admin.shredSubject;
|
|
7910
|
-
return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' });
|
|
8269
|
+
return shred(actor, tenantId, scopeId, subjectId, undefined, { erasureEpoch: expected.epoch, path: 'orchestrated' }, storeErasures);
|
|
7911
8270
|
},
|
|
7912
8271
|
// -- impersonation (K-42, #868) ----------------------------------------
|
|
7913
8272
|
beginImpersonation: async (actor, input) => {
|
|
@@ -7983,6 +8342,7 @@ export class SqliteScopeHost {
|
|
|
7983
8342
|
'_substrat_finding_rules', // #1748: the tenant's suppress rules
|
|
7984
8343
|
'_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
|
|
7985
8344
|
'_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
|
|
8345
|
+
'_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
|
|
7986
8346
|
];
|
|
7987
8347
|
const clear = this.directory.transaction(() => {
|
|
7988
8348
|
for (const table of tables) {
|
|
@@ -8091,6 +8451,7 @@ export class SqliteScopeHost {
|
|
|
8091
8451
|
// #1524: the stored gauge's latest sample per non-reaped scope. A directory read only.
|
|
8092
8452
|
storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
|
|
8093
8453
|
storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
|
|
8454
|
+
storeStorage: listScopeStoreStorageRows(redactionSqlOf(this.directory), only),
|
|
8094
8455
|
});
|
|
8095
8456
|
// The count that matters for K-24 is how many TENANTS this reading covered —
|
|
8096
8457
|
// "read the meter for one tenant" and "metered the whole fleet" are different
|
|
@@ -8541,6 +8902,10 @@ export class SqliteScopeHost {
|
|
|
8541
8902
|
const { tenantId, scopeId, ...after } = memberChangeAudit.parse(entry);
|
|
8542
8903
|
this.recordAdmin(actor, 'manageScopeMember', { tenantId, scopeId }, null, after);
|
|
8543
8904
|
},
|
|
8905
|
+
recordConnectionBindingChange: async (actor, entry) => {
|
|
8906
|
+
const { tenantId, scopeId, ...after } = connectionBindingAudit.parse(entry);
|
|
8907
|
+
this.recordAdmin(actor, 'bindConnectionScope', { tenantId, scopeId }, null, after);
|
|
8908
|
+
},
|
|
8544
8909
|
recordPeerBindingChange: async (actor, entry) => {
|
|
8545
8910
|
const { tenantId, scopeId, ...after } = peerBindingAudit.parse(entry);
|
|
8546
8911
|
this.recordAdmin(actor, 'setPeerBinding', { tenantId, scopeId }, null, after);
|
|
@@ -9707,6 +10072,18 @@ export class SqliteScopeHost {
|
|
|
9707
10072
|
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
9708
10073
|
minted,
|
|
9709
10074
|
}),
|
|
10075
|
+
// Upload links: mint / revoke / list, written once in the kernel — the same four things
|
|
10076
|
+
// `capabilities` is handed, and the same consumer-is-system-actor rule.
|
|
10077
|
+
attachments: createAttachmentVerbs({
|
|
10078
|
+
sql: spineSql(rt.db),
|
|
10079
|
+
subject: overrideActor ? { kind: 'system', id: overrideActor.system } : subject,
|
|
10080
|
+
now: at,
|
|
10081
|
+
check: runCheck,
|
|
10082
|
+
emit: (event) => writeEvent(event, 'kernel'),
|
|
10083
|
+
assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
|
|
10084
|
+
minted,
|
|
10085
|
+
targetOf: (entityType) => this.uploadTargetOf(entityType),
|
|
10086
|
+
}),
|
|
9710
10087
|
// K-16 / #1864: link and relink, written once in the kernel over the raw spine seam.
|
|
9711
10088
|
...createEntityEdgeVerbs({
|
|
9712
10089
|
sql: spineSql(rt.db),
|