@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.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +289 -191
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
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
|
-
|
|
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
|
-
|
|
6779
|
-
|
|
6780
|
-
this.
|
|
6781
|
-
.
|
|
6782
|
-
|
|
6783
|
-
|
|
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
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
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
|
|
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,
|
|
6952
|
-
//
|
|
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
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
//
|
|
6959
|
-
//
|
|
6960
|
-
//
|
|
6961
|
-
this.
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
.
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
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
|
|
7005
|
-
|
|
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
|
|
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
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7120
|
-
|
|
7121
|
-
|
|
7122
|
-
|
|
7123
|
-
.
|
|
7124
|
-
|
|
7125
|
-
|
|
7126
|
-
|
|
7127
|
-
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
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(
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
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
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
.
|
|
7162
|
-
|
|
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
|
-
|
|
7175
|
-
//
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
|
|
7179
|
-
|
|
7180
|
-
|
|
7181
|
-
|
|
7182
|
-
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7187
|
-
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
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
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
|
|
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
|
|
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
|
-
|
|
8393
|
-
|
|
8394
|
-
|
|
8395
|
-
|
|
8396
|
-
|
|
8397
|
-
|
|
8398
|
-
|
|
8399
|
-
|
|
8400
|
-
|
|
8401
|
-
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
.
|
|
8406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
|
|
9217
|
-
|
|
9218
|
-
|
|
9219
|
-
|
|
9220
|
-
|
|
9221
|
-
|
|
9222
|
-
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
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
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
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
|