@substrat-run/adapter-cloudflare 0.164.0 → 0.166.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.
package/dist/host.js CHANGED
@@ -3,7 +3,7 @@ import { isRewindRefusal, REWIND_REFUSED } from './rewind-refusal.js';
3
3
  import { isSystemDoorMoved, SYSTEM_DOOR_MOVED, SYSTEM_DOOR_REGATES } from './system-door.js';
4
4
  import { delegatedReadParams, operationSeriesCount, fromWireFailure, isSubstratError, exportReadInput, exportedBatch, importBatch, importResult, importState, importsOfManifestJson, connectionLandingsOfManifestJson, connectorsOfManifestJson, exportsOfManifestJson, assertNoBindingShapeBreak, importCursorMove, importCursorMoveAt, importCursorMoved, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, findingEntry, findingRuleEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, deploymentWireAuth, verticalChannel, verticalVersion, connection, capabilityExchange, mintedCapability, boundedBecomeMint, boundedBecomeRevoke, boundedUnbind, entityRef, becomeLinkState, principalBecomeCapabilityInput, capabilityGrant, principalId, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchOutcome, systemScheduleEntry, systemSwitchRecord, entitlementGrant, entitlementGrantInput, capabilityFilter, instant, meterReading, scopeStorageSample, requestDay, requestRollupDay, meterDay, meterDayTake, verticalOwnerPeriod, subjectRef, createConnectionInput, connectLinkFilter, connectLinkKey, consumeConnectLinkInput, mintConnectLinkInput, projectedConnectionGrant, projectedConnectionKey, moduleManifest, org as orgSchema, orgMembership, resolvedIdentity, identityMembership, roleDefinition, scope as scopeSchema, tenant as tenantSchema, tenantRole, delegatedReadRecord, ownerTransferAudit, memberChangeAudit, peerBindingAudit, connectionBindingAudit, copyMarkAudit, orphanSweepAudit, subjectShredReceipt, connectorDispatchKind, callsOfManifestJson, outboundOfManifestJson, SCOPE_GATE_REASONS, substratError, errorCodeOf, redrainEventsInput, coverage, peerCoverage, peerGrantsEntry, peerSwitch, peerSwitchOutcome, verticalCaller, verticalResolution, peerInstanceResolution, peerBinding as peerBindingSchema, verticalSlug, entityObjectRef, ATTACHMENT_BLOB_BINDING, blobStoreBindingName, lifecycleDelivery, scopeLifecycle, } from '@substrat-run/contracts';
5
5
  import { normalizeHostname, toRouteTarget } from './route-resolver.js';
6
- import { attachmentBlobKey, normalizeUploadMediaType, redeemOrReplayUploadLink, attachmentSha256, bytesStream, isSizedByteStream, openAttachmentByteStream, putAttachmentBytes, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, readableScopes, resolveScopeRecord, ulid, withheldOpsFailure, KEPT_COPY_REFUSAL, capabilityTokenHash, checkBecomeInput, assertBecomeLinkStateIds, plausibleSessionToken, foldModelUsage, backoffAt, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, jobRunOf, assertLeaseMs, runDueJobRuns, SYSTEM_DOOR_WAIT, startJobRun, attachmentTextJob, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, attachmentTextBackfillJob, searchLimit, searchMatchExpression, globalFetch, fetchLikeOf, deadlineFetch, assertRedrainWindow, platformRequestOf, undrainedEventsOf, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, recordedOffFromWire, peerSubjectRef, SWITCH_KINDS, reassertActionOf, reassertEntry, reassertOffRow, reassertOnRow, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditWarningOf, switchNotFoundMessage, recordWriteSuperseded, switchSubjectOf, switchSupersededMessage, withRecorded, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, CrossVerticalRegistry, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, assertPromoteReviewCurrent, promoteRaceRefusal, servingRaceRefusal, migratesFrom, servedFromRow, rebindRaceRefusal, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, strandJudgmentsOf, boundShapesOfColumn, installReconciled, collectPeers, peerCallerAt, peerSeats, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, splitManifestMigrations, publishedMigrationDigest, migrationChangeRefusal, unknownRoleError, assertRowLimit, assertRowOffset, emptyImportResult, INERT_SCOPE_REASON, lifecycleReceipt, lifecycleRefusal, scopeNotHeldRefusal, isPrimaryScope, isPrimaryScopeRow, asyncInvocationId, asyncLinePass, assertNoCallerPurge, purgeReportOf, purgeStillDue, assertBoundOperations, registerTrashTargets, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, reservedGrantRefusal, reservedRoleRefusal, reservedRoleSetRefusal, reservedShapeRefusal, SEAT_HOLDING_SCOPE_STATUSES, dumpRoleRefusal, dumpRoleState, memberAddedAudit, shapeTopUpBatch, verticalOwnerNow, } from '@substrat-run/kernel';
6
+ import { attachmentBlobKey, normalizeUploadMediaType, redeemOrReplayUploadLink, attachmentSha256, bytesStream, isSizedByteStream, openAttachmentByteStream, putAttachmentBytes, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, readableScopes, resolveScopeRecord, ulid, withheldOpsFailure, KEPT_COPY_REFUSAL, capabilityTokenHash, checkBecomeInput, assertBecomeLinkStateIds, plausibleSessionToken, foldModelUsage, backoffAt, resolveRetryPolicy, executorOutcomeOf, isDeliveryRefusal, refusalJournalText, isSecretBoxConfigured, unconfiguredSecretBox, createSubjectKeys, generateSealingKeyPair, openSealed, ImpersonationRefused, assertSessionUsable, impersonationRowValues, mapImpersonationRow, newImpersonationSession, jobRunOf, assertLeaseMs, runDueJobRuns, SYSTEM_DOOR_WAIT, startJobRun, attachmentTextJob, assertAttachmentExtractors, resolveAttachmentTextBounds, assertJobRegistrable, ATTACHMENT_TEXT_BACKFILL_JOB, ATTACHMENT_TEXT_JOB, kernelJobFor, attachmentTextBackfillJob, searchLimit, searchMatchExpression, globalFetch, fetchLikeOf, deadlineFetch, assertRedrainWindow, platformRequestOf, undrainedEventsOf, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, recordedOffFromWire, peerSubjectRef, SWITCH_KINDS, reassertActionOf, reassertEntry, reassertOffRow, reassertOnRow, switchActionOf, switchAuditSubject, recordAuditOutcome, auditedCapabilityRevoke, auditedScopePermissionWrite, auditWarningOf, switchNotFoundMessage, recordWriteSuperseded, switchSubjectOf, switchSupersededMessage, withRecorded, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, CrossVerticalRegistry, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, assertPromoteReviewCurrent, promoteRaceRefusal, servingRaceRefusal, migratesFrom, servedFromRow, rebindRaceRefusal, bindExportBreaksOf, bindExportBreakRefusal, scopeMoveOf, runningVersionOf, strandJudgmentsOf, boundShapesOfColumn, installReconciled, collectPeers, peerCallerAt, peerSeats, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, splitManifestMigrations, publishedMigrationDigest, migrationChangeRefusal, unknownRoleError, assertRowLimit, assertRowOffset, emptyImportResult, INERT_SCOPE_REASON, lifecycleReceipt, lifecycleRefusal, scopeNotHeldRefusal, isPrimaryScope, isPrimaryScopeRow, asyncInvocationId, asyncLinePass, assertNoCallerPurge, purgeReportOf, purgeStillDue, assertBoundOperations, registerTrashTargets, assertModuleDeclared, noteDeclaredKeys, noteReservedKeys, reservedGrantRefusal, reservedRoleRefusal, reservedRoleSetRefusal, reservedShapeRefusal, SEAT_HOLDING_SCOPE_STATUSES, dumpRoleRefusal, dumpRoleState, memberAddedAudit, shapeTopUpBatch, verticalOwnerNow, } from '@substrat-run/kernel';
7
7
  import { fenceAttachmentSweep, missingAttachmentStore, sweepOrphanAttachments, unsettledCopyRefusal } from '@substrat-run/kernel';
8
8
  import { assertWireAuthFilter, wireAuthAccessTarget, COPY_RESTORE_FENCE_LAPSED, scopeScriptCopyOf } from '@substrat-run/kernel';
9
9
  import { attributedView, isModuleErasureCounts, moduleErasurePlan, moduleRowsErased, callReservedStoreOperation, callScopeStore, observeStoreCall, replayStoreErasures, scopeStoreLifecycleOf, scopeStoreRunnersOf } from '@substrat-run/kernel';
@@ -1864,14 +1864,14 @@ export class CloudflareScopeHost {
1864
1864
  // the source of truth, carrying the D1 database_id Cloudflare assigned as the ref.
1865
1865
  const name = await tenantStoreDatabaseName(input.tenantId, input.vertical, input.binding);
1866
1866
  const ref = await d1.create(name);
1867
- const stored = await this.directory('putTenantStore', {
1867
+ const stored = await this.directory('putTenantStoreAndAudit', {
1868
1868
  tenantId: input.tenantId,
1869
1869
  vertical: input.vertical,
1870
1870
  binding: input.binding,
1871
1871
  kind: 'relational',
1872
1872
  ref,
1873
1873
  createdAt: new Date().toISOString(),
1874
- });
1874
+ }, this.adminEntry(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref }));
1875
1875
  if (stored.ref !== ref) {
1876
1876
  // A concurrent provision won the ledger write. One database is canonical — the
1877
1877
  // ledger's — so drop ours rather than orphan it (best-effort: a leaked delete
@@ -1879,7 +1879,6 @@ export class CloudflareScopeHost {
1879
1879
  await d1.remove(ref).catch(() => undefined);
1880
1880
  return { binding: input.binding, kind: 'relational', ref: stored.ref };
1881
1881
  }
1882
- await this.recordAdmin(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref });
1883
1882
  return { binding: input.binding, kind: 'relational', ref };
1884
1883
  }
1885
1884
  openTenantStore(handle) {
@@ -1925,19 +1924,18 @@ export class CloudflareScopeHost {
1925
1924
  return { binding: input.binding, kind: 'blob', ref: existing.ref };
1926
1925
  const name = await blobStoreBucketName(input.tenantId, input.vertical, input.binding);
1927
1926
  const ref = await r2.create(name);
1928
- const stored = await this.directory('putBlobStore', {
1927
+ const stored = await this.directory('putBlobStoreAndAudit', {
1929
1928
  tenantId: input.tenantId,
1930
1929
  vertical: input.vertical,
1931
1930
  binding: input.binding,
1932
1931
  kind: 'blob',
1933
1932
  ref,
1934
1933
  createdAt: new Date().toISOString(),
1935
- });
1934
+ }, this.adminEntry(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref }));
1936
1935
  if (stored.ref !== ref) {
1937
1936
  await r2.remove(ref).catch(() => undefined);
1938
1937
  return { binding: input.binding, kind: 'blob', ref: stored.ref };
1939
1938
  }
1940
- await this.recordAdmin(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref });
1941
1939
  return { binding: input.binding, kind: 'blob', ref };
1942
1940
  }
1943
1941
  /**
@@ -3246,11 +3244,14 @@ export class CloudflareScopeHost {
3246
3244
  * configured; the mint has no such reach and no caller needing one, since an owner claim link
3247
3245
  * is minted by the deployment itself behind `/internal/owner-claim`.
3248
3246
  */
3249
- async capabilityScopeStub(rec, scopeId, verb) {
3247
+ assertCapabilityScopeReach(rec, scopeId, verb) {
3250
3248
  if (this.servesScopesElsewhereNow && rec.vertical !== null) {
3251
3249
  throw substratError('unavailable', `${verb} cannot reach scope ${scopeId}: it is served by the '${rec.vertical}' ` +
3252
3250
  'deployment, and this host has no reach into it');
3253
3251
  }
3252
+ }
3253
+ async capabilityScopeStub(rec, scopeId, verb) {
3254
+ this.assertCapabilityScopeReach(rec, scopeId, verb);
3254
3255
  await this.migrateAndRecord(scopeId);
3255
3256
  return this.scopeStub(scopeId);
3256
3257
  }
@@ -3878,13 +3879,18 @@ export class CloudflareScopeHost {
3878
3879
  // #1713: after the directory moved and the move was audited, never before or instead.
3879
3880
  await this.deliverLifecycles(actor, { scopeId });
3880
3881
  };
3881
- const writeGrant = async (subject, permission, node, entity, expiresAt) => {
3882
+ const writeGrant = async (subject, permission, node, entity, expiresAt, audited = false) => {
3883
+ const writeTuple = async (scopeId, object) => {
3884
+ if (audited)
3885
+ unwrapReply(await this.scopeStub(scopeId).writeTupleAndAuditIntent(subject, `granted:${permission}`, object, expiresAt ?? null));
3886
+ else
3887
+ await this.writeScopeTuple(scopeId, subject, `granted:${permission}`, object, expiresAt ?? null);
3888
+ };
3882
3889
  if (entity) {
3883
- await this.writeScopeTuple(node.scopeId, subject, `granted:${permission}`, entityObjectRef(entity, 'HostAdmin'), // #1856: grant and grantToOrg both land here
3884
- expiresAt ?? null);
3890
+ await writeTuple(node.scopeId, entityObjectRef(entity, 'HostAdmin')); // #1856
3885
3891
  }
3886
3892
  else if (node.scopeId) {
3887
- await this.writeScopeTuple(node.scopeId, subject, `granted:${permission}`, `scope:${node.scopeId}`, expiresAt ?? null);
3893
+ await writeTuple(node.scopeId, `scope:${node.scopeId}`);
3888
3894
  }
3889
3895
  else {
3890
3896
  // #2526: a tenant-level grant is a directory tuple, written by the ControlPlaneDO in the
@@ -4461,15 +4467,33 @@ export class CloudflareScopeHost {
4461
4467
  await this.fanOut(tenantId);
4462
4468
  return;
4463
4469
  }
4464
- const refusal = await this.scopeStub(scopeId).reservedRoleRefusalFor(tenantId, assignment.roleKey, 'platform');
4470
+ if (await this.scopePermissionUsesLegacyPlaceholder(tenantId, scopeId)) {
4471
+ const refusal = await this.scopeStub(scopeId).reservedRoleRefusalFor(tenantId, assignment.roleKey, 'platform');
4472
+ if (refusal)
4473
+ throw substratError('forbidden', refusal);
4474
+ await this.writeScopeTuple(scopeId, `principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeId}`, null);
4475
+ await this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment);
4476
+ return;
4477
+ }
4478
+ const refusal = await auditedScopePermissionWrite({
4479
+ action: 'assignRole', request: { ...assignment },
4480
+ record: (after) => this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, after),
4481
+ write: async () => {
4482
+ const refusal = await this.scopeStub(scopeId).reservedRoleRefusalFor(tenantId, assignment.roleKey, 'platform');
4483
+ if (refusal)
4484
+ return refusal;
4485
+ unwrapReply(await this.scopeStub(scopeId).writeTupleAndAuditIntent(`principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeId}`, null));
4486
+ return null;
4487
+ },
4488
+ outcome: (refusal) => ({ phase: refusal ? 'refused' : 'applied', details: { changed: !refusal } }),
4489
+ logError: (message, fields) => console.error(message, fields),
4490
+ });
4465
4491
  if (refusal)
4466
4492
  throw substratError('forbidden', refusal);
4467
- await this.writeScopeTuple(scopeId, `principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeId}`, null);
4468
- await this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment);
4469
4493
  },
4470
4494
  unassignRole: async (actor, assignment) => {
4471
4495
  // Tombstone (K-21) — the checker skips revoked rows. A no-op returns false so
4472
- // a repeat unassign stays silent (no second audit row, no needless fan-out).
4496
+ // a repeat unassign is a refused no-op at a scope node, with no needless fan-out.
4473
4497
  const relation = `role:${assignment.roleKey}`;
4474
4498
  const { tenantId, scopeId } = assignment.node;
4475
4499
  if (!scopeId) {
@@ -4481,17 +4505,27 @@ export class CloudflareScopeHost {
4481
4505
  await this.fanOut(tenantId);
4482
4506
  return;
4483
4507
  }
4484
- const changed = await this.scopeStub(scopeId).revokeTuple(`principal:${assignment.principalId}`, relation, `scope:${scopeId}`, new Date().toISOString());
4485
- if (!changed)
4508
+ if (await this.scopePermissionUsesLegacyPlaceholder(tenantId, scopeId)) {
4509
+ const changed = await this.scopeStub(scopeId).revokeTuple(`principal:${assignment.principalId}`, relation, `scope:${scopeId}`, new Date().toISOString());
4510
+ if (changed)
4511
+ await this.recordAdmin(actor, 'unassignRole', { tenantId, scopeId }, assignment, null);
4486
4512
  return;
4487
- await this.recordAdmin(actor, 'unassignRole', { tenantId, scopeId }, assignment, null);
4513
+ }
4514
+ await auditedScopePermissionWrite({
4515
+ action: 'unassignRole', request: { ...assignment },
4516
+ record: (after) => this.recordAdmin(actor, 'unassignRole', { tenantId, scopeId }, assignment, after),
4517
+ write: () => this.scopeStub(scopeId).revokeTupleAndAuditIntent(`principal:${assignment.principalId}`, relation, `scope:${scopeId}`, new Date().toISOString()),
4518
+ outcome: (changed) => ({ phase: changed ? 'applied' : 'refused', details: { changed } }),
4519
+ logError: (message, fields) => console.error(message, fields),
4520
+ });
4488
4521
  },
4489
4522
  grant: async (actor, raw) => {
4490
4523
  // Parsed like its `grantToConnection`/`grantToSystem` siblings, not taken on
4491
4524
  // trust: the parse is where an `expiresAt` carrying a UTC offset is normalised
4492
4525
  // to Z text (#963), and liveness here is a lexicographic `expires_at > ?`.
4493
4526
  const grant = capabilityGrant.parse(raw);
4494
- await this.refuseReservedGrant([grant.permission], grant.node.scopeId); // #2430
4527
+ const legacy = !!grant.node.scopeId && await this.scopePermissionUsesLegacyPlaceholder(grant.node.tenantId, grant.node.scopeId);
4528
+ await this.refuseReservedGrant([grant.permission], legacy ? grant.node.scopeId : null); // #2430
4495
4529
  const subject = `principal:${grant.principalId}`;
4496
4530
  const target = { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId };
4497
4531
  if (!grant.node.scopeId && !grant.entity) {
@@ -4502,12 +4536,49 @@ export class CloudflareScopeHost {
4502
4536
  return;
4503
4537
  }
4504
4538
  // Scope-level and entity grants write scope tuples (already local): no fan-out.
4505
- await writeGrant(subject, grant.permission, grant.node, grant.entity, grant.expiresAt);
4506
- await this.recordAdmin(actor, 'grant', target, null, grant);
4539
+ if (legacy) {
4540
+ await writeGrant(subject, grant.permission, grant.node, grant.entity, grant.expiresAt);
4541
+ await this.recordAdmin(actor, 'grant', target, null, grant);
4542
+ return;
4543
+ }
4544
+ const refusal = await auditedScopePermissionWrite({
4545
+ action: 'grant', request: { ...grant },
4546
+ record: (after) => this.recordAdmin(actor, 'grant', target, null, after),
4547
+ write: async () => {
4548
+ const refusal = await this.scopeStub(grant.node.scopeId).reservedGrantRefusalFor([grant.permission]);
4549
+ if (refusal)
4550
+ return refusal;
4551
+ await writeGrant(subject, grant.permission, grant.node, grant.entity, grant.expiresAt, true);
4552
+ return null;
4553
+ },
4554
+ outcome: (refusal) => ({ phase: refusal ? 'refused' : 'applied', details: { changed: !refusal } }),
4555
+ logError: (message, fields) => console.error(message, fields),
4556
+ });
4557
+ if (refusal)
4558
+ throw substratError('forbidden', refusal);
4507
4559
  },
4508
4560
  grantEntityShape: async (actor, grant) => {
4509
- await this.grantEntityShapeLocal(grant.node.scopeId, grant.principalId, grant.entity, grant.permissions);
4510
- await this.recordAdmin(actor, 'grantEntityShape', grant.node, null, grant);
4561
+ if (await this.scopePermissionUsesLegacyPlaceholder(grant.node.tenantId, grant.node.scopeId)) {
4562
+ await this.grantEntityShapeLocal(grant.node.scopeId, grant.principalId, grant.entity, grant.permissions);
4563
+ await this.recordAdmin(actor, 'grantEntityShape', grant.node, null, grant);
4564
+ return;
4565
+ }
4566
+ await this.refuseReservedGrant(grant.permissions, null);
4567
+ const refusal = await auditedScopePermissionWrite({
4568
+ action: 'grantEntityShape', request: { ...grant },
4569
+ record: (after) => this.recordAdmin(actor, 'grantEntityShape', grant.node, null, after),
4570
+ write: async () => {
4571
+ const refusal = await this.scopeStub(grant.node.scopeId).reservedGrantRefusalFor([...grant.permissions]);
4572
+ if (refusal)
4573
+ return refusal;
4574
+ await this.scopeStub(grant.node.scopeId).grantEntityShapeAndAuditIntent(grant.principalId, grant.entity, grant.permissions);
4575
+ return null;
4576
+ },
4577
+ outcome: (refusal) => ({ phase: refusal ? 'refused' : 'applied', details: { changed: !refusal } }),
4578
+ logError: (message, fields) => console.error(message, fields),
4579
+ });
4580
+ if (refusal)
4581
+ throw substratError('forbidden', refusal);
4511
4582
  },
4512
4583
  reconcileEntityGrantShapes: async (actor, node, shapes, opts) => {
4513
4584
  // #2184: the bound types are the reconcile route's answer to the platform, not part of this
@@ -4599,13 +4670,25 @@ export class CloudflareScopeHost {
4599
4670
  const target = { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId };
4600
4671
  const after = { moduleId: grant.moduleId, permission: grant.permission, node: grant.node };
4601
4672
  if (grant.node.scopeId) {
4673
+ if (await this.scopePermissionUsesLegacyPlaceholder(grant.node.tenantId, grant.node.scopeId)) {
4674
+ const written = await this.scopeStub(grant.node.scopeId).writeSystemGrant(grant.moduleId, `granted:${grant.permission}`, `scope:${grant.node.scopeId}`, grant.expiresAt ?? null);
4675
+ if (!written)
4676
+ throw substratError('conflict', systemSwitchedOffMessage(grant.moduleId, grant.node.scopeId));
4677
+ await this.recordAdmin(actor, 'grantToSystem', target, null, after);
4678
+ return;
4679
+ }
4602
4680
  // #1666: refused while the module is switched off on this scope — checked and
4603
4681
  // written in one DO unit. Restore is the lever; a grant is not.
4604
- const written = await this.scopeStub(grant.node.scopeId).writeSystemGrant(grant.moduleId, `granted:${grant.permission}`, `scope:${grant.node.scopeId}`, grant.expiresAt ?? null);
4682
+ const written = await auditedScopePermissionWrite({
4683
+ action: 'grantToSystem', request: { ...after, expiresAt: grant.expiresAt ?? null, grantedBy: grant.grantedBy },
4684
+ record: (row) => this.recordAdmin(actor, 'grantToSystem', target, null, row),
4685
+ write: () => this.scopeStub(grant.node.scopeId).writeSystemGrantAndAuditIntent(grant.moduleId, `granted:${grant.permission}`, `scope:${grant.node.scopeId}`, grant.expiresAt ?? null),
4686
+ outcome: (written) => ({ phase: written ? 'applied' : 'refused', details: { changed: written } }),
4687
+ logError: (message, fields) => console.error(message, fields),
4688
+ });
4605
4689
  if (!written) {
4606
4690
  throw substratError('conflict', systemSwitchedOffMessage(grant.moduleId, grant.node.scopeId));
4607
4691
  }
4608
- await this.recordAdmin(actor, 'grantToSystem', target, null, after);
4609
4692
  return;
4610
4693
  }
4611
4694
  // #1743: refused while the record holds the module off on any scope — checked and
@@ -4649,10 +4732,8 @@ export class CloudflareScopeHost {
4649
4732
  return { switchedOffPeers, tenantHeldPeers, fences };
4650
4733
  },
4651
4734
  reassertSystemSwitches: reassertSystemSwitchesOf,
4652
- // #1672 — the platform's two capability verbs. Audited AFTER the write, on both, and
4653
- // the failure each leaves is the safe one: a mint whose audit row did not land never
4654
- // returned its secret, so nobody can ever exchange it; a revoke whose row did not land
4655
- // has still revoked. Neither the secret nor its hash is ever in before/after.
4735
+ // #1672 — the platform's two capability verbs. An intent precedes the scope write and
4736
+ // the outcome follows it. Neither the secret nor its hash is ever in before/after.
4656
4737
  mintCapability: async (actor, tenantId, scopeId, input) => {
4657
4738
  // Checked HERE as well as in the DO: a typed refusal thrown across the RPC arrives as
4658
4739
  // a bare message, and the caller would lose `validation_failed`. One function, so the
@@ -4660,17 +4741,22 @@ export class CloudflareScopeHost {
4660
4741
  checkBecomeInput(input, new Date().toISOString());
4661
4742
  await this.validateScopeAccess(tenantId, scopeId);
4662
4743
  const rec = await this.capabilityScopeRecord(tenantId, scopeId);
4663
- const stub = await this.capabilityScopeStub(rec, scopeId, 'mintCapability');
4664
- const minted = await stub.mintBecomeCapability(input, actor);
4665
- await this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId, vertical: rec.vertical }, null, {
4666
- capabilityId: minted.id,
4667
- mode: 'become',
4668
- principal: input.principal,
4669
- expiresAt: input.expiresAt,
4670
- maxUses: input.maxUses,
4671
- label: input.label ?? null,
4744
+ // A hosted refusal precedes intent; the local migrate happens inside the audited write.
4745
+ this.assertCapabilityScopeReach(rec, scopeId, 'mintCapability');
4746
+ return auditedScopePermissionWrite({
4747
+ action: 'mintCapability', request: {
4748
+ mode: 'become',
4749
+ principal: input.principal,
4750
+ expiresAt: input.expiresAt,
4751
+ maxUses: input.maxUses,
4752
+ label: input.label ?? null,
4753
+ },
4754
+ record: (after) => this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId, vertical: rec.vertical }, null, after),
4755
+ write: async () => (await this.capabilityScopeStub(rec, scopeId, 'mintCapability')).mintBecomeCapabilityAndAuditIntent(input, actor),
4756
+ outcome: (minted) => ({ phase: 'applied', details: { capabilityId: minted.id } }),
4757
+ onUnrecordedOutcome: (minted, error) => ({ ...minted, auditWarning: auditWarningOf('the capability mint', error) }),
4758
+ logError: (message, fields) => console.error(message, fields),
4672
4759
  });
4673
- return minted;
4674
4760
  },
4675
4761
  revokeCapability: async (actor, tenantId, scopeId, capabilityId) => {
4676
4762
  // #1686: a hosted scope's directory is in the deployment serving it, reached through the
@@ -4702,7 +4788,8 @@ export class CloudflareScopeHost {
4702
4788
  // The org must exist in the node's tenant. A grant to a phantom org looks
4703
4789
  // applied, resolves for nobody, and still shows up in the permission diff.
4704
4790
  await requireOrg(node.tenantId, orgId);
4705
- await this.refuseReservedGrant([permission], node.scopeId); // #2430: an org's members are people
4791
+ const legacy = !!node.scopeId && await this.scopePermissionUsesLegacyPlaceholder(node.tenantId, node.scopeId);
4792
+ await this.refuseReservedGrant([permission], legacy ? node.scopeId : null); // #2430: an org's members are people
4706
4793
  const target = { tenantId: node.tenantId, scopeId: node.scopeId };
4707
4794
  const after = { orgId, permission, node, entity };
4708
4795
  if (!node.scopeId && !entity) {
@@ -4711,8 +4798,26 @@ export class CloudflareScopeHost {
4711
4798
  await this.fanOut(node.tenantId);
4712
4799
  return;
4713
4800
  }
4714
- await writeGrant(`org:${orgId}`, permission, node, entity);
4715
- await this.recordAdmin(actor, 'grantToOrg', target, null, after);
4801
+ if (legacy) {
4802
+ await writeGrant(`org:${orgId}`, permission, node, entity);
4803
+ await this.recordAdmin(actor, 'grantToOrg', target, null, after);
4804
+ return;
4805
+ }
4806
+ const refusal = await auditedScopePermissionWrite({
4807
+ action: 'grantToOrg', request: after,
4808
+ record: (row) => this.recordAdmin(actor, 'grantToOrg', target, null, row),
4809
+ write: async () => {
4810
+ const refusal = await this.scopeStub(node.scopeId).reservedGrantRefusalFor([permission]);
4811
+ if (refusal)
4812
+ return refusal;
4813
+ await writeGrant(`org:${orgId}`, permission, node, entity, undefined, true);
4814
+ return null;
4815
+ },
4816
+ outcome: (refusal) => ({ phase: refusal ? 'refused' : 'applied', details: { changed: !refusal } }),
4817
+ logError: (message, fields) => console.error(message, fields),
4818
+ });
4819
+ if (refusal)
4820
+ throw substratError('forbidden', refusal);
4716
4821
  },
4717
4822
  // -- vertical + version registry (#31) ---------------------------------
4718
4823
  // -- the hostname map (K-26) -------------------------------------------
@@ -5510,9 +5615,7 @@ export class CloudflareScopeHost {
5510
5615
  // -- store-driver releases, rollouts and holds (D-63, #2300) -------------------
5511
5616
  recordStoreDriverRelease: async (actor, input) => {
5512
5617
  const at = new Date().toISOString();
5513
- const { record, created } = await this.directory('recordStoreDriverRelease', { ...input, actor, at });
5514
- if (created)
5515
- await this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, record);
5618
+ const { record } = await this.directory('recordStoreDriverReleaseAndAudit', { ...input, actor, at }, this.adminEntry(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, null));
5516
5619
  return record;
5517
5620
  },
5518
5621
  getStoreDriverRelease: async (actor, driver, sha256) => {
@@ -5520,6 +5623,24 @@ export class CloudflareScopeHost {
5520
5623
  await this.recordAccess(actor, 'getStoreDriverRelease', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
5521
5624
  return record;
5522
5625
  },
5626
+ storeDriverApprovals: async (actor, driver) => {
5627
+ const records = await this.cp.storeDriverApprovals(driver);
5628
+ await this.recordAccess(actor, 'storeDriverApprovals', { tenantId: null }, { driver }, records.length);
5629
+ return records;
5630
+ },
5631
+ liveStoreDriverApproval: async (actor, driver, sha256) => {
5632
+ const record = await this.cp.liveStoreDriverApproval(driver, sha256);
5633
+ await this.recordAccess(actor, 'liveStoreDriverApproval', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
5634
+ return record;
5635
+ },
5636
+ approveStoreDriverRelease: async (actor, input) => this.directory('approveStoreDriverReleaseAndAudit', { ...input, actor, at: new Date().toISOString() }, this.adminEntry(actor, 'approveStoreDriverRelease', { tenantId: null }, null, null)),
5637
+ storeDriverRevokeWalk: async (actor, approvalId) => {
5638
+ const record = await this.cp.storeDriverRevokeWalk(approvalId);
5639
+ await this.recordAccess(actor, 'storeDriverRevokeWalk', { tenantId: null }, { approvalId }, record ? 1 : 0);
5640
+ return record;
5641
+ },
5642
+ recordStoreDriverRevokeWalk: async (actor, input) => this.directory('recordStoreDriverRevokeWalk', { ...input, actor }),
5643
+ revokeStoreDriverRelease: async (actor, input) => this.directory('revokeStoreDriverReleaseAndAudit', { ...input, actor, at: new Date().toISOString() }, this.adminEntry(actor, 'revokeStoreDriverRelease', { tenantId: null }, null, null)),
5523
5644
  storeDriverState: async (actor, driver) => {
5524
5645
  const state = await this.cp.storeDriverState(driver);
5525
5646
  await this.recordAccess(actor, 'storeDriverState', { tenantId: null }, { driver }, state.rollouts.length);
@@ -5545,13 +5666,11 @@ export class CloudflareScopeHost {
5545
5666
  await this.cp.releaseStoreDriverHold(input);
5546
5667
  },
5547
5668
  beginStoreDriverRollout: async (actor, input) => {
5548
- const row = await this.directory('beginStoreDriverRollout', { ...input, actor, at: new Date().toISOString() });
5549
- await this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row);
5669
+ const row = await this.directory('beginStoreDriverRolloutAndAudit', { ...input, actor, at: new Date().toISOString() }, this.adminEntry(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, null));
5550
5670
  return row;
5551
5671
  },
5552
5672
  finishStoreDriverRollout: async (actor, input) => {
5553
- const row = await this.directory('finishStoreDriverRollout', { ...input, at: new Date().toISOString() });
5554
- await this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row);
5673
+ const row = await this.directory('finishStoreDriverRolloutAndAudit', { ...input, at: new Date().toISOString() }, this.adminEntry(actor, 'rollOutStoreDriver', { tenantId: null }, null, null));
5555
5674
  return row;
5556
5675
  },
5557
5676
  storeDriverRebuild: async (actor, driver) => {
@@ -5850,8 +5969,7 @@ export class CloudflareScopeHost {
5850
5969
  // admin log too, so an entry written first would be overwritten by the very act
5851
5970
  // it records, leaving the platform's most consequential write invisible.
5852
5971
  const before = (await this.cp.listTenants({ limit: 1000 })).length;
5853
- await this.cp.importDump(dump.tables);
5854
- await this.recordAdmin(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length });
5972
+ await this.directory('importDumpAndAudit', dump.tables, this.adminEntry(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length }));
5855
5973
  },
5856
5974
  activateScope: async (actor, tenantId, scopeId) => {
5857
5975
  // Idempotent on `active`, unaudited because nothing changed. Provisioning is
@@ -6079,11 +6197,10 @@ export class CloudflareScopeHost {
6079
6197
  // anyway leaves the admin log recording an access that could not happen.
6080
6198
  await this.assertScope(input.tenantId, input.scopeId);
6081
6199
  const session = newImpersonationSession(ulid(), actor, input, new Date().toISOString());
6082
- await this.cp.writeImpersonation(impersonationRowValues(session));
6200
+ await this.directory('writeImpersonationAndAudit', impersonationRowValues(session), this.adminEntry(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session));
6083
6201
  // BEFORE the session is usable, so the log entry precedes every row it
6084
6202
  // will ever touch. The reason rides in `after`: it is what a review reads
6085
6203
  // first, and a log saying a session opened but not why is half a record.
6086
- await this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session);
6087
6204
  return session;
6088
6205
  },
6089
6206
  endImpersonation: async (actor, id) => {
@@ -6095,9 +6212,9 @@ export class CloudflareScopeHost {
6095
6212
  // already stopped it.
6096
6213
  if (before.endedAt !== null)
6097
6214
  return before;
6098
- await this.cp.endImpersonation(String(id), new Date().toISOString());
6215
+ const endedAt = new Date().toISOString();
6216
+ await this.directory('endImpersonationAndAudit', String(id), endedAt, this.adminEntry(actor, 'endImpersonation', { tenantId: before.tenantId, scopeId: before.scopeId }, before, { ...before, endedAt }));
6099
6217
  const after = mapImpersonationRow((await this.cp.readImpersonation(String(id))));
6100
- await this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after);
6101
6218
  return after;
6102
6219
  },
6103
6220
  listImpersonations: async (actor, filter) => {
@@ -6175,14 +6292,18 @@ export class CloudflareScopeHost {
6175
6292
  await this.recordAccess(actor, 'readMeters', { tenantId: only ?? null }, filter ?? null, reading.perTenant.length);
6176
6293
  return meterReading.parse(reading);
6177
6294
  },
6295
+ readTenantStorageGauge: async (actor, tenantId, limit) => {
6296
+ const result = await this.cp.readTenantStorageGauge(tenantId, limit);
6297
+ await this.recordAccess(actor, 'readTenantStorageGauge', { tenantId }, { limit }, result.gauge.sampled);
6298
+ return result;
6299
+ },
6178
6300
  registerIdentityPool: async (actor, input) => {
6179
6301
  const parsed = identityPool.parse(input);
6180
- const created = await this.directory('registerIdentityPool', parsed.provider, parsed.topology, parsed.tenantId, new Date().toISOString());
6302
+ const created = await this.directory('registerIdentityPoolAndAudit', parsed.provider, parsed.topology, parsed.tenantId, new Date().toISOString(), this.adminEntry(actor, 'registerIdentityPool', { tenantId: parsed.tenantId }, null, parsed));
6181
6303
  if (!created)
6182
6304
  return; // identical registration is idempotent, unaudited
6183
6305
  // Null tenant for a central pool: it belongs to no single tenant, which is
6184
6306
  // what made the admin log's tenantId nullable.
6185
- await this.recordAdmin(actor, 'registerIdentityPool', { tenantId: parsed.tenantId }, null, parsed);
6186
6307
  },
6187
6308
  getIdentityPool: async (actor, provider) => {
6188
6309
  const r = await this.cp.readPool(provider);
@@ -6239,7 +6360,7 @@ export class CloudflareScopeHost {
6239
6360
  // never seen a plaintext credential.
6240
6361
  const sealed = await this.secretBox.seal(JSON.stringify(input.secret));
6241
6362
  const now = new Date().toISOString();
6242
- await this.cp.insertConnection({
6363
+ await this.directory('insertConnectionAndAudit', {
6243
6364
  id: input.id,
6244
6365
  tenantId: input.tenantId,
6245
6366
  vertical: input.vertical,
@@ -6254,17 +6375,16 @@ export class CloudflareScopeHost {
6254
6375
  createdAt: now,
6255
6376
  keyId: sealed.keyId,
6256
6377
  ciphertext: sealed.ciphertext,
6257
- });
6258
- // METADATA ONLY — the admin log is append-only, so a credential written
6259
- // here could never be removed.
6260
- await this.recordAdmin(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
6378
+ }, this.adminEntry(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
6261
6379
  id: input.id,
6262
6380
  provider: input.provider,
6263
6381
  label: input.label,
6264
6382
  scopes: input.scopes,
6265
6383
  externalAccountRef: input.externalAccountRef ?? null,
6266
6384
  createdBy: input.createdBy ?? actor,
6267
- });
6385
+ }));
6386
+ // METADATA ONLY — the admin log is append-only, so a credential written
6387
+ // here could never be removed.
6268
6388
  },
6269
6389
  connectionSealingKey: async (id) => this.ensureSealingKey(id),
6270
6390
  connectionSealingKeys: async (tenantId, vertical) => {
@@ -6307,26 +6427,22 @@ export class CloudflareScopeHost {
6307
6427
  throw new Error(`connection not found: ${id}`);
6308
6428
  const sealed = await this.secretBox.seal(JSON.stringify(connectionSecret.parse(secret)));
6309
6429
  const now = new Date().toISOString();
6310
- await this.cp.updateConnectionSecret(id, sealed.keyId, sealed.ciphertext, expiresAt === undefined ? row.expires_at : expiresAt, now);
6311
- await this.recordAdmin(actor, 'updateConnectionSecret', { tenantId: row.tenant_id, vertical: row.vertical }, null, {
6430
+ await this.directory('updateConnectionSecretAndAudit', id, sealed.keyId, sealed.ciphertext, expiresAt === undefined ? row.expires_at : expiresAt, now, this.adminEntry(actor, 'updateConnectionSecret', { tenantId: row.tenant_id, vertical: row.vertical }, null, {
6312
6431
  id,
6313
6432
  provider: row.provider,
6314
6433
  rotatedAt: now,
6315
6434
  expiresAt: expiresAt === undefined ? row.expires_at : expiresAt,
6316
- // §3.5.1's attribution, rotate-side: the authorizing tenant principal,
6317
- // never laundered into the actor column.
6318
6435
  ...(opts?.rotatedBy ? { rotatedBy: opts.rotatedBy } : {}),
6319
- });
6436
+ }));
6320
6437
  },
6321
6438
  revokeConnection: async (actor, id) => {
6322
6439
  const row = await this.cp.readConnection(id);
6323
6440
  if (!row)
6324
6441
  throw new Error(`connection not found: ${id}`);
6325
6442
  const now = new Date().toISOString();
6326
- const changed = await this.cp.revokeConnection(id, now);
6443
+ const changed = await this.directory('revokeConnectionAndAudit', id, now, this.adminEntry(actor, 'revokeConnection', { tenantId: row.tenant_id, vertical: row.vertical }, { status: row.status }, { id, provider: row.provider, status: 'revoked', revokedAt: now }));
6327
6444
  if (!changed)
6328
6445
  return; // idempotent, and a no-op is not audited
6329
- await this.recordAdmin(actor, 'revokeConnection', { tenantId: row.tenant_id, vertical: row.vertical }, { status: row.status }, { id, provider: row.provider, status: 'revoked', revokedAt: now });
6330
6446
  },
6331
6447
  // -- a vertical's mailed connect links (connections.md §3.5.4) --------------
6332
6448
  // Parsed here, run in the ControlPlaneDO as the kernel's statements (shared with
@@ -6424,11 +6540,10 @@ export class CloudflareScopeHost {
6424
6540
  if (pool.topology === 'tenant-bound' && pool.tenant_id !== parsed.tenantId) {
6425
6541
  throw substratError('conflict', `identity pool '${parsed.provider}' is bound to tenant ${pool.tenant_id} and cannot link into ${parsed.tenantId}`);
6426
6542
  }
6427
- const changed = await this.directory('linkIdentity', parsed.provider, parsed.externalId, parsed.principal, parsed.tenantId, parsed.scopeId ?? null, new Date().toISOString());
6543
+ const changed = await this.directory('linkIdentityAndAudit', parsed.provider, parsed.externalId, parsed.principal, parsed.tenantId, parsed.scopeId ?? null, new Date().toISOString(), this.adminEntry(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal }));
6428
6544
  // Idempotent: an identity already bound is a no-op, not audited.
6429
6545
  if (!changed)
6430
6546
  return;
6431
- await this.recordAdmin(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal });
6432
6547
  // #406: an identity link is a tenant-level write, so it fans out into the tenant's
6433
6548
  // projected scopes exactly as entitlements do (#304) — a no-op unless scope-local
6434
6549
  // projection is on; the reconcile sweep repairs any drop.
@@ -6437,10 +6552,9 @@ export class CloudflareScopeHost {
6437
6552
  unlinkIdentity: async (actor, tenantId, principal) => {
6438
6553
  // DELETE by principal (audit is the log), so the caller who removed a member
6439
6554
  // can sever their login without knowing the external subject. Idempotent.
6440
- const changed = await this.cp.unlinkIdentity(tenantId, principal);
6555
+ const changed = await this.directory('unlinkIdentityAndAudit', tenantId, principal, this.adminEntry(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null));
6441
6556
  if (!changed)
6442
6557
  return;
6443
- await this.recordAdmin(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null);
6444
6558
  // The unlink must reach the projected scopes so the severed login stops resolving
6445
6559
  // there — this is what makes revocation durable at request time (#406); a dropped
6446
6560
  // fan-out is repaired by the reconcile sweep.
@@ -6532,24 +6646,11 @@ export class CloudflareScopeHost {
6532
6646
  }));
6533
6647
  },
6534
6648
  markAccessLogDrained: async (actor, upToId, drainedAt) => {
6535
- const drained = await this.cp.markAccessLogDrained(upToId, drainedAt);
6536
- if (drained > 0) {
6537
- // The payload is the APPLIED state, so it belongs in `after` (contracts'
6538
- // adminLogEntry: before = prior state, after = the applied payload).
6539
- await this.recordAdmin(actor, 'drainAccessLog', { tenantId: null }, null, { drained, upToId, drainedAt });
6540
- }
6541
- return drained;
6649
+ return this.directory('markAccessLogDrainedAndAudit', upToId, drainedAt, this.adminEntry(actor, 'drainAccessLog', { tenantId: null }, null, null));
6542
6650
  },
6543
6651
  pruneAccessLog: async (actor, limit) => {
6544
6652
  // Checked here as well as in the directory, so the refusal keeps its code across the hop.
6545
- const pruned = await this.cp.pruneAccessLog(assertRowLimit('limit', limit));
6546
- if (pruned > 0) {
6547
- // The payload is the APPLIED state, so it belongs in `after` (contracts'
6548
- // adminLogEntry: before = prior state, after = the applied payload) — the
6549
- // same shape as drainAccessLog's row above (#557).
6550
- await this.recordAdmin(actor, 'pruneAccessLog', { tenantId: null }, null, { pruned });
6551
- }
6552
- return pruned;
6653
+ return this.directory('pruneAccessLogAndAudit', assertRowLimit('limit', limit), this.adminEntry(actor, 'pruneAccessLog', { tenantId: null }, null, null));
6553
6654
  },
6554
6655
  auditLog: async (actor, filter) => {
6555
6656
  const rows = await this.cp.auditLog({
@@ -6675,13 +6776,11 @@ export class CloudflareScopeHost {
6675
6776
  return rows.map((r) => issueEntry.parse(r));
6676
6777
  },
6677
6778
  setIssueStatus: async (actor, fingerprint, status) => {
6678
- const change = await this.cp.setIssueStatus(fingerprint, status, new Date().toISOString());
6779
+ const change = await this.directory('setIssueStatusAndAudit', fingerprint, status, new Date().toISOString(), this.adminEntry(actor, 'setIssueStatus', { tenantId: null }, null, null));
6679
6780
  if (!change)
6680
6781
  return undefined;
6681
- const before = issueEntry.parse(change.before);
6682
6782
  const after = issueEntry.parse(change.after);
6683
- // A lifecycle flip is a staff mutation — audited with the diff (K-33).
6684
- await this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: before.status }, { fingerprint, status: after.status });
6783
+ // A lifecycle flip is recorded in the same directory unit as the state write.
6685
6784
  return after;
6686
6785
  },
6687
6786
  listFindings: async (actor, filter) => {
@@ -6734,6 +6833,16 @@ export class CloudflareScopeHost {
6734
6833
  await this.recordAccess(actor, 'listRequestRollupDays', { tenantId: null }, filter ?? null, rows.length);
6735
6834
  return rows;
6736
6835
  },
6836
+ listTenantRequestMeter: async (actor, tenantId, since, until, limit) => {
6837
+ const rows = await this.cp.listTenantRequestMeter(tenantId, since, until, limit);
6838
+ await this.recordAccess(actor, 'listTenantRequestMeter', { tenantId }, { since, until, limit }, rows.length);
6839
+ return rows;
6840
+ },
6841
+ listRequestMeterDays: async (actor, since, until) => {
6842
+ const rows = await this.cp.listRequestMeterDays(since, until);
6843
+ await this.recordAccess(actor, 'listRequestMeterDays', { tenantId: null }, { since, until }, rows.length);
6844
+ return rows;
6845
+ },
6737
6846
  pruneRequestDays: async (_actor, limit, retentionMonths) => this.cp.pruneRequestDays(assertRowLimit('limit', limit), retentionMonths),
6738
6847
  recordMeterDay: async (_actor, input) => this.cp.recordMeterDay(input),
6739
6848
  listMeterDays: async (actor, filter) => {
@@ -6775,6 +6884,11 @@ export class CloudflareScopeHost {
6775
6884
  await this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
6776
6885
  return rows.map(modelUsageEntryOf);
6777
6886
  },
6887
+ listTenantModelUsageMeter: async (actor, filter) => {
6888
+ const rows = await this.cp.listTenantModelUsageMeter(filter);
6889
+ await this.recordAccess(actor, 'listTenantModelUsageMeter', { tenantId: filter.tenantId }, { since: filter.since, until: filter.until, limit: filter.limit }, rows.length);
6890
+ return rows;
6891
+ },
6778
6892
  summarizeModelUsage: async (actor, window, marginPercent) => {
6779
6893
  const rows = await this.cp.listModelUsage({ ...window, order: 'asc' });
6780
6894
  await this.recordAccess(actor, 'summarizeModelUsage', { tenantId: window.tenantId ?? null }, window, rows.length);
@@ -7216,6 +7330,13 @@ export class CloudflareScopeHost {
7216
7330
  // via newUniqueId (K-7) and stores the mapping in the directory — deferred.
7217
7331
  return this.scopeNs.get(this.scopeNs.idFromName(scopeId));
7218
7332
  }
7333
+ /** The shared coordinator has only a placeholder ScopeDO for a scope served by another deployment.
7334
+ * Until a permission delegation seam exists, preserve its existing write and single-row audit path. */
7335
+ async scopePermissionUsesLegacyPlaceholder(tenantId, scopeId) {
7336
+ if (!this.servesScopesElsewhereNow)
7337
+ return false;
7338
+ return (await this.cp.getScopeRecord(tenantId, scopeId))?.vertical != null;
7339
+ }
7219
7340
  // -- the rewind hold (#1819) ------------------------------------------------
7220
7341
  // A PITR rewind to a bookmark from before a schedule kill switch was pulled brings the
7221
7342
  // module's `system:` grants back live, and nothing in the rewound storage says otherwise.