@substrat-run/adapter-sqlite 0.161.0 → 0.163.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,7 +3,7 @@ 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, 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, rebindRaceRefusal, SERVES_NOW_CONDITION, SERVES_NOW_PREDICATE, servesNowParams, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, fetchLikeOf, deadlineFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf, } from '@substrat-run/kernel';
6
+ 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, 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
8
  import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
9
9
  import { INERT_SCOPE_REASON, isPrimaryScopeRow, normalizeUploadMediaType } from '@substrat-run/kernel';
@@ -343,6 +343,10 @@ const KERNEL_DDL = `
343
343
  -- only) and its watermark per producer, plus the producer-side (type, id) outbox index the
344
344
  -- export read seeks on. Shared with the DO adapter from @substrat-run/kernel.
345
345
  ${VERTICAL_EVENTS_DDL}
346
+ -- RFC wire-signing §4: the nonces of signed calls this scope accepted, and their count.
347
+ -- Store-local, never dumped. Shared with the DO adapter from @substrat-run/kernel, where the
348
+ -- column comments are (its time is integer seconds, the envelope's own unit).
349
+ ${WIRE_NONCE_DDL}
346
350
  CREATE TABLE IF NOT EXISTS _substrat_migrations (
347
351
  module_id TEXT NOT NULL,
348
352
  version TEXT NOT NULL,
@@ -2889,7 +2893,9 @@ export class SqliteScopeHost {
2889
2893
  // A dump written before indexes were excluded may still carry them; skip those
2890
2894
  // too rather than failing a restore over data that is about to be recomputed.
2891
2895
  // Matched without case, as SQLite resolves a table name (#1883 review).
2892
- const replayable = dumped.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
2896
+ // The wire nonce store is this store's own (RFC wire-signing §4): a dump that carries it is not
2897
+ // replayed, so a load starts it empty, as it starts a vertical's search index.
2898
+ const replayable = dumped.filter((t) => !isSearchIndexTable(t.name.toLowerCase()) && !isWireNonceTable(t.name));
2893
2899
  // A Durable Object's dump carries spine tables this adapter keeps in its directory, or does
2894
2900
  // not keep at all; they are skipped by name, and any other unknown spine table is refused.
2895
2901
  const loadable = replayable.filter((t) => !DO_SCOPE_ONLY_SPINE_TABLES.has(t.name.toLowerCase()));
@@ -4225,6 +4231,20 @@ export class SqliteScopeHost {
4225
4231
  async redrainCountLocal(scopeId, drainedBefore) {
4226
4232
  return redrainCountOf(this.heldReadDb(scopeId), drainedBefore);
4227
4233
  }
4234
+ /**
4235
+ * RFC wire-signing §4: spend a signed call's nonce in this scope's own store, the hosted
4236
+ * object's `claimWireNonce` here: `true` once, `false` for a nonce already spent or a claim after
4237
+ * `until`, and a throw when the store is at its cap (after its purge committed). Judged against
4238
+ * this host's clock. No provisioned check, as on the hosted object: the call that provisions a
4239
+ * scope spends its nonce there too.
4240
+ */
4241
+ async claimWireNonceLocal(tenantId, scopeId, nonce, until) {
4242
+ const rt = this.runtime(tenantId, scopeId);
4243
+ const answer = await rt.actor.enqueue(() => rt.db.transaction(() => claimWireNonceIn(switchSqlOf(rt.db), nonce, until, Math.floor(Date.parse(this.clock()) / 1000)))());
4244
+ if (answer === 'full')
4245
+ throw substratError('unavailable', WIRE_NONCE_FULL);
4246
+ return answer;
4247
+ }
4228
4248
  /**
4229
4249
  * The read connection of a scope this host holds, for a read that names only the scope id (#2541),
4230
4250
  * with the directory read ONCE. Refused `conflict` before anything is opened: no row, a reaped one
@@ -6519,6 +6539,19 @@ export class SqliteScopeHost {
6519
6539
  const onNode = (node, body) => node.scopeId
6520
6540
  ? this.runtime(node.tenantId, node.scopeId).actor.turn(body)
6521
6541
  : Promise.resolve().then(body);
6542
+ /**
6543
+ * `onNode`, with the write's admin row (#2526). At the tenant node the write is a directory
6544
+ * row, so it and its row are one directory transaction. At a scope node it is in the scope's
6545
+ * own store, which no directory transaction reaches, so the row follows it (#1292's window).
6546
+ */
6547
+ const auditedOnNode = async (node, write, audit) => {
6548
+ if (!node.scopeId) {
6549
+ this.directoryWriteAndAudit(() => { write(); return true; }, audit);
6550
+ return;
6551
+ }
6552
+ await onNode(node, write);
6553
+ audit();
6554
+ };
6522
6555
  /**
6523
6556
  * One kill switch moved on one scope — the ONE body `revokeFromSystem` / `restoreToSystem`
6524
6557
  * (#1666, a module's `system:` authority) and `revokeFromPeer` / `restoreToPeer` (#1706, a
@@ -6775,14 +6808,25 @@ export class SqliteScopeHost {
6775
6808
  // #2447: judged before the write, against this host's keys and the tenant's other roles, so a
6776
6809
  // role assigned through a door cannot be widened afterwards. A refusal writes nothing, so a
6777
6810
  // seat of the role as it was stored keeps working.
6778
- this.refuseReservedDefinition(tenantId, parsed);
6779
- const before = this.roles.get(`${tenantId}/${parsed.key}`) ?? null;
6780
- this.directory
6781
- .prepare(`INSERT OR REPLACE INTO _substrat_roles (tenant_id, role_key, permissions, source, holder)
6782
- VALUES (?, ?, ?, ?, ?)`)
6783
- .run(tenantId, parsed.key, JSON.stringify(parsed.permissions), String(parsed.source), parsed.holder ?? 'person');
6811
+ // #2526: the judgement, the row's `before`, the write and the row are one directory unit.
6812
+ let before = null;
6813
+ this.directoryWriteAndAudit(() => {
6814
+ this.refuseReservedDefinition(tenantId, parsed);
6815
+ // The row as stored, read as `loadRoles` reads one.
6816
+ const stored = this.directory
6817
+ .prepare('SELECT permissions, source, holder FROM _substrat_roles WHERE tenant_id = ? AND role_key = ?')
6818
+ .get(tenantId, parsed.key);
6819
+ before = stored
6820
+ ? { key: parsed.key, permissions: JSON.parse(stored.permissions), source: stored.source, holder: stored.holder ?? undefined }
6821
+ : null;
6822
+ this.directory
6823
+ .prepare(`INSERT OR REPLACE INTO _substrat_roles (tenant_id, role_key, permissions, source, holder)
6824
+ VALUES (?, ?, ?, ?, ?)`)
6825
+ .run(tenantId, parsed.key, JSON.stringify(parsed.permissions), String(parsed.source), parsed.holder ?? 'person');
6826
+ return true;
6827
+ }, () => this.recordAdmin(actor, 'defineRole', { tenantId }, before, parsed));
6828
+ // The cache follows the commit, so a rolled-back definition is never served.
6784
6829
  this.roles.set(`${tenantId}/${parsed.key}`, { ...parsed, holder: parsed.holder ?? 'person' });
6785
- this.recordAdmin(actor, 'defineRole', { tenantId }, before, parsed);
6786
6830
  },
6787
6831
  listRoles: async (actor, filter) => {
6788
6832
  const where = [];
@@ -6827,17 +6871,18 @@ export class SqliteScopeHost {
6827
6871
  }));
6828
6872
  },
6829
6873
  assignRole: async (actor, assignment) => {
6830
- // #2430: the platform's door — a role mixing reserved and other keys is refused (K-55).
6831
- this.refuseReservedRole(assignment.node.tenantId, assignment.roleKey, 'platform');
6832
6874
  const subject = `principal:${assignment.principalId}`;
6833
- if (assignment.node.scopeId) {
6834
- const scopeNode = assignment.node.scopeId;
6835
- await onNode(assignment.node, () => writeScopeTuple(assignment.node, subject, `role:${assignment.roleKey}`, `scope:${scopeNode}`));
6836
- }
6837
- else {
6838
- writeTenantTuple(assignment.node.tenantId, subject, `role:${assignment.roleKey}`, `tenant:${assignment.node.tenantId}`);
6839
- }
6840
- this.recordAdmin(actor, 'assignRole', { tenantId: assignment.node.tenantId, scopeId: assignment.node.scopeId }, null, assignment);
6875
+ const { tenantId, scopeId } = assignment.node;
6876
+ // #2430: the platform's door — a role mixing reserved and other keys is refused (K-55).
6877
+ // Before the write opens a scope's file; synchronous with a tenant node's unit, so no
6878
+ // definition lands between the two.
6879
+ this.refuseReservedRole(tenantId, assignment.roleKey, 'platform');
6880
+ await auditedOnNode(assignment.node, () => {
6881
+ if (scopeId)
6882
+ writeScopeTuple(assignment.node, subject, `role:${assignment.roleKey}`, `scope:${scopeId}`);
6883
+ else
6884
+ writeTenantTuple(tenantId, subject, `role:${assignment.roleKey}`, `tenant:${tenantId}`);
6885
+ }, () => this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment));
6841
6886
  },
6842
6887
  unassignRole: async (actor, assignment) => {
6843
6888
  // Tombstone (K-21), never DELETE — the checker skips revoked_at rows, so the
@@ -6866,8 +6911,7 @@ export class SqliteScopeHost {
6866
6911
  // to Z text (#963), and liveness here is a lexicographic `expires_at > ?`.
6867
6912
  const grant = capabilityGrant.parse(raw);
6868
6913
  this.refuseReservedGrant([grant.permission]); // #2430
6869
- await onNode(grant.node, () => writeGrant(`principal:${grant.principalId}`, grant.permission, grant.node, grant.entity, grant.expiresAt));
6870
- this.recordAdmin(actor, 'grant', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId }, null, grant);
6914
+ 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));
6871
6915
  },
6872
6916
  grantEntityShape: async (actor, grant) => {
6873
6917
  const { tenantId, scopeId } = grant.node;
@@ -6948,29 +6992,35 @@ export class SqliteScopeHost {
6948
6992
  // row, so a refused grant is gathered by no later provision. A grant recorded before stays.
6949
6993
  this.refuseReservedGrant([grant.permission]);
6950
6994
  // Once BEFORE the turn, so a refused grant never opens (and so creates) the scope's
6951
- // file; then again inside it, with the tuple and the directory row, in ONE body on
6952
- // the scope actor (#1678) — so the tuple cannot roll back with a stranger's
6953
- // transaction and leave the directory row (#592) claiming a grant the scope lacks.
6995
+ // file; then again inside it, in ONE body on the scope actor (#1678), so the tuple
6996
+ // cannot roll back with a stranger's transaction.
6954
6997
  check();
6955
- const conn = await onNode(grant.node, () => {
6956
- const row = check();
6957
- writeGrant(subjectRef({ kind: 'connection', id: grant.connectionId }), grant.permission, grant.node, undefined, grant.expiresAt);
6958
- // #592: the directory-side record, alongside the tuple. The tuple is checked
6959
- // where it lives; this row is what provision/reconcile gather FROM, so the
6960
- // grant reaches scopes provisioned after it without a human replaying it.
6961
- this.directory
6962
- .prepare(`INSERT OR REPLACE INTO _substrat_connection_grants
6963
- (connection_id, tenant_id, vertical, permission, scope_id, expires_at,
6964
- granted_by, granted_at, revoked_at)
6965
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`)
6966
- .run(grant.connectionId, grant.node.tenantId, row.vertical, grant.permission, grant.node.scopeId ?? null, grant.expiresAt ?? null, grant.grantedBy, new Date().toISOString());
6967
- return row;
6968
- });
6969
- this.recordAdmin(actor, 'grantToConnection', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId, vertical: conn.vertical }, null, {
6970
- connectionId: grant.connectionId,
6971
- provider: conn.provider,
6972
- permission: grant.permission,
6973
- node: grant.node,
6998
+ const tuple = () => writeGrant(subjectRef({ kind: 'connection', id: grant.connectionId }), grant.permission, grant.node, undefined, grant.expiresAt);
6999
+ await onNode(grant.node, () => {
7000
+ // #2526: the check, the directory record (#592) and the admin row are one directory unit,
7001
+ // with the tuple too at the tenant node, where it is a directory row. A scope tuple lives
7002
+ // in the scope's own store and is written after the unit: if it fails, the call throws and
7003
+ // the record and its row stand without it. The Cloudflare host's reconcile re-seats a
7004
+ // tuple from the record; this host does not (#2586), so a re-grant is what delivers it.
7005
+ this.directory.transaction(() => {
7006
+ const conn = check();
7007
+ if (!grant.node.scopeId)
7008
+ tuple();
7009
+ this.directory
7010
+ .prepare(`INSERT OR REPLACE INTO _substrat_connection_grants
7011
+ (connection_id, tenant_id, vertical, permission, scope_id, expires_at,
7012
+ granted_by, granted_at, revoked_at)
7013
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)`)
7014
+ .run(grant.connectionId, grant.node.tenantId, conn.vertical, grant.permission, grant.node.scopeId ?? null, grant.expiresAt ?? null, grant.grantedBy, new Date().toISOString());
7015
+ this.recordAdmin(actor, 'grantToConnection', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId, vertical: conn.vertical }, null, {
7016
+ connectionId: grant.connectionId,
7017
+ provider: conn.provider,
7018
+ permission: grant.permission,
7019
+ node: grant.node,
7020
+ });
7021
+ })();
7022
+ if (grant.node.scopeId)
7023
+ tuple();
6974
7024
  });
6975
7025
  },
6976
7026
  grantToSystem: async (actor, raw) => {
@@ -6983,6 +7033,7 @@ export class SqliteScopeHost {
6983
7033
  const grant = systemGrant.parse(raw);
6984
7034
  const write = () => writeGrant(subjectRef({ kind: 'system', id: grant.moduleId }), grant.permission, grant.node, undefined, grant.expiresAt);
6985
7035
  const scope = grant.node.scopeId;
7036
+ const audit = () => this.recordAdmin(actor, 'grantToSystem', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId }, null, { moduleId: grant.moduleId, permission: grant.permission, node: grant.node });
6986
7037
  if (scope) {
6987
7038
  // #1666: refused while the module is switched off on this scope. The check and the
6988
7039
  // write share one synchronous body inside one turn on the scope actor, so no switch
@@ -6997,19 +7048,21 @@ export class SqliteScopeHost {
6997
7048
  }
6998
7049
  write();
6999
7050
  });
7051
+ audit();
7000
7052
  }
7001
7053
  else {
7002
7054
  // Tenant-level: a directory write, which no scope's transaction can hold. #1743:
7003
7055
  // refused while the record holds the module off on any scope (`scopesSwitchedOffFor`);
7004
- // the record and the tenant tuple share the directory, so this is one transaction.
7005
- this.directory.transaction(() => {
7056
+ // the record, the tenant tuple and (#2526) the row share the directory, so this is one
7057
+ // transaction.
7058
+ this.directoryWriteAndAudit(() => {
7006
7059
  const off = scopesSwitchedOffFor(switchSqlOf(this.directory), grant.node.tenantId, grant.moduleId);
7007
7060
  if (off.length > 0)
7008
7061
  throw substratError('conflict', tenantSystemSwitchedOffMessage(grant.moduleId, off));
7009
7062
  write();
7010
- })();
7063
+ return true;
7064
+ }, audit);
7011
7065
  }
7012
- this.recordAdmin(actor, 'grantToSystem', { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId }, null, { moduleId: grant.moduleId, permission: grant.permission, node: grant.node });
7013
7066
  },
7014
7067
  // #1666: the schedule kill switch and its lever back — `system-switch.ts` is the
7015
7068
  // whole rule, shared with the Cloudflare adapter; this is the directory check, the
@@ -7100,108 +7153,125 @@ export class SqliteScopeHost {
7100
7153
  // the write in one body, on the scope actor when the node is a scope (#1678).
7101
7154
  requireOrg(node.tenantId, orgId);
7102
7155
  this.refuseReservedGrant([permission]); // #2430: an org's members are people
7103
- await onNode(node, () => {
7156
+ await auditedOnNode(node, () => {
7104
7157
  requireOrg(node.tenantId, orgId);
7105
7158
  writeGrant(`org:${orgId}`, permission, node, entity);
7106
- });
7107
- this.recordAdmin(actor, 'grantToOrg', { tenantId: node.tenantId, scopeId: node.scopeId }, null, { orgId, permission, node, entity });
7159
+ }, () => this.recordAdmin(actor, 'grantToOrg', { tenantId: node.tenantId, scopeId: node.scopeId }, null, { orgId, permission, node, entity }));
7108
7160
  },
7109
7161
  // -- vertical + version registry (#31) ---------------------------------
7110
7162
  // -- the hostname map (K-26) -------------------------------------------
7111
7163
  bindHostname: async (actor, input) => {
7112
7164
  const parsed = bindHostnameInput.parse(input);
7113
- const scope = this.directory
7114
- .prepare('SELECT tenant_id, vertical FROM scopes WHERE scope_id = ?')
7115
- .get(parsed.scopeId);
7116
- if (!scope || scope.tenant_id !== parsed.tenantId) {
7117
- throw substratError('not_found', `unknown scope ${parsed.scopeId} in tenant ${parsed.tenantId}`);
7118
- }
7119
- const existing = this.directory
7120
- .prepare(`SELECT h.scope_id AS scope_id, s.status AS scope_status
7121
- FROM hostnames h LEFT JOIN scopes s ON s.scope_id = h.scope_id
7122
- WHERE h.hostname = ?`)
7123
- .get(parsed.hostname);
7124
- const holderReleased = existing?.scope_status === 'archived' || existing?.scope_status === 'reaped';
7125
- if (existing && existing.scope_id !== parsed.scopeId && !holderReleased) {
7126
- // A hostname is globally unique and routes to exactly one place. Silently
7127
- // rebinding it would move another tenant's traffic. Exception: the holder is
7128
- // ARCHIVED or REAPED (a deleted app, storage since wiped) — it released the
7129
- // name, so the rebind reclaims it.
7130
- throw substratError('conflict', `hostname '${parsed.hostname}' is already bound to another scope`);
7131
- }
7132
- // Exactly one canonical per (scope, surface): "which one do certs and
7133
- // redirects use" has to have one answer, so a new canonical demotes the old.
7134
- if (parsed.canonical) {
7165
+ // #2526: the scope, the holder it judges, the demotion, the bind and the row are one
7166
+ // directory unit, so a name bound elsewhere meanwhile is refused rather than moved.
7167
+ let vertical = null;
7168
+ this.directoryWriteAndAudit(() => {
7169
+ const scope = this.directory
7170
+ .prepare('SELECT tenant_id, vertical FROM scopes WHERE scope_id = ?')
7171
+ .get(parsed.scopeId);
7172
+ if (!scope || scope.tenant_id !== parsed.tenantId) {
7173
+ throw substratError('not_found', `unknown scope ${parsed.scopeId} in tenant ${parsed.tenantId}`);
7174
+ }
7175
+ vertical = scope.vertical;
7176
+ const existing = this.directory
7177
+ .prepare(`SELECT h.scope_id AS scope_id, s.status AS scope_status
7178
+ FROM hostnames h LEFT JOIN scopes s ON s.scope_id = h.scope_id
7179
+ WHERE h.hostname = ?`)
7180
+ .get(parsed.hostname);
7181
+ const holderReleased = existing?.scope_status === 'archived' || existing?.scope_status === 'reaped';
7182
+ if (existing && existing.scope_id !== parsed.scopeId && !holderReleased) {
7183
+ // A hostname is globally unique and routes to exactly one place. Silently
7184
+ // rebinding it would move another tenant's traffic. Exception: the holder is
7185
+ // ARCHIVED or REAPED (a deleted app, storage since wiped) — it released the
7186
+ // name, so the rebind reclaims it.
7187
+ throw substratError('conflict', `hostname '${parsed.hostname}' is already bound to another scope`);
7188
+ }
7189
+ // Exactly one canonical per (scope, surface): "which one do certs and
7190
+ // redirects use" has to have one answer, so a new canonical demotes the old.
7191
+ if (parsed.canonical) {
7192
+ this.directory
7193
+ .prepare('UPDATE hostnames SET canonical = 0 WHERE scope_id = ? AND surface = ?')
7194
+ .run(parsed.scopeId, parsed.surface);
7195
+ }
7135
7196
  this.directory
7136
- .prepare('UPDATE hostnames SET canonical = 0 WHERE scope_id = ? AND surface = ?')
7137
- .run(parsed.scopeId, parsed.surface);
7138
- }
7139
- this.directory
7140
- .prepare(
7141
- // INSERT OR REPLACE resets issuance columns — a (re)bind starts a fresh
7142
- // lifecycle, so any prior CF hostname id / DNS records must not linger.
7143
- `INSERT OR REPLACE INTO hostnames
7144
- (hostname, tenant_id, scope_id, vertical_slug, surface, region,
7145
- status, status_note, canonical, created_at, custom_hostname_id, validation_records)
7146
- VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?, NULL, NULL)`)
7147
- .run(parsed.hostname, parsed.tenantId, parsed.scopeId, scope.vertical, parsed.surface, parsed.region, parsed.canonical ? 1 : 0, new Date().toISOString());
7148
- this.recordAdmin(actor, 'bindHostname', { tenantId: parsed.tenantId, scopeId: parsed.scopeId, vertical: scope.vertical }, null, parsed);
7197
+ .prepare(
7198
+ // INSERT OR REPLACE resets issuance columns — a (re)bind starts a fresh
7199
+ // lifecycle, so any prior CF hostname id / DNS records must not linger.
7200
+ `INSERT OR REPLACE INTO hostnames
7201
+ (hostname, tenant_id, scope_id, vertical_slug, surface, region,
7202
+ status, status_note, canonical, created_at, custom_hostname_id, validation_records)
7203
+ VALUES (?, ?, ?, ?, ?, ?, 'pending', NULL, ?, ?, NULL, NULL)`)
7204
+ .run(parsed.hostname, parsed.tenantId, parsed.scopeId, scope.vertical, parsed.surface, parsed.region, parsed.canonical ? 1 : 0, new Date().toISOString());
7205
+ return true;
7206
+ }, () => this.recordAdmin(actor, 'bindHostname', { tenantId: parsed.tenantId, scopeId: parsed.scopeId, vertical }, null, parsed));
7149
7207
  },
7150
7208
  setHostnameStatus: async (actor, raw, status, note) => {
7151
7209
  const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
7152
- const row = this.directory
7153
- .prepare('SELECT tenant_id, scope_id, status FROM hostnames WHERE hostname = ?')
7154
- .get(hostname);
7155
- if (!row)
7156
- throw substratError('not_found', `unknown hostname '${hostname}'`);
7157
- if (row.status === status)
7158
- return; // idempotent, and a no-op is not audited
7159
- this.directory
7160
- .prepare('UPDATE hostnames SET status = ?, status_note = ? WHERE hostname = ?')
7161
- .run(status, note ?? null, hostname);
7162
- this.recordAdmin(actor, 'setHostnameStatus', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status, note: note ?? null });
7210
+ // #2526: the row's read (its `before`, and the no-op judgement), the write and the
7211
+ // admin row are one directory unit.
7212
+ let row;
7213
+ this.directoryWriteAndAudit(() => {
7214
+ row = this.directory
7215
+ .prepare('SELECT tenant_id, scope_id, status FROM hostnames WHERE hostname = ?')
7216
+ .get(hostname);
7217
+ if (!row)
7218
+ throw substratError('not_found', `unknown hostname '${hostname}'`);
7219
+ if (row.status === status)
7220
+ return false; // idempotent, and a no-op is not audited
7221
+ this.directory
7222
+ .prepare('UPDATE hostnames SET status = ?, status_note = ? WHERE hostname = ?')
7223
+ .run(status, note ?? null, hostname);
7224
+ return true;
7225
+ }, () => this.recordAdmin(actor, 'setHostnameStatus', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status, note: note ?? null }));
7163
7226
  },
7164
7227
  setHostnameIssuance: async (actor, raw, fields) => {
7165
7228
  const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
7166
- const row = this.directory
7167
- .prepare('SELECT tenant_id, scope_id, status, custom_hostname_id, validation_records FROM hostnames WHERE hostname = ?')
7168
- .get(hostname);
7169
- if (!row)
7170
- throw substratError('not_found', `unknown hostname '${hostname}'`);
7171
7229
  const recordsJson = fields.validationRecords.length
7172
7230
  ? JSON.stringify(fields.validationRecords)
7173
7231
  : null;
7174
- const idUnchanged = fields.customHostnameId === undefined || fields.customHostnameId === row.custom_hostname_id;
7175
- // A poll that changes nothing is not an event — skip it, so the reconcile sweep
7176
- // does not append a no-op admin-log row every interval.
7177
- if (row.status === fields.status &&
7178
- (row.validation_records ?? null) === recordsJson &&
7179
- idUnchanged) {
7180
- return;
7181
- }
7182
- if (fields.customHostnameId !== undefined) {
7183
- this.directory
7184
- .prepare('UPDATE hostnames SET status = ?, status_note = ?, custom_hostname_id = ?, validation_records = ? WHERE hostname = ?')
7185
- .run(fields.status, fields.note ?? null, fields.customHostnameId, recordsJson, hostname);
7186
- }
7187
- else {
7188
- this.directory
7189
- .prepare('UPDATE hostnames SET status = ?, status_note = ?, validation_records = ? WHERE hostname = ?')
7190
- .run(fields.status, fields.note ?? null, recordsJson, hostname);
7191
- }
7192
- this.recordAdmin(actor, 'setHostnameIssuance', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status: fields.status, note: fields.note ?? null });
7232
+ // #2526: the row's read (its `before`, and the no-op judgement), the write and the
7233
+ // admin row are one directory unit.
7234
+ let row;
7235
+ this.directoryWriteAndAudit(() => {
7236
+ row = this.directory
7237
+ .prepare('SELECT tenant_id, scope_id, status, custom_hostname_id, validation_records FROM hostnames WHERE hostname = ?')
7238
+ .get(hostname);
7239
+ if (!row)
7240
+ throw substratError('not_found', `unknown hostname '${hostname}'`);
7241
+ const idUnchanged = fields.customHostnameId === undefined || fields.customHostnameId === row.custom_hostname_id;
7242
+ // A poll that changes nothing is not an event — skip it, so the reconcile sweep
7243
+ // does not append a no-op admin-log row every interval.
7244
+ if (row.status === fields.status &&
7245
+ (row.validation_records ?? null) === recordsJson &&
7246
+ idUnchanged) {
7247
+ return false;
7248
+ }
7249
+ if (fields.customHostnameId !== undefined) {
7250
+ this.directory
7251
+ .prepare('UPDATE hostnames SET status = ?, status_note = ?, custom_hostname_id = ?, validation_records = ? WHERE hostname = ?')
7252
+ .run(fields.status, fields.note ?? null, fields.customHostnameId, recordsJson, hostname);
7253
+ }
7254
+ else {
7255
+ this.directory
7256
+ .prepare('UPDATE hostnames SET status = ?, status_note = ?, validation_records = ? WHERE hostname = ?')
7257
+ .run(fields.status, fields.note ?? null, recordsJson, hostname);
7258
+ }
7259
+ return true;
7260
+ }, () => this.recordAdmin(actor, 'setHostnameIssuance', { tenantId: row.tenant_id, scopeId: row.scope_id }, { status: row.status }, { status: fields.status, note: fields.note ?? null }));
7193
7261
  },
7194
7262
  unbindHostname: async (actor, raw, owner) => {
7195
7263
  const hostname = raw.toLowerCase(); // DNS is case-insensitive; the map is normalized
7196
7264
  // One statement: the row it removes is the row it audits, and an owner is judged by the
7197
7265
  // delete itself, so a name rebound to another scope in between is left alone (#2285).
7198
- const row = this.directory
7199
- .prepare(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
7200
- RETURNING tenant_id, scope_id, status`)
7201
- .get(hostname, ...(owner ? [owner.tenantId, owner.scopeId] : []));
7202
- if (!row)
7203
- return; // idempotent, and a no-op is not audited
7204
- this.recordAdmin(actor, 'unbindHostname', { tenantId: row.tenant_id, scopeId: row.scope_id }, { hostname, status: row.status }, null);
7266
+ // #2526: the delete and its admin row are one directory unit.
7267
+ let row;
7268
+ this.directoryWriteAndAudit(() => {
7269
+ row = this.directory
7270
+ .prepare(`DELETE FROM hostnames WHERE hostname = ?${owner ? ' AND tenant_id = ? AND scope_id = ?' : ''}
7271
+ RETURNING tenant_id, scope_id, status`)
7272
+ .get(hostname, ...(owner ? [owner.tenantId, owner.scopeId] : []));
7273
+ return row !== undefined; // idempotent, and a no-op is not audited
7274
+ }, () => this.recordAdmin(actor, 'unbindHostname', { tenantId: row.tenant_id, scopeId: row.scope_id }, { hostname, status: row.status }, null));
7205
7275
  },
7206
7276
  listHostnames: async (actor, filter) => {
7207
7277
  const where = [];
@@ -7505,8 +7575,7 @@ export class SqliteScopeHost {
7505
7575
  WHERE c.vertical_slug = ? AND c.channel = 'prod'`)
7506
7576
  .get(slug);
7507
7577
  if (prod?.admission_note === AUTO_ADMISSION_NOTE) {
7508
- throw substratError('conflict', `vertical '${slug}' prod version ${prod.id} is auto-admitted (private self-serve) — ` +
7509
- `a staff admit must vouch for it before listing`);
7578
+ throw autoAdmittedProdRefusal(slug, prod.id);
7510
7579
  }
7511
7580
  }
7512
7581
  return { listed: existing.listed };
@@ -7618,7 +7687,7 @@ export class SqliteScopeHost {
7618
7687
  this.recordAccess(actor, 'promotionImpact', { tenantId: null }, { verticalSlug, channel, versionId }, breaks.length);
7619
7688
  return breaks;
7620
7689
  },
7621
- promoteVersion: async (actor, verticalSlug, channel, versionId, acknowledge) => {
7690
+ promoteVersion: async (actor, verticalSlug, channel, versionId, acknowledge, options) => {
7622
7691
  const incoming = await readVersion(versionId);
7623
7692
  if (!incoming)
7624
7693
  throw substratError('not_found', `unknown version ${versionId}`);
@@ -7696,6 +7765,17 @@ export class SqliteScopeHost {
7696
7765
  .get(verticalSlug, channel);
7697
7766
  if ((now?.version_id ?? null) !== (outgoing?.id ?? null))
7698
7767
  return false;
7768
+ // #2565: the listing's rule and the builder's door, read where the listing could land.
7769
+ const listed = readVertical(verticalSlug)?.listed;
7770
+ if (listed && options?.unlistedOnly)
7771
+ throw listedPromoteRefusal();
7772
+ // A re-promote of what prod already names, and what serves (if anything does), moves
7773
+ // nothing, so it is not judged again. One whose serve would change what runs is.
7774
+ const serving = this.servingPointerOfVertical(verticalSlug)?.versionId ?? versionId;
7775
+ if (listed && channel === 'prod' && (outgoing?.id !== versionId || serving !== versionId) &&
7776
+ versionAdmission(versionId).admission_note === AUTO_ADMISSION_NOTE) {
7777
+ throw autoAdmittedProdRefusal(verticalSlug, versionId);
7778
+ }
7699
7779
  this.directory
7700
7780
  .prepare(`INSERT INTO vertical_channels (vertical_slug, channel, version_id, updated_at)
7701
7781
  VALUES (?, ?, ?, ?)
@@ -8389,21 +8469,24 @@ export class SqliteScopeHost {
8389
8469
  },
8390
8470
  createOrg: async (actor, input) => {
8391
8471
  const parsed = createOrgInput.parse(input);
8392
- if (readOrg(parsed.tenantId, parsed.id))
8393
- return; // idempotent, unaudited
8394
- // Checked explicitly rather than left to the UNIQUE index: OR IGNORE would
8395
- // swallow a collision from a DIFFERENT id and report success, silently not
8396
- // creating the org the caller asked for. Fail closed instead (as createTenant).
8397
- const slugOwner = this.directory
8398
- .prepare('SELECT org_id FROM orgs WHERE tenant_id = ? AND slug = ?')
8399
- .get(parsed.tenantId, parsed.slug);
8400
- if (slugOwner) {
8401
- throw substratError('conflict', `org slug '${parsed.slug}' already taken by ${slugOwner.org_id} (slugs are unique per tenant)`);
8402
- }
8403
- this.directory
8404
- .prepare('INSERT INTO orgs (org_id, tenant_id, slug, name, created_at) VALUES (?, ?, ?, ?, ?)')
8405
- .run(parsed.id, parsed.tenantId, parsed.slug, parsed.name, new Date().toISOString());
8406
- this.recordAdmin(actor, 'createOrg', { tenantId: parsed.tenantId }, null, parsed);
8472
+ // #2526: the checks, the insert and the row are one directory unit.
8473
+ this.directoryWriteAndAudit(() => {
8474
+ if (readOrg(parsed.tenantId, parsed.id))
8475
+ return false; // idempotent, unaudited
8476
+ // Checked explicitly rather than left to the UNIQUE index: OR IGNORE would
8477
+ // swallow a collision from a DIFFERENT id and report success, silently not
8478
+ // creating the org the caller asked for. Fail closed instead (as createTenant).
8479
+ const slugOwner = this.directory
8480
+ .prepare('SELECT org_id FROM orgs WHERE tenant_id = ? AND slug = ?')
8481
+ .get(parsed.tenantId, parsed.slug);
8482
+ if (slugOwner) {
8483
+ throw substratError('conflict', `org slug '${parsed.slug}' already taken by ${slugOwner.org_id} (slugs are unique per tenant)`);
8484
+ }
8485
+ this.directory
8486
+ .prepare('INSERT INTO orgs (org_id, tenant_id, slug, name, created_at) VALUES (?, ?, ?, ?, ?)')
8487
+ .run(parsed.id, parsed.tenantId, parsed.slug, parsed.name, new Date().toISOString());
8488
+ return true;
8489
+ }, () => this.recordAdmin(actor, 'createOrg', { tenantId: parsed.tenantId }, null, parsed));
8407
8490
  },
8408
8491
  listOrgs: async (actor, tenantId) => {
8409
8492
  const rows = this.directory
@@ -8418,12 +8501,15 @@ export class SqliteScopeHost {
8418
8501
  return o;
8419
8502
  },
8420
8503
  addMember: async (actor, tenantId, principal, orgId, opts) => {
8421
- requireOrg(tenantId, orgId);
8422
8504
  // INSERT OR REPLACE, so re-adding a revoked member clears the tombstone —
8423
8505
  // they are a member again. The add/revoke history is not lost: it lives in
8424
8506
  // the append-only admin log, which is where "what happened" belongs. The
8425
8507
  // tuple carries "what is true now" plus enough to explain a live proof.
8426
- joinOrg(actor, tenantId, principal, orgId, opts?.expiresAt ?? null);
8508
+ // #2526: the org's check, the tuple and the row are one directory unit.
8509
+ this.directory.transaction(() => {
8510
+ requireOrg(tenantId, orgId);
8511
+ joinOrg(actor, tenantId, principal, orgId, opts?.expiresAt ?? null);
8512
+ })();
8427
8513
  },
8428
8514
  applyMembership: async (actor, change) => {
8429
8515
  // ONE directory transaction, synchronous throughout (#1184): the fence (an add's), the
@@ -8534,6 +8620,8 @@ export class SqliteScopeHost {
8534
8620
  before = readTenant(tenantId);
8535
8621
  if (!before)
8536
8622
  throw substratError('not_found', `unknown tenant: ${tenantId}`);
8623
+ if (before.status === 'reaped')
8624
+ throw substratError('conflict', `tenant ${tenantId} is reaped and cannot change status`);
8537
8625
  return (status === 'deleting'
8538
8626
  ? this.directory
8539
8627
  .prepare('UPDATE tenants SET status = ?, deleting_at = ? WHERE tenant_id = ?')
@@ -8789,11 +8877,14 @@ export class SqliteScopeHost {
8789
8877
  // reason: its shadow tables cannot be replayed (the DO host rejects the name
8790
8878
  // outright), and it is recomputable — `loadDump` rebuilds it from the content
8791
8879
  // tables it just loaded. A dump carries the rows, never the index over them.
8880
+ //
8881
+ // The wire nonce store is dropped as well (RFC wire-signing §4): it describes calls this
8882
+ // store accepted, not the scope's data, so a load builds it empty.
8792
8883
  const defs = db
8793
8884
  .prepare(`SELECT name, sql FROM sqlite_master
8794
8885
  WHERE type = 'table' AND name NOT GLOB 'sqlite_*' AND sql IS NOT NULL
8795
8886
  ORDER BY name`)
8796
- .all().filter(({ name }) => !isSearchIndexTable(name));
8887
+ .all().filter(({ name }) => !isSearchIndexTable(name) && !isWireNonceTable(name));
8797
8888
  const tables = defs.map(({ name, sql }) => {
8798
8889
  // `.raw(true)` gives positional rows; cells stay as-is (blobs are Buffers, not
8799
8890
  // nulled as in a UI read) so the dump is byte-faithful. The name is from the live
@@ -9207,52 +9298,59 @@ export class SqliteScopeHost {
9207
9298
  },
9208
9299
  grantEntitlement: async (actor, tenantId, entitlementKey, plan) => {
9209
9300
  const input = entitlementGrantInput.parse(plan ?? {});
9210
- const existing = this.directory
9211
- .prepare('SELECT expires_at, quota, plan FROM _substrat_entitlements WHERE tenant_id = ? AND entitlement_key = ?')
9212
- .get(tenantId, entitlementKey);
9213
- // PATCH semantics (entitlementGrantInput): omitted preserves, null clears —
9214
- // a bare re-grant on an idempotent provisioning path must not erase a
9215
- // trial's expiry and quietly turn it perpetual.
9216
- const next = {
9217
- expiresAt: input.expiresAt !== undefined ? input.expiresAt : (existing?.expires_at ?? null),
9218
- quota: input.quota !== undefined ? input.quota : (existing?.quota ?? null),
9219
- plan: input.plan !== undefined ? input.plan : (existing?.plan ?? null),
9220
- };
9221
- // Idempotent: a grant that changes nothing is not audited.
9222
- if (existing &&
9223
- existing.expires_at === next.expiresAt &&
9224
- existing.quota === next.quota &&
9225
- existing.plan === next.plan) {
9226
- return;
9227
- }
9228
- const now = new Date().toISOString();
9229
- // granted_at/granted_by restamp on every effective change: a renewal is a
9230
- // new grant act, and the full history is the admin log below.
9231
- this.directory
9232
- .prepare(`INSERT INTO _substrat_entitlements
9233
- (tenant_id, entitlement_key, expires_at, quota, plan, granted_at, granted_by)
9234
- VALUES (?, ?, ?, ?, ?, ?, ?)
9235
- ON CONFLICT (tenant_id, entitlement_key) DO UPDATE SET
9236
- expires_at = excluded.expires_at,
9237
- quota = excluded.quota,
9238
- plan = excluded.plan,
9239
- granted_at = excluded.granted_at,
9240
- granted_by = excluded.granted_by`)
9241
- .run(tenantId, entitlementKey, next.expiresAt, next.quota, next.plan, now, actor);
9242
- this.recordAdmin(actor, 'grantEntitlement', { tenantId }, existing
9301
+ // #2526: the held grant (its `before`, and the no-op judgement), the write and the admin
9302
+ // row are one directory unit.
9303
+ let existing;
9304
+ let next = { expiresAt: null, quota: null, plan: null };
9305
+ this.directoryWriteAndAudit(() => {
9306
+ existing = this.directory
9307
+ .prepare('SELECT expires_at, quota, plan FROM _substrat_entitlements WHERE tenant_id = ? AND entitlement_key = ?')
9308
+ .get(tenantId, entitlementKey);
9309
+ // PATCH semantics (entitlementGrantInput): omitted preserves, null clears —
9310
+ // a bare re-grant on an idempotent provisioning path must not erase a
9311
+ // trial's expiry and quietly turn it perpetual.
9312
+ next = {
9313
+ expiresAt: input.expiresAt !== undefined ? input.expiresAt : (existing?.expires_at ?? null),
9314
+ quota: input.quota !== undefined ? input.quota : (existing?.quota ?? null),
9315
+ plan: input.plan !== undefined ? input.plan : (existing?.plan ?? null),
9316
+ };
9317
+ // Idempotent: a grant that changes nothing is not audited.
9318
+ if (existing &&
9319
+ existing.expires_at === next.expiresAt &&
9320
+ existing.quota === next.quota &&
9321
+ existing.plan === next.plan) {
9322
+ return false;
9323
+ }
9324
+ const now = new Date().toISOString();
9325
+ // granted_at/granted_by restamp on every effective change: a renewal is a
9326
+ // new grant act, and the full history is the admin log.
9327
+ this.directory
9328
+ .prepare(`INSERT INTO _substrat_entitlements
9329
+ (tenant_id, entitlement_key, expires_at, quota, plan, granted_at, granted_by)
9330
+ VALUES (?, ?, ?, ?, ?, ?, ?)
9331
+ ON CONFLICT (tenant_id, entitlement_key) DO UPDATE SET
9332
+ expires_at = excluded.expires_at,
9333
+ quota = excluded.quota,
9334
+ plan = excluded.plan,
9335
+ granted_at = excluded.granted_at,
9336
+ granted_by = excluded.granted_by`)
9337
+ .run(tenantId, entitlementKey, next.expiresAt, next.quota, next.plan, now, actor);
9338
+ return true;
9339
+ }, () => this.recordAdmin(actor, 'grantEntitlement', { tenantId }, existing
9243
9340
  ? { entitlementKey, expiresAt: existing.expires_at, quota: existing.quota, plan: existing.plan }
9244
- : null, { entitlementKey, ...next });
9341
+ : null, { entitlementKey, ...next }));
9245
9342
  },
9246
9343
  revokeEntitlement: async (actor, tenantId, entitlementKey) => {
9247
- const existing = this.directory
9248
- .prepare('SELECT expires_at, quota, plan FROM _substrat_entitlements WHERE tenant_id = ? AND entitlement_key = ?')
9249
- .get(tenantId, entitlementKey);
9250
- if (!existing)
9251
- return; // nothing held, nothing changed
9252
- this.directory
9253
- .prepare('DELETE FROM _substrat_entitlements WHERE tenant_id = ? AND entitlement_key = ?')
9254
- .run(tenantId, entitlementKey);
9255
- this.recordAdmin(actor, 'revokeEntitlement', { tenantId }, { entitlementKey, expiresAt: existing.expires_at, quota: existing.quota, plan: existing.plan }, null);
9344
+ // #2526: one statement removes the grant and answers what it held, which is the admin
9345
+ // row's `before`; the row commits with it.
9346
+ let removed;
9347
+ this.directoryWriteAndAudit(() => {
9348
+ removed = this.directory
9349
+ .prepare(`DELETE FROM _substrat_entitlements WHERE tenant_id = ? AND entitlement_key = ?
9350
+ RETURNING expires_at, quota, plan`)
9351
+ .get(tenantId, entitlementKey);
9352
+ return removed !== undefined; // nothing held, nothing changed
9353
+ }, () => this.recordAdmin(actor, 'revokeEntitlement', { tenantId }, { entitlementKey, expiresAt: removed.expires_at, quota: removed.quota, plan: removed.plan }, null));
9256
9354
  },
9257
9355
  listEntitlements: async (actor, tenantId) => {
9258
9356
  const grants = this.directory