@substrat-run/adapter-sqlite 0.145.0 → 0.147.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 +67 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +292 -35
- package/dist/index.js.map +1 -1
- package/dist/vertical-broker.d.ts.map +1 -1
- package/dist/vertical-broker.js +3 -1
- package/dist/vertical-broker.js.map +1 -1
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync,
|
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
4
|
import { accessLogEntry, adminLogEntry, delegatedReadParams, operationSeriesCount, opsFailureEntry, opsFailureFingerprint, issueEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunEntry, modelUsageEntry, ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, createTenantInput, domainEvent, domainEventInput, exportReadInput, importBatch, importCursorMove, exportsOfManifestJson, assertNoBindingShapeBreak, importsOfManifestJson, entitlementGrant, entitlementGrantInput, eventId, meterReading, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, 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, callsOfManifestJson, outboundOfManifestJson, listLimitOf, pageOf, countedPageOf, SCOPE_GATE_REASONS, substratError, assertReplayableDump, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, copyMarkAudit, redrainEventsInput, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
5
|
-
import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, collectPeers, peerSeats, resolveVerticalInstanceFrom, resolvePeerInstanceFrom, 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, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, 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, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, 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, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, } from '@substrat-run/kernel';
|
|
5
|
+
import { actorOf, asPrincipal, assertAllowed, assertNoSecret, assertPermissionKey, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, capabilityTokenHash, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, entityStateMigrations, assertNoCallerPurge, isUnreachableParent, heldPurgePass, purgeDueOf, purgeHeldBy, lifecycleRefusal, purgeOnlyKeysOf, purgeReportOf, purgeStillDue, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, runPurgePass, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability as exchangeCapabilitySecret, guardSecrets, mintBecomeCapability, plausibleSessionToken, readCapabilityPage, redactSecrets, redactSecretText, moduleLog, consoleLogSink, asyncInvocationId, asyncLinePass, consoleInvocationLineSink, resolveCapabilitySession, revokeCapabilityAsPlatform, assertReadOnlyQuery, attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, guardSpine, guardSqlLimits, DO_SQL_LIMITS, tooManyResultColumns, tooManyTableColumns, parseValidationRecords, resolveScopeRecord, ulid, ISSUE_RETENTION_DAYS, telemetryRetentionStatements, FINDINGS_DDL, SCOPE_STORAGE_DDL, forgetScopeStorage, listScopeStorageAttemptRows, listScopeStorageRows, listScopeStoreStorageRows, pruneScopeStorageRows, recordScopeStorageRows, CONNECT_LINKS_DDL, consumeConnectLinkRow, insertConnectLink, listConnectLinks as listConnectLinkRows, readConnectLink, restoreConnectLinkRow, revokeConnectLinkRow, createFindingRule, findingOfOpsFailure, findingOfSweepRun, pruneFindings, listFindingRules, listFindings, observeFinding, revokeFindingRule, setFindingStatus, assertRowLimit, assertRowOffset, OPS_FAILURE_RETENTION_DAYS, SWEEP_RUN_RETENTION_DAYS, SWEEP_RUNS_INTENT_INDEX, sweepRunsIntentHasKind, MODEL_USAGE_RETENTION_DAYS, foldModelUsage, backoffAt, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, eraseSubjectFromModules, moduleRowsErased, moduleErasurePlan, redactSubjectScopeText, redactSubjectDirectoryText, ISSUE_EXEMPLAR_OWNER_BACKFILL_SQL, issueExemplarOwner, JOB_RUN_PATCH_SQL, JOB_RUN_CLAIM_SQL, JOB_RUN_RENEW_SQL, JOB_RUN_BEGIN_SQL, JOB_RUN_MISS_SQL, JOB_RUN_MISS_SETTLE_SQL, admissionMissOutcome, JOB_STEP_RECORD_SQL, JOB_LEASE_EXPIRED_NOTE, assertLeaseMs, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, admitPeer, 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, splitManifestMigrations, splitVersionMigrationsBatch, versionMigrationsOf, 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, migrationSteps, planMigrations, isSearchIndexTable, searchIndexMigrations, searchIndexPlans, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, 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, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, isPrimaryScope, PROVISIONED_VERSIONS_SQL, installsReconciledOf, strandJudgmentsOf, provisionedVersionsParams, boundShapesColumn, boundShapesOfColumn, installReconciled, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, jobRunOf, runDueJobRuns, startJobRun, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, globalFetch, createUlid, facetEvents, readHistory, walkEventCause, walkEventEffects, assertRedrainWindow, readInvocation, readDeadLetters, readLifecycleFlow, readOperationSeries, domainEventOf, readUndrainedOutbox, undrainedEventsOf, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, unknownRoleError, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf, } from '@substrat-run/kernel';
|
|
6
6
|
import { attributedView, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, shapeTopUpBatch, topUpEntityGrantShapes, settlePlatformRequestIn, } from '@substrat-run/kernel';
|
|
7
7
|
import { ADMIN_LOG_INDEXES_SQL, SETTLE_INTENT_SQL, SETTLE_OUTCOME_SQL, settleOutcomeParamsOf, readAuditedOperations, unknownOutcomeOf, } from '@substrat-run/kernel';
|
|
8
8
|
import { INERT_SCOPE_REASON, isPrimaryScopeRow } from '@substrat-run/kernel';
|
|
@@ -620,6 +620,9 @@ export class SqliteScopeHost {
|
|
|
620
620
|
/** predicate name → the module-contributed implementation. Names are global. */
|
|
621
621
|
predicates = new Map();
|
|
622
622
|
/** operation names whose default binding some manifest withdrew (K-17). */
|
|
623
|
+
scopeStoreRunners;
|
|
624
|
+
scopeStoreVertical;
|
|
625
|
+
scopeStoreLifecycle;
|
|
623
626
|
withdrawn = new Map(); // operation → withdrawing module
|
|
624
627
|
relations = new Map();
|
|
625
628
|
/** entityType → its derived FTS5 index (#827). One entity type, one index. */
|
|
@@ -712,6 +715,9 @@ export class SqliteScopeHost {
|
|
|
712
715
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
713
716
|
this.clock = options.clock ?? (() => instant.parse(new Date().toISOString()));
|
|
714
717
|
this.versionId = options.versionId ?? null;
|
|
718
|
+
this.scopeStoreVertical = options.scopeStores?.vertical;
|
|
719
|
+
this.scopeStoreRunners = scopeStoreRunnersOf(options.scopeStores);
|
|
720
|
+
this.scopeStoreLifecycle = scopeStoreLifecycleOf(this.scopeStoreRunners);
|
|
715
721
|
this.logSink = options.logSink ?? consoleLogSink;
|
|
716
722
|
this.invocationLineSink = options.invocationLineSink ?? consoleInvocationLineSink;
|
|
717
723
|
// Every option is read above and here, BEFORE the directory is claimed: an option that throws
|
|
@@ -2358,6 +2364,7 @@ export class SqliteScopeHost {
|
|
|
2358
2364
|
};
|
|
2359
2365
|
}
|
|
2360
2366
|
async importScope(actor, input, dump) {
|
|
2367
|
+
this.refuseUnusableScopeStores(dump.stores, input.vertical ?? null);
|
|
2361
2368
|
// Create the destination scope (directory row + storage). Its migration step
|
|
2362
2369
|
// creates some tables; the dump then replaces them wholesale, so the end state
|
|
2363
2370
|
// is the dump, not whatever the local module set would have built. Provenance is
|
|
@@ -2370,6 +2377,7 @@ export class SqliteScopeHost {
|
|
|
2370
2377
|
});
|
|
2371
2378
|
// A fork: its callers (snapshotScope) hand it a dump the platform exported.
|
|
2372
2379
|
await this.loadDump(input.tenantId, input.scopeId, { ...dump, exact: true });
|
|
2380
|
+
await this.importScopeStores(input.tenantId, input.scopeId, dump.stores);
|
|
2373
2381
|
await this.admin.activateScope(actor, input.tenantId, input.scopeId);
|
|
2374
2382
|
this.recordAdmin(actor, 'importScope', { tenantId: input.tenantId, scopeId: input.scopeId }, null, { sourceScopeId: dump.scopeId, tables: dump.tables.length, capturedAt: dump.capturedAt });
|
|
2375
2383
|
}
|
|
@@ -2522,6 +2530,7 @@ export class SqliteScopeHost {
|
|
|
2522
2530
|
const existing = await this.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
2523
2531
|
if (!existing)
|
|
2524
2532
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2533
|
+
this.refuseUnusableScopeStores(dump.stores, existing.vertical);
|
|
2525
2534
|
// #1742: a dump from before a switch was pulled brings the module's grants back live, so
|
|
2526
2535
|
// the directory's recorded-off modules go back off in the load's own turn, as a
|
|
2527
2536
|
// provision's seat does — no schedule pass can run in between.
|
|
@@ -2530,9 +2539,17 @@ export class SqliteScopeHost {
|
|
|
2530
2539
|
// #1869: the re-point's source is the separate hint when given; the dump is otherwise as sent.
|
|
2531
2540
|
const source = { tables: dump.tables, scopeId: opts?.sourceScopeId ?? dump.scopeId };
|
|
2532
2541
|
await this.loadDump(tenantId, scopeId, source, (rt) => this.reassertSwitchesInTurn(rt, actor, tenantId, scopeId, undefined, (action, after) => rows.push([action, after])));
|
|
2542
|
+
await this.importScopeStores(tenantId, scopeId, dump.stores);
|
|
2543
|
+
const replay = await this.replayShreddedStoreErasures(actor, tenantId, scopeId, (opts?.sourceScopeId ?? dump.scopeId));
|
|
2533
2544
|
for (const [action, after] of rows)
|
|
2534
2545
|
this.recordAdmin(actor, action, { tenantId, scopeId }, null, after);
|
|
2535
|
-
this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, {
|
|
2546
|
+
this.recordAdmin(actor, 'restoreScope', { tenantId, scopeId }, null, {
|
|
2547
|
+
sourceScopeId: dump.scopeId,
|
|
2548
|
+
tables: dump.tables.length,
|
|
2549
|
+
capturedAt: dump.capturedAt,
|
|
2550
|
+
...replay,
|
|
2551
|
+
});
|
|
2552
|
+
return replay;
|
|
2536
2553
|
}
|
|
2537
2554
|
async snapshotScope(actor, tenantId, scopeId, opts) {
|
|
2538
2555
|
const source = await this.admin.getScopeRecord(actor, tenantId, scopeId);
|
|
@@ -2554,6 +2571,7 @@ export class SqliteScopeHost {
|
|
|
2554
2571
|
if (source.verticalVersionId) {
|
|
2555
2572
|
await this.admin.bindScopeVersion(actor, tenantId, snapshotId, source.verticalVersionId);
|
|
2556
2573
|
}
|
|
2574
|
+
await this.replayShreddedStoreErasures(actor, tenantId, snapshotId, scopeId);
|
|
2557
2575
|
return snapshotId;
|
|
2558
2576
|
}
|
|
2559
2577
|
async deleteSnapshot(actor, tenantId, scopeId) {
|
|
@@ -2567,6 +2585,7 @@ export class SqliteScopeHost {
|
|
|
2567
2585
|
throw substratError('forbidden', `scope ${scopeId} is not a fork or preview — only previews may be deleted; ` +
|
|
2568
2586
|
`archive a primary scope instead`);
|
|
2569
2587
|
}
|
|
2588
|
+
await this.wipeScopeStores(tenantId, scopeId);
|
|
2570
2589
|
// Close and evict the runtime handle before touching the file.
|
|
2571
2590
|
const rt = this.scopes.get(`${tenantId}/${scopeId}`);
|
|
2572
2591
|
if (rt)
|
|
@@ -2635,6 +2654,195 @@ export class SqliteScopeHost {
|
|
|
2635
2654
|
}
|
|
2636
2655
|
return this.buildStub(tenantId, scopeId, rt, asPrincipal(principal), options);
|
|
2637
2656
|
}
|
|
2657
|
+
async getScopeStore(principal, tenantId, scopeId, binding, options) {
|
|
2658
|
+
// The directory check is the same one `getScope` makes. With no runners
|
|
2659
|
+
// configured, the binding is refused after that check.
|
|
2660
|
+
await this.getScope(principal, tenantId, scopeId);
|
|
2661
|
+
if (this.scopeStoreRunners.size === 0) {
|
|
2662
|
+
throw substratError('unavailable', `no scope-store runner is configured for binding '${binding}'`);
|
|
2663
|
+
}
|
|
2664
|
+
const vertical = this.scopeStoreVerticalOf(scopeId);
|
|
2665
|
+
const runner = this.scopeStoreRunners.get(binding);
|
|
2666
|
+
if (!runner) {
|
|
2667
|
+
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2668
|
+
}
|
|
2669
|
+
await this.scopeStoreLifecycle.start();
|
|
2670
|
+
return {
|
|
2671
|
+
tenantId,
|
|
2672
|
+
scopeId,
|
|
2673
|
+
subjectKind: 'principal',
|
|
2674
|
+
invoke: (operation, input, ...extra) => observeStoreCall(binding, operation, () => callScopeStore((request) => runner.invoke({ tenantId, vertical, scopeId, binding }, request), operation, input, ...extra), options?.onStoreCall),
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
/** The scope's vertical when it has one. No vertical means this host has no store to touch. */
|
|
2678
|
+
scopeStoreVerticalIfPresent(scopeId) {
|
|
2679
|
+
const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
2680
|
+
const vertical = row?.vertical;
|
|
2681
|
+
if (!vertical)
|
|
2682
|
+
return undefined;
|
|
2683
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
|
|
2684
|
+
throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and scope ${scopeId} runs '${vertical}'`);
|
|
2685
|
+
}
|
|
2686
|
+
return vertical;
|
|
2687
|
+
}
|
|
2688
|
+
/** The scope's vertical, refused when this host is limited to another one. */
|
|
2689
|
+
scopeStoreVerticalOf(scopeId) {
|
|
2690
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2691
|
+
if (!vertical) {
|
|
2692
|
+
throw substratError('not_found', `scope ${scopeId} has no vertical, so it has no scope store`);
|
|
2693
|
+
}
|
|
2694
|
+
return vertical;
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2697
|
+
* One reserved call on a declared binding. The vertical's stub never reaches
|
|
2698
|
+
* this; `callReservedStoreOperation` refuses a name that is not reserved.
|
|
2699
|
+
*/
|
|
2700
|
+
async invokeReservedStore(address, operation, input) {
|
|
2701
|
+
const runner = this.scopeStoreRunners.get(address.binding);
|
|
2702
|
+
if (!runner)
|
|
2703
|
+
throw substratError('not_found', `no scope store is bound for '${address.binding}'`);
|
|
2704
|
+
await this.scopeStoreLifecycle.start();
|
|
2705
|
+
return callReservedStoreOperation((request) => runner.invoke(address, request), operation, input);
|
|
2706
|
+
}
|
|
2707
|
+
/**
|
|
2708
|
+
* Each declared binding's `substrat.size`. No runners means an empty list,
|
|
2709
|
+
* and the scope is not opened to discover that.
|
|
2710
|
+
*/
|
|
2711
|
+
async readScopeStoreSizes(tenantId, scopeId) {
|
|
2712
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2713
|
+
if (bindings.length === 0)
|
|
2714
|
+
return [];
|
|
2715
|
+
this.scopeReadDbFor(tenantId, scopeId);
|
|
2716
|
+
const vertical = this.scopeStoreVerticalOf(scopeId);
|
|
2717
|
+
const sizes = [];
|
|
2718
|
+
for (const binding of bindings) {
|
|
2719
|
+
const { bytes } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.size');
|
|
2720
|
+
sizes.push({ binding, bytes });
|
|
2721
|
+
}
|
|
2722
|
+
return sizes;
|
|
2723
|
+
}
|
|
2724
|
+
/** The vertical whose stores this scope has on this host, or nothing when it has none. */
|
|
2725
|
+
storeVerticalOf(scopeId) {
|
|
2726
|
+
if (this.scopeStoreRunners.size === 0)
|
|
2727
|
+
return undefined;
|
|
2728
|
+
const row = this.directory.prepare('SELECT vertical FROM scopes WHERE scope_id = ?').get(scopeId);
|
|
2729
|
+
const vertical = row?.vertical ?? undefined;
|
|
2730
|
+
if (!vertical)
|
|
2731
|
+
return undefined;
|
|
2732
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical)
|
|
2733
|
+
return undefined;
|
|
2734
|
+
return vertical;
|
|
2735
|
+
}
|
|
2736
|
+
/**
|
|
2737
|
+
* `substrat.erase` on each declared binding. A store error throws, so the caller
|
|
2738
|
+
* can leave the subject key untouched. Fail closed (#2242): an empty answer means
|
|
2739
|
+
* this host KNOWS it holds nothing for the scope. It always can tell — the
|
|
2740
|
+
* directory names the scope's vertical, and the runners are this host's own — so
|
|
2741
|
+
* unlike a Cloudflare host with no control plane it has no "cannot tell" case.
|
|
2742
|
+
*/
|
|
2743
|
+
async eraseScopeStores(tenantId, scopeId, subjectId) {
|
|
2744
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2745
|
+
// Shred calls this for every scope. No vertical names no store object, and a
|
|
2746
|
+
// vertical this host is not pinned to is one its runners never served: both hold
|
|
2747
|
+
// nothing here, and the key is still destroyed.
|
|
2748
|
+
const vertical = this.storeVerticalOf(scopeId);
|
|
2749
|
+
if (!vertical)
|
|
2750
|
+
return [];
|
|
2751
|
+
const erasures = [];
|
|
2752
|
+
for (const binding of bindings) {
|
|
2753
|
+
const { erased } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.erase', { subjectId });
|
|
2754
|
+
erasures.push({ binding, erased });
|
|
2755
|
+
}
|
|
2756
|
+
return erasures;
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* A dump's stores this host cannot load are refused before any write (#2211, L8): a
|
|
2760
|
+
* binding that is not bound, a store with no kind or a kind other than the bound runner's,
|
|
2761
|
+
* any store at all for a destination with no vertical (no store object can be named for
|
|
2762
|
+
* it, so loading would drop them), or a destination on a vertical other than the one this
|
|
2763
|
+
* host is pinned to.
|
|
2764
|
+
*/
|
|
2765
|
+
refuseUnusableScopeStores(stores, vertical) {
|
|
2766
|
+
const listed = stores ?? [];
|
|
2767
|
+
if (listed.length === 0)
|
|
2768
|
+
return;
|
|
2769
|
+
if (vertical === null) {
|
|
2770
|
+
throw substratError('conflict', 'dump carries scope stores, and the destination scope has no vertical to hold them');
|
|
2771
|
+
}
|
|
2772
|
+
if (this.scopeStoreVertical !== undefined && vertical !== this.scopeStoreVertical) {
|
|
2773
|
+
throw substratError('forbidden', `this host serves vertical '${this.scopeStoreVertical}', and the destination scope runs '${vertical}'`);
|
|
2774
|
+
}
|
|
2775
|
+
for (const store of listed) {
|
|
2776
|
+
const runner = this.scopeStoreRunners.get(store.binding);
|
|
2777
|
+
if (!runner) {
|
|
2778
|
+
throw substratError('not_found', `dump names scope store '${store.binding}', which is not bound`);
|
|
2779
|
+
}
|
|
2780
|
+
if (!store.kind) {
|
|
2781
|
+
throw substratError('validation_failed', `dump's scope store '${store.binding}' names no kind`);
|
|
2782
|
+
}
|
|
2783
|
+
if (store.kind !== runner.kind) {
|
|
2784
|
+
throw substratError('conflict', `dump's scope store '${store.binding}' is kind '${store.kind}', and this host binds kind '${runner.kind}'`);
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
async exportScopeStores(tenantId, scopeId) {
|
|
2789
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2790
|
+
if (bindings.length === 0)
|
|
2791
|
+
return [];
|
|
2792
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2793
|
+
if (!vertical)
|
|
2794
|
+
return [];
|
|
2795
|
+
const stores = [];
|
|
2796
|
+
for (const binding of bindings) {
|
|
2797
|
+
const runner = this.scopeStoreRunners.get(binding);
|
|
2798
|
+
if (!runner)
|
|
2799
|
+
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2800
|
+
const exported = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.export');
|
|
2801
|
+
stores.push({ binding, kind: runner.kind, tables: exported.tables });
|
|
2802
|
+
}
|
|
2803
|
+
return stores;
|
|
2804
|
+
}
|
|
2805
|
+
async importScopeStores(tenantId, scopeId, stores) {
|
|
2806
|
+
const listed = stores ?? [];
|
|
2807
|
+
if (listed.length === 0)
|
|
2808
|
+
return;
|
|
2809
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2810
|
+
if (!vertical) {
|
|
2811
|
+
throw substratError('conflict', `dump carries scope stores, and scope ${scopeId} has no vertical to hold them`);
|
|
2812
|
+
}
|
|
2813
|
+
for (const store of listed) {
|
|
2814
|
+
await this.invokeReservedStore({ tenantId, vertical, scopeId, binding: store.binding }, 'substrat.import', { tables: store.tables });
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* `substrat.erase` again for every tombstoned subject (#2207), on the target's and the
|
|
2819
|
+
* source's marks (`replayStoreErasures`). A target this host names no store for (no
|
|
2820
|
+
* runner, or no vertical a store is kept for) has nothing to replay into: nothing is read.
|
|
2821
|
+
*/
|
|
2822
|
+
async replayShreddedStoreErasures(actor, tenantId, target, source) {
|
|
2823
|
+
if (!this.storeVerticalOf(target))
|
|
2824
|
+
return undefined;
|
|
2825
|
+
return replayStoreErasures({
|
|
2826
|
+
admin: this.admin,
|
|
2827
|
+
actor,
|
|
2828
|
+
tenantId,
|
|
2829
|
+
target,
|
|
2830
|
+
source,
|
|
2831
|
+
erase: async (subjectId) => (await this.eraseScopeStores(tenantId, target, subjectId)).length,
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
/** `substrat.wipe` on each declared binding, before the scope's own storage goes. */
|
|
2835
|
+
async wipeScopeStores(tenantId, scopeId) {
|
|
2836
|
+
const bindings = [...this.scopeStoreRunners.keys()];
|
|
2837
|
+
if (bindings.length === 0)
|
|
2838
|
+
return;
|
|
2839
|
+
const vertical = this.scopeStoreVerticalIfPresent(scopeId);
|
|
2840
|
+
if (!vertical)
|
|
2841
|
+
return;
|
|
2842
|
+
for (const binding of bindings) {
|
|
2843
|
+
await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.wipe');
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2638
2846
|
/**
|
|
2639
2847
|
* A scope stub whose authority is a CONNECTION (#97).
|
|
2640
2848
|
*
|
|
@@ -3067,6 +3275,32 @@ export class SqliteScopeHost {
|
|
|
3067
3275
|
await this.applyPendingMigrations(rt);
|
|
3068
3276
|
return rt;
|
|
3069
3277
|
}
|
|
3278
|
+
/**
|
|
3279
|
+
* The tenant a peer name is read in (#2252): the directory's own slug for that tenant, the one
|
|
3280
|
+
* prefix a push pinned to it registers under. Never parsed out of the name being matched.
|
|
3281
|
+
*/
|
|
3282
|
+
peerTenant(tenantId) {
|
|
3283
|
+
const row = this.directory.prepare('SELECT slug FROM tenants WHERE tenant_id = ?').get(tenantId);
|
|
3284
|
+
return { id: tenantId, slug: row?.slug ?? null };
|
|
3285
|
+
}
|
|
3286
|
+
/**
|
|
3287
|
+
* Every scope of the tenant registered under a spelling of the peer name `vertical` in
|
|
3288
|
+
* `tenant` (`peerVerticalSpellings`), for the kernel's resolvers to choose among (#2252).
|
|
3289
|
+
*/
|
|
3290
|
+
peerCandidates(tenant, vertical) {
|
|
3291
|
+
const rows = this.directory
|
|
3292
|
+
.prepare(`SELECT scope_id, tenant_id, vertical, status, kind, forked_from FROM scopes
|
|
3293
|
+
WHERE tenant_id = ? AND vertical IN (SELECT value FROM json_each(?))`)
|
|
3294
|
+
.all(tenant.id, JSON.stringify(peerVerticalSpellings(vertical, tenant.slug)));
|
|
3295
|
+
return rows.map((r) => ({
|
|
3296
|
+
id: r.scope_id,
|
|
3297
|
+
tenantId: r.tenant_id,
|
|
3298
|
+
vertical: r.vertical,
|
|
3299
|
+
status: r.status,
|
|
3300
|
+
kind: r.kind ?? '',
|
|
3301
|
+
forkedFrom: r.forked_from ?? null,
|
|
3302
|
+
}));
|
|
3303
|
+
}
|
|
3070
3304
|
/** The directory's current hold, read again when background work is about to run. */
|
|
3071
3305
|
scopeWorkRefusal(tenantId, scopeId, allowProvisioning = false) {
|
|
3072
3306
|
const row = this.directory
|
|
@@ -3161,7 +3395,8 @@ export class SqliteScopeHost {
|
|
|
3161
3395
|
* here, and `admitPeer` runs inside the scope's actor task on every invoke (`buildStub`).
|
|
3162
3396
|
*/
|
|
3163
3397
|
async getVerticalScope(caller, tenantId, scopeId, options) {
|
|
3164
|
-
|
|
3398
|
+
// #2252: the caller as this vertical's `peers` names it, as on the hosted door.
|
|
3399
|
+
const parsed = peerCallerAt(this.peerDeclarations(), verticalCaller.parse(caller));
|
|
3165
3400
|
const rt = await this.openActiveScope(tenantId, scopeId);
|
|
3166
3401
|
return this.buildStub(tenantId, scopeId, rt, { kind: 'vertical-peer', caller: parsed }, options);
|
|
3167
3402
|
}
|
|
@@ -4045,10 +4280,15 @@ export class SqliteScopeHost {
|
|
|
4045
4280
|
}
|
|
4046
4281
|
}
|
|
4047
4282
|
async close() {
|
|
4048
|
-
|
|
4049
|
-
this.
|
|
4050
|
-
|
|
4051
|
-
|
|
4283
|
+
try {
|
|
4284
|
+
await this.scopeStoreLifecycle.stop();
|
|
4285
|
+
}
|
|
4286
|
+
finally {
|
|
4287
|
+
for (const rt of [...this.scopes.values()])
|
|
4288
|
+
this.closeRuntime(rt);
|
|
4289
|
+
this.directory.close();
|
|
4290
|
+
this.owner.release();
|
|
4291
|
+
}
|
|
4052
4292
|
}
|
|
4053
4293
|
// -------------------------------------------------------------------------
|
|
4054
4294
|
// Event dispatch (testrun spec §9.2.3): at-least-once, kernel-journaled,
|
|
@@ -6130,32 +6370,16 @@ export class SqliteScopeHost {
|
|
|
6130
6370
|
return rows.map(mapHostname);
|
|
6131
6371
|
},
|
|
6132
6372
|
// #1706: the directory's answer to "vertical Y in tenant T", by the kernel's one rule.
|
|
6373
|
+
// Names match EXACTLY: this is the cross-vertical edges' read (the import cursor's
|
|
6374
|
+
// producer). A peer call's tenant-pinned spelling is `resolvePeerInstance`'s (#2252).
|
|
6133
6375
|
resolveVerticalInstance: async (tenantId, vertical) => {
|
|
6134
|
-
const
|
|
6135
|
-
|
|
6136
|
-
WHERE tenant_id = ? AND vertical = ?`)
|
|
6137
|
-
.all(tenantId, vertical);
|
|
6138
|
-
return resolveVerticalInstanceFrom(rows.map((r) => ({
|
|
6139
|
-
id: r.scope_id,
|
|
6140
|
-
tenantId: r.tenant_id,
|
|
6141
|
-
vertical: r.vertical,
|
|
6142
|
-
status: r.status,
|
|
6143
|
-
kind: r.kind ?? '',
|
|
6144
|
-
forkedFrom: r.forked_from ?? null,
|
|
6145
|
-
})), tenantId, vertical);
|
|
6376
|
+
const tenant = { id: tenantId, slug: null };
|
|
6377
|
+
return resolveVerticalInstanceFrom(this.peerCandidates(tenant, vertical), tenant, vertical);
|
|
6146
6378
|
},
|
|
6147
6379
|
resolvePeerInstance: async (tenantId, callerScopeId, vertical) => {
|
|
6148
6380
|
const binding = this.directory.prepare('SELECT target_scope_id, invalidated FROM peer_bindings WHERE tenant_id = ? AND caller_scope_id = ? AND vertical = ?').get(tenantId, callerScopeId, vertical);
|
|
6149
|
-
const
|
|
6150
|
-
|
|
6151
|
-
return resolvePeerInstanceFrom(rows.map((r) => ({
|
|
6152
|
-
id: r.scope_id,
|
|
6153
|
-
tenantId: r.tenant_id,
|
|
6154
|
-
vertical: r.vertical,
|
|
6155
|
-
status: r.status,
|
|
6156
|
-
kind: r.kind ?? '',
|
|
6157
|
-
forkedFrom: r.forked_from,
|
|
6158
|
-
})), tenantId, vertical, binding?.target_scope_id ?? null, binding?.invalidated === 1);
|
|
6381
|
+
const tenant = this.peerTenant(tenantId);
|
|
6382
|
+
return resolvePeerInstanceFrom(this.peerCandidates(tenant, vertical), tenant, vertical, binding?.target_scope_id ?? null, binding?.invalidated === 1);
|
|
6159
6383
|
},
|
|
6160
6384
|
peerBinding: async (actor, tenantId, callerScopeId, vertical) => {
|
|
6161
6385
|
const row = this.directory.prepare('SELECT target_scope_id, invalidated FROM peer_bindings WHERE tenant_id = ? AND caller_scope_id = ? AND vertical = ?').get(tenantId, callerScopeId, vertical);
|
|
@@ -6174,7 +6398,7 @@ export class SqliteScopeHost {
|
|
|
6174
6398
|
}
|
|
6175
6399
|
if (targetScopeId !== null) {
|
|
6176
6400
|
const target = this.directory.prepare('SELECT tenant_id, vertical, status, kind, forked_from FROM scopes WHERE scope_id = ?').get(targetScopeId);
|
|
6177
|
-
if (!target || target.tenant_id !== tenantId || target.vertical
|
|
6401
|
+
if (!target || target.tenant_id !== tenantId || !samePeerVertical(target.vertical, vertical, this.peerTenant(tenantId).slug) || target.status !== 'active' || !isPrimaryScopeRow(target)) {
|
|
6178
6402
|
throw substratError('not_found', 'target is not a live primary instance of this vertical in this tenant');
|
|
6179
6403
|
}
|
|
6180
6404
|
}
|
|
@@ -7216,6 +7440,11 @@ export class SqliteScopeHost {
|
|
|
7216
7440
|
this.recordAccess(actor, 'scopeDatabaseSize', { tenantId, scopeId }, null, 1);
|
|
7217
7441
|
return bytes;
|
|
7218
7442
|
},
|
|
7443
|
+
scopeStoreSizes: async (actor, tenantId, scopeId) => {
|
|
7444
|
+
const sizes = await this.readScopeStoreSizes(tenantId, scopeId);
|
|
7445
|
+
this.recordAccess(actor, 'scopeStoreSizes', { tenantId, scopeId }, null, sizes.length);
|
|
7446
|
+
return sizes;
|
|
7447
|
+
},
|
|
7219
7448
|
rewindScope: async () => {
|
|
7220
7449
|
throw new Error('point-in-time rewind is not available on this host (PITR is a Durable-Object-plane mechanism) — use the backup restore path');
|
|
7221
7450
|
},
|
|
@@ -7611,7 +7840,8 @@ export class SqliteScopeHost {
|
|
|
7611
7840
|
return { name, ddl: sql, columns, rows };
|
|
7612
7841
|
});
|
|
7613
7842
|
this.recordAccess(actor, 'exportScope', { tenantId, scopeId }, null, tables.length);
|
|
7614
|
-
|
|
7843
|
+
const stores = await this.exportScopeStores(tenantId, scopeId);
|
|
7844
|
+
return { tenantId, scopeId, capturedAt: new Date().toISOString(), tables, stores };
|
|
7615
7845
|
},
|
|
7616
7846
|
// -- directory disaster recovery (#40) ----------------------------------
|
|
7617
7847
|
// The self-hosted half of the same story. A self-host has no DO point-in-time
|
|
@@ -7751,6 +7981,7 @@ export class SqliteScopeHost {
|
|
|
7751
7981
|
const rt = this.scopes.get(`${tenantId}/${scopeId}`);
|
|
7752
7982
|
if (rt)
|
|
7753
7983
|
this.closeRuntime(rt);
|
|
7984
|
+
await this.wipeScopeStores(tenantId, scopeId);
|
|
7754
7985
|
rmSync(join(this.dir, `${tenantId}__${scopeId}.sqlite`), { force: true });
|
|
7755
7986
|
// The recoverable copy the caller stored first (#493), named in the audit entry so
|
|
7756
7987
|
// the trail answers "was there a backup" without correlating two timestamps.
|
|
@@ -7778,7 +8009,19 @@ export class SqliteScopeHost {
|
|
|
7778
8009
|
},
|
|
7779
8010
|
// `stamp` is the erasure epoch an orchestrated erasure's claim compared (#1722). A direct shred
|
|
7780
8011
|
// claims nothing and records the epoch it ran under: the backfill orders erasures by them.
|
|
7781
|
-
|
|
8012
|
+
listShreddedSubjects: async (actor, tenantId, scopeId, cursor) => {
|
|
8013
|
+
this.assertScope(tenantId, scopeId);
|
|
8014
|
+
const rows = this.directory
|
|
8015
|
+
.prepare(`SELECT subject_id FROM _substrat_subject_keys
|
|
8016
|
+
WHERE scope_id = ? AND tenant_id = ? AND shredded_at IS NOT NULL AND subject_id > ?
|
|
8017
|
+
ORDER BY subject_id LIMIT 101`)
|
|
8018
|
+
.all(scopeId, tenantId, cursor ?? '');
|
|
8019
|
+
const subjects = rows.slice(0, 100).map((row) => row.subject_id);
|
|
8020
|
+
const nextCursor = rows.length > 100 ? (subjects[99] ?? null) : null;
|
|
8021
|
+
this.recordAccess(actor, 'listShreddedSubjects', { tenantId, scopeId }, null, subjects.length);
|
|
8022
|
+
return { subjects, nextCursor };
|
|
8023
|
+
},
|
|
8024
|
+
shredSubject: async (actor, tenantId, scopeId, subjectId, stamp, storeErasures) => {
|
|
7782
8025
|
this.assertScope(tenantId, scopeId);
|
|
7783
8026
|
const before = stamp ?? {
|
|
7784
8027
|
erasureEpoch: this.directory.prepare(COPY_BACKFILL_SCOPE_SQL).get(tenantId, scopeId)
|
|
@@ -7853,6 +8096,11 @@ export class SqliteScopeHost {
|
|
|
7853
8096
|
// intents, an issue's exemplar, a sweep record's error. Before the key, for the
|
|
7854
8097
|
// ordering reason above.
|
|
7855
8098
|
redactSubjectDirectoryText(redactionSqlOf(this.directory), { tenantId, scopeId, subjectId, intentIds });
|
|
8099
|
+
// Scope stores after the scope-side redaction and before the key (#2068). A store
|
|
8100
|
+
// that errors throws here, so the key below is untouched and a retry converges.
|
|
8101
|
+
// An orchestrated caller passes the serving script's result and this host does
|
|
8102
|
+
// not erase again.
|
|
8103
|
+
const erasures = storeErasures ?? (await this.eraseScopeStores(tenantId, scopeId, subjectId));
|
|
7856
8104
|
const { existed } = await this.subjectKeysFor(tenantId, scopeId).destroy(subjectId, at);
|
|
7857
8105
|
const receipt = subjectShredReceipt.parse({
|
|
7858
8106
|
subjectId,
|
|
@@ -7860,6 +8108,7 @@ export class SqliteScopeHost {
|
|
|
7860
8108
|
intentsRedacted,
|
|
7861
8109
|
jobRunsRedacted,
|
|
7862
8110
|
...vertical,
|
|
8111
|
+
storeErasures: erasures,
|
|
7863
8112
|
keyDestroyed: existed,
|
|
7864
8113
|
tombstoned: true,
|
|
7865
8114
|
});
|
|
@@ -7880,7 +8129,7 @@ export class SqliteScopeHost {
|
|
|
7880
8129
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
7881
8130
|
return row.erasure_epoch;
|
|
7882
8131
|
},
|
|
7883
|
-
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected) => {
|
|
8132
|
+
finalizeSubjectShred: async (actor, tenantId, scopeId, subjectId, redactions, expected, storeErasures) => {
|
|
7884
8133
|
if (redactions.length === 0)
|
|
7885
8134
|
throw substratError('conflict', 'subject erasure has no confirmed scope redaction');
|
|
7886
8135
|
const claimed = this.directory.prepare(`UPDATE scopes SET erasure_epoch = COALESCE(erasure_epoch, 0) + 1
|
|
@@ -7896,7 +8145,7 @@ export class SqliteScopeHost {
|
|
|
7896
8145
|
// The pure adapter has one co-located scope store. Its existing atomic shred is
|
|
7897
8146
|
// the final local redaction and key destruction, after remote copies confirmed.
|
|
7898
8147
|
const shred = this.admin.shredSubject;
|
|
7899
|
-
return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' });
|
|
8148
|
+
return shred(actor, tenantId, scopeId, subjectId, { erasureEpoch: expected.epoch, path: 'orchestrated' }, storeErasures);
|
|
7900
8149
|
},
|
|
7901
8150
|
// -- impersonation (K-42, #868) ----------------------------------------
|
|
7902
8151
|
beginImpersonation: async (actor, input) => {
|
|
@@ -7972,6 +8221,7 @@ export class SqliteScopeHost {
|
|
|
7972
8221
|
'_substrat_finding_rules', // #1748: the tenant's suppress rules
|
|
7973
8222
|
'_substrat_scope_storage', // #1524: storage samples of scopes whose storage is gone
|
|
7974
8223
|
'_substrat_scope_storage_attempts', // #1524: and the phase's last try at each
|
|
8224
|
+
'_substrat_scope_store_storage', // #2212: scope-store samples, gone with the tenant
|
|
7975
8225
|
];
|
|
7976
8226
|
const clear = this.directory.transaction(() => {
|
|
7977
8227
|
for (const table of tables) {
|
|
@@ -8080,6 +8330,7 @@ export class SqliteScopeHost {
|
|
|
8080
8330
|
// #1524: the stored gauge's latest sample per non-reaped scope. A directory read only.
|
|
8081
8331
|
storage: listScopeStorageRows(redactionSqlOf(this.directory), { tenantId: only, latest: true }),
|
|
8082
8332
|
storageAttempts: listScopeStorageAttemptRows(redactionSqlOf(this.directory), only),
|
|
8333
|
+
storeStorage: listScopeStoreStorageRows(redactionSqlOf(this.directory), only),
|
|
8083
8334
|
});
|
|
8084
8335
|
// The count that matters for K-24 is how many TENANTS this reading covered —
|
|
8085
8336
|
// "read the meter for one tenant" and "metered the whole fleet" are different
|
|
@@ -9507,6 +9758,8 @@ export class SqliteScopeHost {
|
|
|
9507
9758
|
// envelope data module code could set or branch on.
|
|
9508
9759
|
rt.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
|
|
9509
9760
|
};
|
|
9761
|
+
// #2192: the call's invocation id, one reader for `ctx.log` and `ctx.requestId`.
|
|
9762
|
+
const requestIdNow = () => rt.invocationId ?? delivery?.invocationId ?? null;
|
|
9510
9763
|
const ctxRef = {
|
|
9511
9764
|
tenantId: rt.tenantId,
|
|
9512
9765
|
scopeId: rt.scopeId,
|
|
@@ -9526,13 +9779,17 @@ export class SqliteScopeHost {
|
|
|
9526
9779
|
tenantId: rt.tenantId,
|
|
9527
9780
|
scopeId: rt.scopeId,
|
|
9528
9781
|
operation: operation ?? null,
|
|
9529
|
-
invocationId:
|
|
9782
|
+
invocationId: requestIdNow,
|
|
9530
9783
|
// A consumer runs under the system override, so it logs as `system`.
|
|
9531
9784
|
principalKind: overrideActor ? 'system' : subject.kind,
|
|
9532
9785
|
// The string-safe redaction: `redactSecrets` parses its serialization back, and a
|
|
9533
9786
|
// log's text is not JSON.
|
|
9534
9787
|
redact: (text) => redactSecretText(text, minted),
|
|
9535
9788
|
}, this.logSink),
|
|
9789
|
+
// #2192: the same id the log lines carry.
|
|
9790
|
+
get requestId() {
|
|
9791
|
+
return requestIdNow();
|
|
9792
|
+
},
|
|
9536
9793
|
emit: (event) => writeEvent(event, 'module'),
|
|
9537
9794
|
requestPlatform: (request) => {
|
|
9538
9795
|
assertImpersonationWrites(impersonation, 'ctx.requestPlatform');
|