@substrat-run/adapter-cloudflare 0.149.0 → 0.151.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;AA8DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EA2GtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAwBL,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;IAC7B;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAqkB5E;;;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,CAywO7E"}
1
+ {"version":3,"file":"scope-do.d.ts","sourceRoot":"","sources":["../src/scope-do.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AA8DnD,OAAO,EA2DL,KAAK,SAAS,EAsBd,KAAK,kBAAkB,EAEvB,KAAK,gBAAgB,EAkHtB,MAAM,sBAAsB,CAAC;AAoD9B,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,eAAe,CAAC;AAGtD,OAAO,EAwBL,kBAAkB,EAgFnB,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;IAC7B;;;;;OAKG;IACH,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAoFD,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAwkB5E;;;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,CAq0O7E"}
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, principalBecomeCapabilityInput, 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, 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, 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_CONDEMNED_DDL, condemnOrphanAttachments, 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, 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_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';
6
6
  import { isUpgradeRequest, readSubscription, LIVE_DECIDE_ATTEMPTS, LIVE_FANOUT_LIMIT, LIVE_MODE_HEADER, 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, } from './live-reads.js';
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, boundToPeerDeclarations, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, UPLOAD_LINK_DDL, createAttachmentVerbs, inspectUploadLink, redeemUploadLinkInTransaction, 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';
10
+ import { actorOf, admitPeer, boundToPeerDeclarations, collectPeers, switchPeer, peerSubjectRef, subjectGrantState, assertNoSecret, CAPABILITY_DDL, UPLOAD_LINK_DDL, createAttachmentVerbs, inspectUploadLink, redeemUploadLinkInTransaction, 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, checkHeldOperationsOf, countChecks, noteUncheckedCompletion, 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, orphanSweepVersionRefusal } from '@substrat-run/kernel';
12
12
  import { createDoTupleChecker, createLocalControlPlaneReader, scopeTupleReader } from './checker.js';
13
13
  import { settlePlatformRequestIn } from '@substrat-run/kernel';
@@ -355,6 +355,9 @@ const KERNEL_DDL = `
355
355
  -- #2314: the orphan sweep's tombstones, shared from @substrat-run/kernel. An id here
356
356
  -- never takes an attachment row again: the insert path refuses it.
357
357
  ${ATTACHMENT_CONDEMNED_DDL}
358
+ -- #2350: the orphan sweep's dry-run licence, shared from @substrat-run/kernel. Store-local:
359
+ -- never dumped, and every load drops it.
360
+ ${ATTACHMENT_SWEEP_LICENCE_DDL}
358
361
  -- #901: an entity's version is the ULID of the last event about it (a state move
359
362
  -- excepted, #2191), so the newest id per (entity_type, entity_id) is the read. The id
360
363
  -- column sits last so SQLite walks the matched range backwards instead of aggregating.
@@ -729,6 +732,8 @@ export function defineScopeDO(modules, bareOps) {
729
732
  purgeSchedules = new Map();
730
733
  /** #116: the operations that declared `idempotency: false` — refusals, not participants. */
731
734
  operationIdempotencyOptOut = new Set();
735
+ /** #2387: the operations held to a `ctx.check` — the pure adapter's set, from the same kernel read. */
736
+ operationCheckHeld = new Set();
732
737
  modules = new Map();
733
738
  /** #1705: what this deployment exports to other verticals and imports from them. */
734
739
  crossVertical = new CrossVerticalRegistry();
@@ -985,8 +990,11 @@ export function defineScopeDO(modules, bareOps) {
985
990
  `${[...unboundOptOuts].sort().join(', ')} — an opt-out on nothing reads as an ` +
986
991
  'exclusion someone decided, of an operation that does not exist');
987
992
  }
993
+ const checkHeld = checkHeldOperationsOf(registration.operations);
988
994
  for (const [name, handler] of Object.entries(registration.operations?.handlers ?? {})) {
989
995
  this.defineOperation(name, handler);
996
+ if (checkHeld.has(name) && this.operations.has(name))
997
+ this.operationCheckHeld.add(name);
990
998
  // The schema follows the HANDLER, not the name: a withdrawn operation
991
999
  // never binds, and has nothing to parse for.
992
1000
  const schema = declaredInputs[name];
@@ -1006,6 +1014,10 @@ export function defineScopeDO(modules, bareOps) {
1006
1014
  defineOperation(name, handler) {
1007
1015
  if (this.withdrawn.has(name))
1008
1016
  return; // withdrawn by another manifest — never binds
1017
+ // The coordinator and the pure host refuse a repeat the same way (`bindOperation`): a second
1018
+ // registration would otherwise replace the first without a word.
1019
+ if (this.operations.has(name))
1020
+ throw new Error(`operation already defined: ${name}`);
1009
1021
  this.operations.set(name, handler);
1010
1022
  }
1011
1023
  // -- RPC surface ----------------------------------------------------------
@@ -2057,7 +2069,8 @@ export function defineScopeDO(modules, bareOps) {
2057
2069
  ? withheldKeysFor(this.purgeOnlyKeys.get(systemDoor.moduleId), target, purging)
2058
2070
  : undefined;
2059
2071
  await this.revision.transaction(async () => {
2060
- const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, systemDoor, signals, impersonation, operation, capabilityId, minted, undefined, peerSubject, undefined, withheld);
2072
+ const checks = { calls: 0 };
2073
+ const ctx = this.operationContext(principal, tenantId, scopeId, undefined, connectionId, systemDoor, signals, impersonation, operation, capabilityId, minted, undefined, peerSubject, undefined, withheld, checks);
2061
2074
  // #116: a retry is answered from the recording, and nothing else runs
2062
2075
  // — not the guards, not the handler, not the permission check inside
2063
2076
  // it. Keyed by SUBJECT, so a caller only ever reaches its own
@@ -2090,7 +2103,11 @@ export function defineScopeDO(modules, bareOps) {
2090
2103
  const stamps = watchUpdatedStamps(doSpineSql(this.sql), this.updatedStampPlans);
2091
2104
  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);
2092
2105
  await this.runGuards(operation, ctx, parsed);
2106
+ // #2387: the handler's own checks only — the pure adapter's reset, for its reason.
2107
+ checks.calls = 0;
2093
2108
  result = await handler(ctx, parsed);
2109
+ // #2387: the pure adapter's record, through the same kernel function.
2110
+ noteUncheckedCompletion(ctx, operation, checks, this.operationCheckHeld.has(operation));
2094
2111
  stamps?.assertHeld(ctx, operation);
2095
2112
  if (guardedRef && invokeOptions?.ifMatch !== undefined) {
2096
2113
  assertIfMatch(guardedRef, invokeOptions.ifMatch, seen ?? null);
@@ -2887,14 +2904,46 @@ export function defineScopeDO(modules, bareOps) {
2887
2904
  throw misrouted;
2888
2905
  await this.refuseOrphanJudgement(tenantId, vouched);
2889
2906
  // The verdict alone (no ids, the host's first call) changes nothing: no migration, no repair.
2890
- if (ids.length === 0)
2907
+ // A destructive pass's verdict also asks for this store's own dry-run licence (#2350), so a
2908
+ // pass with nothing to condemn is refused all the same.
2909
+ if (ids.length === 0) {
2910
+ if (condemn)
2911
+ await this.queue.enqueue(async () => refuseUnlicensedOrphanSweep(doSpineSql(this.sql), now));
2891
2912
  return [];
2913
+ }
2892
2914
  await this.ensureMigrations();
2893
2915
  return this.queue.enqueue(async () => {
2894
2916
  await this.refuseOrphanJudgement(tenantId, vouched);
2895
2917
  if (!condemn)
2896
2918
  return rowlessAttachmentIds(doSpineSql(this.sql), ids);
2897
- return this.revision.transaction(async () => condemnOrphanAttachments(doSpineSql(this.sql), ids, now));
2919
+ return this.revision.transaction(async () => condemnLicensedOrphanAttachments(doSpineSql(this.sql), ids, now));
2920
+ });
2921
+ });
2922
+ }
2923
+ /**
2924
+ * #2350: the licence half of a pass, behind the same refusals as the judgement. `record` keeps
2925
+ * what a dry run listed as this store's licence; `read` answers the ids it lists; `consume` takes
2926
+ * ids out of it (what a destructive pass deleted, and what is gone some other way). Each write
2927
+ * happens only when it changes the licence, so a dry run that keeps finding the same orphans
2928
+ * leaves the write revision where it was. Only `read` answers ids; the others answer none.
2929
+ */
2930
+ orphanSweepLicenceReply(op, ids, now, tenantId, vouched = false, expectedVersionId) {
2931
+ return replyOf(async () => {
2932
+ const misrouted = orphanSweepVersionRefusal(this.env.SUBSTRAT_VERSION_ID ?? null, expectedVersionId, op !== 'record');
2933
+ if (misrouted)
2934
+ throw misrouted;
2935
+ await this.ensureMigrations();
2936
+ return this.queue.enqueue(async () => {
2937
+ await this.refuseOrphanJudgement(tenantId, vouched);
2938
+ if (op === 'read')
2939
+ return licensedOrphanIds(doSpineSql(this.sql));
2940
+ await this.revision.transaction(async () => {
2941
+ if (op === 'record')
2942
+ recordOrphanSweepLicence(doSpineSql(this.sql), ids, now);
2943
+ else
2944
+ consumeOrphanSweepLicence(doSpineSql(this.sql), ids);
2945
+ });
2946
+ return [];
2898
2947
  });
2899
2948
  });
2900
2949
  }
@@ -4907,7 +4956,10 @@ export function defineScopeDO(modules, bareOps) {
4907
4956
  // in a dump (`exportDumpStamped` hands the stamp over beside one); every load writes its own.
4908
4957
  const cursor = name === '_substrat_meta'
4909
4958
  ? this.sql.exec(`SELECT * FROM "${name}" WHERE key NOT IN (${STORE_LOCAL_META_KEYS.map(() => '?').join(', ')})`, ...STORE_LOCAL_META_KEYS)
4910
- : this.sql.exec(`SELECT * FROM "${name}"`);
4959
+ : // #2350: the orphan sweep's licence is this store's own, so its rows never leave in a dump.
4960
+ name === ATTACHMENT_SWEEP_LICENCE_TABLE
4961
+ ? this.sql.exec(`SELECT * FROM "${name}" WHERE 0`)
4962
+ : this.sql.exec(`SELECT * FROM "${name}"`);
4911
4963
  const columns = cursor.columnNames;
4912
4964
  const rows = Array.from(cursor.raw(), (row) => row);
4913
4965
  return { name, ddl: sql, columns, rows };
@@ -5270,6 +5322,10 @@ export function defineScopeDO(modules, bareOps) {
5270
5322
  // from before a suspension does not lift it; a copy keeps only its own.
5271
5323
  settleLifecycleAfterLoad(this.switchSql(), lifecycleBefore, markCopy === true || isCopyLoad(destScopeId, sourceScopeId));
5272
5324
  reconcileAttachmentText(doSpineSql(this.sql), ulid, now);
5325
+ // #2350: the orphan sweep's licence describes this store's last dry run, never the dump's.
5326
+ // A dump does not carry it; a hand-made one that does is dropped here all the same, so a
5327
+ // loaded store's next pass is a dry run.
5328
+ clearOrphanSweepLicence(doSpineSql(this.sql));
5273
5329
  // Re-point the restored grants at THIS scope (after the spine exists, so a dump
5274
5330
  // that carried no tuples table still finds one here).
5275
5331
  if (destScopeId) {
@@ -5881,7 +5937,9 @@ export function defineScopeDO(modules, bareOps) {
5881
5937
  /** #2055: the delivery a consumer's or an import's context runs for; absent otherwise. */
5882
5938
  delivery,
5883
5939
  /** #119: keys this context's checks refuse whatever the subject holds (`purgeOnlyKeysOf`). */
5884
- withheld) {
5940
+ withheld,
5941
+ /** #2387: where this invocation's checks are counted; absent where nothing reads the count. */
5942
+ checks) {
5885
5943
  const checker = this.checker;
5886
5944
  const relations = this.relations;
5887
5945
  const searchPlans = this.searchPlans;
@@ -5938,7 +5996,7 @@ export function defineScopeDO(modules, bareOps) {
5938
5996
  // Lifted so `grant` reuses the SAME check the operation itself passes —
5939
5997
  // a delegation check that could differ from the operation's would be a
5940
5998
  // second opinion about what the caller holds.
5941
- const runCheck = async (unparsed, entity) => {
5999
+ const decideUncounted = async (unparsed, entity) => {
5942
6000
  // #1642: parsed before the system actor's early return, which never reaches
5943
6001
  // the checker — a cast key would otherwise become that path's proof relation.
5944
6002
  const permission = assertPermissionKey(unparsed);
@@ -5967,6 +6025,8 @@ export function defineScopeDO(modules, bareOps) {
5967
6025
  }
5968
6026
  return decision;
5969
6027
  };
6028
+ // #2387: every check this context makes is counted, the kernel's own verbs' included.
6029
+ const runCheck = checks ? countChecks(decideUncounted, checks) : decideUncounted;
5970
6030
  const runCanAssign = async (roleKey) => {
5971
6031
  const bound = await this.assignmentBound(subject, tenantId, scopeId, roleKey);
5972
6032
  if (!bound) {