@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/control-plane-do.d.ts +82 -19
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +328 -136
- 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 +215 -94
- 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 +10 -10
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('
|
|
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('
|
|
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
|
-
|
|
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
|
|
3884
|
-
expiresAt ?? null);
|
|
3890
|
+
await writeTuple(node.scopeId, entityObjectRef(entity, 'HostAdmin')); // #1856
|
|
3885
3891
|
}
|
|
3886
3892
|
else if (node.scopeId) {
|
|
3887
|
-
await
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4485
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4506
|
-
|
|
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.
|
|
4510
|
-
|
|
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
|
|
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.
|
|
4653
|
-
// the
|
|
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
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
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.
|
|
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
|
-
|
|
4715
|
-
|
|
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
|
|
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('
|
|
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('
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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('
|
|
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.
|
|
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.
|
|
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.
|
|
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('
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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.
|