@substrat-run/adapter-cloudflare 0.136.0 → 0.137.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/checker.d.ts.map +1 -1
- package/dist/checker.js +3 -1
- package/dist/checker.js.map +1 -1
- package/dist/control-plane-do.d.ts +25 -27
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +106 -14
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +109 -9
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +423 -111
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts +2 -0
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +271 -47
- package/dist/scope-do.js.map +1 -1
- package/dist/system-door.d.ts +21 -0
- package/dist/system-door.d.ts.map +1 -0
- package/dist/system-door.js +13 -0
- package/dist/system-door.js.map +1 -0
- package/package.json +9 -9
package/dist/scope-do.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface ScopeDoEnv {
|
|
|
32
32
|
*/
|
|
33
33
|
SUBSTRAT_VERSION_ID?: string;
|
|
34
34
|
}
|
|
35
|
+
/** #2016: how a tenant reads against a scope's record (`ScopeDO.tenantVerdict`). */
|
|
36
|
+
export type TenantVerdict = 'recorded' | 'inferred' | 'foreign' | 'unknown';
|
|
35
37
|
/**
|
|
36
38
|
* What a capability attachment verb (#1686) answers: its value, or its failure as DATA —
|
|
37
39
|
* `invoke`'s envelope discipline, since a throw across the RPC keeps only its message.
|
package/dist/scope-do.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAyCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAuCL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAsFtB,MAAM,sBAAsB,CAAC;AAgG9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAwED,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAqhB5E;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI;IAAE,KAAK,EAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,WAAW,CAAA;CAAE,CAAC;AAgDxG;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAwDxD;AAkBD,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAs2LjE"}
|
package/dist/scope-do.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { REWIND_REFUSED } from './rewind-refusal.js';
|
|
2
|
+
import { SYSTEM_DOOR_MOVED } from './system-door.js';
|
|
2
3
|
import { DurableObject } from 'cloudflare:workers';
|
|
3
4
|
import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, requestFingerprint, substratError, errorCodeOf, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
|
|
4
|
-
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL,
|
|
5
|
+
import { ulid, DO_SQL_LIMITS, unknownRoleError, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, redactSubjectScopeText, assertRowLimit, assertRowOffset, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, recordAttachmentText, searchAttachments, IDEMPOTENCY_DDL, REFUSALS_TABLE_DDL, REFUSALS_INDEX, refusalInsert, refusalOf, markGuardRefusal, REFUSALS_REBUILD, refusalsAdmitGuards, assertIdempotencyKey, assertPermissionKey, idempotencyLookupQuery, idempotencyPruneStatement, idempotencyRecordStatement, idempotencyOptedOutMessage, replayFor, entityVersionQuery, entityVersionOf, assertIfMatch, OUTBOX_ENTITY_INDEX, SCHEDULE_STATE_DDL, SCHEDULE_STATE_REBUILD, scheduleStateHasKind, VERTICAL_EVENTS_DDL, EXPORT_HOPS_SQL, IMPORT_CURSORS_SQL, IMPORT_CURSOR_OF_SQL, OUTBOX_MARK_SQL, exportedSinceQuery, emittedSinceQuery, emittedReportOf, EMITTED_REPORT_CAP, IMPORT_CURSOR_ADVANCE_SQL, IMPORT_RECORD_SQL, moveImportCursor, CrossVerticalRegistry, exportReadPlan, exportReadQuery, planExportBatch, withheldNote, JOB_RUN_DDL, JOB_RUN_DUE_AT, jobRunListLimit, SYSTEM_DOOR_WAIT, } from '@substrat-run/kernel';
|
|
5
6
|
import { isUpgradeRequest, readSubscription, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, } from './live-reads.js';
|
|
6
7
|
import { OperationQueue } from './serialization.js';
|
|
7
8
|
import { doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
|
|
@@ -15,6 +16,21 @@ import { CARRIED_AWAY_KEY, COPY_MARK_CLEARED_KEY, KEPT_COPY_REFUSAL, KEPT_DIVERG
|
|
|
15
16
|
* without asking the directory.
|
|
16
17
|
*/
|
|
17
18
|
const REAPED_MARKER = '_substrat_reaped';
|
|
19
|
+
/**
|
|
20
|
+
* The `_substrat_meta` key of the tenant a scope was provisioned for (#1738): written by the first
|
|
21
|
+
* projection, never re-pointed, and what every door's pair check holds a request to (#2016).
|
|
22
|
+
*/
|
|
23
|
+
const PROVISIONED_FOR_KEY = 'provisioned_for';
|
|
24
|
+
/** #2016: the refusal of a write that would re-point a scope's tenant, in one wording. */
|
|
25
|
+
const tenantReceiptRefusal = (held, asked) => `refused: this scope was provisioned for tenant ${held}, not ${asked}`;
|
|
26
|
+
/** #2016: the spine tables whose rows say a scope holds state (`ScopeDO.holdsData`). */
|
|
27
|
+
const HOLDS_DATA_TABLES = ['_substrat_migrations', '_substrat_tuples', '_substrat_outbox', '_substrat_schedule_state'];
|
|
28
|
+
/** #2016: why `tenantId` is foreign to a scope (`held`, the receipt `tenantVerdict` read), in one wording. */
|
|
29
|
+
const foreignTenant = (held, tenantId) => held !== null
|
|
30
|
+
? tenantReceiptRefusal(held, tenantId)
|
|
31
|
+
: `refused: this scope holds role rows for another tenant and none for ${tenantId}`;
|
|
32
|
+
/** #2016: the load refusals `tenantReceiptRefusal` raised, told apart by identity, not by text. */
|
|
33
|
+
const tenantRefusals = new WeakSet();
|
|
18
34
|
/**
|
|
19
35
|
* The scope spine, as this adapter builds it — one of two hand-written copies (#969).
|
|
20
36
|
*
|
|
@@ -322,7 +338,8 @@ const KERNEL_DDL = `
|
|
|
322
338
|
-- #116: the request-dedupe table, kernel-owned so no vertical migrates for it.
|
|
323
339
|
${IDEMPOTENCY_DDL}
|
|
324
340
|
-- #1745: refused transitions, recorded after the rollback like a denial. Kernel-owned.
|
|
325
|
-
${
|
|
341
|
+
${REFUSALS_TABLE_DDL}
|
|
342
|
+
${REFUSALS_INDEX}
|
|
326
343
|
-- #1672: capabilities — authority carried by a secret (a link share), and the sessions
|
|
327
344
|
-- an exchange trades that secret for. Shared with the pure adapter from
|
|
328
345
|
-- @substrat-run/kernel so the two cannot part company; the column comments are there.
|
|
@@ -644,6 +661,15 @@ export function splitSqlStatements(sql) {
|
|
|
644
661
|
out.push(cur.trim());
|
|
645
662
|
return out;
|
|
646
663
|
}
|
|
664
|
+
/**
|
|
665
|
+
* #1834: the pin missed. Thrown only by `assertSystemDoor`, and module-private, so no operation can
|
|
666
|
+
* throw one: the two RPCs a door call reaches turn it into the `SystemDoorMoved` ANSWER, by class
|
|
667
|
+
* identity, and an operation's own error, whatever its text, stays a failure.
|
|
668
|
+
*/
|
|
669
|
+
class SystemDoorMovedError extends Error {
|
|
670
|
+
}
|
|
671
|
+
/** #1834: the brand only `assertSystemDoor` sets, so nothing else can make a `SystemDoorPass`. */
|
|
672
|
+
const SYSTEM_DOOR_PASSED = Symbol('system door passed');
|
|
647
673
|
export function defineScopeDO(modules, bareOps) {
|
|
648
674
|
return class ScopeDO extends DurableObject {
|
|
649
675
|
sql;
|
|
@@ -1106,25 +1132,76 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1106
1132
|
* pair check), and a stray role row can no longer make a scope serve a tenant it was never
|
|
1107
1133
|
* provisioned for.
|
|
1108
1134
|
*
|
|
1109
|
-
* A scope with NO receipt was provisioned before it existed
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1113
|
-
* (reconcile, or the restore's repair) writes the receipt, after which
|
|
1114
|
-
* never consulted again.
|
|
1135
|
+
* A scope with NO receipt was provisioned before it existed (a load never brings the dump's:
|
|
1136
|
+
* it describes the scope the dump came from, and the store keeps its own, #2016). Only then
|
|
1137
|
+
* is the old inference used: `provisionScopeLocal` projected role definitions under the
|
|
1138
|
+
* tenant, so a `_substrat_roles` row for it says the same thing. The next projection
|
|
1139
|
+
* (reconcile, or the restore's repair) or lifecycle delivery writes the receipt, after which
|
|
1140
|
+
* the inference is never consulted again.
|
|
1115
1141
|
*
|
|
1116
1142
|
* Read without migrating, so asking about a foreign scope leaves its DO as empty as it found it.
|
|
1117
1143
|
*/
|
|
1118
1144
|
async servesTenant(tenantId) {
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1145
|
+
const { verdict } = this.tenantVerdict(tenantId);
|
|
1146
|
+
return verdict === 'recorded' || verdict === 'inferred';
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* #2016: what one door into this scope needs from its storage on a CP-less host, in ONE call —
|
|
1150
|
+
* how `tenantId` reads against the scope (`tenantVerdict`), and the lifecycle the platform
|
|
1151
|
+
* delivered (#1713). The coordinator refuses a foreign pair before any guard, handler or store
|
|
1152
|
+
* lookup runs, and reports an `unknown` one it lets through. Read without migrating, like
|
|
1153
|
+
* `servesTenant`.
|
|
1154
|
+
*/
|
|
1155
|
+
admission(tenantId) {
|
|
1156
|
+
return { verdict: this.tenantVerdict(tenantId).verdict, lifecycle: readLifecycle(this.switchSql()) };
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* #1738 / #2016: this scope's tenant against `tenantId` — the one reading behind the served-here
|
|
1160
|
+
* gate, every door's pair check and the lifecycle delivery's back-fill.
|
|
1161
|
+
*
|
|
1162
|
+
* - `recorded`: the `provisioned_for` receipt names `tenantId`.
|
|
1163
|
+
* - `foreign`: the receipt names another tenant, or — on a scope provisioned before receipts
|
|
1164
|
+
* existed — the scope holds role rows and none of them is `tenantId`'s.
|
|
1165
|
+
* - `inferred`: no receipt, and a role row for `tenantId` (what `provisionScopeLocal` projected
|
|
1166
|
+
* before the receipt existed).
|
|
1167
|
+
* - `unknown`: no receipt and no role rows — never provisioned here, or loaded from a world
|
|
1168
|
+
* that keeps its roles elsewhere and not yet repaired. Nothing to hold the pair against.
|
|
1169
|
+
*
|
|
1170
|
+
* `held` is the receipt read, so a refusal can name it. Read without migrating, so asking about
|
|
1171
|
+
* a foreign scope leaves its DO as empty as it found it.
|
|
1172
|
+
*/
|
|
1173
|
+
tenantVerdict(tenantId) {
|
|
1174
|
+
const held = this.provisionedFor();
|
|
1175
|
+
if (held !== null)
|
|
1176
|
+
return { verdict: held === tenantId ? 'recorded' : 'foreign', held };
|
|
1177
|
+
if (!this.hasTable('_substrat_roles'))
|
|
1178
|
+
return { verdict: 'unknown', held };
|
|
1179
|
+
if (this.sql.exec('SELECT 1 FROM _substrat_roles WHERE tenant_id = ? LIMIT 1', tenantId).toArray().length > 0) {
|
|
1180
|
+
return { verdict: 'inferred', held };
|
|
1124
1181
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1182
|
+
const anyRole = this.sql.exec('SELECT 1 FROM _substrat_roles LIMIT 1').toArray().length > 0;
|
|
1183
|
+
return { verdict: anyRole ? 'foreign' : 'unknown', held };
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* #2016: whether a scope with no receipt and no role rows holds state — rather than being a DO
|
|
1187
|
+
* nothing ever provisioned or loaded here. Only such a scope takes its tenant from a lifecycle
|
|
1188
|
+
* delivery: recording one in an empty DO would make `servesTenant` answer for a scope this
|
|
1189
|
+
* deployment does not hold.
|
|
1190
|
+
*
|
|
1191
|
+
* Every provision since #1738 writes the receipt itself (that IS the durable provision marker),
|
|
1192
|
+
* so this only ever judges a scope from before it, or a load from an older platform. A module
|
|
1193
|
+
* with no SQL migrations still leaves spine state, so any of it counts, not migrations alone:
|
|
1194
|
+
* applied migrations, scope tuples (its grants, `system:` ones included), events, schedule state.
|
|
1195
|
+
*/
|
|
1196
|
+
holdsData() {
|
|
1197
|
+
return HOLDS_DATA_TABLES.some((table) => this.hasTable(table) && this.sql.exec(`SELECT 1 FROM ${table} LIMIT 1`).toArray().length > 0);
|
|
1198
|
+
}
|
|
1199
|
+
/** #1738: the tenant this scope's `provisioned_for` receipt names, or null; read without migrating. */
|
|
1200
|
+
provisionedFor() {
|
|
1201
|
+
return this.hasTable('_substrat_meta') ? this.metaValue(PROVISIONED_FOR_KEY) : null;
|
|
1202
|
+
}
|
|
1203
|
+
hasTable(name) {
|
|
1204
|
+
return this.sql.exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?`, name).toArray().length > 0;
|
|
1128
1205
|
}
|
|
1129
1206
|
/** #1705 PR 2: the outbox's insertion mark (`OUTBOX_MARK_SQL`). */
|
|
1130
1207
|
outboxMark() {
|
|
@@ -1625,21 +1702,33 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1625
1702
|
* `vertical.honoured`: an old DO that ignored this argument would run the call as that
|
|
1626
1703
|
* placeholder, and the coordinator refuses a success without the acknowledgement.
|
|
1627
1704
|
*/
|
|
1628
|
-
verticalCaller
|
|
1705
|
+
verticalCaller,
|
|
1706
|
+
/**
|
|
1707
|
+
* #1834: set on a system-door call — the instance whose state read the door's gate
|
|
1708
|
+
* consulted the rewind hold against. On any other instance nothing runs and the answer is
|
|
1709
|
+
* `systemDoorMoved` instead, because a PITR restore always restarts this object, so another
|
|
1710
|
+
* instance may be rewound storage the gate never saw. The reply carries `systemDoor.honoured`.
|
|
1711
|
+
*/
|
|
1712
|
+
systemDoorInstance) {
|
|
1629
1713
|
if (!failureEnvelope) {
|
|
1630
1714
|
// Legacy path, byte-for-byte what it was: rewrapped so a non-plain error (a
|
|
1631
1715
|
// ZodError, whose `message` is a getter) still arrives with its message.
|
|
1632
1716
|
try {
|
|
1633
|
-
return await this.invokeOrThrow(operation, input, principal, tenantId, scopeId, connectionId, requiredEntitlement, systemModuleId, invokeOptions, impersonation, capabilitySession, verticalCaller);
|
|
1717
|
+
return await this.invokeOrThrow(operation, input, principal, tenantId, scopeId, connectionId, requiredEntitlement, systemModuleId, invokeOptions, impersonation, capabilitySession, verticalCaller, systemDoorInstance);
|
|
1634
1718
|
}
|
|
1635
1719
|
catch (err) {
|
|
1720
|
+
if (err instanceof SystemDoorMovedError)
|
|
1721
|
+
return { result: undefined, platformRequests: 0, ...SYSTEM_DOOR_MOVED };
|
|
1636
1722
|
throw toRpcError(err);
|
|
1637
1723
|
}
|
|
1638
1724
|
}
|
|
1639
1725
|
try {
|
|
1640
|
-
return await this.invokeOrThrow(operation, input, principal, tenantId, scopeId, connectionId, requiredEntitlement, systemModuleId, invokeOptions, impersonation, capabilitySession, verticalCaller);
|
|
1726
|
+
return await this.invokeOrThrow(operation, input, principal, tenantId, scopeId, connectionId, requiredEntitlement, systemModuleId, invokeOptions, impersonation, capabilitySession, verticalCaller, systemDoorInstance);
|
|
1641
1727
|
}
|
|
1642
1728
|
catch (err) {
|
|
1729
|
+
// #1834: the pin missed — an answer, never a failure an operation could have produced.
|
|
1730
|
+
if (err instanceof SystemDoorMovedError)
|
|
1731
|
+
return { result: undefined, platformRequests: 0, ...SYSTEM_DOOR_MOVED };
|
|
1643
1732
|
// The ONE place the error keeps its structure: flattened here, rebuilt by the
|
|
1644
1733
|
// coordinator. `toRpcError` is not applied — that exists to make a throw
|
|
1645
1734
|
// survivable, and this is not a throw.
|
|
@@ -1653,7 +1742,9 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1653
1742
|
/** #1672: a capability session's hash. See `invoke` above. */
|
|
1654
1743
|
capabilitySession,
|
|
1655
1744
|
/** #1706: the calling vertical, as the platform named it. See `invoke` above. */
|
|
1656
|
-
verticalCaller
|
|
1745
|
+
verticalCaller,
|
|
1746
|
+
/** #1834: the instance the system door's gate read. See `invoke` above. */
|
|
1747
|
+
systemDoorInstance) {
|
|
1657
1748
|
await this.ensureMigrations();
|
|
1658
1749
|
const handler = this.operations.get(operation);
|
|
1659
1750
|
// `not_found`, not a bare throw (#113): every vertical hand-matched this message
|
|
@@ -1736,6 +1827,9 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1736
1827
|
// one call. Same placement as the SQLite adapter's actor task, for this reason.
|
|
1737
1828
|
this.invocationId = invokeOptions?.invocationId ?? null;
|
|
1738
1829
|
try {
|
|
1830
|
+
// #1834: before anything opens. This instance is the storage the call meets, for the
|
|
1831
|
+
// whole call: a restore restarts the object, so it cannot move under a running body.
|
|
1832
|
+
const systemDoor = this.assertSystemDoor(systemModuleId, systemDoorInstance);
|
|
1739
1833
|
// #1672: the capability session, resolved on EVERY call and here — inside the queued
|
|
1740
1834
|
// body, the one region where this call holds the DO to itself — so nothing can
|
|
1741
1835
|
// revoke between this read and the transaction. Refuses a stale session, a revoked
|
|
@@ -1810,7 +1904,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1810
1904
|
// emitted events back as one — verified across `await` in workerd.
|
|
1811
1905
|
try {
|
|
1812
1906
|
await this.revision.transaction(async () => {
|
|
1813
|
-
const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId,
|
|
1907
|
+
const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, systemDoor, signals, impersonation, operation, capabilityId, minted, undefined, peerSubject);
|
|
1814
1908
|
// #116: a retry is answered from the recording, and nothing else runs
|
|
1815
1909
|
// — not the guards, not the handler, not the permission check inside
|
|
1816
1910
|
// it. Keyed by SUBJECT, so a caller only ever reaches its own
|
|
@@ -1878,6 +1972,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1878
1972
|
impersonation: { honoured: true },
|
|
1879
1973
|
...(capabilitySession !== undefined ? { capability: { honoured: true } } : {}),
|
|
1880
1974
|
...(verticalCaller !== undefined ? { vertical: { honoured: true } } : {}),
|
|
1975
|
+
...(systemDoorInstance !== undefined ? { systemDoor: { honoured: true } } : {}),
|
|
1881
1976
|
...(idempotencyKey !== undefined
|
|
1882
1977
|
? { idempotency: { keyHonoured: true, replayed } }
|
|
1883
1978
|
: {}),
|
|
@@ -1942,6 +2037,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
1942
2037
|
...(capabilitySession !== undefined ? { capability: { honoured: true } } : {}),
|
|
1943
2038
|
// #1706: the same, for the peer door.
|
|
1944
2039
|
...(verticalCaller !== undefined ? { vertical: { honoured: true } } : {}),
|
|
2040
|
+
// #1834: the same, for the system door's instance pin.
|
|
2041
|
+
...(systemDoorInstance !== undefined ? { systemDoor: { honoured: true } } : {}),
|
|
1945
2042
|
// The acknowledgement the coordinator's skew check reads (#116), on the
|
|
1946
2043
|
// same reasoning as `ifMatchChecked` below and with a sharper failure: a
|
|
1947
2044
|
// DO too old to know about keys would EXECUTE THE OPERATION AGAIN and
|
|
@@ -2383,8 +2480,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2383
2480
|
return record;
|
|
2384
2481
|
}
|
|
2385
2482
|
/** Authorize a job's read as its registered module, never as a person. */
|
|
2386
|
-
async systemAttachmentAuthorize(attachmentId, moduleId, tenantId, scopeId
|
|
2483
|
+
async systemAttachmentAuthorize(attachmentId, moduleId, tenantId, scopeId,
|
|
2484
|
+
/** #1834: the instance the system door's gate read; see `invoke`. */
|
|
2485
|
+
systemDoorInstance) {
|
|
2387
2486
|
await this.ensureMigrations();
|
|
2487
|
+
let systemDoor;
|
|
2488
|
+
try {
|
|
2489
|
+
systemDoor = this.assertSystemDoor(moduleId, systemDoorInstance);
|
|
2490
|
+
}
|
|
2491
|
+
catch (err) {
|
|
2492
|
+
if (err instanceof SystemDoorMovedError)
|
|
2493
|
+
return SYSTEM_DOOR_MOVED;
|
|
2494
|
+
throw toRpcError(err);
|
|
2495
|
+
}
|
|
2388
2496
|
if (!this.modules.has(moduleId)) {
|
|
2389
2497
|
throw toRpcError(substratError('not_found', `module not registered in this scope: ${moduleId}`));
|
|
2390
2498
|
}
|
|
@@ -2393,7 +2501,7 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2393
2501
|
return null;
|
|
2394
2502
|
const gate = this.attachmentGate(record.entity.entityType);
|
|
2395
2503
|
try {
|
|
2396
|
-
const ctx = this.operationContext(this.systemPrincipal, tenantId, scopeId, undefined, undefined,
|
|
2504
|
+
const ctx = this.operationContext(this.systemPrincipal, tenantId, scopeId, undefined, undefined, systemDoor, undefined, undefined, 'attachments.open');
|
|
2397
2505
|
assertAllowed(await ctx.check(gate.read, record.entity));
|
|
2398
2506
|
}
|
|
2399
2507
|
catch (err) {
|
|
@@ -2585,6 +2693,41 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2585
2693
|
async systemScheduleState(moduleId) {
|
|
2586
2694
|
return systemScheduleState(this.switchSql(), moduleId, new Date().toISOString());
|
|
2587
2695
|
}
|
|
2696
|
+
/**
|
|
2697
|
+
* #1834: the system door's state read — `systemScheduleState`, and the instance that
|
|
2698
|
+
* answered it. The door consults the rewind hold after this read and pins every call it
|
|
2699
|
+
* then makes to this instance (`assertSystemDoor`).
|
|
2700
|
+
*/
|
|
2701
|
+
async systemDoorState(moduleId) {
|
|
2702
|
+
return { state: await this.systemScheduleState(moduleId), instance: this.instanceId };
|
|
2703
|
+
}
|
|
2704
|
+
/**
|
|
2705
|
+
* #1834: the system door's check on this side, and the ONLY maker of the `SystemDoorPass`
|
|
2706
|
+
* the operation context needs to act as `system:<moduleId>`. A call acting as a module must
|
|
2707
|
+
* carry the instance the host's door gate read (refused without one: no door gated it), and
|
|
2708
|
+
* is refused on any other instance. A PITR restore always restarts this object, so a call
|
|
2709
|
+
* landing on a new instance may meet rewound storage whose module the gate never checked
|
|
2710
|
+
* against the rewind hold. Both are refused before anything opens; the moved one is the
|
|
2711
|
+
* module-private `SystemDoorMovedError`, which the RPC answers as `SystemDoorMoved`, so the door
|
|
2712
|
+
* gates again and retries. No module, no pass.
|
|
2713
|
+
*/
|
|
2714
|
+
assertSystemDoor(moduleId, expected) {
|
|
2715
|
+
if (moduleId === undefined)
|
|
2716
|
+
return undefined;
|
|
2717
|
+
// Strict, and deliberately so: no pin means no door gated this call, so it is refused rather
|
|
2718
|
+
// than run unchecked. The one innocent caller is a worker a deploy behind, still running host
|
|
2719
|
+
// code from before the pin, during a rolling deploy. Its refusal is transient: the host and this
|
|
2720
|
+
// object ship in the same bundle, so the window lasts as long as the rollout. Nothing ran, and
|
|
2721
|
+
// the reason says "not now" (`SYSTEM_DOOR_WAIT`) to whoever reads it: a schedule fires on the
|
|
2722
|
+
// next pass, and a job run retries on its next drive.
|
|
2723
|
+
if (expected === undefined) {
|
|
2724
|
+
throw substratError('forbidden', `a call acting as module '${moduleId}' reached this scope without passing the system door`, { reason: SYSTEM_DOOR_WAIT });
|
|
2725
|
+
}
|
|
2726
|
+
if (expected !== this.instanceId) {
|
|
2727
|
+
throw new SystemDoorMovedError("the scope restarted after the system door's gate read it; gate it again");
|
|
2728
|
+
}
|
|
2729
|
+
return { moduleId, [SYSTEM_DOOR_PASSED]: true };
|
|
2730
|
+
}
|
|
2588
2731
|
/**
|
|
2589
2732
|
* The pre-#1666 read, kept for ONE reason: a coordinator a deploy behind this DO still
|
|
2590
2733
|
* calls it. It answers the new question, not the old one — the old predicate counted any
|
|
@@ -2614,8 +2757,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
2614
2757
|
* `switchSystemSchedules`, serialized on the queue with every other tuple write and run
|
|
2615
2758
|
* as one `transactionSync`, so its reads and writes are one unit.
|
|
2616
2759
|
*/
|
|
2617
|
-
async switchSystemSchedules(moduleId, scopeId, to, at) {
|
|
2618
|
-
const outcome = await this.queue.enqueue(() => this.revision.transactionSync(() => switchSystemSchedules(this.switchSql(), { moduleId, scopeId, to, at })));
|
|
2760
|
+
async switchSystemSchedules(moduleId, scopeId, to, at, tenantHeld = false) {
|
|
2761
|
+
const outcome = await this.queue.enqueue(() => this.revision.transactionSync(() => switchSystemSchedules(this.switchSql(), { moduleId, scopeId, to, at, tenantHeld })));
|
|
2619
2762
|
// #1819: the instance that applied it, which the rewind hold's release rule reads.
|
|
2620
2763
|
return { ...outcome, instance: this.instanceId };
|
|
2621
2764
|
}
|
|
@@ -3093,6 +3236,18 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3093
3236
|
ORDER BY id LIMIT ?`, now, afterId ?? null, afterId ?? null, limit)
|
|
3094
3237
|
.toArray();
|
|
3095
3238
|
}
|
|
3239
|
+
/**
|
|
3240
|
+
* #1834: the drive's ONE snapshot of due runs — keys only, in the order they became due
|
|
3241
|
+
* (`JOB_RUN_DUE_AT`, then id). Spelled exactly as the pure adapter spells it. `jobRunsDue`
|
|
3242
|
+
* above stays for a coordinator a deploy behind, which still pages by id.
|
|
3243
|
+
*/
|
|
3244
|
+
async jobRunsDueKeys(now, max) {
|
|
3245
|
+
return this.sql
|
|
3246
|
+
.exec(`SELECT id, module_id, job FROM _substrat_job_runs
|
|
3247
|
+
WHERE status = 'running' AND (next_attempt_at IS NULL OR next_attempt_at <= ?)
|
|
3248
|
+
ORDER BY ${JOB_RUN_DUE_AT}, id LIMIT ?`, now, max)
|
|
3249
|
+
.toArray();
|
|
3250
|
+
}
|
|
3096
3251
|
/** The operator read, newest first. */
|
|
3097
3252
|
async jobRunList(filter) {
|
|
3098
3253
|
const where = [];
|
|
@@ -3174,7 +3329,15 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3174
3329
|
throw new Error(`unknown guard predicate: '${guard.predicate}' — declared by ${guard.declaredBy} ` +
|
|
3175
3330
|
`before '${operation}'; no registered module contributes it (operation blocked)`);
|
|
3176
3331
|
}
|
|
3177
|
-
|
|
3332
|
+
try {
|
|
3333
|
+
await predicate.handler(ctx, guard.config, input);
|
|
3334
|
+
}
|
|
3335
|
+
catch (err) {
|
|
3336
|
+
// #1745: a guard refusing is recorded after the rollback, as a transition is —
|
|
3337
|
+
// marked here, where which guard threw is still known. Always rethrown.
|
|
3338
|
+
markGuardRefusal(err, guard.predicate, operation);
|
|
3339
|
+
throw err;
|
|
3340
|
+
}
|
|
3178
3341
|
}
|
|
3179
3342
|
}
|
|
3180
3343
|
// -- migrations (port of applyPendingMigrations) --------------------------
|
|
@@ -3324,10 +3487,28 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3324
3487
|
* older than the one held. Bookkeeping, like the copy marker: it changes what runs here, not
|
|
3325
3488
|
* the scope's data, so it does not advance the write revision a carry fences on.
|
|
3326
3489
|
*/
|
|
3327
|
-
setLifecycle(next
|
|
3490
|
+
setLifecycle(next,
|
|
3491
|
+
/** #2016: the tenant the directory delivered this lifecycle for. A scope it is foreign to
|
|
3492
|
+
* (`tenantVerdict`) refuses the delivery untouched; one provisioned before the receipt
|
|
3493
|
+
* existed, whose role rows name this tenant, records it — the back-fill. */
|
|
3494
|
+
tenantId) {
|
|
3495
|
+
const { verdict, held } = tenantId === undefined ? { verdict: null, held: null } : this.tenantVerdict(tenantId);
|
|
3496
|
+
if (verdict === 'foreign') {
|
|
3497
|
+
return { refused: 'tenant', message: `lifecycle delivery ${foreignTenant(held, tenantId)}` };
|
|
3498
|
+
}
|
|
3499
|
+
// The back-fill: a scope provisioned before the receipt (its role rows agree), or one whose
|
|
3500
|
+
// tenant nothing recorded although it holds data (a load from a world that keeps its roles
|
|
3501
|
+
// elsewhere). The directory's word records it; a DO never provisioned here records nothing.
|
|
3502
|
+
const backfill = verdict === 'inferred' || (verdict === 'unknown' && this.holdsData());
|
|
3328
3503
|
let out;
|
|
3329
3504
|
this.revision.transactionSync(() => {
|
|
3330
|
-
|
|
3505
|
+
// A write the carry fences on, not bookkeeping: it happens once per legacy scope, and
|
|
3506
|
+
// over-counting only refuses a restore that could have landed. A carried-away copy takes
|
|
3507
|
+
// no write at all, so it is left to the next projection.
|
|
3508
|
+
if (backfill && !this.carriedAwayCopy) {
|
|
3509
|
+
this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, PROVISIONED_FOR_KEY, tenantId);
|
|
3510
|
+
}
|
|
3511
|
+
out = { ...this.revision.bookkeeping(() => writeLifecycle(this.switchSql(), next)), tenantRecorded: this.provisionedFor() !== null };
|
|
3331
3512
|
});
|
|
3332
3513
|
return out;
|
|
3333
3514
|
}
|
|
@@ -3826,6 +4007,14 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3826
4007
|
// both adapters by the query-plan test rather than by that gate.
|
|
3827
4008
|
this.sql.exec('CREATE INDEX IF NOT EXISTS _substrat_outbox_invocation ON _substrat_outbox (invocation_id, id)');
|
|
3828
4009
|
this.ensureScheduleStateKind();
|
|
4010
|
+
this.ensureRefusalsAdmitGuards();
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* #1745: `_substrat_refusals`, rebuilt to admit a guard row (a nullable `from_state`, plus
|
|
4014
|
+
* `guard` and `reason`) on a scope DO whose table predates it.
|
|
4015
|
+
*/
|
|
4016
|
+
ensureRefusalsAdmitGuards() {
|
|
4017
|
+
this.rebuildIfStale('_substrat_refusals', refusalsAdmitGuards, REFUSALS_REBUILD);
|
|
3829
4018
|
}
|
|
3830
4019
|
/**
|
|
3831
4020
|
* #1288: `_substrat_schedule_state`, rebuilt with `kind` in its key on a scope DO
|
|
@@ -3837,23 +4026,31 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3837
4026
|
* the stored DDL is the one probe both adapters can make.
|
|
3838
4027
|
*/
|
|
3839
4028
|
ensureScheduleStateKind() {
|
|
4029
|
+
this.rebuildIfStale('_substrat_schedule_state', scheduleStateHasKind, SCHEDULE_STATE_REBUILD);
|
|
4030
|
+
}
|
|
4031
|
+
/**
|
|
4032
|
+
* Run a kernel create-copy-drop-rename `script` over `table` when the stored DDL fails
|
|
4033
|
+
* `isCurrent` — the one shape `ensureScheduleStateKind` and `ensureRefusalsAdmitGuards`
|
|
4034
|
+
* share, so the pure adapter's twin rebuilds byte-identically.
|
|
4035
|
+
*/
|
|
4036
|
+
rebuildIfStale(table, isCurrent, script) {
|
|
3840
4037
|
const row = this.sql
|
|
3841
|
-
.exec(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`,
|
|
4038
|
+
.exec(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`, table)
|
|
3842
4039
|
.toArray()[0];
|
|
3843
|
-
if (!row ||
|
|
4040
|
+
if (!row || isCurrent(row.sql))
|
|
3844
4041
|
return;
|
|
3845
|
-
// In a transaction, for the reason the kernel
|
|
4042
|
+
// In a transaction, for the reason the kernel constants spell out: create-copy-
|
|
3846
4043
|
// drop-rename has two intermediate states and both are unrecoverable on the next
|
|
3847
4044
|
// wake. `transactionSync`, not the async one every operation uses — the DO
|
|
3848
4045
|
// runtime forbids a manual BEGIN through `sql.exec`, and this body is wholly
|
|
3849
4046
|
// synchronous, which is the one case the sync API is for (it commits at the
|
|
3850
4047
|
// first await, and there is none). It also has to be sync because the caller is.
|
|
3851
4048
|
this.revision.transactionSync(() => {
|
|
3852
|
-
for (const stmt of splitSqlStatements(
|
|
4049
|
+
for (const stmt of splitSqlStatements(script))
|
|
3853
4050
|
this.sql.exec(stmt);
|
|
3854
4051
|
});
|
|
3855
4052
|
}
|
|
3856
|
-
async importDump(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, } = {}) {
|
|
4053
|
+
async importDump(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, } = {}) {
|
|
3857
4054
|
// The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
|
|
3858
4055
|
//
|
|
3859
4056
|
// Two distinct FK hazards, and both need the deferral — the second is why it cannot
|
|
@@ -3892,6 +4089,15 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3892
4089
|
const before = this.loadMarker();
|
|
3893
4090
|
// #1713: the lifecycle the platform delivered here, read before the drops take it.
|
|
3894
4091
|
const lifecycleBefore = readLifecycle(this.switchSql());
|
|
4092
|
+
// #2016: this store's own tenant receipt, read before the drops take it. A load never
|
|
4093
|
+
// changes which tenant the scope belongs to: the platform's word for it (`provisionedFor`)
|
|
4094
|
+
// must agree with the receipt held, and is refused before the first drop if it does not.
|
|
4095
|
+
const receiptBefore = this.provisionedFor();
|
|
4096
|
+
if (provisionedFor !== undefined && receiptBefore !== null && receiptBefore !== provisionedFor) {
|
|
4097
|
+
const refused = substratError('conflict', tenantReceiptRefusal(receiptBefore, provisionedFor));
|
|
4098
|
+
tenantRefusals.add(refused);
|
|
4099
|
+
throw refused;
|
|
4100
|
+
}
|
|
3895
4101
|
// #1722 (Codex #2008 r7): a kept copy holds writes nothing else has. No load replaces it
|
|
3896
4102
|
// except its own resolution, and that only at the revision the operator acted on.
|
|
3897
4103
|
const kept = this.metaValue(KEPT_DIVERGENT_KEY);
|
|
@@ -3962,9 +4168,15 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
3962
4168
|
this.sql.exec(insert, ...row);
|
|
3963
4169
|
}
|
|
3964
4170
|
// #1738: a dump's `provisioned_for` names the scope (and tenant) it was captured from,
|
|
3965
|
-
// not this one
|
|
3966
|
-
//
|
|
3967
|
-
|
|
4171
|
+
// not this one, so it is never carried over. #2016: what stays is THIS store's own —
|
|
4172
|
+
// the platform's word when the load carries it (a copy into a fresh scope records its
|
|
4173
|
+
// own tenant here), else the receipt held before the drops. A store that held none
|
|
4174
|
+
// keeps none, and the repair projection that follows writes it.
|
|
4175
|
+
this.sql.exec(`DELETE FROM _substrat_meta WHERE key = ?`, PROVISIONED_FOR_KEY);
|
|
4176
|
+
const receipt = provisionedFor ?? receiptBefore;
|
|
4177
|
+
if (receipt !== null) {
|
|
4178
|
+
this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, PROVISIONED_FOR_KEY, receipt);
|
|
4179
|
+
}
|
|
3968
4180
|
// #1722: whatever stamp an export read here no longer describes this store, so every load
|
|
3969
4181
|
// replaces it: with the carry's own, or with none. And the write revision moves on from
|
|
3970
4182
|
// where it stood before the drops, so it never goes back.
|
|
@@ -4077,6 +4289,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4077
4289
|
const code = errorCodeOf(e);
|
|
4078
4290
|
if (code === 'precondition_failed')
|
|
4079
4291
|
return { refused: 'changed' };
|
|
4292
|
+
if (e instanceof Error && tenantRefusals.has(e))
|
|
4293
|
+
return { refused: 'tenant', message: e.message };
|
|
4080
4294
|
if (code === 'conflict')
|
|
4081
4295
|
return { refused: 'kept' };
|
|
4082
4296
|
throw toRpcError(e);
|
|
@@ -4485,9 +4699,9 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4485
4699
|
* survives — the whole point, since the denial is the write the operation could not make.
|
|
4486
4700
|
*/
|
|
4487
4701
|
/** K-42: the session a refused call ran under travels with the denial row. */
|
|
4488
|
-
/** #1745: a refused transition the operation failed with, written after its rollback. */
|
|
4702
|
+
/** #1745: a refused transition or refusing guard the operation failed with, written after its rollback. */
|
|
4489
4703
|
recordRefusal(subject, tenantId, scopeId, operation, err, invocationId, impersonation) {
|
|
4490
|
-
const refused =
|
|
4704
|
+
const refused = refusalOf(err);
|
|
4491
4705
|
if (!refused)
|
|
4492
4706
|
return;
|
|
4493
4707
|
const q = refusalInsert({
|
|
@@ -4538,7 +4752,12 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4538
4752
|
invocationId, new Date().toISOString());
|
|
4539
4753
|
}
|
|
4540
4754
|
// -- operation context (port of operationContext) -------------------------
|
|
4541
|
-
operationContext(principal, tenantId, scopeId, systemActor, connectionId,
|
|
4755
|
+
operationContext(principal, tenantId, scopeId, systemActor, connectionId,
|
|
4756
|
+
/**
|
|
4757
|
+
* #1834: the system subject, only as the door check hands it out (`assertSystemDoor`) — so
|
|
4758
|
+
* no path can act as `system:<moduleId>` without passing that check.
|
|
4759
|
+
*/
|
|
4760
|
+
systemDoor,
|
|
4542
4761
|
/** #458: per-invoke tally of `ctx.requestPlatform` calls; absent for consumer dispatch. */
|
|
4543
4762
|
signals,
|
|
4544
4763
|
/**
|
|
@@ -4603,8 +4822,8 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
4603
4822
|
? peerSubject
|
|
4604
4823
|
: capabilityId
|
|
4605
4824
|
? { kind: 'capability', id: capabilityId }
|
|
4606
|
-
:
|
|
4607
|
-
? { kind: 'system', id:
|
|
4825
|
+
: systemDoor
|
|
4826
|
+
? { kind: 'system', id: systemDoor.moduleId }
|
|
4608
4827
|
: connectionId
|
|
4609
4828
|
? { kind: 'connection', id: connectionId }
|
|
4610
4829
|
: { kind: 'principal', id: principal };
|
|
@@ -5048,14 +5267,19 @@ export function defineScopeDO(modules, bareOps) {
|
|
|
5048
5267
|
// #1738: the receipt `servesTenant` reads. First writer wins: absent or equal is written,
|
|
5049
5268
|
// a receipt for ANOTHER tenant refuses the whole projection (K-3), before a single row
|
|
5050
5269
|
// moves, so a misdirected projection can never re-point a scope or leave its roles behind.
|
|
5051
|
-
// A restore does not trip this: `importDump`
|
|
5052
|
-
// projection that follows finds
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5270
|
+
// A restore does not trip this: `importDump` keeps the destination's own receipt and
|
|
5271
|
+
// never the dump's (#2016), so the repair projection that follows finds its own tenant.
|
|
5272
|
+
// #2016: a scope with no receipt yet takes its first one only where its role rows agree
|
|
5273
|
+
// (`tenantVerdict`, the same reading the lifecycle back-fill uses), so a projection for
|
|
5274
|
+
// another tenant cannot pin a legacy scope to it.
|
|
5275
|
+
const { verdict, held } = this.tenantVerdict(tenantId);
|
|
5276
|
+
if (verdict === 'foreign') {
|
|
5277
|
+
throw substratError('conflict', held !== null
|
|
5278
|
+
? `applyProjection refused: this scope was provisioned for tenant ${held}, and a projection for tenant ${tenantId} would re-point it`
|
|
5279
|
+
: `applyProjection ${foreignTenant(held, tenantId)}`);
|
|
5056
5280
|
}
|
|
5057
5281
|
// Written before any guard below can return early, so every projection leaves it.
|
|
5058
|
-
this.sql.exec(`INSERT OR REPLACE INTO _substrat_meta (key, value) VALUES (
|
|
5282
|
+
this.sql.exec(`INSERT OR REPLACE INTO _substrat_meta (key, value) VALUES (?, ?)`, PROVISIONED_FOR_KEY, tenantId);
|
|
5059
5283
|
this.sql.exec(`DELETE FROM _substrat_roles WHERE tenant_id = ?`, tenantId);
|
|
5060
5284
|
for (const r of roles) {
|
|
5061
5285
|
this.sql.exec(`INSERT OR REPLACE INTO _substrat_roles (tenant_id, role_key, permissions, source, revoked_at)
|