@substrat-run/adapter-cloudflare 0.138.0 → 0.139.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":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyDnD,OAAO,EA8CL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EA0FtB,MAAM,sBAAsB,CAAC;AAwC9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAkEtD;;;;;;;;;;;;;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;AAshB5E;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAgDtD;;;;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,CAumMjE"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyDnD,OAAO,EAoDL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EA8FtB,MAAM,sBAAsB,CAAC;AAwC9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AA8EtD;;;;;;;;;;;;;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;AAyhB5E;;;GAGG;AACH,MAAM,MAAM,yBAAyB,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;AAgDtD;;;;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,CAotMjE"}
package/dist/scope-do.js CHANGED
@@ -2,12 +2,12 @@ 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, 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';
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_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, 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, 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, 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, applyScopeRoleChange, changeScopeRole, revokeScopeRoles, scopeRoleHolders, 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, listIndexDdl, listIndexPlans, moduleMigrations, listQuery, cursorOf, searchLimit, searchMatchExpression, searchQuery, ATTACHMENT_TEXT_DDL, 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_FANOUT_LIMIT, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, decodeLiveWithin, } from './live-reads.js';
7
7
  import { replyOf } from './do-reply.js';
8
8
  import { OperationQueue } from './serialization.js';
9
9
  import { doScopedSql, doBuiltColumnsOf, doRedactionSql, doSpineSql } from './sql.js';
10
- import { actorOf, admitPeer, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, COPY_ORIGIN_DDL, SWITCH_FENCES_DDL, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, 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, CAPABILITY_EXCHANGE_OPERATION, capabilityAttachmentWriteRefused, createCapabilityVerbs, createEntityEdgeVerbs, createEntityStateVerbs, createTrashedReads, searchStateWhere, uncheckedView, addStatePlans, entityStateTriggerDdl, statefulTablesOf, assertEntityStateIntact, stateListIndexNames, 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 { createDoTupleChecker, createLocalControlPlaneReader, scopeTupleReader } from './checker.js';
12
12
  import { CARRIED_AWAY_KEY, COPY_MARK_CLEARED_KEY, KEPT_COPY_REFUSAL, KEPT_DIVERGENT_KEY, LOAD_STAMP_KEY, STORE_LOCAL_META_KEYS, WRITE_REVISION_KEY, carriedAwayDump, isCopyMarkInsert, isWriteStatement, assertNoSpineReference, assertSpineTablesBuilt, capabilitiesForLoad, clearCopyMarker, dumpRowsInsert, isSpineTable, markCopyOrigin, repointScopeGrants, settleCopiedWork, emittedHere, IS_COPY_SQL, isCopyLoad, isLifecycleWrite, readLifecycle, settleLifecycleAfterLoad, writeLifecycle, spineColumnAdditions } from '@substrat-run/kernel';
13
13
  /**
@@ -349,6 +349,9 @@ const KERNEL_DDL = `
349
349
  -- #1686: where a copied scope's data came from, and the event id its own events start above.
350
350
  -- Shared with the other adapter from @substrat-run/kernel; the column comments are there.
351
351
  ${COPY_ORIGIN_DDL}
352
+ -- #119: the kernel's authorization for one archive/trash move, read by the derived update
353
+ -- trigger. Shared with the other adapter from @substrat-run/kernel; the comments are there.
354
+ ${ENTITY_STATE_MOVES_DDL}
352
355
  `;
353
356
  /**
354
357
  * Workers RPC carries a plain `Error`'s MESSAGE faithfully and nothing else. A custom
@@ -708,6 +711,8 @@ export function defineScopeDO(modules, bareOps) {
708
711
  searchPlans = new Map();
709
712
  /** #811: the paged lists modules declare, by entity type. Same one-owner rule. */
710
713
  listPlans = new Map();
714
+ /** #119: entity type → its archive/trash plan, from every registered module. */
715
+ statePlans = new Map();
711
716
  /** entityType → the declared attachment gate (#473): read key + write key (default: read). */
712
717
  attachmentTargets = new Map();
713
718
  /**
@@ -819,8 +824,10 @@ export function defineScopeDO(modules, bareOps) {
819
824
  }
820
825
  this.searchPlans.set(plan.entityType, plan);
821
826
  }
827
+ // #119: the archive/trash plans, refusing a second owner of one entity type.
828
+ addStatePlans(this.statePlans, manifest.id, manifest.entityStates, manifest.permissions);
822
829
  // #811: the list indexes `lists` declares, same placement and same reason.
823
- for (const plan of listIndexPlans(manifest.id, manifest.lists)) {
830
+ for (const plan of listIndexPlans(manifest.id, manifest.lists, manifest.entityStates)) {
824
831
  const existing = this.listPlans.get(plan.entityType);
825
832
  if (existing) {
826
833
  throw new Error(`list: '${plan.entityType}' declares a paged list in both '${existing.moduleId}' and ` +
@@ -1306,13 +1313,12 @@ export function defineScopeDO(modules, bareOps) {
1306
1313
  const { hops: _hops, ...fact } = e;
1307
1314
  const event = structuredClone({ ...fact, source });
1308
1315
  const unit = unitOf(imp.moduleId, e.id, e.type);
1309
- this.causedBy = e.id;
1310
- this.unitInvocationId = unit.invocationId;
1311
1316
  try {
1312
1317
  await this.revision.transaction(async () => {
1313
1318
  // As the producer's principal: real checks against the grants this vertical's
1314
1319
  // `peers` gave it; its emits carry `{ vertical, scope }` and what they passed.
1315
- await imp.handler(this.importContext(tenantId, scopeId, peerSubject), event);
1320
+ // What it emits was emitted BECAUSE of the producer's event (#1237).
1321
+ await imp.handler(this.importContext(tenantId, scopeId, peerSubject, { causedBy: e.id, invocationId: unit.invocationId }), event);
1316
1322
  this.sql.exec(`INSERT INTO _substrat_deliveries (event_id, consumer_module, delivered_at, invocation_id)
1317
1323
  VALUES (?, ?, ?, NULL)`, e.id, imp.moduleId, new Date().toISOString());
1318
1324
  });
@@ -1325,10 +1331,6 @@ export function defineScopeDO(modules, bareOps) {
1325
1331
  result.deadLettered += 1;
1326
1332
  lines.write({ ...unit, outcome: 'dead-lettered', error: err });
1327
1333
  }
1328
- finally {
1329
- this.causedBy = null;
1330
- this.unitInvocationId = null;
1331
- }
1332
1334
  }
1333
1335
  if (!ran)
1334
1336
  result.duplicates += 1;
@@ -1345,8 +1347,12 @@ export function defineScopeDO(modules, bareOps) {
1345
1347
  * passes it (#1706). `principal` is a placeholder that the subject never reads, and there is
1346
1348
  * no operation, so the emitted rows' `operation` is NULL, as it is for any consumer.
1347
1349
  */
1348
- importContext(tenantId, scopeId, peerSubject) {
1349
- return this.operationContext(principalId.parse(ulid()), tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [], undefined, peerSubject);
1350
+ /** A module consumer's context: the system override, and the delivery it runs for (#2055). */
1351
+ consumerContext(tenantId, scopeId, moduleId, delivery) {
1352
+ return this.operationContext(this.systemPrincipal, tenantId, scopeId, { system: moduleId }, undefined, undefined, undefined, undefined, undefined, undefined, [], undefined, undefined, delivery);
1353
+ }
1354
+ importContext(tenantId, scopeId, peerSubject, delivery) {
1355
+ return this.operationContext(principalId.parse(ulid()), tenantId, scopeId, undefined, undefined, undefined, undefined, undefined, undefined, undefined, [], undefined, peerSubject, delivery);
1350
1356
  }
1351
1357
  async markEventsDrained(eventIds, at) {
1352
1358
  if (eventIds.length === 0)
@@ -2653,6 +2659,20 @@ export function defineScopeDO(modules, bareOps) {
2653
2659
  await this.ensureMigrations();
2654
2660
  return this.queue.enqueue(async () => this.revision.transactionSync(() => recordAttachmentText(doSpineSql(this.sql), attachmentId, outcome, new Date().toISOString())));
2655
2661
  }
2662
+ /**
2663
+ * #1575: start the scope's one-shot backfill, unless it is marked or holds no
2664
+ * attachments (`startAttachmentTextBackfill` reads before it writes, so a marked scope
2665
+ * writes nothing — and moves no write revision — on every drive).
2666
+ */
2667
+ async attachmentTextBackfillStart() {
2668
+ await this.ensureMigrations();
2669
+ await this.queue.enqueue(async () => this.revision.transactionSync(() => startAttachmentTextBackfill(doSpineSql(this.sql), ulid(), new Date().toISOString())));
2670
+ }
2671
+ /** #1575: one backfill batch, in one transaction (`queueAttachmentTextBackfill`). */
2672
+ async attachmentTextBackfillBatch(after) {
2673
+ await this.ensureMigrations();
2674
+ return this.queue.enqueue(async () => this.revision.transactionSync(() => queueAttachmentTextBackfill(doSpineSql(this.sql), after, ulid, new Date().toISOString())));
2675
+ }
2656
2676
  // -- attachments through a capability (#1686) -------------------------------
2657
2677
  // The coordinator hashed the session token; only the hash arrives. Each verb resolves it
2658
2678
  // INSIDE the queue, as `invoke` does, so nothing can revoke between the resolution and
@@ -2931,12 +2951,33 @@ export function defineScopeDO(modules, bareOps) {
2931
2951
  return this.queue.enqueue(async () => {
2932
2952
  const bound = await this.assignmentBound({ kind: 'principal', id: caller }, tenantId, scopeId, roleKey);
2933
2953
  if (bound?.covered) {
2934
- this.sql.exec(`INSERT OR REPLACE INTO _substrat_tuples (subject, relation, object, expires_at)
2935
- VALUES (?, ?, ?, NULL)`, `principal:${assignee}`, `role:${roleKey}`, `scope:${scopeId}`);
2954
+ applyScopeRoleChange(this.switchSql(), scopeId, assignee, { revoke: [], grant: roleKey }, new Date().toISOString());
2936
2955
  }
2937
2956
  return bound;
2938
2957
  });
2939
2958
  }
2959
+ /** The scope's live scope-level role assignments, or one principal's (#1150). */
2960
+ async scopeRoleHoldersFor(scopeId, principal) {
2961
+ await this.ensureMigrations();
2962
+ return this.queue.enqueue(() => scopeRoleHolders(this.switchSql(), scopeId, new Date().toISOString(), principal));
2963
+ }
2964
+ /**
2965
+ * The kernel's `changeScopeRole` in one serialized scope task (#1150). Its two refusals come
2966
+ * back as values: an error thrown here crosses the RPC flattened, so the coordinator types them.
2967
+ */
2968
+ async changeScopeRoleBoundedFor(tenantId, scopeId, caller, principal, from, to) {
2969
+ await this.ensureMigrations();
2970
+ return this.queue.enqueue(() => changeScopeRole(this.switchSql(), scopeId, principal, from, to, new Date().toISOString(), this.roleBound(caller, tenantId, scopeId), (run) => this.revision.transactionSync(run)));
2971
+ }
2972
+ /** The kernel's `revokeScopeRoles` in one serialized scope task (#1150). */
2973
+ async revokeScopeRolesBoundedFor(tenantId, scopeId, caller, principal) {
2974
+ await this.ensureMigrations();
2975
+ return this.queue.enqueue(() => revokeScopeRoles(this.switchSql(), scopeId, principal, new Date().toISOString(), this.roleBound(caller, tenantId, scopeId), (run) => this.revision.transactionSync(run)));
2976
+ }
2977
+ /** The caller's bound per role at the scope; `null` for a role the tenant does not define. */
2978
+ roleBound(caller, tenantId, scopeId) {
2979
+ return (roleKey) => this.assignmentBound({ kind: 'principal', id: caller }, tenantId, scopeId, roleKey);
2980
+ }
2940
2981
  /**
2941
2982
  * §5.1's assignment bound, resolved against the SAME role table the checker
2942
2983
  * expands — the tenant's projected role, not a vertical's compile-time `ROLES`
@@ -4464,6 +4505,23 @@ export function defineScopeDO(modules, bareOps) {
4464
4505
  for (const stmt of splitSqlStatements(searchIndexDdl(plan)))
4465
4506
  this.sql.exec(stmt);
4466
4507
  }
4508
+ // #119: the never-born-archived trigger went with the dropped table. Put back AFTER the
4509
+ // rows, which may legitimately arrive archived or trashed.
4510
+ for (const plan of this.statePlans.values()) {
4511
+ if (!present.has(plan.table))
4512
+ continue;
4513
+ for (const stmt of splitSqlStatements(entityStateTriggerDdl(plan)))
4514
+ this.sql.exec(stmt);
4515
+ }
4516
+ // #811 / #119: the derived list indexes went with the dropped table too, and a load never
4517
+ // put them back — an archivable entity's partial indexes are part of what the kernel
4518
+ // checks after DDL.
4519
+ for (const plan of this.listPlans.values()) {
4520
+ if (!present.has(plan.table))
4521
+ continue;
4522
+ for (const stmt of splitSqlStatements(listIndexDdl(plan)))
4523
+ this.sql.exec(stmt);
4524
+ }
4467
4525
  // #1335 / #1686: the outbox arrived with the dump, so this DO's event ids resume above it,
4468
4526
  // as on a wake. A copy's own events then sort above every copied one, which is what
4469
4527
  // `emittedHere()` relies on. A dump whose top id is no ULID leaves the floor where it was.
@@ -4792,36 +4850,17 @@ export function defineScopeDO(modules, bareOps) {
4792
4850
  return redacted;
4793
4851
  }
4794
4852
  // -- event dispatch (port of dispatch) ------------------------------------
4795
- /**
4796
- * #1237: the event currently being delivered, or null.
4797
- *
4798
- * The host carries a field of the same name for the admin log, and it is
4799
- * unreachable from here — it lives in the worker that holds the stub, while every
4800
- * emit runs inside this Durable Object. So the DO keeps its own, and the two are
4801
- * deliberately separate rather than one passed across the hop.
4802
- *
4803
- * One instance IS one scope, so this field is per-scope by construction. The
4804
- * SQLite twin has to say so explicitly (`ScopeRuntime.causedBy`): that host holds
4805
- * every scope in the process, so the same field on the host would be read by
4806
- * another scope's emit the moment a consumer awaits.
4807
- */
4808
- causedBy = null;
4809
4853
  /**
4810
4854
  * #1237: the invocation currently running in this DO, or null.
4811
4855
  *
4812
- * DO-local, like `causedBy` and for a simpler reason: a Durable Object IS one
4813
- * scope, so there is no other scope's call to confuse it with. It still has to be
4814
- * cleared, because the DO outlives the request and a value left set would stamp a
4815
- * later alarm-driven drain with a call it had nothing to do with.
4856
+ * DO-local: a Durable Object IS one scope, so there is no other scope's call to
4857
+ * confuse it with, and `invoke` sets and clears it inside its queued body, which no
4858
+ * other emitting body interleaves with. It still has to be cleared, because the DO
4859
+ * outlives the request and a value left set would stamp a later alarm-driven drain
4860
+ * with a call it had nothing to do with. A consumer's own cause is not kept here: it is
4861
+ * passed into the consumer's context (`ConsumerDelivery`, #2055).
4816
4862
  */
4817
4863
  invocationId = null;
4818
- /**
4819
- * #1901: the id a unit of async work with no call around it logs under — a consumer a
4820
- * seed or an import delivered — set for its handler's duration and cleared after. Read
4821
- * by `ctx.log` only, never the spine: an event a consumer emits outside a call still
4822
- * records no invocation (#1525), while its log lines join the consumer's own line.
4823
- */
4824
- unitInvocationId = null;
4825
4864
  async dispatch(tenantId, scopeId,
4826
4865
  /**
4827
4866
  * #1525: the call this drain is running in, or null. Every round of the loop
@@ -4884,16 +4923,15 @@ export function defineScopeDO(modules, bareOps) {
4884
4923
  lines.write({ ...unit, outcome: 'dead-lettered' });
4885
4924
  continue;
4886
4925
  }
4887
- // #1237: anything this consumer emits was emitted BECAUSE of this event
4888
- // — the step a backwards walk used to stop dead at, since a consumer
4889
- // emit records no operation either.
4890
- this.causedBy = event.id;
4891
- // #1901: the handler's `ctx.log` lines join this delivery's line.
4892
- this.unitInvocationId = unit.invocationId;
4893
4926
  try {
4894
4927
  await this.revision.transaction(async () => {
4895
- const ctx = this.operationContext(this.systemPrincipal, tenantId, scopeId, {
4896
- system: mod.id,
4928
+ // #1237: anything this consumer emits was emitted BECAUSE of this event —
4929
+ // the step a backwards walk used to stop dead at, since a consumer emit
4930
+ // records no operation either. #1901: its `ctx.log` lines join this
4931
+ // delivery's line.
4932
+ const ctx = this.consumerContext(tenantId, scopeId, mod.id, {
4933
+ causedBy: event.id,
4934
+ invocationId: unit.invocationId,
4897
4935
  });
4898
4936
  await consumer.handler(ctx, event);
4899
4937
  this.sql.exec(`INSERT INTO _substrat_deliveries
@@ -4911,14 +4949,6 @@ export function defineScopeDO(modules, bareOps) {
4911
4949
  VALUES (?, ?, ?, ?, ?)`, event.id, mod.id, new Date().toISOString(), String(err), invocationId);
4912
4950
  lines.write({ ...unit, outcome: 'dead-lettered', error: err });
4913
4951
  }
4914
- finally {
4915
- this.unitInvocationId = null;
4916
- // Cleared on BOTH paths. Left set, the id leaks onto every later emit
4917
- // this DO makes — an operation's own event stamped as caused by
4918
- // whatever was delivered last, which is worse than recording nothing
4919
- // because it reads as a fact.
4920
- this.causedBy = null;
4921
- }
4922
4952
  }
4923
4953
  }
4924
4954
  }
@@ -5033,11 +5063,14 @@ export function defineScopeDO(modules, bareOps) {
5033
5063
  * the queued body. Mutually exclusive with the other non-person callers; `principal` is
5034
5064
  * then a placeholder that the subject below never reads.
5035
5065
  */
5036
- peerSubject) {
5066
+ peerSubject,
5067
+ /** #2055: the delivery a consumer's or an import's context runs for; absent otherwise. */
5068
+ delivery) {
5037
5069
  const checker = this.checker;
5038
5070
  const relations = this.relations;
5039
5071
  const searchPlans = this.searchPlans;
5040
5072
  const listPlans = this.listPlans;
5073
+ const statePlans = this.statePlans;
5041
5074
  const sql = this.sql;
5042
5075
  const mintEventId = this.mintEventId;
5043
5076
  /**
@@ -5160,10 +5193,10 @@ export function defineScopeDO(modules, bareOps) {
5160
5193
  // about the deploy, so it never rides `DomainEvent` for module code to
5161
5194
  // branch on; it exists for the observability joins the column serves.
5162
5195
  this.env.SUBSTRAT_VERSION_ID ?? null,
5163
- // #1237: whatever delivery is in flight, if any — read off the DO the same
5164
- // way the version is read off its env. A fact about the surrounding
5165
- // dispatch, never envelope data module code could set or branch on.
5166
- this.causedBy,
5196
+ // #1237: the delivery this context was built for, if any — passed in, never read
5197
+ // off the DO (#2055). A fact about the surrounding dispatch, never envelope data
5198
+ // module code could set or branch on.
5199
+ delivery?.causedBy ?? null,
5167
5200
  // #1237: a fact about the surrounding CALL, like the version above.
5168
5201
  this.invocationId, full.payload === undefined ? null : JSON.stringify(full.payload));
5169
5202
  };
@@ -5173,7 +5206,11 @@ export function defineScopeDO(modules, bareOps) {
5173
5206
  // #1672: a capability's own id stands in so the type holds — it is not a person, and
5174
5207
  // the event actor says what it is instead. Every other door passes its own value.
5175
5208
  principal: capabilityId ? capabilityId : principal,
5176
- sql: guardSecrets(doScopedSql(sql), minted),
5209
+ sql: guardSecrets(doScopedSql(sql, statefulTablesOf(statePlans),
5210
+ // #119: after runtime DDL, the stateful tables must still carry what the kernel derived.
5211
+ statePlans.size
5212
+ ? () => assertEntityStateIntact(doSpineSql(sql), statePlans, stateListIndexNames(listPlans.values()))
5213
+ : undefined), minted),
5177
5214
  now: () => at,
5178
5215
  // #1746/#1747: the host stamps who and where; module code supplies only the template
5179
5216
  // and its fields. A consumer runs under the system override, so it logs as `system`.
@@ -5182,7 +5219,7 @@ export function defineScopeDO(modules, bareOps) {
5182
5219
  tenantId,
5183
5220
  scopeId,
5184
5221
  operation: operation ?? null,
5185
- invocationId: () => this.invocationId ?? this.unitInvocationId,
5222
+ invocationId: () => this.invocationId ?? delivery?.invocationId ?? null,
5186
5223
  principalKind: systemActor ? 'system' : subject.kind,
5187
5224
  // The string-safe redaction: `redactSecrets` parses its serialization back, and a
5188
5225
  // log's text is not JSON.
@@ -5240,7 +5277,9 @@ export function defineScopeDO(modules, bareOps) {
5240
5277
  const plan = searchPlans.get(entityType);
5241
5278
  if (!plan)
5242
5279
  throw new NotSearchable(entityType);
5243
- const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit));
5280
+ const q = searchQuery(plan, searchMatchExpression(term, plan.tokenizer), searchLimit(options?.limit),
5281
+ // #119: active rows unless the archive is asked for.
5282
+ searchStateWhere(statePlans, entityType, options?.view));
5244
5283
  return sql.exec(q.sql, ...q.params).toArray().map((row) => ({ entityType, id: row.id, rank: row.rank }));
5245
5284
  },
5246
5285
  /**
@@ -5260,10 +5299,12 @@ export function defineScopeDO(modules, bareOps) {
5260
5299
  order: params.order,
5261
5300
  cursor: params.cursor,
5262
5301
  filters: params.filters,
5302
+ // #119: the bin is the checked reader's, never this one's.
5303
+ view: uncheckedView('ctx.page', entityType, params.view),
5263
5304
  });
5264
5305
  const rows = sql.exec(q.sql, ...q.params).toArray();
5265
5306
  const last = rows.length >= limit ? rows[rows.length - 1] : undefined;
5266
- const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order);
5307
+ const nextCursor = last === undefined ? null : cursorOf(last, q.sortColumn, plan.idColumn, q.order, q.view);
5267
5308
  const page = { entries: rows, nextCursor };
5268
5309
  if (!params.total)
5269
5310
  return page;
@@ -5329,6 +5370,22 @@ export function defineScopeDO(modules, bareOps) {
5329
5370
  emit: (event) => writeEvent(event, 'kernel'),
5330
5371
  assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
5331
5372
  }),
5373
+ // #119: archive and trash — the pure adapter's wiring, over the raw spine seam.
5374
+ ...createEntityStateVerbs({
5375
+ sql: doSpineSql(sql),
5376
+ plans: statePlans,
5377
+ now: at,
5378
+ check: runCheck,
5379
+ emit: (event) => writeEvent(event, 'kernel'),
5380
+ assertWrites: (verb) => assertImpersonationWrites(impersonation, verb),
5381
+ }),
5382
+ ...createTrashedReads({
5383
+ query: (q, params) => sql.exec(q, ...params).toArray(),
5384
+ listPlans,
5385
+ searchPlans,
5386
+ statePlans,
5387
+ check: runCheck,
5388
+ }),
5332
5389
  entitlement: async (key) => {
5333
5390
  const held = await entitlementReader().listEntitlements(tenantId);
5334
5391
  return held.find((e) => e.key === key) ?? null;