@substrat-run/adapter-cloudflare 0.144.0 → 0.145.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.
@@ -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;AA4DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAqGtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAkBL,kBAAkB,EAyEnB,MAAM,sBAAsB,CAAC;AA0B9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AA+jB5E;;;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,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAAC,UAAU,CAAC,CA0gO7E"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA4DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAqGtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAkBL,kBAAkB,EA4EnB,MAAM,sBAAsB,CAAC;AA0B9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AA+jB5E;;;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,GACxD,KAAK,GAAG,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,KAAK,aAAa,CAAC,UAAU,CAAC,CA2jO7E"}
package/dist/scope-do.js CHANGED
@@ -7,7 +7,7 @@ import { isUpgradeRequest, readSubscription, LIVE_DECIDE_ATTEMPTS, LIVE_FANOUT_L
7
7
  import { replyOf } from './do-reply.js';
8
8
  import { OperationQueue } from './serialization.js';
9
9
  import { assertNoWorkerdStorage, doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
10
- import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, splitSqlStatements, executableSqlStatements, blankSqlComments, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, assertNoCallerPurge, isUnreachableParent, PURGE_BATCH, heldPurgePass, lifecycleRefusal, purgeHeldBy, purgeDueOf, purgeOnlyKeysOf, runPurgePass, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
10
+ import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, ENTITY_STATE_MOVES_DDL, SWITCH_FENCES_DDL, TABLE_OWNERS_DDL, assertMigrationLeavesLedgerAlone, recordOwnershipSteps, runMigrationStatements, splitSqlStatements, executableSqlStatements, blankSqlComments, CAPABILITY_EXCHANGE_OPERATION, CAPABILITY_BECOME_MINT_OPERATION, becomeMintCheck, mintBecomeCapabilityAsPrincipal, revokeBecomeCapabilityAsPrincipal, readBecomeLinkStates, ENTITY_UNBIND_OPERATION, allowedFor, unbindEntityHolderBounded, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, CONTINUATION_POSITION_CAP, visibleContinuation, searchStateWhere, uncheckedView, addStatePlans, addUpdatedStampPlans, watchUpdatedStamps, assertNoCallerPurge, isUnreachableParent, PURGE_BATCH, heldPurgePass, lifecycleRefusal, purgeHeldBy, purgeDueOf, purgeOnlyKeysOf, runPurgePass, refuseTrashedTarget, assertBoundOperations, registerTrashTargets, withheldKeysFor, statefulTablesOf, afterMigration, afterRuntimeDdl, derivesAnything, repairDerivedObjects, StateColumnLost, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, asyncInvocationId, asyncLinePass, resolveCapabilitySession, revokeCapabilityAsPlatform, readCapabilityPage, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readOperationSeries, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
11
11
  import { COPY_RESTORE_FENCE_LAPSED, copyRestoreFenceLapsed } from '@substrat-run/kernel';
12
12
  import { createDoTupleChecker, createLocalControlPlaneReader, scopeTupleReader } from './checker.js';
13
13
  import { settlePlatformRequestIn } from '@substrat-run/kernel';
@@ -352,9 +352,9 @@ const KERNEL_DDL = `
352
352
  -- #1575: attachment text and its FTS5 index, shared from @substrat-run/kernel. After
353
353
  -- _substrat_attachments, whose delete trigger it adds.
354
354
  ${ATTACHMENT_TEXT_DDL}
355
- -- #901: an entity's version is the ULID of the last event about it, so
356
- -- MAX(id) per (entity_type, entity_id) is the read. The id column sits last so
357
- -- SQLite walks to the end of the matched range instead of aggregating over it.
355
+ -- #901: an entity's version is the ULID of the last event about it (a state move
356
+ -- excepted, #2191), so the newest id per (entity_type, entity_id) is the read. The id
357
+ -- column sits last so SQLite walks the matched range backwards instead of aggregating.
358
358
  ${OUTBOX_ENTITY_INDEX}
359
359
  -- #116: the request-dedupe table, kernel-owned so no vertical migrates for it.
360
360
  ${IDEMPOTENCY_DDL}
@@ -746,6 +746,8 @@ export function defineScopeDO(modules, bareOps) {
746
746
  listPlans = new Map();
747
747
  /** #119: entity type → its archive/trash plan, from every registered module. */
748
748
  statePlans = new Map();
749
+ /** #2166: entity type → the updated stamps the host holds, from every registered module. */
750
+ updatedStampPlans = new Map();
749
751
  /** #2068: each registered module's erasure, in registration order — what `redactSubject` runs. */
750
752
  erasurePlans = [];
751
753
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
@@ -869,6 +871,7 @@ export function defineScopeDO(modules, bareOps) {
869
871
  }
870
872
  // #119: the archive/trash plans, refusing a second owner of one entity type.
871
873
  addStatePlans(this.statePlans, manifest.id, manifest.entityStates, manifest.permissions);
874
+ addUpdatedStampPlans(this.updatedStampPlans, manifest.id, manifest.updatedStamps);
872
875
  // #811: the list indexes `lists` declares, same placement and same reason.
873
876
  for (const plan of listIndexPlans(manifest.id, manifest.lists, manifest.entityStates)) {
874
877
  const existing = this.listPlans.get(plan.entityType);
@@ -1367,7 +1370,10 @@ export function defineScopeDO(modules, bareOps) {
1367
1370
  // As the producer's principal: real checks against the grants this vertical's
1368
1371
  // `peers` gave it; its emits carry `{ vertical, scope }` and what they passed.
1369
1372
  // What it emits was emitted BECAUSE of the producer's event (#1237).
1370
- await imp.handler(this.importContext(tenantId, scopeId, peerSubject, { causedBy: e.id, invocationId: unit.invocationId }), event);
1373
+ const ctx = this.importContext(tenantId, scopeId, peerSubject, { causedBy: e.id, invocationId: unit.invocationId });
1374
+ const stamps = watchUpdatedStamps(doSpineSql(this.sql), this.updatedStampPlans);
1375
+ await imp.handler(ctx, event);
1376
+ stamps?.assertHeld(ctx, `${imp.moduleId} consuming ${e.type}`);
1371
1377
  this.sql.exec(`INSERT INTO _substrat_deliveries (event_id, consumer_module, delivered_at, invocation_id)
1372
1378
  VALUES (?, ?, ?, NULL)`, e.id, imp.moduleId, new Date().toISOString());
1373
1379
  });
@@ -1684,14 +1690,16 @@ export function defineScopeDO(modules, bareOps) {
1684
1690
  return { ...one, done: one.next === null };
1685
1691
  }
1686
1692
  let [toppedUp, retired, retirementsLeftOpen, from] = [0, 0, 0, null];
1693
+ let boundEver;
1687
1694
  do {
1688
1695
  const one = await pass(from);
1689
1696
  toppedUp += one.toppedUp;
1690
1697
  retired += one.retired;
1691
1698
  retirementsLeftOpen += one.retirementsLeftOpen;
1692
1699
  from = one.next;
1700
+ boundEver = one.boundEver;
1693
1701
  } while (from);
1694
- return { toppedUp, retired, retirementsLeftOpen, next: null, done: true };
1702
+ return { toppedUp, retired, retirementsLeftOpen, next: null, done: true, ...(boundEver ? { boundEver } : {}) };
1695
1703
  }
1696
1704
  /**
1697
1705
  * The dispatch capability's admission (#726 remedy B): this delivery may read the
@@ -2061,9 +2069,12 @@ export function defineScopeDO(modules, bareOps) {
2061
2069
  : undefined;
2062
2070
  // #119: the host's trash refusal, before the guards and the handler — the pure
2063
2071
  // adapter's, through the same kernel function.
2064
- await refuseTrashedTarget({ sql: doSpineSql(this.sql), plans: this.statePlans, check: ctx.check }, operation, target, parsed, purging ? { now: new Date().toISOString(), gate: this.purgeGateFacts(systemDoor.moduleId, tenantId) } : undefined);
2072
+ // #2166: what the guards and the handler emit about a stamped entity, held below.
2073
+ const stamps = watchUpdatedStamps(doSpineSql(this.sql), this.updatedStampPlans);
2074
+ await refuseTrashedTarget({ sql: doSpineSql(this.sql), plans: this.statePlans, check: ctx.check, now: ctx.now() }, operation, target, parsed, purging ? { gate: this.purgeGateFacts(systemDoor.moduleId, tenantId) } : undefined);
2065
2075
  await this.runGuards(operation, ctx, parsed);
2066
2076
  result = await handler(ctx, parsed);
2077
+ stamps?.assertHeld(ctx, operation);
2067
2078
  if (guardedRef && invokeOptions?.ifMatch !== undefined) {
2068
2079
  assertIfMatch(guardedRef, invokeOptions.ifMatch, seen ?? null);
2069
2080
  }
@@ -2564,6 +2575,12 @@ export function defineScopeDO(modules, bareOps) {
2564
2575
  * principal holding that root in several tabs is one check, fanned out to each socket.
2565
2576
  */
2566
2577
  const rootChecks = new Map();
2578
+ /**
2579
+ * A row beneath a checked root that the key reaches only through a record whose shape
2580
+ * protects it (#2177): decided by the subscriber's own check of the key on the row, asked
2581
+ * once per (principal, key, row) per pass, under the same epoch rules as `rootChecks`.
2582
+ */
2583
+ const rowChecks = new Map();
2567
2584
  const ancestors = new Map();
2568
2585
  const openEpoch = (now) => ({
2569
2586
  writes: this.revision.statementsWritten,
@@ -2577,17 +2594,26 @@ export function defineScopeDO(modules, bareOps) {
2577
2594
  if (!holds(epoch, now)) {
2578
2595
  epoch = openEpoch(now);
2579
2596
  rootChecks.clear();
2597
+ rowChecks.clear();
2580
2598
  ancestors.clear();
2581
2599
  }
2582
2600
  return epoch;
2583
2601
  };
2584
- const reaches = async (row, root, now) => {
2585
- let up = ancestors.get(row.id);
2602
+ /**
2603
+ * Is the row beneath the root? With a `key` (#2177), only along a path the key's own check
2604
+ * would climb: it stops at a record whose shape protects the key. Memoised per (row, key),
2605
+ * since the stop depends on it.
2606
+ */
2607
+ const reaches = async (row, root, now, key) => {
2608
+ const memo = `${row.id}\n${key ?? ''}`;
2609
+ let up = ancestors.get(memo);
2586
2610
  if (!up) {
2587
2611
  // A walk that cannot answer is a frame not sent — the same fail-closed rule as the check.
2588
2612
  // Walked at the decision's own instant; remembered only while its epoch holds.
2589
- up = ancestorsWithin(parents, { entityType: row.entity_type, entityId: row.entity_id }, now).catch(() => new Set());
2590
- ancestors.set(row.id, up);
2613
+ up = Promise.resolve()
2614
+ .then(() => ancestorsWithin(parents, { entityType: row.entity_type, entityId: row.entity_id }, now, key))
2615
+ .catch(() => new Set());
2616
+ ancestors.set(memo, up);
2591
2617
  }
2592
2618
  return (await up).has(`${root.entityType}:${root.entityId}`);
2593
2619
  };
@@ -2655,7 +2681,13 @@ export function defineScopeDO(modules, bareOps) {
2655
2681
  rootVerdict = undefined;
2656
2682
  // Narrowing first: it is memoised across sockets, and a row outside the root
2657
2683
  // is out whatever the principal holds.
2658
- if (within && !(await reaches(row, within, now)))
2684
+ // A checked root walks with its key first (#2177). The keyed walk is the plain walk with
2685
+ // stops, so whatever it reaches the plain walk reaches too: a hit settles "beneath the
2686
+ // root" in one walk, and only a miss needs the plain walk to tell a row outside the root
2687
+ // from one the key reaches only through a record whose shape protects it.
2688
+ const key = within?.checked;
2689
+ const keyed = within && key !== undefined ? await reaches(row, within, now, key) : false;
2690
+ if (within && !keyed && !(await reaches(row, within, now)))
2659
2691
  return 'skip';
2660
2692
  if (within?.checked !== undefined) {
2661
2693
  if (rootVerdict === undefined) {
@@ -2673,6 +2705,27 @@ export function defineScopeDO(modules, bareOps) {
2673
2705
  }
2674
2706
  if (!rootVerdict.allowed)
2675
2707
  return 'revoked';
2708
+ // The root check speaks for a row the key reaches through the root. A row it reaches
2709
+ // only through a record whose shape protects the key (#2177) is the subscriber's own
2710
+ // check on the row: a holder by role or by the record's shape is nudged, a holder by a
2711
+ // grant above the record is not — the answer `ctx.check` gives, by asking it.
2712
+ if (!keyed) {
2713
+ const ask = () => context()
2714
+ .check(within.checked, { entityType: row.entity_type, entityId: row.entity_id })
2715
+ .then((d) => d.allowed, () => false);
2716
+ let allowed;
2717
+ if (e.remote) {
2718
+ allowed = ask();
2719
+ }
2720
+ else {
2721
+ const memo = `${subscriber.principal}\n${key}\n${row.id}`;
2722
+ allowed = rowChecks.get(memo);
2723
+ if (!allowed)
2724
+ rowChecks.set(memo, (allowed = ask()));
2725
+ }
2726
+ if (!(await allowed))
2727
+ return 'skip';
2728
+ }
2676
2729
  }
2677
2730
  if (within?.vouched !== undefined || within?.checked !== undefined) {
2678
2731
  // The root was vouched for or checked, so the walk above was the row filter —
@@ -5541,7 +5594,9 @@ export function defineScopeDO(modules, bareOps) {
5541
5594
  causedBy: event.id,
5542
5595
  invocationId: unit.invocationId,
5543
5596
  });
5597
+ const stamps = watchUpdatedStamps(doSpineSql(this.sql), this.updatedStampPlans);
5544
5598
  await consumer.handler(ctx, event);
5599
+ stamps?.assertHeld(ctx, `${mod.id} consuming ${event.type}`);
5545
5600
  this.sql.exec(`INSERT INTO _substrat_deliveries
5546
5601
  (event_id, consumer_module, delivered_at, invocation_id)
5547
5602
  VALUES (?, ?, ?, ?)`, event.id, mod.id, new Date().toISOString(), invocationId);