@substrat-run/adapter-sqlite 0.159.0 → 0.160.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 +53 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +390 -135
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync, writeSync } from 'node:fs';
|
|
2
3
|
import { dirname, join } from 'node:path';
|
|
3
4
|
import Database from 'better-sqlite3';
|
|
4
|
-
import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED,
|
|
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, checkHeldOperationsOf, countChecks, noteUncheckedCompletion, 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, assertModuleDeclared, noteReservedKeys, reservedGrantRefusal, reservedRoleRefusal, reservedShapeRefusal, 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, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, 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';
|
|
5
|
+
import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, assertNoBindingShapeBreak, importsOfManifestJson, connectionLandingsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, 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, deploymentWireAuth, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, connectionBindingAudit, copyMarkAudit, orphanSweepAudit, redrainEventsInput, REDRAIN_BATCH, errorCodeOf, } from '@substrat-run/contracts';
|
|
6
|
+
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, assertPlatformRequestRoom, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, scopeNotHeldRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, checkHeldOperationsOf, countChecks, noteUncheckedCompletion, 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, isSizedByteStream, knownLengthStream, putAttachmentBytes, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, readableScopes, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, ATTACHMENT_BYTES_SQL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeAttachmentStorageRows, retireScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, REQUEST_ROLLUP_DDL, listRequestDayRows, listRequestRollupDayRows, pruneRequestDayRows, recordRequestDayRows, METER_DAYS_DDL, VERTICAL_OWNERS_BACKFILL, VERTICAL_OWNERS_DDL, closeVerticalOwnerRow, listVerticalOwnerRows, openVerticalOwnerRow, verticalOwnerNow, listMeterDayRows, listMeterDayTakeRows, pruneMeterDayRows, recordMeterDayRows, 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, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, reservedGrantRefusal, reservedRoleDefinitionRefusal, reservedRoleRefusal, reservedShapeRefusal, SCOPE_SEATED_ROLES_SQL, SEAT_HOLDING_SCOPE_STATUSES, TENANT_SEATED_ROLES_SQL, 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, DEPLOYMENT_WIRE_AUTH_DDL, assertWireAuthFilter, deploymentWireAuthOf, forgetVerticalWireAuth, wireAuthAccessTarget, writeDeploymentWireAuth, 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, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, 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, fetchLikeOf, deadlineFetch, 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
7
|
import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
|
|
7
8
|
import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
|
|
8
9
|
import { INERT_SCOPE_REASON, isPrimaryScopeRow, normalizeUploadMediaType } from '@substrat-run/kernel';
|
|
10
|
+
import { opsFailureReferenceWhere } from '@substrat-run/kernel';
|
|
9
11
|
import { COPY_BACKFILL_SCOPE_SQL, COPY_BACKFILL_SQL, COPY_BACKFILL_SUPERSEDE_SQL, COPY_CLAIM_SQL, COPY_EXPIRED_SQL, COPY_MOVE_RENEW_SQL, copyMoveRenewParams, PENDING_COPY_KEEP_ALIVE_SQL, pendingCopyKeepAliveParams, 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
12
|
import { GRANT_CHILDREN_INDEX_DDL, SHAPE_MARKER_INDEX_DDL, grantedEntitiesForContext } from '@substrat-run/kernel';
|
|
11
13
|
import { LEGACY_SCOPE_ROWS_BACKFILL, assertSpineTablesBuilt, capabilitiesForLoad, dumpRowsInsert, isSpineTable, loadDirectoryDump, repointScopeGrants, settleCopiedWork, emittedHere, spineColumnAdditions } from '@substrat-run/kernel';
|
|
@@ -28,6 +30,45 @@ export function isStorageFault(err) {
|
|
|
28
30
|
* this map — cannot write one. Registered as each context is built.
|
|
29
31
|
*/
|
|
30
32
|
const kernelEmitters = new WeakMap();
|
|
33
|
+
/**
|
|
34
|
+
* Node's running SHA-256 (#2498), for a streamed upload — the kernel's `putAttachmentBytes` takes
|
|
35
|
+
* it. `node:crypto` because Web Crypto has no running digest, and hashing the written file
|
|
36
|
+
* afterwards would read it whole into memory, which is what streaming is for.
|
|
37
|
+
*/
|
|
38
|
+
function nodeSha256() {
|
|
39
|
+
const hash = createHash('sha256');
|
|
40
|
+
return {
|
|
41
|
+
update: (chunk) => {
|
|
42
|
+
hash.update(chunk);
|
|
43
|
+
},
|
|
44
|
+
hex: async () => hash.digest('hex'),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Write a sized stream to `path` one chunk at a time (#2498), so the file is never held whole,
|
|
49
|
+
* held to its declared length as the store contract says: a stream that carries more or fewer
|
|
50
|
+
* bytes leaves no file.
|
|
51
|
+
*/
|
|
52
|
+
async function writeStreamToFile(path, body) {
|
|
53
|
+
const reader = knownLengthStream(body).getReader();
|
|
54
|
+
const fd = openSync(path, 'w');
|
|
55
|
+
try {
|
|
56
|
+
for (;;) {
|
|
57
|
+
const { done, value } = await reader.read();
|
|
58
|
+
if (done)
|
|
59
|
+
break;
|
|
60
|
+
if (value)
|
|
61
|
+
writeSync(fd, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
rmSync(path, { force: true });
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
closeSync(fd);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
31
72
|
function kernelEmit(ctx, event) {
|
|
32
73
|
const write = kernelEmitters.get(ctx);
|
|
33
74
|
if (!write)
|
|
@@ -508,6 +549,10 @@ const admitByDelivery = (rt, eventId, row) => {
|
|
|
508
549
|
* it refuses another process.
|
|
509
550
|
*/
|
|
510
551
|
const PROCESS_HOST_DIRS = new Map();
|
|
552
|
+
/** A scope database's size in bytes: allocated pages, free ones included, which is what the file costs on disk (#1524). */
|
|
553
|
+
const databaseBytesOf = (db) => db.pragma('page_count', { simple: true }) * db.pragma('page_size', { simple: true });
|
|
554
|
+
/** What a scope database's attachment rows sum to (#2501). */
|
|
555
|
+
const attachmentBytesOf = (db) => db.prepare(ATTACHMENT_BYTES_SQL).get().bytes;
|
|
511
556
|
/**
|
|
512
557
|
* #119: claim a host directory for this process, for one live `SqliteScopeHost`. The first host in
|
|
513
558
|
* the process takes an exclusive lock on `_host.lock` (a SQLite file: `locking_mode = EXCLUSIVE`
|
|
@@ -631,6 +676,8 @@ export class SqliteScopeHost {
|
|
|
631
676
|
modules = new Map();
|
|
632
677
|
/** #2430: what a registered module declares `holder: 'system'` — the role doors' refusal reads it. */
|
|
633
678
|
reservedKeys = new Set();
|
|
679
|
+
/** #2447: every key a registered or listed module declares, marked or not — a role definition reads it. */
|
|
680
|
+
declaredKeys = new Set();
|
|
634
681
|
/** #2430: the declared permission surface `registerModule` holds each module to, when given. */
|
|
635
682
|
permissions;
|
|
636
683
|
/** #1705: what this deployment exports to other verticals and imports from them. */
|
|
@@ -722,6 +769,7 @@ export class SqliteScopeHost {
|
|
|
722
769
|
wrapBlobStore;
|
|
723
770
|
attachmentTextBounds;
|
|
724
771
|
fetchImpl;
|
|
772
|
+
streamIdleMs;
|
|
725
773
|
connectorCalls;
|
|
726
774
|
clock;
|
|
727
775
|
versionId;
|
|
@@ -741,15 +789,19 @@ export class SqliteScopeHost {
|
|
|
741
789
|
this.attachmentExtractors = options.attachmentExtractors ?? [];
|
|
742
790
|
this.wrapBlobStore = options.wrapBlobStore ?? ((store) => store);
|
|
743
791
|
this.attachmentTextBounds = resolveAttachmentTextBounds(options.attachmentTextBounds);
|
|
744
|
-
|
|
792
|
+
// #2498: whatever fetch is injected, a sized stream body reaches it converted (`fetchLikeOf`).
|
|
793
|
+
this.fetchImpl = fetchLikeOf(options.fetch ?? globalFetch);
|
|
794
|
+
this.streamIdleMs = options.streamIdleTimeoutMs;
|
|
745
795
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
746
796
|
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
747
797
|
this.versionId = options.versionId ?? null;
|
|
748
798
|
this.migrationsDigest = options.migrationsDigest;
|
|
749
799
|
this.permissions = options.permissions;
|
|
750
800
|
// #2430: what the declared surface reserves holds here too, whatever is registered under its ids.
|
|
751
|
-
for (const m of options.permissions?.modules ?? [])
|
|
801
|
+
for (const m of options.permissions?.modules ?? []) {
|
|
752
802
|
noteReservedKeys(this.reservedKeys, m.manifest);
|
|
803
|
+
noteDeclaredKeys(this.declaredKeys, m.manifest);
|
|
804
|
+
}
|
|
753
805
|
this.scopeStoreVertical = options.scopeStores?.vertical;
|
|
754
806
|
this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
|
|
755
807
|
this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
|
|
@@ -1047,6 +1099,7 @@ export class SqliteScopeHost {
|
|
|
1047
1099
|
);
|
|
1048
1100
|
${VERSION_MIGRATIONS_DDL}
|
|
1049
1101
|
${VERTICAL_SCOPE_STORES_DDL}
|
|
1102
|
+
${DEPLOYMENT_WIRE_AUTH_DDL}
|
|
1050
1103
|
-- Channels (#31 step 2): a named pointer per vertical. Promotion moves it,
|
|
1051
1104
|
-- and promotion is where the migration and permission diffs fire — the
|
|
1052
1105
|
-- moment a change reaches anyone, rather than the moment it was typed.
|
|
@@ -1437,6 +1490,12 @@ export class SqliteScopeHost {
|
|
|
1437
1490
|
CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
|
|
1438
1491
|
-- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
|
|
1439
1492
|
${SCOPE_STORAGE_DDL}
|
|
1493
|
+
-- #2501: the daily request rollup — one row per (UTC day, tenant, vertical, scope), kernel-owned DDL.
|
|
1494
|
+
${REQUEST_ROLLUP_DDL}
|
|
1495
|
+
-- #2501: the daily meter snapshot — one row per (UTC day, tenant), kernel-owned DDL.
|
|
1496
|
+
${METER_DAYS_DDL}
|
|
1497
|
+
-- #2501: who owned each vertical slug, period by period — kernel-owned DDL.
|
|
1498
|
+
${VERTICAL_OWNERS_DDL}
|
|
1440
1499
|
CREATE INDEX IF NOT EXISTS scopes_tenant ON scopes (tenant_id, scope_id);
|
|
1441
1500
|
`);
|
|
1442
1501
|
this.ensureDirectoryColumns();
|
|
@@ -1558,7 +1617,8 @@ export class SqliteScopeHost {
|
|
|
1558
1617
|
.prepare('SELECT vertical FROM scopes WHERE scope_id = ?')
|
|
1559
1618
|
.get(rt.scopeId)?.vertical ?? null;
|
|
1560
1619
|
const admin = this.admin;
|
|
1561
|
-
|
|
1620
|
+
// #2518: the call under `timeoutMs`, a streamed body under the idle window instead.
|
|
1621
|
+
const fetchImpl = deadlineFetch(this.fetchImpl, timeoutMs, { idleMs: this.streamIdleMs });
|
|
1562
1622
|
return {
|
|
1563
1623
|
admin,
|
|
1564
1624
|
tenantId: rt.tenantId,
|
|
@@ -1611,10 +1671,7 @@ export class SqliteScopeHost {
|
|
|
1611
1671
|
// records beside the health line carries a duration.
|
|
1612
1672
|
const started = Date.now();
|
|
1613
1673
|
try {
|
|
1614
|
-
const res = await fetchImpl(input,
|
|
1615
|
-
...init,
|
|
1616
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
1617
|
-
});
|
|
1674
|
+
const res = await fetchImpl(input, init);
|
|
1618
1675
|
// A 5xx is the provider failing; a 4xx is usually us. Both are
|
|
1619
1676
|
// worth recording, because "the connection stopped working" is the
|
|
1620
1677
|
// question a health view answers.
|
|
@@ -1744,6 +1801,7 @@ export class SqliteScopeHost {
|
|
|
1744
1801
|
purgeOnlyKeys: purgeOnlyKeysOf(manifest.schedules ?? []),
|
|
1745
1802
|
});
|
|
1746
1803
|
noteReservedKeys(this.reservedKeys, manifest);
|
|
1804
|
+
noteDeclaredKeys(this.declaredKeys, manifest);
|
|
1747
1805
|
if (erasure)
|
|
1748
1806
|
this.erasurePlans.push(erasure);
|
|
1749
1807
|
for (const rel of manifest.entityRelations ?? []) {
|
|
@@ -2130,7 +2188,10 @@ export class SqliteScopeHost {
|
|
|
2130
2188
|
put: async (key, body, opts) => {
|
|
2131
2189
|
const p = resolveKey(key);
|
|
2132
2190
|
mkdirSync(dirname(p), { recursive: true });
|
|
2133
|
-
|
|
2191
|
+
if (isSizedByteStream(body))
|
|
2192
|
+
await writeStreamToFile(p, body);
|
|
2193
|
+
else
|
|
2194
|
+
writeFileSync(p, body);
|
|
2134
2195
|
writeFileSync(`${p}.meta`, JSON.stringify({ contentType: opts?.contentType ?? null }));
|
|
2135
2196
|
},
|
|
2136
2197
|
get: async (key) => {
|
|
@@ -2416,24 +2477,33 @@ export class SqliteScopeHost {
|
|
|
2416
2477
|
});
|
|
2417
2478
|
}
|
|
2418
2479
|
const gate = targetGate(input.entity.entityType);
|
|
2480
|
+
// A streamed body is the case where bytes are the cost, so the write gate is asked before
|
|
2481
|
+
// the first one is read (#2516). The row's own check below stays: this one is only early.
|
|
2482
|
+
if (isSizedByteStream(input.body)) {
|
|
2483
|
+
// A read that writes nothing: one actor turn, no transaction and no drain (the denial is
|
|
2484
|
+
// still recorded). Already inside the turn on a reentrant surface, which must not queue again.
|
|
2485
|
+
const askGate = () => reentrant('attachments.upload', async (ctx) => {
|
|
2486
|
+
assertAllowed(await ctx.check(gate.write, input.entity));
|
|
2487
|
+
});
|
|
2488
|
+
await (opts.reentrant ? askGate() : rt.actor.enqueue(askGate));
|
|
2489
|
+
}
|
|
2419
2490
|
const creator = authority;
|
|
2420
2491
|
const id = ulid();
|
|
2421
2492
|
const key = attachmentBlobKey(rt.scopeId, id);
|
|
2422
|
-
const
|
|
2493
|
+
const fields = {
|
|
2423
2494
|
id,
|
|
2424
2495
|
entity: input.entity,
|
|
2425
2496
|
filename: input.filename,
|
|
2426
2497
|
contentType: input.contentType,
|
|
2427
|
-
size: input.body.byteLength,
|
|
2428
|
-
sha256: await attachmentSha256(input.body),
|
|
2429
2498
|
visibility: input.visibility,
|
|
2430
2499
|
createdBy: creator.id,
|
|
2431
2500
|
createdAt: new Date().toISOString(),
|
|
2432
|
-
}
|
|
2501
|
+
};
|
|
2433
2502
|
// Bytes first, row second: a crash between the two leaves an orphaned object that no
|
|
2434
2503
|
// row points at (#2314). A refused check compensates the object away below; a drain
|
|
2435
2504
|
// fault after the commit is not a refusal (`serialized` keeps it from becoming one).
|
|
2436
|
-
|
|
2505
|
+
// A streamed body is hashed and counted as it is written (#2498).
|
|
2506
|
+
const record = await putAttachmentBytes(store, key, fields, input.body, nodeSha256);
|
|
2437
2507
|
try {
|
|
2438
2508
|
return await guarded('attachments.upload', async (ctx) => {
|
|
2439
2509
|
assertAllowed(await ctx.check(gate.write, input.entity));
|
|
@@ -2760,6 +2830,7 @@ export class SqliteScopeHost {
|
|
|
2760
2830
|
scopeId: snapshotId,
|
|
2761
2831
|
kind: opts?.kind ?? 'archive',
|
|
2762
2832
|
vertical: source.vertical,
|
|
2833
|
+
verticalSource: 'copy',
|
|
2763
2834
|
jurisdiction: source.jurisdiction,
|
|
2764
2835
|
expiresAt: opts?.expiresAt,
|
|
2765
2836
|
// forkedFrom/forkedAt are stamped from the dump by importScope.
|
|
@@ -3785,6 +3856,92 @@ export class SqliteScopeHost {
|
|
|
3785
3856
|
if (refusal)
|
|
3786
3857
|
throw substratError('forbidden', refusal);
|
|
3787
3858
|
}
|
|
3859
|
+
/**
|
|
3860
|
+
* #2447: refuse defining `role` for `tenantId` (`reservedRoleDefinitionRefusal`): against the keys
|
|
3861
|
+
* the modules registered or listed here reserve and declare, and the tenant's other roles.
|
|
3862
|
+
*/
|
|
3863
|
+
refuseReservedDefinition(tenantId, role) {
|
|
3864
|
+
const prefix = `${tenantId}/`;
|
|
3865
|
+
const tenantRoles = [...this.roles].filter(([k]) => k.startsWith(prefix)).map(([, r]) => r);
|
|
3866
|
+
const stored = this.roles.get(`${tenantId}/${role.key}`);
|
|
3867
|
+
// The seats are read only for the one case they decide: a system-held definition of a key with no
|
|
3868
|
+
// system-held definition stored (none at all included: a seat can outlive its role's row).
|
|
3869
|
+
const seated = stored?.holder !== 'system' && role.holder === 'system' && this.roleSeated(tenantId, role.key);
|
|
3870
|
+
const refusal = reservedRoleDefinitionRefusal(role, { reserved: this.reservedKeys, declared: this.declaredKeys }, tenantRoles, seated);
|
|
3871
|
+
if (refusal)
|
|
3872
|
+
throw substratError('forbidden', refusal);
|
|
3873
|
+
}
|
|
3874
|
+
/**
|
|
3875
|
+
* #2447: does anyone but a system principal hold a live seat in `roleKey` in this tenant — a
|
|
3876
|
+
* tenant-level assignment, or a scope-level one in any of its scopes not reaped
|
|
3877
|
+
* (`SEAT_HOLDING_SCOPE_STATUSES`: an archived scope's seats come back on unarchive, one archived
|
|
3878
|
+
* straight from provisioning included)? A scope with no store on disk holds none and is not opened.
|
|
3879
|
+
*/
|
|
3880
|
+
roleSeated(tenantId, roleKey) {
|
|
3881
|
+
const now = this.clock();
|
|
3882
|
+
const holds = (rows) => rows.some((r) => r.role_key === roleKey);
|
|
3883
|
+
if (holds(this.directory.prepare(TENANT_SEATED_ROLES_SQL).all(tenantId, now)))
|
|
3884
|
+
return true;
|
|
3885
|
+
const scopes = this.directory
|
|
3886
|
+
.prepare(`SELECT scope_id FROM scopes WHERE tenant_id = ? AND status IN (${SEAT_HOLDING_SCOPE_STATUSES.map(() => '?').join(', ')})`)
|
|
3887
|
+
.all(tenantId, ...SEAT_HOLDING_SCOPE_STATUSES);
|
|
3888
|
+
return scopes.some((s) => {
|
|
3889
|
+
// Read without creating storage: a scope whose store is neither open nor on disk never held a
|
|
3890
|
+
// seat (a scope archived straight from provisioning may be one), and opening it would create it.
|
|
3891
|
+
const id = s.scope_id;
|
|
3892
|
+
if (!this.scopes.has(`${tenantId}/${id}`) && !existsSync(join(this.dir, `${tenantId}__${id}.sqlite`)))
|
|
3893
|
+
return false;
|
|
3894
|
+
return holds(this.runtime(tenantId, id).db.prepare(SCOPE_SEATED_ROLES_SQL).all(now));
|
|
3895
|
+
});
|
|
3896
|
+
}
|
|
3897
|
+
/**
|
|
3898
|
+
* #2446: the roles this host stores for `tenantId` and the role keys anyone but a system principal
|
|
3899
|
+
* holds a live seat in at `scopeId` (`SCOPE_SEATED_ROLES_SQL`): the Cloudflare host's
|
|
3900
|
+
* `roleStateLocal`, the state a CP-less reconcile judges a role set against and a promote judges the
|
|
3901
|
+
* incoming version's set with. It is deliberately narrower than this host's own `defineRole`
|
|
3902
|
+
* judgement (`roleSeated`), which also counts tenant-level seats and every other scope's: a
|
|
3903
|
+
* reconcile defines a scope's role set and reads that scope's seats only, and this answers what it
|
|
3904
|
+
* reads (`roleStateContractSuite` pins the two adapters to the same answer, a tenant-level seat
|
|
3905
|
+
* and another scope's seat excluded). Read-only: a scope with no store on disk holds no seat and is
|
|
3906
|
+
* not opened, so the read creates nothing.
|
|
3907
|
+
*/
|
|
3908
|
+
async roleStateLocal(tenantId, scopeId) {
|
|
3909
|
+
const prefix = `${tenantId}/`;
|
|
3910
|
+
const roles = [...this.roles]
|
|
3911
|
+
.filter(([k]) => k.startsWith(prefix))
|
|
3912
|
+
.map(([, r]) => ({ key: r.key, permissions: [...r.permissions], ...(r.holder ? { holder: r.holder } : {}) }));
|
|
3913
|
+
const stored = this.scopes.has(`${tenantId}/${scopeId}`) || existsSync(join(this.dir, `${tenantId}__${scopeId}.sqlite`));
|
|
3914
|
+
const seated = stored
|
|
3915
|
+
? [...new Set(this.runtime(tenantId, scopeId).db.prepare(SCOPE_SEATED_ROLES_SQL).all(this.clock()).map((r) => r.role_key))]
|
|
3916
|
+
: [];
|
|
3917
|
+
return { roles, seated };
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* The scope database's size in bytes, asked by scope id alone (#2541): the Cloudflare host's
|
|
3921
|
+
* `databaseSizeLocal`, which a vertical's `/internal/database-size` calls. Counts as
|
|
3922
|
+
* `scopeDatabaseSize` does. A scope this host does not hold is refused before any store is opened.
|
|
3923
|
+
*/
|
|
3924
|
+
async databaseSizeLocal(scopeId) {
|
|
3925
|
+
return databaseBytesOf(this.heldReadDb(scopeId));
|
|
3926
|
+
}
|
|
3927
|
+
/** What the scope's attachment rows sum to (#2501), asked by scope id alone: refused as the size is (#2541). */
|
|
3928
|
+
async attachmentSizeLocal(scopeId) {
|
|
3929
|
+
return attachmentBytesOf(this.heldReadDb(scopeId));
|
|
3930
|
+
}
|
|
3931
|
+
/**
|
|
3932
|
+
* The read connection of a scope this host holds, for a read that names only the scope id (#2541),
|
|
3933
|
+
* with the directory read ONCE. Refused `conflict` before anything is opened: no row, a reaped one
|
|
3934
|
+
* (a tombstone whose store is gone), or a scope with no store open or on disk (archived straight
|
|
3935
|
+
* from provisioning, as the Cloudflare host's object holding nothing is), since `runtime()` would
|
|
3936
|
+
* create the store those leave absent.
|
|
3937
|
+
*/
|
|
3938
|
+
heldReadDb(scopeId) {
|
|
3939
|
+
const row = this.directory.prepare('SELECT tenant_id, status FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
3940
|
+
const stored = row !== undefined && (this.scopes.has(`${row.tenant_id}/${scopeId}`) || existsSync(join(this.dir, `${row.tenant_id}__${scopeId}.sqlite`)));
|
|
3941
|
+
if (!row || row.status === 'reaped' || !stored)
|
|
3942
|
+
throw substratError('conflict', scopeNotHeldRefusal(scopeId));
|
|
3943
|
+
return this.readDb(this.runtime(row.tenant_id, scopeId));
|
|
3944
|
+
}
|
|
3788
3945
|
/** #2430: refuse a raw grant of a key a module registered here reserves (`reservedGrantRefusal`). */
|
|
3789
3946
|
refuseReservedGrant(permissions) {
|
|
3790
3947
|
const refusal = reservedGrantRefusal(permissions, this.reservedKeys);
|
|
@@ -3921,7 +4078,9 @@ export class SqliteScopeHost {
|
|
|
3921
4078
|
});
|
|
3922
4079
|
return report;
|
|
3923
4080
|
}
|
|
3924
|
-
async runDueSchedules(moduleId, tenantId, scopeId
|
|
4081
|
+
async runDueSchedules(moduleId, tenantId, scopeId,
|
|
4082
|
+
/** #2517: what the sweep tick's earlier calls did to this scope (`ScopeHost.runDueSchedules`). */
|
|
4083
|
+
tick) {
|
|
3925
4084
|
const report = { fired: 0, skipped: 0, failed: 0, errors: [], runs: [] };
|
|
3926
4085
|
const mod = this.modules.get(moduleId);
|
|
3927
4086
|
if (!mod || mod.schedules.length === 0)
|
|
@@ -3989,6 +4148,16 @@ export class SqliteScopeHost {
|
|
|
3989
4148
|
// scope at once. `ScheduleRunReport.platformRequests` says what that does and does not count.
|
|
3990
4149
|
let platformRequests = 0;
|
|
3991
4150
|
const counted = { onPlatformRequests: (n) => (platformRequests += n) };
|
|
4151
|
+
// #2517: the part of that the purge runs enqueued, which is what defers a later purge head the
|
|
4152
|
+
// cap refuses (`runPurgePass`): this call's, on top of what the tick's earlier calls report.
|
|
4153
|
+
const earlierPurgeRequests = tick?.earlierPurgeRequests ?? 0;
|
|
4154
|
+
let purgeRequests = 0;
|
|
4155
|
+
const purgeCounted = {
|
|
4156
|
+
onPlatformRequests: (n) => {
|
|
4157
|
+
platformRequests += n;
|
|
4158
|
+
purgeRequests += n;
|
|
4159
|
+
},
|
|
4160
|
+
};
|
|
3992
4161
|
for (const schedule of mod.schedules) {
|
|
3993
4162
|
const lastRunAt = gate.lastRuns.get(schedule.operation) ?? null;
|
|
3994
4163
|
const lastRun = lastRunAt ? Date.parse(lastRunAt) : null;
|
|
@@ -4016,7 +4185,16 @@ export class SqliteScopeHost {
|
|
|
4016
4185
|
// #119: a purge horizon's schedule runs its operation once per due entity, never once.
|
|
4017
4186
|
// #2192: under the run's own id, so the purges and their deliveries join this run's line.
|
|
4018
4187
|
// #2431: each purge is an invoke, and reports what it enqueued through `counted` like any other.
|
|
4019
|
-
|
|
4188
|
+
// #2517: what the purge runs before this one in the tick enqueued, so a head the cap refuses
|
|
4189
|
+
// because they filled the queue is deferred to the next tick rather than failed.
|
|
4190
|
+
const pass = await this.runPurgeSweep(moduleId, tenantId, scopeId, schedule.operation, invocationId, purgeCounted, earlierPurgeRequests + purgeRequests);
|
|
4191
|
+
if (pass.deferred) {
|
|
4192
|
+
// Ran nothing: no line, no cadence row, `skipped` like any run that did not happen.
|
|
4193
|
+
report.skipped += 1;
|
|
4194
|
+
report.deferred = (report.deferred ?? 0) + 1;
|
|
4195
|
+
report.runs.push({ operation: schedule.operation, outcome: 'skipped' });
|
|
4196
|
+
continue;
|
|
4197
|
+
}
|
|
4020
4198
|
stillDue = purgeStillDue(pass);
|
|
4021
4199
|
const outcome = purgeReportOf(schedule.operation, schedule.purge.entityType, pass);
|
|
4022
4200
|
report.errors.push(...outcome.errors);
|
|
@@ -4079,6 +4257,8 @@ export class SqliteScopeHost {
|
|
|
4079
4257
|
lines.end();
|
|
4080
4258
|
if (platformRequests > 0)
|
|
4081
4259
|
report.platformRequests = platformRequests;
|
|
4260
|
+
if (purgeRequests > 0)
|
|
4261
|
+
report.purgeRequests = purgeRequests;
|
|
4082
4262
|
return report;
|
|
4083
4263
|
}
|
|
4084
4264
|
/**
|
|
@@ -4093,7 +4273,9 @@ export class SqliteScopeHost {
|
|
|
4093
4273
|
/** #2192: the schedule run this pass belongs to; every purge in it runs under that id. */
|
|
4094
4274
|
runInvocationId,
|
|
4095
4275
|
/** #2431: how each purge reports the platform requests it enqueued. */
|
|
4096
|
-
counted
|
|
4276
|
+
counted,
|
|
4277
|
+
/** #2517: platform requests the purge runs earlier in this tick enqueued. */
|
|
4278
|
+
earlierPurgeRequests = 0) {
|
|
4097
4279
|
const entityType = this.modules
|
|
4098
4280
|
.get(moduleId)
|
|
4099
4281
|
?.schedules.find((s) => s.operation === operation && s.purge)?.purge?.entityType;
|
|
@@ -4121,7 +4303,7 @@ export class SqliteScopeHost {
|
|
|
4121
4303
|
const stub = await this.openSystemScope(moduleId, tenantId, scopeId, true, counted);
|
|
4122
4304
|
return runPurgePass(operation, due, async (entityId) => {
|
|
4123
4305
|
await stub.invoke(operation, { [due.idFrom]: entityId }, { invocationId: runInvocationId });
|
|
4124
|
-
}, (write) => rt.actor.turn(() => write(spineSql(rt.db))));
|
|
4306
|
+
}, (write) => rt.actor.turn(() => write(spineSql(rt.db))), earlierPurgeRequests);
|
|
4125
4307
|
}
|
|
4126
4308
|
/**
|
|
4127
4309
|
* #119: the purge gate's facts (`PurgeGateFacts`) — the module's switch from the scope's own
|
|
@@ -5600,15 +5782,20 @@ export class SqliteScopeHost {
|
|
|
5600
5782
|
// #1748: the evidence, its issue and its finding in ONE transaction — a detector that
|
|
5601
5783
|
// throws takes the evidence with it, so the caller's retry observes it rather than finding
|
|
5602
5784
|
// the row already there and the observation lost.
|
|
5785
|
+
// #2540: a tenant's row with a `detail` is a pair — the tenant's row says what was answered, and
|
|
5786
|
+
// a row with no tenant, which only staff read, carries the failure's own text. Counted once.
|
|
5787
|
+
const detail = entry.tenantId && typeof entry.detail === 'string' ? entry.detail : null;
|
|
5603
5788
|
this.directory.transaction(() => {
|
|
5604
|
-
this.directory
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
.run(ulid(), entry.actor, entry.operation, entry.stage ?? null, entry.tenantId ?? null, entry.scopeId ?? null, entry.vertical ?? null, entry.version ?? null, entry.status ?? null,
|
|
5789
|
+
const insert = this.directory.prepare(`INSERT INTO _substrat_ops_failures
|
|
5790
|
+
(id, actor, operation, stage, tenant_id, scope_id, vertical, version, status, message, reference, origin, code, fingerprint, at)
|
|
5791
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
5792
|
+
const row = (tenantId, message) => insert.run(ulid(), entry.actor, entry.operation, entry.stage ?? null, tenantId, entry.scopeId ?? null, entry.vertical ?? null, entry.version ?? null, entry.status ?? null,
|
|
5609
5793
|
// Bounded here, not trusted from the catch site: one runaway upstream
|
|
5610
5794
|
// body must not become a runaway directory row (#559).
|
|
5611
|
-
|
|
5795
|
+
message.slice(0, 2000), entry.reference ?? null, entry.origin ?? null, entry.code ?? null, fingerprint, at);
|
|
5796
|
+
if (detail !== null)
|
|
5797
|
+
row(null, detail);
|
|
5798
|
+
row(entry.tenantId ?? null, entry.message);
|
|
5612
5799
|
// Prune-on-write (#559): retention lives here, not in a cron — every insert
|
|
5613
5800
|
// pays for its own housekeeping, so the table stays bounded even where no
|
|
5614
5801
|
// scheduled pass runs (this adapter has none).
|
|
@@ -5632,7 +5819,11 @@ export class SqliteScopeHost {
|
|
|
5632
5819
|
last_version = COALESCE(excluded.last_version, last_version),
|
|
5633
5820
|
origin = COALESCE(excluded.origin, origin),
|
|
5634
5821
|
status = CASE WHEN status = 'resolved' THEN 'regressed' ELSE status END`)
|
|
5635
|
-
.run(fingerprint, entry.operation, entry.stage ?? null, entry.origin ?? null, entry.code ?? null, at, at,
|
|
5822
|
+
.run(fingerprint, entry.operation, entry.stage ?? null, entry.origin ?? null, entry.code ?? null, at, at,
|
|
5823
|
+
// The issues are staff's (#2540): a pair's exemplar is its own text, not the tenant's
|
|
5824
|
+
// sentence, and it is owned as the row it is copied from is, the platform's — so erasure
|
|
5825
|
+
// reaches it on an id match, as it reaches that row.
|
|
5826
|
+
(detail ?? entry.message).slice(0, 2000), detail !== null ? null : (entry.tenantId ?? null), issueExemplarOwner(detail !== null ? null : (entry.tenantId ?? null)), entry.vertical ?? null, entry.version ?? null);
|
|
5636
5827
|
const issueHorizon = new Date(Date.now() - ISSUE_RETENTION_DAYS * 86_400_000).toISOString();
|
|
5637
5828
|
this.directory.prepare('DELETE FROM _substrat_issues WHERE last_seen < ?').run(issueHorizon);
|
|
5638
5829
|
// #1748: the tenant's Recurring finding, over the tenant's own occurrences only.
|
|
@@ -5833,6 +6024,32 @@ export class SqliteScopeHost {
|
|
|
5833
6024
|
archivedFromStatus: (r.archived_from_status ?? null),
|
|
5834
6025
|
createdAt: r.created_at,
|
|
5835
6026
|
});
|
|
6027
|
+
// The directory rows `listScopes` and `listScopesReadable` map, filtered and paged alike.
|
|
6028
|
+
const scopeRows = (filter) => {
|
|
6029
|
+
const where = [];
|
|
6030
|
+
const params = [];
|
|
6031
|
+
if (filter?.tenantId) {
|
|
6032
|
+
where.push('tenant_id = ?');
|
|
6033
|
+
params.push(filter.tenantId);
|
|
6034
|
+
}
|
|
6035
|
+
if (filter?.status) {
|
|
6036
|
+
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
6037
|
+
// An empty array means "no status is acceptable" — match nothing, rather
|
|
6038
|
+
// than degenerating into an unfiltered read of the whole fleet.
|
|
6039
|
+
if (statuses.length === 0)
|
|
6040
|
+
return [];
|
|
6041
|
+
// One JSON array, the shape the DO twin needs for its 100-parameter limit (#1776).
|
|
6042
|
+
where.push('status IN (SELECT value FROM json_each(?))');
|
|
6043
|
+
params.push(JSON.stringify(statuses));
|
|
6044
|
+
}
|
|
6045
|
+
if (filter?.vertical) {
|
|
6046
|
+
where.push('vertical = ?');
|
|
6047
|
+
params.push(filter.vertical);
|
|
6048
|
+
}
|
|
6049
|
+
const tail = keysetTail(where, params, 'scope_id', filter);
|
|
6050
|
+
const sql = 'SELECT * FROM scopes' + (where.length ? ` WHERE ${where.join(' AND ')}` : '') + tail;
|
|
6051
|
+
return this.directory.prepare(sql).all(...params);
|
|
6052
|
+
};
|
|
5836
6053
|
// The (version, scope) pair a bind and its impact read both start from, and the refusals
|
|
5837
6054
|
// that come before any export-break question, in the order the bind makes them (#1756).
|
|
5838
6055
|
const bindTarget = async (tenantId, scopeId, versionId) => {
|
|
@@ -5900,8 +6117,8 @@ export class SqliteScopeHost {
|
|
|
5900
6117
|
}
|
|
5901
6118
|
if (to === 'reaped') {
|
|
5902
6119
|
forgetSwitchesOf(switchSqlOf(this.directory), scopeId);
|
|
5903
|
-
// #
|
|
5904
|
-
|
|
6120
|
+
// #2515: its storage is gone, and its measured days are kept for the invoice (retention ends them).
|
|
6121
|
+
retireScopeStorage(redactionSqlOf(this.directory), scopeId);
|
|
5905
6122
|
}
|
|
5906
6123
|
// The audit target carries the scope's vertical (control-plane.md §4.4:
|
|
5907
6124
|
// "vertical stays null until §4.2 lifecycle actions that name one"). It is
|
|
@@ -6236,6 +6453,10 @@ export class SqliteScopeHost {
|
|
|
6236
6453
|
canStoreSecrets: isSecretBoxConfigured(this.secretBox),
|
|
6237
6454
|
defineRole: async (actor, tenantId, role) => {
|
|
6238
6455
|
const parsed = roleDefinition.parse(role);
|
|
6456
|
+
// #2447: judged before the write, against this host's keys and the tenant's other roles, so a
|
|
6457
|
+
// role assigned through a door cannot be widened afterwards. A refusal writes nothing, so a
|
|
6458
|
+
// seat of the role as it was stored keeps working.
|
|
6459
|
+
this.refuseReservedDefinition(tenantId, parsed);
|
|
6239
6460
|
const before = this.roles.get(`${tenantId}/${parsed.key}`) ?? null;
|
|
6240
6461
|
this.directory
|
|
6241
6462
|
.prepare(`INSERT OR REPLACE INTO _substrat_roles (tenant_id, role_key, permissions, source, holder)
|
|
@@ -6856,10 +7077,17 @@ export class SqliteScopeHost {
|
|
|
6856
7077
|
}
|
|
6857
7078
|
throw substratError('conflict', `vertical '${parsed.slug}' is already registered as ${existing.source}`);
|
|
6858
7079
|
}
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
7080
|
+
const createdAt = verticalOwnerNow();
|
|
7081
|
+
// #2501: the slug's ownership period opens with its row, in the same transaction.
|
|
7082
|
+
this.directoryWriteAndAudit(() => {
|
|
7083
|
+
const changed = this.directory
|
|
7084
|
+
.prepare('INSERT INTO verticals (slug, name, source, owner_tenant, env_spec, install_spec, listed, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)')
|
|
7085
|
+
.run(parsed.slug, parsed.name, parsed.source, parsed.ownerTenant, envSpecJson, installSpecJson, parsed.listed ? 1 : 0, createdAt)
|
|
7086
|
+
.changes > 0;
|
|
7087
|
+
if (changed)
|
|
7088
|
+
openVerticalOwnerRow(redactionSqlOf(this.directory), parsed.slug, parsed.ownerTenant, createdAt);
|
|
7089
|
+
return changed;
|
|
7090
|
+
}, () => this.recordAdmin(actor, 'registerVertical', { tenantId: null }, null, parsed));
|
|
6863
7091
|
},
|
|
6864
7092
|
listVerticals: async (actor, page) => {
|
|
6865
7093
|
const where = [];
|
|
@@ -6891,7 +7119,7 @@ export class SqliteScopeHost {
|
|
|
6891
7119
|
const migrationDigest = await publishedMigrationDigest({
|
|
6892
7120
|
id: audited.id, migrationDigest: audited.migrationDigest, manifestJson: manifestJson ?? null, migrations: split.migrations,
|
|
6893
7121
|
});
|
|
6894
|
-
this.
|
|
7122
|
+
this.directoryWriteAndAudit(() => {
|
|
6895
7123
|
this.directory
|
|
6896
7124
|
.prepare(`INSERT INTO vertical_versions
|
|
6897
7125
|
(id, vertical_slug, version, manifest_digest, permission_digest,
|
|
@@ -6902,14 +7130,14 @@ export class SqliteScopeHost {
|
|
|
6902
7130
|
writeVersionMigrations(switchSqlOf(this.directory), audited.id, split.migrations);
|
|
6903
7131
|
// #2286: the version's scope-store bindings, refusing one moved to another driver, in the insert's transaction.
|
|
6904
7132
|
indexVersionScopeStores(switchSqlOf(this.directory), audited.verticalSlug, audited.id, split.manifestJson);
|
|
6905
|
-
|
|
6906
|
-
this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
|
|
7133
|
+
return true;
|
|
7134
|
+
}, () => this.recordAdmin(actor, 'publishVersion', { tenantId: null }, null, {
|
|
6907
7135
|
...audited,
|
|
6908
7136
|
// The digest the registry holds, which differs from the pushed one only for an unheld version.
|
|
6909
7137
|
migrationDigest,
|
|
6910
7138
|
...(origin ? { origin } : {}),
|
|
6911
7139
|
admission: selfAdmits ? 'admitted' : 'pending',
|
|
6912
|
-
});
|
|
7140
|
+
}));
|
|
6913
7141
|
},
|
|
6914
7142
|
verticalScopeStores: async (actor, verticalSlug) => {
|
|
6915
7143
|
// One transaction: versions not indexed yet are indexed by the read (#2286).
|
|
@@ -6958,44 +7186,41 @@ export class SqliteScopeHost {
|
|
|
6958
7186
|
}
|
|
6959
7187
|
}
|
|
6960
7188
|
// Resolve any pending publish request either way, and set the flag.
|
|
6961
|
-
this.directory
|
|
7189
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
6962
7190
|
.prepare('UPDATE verticals SET listed = ?, publish_requested_at = NULL WHERE slug = ?')
|
|
6963
|
-
.run(listed ? 1 : 0, slug);
|
|
6964
|
-
this.recordAdmin(actor, 'setVerticalListed', { tenantId: null }, { listed: existing.listed }, { listed });
|
|
7191
|
+
.run(listed ? 1 : 0, slug).changes > 0, () => this.recordAdmin(actor, 'setVerticalListed', { tenantId: null }, { listed: existing.listed }, { listed }));
|
|
6965
7192
|
},
|
|
6966
7193
|
requestPublish: async (actor, slug) => {
|
|
6967
7194
|
const existing = readVertical(slug);
|
|
6968
7195
|
if (!existing)
|
|
6969
7196
|
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
6970
|
-
this.
|
|
6971
|
-
|
|
7197
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7198
|
+
.prepare('UPDATE verticals SET publish_requested_at = ? WHERE slug = ?')
|
|
7199
|
+
.run(new Date().toISOString(), slug).changes > 0, () => this.recordAdmin(actor, 'requestPublish', { tenantId: null }, null, { slug }));
|
|
6972
7200
|
},
|
|
6973
7201
|
setVerticalInstallsBlocked: async (actor, slug, blocked) => {
|
|
6974
7202
|
const existing = readVertical(slug);
|
|
6975
7203
|
if (!existing)
|
|
6976
7204
|
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
6977
|
-
this.directory
|
|
7205
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
6978
7206
|
.prepare('UPDATE verticals SET installs_blocked = ? WHERE slug = ?')
|
|
6979
|
-
.run(blocked ? 1 : 0, slug);
|
|
6980
|
-
this.recordAdmin(actor, 'setVerticalInstallsBlocked', { tenantId: null }, { installsBlocked: existing.installsBlocked }, { installsBlocked: blocked });
|
|
7207
|
+
.run(blocked ? 1 : 0, slug).changes > 0, () => this.recordAdmin(actor, 'setVerticalInstallsBlocked', { tenantId: null }, { installsBlocked: existing.installsBlocked }, { installsBlocked: blocked }));
|
|
6981
7208
|
},
|
|
6982
7209
|
setVerticalTenantProvisioner: async (actor, slug, granted) => {
|
|
6983
7210
|
const existing = readVertical(slug);
|
|
6984
7211
|
if (!existing)
|
|
6985
7212
|
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
6986
|
-
this.directory
|
|
7213
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
6987
7214
|
.prepare('UPDATE verticals SET tenant_provisioner = ? WHERE slug = ?')
|
|
6988
|
-
.run(granted ? 1 : 0, slug);
|
|
6989
|
-
this.recordAdmin(actor, 'setVerticalTenantProvisioner', { tenantId: null }, { tenantProvisioner: existing.tenantProvisioner }, { tenantProvisioner: granted });
|
|
7215
|
+
.run(granted ? 1 : 0, slug).changes > 0, () => this.recordAdmin(actor, 'setVerticalTenantProvisioner', { tenantId: null }, { tenantProvisioner: existing.tenantProvisioner }, { tenantProvisioner: granted }));
|
|
6990
7216
|
},
|
|
6991
7217
|
setVerticalEmailSender: async (actor, slug, granted) => {
|
|
6992
7218
|
const existing = readVertical(slug);
|
|
6993
7219
|
if (!existing)
|
|
6994
7220
|
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
6995
|
-
this.directory
|
|
7221
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
6996
7222
|
.prepare('UPDATE verticals SET email_sender = ? WHERE slug = ?')
|
|
6997
|
-
.run(granted ? 1 : 0, slug);
|
|
6998
|
-
this.recordAdmin(actor, 'setVerticalEmailSender', { tenantId: null }, { emailSender: existing.emailSender }, { emailSender: granted });
|
|
7223
|
+
.run(granted ? 1 : 0, slug).changes > 0, () => this.recordAdmin(actor, 'setVerticalEmailSender', { tenantId: null }, { emailSender: existing.emailSender }, { emailSender: granted }));
|
|
6999
7224
|
},
|
|
7000
7225
|
deleteVertical: async (actor, slug) => {
|
|
7001
7226
|
const existing = readVertical(slug);
|
|
@@ -7020,16 +7245,27 @@ export class SqliteScopeHost {
|
|
|
7020
7245
|
}
|
|
7021
7246
|
// Deployed dispatch scripts are NOT reaped here — they become orphans for the
|
|
7022
7247
|
// cleanup script (#248), never destroyed alongside a registry row.
|
|
7023
|
-
|
|
7024
|
-
this.
|
|
7025
|
-
|
|
7026
|
-
.prepare(
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7248
|
+
// #2501: one transaction, so the row and its ownership period close together.
|
|
7249
|
+
this.directoryWriteAndAudit(() => {
|
|
7250
|
+
this.directory.prepare('DELETE FROM vertical_channels WHERE vertical_slug = ?').run(slug);
|
|
7251
|
+
this.directory.prepare('DELETE FROM vertical_channel_history WHERE vertical_slug = ?').run(slug);
|
|
7252
|
+
this.directory
|
|
7253
|
+
.prepare(`DELETE FROM vertical_version_migrations
|
|
7254
|
+
WHERE version_id IN (SELECT id FROM vertical_versions WHERE vertical_slug = ?)`)
|
|
7255
|
+
.run(slug);
|
|
7256
|
+
forgetVerticalScopeStores(switchSqlOf(this.directory), slug);
|
|
7257
|
+
forgetVerticalWireAuth(switchSqlOf(this.directory), slug);
|
|
7258
|
+
this.directory.prepare('DELETE FROM vertical_versions WHERE vertical_slug = ?').run(slug);
|
|
7259
|
+
const changed = this.directory.prepare('DELETE FROM verticals WHERE slug = ?').run(slug).changes > 0;
|
|
7260
|
+
if (changed)
|
|
7261
|
+
closeVerticalOwnerRow(redactionSqlOf(this.directory), slug, verticalOwnerNow());
|
|
7262
|
+
return changed;
|
|
7263
|
+
}, () => this.recordAdmin(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.ownerTenant }, null));
|
|
7264
|
+
},
|
|
7265
|
+
listVerticalOwners: async (actor, slug) => {
|
|
7266
|
+
const periods = listVerticalOwnerRows(redactionSqlOf(this.directory), slug);
|
|
7267
|
+
this.recordAccess(actor, 'listVerticalOwners', { tenantId: null }, { slug }, periods.length);
|
|
7268
|
+
return periods;
|
|
7033
7269
|
},
|
|
7034
7270
|
admitVersion: async (actor, versionId) => {
|
|
7035
7271
|
const v = await readVersion(versionId);
|
|
@@ -7040,24 +7276,22 @@ export class SqliteScopeHost {
|
|
|
7040
7276
|
// manual vouch by clearing the note (what the publish seam requires).
|
|
7041
7277
|
if (v.admissionNote !== AUTO_ADMISSION_NOTE)
|
|
7042
7278
|
return;
|
|
7043
|
-
this.directory
|
|
7279
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7044
7280
|
.prepare('UPDATE vertical_versions SET admission_note = NULL WHERE id = ?')
|
|
7045
|
-
.run(versionId)
|
|
7046
|
-
this.recordAdmin(actor, 'admitVersion', { tenantId: null }, { admission: v.admission, note: v.admissionNote }, {
|
|
7281
|
+
.run(versionId).changes > 0, () => this.recordAdmin(actor, 'admitVersion', { tenantId: null }, { admission: v.admission, note: v.admissionNote }, {
|
|
7047
7282
|
admission: 'admitted',
|
|
7048
7283
|
note: null,
|
|
7049
|
-
});
|
|
7284
|
+
}));
|
|
7050
7285
|
return;
|
|
7051
7286
|
}
|
|
7052
7287
|
if (v.admission === 'rejected') {
|
|
7053
7288
|
throw substratError('conflict', `version ${versionId} was rejected — publish a new one`);
|
|
7054
7289
|
}
|
|
7055
|
-
this.directory
|
|
7290
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7056
7291
|
.prepare("UPDATE vertical_versions SET admission = 'admitted' WHERE id = ?")
|
|
7057
|
-
.run(versionId)
|
|
7058
|
-
this.recordAdmin(actor, 'admitVersion', { tenantId: null }, { admission: v.admission }, {
|
|
7292
|
+
.run(versionId).changes > 0, () => this.recordAdmin(actor, 'admitVersion', { tenantId: null }, { admission: v.admission }, {
|
|
7059
7293
|
admission: 'admitted',
|
|
7060
|
-
});
|
|
7294
|
+
}));
|
|
7061
7295
|
},
|
|
7062
7296
|
rejectVersion: async (actor, versionId, note) => {
|
|
7063
7297
|
const v = await readVersion(versionId);
|
|
@@ -7068,13 +7302,12 @@ export class SqliteScopeHost {
|
|
|
7068
7302
|
}
|
|
7069
7303
|
if (v.admission === 'rejected')
|
|
7070
7304
|
return; // idempotent
|
|
7071
|
-
this.directory
|
|
7305
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7072
7306
|
.prepare("UPDATE vertical_versions SET admission = 'rejected', admission_note = ? WHERE id = ?")
|
|
7073
|
-
.run(note, versionId)
|
|
7074
|
-
this.recordAdmin(actor, 'rejectVersion', { tenantId: null }, { admission: v.admission }, {
|
|
7307
|
+
.run(note, versionId).changes > 0, () => this.recordAdmin(actor, 'rejectVersion', { tenantId: null }, { admission: v.admission }, {
|
|
7075
7308
|
admission: 'rejected',
|
|
7076
7309
|
note,
|
|
7077
|
-
});
|
|
7310
|
+
}));
|
|
7078
7311
|
},
|
|
7079
7312
|
promotionImpact: async (actor, verticalSlug, channel, versionId) => {
|
|
7080
7313
|
const incoming = await readVersion(versionId);
|
|
@@ -7158,8 +7391,9 @@ export class SqliteScopeHost {
|
|
|
7158
7391
|
// #2333: a compare-and-set. Every gate above — the review binding, the digest checkpoints,
|
|
7159
7392
|
// the export breaks — was judged against `outgoing`, across awaits another promote can run
|
|
7160
7393
|
// in. So the pointer moves only if it still names `outgoing`, read and written in one
|
|
7161
|
-
// transaction; the promote that lost the race is refused as stale and writes nothing.
|
|
7162
|
-
|
|
7394
|
+
// transaction; the promote that lost the race is refused as stale and writes nothing. Its admin
|
|
7395
|
+
// row commits with the move, ahead of the rebinds below, which record their own.
|
|
7396
|
+
const moved = this.directoryWriteAndAudit(() => {
|
|
7163
7397
|
const now = this.directory
|
|
7164
7398
|
.prepare('SELECT version_id FROM vertical_channels WHERE vertical_slug = ? AND channel = ?')
|
|
7165
7399
|
.get(verticalSlug, channel);
|
|
@@ -7178,7 +7412,10 @@ export class SqliteScopeHost {
|
|
|
7178
7412
|
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
7179
7413
|
.run(ulid(), verticalSlug, channel, versionId, outgoing?.id ?? null, actor, promotedAt);
|
|
7180
7414
|
return true;
|
|
7181
|
-
}
|
|
7415
|
+
},
|
|
7416
|
+
// The acknowledgement is recorded, not just enforced: that is what turns
|
|
7417
|
+
// "someone reviewed the permission change" into evidence.
|
|
7418
|
+
() => this.recordAdmin(actor, 'promoteVersion', { tenantId: null, vertical: verticalSlug }, outgoing ? { versionId: outgoing.id, version: outgoing.version } : null, { channel, versionId, version: incoming.version, acknowledged: ack }));
|
|
7182
7419
|
if (!moved)
|
|
7183
7420
|
throw promoteRaceRefusal(channel);
|
|
7184
7421
|
// For a PRIVATE vertical, prod IS what the owner's apps run: re-point the
|
|
@@ -7208,9 +7445,6 @@ export class SqliteScopeHost {
|
|
|
7208
7445
|
.prepare('UPDATE scopes SET vertical_version_id = ?, vertical = ? WHERE scope_id = ?')
|
|
7209
7446
|
.run(versionId, verticalSlug, s.scope_id).changes > 0, () => this.recordAdmin(actor, 'bindScopeVersion', { tenantId: s.tenant_id, scopeId: s.scope_id }, prev ? { versionId: prev.id, version: prev.version } : null, { versionId, vertical: verticalSlug, version: incoming.version, via: 'promoteVersion' }));
|
|
7210
7447
|
}
|
|
7211
|
-
// The acknowledgement is recorded, not just enforced: that is what turns
|
|
7212
|
-
// "someone reviewed the permission change" into evidence.
|
|
7213
|
-
this.recordAdmin(actor, 'promoteVersion', { tenantId: null, vertical: verticalSlug }, outgoing ? { versionId: outgoing.id, version: outgoing.version } : null, { channel, versionId, version: incoming.version, acknowledged: ack });
|
|
7214
7448
|
},
|
|
7215
7449
|
listChannels: async (actor, verticalSlug, page) => {
|
|
7216
7450
|
// `prod` is the only live channel (#509 retired dev/staging). Filtering here keeps a
|
|
@@ -7482,11 +7716,10 @@ export class SqliteScopeHost {
|
|
|
7482
7716
|
throw substratError('not_found', `unknown vertical '${verticalSlug}'`);
|
|
7483
7717
|
// #2178: every install on the serving script runs the new version from this moment.
|
|
7484
7718
|
this.assertNoStrandFrom([r.serving_version_id], parsed.versionId, r.serving_ref ? this.provisionedVersionsOf(verticalSlug, r.serving_version_id ? { ref: r.serving_ref, versionId: r.serving_version_id } : null, null) : []);
|
|
7485
|
-
this.directory
|
|
7719
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7486
7720
|
.prepare(`UPDATE verticals SET serving_ref = ?, serving_version_id = ?,
|
|
7487
7721
|
serving_do_classes = ?, serving_migration_tag = ? WHERE slug = ?`)
|
|
7488
|
-
.run(parsed.ref, parsed.versionId, JSON.stringify(parsed.doClasses), parsed.migrationTag, verticalSlug);
|
|
7489
|
-
this.recordAdmin(actor, 'setVerticalServing', { tenantId: null }, r.serving_ref ? { ref: r.serving_ref, versionId: r.serving_version_id } : null, { vertical: verticalSlug, ref: parsed.ref, versionId: parsed.versionId });
|
|
7722
|
+
.run(parsed.ref, parsed.versionId, JSON.stringify(parsed.doClasses), parsed.migrationTag, verticalSlug).changes > 0, () => this.recordAdmin(actor, 'setVerticalServing', { tenantId: null }, r.serving_ref ? { ref: r.serving_ref, versionId: r.serving_version_id } : null, { vertical: verticalSlug, ref: parsed.ref, versionId: parsed.versionId }));
|
|
7490
7723
|
},
|
|
7491
7724
|
versionManifest: async (actor, verticalSlug, versionId) => {
|
|
7492
7725
|
const v = this.directory
|
|
@@ -7506,6 +7739,20 @@ export class SqliteScopeHost {
|
|
|
7506
7739
|
this.recordAccess(actor, 'versionMigrations', {}, { verticalSlug, versionId }, v.migrations?.length ?? 0);
|
|
7507
7740
|
return v.migrations;
|
|
7508
7741
|
},
|
|
7742
|
+
recordWireAuth: async (actor, input) => {
|
|
7743
|
+
const row = deploymentWireAuth.parse(input);
|
|
7744
|
+
this.directoryWriteAndAudit(() => {
|
|
7745
|
+
writeDeploymentWireAuth(switchSqlOf(this.directory), row);
|
|
7746
|
+
return true;
|
|
7747
|
+
}, () => this.recordAdmin(actor, 'recordWireAuth', { tenantId: null, vertical: row.verticalSlug }, null, { vertical: row.verticalSlug, deploymentRef: row.deploymentRef, versionId: row.versionId, record: row.record }));
|
|
7748
|
+
},
|
|
7749
|
+
listWireAuth: async (actor, input) => {
|
|
7750
|
+
// The judged copy, not the caller's object: only its own fields, the reach first, reach the log.
|
|
7751
|
+
const filter = assertWireAuthFilter(input);
|
|
7752
|
+
const rows = deploymentWireAuthOf(switchSqlOf(this.directory), filter);
|
|
7753
|
+
this.recordAccess(actor, 'listWireAuth', wireAuthAccessTarget(filter), filter, rows.length);
|
|
7754
|
+
return rows;
|
|
7755
|
+
},
|
|
7509
7756
|
setScopeServingRef: async (actor, tenantId, scopeId, servingRef, opts) => {
|
|
7510
7757
|
const scope = this.directory.prepare('SELECT * FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
7511
7758
|
if (!scope || scope.tenant_id !== tenantId) {
|
|
@@ -7796,11 +8043,12 @@ export class SqliteScopeHost {
|
|
|
7796
8043
|
this.recordAccess(actor, 'operationSeries', { tenantId, scopeId }, delegatedReadParams.operationSeries(input), operationSeriesCount(series));
|
|
7797
8044
|
return series;
|
|
7798
8045
|
},
|
|
8046
|
+
// Every row, newest first (#2553); `rowid DESC` orders migrations stamped in one millisecond.
|
|
7799
8047
|
scopeAppliedMigrations: async (actor, tenantId, scopeId) => {
|
|
7800
8048
|
const db = this.scopeReadDbFor(tenantId, scopeId);
|
|
7801
8049
|
const rows = db
|
|
7802
8050
|
.prepare(`SELECT module_id, version, applied_at, duration_ms, rows_changed FROM _substrat_migrations
|
|
7803
|
-
ORDER BY applied_at DESC,
|
|
8051
|
+
ORDER BY applied_at DESC, rowid DESC`)
|
|
7804
8052
|
.all();
|
|
7805
8053
|
this.recordAccess(actor, 'scopeAppliedMigrations', { tenantId, scopeId }, null, rows.length);
|
|
7806
8054
|
return rows.map((r) => ({
|
|
@@ -7826,10 +8074,7 @@ export class SqliteScopeHost {
|
|
|
7826
8074
|
scopeDatabaseSize: async (actor, tenantId, scopeId) => {
|
|
7827
8075
|
// #1524: the SQLite twin of a DO's `databaseSize`. It counts allocated pages,
|
|
7828
8076
|
// free ones included, which is what the file costs on disk, not the live row bytes.
|
|
7829
|
-
const
|
|
7830
|
-
const pages = db.pragma('page_count', { simple: true });
|
|
7831
|
-
const pageSize = db.pragma('page_size', { simple: true });
|
|
7832
|
-
const bytes = pages * pageSize;
|
|
8077
|
+
const bytes = databaseBytesOf(this.scopeReadDbFor(tenantId, scopeId));
|
|
7833
8078
|
this.recordAccess(actor, 'scopeDatabaseSize', { tenantId, scopeId }, null, 1);
|
|
7834
8079
|
return bytes;
|
|
7835
8080
|
},
|
|
@@ -7838,6 +8083,11 @@ export class SqliteScopeHost {
|
|
|
7838
8083
|
this.recordAccess(actor, 'scopeStoreSizes', { tenantId, scopeId }, null, sizes.length);
|
|
7839
8084
|
return sizes;
|
|
7840
8085
|
},
|
|
8086
|
+
scopeAttachmentBytes: async (actor, tenantId, scopeId) => {
|
|
8087
|
+
const bytes = attachmentBytesOf(this.scopeReadDbFor(tenantId, scopeId));
|
|
8088
|
+
this.recordAccess(actor, 'scopeAttachmentBytes', { tenantId, scopeId }, null, 1);
|
|
8089
|
+
return bytes;
|
|
8090
|
+
},
|
|
7841
8091
|
rewindScope: async () => {
|
|
7842
8092
|
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');
|
|
7843
8093
|
},
|
|
@@ -7963,11 +8213,10 @@ export class SqliteScopeHost {
|
|
|
7963
8213
|
if (slugOwner) {
|
|
7964
8214
|
throw substratError('conflict', `tenant slug '${parsed.slug}' already taken by ${slugOwner.tenant_id} (slugs are unique)`);
|
|
7965
8215
|
}
|
|
7966
|
-
this.directory
|
|
8216
|
+
this.directoryWriteAndAudit(() => this.directory
|
|
7967
8217
|
.prepare(`INSERT INTO tenants (tenant_id, slug, name, status, created_at, provisioned_by_tenant)
|
|
7968
8218
|
VALUES (?, ?, ?, 'active', ?, ?)`)
|
|
7969
|
-
.run(parsed.id, parsed.slug, parsed.name, new Date().toISOString(), parsed.provisionedByTenant ?? null);
|
|
7970
|
-
this.recordAdmin(actor, 'createTenant', { tenantId: parsed.id }, null, readTenant(parsed.id));
|
|
8219
|
+
.run(parsed.id, parsed.slug, parsed.name, new Date().toISOString(), parsed.provisionedByTenant ?? null).changes > 0, () => this.recordAdmin(actor, 'createTenant', { tenantId: parsed.id }, null, readTenant(parsed.id)));
|
|
7971
8220
|
},
|
|
7972
8221
|
setTenantStatus: async (actor, tenantId, status) => {
|
|
7973
8222
|
const before = readTenant(tenantId);
|
|
@@ -7982,17 +8231,13 @@ export class SqliteScopeHost {
|
|
|
7982
8231
|
// Stamp/clear deleting_at so the grace-window sweep can age tenants (§4.8):
|
|
7983
8232
|
// entering `deleting` stamps, leaving it (an un-delete) clears — exactly the
|
|
7984
8233
|
// shape suspendScope/archiveScope use for archived_at.
|
|
7985
|
-
|
|
7986
|
-
this.directory
|
|
8234
|
+
this.directoryWriteAndAudit(() => (status === 'deleting'
|
|
8235
|
+
? this.directory
|
|
7987
8236
|
.prepare('UPDATE tenants SET status = ?, deleting_at = ? WHERE tenant_id = ?')
|
|
7988
|
-
.run(status, new Date().toISOString(), tenantId)
|
|
7989
|
-
|
|
7990
|
-
else {
|
|
7991
|
-
this.directory
|
|
8237
|
+
.run(status, new Date().toISOString(), tenantId)
|
|
8238
|
+
: this.directory
|
|
7992
8239
|
.prepare('UPDATE tenants SET status = ?, deleting_at = NULL WHERE tenant_id = ?')
|
|
7993
|
-
.run(status, tenantId);
|
|
7994
|
-
}
|
|
7995
|
-
this.recordAdmin(actor, 'setTenantStatus', { tenantId }, { status: before.status }, { status });
|
|
8240
|
+
.run(status, tenantId)).changes > 0, () => this.recordAdmin(actor, 'setTenantStatus', { tenantId }, { status: before.status }, { status }));
|
|
7996
8241
|
},
|
|
7997
8242
|
setTenantName: async (actor, tenantId, name) => {
|
|
7998
8243
|
const before = readTenant(tenantId);
|
|
@@ -8000,8 +8245,7 @@ export class SqliteScopeHost {
|
|
|
8000
8245
|
throw substratError('not_found', `unknown tenant: ${tenantId}`);
|
|
8001
8246
|
if (before.name === name)
|
|
8002
8247
|
return; // no-op is not audited — nothing changed
|
|
8003
|
-
this.directory.prepare('UPDATE tenants SET name = ? WHERE tenant_id = ?').run(name, tenantId);
|
|
8004
|
-
this.recordAdmin(actor, 'setTenantName', { tenantId }, { name: before.name }, { name });
|
|
8248
|
+
this.directoryWriteAndAudit(() => this.directory.prepare('UPDATE tenants SET name = ? WHERE tenant_id = ?').run(name, tenantId).changes > 0, () => this.recordAdmin(actor, 'setTenantName', { tenantId }, { name: before.name }, { name }));
|
|
8005
8249
|
},
|
|
8006
8250
|
listTenants: async (actor, page) => {
|
|
8007
8251
|
const where = [];
|
|
@@ -8019,32 +8263,15 @@ export class SqliteScopeHost {
|
|
|
8019
8263
|
return t;
|
|
8020
8264
|
},
|
|
8021
8265
|
listScopes: async (actor, filter) => {
|
|
8022
|
-
const
|
|
8023
|
-
const params = [];
|
|
8024
|
-
if (filter?.tenantId) {
|
|
8025
|
-
where.push('tenant_id = ?');
|
|
8026
|
-
params.push(filter.tenantId);
|
|
8027
|
-
}
|
|
8028
|
-
if (filter?.status) {
|
|
8029
|
-
const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
|
|
8030
|
-
// An empty array means "no status is acceptable" — match nothing, rather
|
|
8031
|
-
// than degenerating into an unfiltered read of the whole fleet.
|
|
8032
|
-
if (statuses.length === 0)
|
|
8033
|
-
return [];
|
|
8034
|
-
// One JSON array, the shape the DO twin needs for its 100-parameter limit (#1776).
|
|
8035
|
-
where.push('status IN (SELECT value FROM json_each(?))');
|
|
8036
|
-
params.push(JSON.stringify(statuses));
|
|
8037
|
-
}
|
|
8038
|
-
if (filter?.vertical) {
|
|
8039
|
-
where.push('vertical = ?');
|
|
8040
|
-
params.push(filter.vertical);
|
|
8041
|
-
}
|
|
8042
|
-
const tail = keysetTail(where, params, 'scope_id', filter);
|
|
8043
|
-
const sql = 'SELECT * FROM scopes' + (where.length ? ` WHERE ${where.join(' AND ')}` : '') + tail;
|
|
8044
|
-
const scopes = this.directory.prepare(sql).all(...params).map(mapScope);
|
|
8266
|
+
const scopes = scopeRows(filter).map(mapScope);
|
|
8045
8267
|
this.recordAccess(actor, 'listScopes', { tenantId: filter?.tenantId ?? null }, filter, scopes.length);
|
|
8046
8268
|
return scopes;
|
|
8047
8269
|
},
|
|
8270
|
+
listScopesReadable: async (actor, filter) => {
|
|
8271
|
+
const rows = scopeRows(filter);
|
|
8272
|
+
this.recordAccess(actor, 'listScopesReadable', { tenantId: filter?.tenantId ?? null }, filter, rows.length);
|
|
8273
|
+
return readableScopes(rows, mapScope);
|
|
8274
|
+
},
|
|
8048
8275
|
listTenantStores: async (actor, filter) => {
|
|
8049
8276
|
const where = [];
|
|
8050
8277
|
const params = [];
|
|
@@ -8687,23 +8914,22 @@ export class SqliteScopeHost {
|
|
|
8687
8914
|
'_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
|
|
8688
8915
|
'_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
|
|
8689
8916
|
'_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
|
|
8917
|
+
'_substrat_scope_attachment_storage', // #2501: attachment samples, gone with the tenant
|
|
8690
8918
|
];
|
|
8691
|
-
|
|
8919
|
+
this.directoryWriteAndAudit(() => {
|
|
8692
8920
|
for (const table of tables) {
|
|
8693
8921
|
this.directory.prepare(`DELETE FROM ${table} WHERE tenant_id = ?`).run(tenantId);
|
|
8694
8922
|
}
|
|
8695
|
-
this.directory
|
|
8923
|
+
return this.directory
|
|
8696
8924
|
.prepare(`UPDATE tenants SET status = 'reaped', deleting_at = NULL WHERE tenant_id = ?`)
|
|
8697
|
-
.run(tenantId);
|
|
8698
|
-
});
|
|
8699
|
-
clear();
|
|
8925
|
+
.run(tenantId).changes > 0;
|
|
8926
|
+
}, () => this.recordAdmin(actor, 'reapTenant', { tenantId }, { status: before.status }, { status: 'reaped' }));
|
|
8700
8927
|
// Evict the in-memory role cache for this tenant (the one directory table
|
|
8701
8928
|
// mirrored in memory) so a reaped role never resolves after the DELETE.
|
|
8702
8929
|
for (const cacheKey of [...this.roles.keys()]) {
|
|
8703
8930
|
if (cacheKey.startsWith(`${tenantId}/`))
|
|
8704
8931
|
this.roles.delete(cacheKey);
|
|
8705
8932
|
}
|
|
8706
|
-
this.recordAdmin(actor, 'reapTenant', { tenantId }, { status: before.status }, { status: 'reaped' });
|
|
8707
8933
|
},
|
|
8708
8934
|
grantEntitlement: async (actor, tenantId, entitlementKey, plan) => {
|
|
8709
8935
|
const input = entitlementGrantInput.parse(plan ?? {});
|
|
@@ -8796,6 +9022,7 @@ export class SqliteScopeHost {
|
|
|
8796
9022
|
storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
|
|
8797
9023
|
storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
|
|
8798
9024
|
storeStorage: listScopeStoreStorageRows(redactionSqlOf(this.directory), only),
|
|
9025
|
+
attachmentStorage: listScopeAttachmentStorageRows(redactionSqlOf(this.directory), only),
|
|
8799
9026
|
});
|
|
8800
9027
|
// The count that matters for K-24 is how many TENANTS this reading covered —
|
|
8801
9028
|
// "read the meter for one tenant" and "metered the whole fleet" are different
|
|
@@ -9449,8 +9676,9 @@ export class SqliteScopeHost {
|
|
|
9449
9676
|
params.push(filter.fingerprint);
|
|
9450
9677
|
}
|
|
9451
9678
|
if (filter?.reference) {
|
|
9452
|
-
|
|
9453
|
-
|
|
9679
|
+
const byReference = opsFailureReferenceWhere(filter.reference, filter.referenceInText === true);
|
|
9680
|
+
where.push(byReference.sql);
|
|
9681
|
+
params.push(...byReference.params);
|
|
9454
9682
|
}
|
|
9455
9683
|
if (filter?.since) {
|
|
9456
9684
|
where.push('at >= ?');
|
|
@@ -9683,6 +9911,33 @@ export class SqliteScopeHost {
|
|
|
9683
9911
|
return rows;
|
|
9684
9912
|
},
|
|
9685
9913
|
pruneScopeStorage: async (_actor, limit) => pruneScopeStorageRows(redactionSqlOf(this.directory), Date.now(), limit),
|
|
9914
|
+
recordRequestDay: async (_actor, input) => ({
|
|
9915
|
+
recorded: this.directory.transaction(() => recordRequestDayRows(redactionSqlOf(this.directory), input))(),
|
|
9916
|
+
}),
|
|
9917
|
+
listRequestDays: async (actor, filter) => {
|
|
9918
|
+
const rows = listRequestDayRows(redactionSqlOf(this.directory), filter);
|
|
9919
|
+
this.recordAccess(actor, 'listRequestDays', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter ?? null, rows.length);
|
|
9920
|
+
return rows;
|
|
9921
|
+
},
|
|
9922
|
+
listRequestRollupDays: async (actor, filter) => {
|
|
9923
|
+
const rows = listRequestRollupDayRows(redactionSqlOf(this.directory), filter);
|
|
9924
|
+
this.recordAccess(actor, 'listRequestRollupDays', { tenantId: null }, filter ?? null, rows.length);
|
|
9925
|
+
return rows;
|
|
9926
|
+
},
|
|
9927
|
+
pruneRequestDays: async (_actor, limit, retentionMonths) => this.directory.transaction(() => pruneRequestDayRows(redactionSqlOf(this.directory), Date.now(), limit, retentionMonths))(),
|
|
9928
|
+
// #2501: the snapshot reads and writes in one transaction, so its rows are one instant's reading.
|
|
9929
|
+
recordMeterDay: async (_actor, input) => this.directory.transaction(() => recordMeterDayRows(redactionSqlOf(this.directory), input))(),
|
|
9930
|
+
listMeterDays: async (actor, filter) => {
|
|
9931
|
+
const rows = listMeterDayRows(redactionSqlOf(this.directory), filter);
|
|
9932
|
+
this.recordAccess(actor, 'listMeterDays', { tenantId: filter?.tenantId ?? null }, filter ?? null, rows.length);
|
|
9933
|
+
return rows;
|
|
9934
|
+
},
|
|
9935
|
+
listMeterDayTakes: async (actor, filter) => {
|
|
9936
|
+
const rows = listMeterDayTakeRows(redactionSqlOf(this.directory), filter);
|
|
9937
|
+
this.recordAccess(actor, 'listMeterDayTakes', { tenantId: null }, filter ?? null, rows.length);
|
|
9938
|
+
return rows;
|
|
9939
|
+
},
|
|
9940
|
+
pruneMeterDays: async (_actor, limit, retentionMonths) => this.directory.transaction(() => pruneMeterDayRows(redactionSqlOf(this.directory), Date.now(), limit, retentionMonths))(),
|
|
9686
9941
|
listModelUsage: async (actor, filter) => {
|
|
9687
9942
|
const rows = this.selectModelUsage(filter);
|
|
9688
9943
|
this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
|
|
@@ -10034,6 +10289,8 @@ export class SqliteScopeHost {
|
|
|
10034
10289
|
}
|
|
10035
10290
|
for (const stmt of LEGACY_SCOPE_ROWS_BACKFILL)
|
|
10036
10291
|
this.directory.exec(stmt);
|
|
10292
|
+
// #2501: an ownership period for every vertical registered before the history existed.
|
|
10293
|
+
this.directory.exec(VERTICAL_OWNERS_BACKFILL);
|
|
10037
10294
|
// Created after the backfill: a UNIQUE index over NULL slugs would permit the
|
|
10038
10295
|
// duplicates it exists to forbid (SQLite treats NULLs as distinct).
|
|
10039
10296
|
//
|
|
@@ -10364,9 +10621,7 @@ export class SqliteScopeHost {
|
|
|
10364
10621
|
const pending = rt.db
|
|
10365
10622
|
.prepare(`SELECT COUNT(*) AS c FROM _substrat_platform_requests WHERE status = 'pending'`)
|
|
10366
10623
|
.get().c;
|
|
10367
|
-
|
|
10368
|
-
throw new Error(`too many pending platform requests (${pending}); retry once some have drained`);
|
|
10369
|
-
}
|
|
10624
|
+
assertPlatformRequestRoom(pending);
|
|
10370
10625
|
const id = platformRequestId.parse(ulid());
|
|
10371
10626
|
const requestedBy = overrideActor ?? actorOf(subject);
|
|
10372
10627
|
rt.db
|