@substrat-run/adapter-cloudflare 0.127.0 → 0.129.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,CAq9JjE"}
package/dist/scope-do.js CHANGED
@@ -4,10 +4,10 @@ import { ATTACHMENT_ADDED, ATTACHMENT_REMOVED, attachmentRecord, domainEvent, do
4
4
  import { ulid, DO_SQL_LIMITS, createUlid, assertAllowed, ConnectionSealingKeyUnavailableError, noSealingKeyMessage, sealTo, assertReadOnlyQuery, entitlementDenial, platformRequestHistoryQuery, platformRequestOf, PLATFORM_REQUEST_COLUMNS, PLATFORM_REQUEST_REDACTION_SQL, platformRequestRedactionParams, platformRequestRedactionQuery, intentPayloadCarriesSubject, redactSubjectJobRuns, JOB_RUN_PATCH_SQL, JOB_STEP_RECORD_SQL, DELIVERY_ERROR_REDACTION_SQL, REDACTED_DELIVERY_NOTE, seatScopeTuple, effectiveRoleGrantQuery, switchRecordedOff, switchSystemSchedules, peerGrantsStatus, systemGrantsStatus, systemScheduleState, systemSwitchedOff, denialListQuery, denialSummaryQuery, denialTotalsQuery, DENIAL_WINDOW_QUERY, mapDenialRow, mapDenialSummaryBuckets, PermissionDenied, assertImpersonationWrites, assertModuleEnqueueableKind, impersonationStampOf, createAtomic, NotSearchable, isSearchIndexTable, searchIndexDdl, searchIndexPlans, NotListable, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, IDEMPOTENCY_DDL, 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, jobRunListLimit, } from '@substrat-run/kernel';
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
- 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';
7
+ import { doScopedSql, doSpineColumnsOf, 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, 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 { assertNoSpineReference, 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) {
@@ -2917,6 +2928,9 @@ export function defineScopeDO(modules, bareOps) {
2917
2928
  .exec('SELECT 1 FROM _substrat_migrations WHERE module_id = ? AND version = ?', moduleId, migration.version)
2918
2929
  .toArray()[0];
2919
2930
  if (!already) {
2931
+ // #1898: a migration runs on this DO's own handle, not `ctx.sql`, so the
2932
+ // spine guard's REFERENCES rule is applied here.
2933
+ assertNoSpineReference(migration.sql, `migration ${key}`);
2920
2934
  for (const stmt of splitSqlStatements(migration.sql)) {
2921
2935
  this.sql.exec(stmt);
2922
2936
  }
@@ -2929,6 +2943,10 @@ export function defineScopeDO(modules, bareOps) {
2929
2943
  // re-parsing the thrown message. The throw stays: `invoke` awaits
2930
2944
  // `ensureMigrations` on every operation and relies on the rejection to
2931
2945
  // fail closed, so resolving here would serve a half-migrated schema.
2946
+ // workerd logs this throw as `Uncaught (in promise)` when it leaves an RPC method,
2947
+ // as it does every exception an RPC call returns (a dump refusal, a denied check).
2948
+ // It is not an unhandled rejection: every caller awaits the memoised promise, and
2949
+ // the coordinator records the failure it receives (#1898 review).
2932
2950
  this.lastFailure = { version: key, error: err.message };
2933
2951
  throw new Error(`migration failed for ${key} — scope fails closed: ${err.message}`);
2934
2952
  }
@@ -3286,7 +3304,7 @@ export function defineScopeDO(modules, bareOps) {
3286
3304
  // carries the rows, never the index over them.
3287
3305
  const defs = this.sql
3288
3306
  .exec(`SELECT name, sql FROM sqlite_master
3289
- WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL
3307
+ WHERE type = 'table' AND name NOT GLOB 'sqlite_*' AND sql IS NOT NULL
3290
3308
  ORDER BY name`)
3291
3309
  .toArray().filter(({ name }) => !isSearchIndexTable(name));
3292
3310
  return defs.map(({ name, sql }) => {
@@ -3300,8 +3318,9 @@ export function defineScopeDO(modules, bareOps) {
3300
3318
  }
3301
3319
  /**
3302
3320
  * 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.
3321
+ * (host.ts importScope). Drop-then-replay: the provisioning schema is wiped and rebuilt,
3322
+ * a vertical's tables from the dump's DDL and the `_substrat_*` spine from KERNEL_DDL
3323
+ * (#1883), then the dump's rows go back in.
3305
3324
  *
3306
3325
  * `destScopeId` is the scope being written INTO. A dump carries scope-level tuples
3307
3326
  * naming the scope it was captured from, so restoring one anywhere else needs them
@@ -3312,14 +3331,18 @@ export function defineScopeDO(modules, bareOps) {
3312
3331
  */
3313
3332
  /**
3314
3333
  * 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
3334
+ * scope DO created before a column keeps the old shape until this runs on its next
3335
+ * wake. `importDump` runs it too, after rebuilding the spine from KERNEL_DDL: there every
3336
+ * ALTER is a duplicate, since a restore never brings a legacy spine table (#1883), but the
3337
+ * pass also creates `_substrat_outbox_invocation`, which KERNEL_DDL deliberately does not
3338
+ * and the restore's DROP of the outbox took with it. Attempt-and-tolerate: DO SQLite restricts PRAGMA, so there
3319
3339
  * is no column probe, and a duplicate is the steady state after the first cold
3320
3340
  * start (same argument as ControlPlaneDO.addColumn).
3321
3341
  */
3322
3342
  applySpineColumnAdditions() {
3343
+ // Every column here is nullable with no DEFAULT (`attempts` is grandfathered): a restore may
3344
+ // have added it bare already, and then this ALTER is skipped as a duplicate (#1883).
3345
+ // `lint:spine-ddl` refuses one that is not.
3323
3346
  for (const alter of [
3324
3347
  'ALTER TABLE _substrat_tuples ADD COLUMN revoked_at TEXT',
3325
3348
  // Executor retry state (#100). The defaults read as "terminal", which is
@@ -3440,7 +3463,8 @@ export function defineScopeDO(modules, bareOps) {
3440
3463
  // cycles, and self-referencing rows within a single table.
3441
3464
  // A dump written before indexes were excluded may still carry them; skipped
3442
3465
  // rather than failing a restore over data about to be recomputed.
3443
- const replayable = tables.filter((t) => !isSearchIndexTable(t.name));
3466
+ // Matched without case, as SQLite resolves a table name (#1883 review).
3467
+ const replayable = tables.filter((t) => !isSearchIndexTable(t.name.toLowerCase()));
3444
3468
  // The dump is untrusted input (#1143). `SqlStorage.exec` runs every statement
3445
3469
  // in the string it is given, so a `ddl` with anything appended to its CREATE
3446
3470
  // TABLE executed that too — with entirely plain identifiers, which is why no
@@ -3456,53 +3480,59 @@ export function defineScopeDO(modules, bareOps) {
3456
3480
  this.sql.exec('PRAGMA defer_foreign_keys = ON');
3457
3481
  // Real tables only; `sqlite_*` internals are auto-managed and un-droppable.
3458
3482
  const existing = this.sql
3459
- .exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`)
3483
+ .exec(`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT GLOB 'sqlite_*'`)
3460
3484
  .toArray();
3461
3485
  // Search index tables are left alone here and rebuilt below (#827): dropping a
3462
3486
  // shadow table directly is an error, and `sqlite_master` order would reach one
3463
3487
  // before its virtual table.
3464
3488
  for (const { name } of existing) {
3465
- if (isSearchIndexTable(name))
3489
+ if (isSearchIndexTable(name.toLowerCase()))
3466
3490
  continue;
3467
3491
  this.sql.exec(`DROP TABLE IF EXISTS "${name}"`);
3468
3492
  }
3493
+ // A vertical's tables take the dump's own DDL. The spine never does (#1883): a dump
3494
+ // that declared a column the checker compares on as COLLATE NOCASE would otherwise
3495
+ // decide how this DO's permission checks match. Every `_substrat_*` table is built
3496
+ // from KERNEL_DDL instead, and the dump contributes only rows, by column name
3497
+ // (`spineRowsInsert`), a missing column taking the kernel's default.
3469
3498
  for (const t of replayable)
3470
- this.sql.exec(t.ddl);
3499
+ if (!isSpineTable(t.name))
3500
+ this.sql.exec(t.ddl);
3501
+ // KERNEL_DDL also builds what the dump did not carry (#321). A dump captured from a
3502
+ // WORLD that stores some `_substrat_*` tables ELSEWHERE carries only a subset — an
3503
+ // `@substrat-run/adapter-sqlite` scope file keeps `_substrat_roles` /
3504
+ // `_substrat_tenant_tuples` in its DIRECTORY database, so its per-scope dump omits
3505
+ // them, and without them the very next permission check would raise a bare `no such
3506
+ // table: _substrat_roles`. Roles land empty here and are re-projected by the
3507
+ // restore's repair leg (host.projectRolesLocal) — the spine's job is only to exist so
3508
+ // the checker can read it. The column pass follows, for the one outbox index KERNEL_DDL
3509
+ // leaves to it.
3510
+ for (const stmt of splitSqlStatements(KERNEL_DDL))
3511
+ this.sql.exec(stmt);
3512
+ this.applySpineColumnAdditions();
3513
+ const columnsOf = (name) => doSpineColumnsOf(this.sql, name);
3514
+ assertSpineTablesBuilt(replayable.map((t) => t.name), columnsOf);
3515
+ // A spine column this kernel does not know (a dump from a newer one) is kept, as a plain
3516
+ // untyped column the checker never reads.
3471
3517
  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})`;
3518
+ if (isSpineTable(t.name))
3519
+ for (const alter of spineColumnAdditions(t, columnsOf(t.name)))
3520
+ this.sql.exec(alter);
3521
+ }
3522
+ for (const t of replayable) {
3523
+ const insert = dumpRowsInsert(t, columnsOf);
3477
3524
  for (const row of t.rows)
3478
3525
  this.sql.exec(insert, ...row);
3479
3526
  }
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
3527
  // #1738: a dump's `provisioned_for` names the scope (and tenant) it was captured from,
3499
3528
  // not this one. Dropped, so a restore never carries a receipt over; the repair
3500
3529
  // projection that follows writes this scope's own.
3501
3530
  this.sql.exec(`DELETE FROM _substrat_meta WHERE key = 'provisioned_for'`);
3502
3531
  // Re-point the restored grants at THIS scope (after the spine exists, so a dump
3503
3532
  // that carried no tuples table still finds one here).
3504
- if (destScopeId)
3505
- this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact });
3533
+ if (destScopeId) {
3534
+ this.rewriteScopeTuples(destScopeId, sourceScopeId && { scopeId: sourceScopeId, exact }, new Date().toISOString());
3535
+ }
3506
3536
  // #1742: the recorded-off modules go back off INSIDE the replay's transaction, with
3507
3537
  // the spine and the re-point. A switch that throws rolls the whole restore back, so
3508
3538
  // the dump's grants never commit live without the switch that should cover them.
@@ -3574,12 +3604,12 @@ export function defineScopeDO(modules, bareOps) {
3574
3604
  *
3575
3605
  * Which rows move is `repointScopeGrants`'s (#1869): the dump's source scope, exactly.
3576
3606
  *
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.
3607
+ * (subject, relation, object) is the primary key, so a moved row can meet one the dump
3608
+ * already holds for the destination scope. Which survives is that function's rule too
3609
+ * (#1882), with expiry judged at `now`, as this DO's checker judges it.
3580
3610
  */
3581
- rewriteScopeTuples(destScopeId, source) {
3582
- repointScopeGrants(this.switchSql(), destScopeId, source);
3611
+ rewriteScopeTuples(destScopeId, source, now) {
3612
+ repointScopeGrants(this.switchSql(), destScopeId, source, now);
3583
3613
  }
3584
3614
  /**
3585
3615
  * Wipe this scope's storage — the reap half of deleteSnapshot (preview-and-