@substrat-run/adapter-sqlite 0.164.0 → 0.166.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readdirSync, rea
3
3
  import { dirname, join } from 'node:path';
4
4
  import Database from 'better-sqlite3';
5
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, connectorsOfManifestJson, 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, openAttachmentByteStream, 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, dumpRoleRefusal, 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, servingRaceRefusal, autoAdmittedProdRefusal, listedPromoteRefusal, rebindRaceRefusal, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, migratesFrom, servedFromRow, servesAsFrom, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, WIRE_NONCE_DDL, WIRE_NONCE_FULL, claimWireNonceIn, isWireNonceTable, 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
+ 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, openAttachmentByteStream, 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, readBoundedTenantStorageGauge, REQUEST_ROLLUP_DDL, listRequestDayRows, listRequestRollupDayRows, pruneRequestDayRows, recordRequestDayRows, readTenantRequestMeterRows, readRequestMeterDayRows, 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, dumpRoleRefusal, 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, auditedScopePermissionWrite, 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, approveStoreDriverReleaseIn, legacyStoreDriverConflict, legacyStoreDriverRenames, beginStoreDriverRolloutIn, finishStoreDriverRolloutIn, holdStoreDriverIn, advanceStoreDriverRebuildIn, claimStoreDriverRebuildIn, storeDriverRebuildIn, recordStoreDriverReleaseIn, recordStoreDriverRevokeWalkIn, releaseStoreDriverHoldIn, revokeStoreDriverReleaseIn, liveStoreDriverApprovalIn, storeDriverApprovalsIn, storeDriverRevokeWalkIn, 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, servingRaceRefusal, autoAdmittedProdRefusal, listedPromoteRefusal, rebindRaceRefusal, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, migratesFrom, servedFromRow, servesAsFrom, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, WIRE_NONCE_DDL, WIRE_NONCE_FULL, claimWireNonceIn, isWireNonceTable, 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';
7
7
  import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
8
+ import { canonicalInstant } from '@substrat-run/kernel';
8
9
  import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
9
10
  import { INERT_SCOPE_REASON, isPrimaryScopeRow, normalizeUploadMediaType } from '@substrat-run/kernel';
10
11
  import { opsFailureReferenceWhere } from '@substrat-run/kernel';
@@ -1630,6 +1631,7 @@ export class SqliteScopeHost {
1630
1631
  elapsed_ms INTEGER NOT NULL
1631
1632
  );
1632
1633
  CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant ON _substrat_model_usage (tenant_id, at);
1634
+ CREATE INDEX IF NOT EXISTS _substrat_model_usage_tenant_page ON _substrat_model_usage (tenant_id, at, id);
1633
1635
  CREATE INDEX IF NOT EXISTS _substrat_model_usage_at ON _substrat_model_usage (at);
1634
1636
  -- #1524: the stored storage gauge — one row per (scope, UTC day), kernel-owned DDL.
1635
1637
  ${SCOPE_STORAGE_DDL}
@@ -2234,11 +2236,10 @@ export class SqliteScopeHost {
2234
2236
  // Physically mint the database now, so a successful mint means the file exists (matching
2235
2237
  // "the platform mints the store"); the vertical then runs its OWN migrations against it.
2236
2238
  this.tenantStoreDb(ref);
2237
- this.directory
2239
+ this.directoryWriteAndAudit(() => this.directory
2238
2240
  .prepare(`INSERT INTO tenant_stores (tenant_id, vertical, binding, kind, ref, created_at)
2239
2241
  VALUES (?, ?, ?, 'relational', ?, ?)`)
2240
- .run(input.tenantId, input.vertical, input.binding, ref, new Date().toISOString());
2241
- this.recordAdmin(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref });
2242
+ .run(input.tenantId, input.vertical, input.binding, ref, new Date().toISOString()).changes > 0, () => this.recordAdmin(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref }));
2242
2243
  return { binding: input.binding, kind: 'relational', ref };
2243
2244
  }
2244
2245
  openTenantStore(handle) {
@@ -2295,11 +2296,10 @@ export class SqliteScopeHost {
2295
2296
  const safeVertical = input.vertical.replace(/[^A-Za-z0-9_-]+/g, '-');
2296
2297
  const ref = `blob__${input.tenantId}__${safeVertical}__${input.binding}__${ulid()}`;
2297
2298
  mkdirSync(join(this.dir, ref), { recursive: true });
2298
- this.directory
2299
+ this.directoryWriteAndAudit(() => this.directory
2299
2300
  .prepare(`INSERT INTO blob_stores (tenant_id, vertical, binding, kind, ref, created_at)
2300
2301
  VALUES (?, ?, ?, 'blob', ?, ?)`)
2301
- .run(input.tenantId, input.vertical, input.binding, ref, new Date().toISOString());
2302
- this.recordAdmin(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref });
2302
+ .run(input.tenantId, input.vertical, input.binding, ref, new Date().toISOString()).changes > 0, () => this.recordAdmin(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref }));
2303
2303
  return { binding: input.binding, kind: 'blob', ref };
2304
2304
  }
2305
2305
  /** A `TenantBlobStore` over the per-tenant directory a `ref` names (#473). Keys are
@@ -6549,15 +6549,19 @@ export class SqliteScopeHost {
6549
6549
  /**
6550
6550
  * `onNode`, with the write's admin row (#2526). At the tenant node the write is a directory
6551
6551
  * row, so it and its row are one directory transaction. At a scope node it is in the scope's
6552
- * own store, which no directory transaction reaches, so the row follows it (#1292's window).
6552
+ * own store, which no directory transaction reaches, so intent precedes the scope write.
6553
6553
  */
6554
- const auditedOnNode = async (node, write, audit) => {
6554
+ const auditedOnNode = async (node, write, audit, scopeAudit) => {
6555
6555
  if (!node.scopeId) {
6556
6556
  this.directoryWriteAndAudit(() => { write(); return true; }, audit);
6557
6557
  return;
6558
6558
  }
6559
- await onNode(node, write);
6560
- audit();
6559
+ await auditedScopePermissionWrite({
6560
+ ...scopeAudit,
6561
+ record: (after) => this.recordAdmin(scopeAudit.actor, scopeAudit.action, node, null, after),
6562
+ write: () => onNode(node, write),
6563
+ logError: (message, fields) => console.error(message, fields),
6564
+ });
6561
6565
  };
6562
6566
  /**
6563
6567
  * One kill switch moved on one scope — the ONE body `revokeFromSystem` / `restoreToSystem`
@@ -6889,12 +6893,12 @@ export class SqliteScopeHost {
6889
6893
  writeScopeTuple(assignment.node, subject, `role:${assignment.roleKey}`, `scope:${scopeId}`);
6890
6894
  else
6891
6895
  writeTenantTuple(tenantId, subject, `role:${assignment.roleKey}`, `tenant:${tenantId}`);
6892
- }, () => this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment));
6896
+ }, () => this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment), { actor, action: 'assignRole', request: { ...assignment } });
6893
6897
  },
6894
6898
  unassignRole: async (actor, assignment) => {
6895
6899
  // Tombstone (K-21), never DELETE — the checker skips revoked_at rows, so the
6896
6900
  // assignment stops resolving while staying visible to audit. Guarded on
6897
- // `revoked_at IS NULL` so a repeat unassign is a silent no-op.
6901
+ // `revoked_at IS NULL` so a repeat unassign records a refused no-op.
6898
6902
  const scopeNode = assignment.node.scopeId;
6899
6903
  if (!scopeNode) {
6900
6904
  // One directory transaction (#1184): the revoke, the removal fence and the audit row.
@@ -6903,14 +6907,14 @@ export class SqliteScopeHost {
6903
6907
  }
6904
6908
  // On the scope actor (#1678): a revoke that joined a stranger's transaction and
6905
6909
  // rolled back with it left the role LIVE after the caller was told it was gone.
6906
- const rt = this.runtime(assignment.node.tenantId, scopeNode);
6907
- const changes = await rt.actor.turn(() => rt.db
6908
- .prepare(`UPDATE _substrat_tuples SET revoked_at = ?
6909
- WHERE subject = ? AND relation = ? AND object = ? AND revoked_at IS NULL`)
6910
- .run(new Date().toISOString(), `principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeNode}`).changes);
6911
- if (changes === 0)
6912
- return; // never assigned, or already revoked — idempotent, unaudited
6913
- this.recordAdmin(actor, 'unassignRole', { tenantId: assignment.node.tenantId, scopeId: scopeNode }, assignment, null);
6910
+ await auditedScopePermissionWrite({
6911
+ action: 'unassignRole', request: { ...assignment },
6912
+ record: (after) => this.recordAdmin(actor, 'unassignRole', { tenantId: assignment.node.tenantId, scopeId: scopeNode }, assignment, after),
6913
+ write: () => onNode(assignment.node, () => this.runtime(assignment.node.tenantId, scopeNode).db.prepare(`UPDATE _substrat_tuples SET revoked_at = ?
6914
+ WHERE subject = ? AND relation = ? AND object = ? AND revoked_at IS NULL`).run(new Date().toISOString(), `principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeNode}`).changes),
6915
+ outcome: (changes) => ({ phase: changes === 0 ? 'refused' : 'applied', details: { changed: changes !== 0 } }),
6916
+ logError: (message, fields) => console.error(message, fields),
6917
+ });
6914
6918
  },
6915
6919
  grant: async (actor, raw) => {
6916
6920
  // Parsed like its `grantToConnection`/`grantToSystem` siblings, not taken on
@@ -6918,16 +6922,22 @@ export class SqliteScopeHost {
6918
6922
  // to Z text (#963), and liveness here is a lexicographic `expires_at > ?`.
6919
6923
  const grant = capabilityGrant.parse(raw);
6920
6924
  this.refuseReservedGrant([grant.permission]); // #2430
6921
- await auditedOnNode(grant.node, () => writeGrant(`principal:${grant.principalId}`, grant.permission, grant.node, grant.entity, grant.expiresAt), () => this.recordAdmin(actor, 'grant', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId }, null, grant));
6925
+ await auditedOnNode(grant.node, () => writeGrant(`principal:${grant.principalId}`, grant.permission, grant.node, grant.entity, grant.expiresAt), () => this.recordAdmin(actor, 'grant', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId }, null, grant), { actor, action: 'grant', request: { ...grant } });
6922
6926
  },
6923
6927
  grantEntityShape: async (actor, grant) => {
6924
6928
  const { tenantId, scopeId } = grant.node;
6925
6929
  this.assertScope(tenantId, scopeId);
6926
6930
  this.refuseReservedGrant(grant.permissions); // #2430
6927
- const rt = this.runtime(tenantId, scopeId);
6928
6931
  // One unit on the scope actor (#1678): the marker and its keys land together or not at all.
6929
- await rt.actor.turn(() => rt.db.transaction(() => grantEntityShapeIn(switchSqlOf(rt.db), grant.principalId, grant.entity, grant.permissions))());
6930
- this.recordAdmin(actor, 'grantEntityShape', { tenantId, scopeId }, null, grant);
6932
+ await auditedScopePermissionWrite({
6933
+ action: 'grantEntityShape', request: { ...grant },
6934
+ record: (after) => this.recordAdmin(actor, 'grantEntityShape', { tenantId, scopeId }, null, after),
6935
+ write: () => {
6936
+ const rt = this.runtime(tenantId, scopeId);
6937
+ return rt.actor.turn(() => rt.db.transaction(() => grantEntityShapeIn(switchSqlOf(rt.db), grant.principalId, grant.entity, grant.permissions))());
6938
+ },
6939
+ logError: (message, fields) => console.error(message, fields),
6940
+ });
6931
6941
  },
6932
6942
  reconcileEntityGrantShapes: async (actor, node, shapes, opts) => {
6933
6943
  const limit = shapeTopUpBatch(opts?.batch);
@@ -7048,14 +7058,21 @@ export class SqliteScopeHost {
7048
7058
  // `invoke`'s open transaction and roll back with it after this verb returned. The
7049
7059
  // turn is re-entrant: from inside one of this scope's tasks it joins that task.
7050
7060
  // Restore is the lever; a grant is not.
7051
- const rt = this.runtime(grant.node.tenantId, scope);
7052
- await rt.actor.turn(() => {
7053
- if (systemSwitchedOff(switchSqlOf(rt.db), grant.moduleId)) {
7054
- throw substratError('conflict', systemSwitchedOffMessage(grant.moduleId, scope));
7055
- }
7056
- write();
7061
+ await auditedScopePermissionWrite({
7062
+ action: 'grantToSystem', request: { moduleId: grant.moduleId, permission: grant.permission, node: grant.node,
7063
+ expiresAt: grant.expiresAt ?? null, grantedBy: grant.grantedBy },
7064
+ record: (after) => this.recordAdmin(actor, 'grantToSystem', { tenantId: grant.node.tenantId, scopeId: scope }, null, after),
7065
+ write: () => {
7066
+ const rt = this.runtime(grant.node.tenantId, scope);
7067
+ return rt.actor.turn(() => {
7068
+ if (systemSwitchedOff(switchSqlOf(rt.db), grant.moduleId)) {
7069
+ throw substratError('conflict', systemSwitchedOffMessage(grant.moduleId, scope));
7070
+ }
7071
+ write();
7072
+ });
7073
+ },
7074
+ logError: (message, fields) => console.error(message, fields),
7057
7075
  });
7058
- audit();
7059
7076
  }
7060
7077
  else {
7061
7078
  // Tenant-level: a directory write, which no scope's transaction can hold. #1743:
@@ -7113,17 +7130,23 @@ export class SqliteScopeHost {
7113
7130
  // (#1678: every scope-level admin write takes one, so none lands mid-invoke inside a
7114
7131
  // stranger's transaction) and both in the admin log without the secret or its hash.
7115
7132
  mintCapability: async (actor, tenantId, scopeId, input) => {
7116
- const rt = await this.openActiveScope(tenantId, scopeId);
7117
- const minted = await rt.actor.turn(() => mintBecomeCapability(spineSql(rt.db), input, actor, this.clock()));
7118
- this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId }, null, {
7119
- capabilityId: minted.id,
7120
- mode: 'become',
7121
- principal: input.principal,
7122
- expiresAt: input.expiresAt,
7123
- maxUses: input.maxUses,
7124
- label: input.label ?? null,
7133
+ return auditedScopePermissionWrite({
7134
+ action: 'mintCapability', request: {
7135
+ mode: 'become',
7136
+ principal: input.principal,
7137
+ expiresAt: input.expiresAt,
7138
+ maxUses: input.maxUses,
7139
+ label: input.label ?? null,
7140
+ },
7141
+ record: (after) => this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId }, null, after),
7142
+ write: async () => {
7143
+ const rt = await this.openActiveScope(tenantId, scopeId);
7144
+ return rt.actor.turn(() => mintBecomeCapability(spineSql(rt.db), input, actor, this.clock()));
7145
+ },
7146
+ outcome: (minted) => ({ phase: 'applied', details: { capabilityId: minted.id } }),
7147
+ onUnrecordedOutcome: (minted, error) => ({ ...minted, auditWarning: auditWarningOf('the capability mint', error) }),
7148
+ logError: (message, fields) => console.error(message, fields),
7125
7149
  });
7126
- return minted;
7127
7150
  },
7128
7151
  revokeCapability: async (actor, tenantId, scopeId, capabilityId) => {
7129
7152
  const scope = this.directory
@@ -7163,7 +7186,7 @@ export class SqliteScopeHost {
7163
7186
  await auditedOnNode(node, () => {
7164
7187
  requireOrg(node.tenantId, orgId);
7165
7188
  writeGrant(`org:${orgId}`, permission, node, entity);
7166
- }, () => this.recordAdmin(actor, 'grantToOrg', { tenantId: node.tenantId, scopeId: node.scopeId }, null, { orgId, permission, node, entity }));
7189
+ }, () => this.recordAdmin(actor, 'grantToOrg', { tenantId: node.tenantId, scopeId: node.scopeId }, null, { orgId, permission, node, entity }), { actor, action: 'grantToOrg', request: { orgId, permission, node, entity } });
7167
7190
  },
7168
7191
  // -- vertical + version registry (#31) ---------------------------------
7169
7192
  // -- the hostname map (K-26) -------------------------------------------
@@ -8743,16 +8766,54 @@ export class SqliteScopeHost {
8743
8766
  // this host's clock, so a test can let a lease lapse without waiting.
8744
8767
  recordStoreDriverRelease: async (actor, input) => {
8745
8768
  const at = this.clock();
8746
- const { record, created } = this.directory.transaction(() => recordStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, actor, at }))();
8747
- if (created)
8748
- this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, record, at);
8749
- return record;
8769
+ let result;
8770
+ this.directoryWriteAndAudit(() => {
8771
+ result = recordStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, actor, at });
8772
+ return result.created;
8773
+ }, () => this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, result.record, at));
8774
+ return result.record;
8750
8775
  },
8751
8776
  getStoreDriverRelease: async (actor, driver, sha256) => {
8752
8777
  const record = storeDriverReleaseIn(switchSqlOf(this.directory), driver, sha256);
8753
8778
  this.recordAccess(actor, 'getStoreDriverRelease', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
8754
8779
  return record;
8755
8780
  },
8781
+ storeDriverApprovals: async (actor, driver) => {
8782
+ const records = storeDriverApprovalsIn(switchSqlOf(this.directory), driver);
8783
+ this.recordAccess(actor, 'storeDriverApprovals', { tenantId: null }, { driver }, records.length);
8784
+ return records;
8785
+ },
8786
+ liveStoreDriverApproval: async (actor, driver, sha256) => {
8787
+ const record = liveStoreDriverApprovalIn(switchSqlOf(this.directory), driver, sha256);
8788
+ this.recordAccess(actor, 'liveStoreDriverApproval', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
8789
+ return record;
8790
+ },
8791
+ approveStoreDriverRelease: async (actor, input) => {
8792
+ const at = this.clock();
8793
+ let record;
8794
+ this.directoryWriteAndAudit(() => {
8795
+ record = approveStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, approvalId: ulid(), actor, at });
8796
+ return true;
8797
+ }, () => this.recordAdmin(actor, 'approveStoreDriverRelease', { tenantId: null }, null, record, at));
8798
+ return record;
8799
+ },
8800
+ storeDriverRevokeWalk: async (actor, approvalId) => {
8801
+ const record = storeDriverRevokeWalkIn(switchSqlOf(this.directory), approvalId);
8802
+ this.recordAccess(actor, 'storeDriverRevokeWalk', { tenantId: null }, { approvalId }, record ? 1 : 0);
8803
+ return record;
8804
+ },
8805
+ recordStoreDriverRevokeWalk: async (actor, input) => this.directory.transaction(() => recordStoreDriverRevokeWalkIn(switchSqlOf(this.directory), { ...input, actor, nowMs: Date.parse(this.clock()) }))(),
8806
+ revokeStoreDriverRelease: async (actor, input) => {
8807
+ const at = this.clock();
8808
+ let before = null;
8809
+ let record;
8810
+ this.directoryWriteAndAudit(() => {
8811
+ before = liveStoreDriverApprovalIn(switchSqlOf(this.directory), input.driver, input.sha256) ?? null;
8812
+ record = revokeStoreDriverReleaseIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) });
8813
+ return true;
8814
+ }, () => this.recordAdmin(actor, 'revokeStoreDriverRelease', { tenantId: null }, before, record, at));
8815
+ return record;
8816
+ },
8756
8817
  storeDriverState: async (actor, driver) => {
8757
8818
  const state = storeDriverStateIn(switchSqlOf(this.directory), driver, Date.parse(this.clock()));
8758
8819
  this.recordAccess(actor, 'storeDriverState', { tenantId: null }, { driver }, state.rollouts.length);
@@ -8779,14 +8840,20 @@ export class SqliteScopeHost {
8779
8840
  },
8780
8841
  beginStoreDriverRollout: async (actor, input) => {
8781
8842
  const at = this.clock();
8782
- const row = this.directory.transaction(() => beginStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) }))();
8783
- this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at);
8843
+ let row;
8844
+ this.directoryWriteAndAudit(() => {
8845
+ row = beginStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, actor, at, nowMs: Date.parse(at) });
8846
+ return true;
8847
+ }, () => this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at));
8784
8848
  return row;
8785
8849
  },
8786
8850
  finishStoreDriverRollout: async (actor, input) => {
8787
8851
  const at = this.clock();
8788
- const row = this.directory.transaction(() => finishStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) }))();
8789
- this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at);
8852
+ let row;
8853
+ this.directoryWriteAndAudit(() => {
8854
+ row = finishStoreDriverRolloutIn(switchSqlOf(this.directory), { ...input, at, nowMs: Date.parse(at) });
8855
+ return true;
8856
+ }, () => this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row, at));
8790
8857
  return row;
8791
8858
  },
8792
8859
  storeDriverRebuild: async (actor, driver) => {
@@ -8987,6 +9054,9 @@ export class SqliteScopeHost {
8987
9054
  insert.run(...row);
8988
9055
  },
8989
9056
  });
9057
+ // The dump's rows have landed in the rebuilt tables. Append this restore's
9058
+ // own row before commit, so a failed insert rolls the imported state back.
9059
+ this.recordAdmin(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length });
8990
9060
  })();
8991
9061
  // #1764's split, over any version the dump stored before it.
8992
9062
  this.splitVersionMigrations();
@@ -8994,7 +9064,6 @@ export class SqliteScopeHost {
8994
9064
  // table would leave every permission check reading the PRE-restore roles until
8995
9065
  // the process restarted — the exact silent-divergence a recovery must not have.
8996
9066
  this.loadRoles();
8997
- this.recordAdmin(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length });
8998
9067
  },
8999
9068
  activateScope: async (actor, tenantId, scopeId) => {
9000
9069
  // Idempotent on `active`, unaudited because nothing changed. Provisioning is
@@ -9227,15 +9296,15 @@ export class SqliteScopeHost {
9227
9296
  // records an access that could never have happened.
9228
9297
  this.assertScope(input.tenantId, input.scopeId);
9229
9298
  const session = newImpersonationSession(ulid(), actor, input, this.clock());
9230
- this.directory
9299
+ this.directoryWriteAndAudit(() => this.directory
9231
9300
  .prepare(`INSERT INTO _substrat_impersonations
9232
9301
  (${IMPERSONATION_COLUMNS}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
9233
- .run(...impersonationRowValues(session));
9302
+ .run(...impersonationRowValues(session)).changes > 0,
9234
9303
  // BEFORE the session is returned, so the log entry precedes every row it
9235
9304
  // will ever touch. The reason rides in `after` — it is the field a review
9236
9305
  // reads first, and a log saying a session opened but not why is half a
9237
9306
  // record.
9238
- this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session);
9307
+ () => this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session));
9239
9308
  return session;
9240
9309
  },
9241
9310
  endImpersonation: async (actor, id) => {
@@ -9244,11 +9313,14 @@ export class SqliteScopeHost {
9244
9313
  // already stopped it, and it must not move the moment it stopped.
9245
9314
  if (before.endedAt !== null)
9246
9315
  return before;
9247
- this.directory
9248
- .prepare('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ?')
9249
- .run(this.clock(), id);
9250
- const after = this.readImpersonation(id);
9251
- this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after);
9316
+ let after;
9317
+ this.directoryWriteAndAudit(() => {
9318
+ const changed = this.directory
9319
+ .prepare('UPDATE _substrat_impersonations SET ended_at = ? WHERE id = ?')
9320
+ .run(this.clock(), id).changes > 0;
9321
+ after = this.readImpersonation(id);
9322
+ return changed;
9323
+ }, () => this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after));
9252
9324
  return after;
9253
9325
  },
9254
9326
  listImpersonations: async (actor, filter) => {
@@ -9419,26 +9491,30 @@ export class SqliteScopeHost {
9419
9491
  this.recordAccess(actor, 'readMeters', { tenantId: only ?? null }, filter ?? null, reading.perTenant.length);
9420
9492
  return meterReading.parse(reading);
9421
9493
  },
9494
+ readTenantStorageGauge: async (actor, tenantId, limit) => {
9495
+ const result = readBoundedTenantStorageGauge(redactionSqlOf(this.directory), tenantId, limit);
9496
+ this.recordAccess(actor, 'readTenantStorageGauge', { tenantId }, { limit }, result.gauge.sampled);
9497
+ return result;
9498
+ },
9422
9499
  registerIdentityPool: async (actor, input) => {
9423
9500
  const parsed = identityPool.parse(input);
9424
- const existing = readPool(parsed.provider);
9425
- if (existing) {
9426
- // Idempotent on an identical registration. A CONFLICTING one throws:
9427
- // flipping a live pool's topology silently reinterprets every row it owns
9428
- // — the same externalId across tenants would change from one human to two.
9429
- if (existing.topology === parsed.topology && existing.tenantId === parsed.tenantId) {
9430
- return;
9501
+ this.directoryWriteAndAudit(() => {
9502
+ const existing = readPool(parsed.provider);
9503
+ if (existing) {
9504
+ // Idempotent on an identical registration. A conflicting one is refused.
9505
+ if (existing.topology === parsed.topology && existing.tenantId === parsed.tenantId) {
9506
+ return false;
9507
+ }
9508
+ throw substratError('conflict', `identity pool '${parsed.provider}' is already registered as ${existing.topology}` +
9509
+ `${existing.tenantId ? ` for tenant ${existing.tenantId}` : ''}`);
9431
9510
  }
9432
- throw substratError('conflict', `identity pool '${parsed.provider}' is already registered as ${existing.topology}` +
9433
- `${existing.tenantId ? ` for tenant ${existing.tenantId}` : ''}`);
9434
- }
9435
- this.directory
9436
- .prepare('INSERT INTO _substrat_identity_pools (provider, topology, tenant_id, created_at) VALUES (?, ?, ?, ?)')
9437
- .run(parsed.provider, parsed.topology, parsed.tenantId, new Date().toISOString());
9438
- this.recordAdmin(actor, 'registerIdentityPool',
9511
+ return this.directory
9512
+ .prepare('INSERT INTO _substrat_identity_pools (provider, topology, tenant_id, created_at) VALUES (?, ?, ?, ?)')
9513
+ .run(parsed.provider, parsed.topology, parsed.tenantId, new Date().toISOString()).changes > 0;
9514
+ }, () => this.recordAdmin(actor, 'registerIdentityPool',
9439
9515
  // Null for a central pool: it belongs to no single tenant, which is what
9440
9516
  // made the admin log's tenantId nullable.
9441
- { tenantId: parsed.tenantId }, null, parsed);
9517
+ { tenantId: parsed.tenantId }, null, parsed));
9442
9518
  },
9443
9519
  getIdentityPool: async (actor, provider) => {
9444
9520
  const pool = readPool(provider);
@@ -9517,42 +9593,46 @@ export class SqliteScopeHost {
9517
9593
  const input = createConnectionInput.parse(raw);
9518
9594
  const sealed = await this.secretBox.seal(JSON.stringify(input.secret));
9519
9595
  const now = new Date().toISOString();
9520
- try {
9521
- this.directory
9522
- .prepare(`INSERT INTO _substrat_connections
9596
+ this.directoryWriteAndAudit(() => {
9597
+ try {
9598
+ this.directory
9599
+ .prepare(`INSERT INTO _substrat_connections
9523
9600
  (id, tenant_id, vertical, provider, label, status, external_account_ref,
9524
9601
  scopes, expires_at, last_ok_at, last_error, last_error_at,
9525
9602
  created_by, created_at, revoked_at)
9526
9603
  VALUES (?, ?, ?, ?, ?, 'active', ?, ?, ?, NULL, NULL, NULL, ?, ?, NULL)`)
9527
- .run(input.id, input.tenantId, input.vertical, input.provider, input.label, input.externalAccountRef ?? null, JSON.stringify(input.scopes), input.expiresAt ?? null,
9528
- // The authorizing principal when supplied (a self-serve connect), else the
9529
- // effecting platform actor. See connections.md §3.5.1 / createConnectionInput.
9530
- input.createdBy ?? actor, now);
9531
- }
9532
- catch (err) {
9533
- if (/UNIQUE constraint failed/i.test(err.message)) {
9534
- const account = input.externalAccountRef
9535
- ? ` under account '${input.externalAccountRef}'`
9536
- : '';
9537
- throw new Error(`tenant ${input.tenantId} already has a live '${input.provider}' connection ` +
9538
- `for vertical '${input.vertical}'${account} — revoke it before connecting another`);
9604
+ .run(input.id, input.tenantId, input.vertical, input.provider, input.label, input.externalAccountRef ?? null, JSON.stringify(input.scopes), input.expiresAt ?? null,
9605
+ // The authorizing principal when supplied (a self-serve connect), else the
9606
+ // effecting platform actor. See connections.md §3.5.1 / createConnectionInput.
9607
+ input.createdBy ?? actor, now);
9539
9608
  }
9540
- throw err;
9541
- }
9542
- this.directory
9543
- .prepare(`INSERT INTO _substrat_connection_secrets (connection_id, key_id, ciphertext, updated_at)
9609
+ catch (err) {
9610
+ if (/UNIQUE constraint failed/i.test(err.message)) {
9611
+ const account = input.externalAccountRef
9612
+ ? ` under account '${input.externalAccountRef}'`
9613
+ : '';
9614
+ throw new Error(`tenant ${input.tenantId} already has a live '${input.provider}' connection ` +
9615
+ `for vertical '${input.vertical}'${account} — revoke it before connecting another`);
9616
+ }
9617
+ throw err;
9618
+ }
9619
+ this.directory
9620
+ .prepare(`INSERT INTO _substrat_connection_secrets (connection_id, key_id, ciphertext, updated_at)
9544
9621
  VALUES (?, ?, ?, ?)`)
9545
- .run(input.id, sealed.keyId, sealed.ciphertext, now);
9546
- // METADATA ONLY. `_substrat_admin_log` is append-only, so a credential
9547
- // written here could never be removed — the redaction is the point, and
9548
- // it is structural rather than a rule someone has to remember.
9549
- this.recordAdmin(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
9550
- id: input.id,
9551
- provider: input.provider,
9552
- label: input.label,
9553
- scopes: input.scopes,
9554
- externalAccountRef: input.externalAccountRef ?? null,
9555
- createdBy: input.createdBy ?? actor,
9622
+ .run(input.id, sealed.keyId, sealed.ciphertext, now);
9623
+ return true;
9624
+ }, () => {
9625
+ // METADATA ONLY. `_substrat_admin_log` is append-only, so a credential
9626
+ // written here could never be removed — the redaction is the point, and
9627
+ // it is structural rather than a rule someone has to remember.
9628
+ this.recordAdmin(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
9629
+ id: input.id,
9630
+ provider: input.provider,
9631
+ label: input.label,
9632
+ scopes: input.scopes,
9633
+ externalAccountRef: input.externalAccountRef ?? null,
9634
+ createdBy: input.createdBy ?? actor,
9635
+ });
9556
9636
  });
9557
9637
  },
9558
9638
  connectionSealingKey: (id) => this.ensureSealingKey(id),
@@ -9617,25 +9697,29 @@ export class SqliteScopeHost {
9617
9697
  const row = this.connectionRow(id);
9618
9698
  const sealed = await this.secretBox.seal(JSON.stringify(connectionSecret.parse(secret)));
9619
9699
  const now = new Date().toISOString();
9620
- this.directory
9621
- .prepare(`UPDATE _substrat_connection_secrets
9700
+ this.directoryWriteAndAudit(() => {
9701
+ this.directory
9702
+ .prepare(`UPDATE _substrat_connection_secrets
9622
9703
  SET key_id = ?, ciphertext = ?, updated_at = ? WHERE connection_id = ?`)
9623
- .run(sealed.keyId, sealed.ciphertext, now, id);
9624
- // A refresh revives a connection that had lapsed or errored.
9625
- this.directory
9626
- .prepare(`UPDATE _substrat_connections
9704
+ .run(sealed.keyId, sealed.ciphertext, now, id);
9705
+ // A refresh revives a connection that had lapsed or errored.
9706
+ this.directory
9707
+ .prepare(`UPDATE _substrat_connections
9627
9708
  SET status = 'active', expires_at = ?, last_error = NULL, last_error_at = NULL
9628
9709
  WHERE id = ?`)
9629
- .run(expiresAt === undefined ? row.expires_at : expiresAt, id);
9630
- // The event, never the token. "Rotated at T" is the auditable fact — plus WHO
9631
- // authorized it when the rotation was a tenant admin's act (§3.5.1's attribution,
9632
- // rotate-side): the principal, never laundered into the actor column.
9633
- this.recordAdmin(actor, 'updateConnectionSecret', { tenantId: row.tenant_id, vertical: row.vertical }, null, {
9634
- id,
9635
- provider: row.provider,
9636
- rotatedAt: now,
9637
- expiresAt: expiresAt === undefined ? row.expires_at : expiresAt,
9638
- ...(opts?.rotatedBy ? { rotatedBy: opts.rotatedBy } : {}),
9710
+ .run(expiresAt === undefined ? row.expires_at : expiresAt, id);
9711
+ return true;
9712
+ }, () => {
9713
+ // The event, never the token. "Rotated at T" is the auditable fact — plus WHO
9714
+ // authorized it when the rotation was a tenant admin's act (§3.5.1's attribution,
9715
+ // rotate-side): the principal, never laundered into the actor column.
9716
+ this.recordAdmin(actor, 'updateConnectionSecret', { tenantId: row.tenant_id, vertical: row.vertical }, null, {
9717
+ id,
9718
+ provider: row.provider,
9719
+ rotatedAt: now,
9720
+ expiresAt: expiresAt === undefined ? row.expires_at : expiresAt,
9721
+ ...(opts?.rotatedBy ? { rotatedBy: opts.rotatedBy } : {}),
9722
+ });
9639
9723
  });
9640
9724
  },
9641
9725
  revokeConnection: async (actor, id) => {
@@ -9643,27 +9727,29 @@ export class SqliteScopeHost {
9643
9727
  if (row.revoked_at)
9644
9728
  return; // idempotent, and a no-op is not audited
9645
9729
  const now = new Date().toISOString();
9646
- this.directory
9647
- .prepare(`UPDATE _substrat_connections SET status = 'revoked', revoked_at = ? WHERE id = ?`)
9648
- .run(now, id);
9649
- // The sealed blob goes NOW. A tombstoned connection is evidence that a
9650
- // grant existed (K-21); keeping the usable credential would make it a
9651
- // liability instead. The row says what happened; the secret does not.
9652
- this.directory
9653
- .prepare('DELETE FROM _substrat_connection_secrets WHERE connection_id = ?')
9654
- .run(id);
9655
- // Connector state dies with the connection: it is that connection's
9656
- // private bookkeeping and means nothing once it can no longer act.
9657
- this.directory
9658
- .prepare('DELETE FROM _substrat_connector_state WHERE connection_id = ?')
9659
- .run(id);
9660
- // #592: its grants tombstone (K-21 — evidence, not roster). Absent from the
9661
- // next gather, so no later provision/reconcile delivers them again.
9662
- this.directory
9663
- .prepare(`UPDATE _substrat_connection_grants SET revoked_at = ?
9730
+ this.directoryWriteAndAudit(() => {
9731
+ this.directory
9732
+ .prepare(`UPDATE _substrat_connections SET status = 'revoked', revoked_at = ? WHERE id = ?`)
9733
+ .run(now, id);
9734
+ // The sealed blob goes NOW. A tombstoned connection is evidence that a
9735
+ // grant existed (K-21); keeping the usable credential would make it a
9736
+ // liability instead. The row says what happened; the secret does not.
9737
+ this.directory
9738
+ .prepare('DELETE FROM _substrat_connection_secrets WHERE connection_id = ?')
9739
+ .run(id);
9740
+ // Connector state dies with the connection: it is that connection's
9741
+ // private bookkeeping and means nothing once it can no longer act.
9742
+ this.directory
9743
+ .prepare('DELETE FROM _substrat_connector_state WHERE connection_id = ?')
9744
+ .run(id);
9745
+ // #592: its grants tombstone (K-21 — evidence, not roster). Absent from the
9746
+ // next gather, so no later provision/reconcile delivers them again.
9747
+ this.directory
9748
+ .prepare(`UPDATE _substrat_connection_grants SET revoked_at = ?
9664
9749
  WHERE connection_id = ? AND revoked_at IS NULL`)
9665
- .run(now, id);
9666
- this.recordAdmin(actor, 'revokeConnection', { tenantId: row.tenant_id, vertical: row.vertical }, { status: row.status }, { id, provider: row.provider, status: 'revoked', revokedAt: now });
9750
+ .run(now, id);
9751
+ return true;
9752
+ }, () => this.recordAdmin(actor, 'revokeConnection', { tenantId: row.tenant_id, vertical: row.vertical }, { status: row.status }, { id, provider: row.provider, status: 'revoked', revokedAt: now }));
9667
9753
  },
9668
9754
  // -- a vertical's mailed connect links (connections.md §3.5.4) --------------
9669
9755
  // The statements are the kernel's (`connect-links.ts`), shared with the hosted
@@ -9803,32 +9889,30 @@ export class SqliteScopeHost {
9803
9889
  // Read before write. `INSERT OR IGNORE` alone cannot tell "already bound to the
9804
9890
  // same principal" (idempotent) from "already bound to someone else" (a
9805
9891
  // collision), and silently ignoring the second resolves one person as another.
9806
- const existing = this.directory
9807
- .prepare(`SELECT principal_id FROM _substrat_identities
9892
+ this.directoryWriteAndAudit(() => {
9893
+ const existing = this.directory
9894
+ .prepare(`SELECT principal_id FROM _substrat_identities
9808
9895
  WHERE tenant_id = ? AND provider = ? AND external_id = ?`)
9809
- .get(parsed.tenantId, parsed.provider, parsed.externalId);
9810
- if (existing) {
9811
- if (existing.principal_id === parsed.principal)
9812
- return; // idempotent, unaudited
9813
- throw substratError('conflict', `identity ${parsed.provider}:${parsed.externalId} in tenant ${parsed.tenantId} ` +
9814
- `is already bound to ${existing.principal_id}`);
9815
- }
9816
- this.directory
9817
- .prepare(`INSERT INTO _substrat_identities
9896
+ .get(parsed.tenantId, parsed.provider, parsed.externalId);
9897
+ if (existing) {
9898
+ if (existing.principal_id === parsed.principal)
9899
+ return false; // idempotent, unaudited
9900
+ throw substratError('conflict', `identity ${parsed.provider}:${parsed.externalId} in tenant ${parsed.tenantId} ` +
9901
+ `is already bound to ${existing.principal_id}`);
9902
+ }
9903
+ return this.directory
9904
+ .prepare(`INSERT INTO _substrat_identities
9818
9905
  (provider, external_id, principal_id, tenant_id, scope_id, created_at)
9819
9906
  VALUES (?, ?, ?, ?, ?, ?)`)
9820
- .run(parsed.provider, parsed.externalId, parsed.principal, parsed.tenantId, parsed.scopeId ?? null, new Date().toISOString());
9821
- this.recordAdmin(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal });
9907
+ .run(parsed.provider, parsed.externalId, parsed.principal, parsed.tenantId, parsed.scopeId ?? null, new Date().toISOString()).changes > 0;
9908
+ }, () => this.recordAdmin(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal }));
9822
9909
  },
9823
9910
  unlinkIdentity: async (actor, tenantId, principal) => {
9824
9911
  // DELETE, not a tombstone — the identity map is current state (audit is the log),
9825
9912
  // and re-inviting must be able to re-link a fresh principal for the same person.
9826
- const info = this.directory
9913
+ this.directoryWriteAndAudit(() => this.directory
9827
9914
  .prepare(`DELETE FROM _substrat_identities WHERE tenant_id = ? AND principal_id = ?`)
9828
- .run(tenantId, principal);
9829
- if (info.changes === 0)
9830
- return; // no link — idempotent, unaudited
9831
- this.recordAdmin(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null);
9915
+ .run(tenantId, principal).changes > 0, () => this.recordAdmin(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null));
9832
9916
  },
9833
9917
  resolveIdentity: async (tenantId, provider, externalId) => {
9834
9918
  const row = this.directory
@@ -9924,32 +10008,40 @@ export class SqliteScopeHost {
9924
10008
  // `drained_at IS NULL` makes the stamp idempotent: a retried pass that ships
9925
10009
  // the same batch twice re-stamps nothing and reports 0, so the admin log does
9926
10010
  // not grow a row claiming an egress that already happened.
9927
- const info = this.directory
9928
- .prepare(`UPDATE _substrat_access_log SET drained_at = ?
10011
+ let drained = 0;
10012
+ this.directoryWriteAndAudit(() => {
10013
+ const info = this.directory
10014
+ .prepare(`UPDATE _substrat_access_log SET drained_at = ?
9929
10015
  WHERE id <= ? AND drained_at IS NULL`)
9930
- .run(drainedAt, upToId);
9931
- if (info.changes > 0) {
10016
+ .run(drainedAt, upToId);
10017
+ drained = info.changes;
10018
+ return drained > 0;
10019
+ }, () => {
9932
10020
  // The payload is the APPLIED state, so it belongs in `after` (contracts'
9933
10021
  // adminLogEntry: before = prior state, after = the applied payload).
9934
- this.recordAdmin(actor, 'drainAccessLog', { tenantId: null }, null, { drained: info.changes, upToId, drainedAt });
9935
- }
9936
- return info.changes;
10022
+ this.recordAdmin(actor, 'drainAccessLog', { tenantId: null }, null, { drained, upToId, drainedAt });
10023
+ });
10024
+ return drained;
9937
10025
  },
9938
10026
  pruneAccessLog: async (actor, limit) => {
9939
10027
  assertRowLimit('limit', limit);
9940
10028
  // ONLY drained rows. Age alone is not a licence to delete evidence.
9941
- const info = this.directory
9942
- .prepare(`DELETE FROM _substrat_access_log WHERE id IN (
10029
+ let pruned = 0;
10030
+ this.directoryWriteAndAudit(() => {
10031
+ const info = this.directory
10032
+ .prepare(`DELETE FROM _substrat_access_log WHERE id IN (
9943
10033
  SELECT id FROM _substrat_access_log WHERE drained_at IS NOT NULL ORDER BY id LIMIT ?
9944
10034
  )`)
9945
- .run(limit);
9946
- if (info.changes > 0) {
10035
+ .run(limit);
10036
+ pruned = info.changes;
10037
+ return pruned > 0;
10038
+ }, () => {
9947
10039
  // The payload is the APPLIED state, so it belongs in `after` (contracts'
9948
10040
  // adminLogEntry: before = prior state, after = the applied payload) — the
9949
10041
  // same shape as drainAccessLog's row above (#557).
9950
- this.recordAdmin(actor, 'pruneAccessLog', { tenantId: null }, null, { pruned: info.changes });
9951
- }
9952
- return info.changes;
10042
+ this.recordAdmin(actor, 'pruneAccessLog', { tenantId: null }, null, { pruned });
10043
+ });
10044
+ return pruned;
9953
10045
  },
9954
10046
  auditLog: async (actor, filter) => {
9955
10047
  const where = [];
@@ -10236,21 +10328,27 @@ export class SqliteScopeHost {
10236
10328
  return rows.map((r) => issueEntry.parse(issueOf(r)));
10237
10329
  },
10238
10330
  setIssueStatus: async (actor, fingerprint, status) => {
10239
- const existing = this.directory
10240
- .prepare('SELECT * FROM _substrat_issues WHERE fingerprint = ?')
10241
- .get(fingerprint);
10242
- if (!existing)
10243
- return undefined;
10244
- const resolvedAt = status === 'resolved' ? new Date().toISOString() : null;
10245
- // "Resolved under X": freeze the newest version at the verdict, so a later
10246
- // regression can name the pair. A reopen or ignore clears it with the timestamp.
10247
- const resolvedVersion = status === 'resolved' ? existing.last_version : null;
10248
- this.directory
10249
- .prepare('UPDATE _substrat_issues SET status = ?, resolved_at = ?, resolved_version = ? WHERE fingerprint = ?')
10250
- .run(status, resolvedAt, resolvedVersion, fingerprint);
10251
- const after = issueEntry.parse(issueOf({ ...existing, status, resolved_at: resolvedAt, resolved_version: resolvedVersion }));
10252
- // A lifecycle flip is a staff mutation — audited with the diff (K-33).
10253
- this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: existing.status }, { fingerprint, status: after.status });
10331
+ let existing;
10332
+ let after;
10333
+ this.directoryWriteAndAudit(() => {
10334
+ existing = this.directory
10335
+ .prepare('SELECT * FROM _substrat_issues WHERE fingerprint = ?')
10336
+ .get(fingerprint);
10337
+ if (!existing)
10338
+ return false;
10339
+ const resolvedAt = status === 'resolved' ? new Date().toISOString() : null;
10340
+ // "Resolved under X": freeze the newest version at the verdict, so a later
10341
+ // regression can name the pair. A reopen or ignore clears it with the timestamp.
10342
+ const resolvedVersion = status === 'resolved' ? existing.last_version : null;
10343
+ this.directory
10344
+ .prepare('UPDATE _substrat_issues SET status = ?, resolved_at = ?, resolved_version = ? WHERE fingerprint = ?')
10345
+ .run(status, resolvedAt, resolvedVersion, fingerprint);
10346
+ after = issueEntry.parse(issueOf({ ...existing, status, resolved_at: resolvedAt, resolved_version: resolvedVersion }));
10347
+ return true;
10348
+ }, () => {
10349
+ // A lifecycle flip is a staff mutation — audited with the diff (K-33).
10350
+ this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: existing.status }, { fingerprint, status: after.status });
10351
+ });
10254
10352
  return after;
10255
10353
  },
10256
10354
  listFindings: async (actor, filter) => {
@@ -10313,6 +10411,16 @@ export class SqliteScopeHost {
10313
10411
  this.recordAccess(actor, 'listRequestRollupDays', { tenantId: null }, filter ?? null, rows.length);
10314
10412
  return rows;
10315
10413
  },
10414
+ listTenantRequestMeter: async (actor, tenantId, since, until, limit) => {
10415
+ const rows = readTenantRequestMeterRows(redactionSqlOf(this.directory), tenantId, since, until, limit);
10416
+ this.recordAccess(actor, 'listTenantRequestMeter', { tenantId }, { since, until, limit }, rows.length);
10417
+ return rows;
10418
+ },
10419
+ listRequestMeterDays: async (actor, since, until) => {
10420
+ const rows = readRequestMeterDayRows(redactionSqlOf(this.directory), since, until);
10421
+ this.recordAccess(actor, 'listRequestMeterDays', { tenantId: null }, { since, until }, rows.length);
10422
+ return rows;
10423
+ },
10316
10424
  pruneRequestDays: async (_actor, limit, retentionMonths) => this.directory.transaction(() => pruneRequestDayRows(redactionSqlOf(this.directory), Date.now(), limit, retentionMonths))(),
10317
10425
  // #2501: the snapshot reads and writes in one transaction, so its rows are one instant's reading.
10318
10426
  recordMeterDay: async (_actor, input) => this.directory.transaction(() => recordMeterDayRows(redactionSqlOf(this.directory), input))(),
@@ -10332,6 +10440,22 @@ export class SqliteScopeHost {
10332
10440
  this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
10333
10441
  return rows;
10334
10442
  },
10443
+ listTenantModelUsageMeter: async (actor, filter) => {
10444
+ const cursor = filter.cursor;
10445
+ const since = canonicalInstant(filter.since);
10446
+ const until = canonicalInstant(filter.until);
10447
+ const rows = this.directory.prepare(`SELECT id, at, substr(vertical, 1, 64) AS vertical, substr(model, 1, 128) AS model,
10448
+ CASE WHEN length(vertical) > 64 OR length(model) > 128 THEN 1 ELSE 0 END AS label_truncated,
10449
+ reported, input_tokens, output_tokens
10450
+ FROM _substrat_model_usage
10451
+ WHERE tenant_id = ? AND at >= ? AND at < ?
10452
+ ${cursor ? 'AND (at < ? OR (at = ? AND id < ?))' : ''}
10453
+ ORDER BY at DESC, id DESC LIMIT ?`).all(filter.tenantId, since, until, ...(cursor ? [cursor.at, cursor.at, cursor.id] : []), Math.min(assertRowLimit('limit', filter.limit), 501));
10454
+ this.recordAccess(actor, 'listTenantModelUsageMeter', { tenantId: filter.tenantId }, { since, until, limit: filter.limit }, rows.length);
10455
+ return rows.map((row) => ({ id: row.id, at: row.at, vertical: row.vertical, model: row.model,
10456
+ labelTruncated: row.label_truncated !== 0, reported: row.reported !== 0,
10457
+ inputTokens: row.input_tokens, outputTokens: row.output_tokens }));
10458
+ },
10335
10459
  summarizeModelUsage: async (actor, window, marginPercent) => {
10336
10460
  const rows = this.selectModelUsage({ ...window, order: 'asc' });
10337
10461
  this.recordAccess(actor, 'summarizeModelUsage', { tenantId: window.tenantId ?? null }, window, rows.length);