@substrat-run/adapter-cloudflare 0.126.0 → 0.128.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":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAmCL,KAAK,SAAS,EAmBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAoEtB,MAAM,sBAAsB,CAAC;AAmF9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAsbD;;;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;AAED,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,CAs7JjE"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAcL,KAAK,WAAW,EAqCjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAmCL,KAAK,SAAS,EAmBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAoEtB,MAAM,sBAAsB,CAAC;AAsF9B;;;;;;;;;;;;;GAaG;AAEH,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAsbD;;;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;AAED,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,CA29JjE"}
package/dist/scope-do.js CHANGED
@@ -5,9 +5,9 @@ import { ulid, DO_SQL_LIMITS, createUlid, assertAllowed, ConnectionSealingKeyUna
5
5
  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
6
  import { OperationQueue } from './serialization.js';
7
7
  import { doScopedSql, doSpineSql } from './sql.js';
8
- import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, domainEventOf, facetEvents, readDeadLetters, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
8
+ import { actorOf, admitPeer, collectPeers, switchPeer, assertNoSecret, CAPABILITY_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, exchangeCapability, guardSecrets, mintBecomeCapability, redactSecrets, redactSecretText, moduleLog, resolveCapabilitySession, revokeCapabilityAsPlatform, domainEventOf, facetEvents, readDeadLetters, readLifecycleFlow, readHistory, readInvocation, readUndrainedOutbox, walkEventCause, walkEventEffects, } from '@substrat-run/kernel';
9
9
  import { createDoTupleChecker, createLocalControlPlaneReader } from './checker.js';
10
- import { repointScopeGrants } from '@substrat-run/kernel';
10
+ import { assertSpineTablesBuilt, dumpRowsInsert, isSpineTable, repointScopeGrants, spineColumnAdditions } from '@substrat-run/kernel';
11
11
  /**
12
12
  * The key marking a scope DO whose storage was destroyed (`destroyStorage`).
13
13
  * Written after `deleteAll()` so it survives the wipe, and never cleared: `reaped`
@@ -1190,6 +1190,10 @@ export function defineScopeDO(modules, bareOps) {
1190
1190
  deadLetters(input) {
1191
1191
  return readDeadLetters({ sql: doScopedSql(this.sql) }, { limit: input.limit, cursor: input.cursor });
1192
1192
  }
1193
+ /** #1744: one entity's lifecycle replayed over this scope's outbox, where it lives. */
1194
+ lifecycleFlow(input) {
1195
+ return readLifecycleFlow({ sql: doScopedSql(this.sql) }, input);
1196
+ }
1193
1197
  migrationBookmarks(limit = 20) {
1194
1198
  return this.sql
1195
1199
  .exec(`SELECT bookmark, taken_at, pending FROM _substrat_migration_bookmarks
@@ -1337,6 +1341,13 @@ export function defineScopeDO(modules, bareOps) {
1337
1341
  `the attachments of the entity its event names, and no others`);
1338
1342
  }
1339
1343
  }
1344
+ /** Does `subject` hold a role this scope can expand (#1665)? The #1659 lockout predicate
1345
+ * narrowed to one holder: a live role tuple, scope or tenant level, for a current role. */
1346
+ async hasEffectiveRoleGrantFor(tenantId, subject) {
1347
+ const q = effectiveRoleGrantQuery(tenantId, new Date().toISOString(), subject);
1348
+ const row = this.sql.exec(q.sql, ...q.params).toArray()[0];
1349
+ return row?.effective === 1;
1350
+ }
1340
1351
  /** Tombstone a scope tuple (K-21) — the row stays, the walk skips it. Returns
1341
1352
  * whether anything changed so a repeat revoke is a silent no-op. */
1342
1353
  async revokeTuple(subject, relation, object, at) {
@@ -3300,8 +3311,9 @@ export function defineScopeDO(modules, bareOps) {
3300
3311
  }
3301
3312
  /**
3302
3313
  * Load a dump into this (freshly-provisioned) scope — the write half of the fork
3303
- * (host.ts importScope). Drop-then-replay: the dump's schema is authoritative, so
3304
- * the provisioning schema is wiped and rebuilt from the dump verbatim.
3314
+ * (host.ts importScope). Drop-then-replay: the provisioning schema is wiped and rebuilt,
3315
+ * a vertical's tables from the dump's DDL and the `_substrat_*` spine from KERNEL_DDL
3316
+ * (#1883), then the dump's rows go back in.
3305
3317
  *
3306
3318
  * `destScopeId` is the scope being written INTO. A dump carries scope-level tuples
3307
3319
  * naming the scope it was captured from, so restoring one anywhere else needs them
@@ -3312,14 +3324,18 @@ export function defineScopeDO(modules, bareOps) {
3312
3324
  */
3313
3325
  /**
3314
3326
  * The additive spine-column migrations. KERNEL_DDL is all IF NOT EXISTS, so a
3315
- * scope DO created before a column keeps the old shape — and so does a table a
3316
- * DUMP replay just recreated from legacy DDL, which is why `importDump` re-runs
3317
- * this after the replay: without it, the next INSERT naming the column fails in
3318
- * this very instance. Attempt-and-tolerate: DO SQLite restricts PRAGMA, so there
3327
+ * scope DO created before a column keeps the old shape until this runs on its next
3328
+ * wake. `importDump` runs it too, after rebuilding the spine from KERNEL_DDL: there every
3329
+ * ALTER is a duplicate, since a restore never brings a legacy spine table (#1883), but the
3330
+ * pass also creates `_substrat_outbox_invocation`, which KERNEL_DDL deliberately does not
3331
+ * and the restore's DROP of the outbox took with it. Attempt-and-tolerate: DO SQLite restricts PRAGMA, so there
3319
3332
  * is no column probe, and a duplicate is the steady state after the first cold
3320
3333
  * start (same argument as ControlPlaneDO.addColumn).
3321
3334
  */
3322
3335
  applySpineColumnAdditions() {
3336
+ // Every column here is nullable with no DEFAULT (`attempts` is grandfathered): a restore may
3337
+ // have added it bare already, and then this ALTER is skipped as a duplicate (#1883).
3338
+ // `lint:spine-ddl` refuses one that is not.
3323
3339
  for (const alter of [
3324
3340
  'ALTER TABLE _substrat_tuples ADD COLUMN revoked_at TEXT',
3325
3341
  // Executor retry state (#100). The defaults read as "terminal", which is
@@ -3440,7 +3456,8 @@ export function defineScopeDO(modules, bareOps) {
3440
3456
  // cycles, and self-referencing rows within a single table.
3441
3457
  // A dump written before indexes were excluded may still carry them; skipped
3442
3458
  // rather than failing a restore over data about to be recomputed.
3443
- const replayable = tables.filter((t) => !isSearchIndexTable(t.name));
3459
+ // Matched without case, as SQLite resolves a table name (#1883 review).
3460
+ const replayable = tables.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
3444
3461
  // The dump is untrusted input (#1143). `SqlStorage.exec` runs every statement
3445
3462
  // in the string it is given, so a `ddl` with anything appended to its CREATE
3446
3463
  // TABLE executed that too — with entirely plain identifiers, which is why no
@@ -3462,47 +3479,53 @@ export function defineScopeDO(modules, bareOps) {
3462
3479
  // shadow table directly is an error, and `sqlite_master` order would reach one
3463
3480
  // before its virtual table.
3464
3481
  for (const { name } of existing) {
3465
- if (isSearchIndexTable(name))
3482
+ if (isSearchIndexTable(name.toLowerCase()))
3466
3483
  continue;
3467
3484
  this.sql.exec(`DROP TABLE IF EXISTS "${name}"`);
3468
3485
  }
3486
+ // A vertical's tables take the dump's own DDL. The spine never does (#1883): a dump
3487
+ // that declared a column the checker compares on as COLLATE NOCASE would otherwise
3488
+ // decide how this DO's permission checks match. Every `_substrat_*` table is built
3489
+ // from KERNEL_DDL instead, and the dump contributes only rows, by column name
3490
+ // (`spineRowsInsert`), a missing column taking the kernel's default.
3469
3491
  for (const t of replayable)
3470
- this.sql.exec(t.ddl);
3492
+ if (!isSpineTable(t.name))
3493
+ this.sql.exec(t.ddl);
3494
+ // KERNEL_DDL also builds what the dump did not carry (#321). A dump captured from a
3495
+ // WORLD that stores some `_substrat_*` tables ELSEWHERE carries only a subset — an
3496
+ // `@substrat-run/adapter-sqlite` scope file keeps `_substrat_roles` /
3497
+ // `_substrat_tenant_tuples` in its DIRECTORY database, so its per-scope dump omits
3498
+ // them, and without them the very next permission check would raise a bare `no such
3499
+ // table: _substrat_roles`. Roles land empty here and are re-projected by the
3500
+ // restore's repair leg (host.projectRolesLocal) — the spine's job is only to exist so
3501
+ // the checker can read it. The column pass follows, for the one outbox index KERNEL_DDL
3502
+ // leaves to it.
3503
+ for (const stmt of splitSqlStatements(KERNEL_DDL))
3504
+ this.sql.exec(stmt);
3505
+ this.applySpineColumnAdditions();
3506
+ const columnsOf = (name) => this.spineColumnsOf(name);
3507
+ assertSpineTablesBuilt(replayable.map((t) => t.name), columnsOf);
3508
+ // A spine column this kernel does not know (a dump from a newer one) is kept, as a plain
3509
+ // untyped column the checker never reads.
3471
3510
  for (const t of replayable) {
3472
- if (t.rows.length === 0)
3473
- continue;
3474
- const cols = t.columns.map((c) => `"${c}"`).join(', ');
3475
- const placeholders = t.columns.map(() => '?').join(', ');
3476
- const insert = `INSERT INTO "${t.name}" (${cols}) VALUES (${placeholders})`;
3511
+ if (isSpineTable(t.name))
3512
+ for (const alter of spineColumnAdditions(t, columnsOf(t.name)))
3513
+ this.sql.exec(alter);
3514
+ }
3515
+ for (const t of replayable) {
3516
+ const insert = dumpRowsInsert(t, columnsOf);
3477
3517
  for (const row of t.rows)
3478
3518
  this.sql.exec(insert, ...row);
3479
3519
  }
3480
- // Re-assert the kernel spine (#321). A dump captured from a WORLD that stores some
3481
- // `_substrat_*` tables ELSEWHERE carries only a subset — an `@substrat-run/adapter-
3482
- // sqlite` scope file, for instance, keeps `_substrat_roles`/`_substrat_tenant_tuples`
3483
- // in its DIRECTORY database, so its per-scope dump omits them. Replaying such a dump
3484
- // verbatim would leave this DO missing those spine tables, and the very next
3485
- // permission check would raise a bare `no such table: _substrat_roles`. KERNEL_DDL is
3486
- // all IF NOT EXISTS, so this recreates only what the dump did not carry (empty), and
3487
- // never disturbs a table the dump DID bring. Roles land empty here and are re-projected
3488
- // by the restore's repair leg (host.projectRolesLocal) — the spine's job is only to
3489
- // exist so the checker can read it.
3490
- for (const stmt of splitSqlStatements(KERNEL_DDL))
3491
- this.sql.exec(stmt);
3492
- // …and the additive columns, for the same reason KERNEL_DDL is re-asserted: a
3493
- // dump captured before a column existed replays DDL WITHOUT it, and IF NOT
3494
- // EXISTS cannot widen a table the dump did bring. Without this, the next emit
3495
- // in this instance fails with `no such column` until a cold start re-runs the
3496
- // constructor's pass.
3497
- this.applySpineColumnAdditions();
3498
3520
  // #1738: a dump's `provisioned_for` names the scope (and tenant) it was captured from,
3499
3521
  // not this one. Dropped, so a restore never carries a receipt over; the repair
3500
3522
  // projection that follows writes this scope's own.
3501
3523
  this.sql.exec(`DELETE FROM _substrat_meta WHERE key = 'provisioned_for'`);
3502
3524
  // Re-point the restored grants at THIS scope (after the spine exists, so a dump
3503
3525
  // that carried no tuples table still finds one here).
3504
- if (destScopeId)
3505
- this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact });
3526
+ if (destScopeId) {
3527
+ this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact }, new Date().toISOString());
3528
+ }
3506
3529
  // #1742: the recorded-off modules go back off INSIDE the replay's transaction, with
3507
3530
  // the spine and the re-point. A switch that throws rolls the whole restore back, so
3508
3531
  // the dump's grants never commit live without the switch that should cover them.
@@ -3574,12 +3597,24 @@ export function defineScopeDO(modules, bareOps) {
3574
3597
  *
3575
3598
  * Which rows move is `repointScopeGrants`'s (#1869): the dump's source scope, exactly.
3576
3599
  *
3577
- * `UPDATE OR REPLACE` because (subject, relation, object) is the primary key: if the
3578
- * dump already held a tuple for the destination scope, the rewritten row collapses
3579
- * onto it instead of failing the whole restore.
3600
+ * (subject, relation, object) is the primary key, so a moved row can meet one the dump
3601
+ * already holds for the destination scope. Which survives is that function's rule too
3602
+ * (#1882), with expiry judged at `now`, as this DO's checker judges it.
3580
3603
  */
3581
- rewriteScopeTuples(destScopeId, source) {
3582
- repointScopeGrants(this.switchSql(), destScopeId, source);
3604
+ rewriteScopeTuples(destScopeId, source, now) {
3605
+ repointScopeGrants(this.switchSql(), destScopeId, source, now);
3606
+ }
3607
+ /**
3608
+ * The columns of spine table `name` as KERNEL_DDL built it here, or `undefined` when it built
3609
+ * no such table — what `spineRowsInsert` judges a dump's columns against (#1883). Read off an
3610
+ * empty `SELECT`, because DO SQLite restricts PRAGMA. The name is matched without case, as
3611
+ * SQLite resolves a table name: a dump's `_Substrat_tuples` is the kernel's tuples table.
3612
+ */
3613
+ spineColumnsOf(name) {
3614
+ const built = this.sql
3615
+ .exec(`SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ? COLLATE NOCASE`, name)
3616
+ .toArray();
3617
+ return built.length === 0 ? undefined : this.sql.exec(`SELECT * FROM "${name}" LIMIT 0`).columnNames;
3583
3618
  }
3584
3619
  /**
3585
3620
  * Wipe this scope's storage — the reap half of deleteSnapshot (preview-and-