@substrat-run/adapter-cloudflare 0.162.0 → 0.164.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.
@@ -59,6 +59,42 @@ type ProjectedRoleRow = {
59
59
  source: string;
60
60
  holder?: 'person' | 'system' | null;
61
61
  };
62
+ /**
63
+ * #2555: the directory's current permission state for one scope, as a fan-out projects it
64
+ * (`tenantProjection` and the vertical's connection keys), written by a control-plane-full load in
65
+ * its own transaction after the dump's rows are in. The roles are judged there against the state
66
+ * the dump has just brought, with `judge`'s reservations joined with the class's.
67
+ */
68
+ export type LoadProjection = {
69
+ tenantId: TenantId;
70
+ roles: ProjectedRoleRow[];
71
+ tuples: {
72
+ subject: string;
73
+ relation: string;
74
+ object: string;
75
+ expires_at: string | null;
76
+ revoked_at: string | null;
77
+ }[];
78
+ entitlements: {
79
+ entitlement_key: string;
80
+ expires_at: string | null;
81
+ quota: number | null;
82
+ plan: string | null;
83
+ }[];
84
+ identities: {
85
+ provider: string;
86
+ external_id: string;
87
+ principal_id: string;
88
+ scope_id: string | null;
89
+ }[];
90
+ connectionKeys: {
91
+ connection_id: string;
92
+ provider: string;
93
+ key_id: string;
94
+ public_key: string;
95
+ }[];
96
+ judge: RoleJudge;
97
+ };
62
98
  /**
63
99
  * #2531: a load's role repair, judged and written in the load's own transaction after the dump's
64
100
  * rows are in: the vertical's role set for `tenantId`, projected as `projectRolesLocal` projects it.
@@ -1 +1 @@
1
- {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAmDL,KAAK,QAAQ,EAWd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EA8DL,KAAK,mBAAmB,EASxB,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAmHtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAwBL,kBAAkB,EAiFnB,MAAM,sBAAsB,CAAC;AA2B9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAgB5E;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAEnE,iDAAiD;AACjD,KAAK,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,IAAI,CAAA;CAAE,CAAC;AAEvH;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,CAAC;AA4rBjG;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAmBtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,UAAU,GAAG,SAAS,CAOxD;AAED,gHAAgH;AAChH,OAAO,EAAE,kBAAkB,EAAE,CAAC;AA2B9B,wBAAgB,aAAa,CAC3B,OAAO,EAAE,kBAAkB,EAAE,EAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACzD;;;;GAIG;AACH,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,mBAAmB,CAAA;CAAO,GAClD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAAC,UAAU,CAAC,CA67P7E"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"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"}
package/dist/scope-do.js CHANGED
@@ -2,7 +2,7 @@ import { REWIND_REFUSED } from './rewind-refusal.js';
2
2
  import { SYSTEM_DOOR_MOVED } from './system-door.js';
3
3
  import { DurableObject } from 'cloudflare:workers';
4
4
  import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, domainEventInput, eventId, instant, objectRef, entityObjectRef, assertModuleEmittableType, assertKernelAuthoredType, toWireFailure, grantRefFromProof, principalId, principalBecomeCapabilityInput, scopeId as scopeIdOf, tenantId as tenantIdOf, platformRequestInput, platformRequestId, MAX_PENDING_PLATFORM_REQUESTS, MAX_PENDING_SWEEP_RUNS, SWEEP_RUNS_KIND, SCOPE_DUMP_PAGE_MAX_BYTES, SCOPE_TABLE_PAGE_MAX, SCOPE_QUERY_ROW_MAX, listLimitOf, pageOf, countedPageOf, requestFingerprint, substratError, errorCodeOf, assertReplayableDump, REDRAIN_BATCH, } from '@substrat-run/contracts';
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, eraseSubjectFromModules, moduleErasurePlan, redactSubjectScopeText, assertRowLimit, assertRowOffset, 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_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, topUpEntityGrantShapes, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, SCOPE_SEATED_ROLES_SQL, inUnitRoleRefusal, reservedRoleSetRefusedRole, reservedGrantRefusal, reservedRoleRefusal, reservedShapeRefusal, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSetDivergence, migrationSteps, planMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, ATTACHMENT_BYTES_SQL, ATTACHMENT_CONDEMNED_DDL, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, refuseCondemnedAttachment, refuseCondemnedInDump, rowlessAttachmentIds, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, queueAttachmentTextBackfill, startAttachmentTextBackfill, 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, emptyImportResult, 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, ancestorsWithin, } from '@substrat-run/kernel';
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, eraseSubjectFromModules, moduleErasurePlan, redactSubjectScopeText, assertRowLimit, assertRowOffset, 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_LEASE_EXPIRED_NOTE, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, delegatedRevokeSql, writeExplicitTupleIn, grantEntityShapeIn, topUpEntityGrantShapes, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, SCOPE_SEATED_ROLES_SQL, inUnitRoleRefusal, loadedRoleSetRefusedRole, reservedRoleSetRefusedRole, reservedGrantRefusal, reservedRoleRefusal, reservedShapeRefusal, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, refusalListQuery, mapRefusalRow, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, MIGRATION_DIGEST_FENCE_DDL, MIGRATION_DIGEST_MARK_LEGACY, assertJournalDumpCoherent, assertMigrationSql, migrationDivergence, migrationFailedError, migrationSetDivergence, migrationSteps, planMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, ATTACHMENT_BYTES_SQL, ATTACHMENT_CONDEMNED_DDL, ATTACHMENT_SWEEP_LICENCE_DDL, ATTACHMENT_SWEEP_LICENCE_TABLE, WIRE_NONCE_DDL, WIRE_NONCE_FULL, claimWireNonceIn, isWireNonceTable, isWireNonceWrite, clearOrphanSweepLicence, condemnLicensedOrphanAttachments, consumeOrphanSweepLicence, licensedOrphanIds, recordOrphanSweepLicence, refuseUnlicensedOrphanSweep, refuseCondemnedAttachment, refuseCondemnedInDump, rowlessAttachmentIds, attachmentRecordOfRow, enqueueAttachmentText, reconcileAttachmentText, queueAttachmentTextBackfill, startAttachmentTextBackfill, 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, emptyImportResult, 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, ancestorsWithin, } from '@substrat-run/kernel';
6
6
  import { isUpgradeRequest, readSubscription, LIVE_DECIDE_ATTEMPTS, LIVE_FANOUT_LIMIT, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, LIVE_EXPIRES_HEADER, LIVE_CLOSE, LIVE_SOCKETS_PER_PRINCIPAL, decodeLiveWithin, liveInstant, liveRefusal, liveRefusalStatus, } from './live-reads.js';
7
7
  import { replyOf } from './do-reply.js';
8
8
  import { OperationQueue } from './serialization.js';
@@ -390,6 +390,10 @@ const KERNEL_DDL = `
390
390
  -- #119: the kernel's authorization for one archive/trash move, read by the derived update
391
391
  -- trigger. Shared with the other adapter from @substrat-run/kernel; the comments are there.
392
392
  ${ENTITY_STATE_MOVES_DDL}
393
+ -- RFC wire-signing §4: the nonces of signed calls this scope accepted, and their count.
394
+ -- Store-local: never dumped, written on the bookkeeping path. Shared with the pure adapter
395
+ -- from @substrat-run/kernel, where the column comments are.
396
+ ${WIRE_NONCE_DDL}
393
397
  `;
394
398
  /**
395
399
  * Workers RPC carries a plain `Error`'s MESSAGE faithfully and nothing else. A custom
@@ -425,6 +429,8 @@ function kernelEmit(ctx, event) {
425
429
  throw new Error('kernelEmit: not an operation context this host built');
426
430
  write(event);
427
431
  }
432
+ /** #2555: the `_substrat_meta` keys a control-plane-full load keeps from the destination. */
433
+ const PERMISSION_FLAG_KEYS = ['permission_source', 'entitlements_enforced'];
428
434
  /** #2450: a commit or a page naming a stage this store no longer holds. */
429
435
  const STAGE_GONE = 'the staged load is not here any more (another carry replaced it, or it was already loaded); nothing was loaded';
430
436
  /** A competing page zero must leave the stage a live copy owns intact. */
@@ -551,11 +557,11 @@ class WriteRevision {
551
557
  if (write)
552
558
  this.written++;
553
559
  if (this.keeping && write) {
554
- // Bookkeeping takes the copy-marker insert and the lifecycle delivery (#1713) and nothing
555
- // else: any other write here would be one the revision never saw, which is the hole this
560
+ // Bookkeeping takes the copy-marker insert, the lifecycle delivery (#1713) and the wire
561
+ // nonce claim's own statements (RFC wire-signing §4) and nothing else: any other write here would be one the revision never saw, which is the hole this
556
562
  // class exists to close.
557
- if (!isCopyMarkInsert(query) && !isLifecycleWrite(query)) {
558
- throw substratError('internal', `the bookkeeping path takes only the copy-marker insert and the lifecycle delivery (#1722, #1713), not: ${query.slice(0, 80)}`);
563
+ if (!isCopyMarkInsert(query) && !isLifecycleWrite(query) && !isWireNonceWrite(query)) {
564
+ throw substratError('internal', `the bookkeeping path takes only the copy-marker insert, the lifecycle delivery and the wire nonce claim (#1722, #1713), not: ${query.slice(0, 80)}`);
559
565
  }
560
566
  return raw.exec(query, ...bindings);
561
567
  }
@@ -582,8 +588,9 @@ class WriteRevision {
582
588
  * r10). That only ever restricts what the store may run and changes no data, so it advances no
583
589
  * write revision: a backfill that marks a carry's source between its export and its wipe must
584
590
  * not read as a write the carry did not copy (that would keep the copy for nothing). Enforced,
585
- * not trusted: any write in here but the marker insert (`isCopyMarkInsert`) or the lifecycle
586
- * delivery (`isLifecycleWrite`, #1713) throws. Clearing a
591
+ * not trusted: any write in here but the marker insert (`isCopyMarkInsert`), the lifecycle
592
+ * delivery (`isLifecycleWrite`, #1713) or a wire nonce claim (`isWireNonceWrite`, RFC
593
+ * wire-signing §4: a nonce describes a call this store accepted, not its data) throws. Clearing a
587
594
  * marker is NOT bookkeeping (r11): it loosens the store, so it is a write a carry fences on.
588
595
  */
589
596
  bookkeeping(run) {
@@ -3897,9 +3904,9 @@ options = {}) {
3897
3904
  * The answer is a fixed sentence naming the role key only, whoever reads it. The rule's own
3898
3905
  * wording is the pre-check's, which a caller still asks first for the common case.
3899
3906
  */
3900
- refuseRoleSetInUnit(tenantId, roles, judge) {
3907
+ refuseRoleSetInUnit(tenantId, roles, { judge, loaded }) {
3901
3908
  const state = this.roleStateSync(tenantId);
3902
- const refused = reservedRoleSetRefusedRole(roles, { reserved: new Set([...judge.reserved, ...this.reservedKeys]), declared: new Set([...judge.declared, ...this.declaredKeys]) }, { roles: state.roles, seated: new Set(state.seated), ...(state.dropped ? { dropped: state.dropped } : {}) });
3909
+ const refused = (loaded ? loadedRoleSetRefusedRole : reservedRoleSetRefusedRole)(roles, { reserved: new Set([...judge.reserved, ...this.reservedKeys]), declared: new Set([...judge.declared, ...this.declaredKeys]) }, { roles: state.roles, seated: new Set(state.seated), ...(state.dropped ? { dropped: state.dropped } : {}) });
3903
3910
  if (refused === null)
3904
3911
  return;
3905
3912
  const error = substratError('forbidden', inUnitRoleRefusal(refused.key));
@@ -4929,6 +4936,24 @@ options = {}) {
4929
4936
  });
4930
4937
  return marked;
4931
4938
  }
4939
+ /**
4940
+ * RFC wire-signing §4: spend a signed call's nonce in this scope's own store, the claim the
4941
+ * platform's entry makes through the vertical's host (`claimWireNonceLocal`) after every other
4942
+ * check of the envelope has passed. `true` once per nonce; `false` for one already spent, or a
4943
+ * claim after `until`. A store at its cap throws, after its purge committed, which the entry
4944
+ * answers as the store's failure. In storage, so it survives an eviction. Bookkeeping, like the
4945
+ * copy marker: a nonce describes a call this store accepted, not its data, so it advances no
4946
+ * write revision a carry fences on, and no dump carries it.
4947
+ */
4948
+ claimWireNonce(nonce, until) {
4949
+ let answer;
4950
+ this.revision.transactionSync(() => {
4951
+ answer = this.revision.bookkeeping(() => claimWireNonceIn(this.switchSql(), nonce, until, Math.floor(Date.now() / 1000)));
4952
+ });
4953
+ if (answer === 'full')
4954
+ throw new Error(WIRE_NONCE_FULL);
4955
+ return answer;
4956
+ }
4932
4957
  /**
4933
4958
  * Clear a mistaken copy classification (#2005, `clearCopyMarker`); a load's events mark stays,
4934
4959
  * so copied work still never runs here (#2009). A
@@ -5328,7 +5353,8 @@ options = {}) {
5328
5353
  * The tables a dump carries, by name. The derived search index (#827) is excluded: its shadow
5329
5354
  * tables cannot be replayed — this host answers "object name reserved for internal use" — and
5330
5355
  * it is recomputable, so `importDump` rebuilds it from the loaded rows. A dump carries the rows,
5331
- * never the index over them. A staged load's own tables (#2450) are this store's, not its data.
5356
+ * never the index over them. A staged load's own tables (#2450) are this store's, not its data,
5357
+ * and so is the wire nonce store (RFC wire-signing §4): a load builds it empty.
5332
5358
  */
5333
5359
  dumpTableDefs() {
5334
5360
  return this.sql
@@ -5336,7 +5362,7 @@ options = {}) {
5336
5362
  WHERE type = 'table' AND name NOT GLOB 'sqlite_*' AND name NOT GLOB '_cf_*'
5337
5363
  AND name NOT GLOB '${STAGE_GLOB}' AND sql IS NOT NULL
5338
5364
  ORDER BY name`)
5339
- .toArray().filter(({ name }) => !isSearchIndexTable(name));
5365
+ .toArray().filter(({ name }) => !isSearchIndexTable(name) && !isWireNonceTable(name));
5340
5366
  }
5341
5367
  /**
5342
5368
  * One page of `exportDumpStamped` (#2450): the tables in the same order, each table's rows in
@@ -5609,7 +5635,7 @@ options = {}) {
5609
5635
  * in memory at once. `stageId` names the staged load the rows come from: checked inside the
5610
5636
  * load's transaction, and its tables dropped in it once the rows are in.
5611
5637
  */
5612
- async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, } = {}, stageId) {
5638
+ async loadTables(tables, destScopeId, { switchOff, sourceScopeId, exact, loadStamp, expect, resolveKept, markCopy, provisionedFor, fence, roleRepair, onRolesProjected, projection, keepPermissionFlags, } = {}, stageId) {
5613
5639
  // The WHOLE drop-then-replay runs under deferred foreign keys, in one transaction.
5614
5640
  //
5615
5641
  // Two distinct FK hazards, and both need the deferral — the second is why it cannot
@@ -5632,8 +5658,10 @@ options = {}) {
5632
5658
  // rather than failing a restore over data about to be recomputed.
5633
5659
  // Matched without case, as SQLite resolves a table name (#1883 review).
5634
5660
  // #1686: a copy into another scope id loads no capability rows; a same-scope restore keeps them.
5661
+ // The wire nonce store is this store's own (RFC wire-signing §4): never replayed, so a load
5662
+ // starts it empty.
5635
5663
  const replayable = tables
5636
- .filter((t) => !isSearchIndexTable(t.name.toLowerCase()))
5664
+ .filter((t) => !isSearchIndexTable(t.name.toLowerCase()) && !isWireNonceTable(t.name))
5637
5665
  .map((t) => (loadDropsRowsOf(t.name, destScopeId, sourceScopeId) ? { ...t, rows: () => [] } : t));
5638
5666
  // The dump is untrusted input (#1143). `SqlStorage.exec` runs every statement
5639
5667
  // in the string it is given, so a `ddl` with anything appended to its CREATE
@@ -5649,7 +5677,10 @@ options = {}) {
5649
5677
  // #2531: a reaped store takes no role repair, judged or not, as `projectInUnit` drops a
5650
5678
  // projection there: the load comes back with no roles, no receipt and no enforcement flip,
5651
5679
  // as it did before the repair rode the load. Read before the unit, as `projectInUnit` reads it.
5652
- const repair = roleRepair && !(await this.isReaped()) ? roleRepair : undefined;
5680
+ // #2555: the directory's projection is dropped there too, for the same reason.
5681
+ const reaped = roleRepair || projection ? await this.isReaped() : false;
5682
+ const repair = roleRepair && !reaped ? roleRepair : undefined;
5683
+ const directory = projection && !reaped ? projection : undefined;
5653
5684
  const switched = await this.revision.transaction(async () => {
5654
5685
  // #1722: first, before any read: a store a reap has since emptied must refuse the late load
5655
5686
  // too, and the check is against this store's clock, inside the load's own transaction.
@@ -5679,6 +5710,8 @@ options = {}) {
5679
5710
  return { name: t.name, columns: ['id'], rows: at < 0 ? [] : Array.from(t.rows(), (row) => [row[at]]) };
5680
5711
  }));
5681
5712
  const receiptBefore = this.provisionedFor();
5713
+ // #2555: this store's own permission flags, read before the drops take them.
5714
+ const flagsBefore = keepPermissionFlags ? PERMISSION_FLAG_KEYS.map((key) => [key, this.metaValue(key)]) : [];
5682
5715
  if (provisionedFor !== undefined && receiptBefore !== null && receiptBefore !== provisionedFor) {
5683
5716
  const refused = substratError('conflict', tenantReceiptRefusal(receiptBefore, provisionedFor));
5684
5717
  tenantRefusals.add(refused);
@@ -5776,6 +5809,15 @@ options = {}) {
5776
5809
  if (receipt !== null) {
5777
5810
  this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, PROVISIONED_FOR_KEY, receipt);
5778
5811
  }
5812
+ // #2555: which reader the checker uses, and whether entitlements are enforced, are this
5813
+ // store's, as its receipt is: the dump's values describe the scope it was captured from,
5814
+ // when it was. A flag this store did not hold stays unset. The projection below may then
5815
+ // move them as a fan-out would.
5816
+ for (const [key, value] of flagsBefore) {
5817
+ this.sql.exec(`DELETE FROM _substrat_meta WHERE key = ?`, key);
5818
+ if (value !== null)
5819
+ this.sql.exec(`INSERT INTO _substrat_meta (key, value) VALUES (?, ?)`, key, value);
5820
+ }
5779
5821
  // #1722: whatever stamp an export read here no longer describes this store, so every load
5780
5822
  // replaces it: with the carry's own, or with none. And the write revision moves on from
5781
5823
  // where it stood before the drops, so it never goes back.
@@ -5829,9 +5871,19 @@ options = {}) {
5829
5871
  // seats the dump has just brought (`refuseRoleSetInUnit`), then projected exactly as
5830
5872
  // `projectRolesLocal` projects it. A refusal rolls the whole load back, so the scope keeps
5831
5873
  // its rows and a staged load keeps its stage; an admitted load lands with its repair.
5832
- if (repair) {
5874
+ // #2555: a control-plane-full load writes the directory's current state the same way, in the
5875
+ // same place: the tenant's roles, tuples, entitlements, identity links and the vertical's
5876
+ // connection keys, each a full replace as a fan-out writes it, so nothing the directory has
5877
+ // since revoked comes back with the dump. Scope-level tuples (the seats) are not touched.
5878
+ // The roles are judged only where the loaded state has a part in them (`loaded`).
5879
+ if (repair || directory) {
5833
5880
  try {
5834
- this.projectionBody(repair.judge, repair.tenantId, repair.roles, []);
5881
+ if (repair)
5882
+ this.projectionBody({ judge: repair.judge }, repair.tenantId, repair.roles, []);
5883
+ else if (directory) {
5884
+ const { judge, tenantId, roles, tuples, entitlements, identities, connectionKeys } = directory;
5885
+ this.projectionBody({ judge, loaded: true }, tenantId, roles, tuples, entitlements, undefined, identities, connectionKeys);
5886
+ }
5835
5887
  }
5836
5888
  catch (e) {
5837
5889
  // The projection's own tenant refusal (its receipt check) is a tenant refusal of the
@@ -5914,6 +5966,35 @@ options = {}) {
5914
5966
  async importDumpJudged(tables, destScopeId, opts, roleRepair) {
5915
5967
  return this.judgedLoad((onRolesProjected) => this.checkedLoad(() => this.importDump(tables, destScopeId, { ...opts, roleRepair, onRolesProjected })));
5916
5968
  }
5969
+ /**
5970
+ * #2555: a control-plane-full restore or import. `importDump`, with this store's permission flags
5971
+ * kept through it and, when `projection` is given (a host that projects permissions into its
5972
+ * scopes), the directory's current state judged and projected last in the load's transaction. A
5973
+ * refusal rolls the whole load back. The role judgement's own is answered as `refusedRoles`, so
5974
+ * the host can tell it from any other; every other is answered as data so its code survives the
5975
+ * hop. A method of its own, so a host meeting a ScopeDO from before it is refused by the
5976
+ * runtime, nothing loaded, rather than loading the dump's permission state as is.
5977
+ */
5978
+ importDumpProjected(tables, destScopeId, opts, projection) {
5979
+ return replyOf(async () => {
5980
+ try {
5981
+ return {
5982
+ switchedOff: await this.importDump(tables, destScopeId, {
5983
+ switchOff: opts.switchOff,
5984
+ sourceScopeId: opts.sourceScopeId,
5985
+ exact: opts.exact,
5986
+ keepPermissionFlags: true,
5987
+ ...(projection ? { projection } : {}),
5988
+ }),
5989
+ };
5990
+ }
5991
+ catch (e) {
5992
+ if (e instanceof Error && roleRefusals.has(e))
5993
+ return { refusedRoles: e.message };
5994
+ throw e;
5995
+ }
5996
+ });
5997
+ }
5917
5998
  /** #2531: `commitStagedDumpChecked` with the role repair in the load's transaction (`importDumpJudged`). */
5918
5999
  async commitStagedDumpJudged(stageId, destScopeId, opts, roleRepair) {
5919
6000
  return this.judgedLoad((onRolesProjected) => this.commitStagedDumpChecked(stageId, destScopeId, { ...opts, roleRepair, onRolesProjected }));
@@ -7176,13 +7257,13 @@ options = {}) {
7176
7257
  // The whole body is synchronous storage work, so it is ONE transaction (#1738): the queue
7177
7258
  // serializes but does not roll back, and a role insert that throws part-way must not leave
7178
7259
  // the receipt (or a half-replaced role set) behind for `servesTenant` to trust.
7179
- return this.queue.enqueue(() => this.revision.transactionSync(() => this.projectionBody(judge, ...args)));
7260
+ return this.queue.enqueue(() => this.revision.transactionSync(() => this.projectionBody(judge && { judge }, ...args)));
7180
7261
  }
7181
7262
  /**
7182
7263
  * The projection's writes, synchronous, for a unit the caller holds: `projectInUnit`'s, or a
7183
7264
  * load's (`loadTables`, #2531), so a restore's role repair lands in the load's transaction.
7184
7265
  */
7185
- projectionBody(judge, ...[tenantId, roles, tuples, entitlements, scopeTuples, identities, connectionKeys, switchOff, serviceSubjects]) {
7266
+ projectionBody(judgement, ...[tenantId, roles, tuples, entitlements, scopeTuples, identities, connectionKeys, switchOff, serviceSubjects]) {
7186
7267
  // #1738: the receipt `servesTenant` reads. First writer wins: absent or equal is written,
7187
7268
  // a receipt for ANOTHER tenant refuses the whole projection (K-3), before a single row
7188
7269
  // moves, so a misdirected projection can never re-point a scope or leave its roles behind.
@@ -7198,8 +7279,8 @@ options = {}) {
7198
7279
  : `applyProjection ${foreignTenant(held, tenantId)}`);
7199
7280
  }
7200
7281
  // #2531: judged before the first write of the unit, on what the scope holds now.
7201
- if (judge)
7202
- this.refuseRoleSetInUnit(tenantId, roles.map(roleShapeOfRow), judge);
7282
+ if (judgement)
7283
+ this.refuseRoleSetInUnit(tenantId, roles.map(roleShapeOfRow), judgement);
7203
7284
  // Written before any guard below can return early, so every projection leaves it.
7204
7285
  this.sql.exec(`INSERT OR REPLACE INTO _substrat_meta (key, value) VALUES (?, ?)`, PROVISIONED_FOR_KEY, tenantId);
7205
7286
  // #2531: a role the new set leaves out is TOMBSTONED, not deleted: it keeps the keys and the