@substrat-run/adapter-cloudflare 0.165.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';
@@ -3244,11 +3244,14 @@ export class CloudflareScopeHost {
3244
3244
  * configured; the mint has no such reach and no caller needing one, since an owner claim link
3245
3245
  * is minted by the deployment itself behind `/internal/owner-claim`.
3246
3246
  */
3247
- async capabilityScopeStub(rec, scopeId, verb) {
3247
+ assertCapabilityScopeReach(rec, scopeId, verb) {
3248
3248
  if (this.servesScopesElsewhereNow && rec.vertical !== null) {
3249
3249
  throw substratError('unavailable', `${verb} cannot reach scope ${scopeId}: it is served by the '${rec.vertical}' ` +
3250
3250
  'deployment, and this host has no reach into it');
3251
3251
  }
3252
+ }
3253
+ async capabilityScopeStub(rec, scopeId, verb) {
3254
+ this.assertCapabilityScopeReach(rec, scopeId, verb);
3252
3255
  await this.migrateAndRecord(scopeId);
3253
3256
  return this.scopeStub(scopeId);
3254
3257
  }
@@ -3876,13 +3879,18 @@ export class CloudflareScopeHost {
3876
3879
  // #1713: after the directory moved and the move was audited, never before or instead.
3877
3880
  await this.deliverLifecycles(actor, { scopeId });
3878
3881
  };
3879
- 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
+ };
3880
3889
  if (entity) {
3881
- await this.writeScopeTuple(node.scopeId, subject, `granted:${permission}`, entityObjectRef(entity, 'HostAdmin'), // #1856: grant and grantToOrg both land here
3882
- expiresAt ?? null);
3890
+ await writeTuple(node.scopeId, entityObjectRef(entity, 'HostAdmin')); // #1856
3883
3891
  }
3884
3892
  else if (node.scopeId) {
3885
- await this.writeScopeTuple(node.scopeId, subject, `granted:${permission}`, `scope:${node.scopeId}`, expiresAt ?? null);
3893
+ await writeTuple(node.scopeId, `scope:${node.scopeId}`);
3886
3894
  }
3887
3895
  else {
3888
3896
  // #2526: a tenant-level grant is a directory tuple, written by the ControlPlaneDO in the
@@ -4459,15 +4467,33 @@ export class CloudflareScopeHost {
4459
4467
  await this.fanOut(tenantId);
4460
4468
  return;
4461
4469
  }
4462
- 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
+ });
4463
4491
  if (refusal)
4464
4492
  throw substratError('forbidden', refusal);
4465
- await this.writeScopeTuple(scopeId, `principal:${assignment.principalId}`, `role:${assignment.roleKey}`, `scope:${scopeId}`, null);
4466
- await this.recordAdmin(actor, 'assignRole', { tenantId, scopeId }, null, assignment);
4467
4493
  },
4468
4494
  unassignRole: async (actor, assignment) => {
4469
4495
  // Tombstone (K-21) — the checker skips revoked rows. A no-op returns false so
4470
- // 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.
4471
4497
  const relation = `role:${assignment.roleKey}`;
4472
4498
  const { tenantId, scopeId } = assignment.node;
4473
4499
  if (!scopeId) {
@@ -4479,17 +4505,27 @@ export class CloudflareScopeHost {
4479
4505
  await this.fanOut(tenantId);
4480
4506
  return;
4481
4507
  }
4482
- const changed = await this.scopeStub(scopeId).revokeTuple(`principal:${assignment.principalId}`, relation, `scope:${scopeId}`, new Date().toISOString());
4483
- 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);
4484
4512
  return;
4485
- 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
+ });
4486
4521
  },
4487
4522
  grant: async (actor, raw) => {
4488
4523
  // Parsed like its `grantToConnection`/`grantToSystem` siblings, not taken on
4489
4524
  // trust: the parse is where an `expiresAt` carrying a UTC offset is normalised
4490
4525
  // to Z text (#963), and liveness here is a lexicographic `expires_at > ?`.
4491
4526
  const grant = capabilityGrant.parse(raw);
4492
- 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
4493
4529
  const subject = `principal:${grant.principalId}`;
4494
4530
  const target = { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId };
4495
4531
  if (!grant.node.scopeId && !grant.entity) {
@@ -4500,12 +4536,49 @@ export class CloudflareScopeHost {
4500
4536
  return;
4501
4537
  }
4502
4538
  // Scope-level and entity grants write scope tuples (already local): no fan-out.
4503
- await writeGrant(subject, grant.permission, grant.node, grant.entity, grant.expiresAt);
4504
- 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);
4505
4559
  },
4506
4560
  grantEntityShape: async (actor, grant) => {
4507
- await this.grantEntityShapeLocal(grant.node.scopeId, grant.principalId, grant.entity, grant.permissions);
4508
- 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);
4509
4582
  },
4510
4583
  reconcileEntityGrantShapes: async (actor, node, shapes, opts) => {
4511
4584
  // #2184: the bound types are the reconcile route's answer to the platform, not part of this
@@ -4597,13 +4670,25 @@ export class CloudflareScopeHost {
4597
4670
  const target = { tenantId: grant.node.tenantId, scopeId: grant.node.scopeId };
4598
4671
  const after = { moduleId: grant.moduleId, permission: grant.permission, node: grant.node };
4599
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
+ }
4600
4680
  // #1666: refused while the module is switched off on this scope — checked and
4601
4681
  // written in one DO unit. Restore is the lever; a grant is not.
4602
- 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
+ });
4603
4689
  if (!written) {
4604
4690
  throw substratError('conflict', systemSwitchedOffMessage(grant.moduleId, grant.node.scopeId));
4605
4691
  }
4606
- await this.recordAdmin(actor, 'grantToSystem', target, null, after);
4607
4692
  return;
4608
4693
  }
4609
4694
  // #1743: refused while the record holds the module off on any scope — checked and
@@ -4647,10 +4732,8 @@ export class CloudflareScopeHost {
4647
4732
  return { switchedOffPeers, tenantHeldPeers, fences };
4648
4733
  },
4649
4734
  reassertSystemSwitches: reassertSystemSwitchesOf,
4650
- // #1672 — the platform's two capability verbs. Audited AFTER the write, on both, and
4651
- // the failure each leaves is the safe one: a mint whose audit row did not land never
4652
- // returned its secret, so nobody can ever exchange it; a revoke whose row did not land
4653
- // 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.
4654
4737
  mintCapability: async (actor, tenantId, scopeId, input) => {
4655
4738
  // Checked HERE as well as in the DO: a typed refusal thrown across the RPC arrives as
4656
4739
  // a bare message, and the caller would lose `validation_failed`. One function, so the
@@ -4658,17 +4741,22 @@ export class CloudflareScopeHost {
4658
4741
  checkBecomeInput(input, new Date().toISOString());
4659
4742
  await this.validateScopeAccess(tenantId, scopeId);
4660
4743
  const rec = await this.capabilityScopeRecord(tenantId, scopeId);
4661
- const stub = await this.capabilityScopeStub(rec, scopeId, 'mintCapability');
4662
- const minted = await stub.mintBecomeCapability(input, actor);
4663
- await this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId, vertical: rec.vertical }, null, {
4664
- capabilityId: minted.id,
4665
- mode: 'become',
4666
- principal: input.principal,
4667
- expiresAt: input.expiresAt,
4668
- maxUses: input.maxUses,
4669
- 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),
4670
4759
  });
4671
- return minted;
4672
4760
  },
4673
4761
  revokeCapability: async (actor, tenantId, scopeId, capabilityId) => {
4674
4762
  // #1686: a hosted scope's directory is in the deployment serving it, reached through the
@@ -4700,7 +4788,8 @@ export class CloudflareScopeHost {
4700
4788
  // The org must exist in the node's tenant. A grant to a phantom org looks
4701
4789
  // applied, resolves for nobody, and still shows up in the permission diff.
4702
4790
  await requireOrg(node.tenantId, orgId);
4703
- 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
4704
4793
  const target = { tenantId: node.tenantId, scopeId: node.scopeId };
4705
4794
  const after = { orgId, permission, node, entity };
4706
4795
  if (!node.scopeId && !entity) {
@@ -4709,8 +4798,26 @@ export class CloudflareScopeHost {
4709
4798
  await this.fanOut(node.tenantId);
4710
4799
  return;
4711
4800
  }
4712
- await writeGrant(`org:${orgId}`, permission, node, entity);
4713
- 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);
4714
4821
  },
4715
4822
  // -- vertical + version registry (#31) ---------------------------------
4716
4823
  // -- the hostname map (K-26) -------------------------------------------
@@ -5516,6 +5623,24 @@ export class CloudflareScopeHost {
5516
5623
  await this.recordAccess(actor, 'getStoreDriverRelease', { tenantId: null }, { driver, sha256 }, record ? 1 : 0);
5517
5624
  return record;
5518
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)),
5519
5644
  storeDriverState: async (actor, driver) => {
5520
5645
  const state = await this.cp.storeDriverState(driver);
5521
5646
  await this.recordAccess(actor, 'storeDriverState', { tenantId: null }, { driver }, state.rollouts.length);
@@ -6167,6 +6292,11 @@ export class CloudflareScopeHost {
6167
6292
  await this.recordAccess(actor, 'readMeters', { tenantId: only ?? null }, filter ?? null, reading.perTenant.length);
6168
6293
  return meterReading.parse(reading);
6169
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
+ },
6170
6300
  registerIdentityPool: async (actor, input) => {
6171
6301
  const parsed = identityPool.parse(input);
6172
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));
@@ -6703,6 +6833,16 @@ export class CloudflareScopeHost {
6703
6833
  await this.recordAccess(actor, 'listRequestRollupDays', { tenantId: null }, filter ?? null, rows.length);
6704
6834
  return rows;
6705
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
+ },
6706
6846
  pruneRequestDays: async (_actor, limit, retentionMonths) => this.cp.pruneRequestDays(assertRowLimit('limit', limit), retentionMonths),
6707
6847
  recordMeterDay: async (_actor, input) => this.cp.recordMeterDay(input),
6708
6848
  listMeterDays: async (actor, filter) => {
@@ -6744,6 +6884,11 @@ export class CloudflareScopeHost {
6744
6884
  await this.recordAccess(actor, 'listModelUsage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter, rows.length);
6745
6885
  return rows.map(modelUsageEntryOf);
6746
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
+ },
6747
6892
  summarizeModelUsage: async (actor, window, marginPercent) => {
6748
6893
  const rows = await this.cp.listModelUsage({ ...window, order: 'asc' });
6749
6894
  await this.recordAccess(actor, 'summarizeModelUsage', { tenantId: window.tenantId ?? null }, window, rows.length);
@@ -7185,6 +7330,13 @@ export class CloudflareScopeHost {
7185
7330
  // via newUniqueId (K-7) and stores the mapping in the directory — deferred.
7186
7331
  return this.scopeNs.get(this.scopeNs.idFromName(scopeId));
7187
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
+ }
7188
7340
  // -- the rewind hold (#1819) ------------------------------------------------
7189
7341
  // A PITR rewind to a bookmark from before a schedule kill switch was pulled brings the
7190
7342
  // module's `system:` grants back live, and nothing in the rewound storage says otherwise.