@substrat-run/adapter-cloudflare 0.165.0 → 0.167.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/control-plane-do.d.ts +43 -2
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +60 -1
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +9 -0
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +189 -37
- package/dist/host.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +16 -0
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
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
|
-
|
|
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
|
|
3882
|
-
expiresAt ?? null);
|
|
3890
|
+
await writeTuple(node.scopeId, entityObjectRef(entity, 'HostAdmin')); // #1856
|
|
3883
3891
|
}
|
|
3884
3892
|
else if (node.scopeId) {
|
|
3885
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4483
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4504
|
-
|
|
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.
|
|
4508
|
-
|
|
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
|
|
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.
|
|
4651
|
-
// the
|
|
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
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
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.
|
|
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
|
-
|
|
4713
|
-
|
|
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.
|