@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.d.ts.map +1 -1
- package/dist/index.js +312 -188
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|
|
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
|
|
6560
|
-
|
|
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
|
|
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
|
-
|
|
6907
|
-
|
|
6908
|
-
.
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
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
|
|
6930
|
-
|
|
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
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
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
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
|
|
7124
|
-
|
|
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
|
-
|
|
8747
|
-
|
|
8748
|
-
this.
|
|
8749
|
-
|
|
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
|
-
|
|
8783
|
-
this.
|
|
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
|
-
|
|
8789
|
-
this.
|
|
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
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
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
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
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
|
-
|
|
9433
|
-
|
|
9434
|
-
|
|
9435
|
-
this.
|
|
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
|
-
|
|
9521
|
-
|
|
9522
|
-
.
|
|
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
|
-
|
|
9528
|
-
|
|
9529
|
-
|
|
9530
|
-
|
|
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
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
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
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
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.
|
|
9621
|
-
.
|
|
9700
|
+
this.directoryWriteAndAudit(() => {
|
|
9701
|
+
this.directory
|
|
9702
|
+
.prepare(`UPDATE _substrat_connection_secrets
|
|
9622
9703
|
SET key_id = ?, ciphertext = ?, updated_at = ? WHERE connection_id = ?`)
|
|
9623
|
-
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
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
|
-
|
|
9630
|
-
|
|
9631
|
-
|
|
9632
|
-
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
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.
|
|
9647
|
-
.
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
.
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
.
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
.
|
|
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
|
-
|
|
9666
|
-
|
|
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
|
-
|
|
9807
|
-
|
|
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
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9928
|
-
|
|
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
|
-
|
|
9931
|
-
|
|
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
|
|
9935
|
-
}
|
|
9936
|
-
return
|
|
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
|
-
|
|
9942
|
-
|
|
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
|
-
|
|
9946
|
-
|
|
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
|
|
9951
|
-
}
|
|
9952
|
-
return
|
|
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
|
-
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
|
|
10252
|
-
|
|
10253
|
-
|
|
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);
|