@substrat-run/adapter-cloudflare 0.141.0 → 0.143.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/connector-calls.d.ts +74 -7
- package/dist/connector-calls.d.ts.map +1 -1
- package/dist/connector-calls.js +56 -6
- package/dist/connector-calls.js.map +1 -1
- package/dist/control-plane-do.d.ts +20 -3
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +59 -10
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +74 -11
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +139 -43
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/live-reads.d.ts +10 -2
- package/dist/live-reads.d.ts.map +1 -1
- package/dist/live-reads.js +10 -2
- package/dist/live-reads.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +91 -18
- package/dist/scope-do.js.map +1 -1
- package/package.json +9 -9
package/dist/host.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { env as ambientEnv } from 'cloudflare:workers';
|
|
2
2
|
import { isRewindRefusal, REWIND_REFUSED } from './rewind-refusal.js';
|
|
3
3
|
import { isSystemDoorMoved, SYSTEM_DOOR_MOVED, SYSTEM_DOOR_REGATES } from './system-door.js';
|
|
4
|
-
import { delegatedReadParams, operationSeriesCount, fromWireFailure, isSubstratError, exportReadInput, exportedBatch, importBatch, importResult, importState, importsOfManifestJson, exportsOfManifestJson, importCursorMove, importCursorMoveAt, importCursorMoved, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, findingEntry, findingRuleEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityExchange, mintedCapability, capabilityGrant, principalId, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchOutcome, systemScheduleEntry, systemSwitchRecord, entitlementGrant, entitlementGrantInput, capabilityFilter, instant, meterReading, 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, copyMarkAudit, subjectShredReceipt, connectorDispatchKind, callsOfManifestJson, outboundOfManifestJson, SCOPE_GATE_REASONS, substratError, redrainEventsInput, coverage, peerCoverage, peerGrantsEntry, peerSwitch, peerSwitchOutcome, verticalCaller, verticalResolution, peerInstanceResolution, peerBinding as peerBindingSchema, verticalSlug, entityObjectRef, ATTACHMENT_BLOB_BINDING, blobStoreBindingName, lifecycleDelivery, scopeLifecycle, } from '@substrat-run/contracts';
|
|
4
|
+
import { delegatedReadParams, operationSeriesCount, fromWireFailure, isSubstratError, exportReadInput, exportedBatch, importBatch, importResult, importState, importsOfManifestJson, exportsOfManifestJson, importCursorMove, importCursorMoveAt, importCursorMoved, accessLogEntry, adminLogEntry, opsFailureEntry, opsFailureFingerprint, issueEntry, findingEntry, findingRuleEntry, sweepRunEntry, FRESHNESS_HEARTBEAT_MINUTES, sweepRunsPayload, modelUsageEntry, attachmentRecord, createTenantInput, identityLink, identityPool, createOrgInput, promotionAcknowledgement, bindAcknowledgement, bindHostnameInput, channelHistoryEntry, hostnameBinding, publishVersionInput, AUTO_ADMISSION_NOTE, registerVerticalInput, vertical as verticalSchema, verticalServingState, verticalChannel, verticalVersion, connection, capabilityExchange, mintedCapability, boundedBecomeMint, boundedBecomeRevoke, becomeLinkState, principalBecomeCapabilityInput, capabilityGrant, principalId, connectionGrant, connectionGrantRecord, connectionSecret, systemGrant, systemSwitch, systemSwitchOutcome, systemScheduleEntry, systemSwitchRecord, entitlementGrant, entitlementGrantInput, capabilityFilter, instant, meterReading, scopeStorageSample, 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, copyMarkAudit, subjectShredReceipt, connectorDispatchKind, callsOfManifestJson, outboundOfManifestJson, SCOPE_GATE_REASONS, substratError, 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, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, KEPT_COPY_REFUSAL, capabilityTokenHash, checkBecomeInput, 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, assertRedrainWindow, platformRequestOf, undrainedEventsOf, inUnitMovesToAudit, staleCarryRevertRow, staleCarryReverts, recordedOffFromWire, peerSubjectRef, SWITCH_KINDS, reassertActionOf, reassertEntry, reassertOffRow, reassertOnRow, switchActionOf, switchAuditSubject, recordAuditOutcome, auditWarningOf, switchNotFoundMessage, recordWriteSuperseded, switchSubjectOf, switchSupersededMessage, withRecorded, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, CrossVerticalRegistry, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, bindExportBreaksOf, bindExportBreakRefusal, collectPeers, peerSeats, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, splitManifestMigrations, unknownRoleError, assertRowLimit, assertRowOffset, emptyImportResult, INERT_SCOPE_REASON, lifecycleReceipt, lifecycleRefusal, isPrimaryScope, isPrimaryScopeRow, asyncInvocationId, asyncLinePass, assertNoCallerPurge, purgeReportOf, purgeStillDue, registerTrashTargets, memberAddedAudit, shapeTopUpBatch, } from '@substrat-run/kernel';
|
|
6
|
+
import { attachmentBlobKey, attachmentSha256, entitlementDenial, requiredEntitlementFor, foldMeterReading, parseValidationRecords, resolveScopeRecord, ulid, 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, 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, bindExportBreaksOf, bindExportBreakRefusal, collectPeers, peerSeats, connectorCallRecord, noopConnectorCallRecorder, recordConnectorCall, settleConnectionUse, splitManifestMigrations, unknownRoleError, assertRowLimit, assertRowOffset, emptyImportResult, INERT_SCOPE_REASON, lifecycleReceipt, lifecycleRefusal, isPrimaryScope, isPrimaryScopeRow, asyncInvocationId, asyncLinePass, assertNoCallerPurge, purgeReportOf, purgeStillDue, assertBoundOperations, registerTrashTargets, memberAddedAudit, shapeTopUpBatch, } from '@substrat-run/kernel';
|
|
7
7
|
import { COPY_RESTORE_FENCE_LAPSED, scopeScriptCopyOf } from '@substrat-run/kernel';
|
|
8
8
|
import { attributedView, isModuleErasureCounts, moduleErasurePlan, moduleRowsErased } from '@substrat-run/kernel';
|
|
9
9
|
import { isOrangeToOrange, isUpgradeRequest, LIVE_MODE_HEADER, LIVE_PRINCIPAL_HEADER, LIVE_SCOPE_HEADER, LIVE_SUBSCRIBE_PATH, LIVE_TENANT_HEADER, LIVE_WITHIN_HEADER, LIVE_EXPIRES_HEADER, liveInstant, encodeLiveWithin, liveWithinOf, } from './live-reads.js';
|
|
@@ -314,6 +314,7 @@ export class CloudflareScopeHost {
|
|
|
314
314
|
peerSwitchDelegation;
|
|
315
315
|
/** #1705 PR 3: the replay lever's reach into the deployment serving a consumer scope. */
|
|
316
316
|
importCursorDelegation;
|
|
317
|
+
capabilityDelegation;
|
|
317
318
|
/**
|
|
318
319
|
* #1819: the last read of the rewind hold, and when it was taken (before the read went out).
|
|
319
320
|
* Per instance, so per pass: the host is built per request, and never outlives one.
|
|
@@ -359,6 +360,7 @@ export class CloudflareScopeHost {
|
|
|
359
360
|
this.lifecycleDelegation = options.lifecycleDelegation;
|
|
360
361
|
this.peerSwitchDelegation = options.peerSwitchDelegation;
|
|
361
362
|
this.importCursorDelegation = options.importCursorDelegation;
|
|
363
|
+
this.capabilityDelegation = options.capabilityDelegation;
|
|
362
364
|
this.admin = this.buildAdmin();
|
|
363
365
|
}
|
|
364
366
|
// -- registration mechanics (validation only) -----------------------------
|
|
@@ -770,7 +772,7 @@ export class CloudflareScopeHost {
|
|
|
770
772
|
async listPlatformRequests(tenantId, scopeId) {
|
|
771
773
|
await this.validateScopeAccess(tenantId, scopeId);
|
|
772
774
|
await this.migrateAndRecord(scopeId);
|
|
773
|
-
// Tolerant (#1588):
|
|
775
|
+
// Tolerant (#1588, #1637): an undecodable row comes back naming why, never throws for the list.
|
|
774
776
|
return (await this.scopeStub(scopeId).pendingPlatformRequests()).map(platformRequestOf);
|
|
775
777
|
}
|
|
776
778
|
async listPlatformRequestHistory(tenantId, scopeId, filter) {
|
|
@@ -905,12 +907,13 @@ export class CloudflareScopeHost {
|
|
|
905
907
|
}
|
|
906
908
|
/**
|
|
907
909
|
* The platform's revoke on the CP-less path (#1686) — how a re-minted claim link retires the
|
|
908
|
-
* previous one
|
|
909
|
-
*
|
|
910
|
-
*
|
|
910
|
+
* previous one, and the far end of the operator's revoke (`CapabilityDelegation`, behind
|
|
911
|
+
* `/internal/capabilities/revoke`). The record as it stood before (revoked now, or already),
|
|
912
|
+
* for the control plane's admin log; null when the scope holds no such capability. No
|
|
913
|
+
* lifecycle gate: a revoke only narrows, so a held scope still takes one. Idempotent.
|
|
911
914
|
*/
|
|
912
915
|
async revokeCapabilityLocal(scopeId, capabilityId, actor) {
|
|
913
|
-
return
|
|
916
|
+
return this.scopeStub(scopeId).revokeCapabilityAsPlatform(capabilityId, actor);
|
|
914
917
|
}
|
|
915
918
|
/**
|
|
916
919
|
* Copy one scope's data into a fresh scope DO, entirely within THIS deployment —
|
|
@@ -1268,9 +1271,11 @@ export class CloudflareScopeHost {
|
|
|
1268
1271
|
}
|
|
1269
1272
|
this.predicateNames.set(name, manifest.id);
|
|
1270
1273
|
}
|
|
1274
|
+
// #1835: a forged `operations` is refused at construction too, before its scope ever wakes.
|
|
1275
|
+
assertBoundOperations(manifest.id, registration.operations);
|
|
1271
1276
|
// #119: the trash rules, refused here too so a bad module fails at construction, not at its
|
|
1272
1277
|
// scope's first wake. The scope holds the targets; the coordinator only needs the verdict.
|
|
1273
|
-
registerTrashTargets(manifest.id, new Set(Object.keys(registration.operations ?? {})), registration.
|
|
1278
|
+
registerTrashTargets(manifest.id, new Set(Object.keys(registration.operations?.handlers ?? {})), registration.operations?.inputs, manifest.entityStates, manifest.schedules);
|
|
1274
1279
|
// #1705: after the checks above, so a refused module leaves nothing registered here.
|
|
1275
1280
|
this.crossVertical.register(manifest, registration.imports);
|
|
1276
1281
|
this.moduleIds.add(manifest.id);
|
|
@@ -1284,7 +1289,7 @@ export class CloudflareScopeHost {
|
|
|
1284
1289
|
this.moduleFreshness.set(manifest.id, manifest.freshness);
|
|
1285
1290
|
}
|
|
1286
1291
|
this.migrationTotal += migrations.length;
|
|
1287
|
-
const ownOperations = new Set(Object.keys(registration.operations ?? {}));
|
|
1292
|
+
const ownOperations = new Set(Object.keys(registration.operations?.handlers ?? {}));
|
|
1288
1293
|
for (const name of manifest.withdraws ?? []) {
|
|
1289
1294
|
if (ownOperations.has(name)) {
|
|
1290
1295
|
throw new Error(`${manifest.id} withdraws its own operation: ${name} (a module cannot withdraw itself — just don't register it)`);
|
|
@@ -1296,9 +1301,9 @@ export class CloudflareScopeHost {
|
|
|
1296
1301
|
// #893: the facade validates what the DO will enforce. A schema declared for
|
|
1297
1302
|
// an operation this module does not bind enforces nothing while reading as
|
|
1298
1303
|
// coverage — refused here so it is caught at registration rather than never.
|
|
1299
|
-
const unboundInputs = Object.keys(registration.
|
|
1304
|
+
const unboundInputs = Object.keys(registration.operations?.inputs ?? {}).filter((name) => !ownOperations.has(name));
|
|
1300
1305
|
if (unboundInputs.length > 0) {
|
|
1301
|
-
throw new Error(`${manifest.id} declares
|
|
1306
|
+
throw new Error(`${manifest.id} declares operations.inputs for unbound operation(s): ` +
|
|
1302
1307
|
`${unboundInputs.sort().join(', ')} — a schema on nothing reads as a parse that is not there`);
|
|
1303
1308
|
}
|
|
1304
1309
|
const binding = {
|
|
@@ -1306,7 +1311,7 @@ export class CloudflareScopeHost {
|
|
|
1306
1311
|
entitlementKey: manifest.entitlementKey,
|
|
1307
1312
|
scheduledOperations: new Set((manifest.schedules ?? []).map((sch) => sch.operation)),
|
|
1308
1313
|
};
|
|
1309
|
-
for (const name of Object.keys(registration.operations ?? {})) {
|
|
1314
|
+
for (const name of Object.keys(registration.operations?.handlers ?? {})) {
|
|
1310
1315
|
this.bindOperation(name);
|
|
1311
1316
|
this.operationEntitlement.set(name, binding);
|
|
1312
1317
|
}
|
|
@@ -2218,6 +2223,38 @@ export class CloudflareScopeHost {
|
|
|
2218
2223
|
const result = await this.scopeStub(scopeId).revokeScopeRolesBoundedFor(tenantId, scopeId, principalId.parse(caller), principalId.parse(principal));
|
|
2219
2224
|
return { coverage: coverage.parse(result.coverage), revoked: result.revoked };
|
|
2220
2225
|
}
|
|
2226
|
+
/**
|
|
2227
|
+
* A principal's `become` mint (#1686) — a member invite's link, minted by the member who
|
|
2228
|
+
* invites. The bound (`becomeMintCheck`: the caller holds everything the target holds here)
|
|
2229
|
+
* and the write are one ScopeDO task; a refusal is `{ ok: false, coverage }` and writes
|
|
2230
|
+
* nothing. Same gate as the scope-role verbs, which the invite grants with beside it.
|
|
2231
|
+
*/
|
|
2232
|
+
async mintBecomeCapabilityBounded(tenantId, scopeId, caller, input) {
|
|
2233
|
+
await this.scopeRoleGate(tenantId, scopeId, 'mintBecomeCapabilityBounded');
|
|
2234
|
+
return boundedBecomeMint.parse(await this.scopeStub(scopeId).mintBecomeCapabilityBoundedFor(tenantId, scopeId, principalId.parse(caller), principalBecomeCapabilityInput.parse(input)));
|
|
2235
|
+
}
|
|
2236
|
+
/**
|
|
2237
|
+
* Revoke a `become` capability a principal minted (#1686) — what withdrawing a member invite
|
|
2238
|
+
* does to its link. `revoked` is false when it was already revoked or is not such a capability
|
|
2239
|
+
* (a platform-minted claim link, an `act` share). The kernel bounds `by` in the same ScopeDO
|
|
2240
|
+
* task: the link's minter, or someone holding everything its principal holds now.
|
|
2241
|
+
*/
|
|
2242
|
+
async revokeBecomeCapability(tenantId, scopeId, capabilityId, by) {
|
|
2243
|
+
await this.scopeRoleGate(tenantId, scopeId, 'revokeBecomeCapability');
|
|
2244
|
+
return boundedBecomeRevoke.parse(await this.scopeStub(scopeId).revokeBecomeCapabilityFor(tenantId, scopeId, capabilityId, principalId.parse(by)));
|
|
2245
|
+
}
|
|
2246
|
+
/**
|
|
2247
|
+
* Where each named `become` link stands (#1686) — what a pending-invite list shows beside each
|
|
2248
|
+
* invite, so a link the kernel revoked or that expired is never shown as open.
|
|
2249
|
+
*/
|
|
2250
|
+
async becomeLinkStates(tenantId, scopeId, ids) {
|
|
2251
|
+
// Refused here, on the near side, where the typed refusal reaches the caller as one.
|
|
2252
|
+
assertBecomeLinkStateIds(ids);
|
|
2253
|
+
await this.scopeRoleGate(tenantId, scopeId, 'becomeLinkStates');
|
|
2254
|
+
if (ids.length === 0)
|
|
2255
|
+
return [];
|
|
2256
|
+
return (await this.scopeStub(scopeId).becomeLinkStatesFor(tenantId, scopeId, [...ids])).map((state) => becomeLinkState.parse(state));
|
|
2257
|
+
}
|
|
2221
2258
|
/** The (tenant, scope) gate the scope-role verbs share — `assignScopeRoleBounded`'s two checks. */
|
|
2222
2259
|
async scopeRoleGate(tenantId, scopeId, verb) {
|
|
2223
2260
|
if (this.cpLess && !(await this.scopeStub(scopeId).servesTenant(tenantId))) {
|
|
@@ -2315,26 +2352,34 @@ export class CloudflareScopeHost {
|
|
|
2315
2352
|
this.systemSwitchDelegation ||
|
|
2316
2353
|
this.peerSwitchDelegation ||
|
|
2317
2354
|
this.eventDrainDelegation ||
|
|
2318
|
-
this.importCursorDelegation
|
|
2355
|
+
this.importCursorDelegation ||
|
|
2356
|
+
this.capabilityDelegation);
|
|
2319
2357
|
}
|
|
2320
2358
|
/**
|
|
2321
2359
|
* Where a platform capability verb (#1672) may write: this host's own ScopeDO — refused,
|
|
2322
2360
|
* loudly, on the shared control plane for a scope bound to a vertical, whose storage lives
|
|
2323
2361
|
* in that vertical's deployment. Writing through `this.scopeStub` there would mint into
|
|
2324
|
-
* an empty DO of the wrong namespace, a capability nobody could ever exchange.
|
|
2325
|
-
*
|
|
2326
|
-
*
|
|
2362
|
+
* an empty DO of the wrong namespace, a capability nobody could ever exchange. The revoke
|
|
2363
|
+
* reaches a hosted scope through `capabilityDelegation` instead and asks here only when none is
|
|
2364
|
+
* configured; the mint has no such reach and no caller needing one, since an owner claim link
|
|
2365
|
+
* is minted by the deployment itself behind `/internal/owner-claim`.
|
|
2327
2366
|
*/
|
|
2328
|
-
async capabilityScopeStub(
|
|
2329
|
-
const rec = await this.cp.getScopeRecord(tenantId, scopeId);
|
|
2330
|
-
if (!rec)
|
|
2331
|
-
throw unknownScopeForTenant(tenantId, scopeId);
|
|
2367
|
+
async capabilityScopeStub(rec, scopeId, verb) {
|
|
2332
2368
|
if (this.servesScopesElsewhereNow && rec.vertical !== null) {
|
|
2333
2369
|
throw substratError('unavailable', `${verb} cannot reach scope ${scopeId}: it is served by the '${rec.vertical}' ` +
|
|
2334
|
-
'deployment, and
|
|
2370
|
+
'deployment, and this host has no reach into it');
|
|
2335
2371
|
}
|
|
2336
2372
|
await this.migrateAndRecord(scopeId);
|
|
2337
|
-
return
|
|
2373
|
+
return this.scopeStub(scopeId);
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* The directory's record of a scope a platform capability verb addresses: `not_found`, or
|
|
2377
|
+
* `conflict` for a REAPED scope (`scopeRecordForRead`'s refusal). Its row is a tombstone and its
|
|
2378
|
+
* storage is gone, so addressing its DO — here or in the deployment — would construct an empty
|
|
2379
|
+
* one. An archived scope still answers: its bytes, and the capabilities in them, are there.
|
|
2380
|
+
*/
|
|
2381
|
+
async capabilityScopeRecord(tenantId, scopeId) {
|
|
2382
|
+
return this.scopeRecordForRead(tenantId, scopeId);
|
|
2338
2383
|
}
|
|
2339
2384
|
/** #1705: what this deployment imports — the sweep's reason to call no scope when it is empty. */
|
|
2340
2385
|
registeredImports() {
|
|
@@ -2862,6 +2907,7 @@ export class CloudflareScopeHost {
|
|
|
2862
2907
|
expiresAt: r.expires_at,
|
|
2863
2908
|
...(r.serving_ref ? { servingRef: r.serving_ref } : {}),
|
|
2864
2909
|
archivedAt: r.archived_at ?? null,
|
|
2910
|
+
archivedFromStatus: (r.archived_from_status ?? null),
|
|
2865
2911
|
createdAt: r.created_at,
|
|
2866
2912
|
});
|
|
2867
2913
|
// The (version, scope) pair a bind and its impact read both start from, and the refusals
|
|
@@ -3508,11 +3554,11 @@ export class CloudflareScopeHost {
|
|
|
3508
3554
|
await this.recordAdmin(actor, 'grantEntityShape', grant.node, null, grant);
|
|
3509
3555
|
},
|
|
3510
3556
|
reconcileEntityGrantShapes: async (actor, node, shapes, opts) => {
|
|
3511
|
-
const
|
|
3512
|
-
if (toppedUp > 0 || retired > 0) {
|
|
3513
|
-
await this.recordAdmin(actor, 'reconcileEntityGrantShapes', node, null, { shapes,
|
|
3557
|
+
const result = await this.topUpEntityGrantShapesLocal(node.tenantId, node.scopeId, shapes, opts?.batch);
|
|
3558
|
+
if (result.toppedUp > 0 || result.retired > 0) {
|
|
3559
|
+
await this.recordAdmin(actor, 'reconcileEntityGrantShapes', node, null, { shapes, ...result });
|
|
3514
3560
|
}
|
|
3515
|
-
return
|
|
3561
|
+
return result;
|
|
3516
3562
|
},
|
|
3517
3563
|
grantToConnection: async (actor, raw) => {
|
|
3518
3564
|
const grant = connectionGrant.parse(raw);
|
|
@@ -3644,9 +3690,10 @@ export class CloudflareScopeHost {
|
|
|
3644
3690
|
// two sides cannot disagree about what a valid mint is.
|
|
3645
3691
|
checkBecomeInput(input, new Date().toISOString());
|
|
3646
3692
|
await this.validateScopeAccess(tenantId, scopeId);
|
|
3647
|
-
const
|
|
3693
|
+
const rec = await this.capabilityScopeRecord(tenantId, scopeId);
|
|
3694
|
+
const stub = await this.capabilityScopeStub(rec, scopeId, 'mintCapability');
|
|
3648
3695
|
const minted = await stub.mintBecomeCapability(input, actor);
|
|
3649
|
-
await this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId, vertical }, null, {
|
|
3696
|
+
await this.recordAdmin(actor, 'mintCapability', { tenantId, scopeId, vertical: rec.vertical }, null, {
|
|
3650
3697
|
capabilityId: minted.id,
|
|
3651
3698
|
mode: 'become',
|
|
3652
3699
|
principal: input.principal,
|
|
@@ -3657,14 +3704,29 @@ export class CloudflareScopeHost {
|
|
|
3657
3704
|
return minted;
|
|
3658
3705
|
},
|
|
3659
3706
|
revokeCapability: async (actor, tenantId, scopeId, capabilityId) => {
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3707
|
+
// #1686: a hosted scope's directory is in the deployment serving it, reached through the
|
|
3708
|
+
// delegation; this host's own ScopeDO otherwise. Where it lands is settled before the
|
|
3709
|
+
// audit opens: a scope this host cannot reach is refused with nothing written.
|
|
3710
|
+
const rec = await this.capabilityScopeRecord(tenantId, scopeId);
|
|
3711
|
+
const { vertical } = rec;
|
|
3712
|
+
const delegation = this.capabilityDelegation;
|
|
3713
|
+
let revoke;
|
|
3714
|
+
if (vertical !== null && delegation) {
|
|
3715
|
+
const served = { vertical, verticalVersionId: rec.vertical_version_id, servingRef: rec.serving_ref };
|
|
3716
|
+
revoke = () => delegation.revoke({ tenantId, scopeId, served, capabilityId, actor });
|
|
3717
|
+
}
|
|
3718
|
+
else {
|
|
3719
|
+
const stub = await this.capabilityScopeStub(rec, scopeId, 'revokeCapability');
|
|
3720
|
+
revoke = () => stub.revokeCapabilityAsPlatform(capabilityId, actor);
|
|
3664
3721
|
}
|
|
3665
|
-
|
|
3722
|
+
// Intent first, then the outcome (#1666's grammar): a revoke whose answer is lost, or
|
|
3723
|
+
// whose outcome row cannot be written, is still on the admin log, and settles `unknown`.
|
|
3724
|
+
await auditedCapabilityRevoke({
|
|
3666
3725
|
capabilityId,
|
|
3667
|
-
|
|
3726
|
+
scopeId,
|
|
3727
|
+
record: (before, after) => this.recordAdmin(actor, 'revokeCapability', { tenantId, scopeId, vertical }, before, after),
|
|
3728
|
+
revoke,
|
|
3729
|
+
logError: (message, fields) => console.error(message, fields),
|
|
3668
3730
|
});
|
|
3669
3731
|
},
|
|
3670
3732
|
grantToOrg: async (actor, orgId, permission, node, entity) => {
|
|
@@ -4165,6 +4227,12 @@ export class CloudflareScopeHost {
|
|
|
4165
4227
|
if (result === 'pending')
|
|
4166
4228
|
throw substratError('precondition_failed', `scope ${scopeId} has a copy move in flight; retry reap after it settles`);
|
|
4167
4229
|
},
|
|
4230
|
+
backfillScopeScriptCopy: async (actor, tenantId, scopeId, scriptRef, opts) => {
|
|
4231
|
+
if (!scriptRef)
|
|
4232
|
+
throw substratError('conflict', 'a backfilled copy must name a real script');
|
|
4233
|
+
// The audit row is written in the insert's own unit, stamped there with its erasure epoch.
|
|
4234
|
+
return this.cp.backfillScopeScriptCopy(tenantId, scopeId, scriptRef, this.adminEntry(actor, 'backfillScopeCopy', { tenantId, scopeId }, null, { scriptRef, fromLogId: opts?.fromLogId ?? null }));
|
|
4235
|
+
},
|
|
4168
4236
|
claimExpiredScopeScriptCopies: async (_actor, input) => (await this.cp.claimExpiredScopeScriptCopies(input)).map(scopeScriptCopyOf),
|
|
4169
4237
|
settleScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId, state, marker, opts) => this.cp.settleScopeScriptCopy(tenantId, scopeId, scriptRef, moveId, state, marker?.loadStamp ?? null, marker?.revision ?? null, opts?.claimedBy),
|
|
4170
4238
|
touchScopeScriptCopy: async (_actor, tenantId, scopeId, scriptRef, moveId) => this.cp.touchScopeScriptCopy(tenantId, scopeId, scriptRef, moveId),
|
|
@@ -4836,6 +4904,9 @@ export class CloudflareScopeHost {
|
|
|
4836
4904
|
},
|
|
4837
4905
|
shredSubject: async (actor, tenantId, scopeId, subjectId) => {
|
|
4838
4906
|
await this.assertScope(tenantId, scopeId);
|
|
4907
|
+
// #1722: a direct shred claims nothing; it records the epoch it ran under, as an orchestrated
|
|
4908
|
+
// one records the epoch its claim compared, so the backfill can order every erasure.
|
|
4909
|
+
const stamp = { erasureEpoch: await this.cp.scopeErasureEpoch(tenantId, scopeId), path: 'direct' };
|
|
4839
4910
|
// Redact the live spine FIRST, destroy the key LAST. Both halves are idempotent and
|
|
4840
4911
|
// a crash between them converges on retry, so the order is decided by which
|
|
4841
4912
|
// half-done state harms the person: dying after the redaction leaves ciphertext in
|
|
@@ -4909,7 +4980,7 @@ export class CloudflareScopeHost {
|
|
|
4909
4980
|
// BOTH logs, deliberately: the admin log because this is a mutation, the access log
|
|
4910
4981
|
// because it destroys evidence. An erasure is the one action where "who asked for
|
|
4911
4982
|
// this to disappear" is itself part of the record.
|
|
4912
|
-
await this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId },
|
|
4983
|
+
await this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, stamp, receipt);
|
|
4913
4984
|
// BOTH counts: the access log's number is "how much evidence this destroyed", and
|
|
4914
4985
|
// an intent payload is a whole event's worth of it.
|
|
4915
4986
|
await this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, eventsRedacted + intentsRedacted + jobRunsRedacted + idempotencyResults + moduleRowsErased(vertical));
|
|
@@ -4937,7 +5008,8 @@ export class CloudflareScopeHost {
|
|
|
4937
5008
|
keyDestroyed: existed,
|
|
4938
5009
|
tombstoned: true,
|
|
4939
5010
|
});
|
|
4940
|
-
|
|
5011
|
+
// The epoch the claim compared (#1722): the backfill orders itself against this erasure by it.
|
|
5012
|
+
await this.recordAdmin(actor, 'shredSubject', { tenantId, scopeId }, { erasureEpoch: expected.epoch, path: 'orchestrated' }, receipt);
|
|
4941
5013
|
await this.recordAccess(actor, 'shredSubject', { tenantId, scopeId }, { subjectId }, redactions.reduce((n, r) => n + r.events + r.intents + r.jobRuns + r.idempotencyResults + moduleRowsErased(r.vertical), 0));
|
|
4942
5014
|
return receipt;
|
|
4943
5015
|
},
|
|
@@ -5036,6 +5108,7 @@ export class CloudflareScopeHost {
|
|
|
5036
5108
|
scopes: rows.scopes.map((r) => ({
|
|
5037
5109
|
tenantId: r.tenant_id,
|
|
5038
5110
|
status: r.status,
|
|
5111
|
+
archivedFromStatus: r.archived_from_status,
|
|
5039
5112
|
})),
|
|
5040
5113
|
entitlements: rows.entitlements.map((r) => ({
|
|
5041
5114
|
tenantId: r.tenant_id,
|
|
@@ -5043,6 +5116,8 @@ export class CloudflareScopeHost {
|
|
|
5043
5116
|
plan: r.plan,
|
|
5044
5117
|
expiresAt: r.expires_at,
|
|
5045
5118
|
})),
|
|
5119
|
+
storage: rows.storage,
|
|
5120
|
+
storageAttempts: rows.storageAttempts,
|
|
5046
5121
|
});
|
|
5047
5122
|
// Tenants covered, not totals: "read one tenant's meter" and "metered the whole
|
|
5048
5123
|
// fleet" are different acts, and K-24 exists to tell them apart.
|
|
@@ -5140,7 +5215,7 @@ export class CloudflareScopeHost {
|
|
|
5140
5215
|
createdBy: input.createdBy ?? actor,
|
|
5141
5216
|
});
|
|
5142
5217
|
},
|
|
5143
|
-
connectionSealingKey: (id) => this.ensureSealingKey(id),
|
|
5218
|
+
connectionSealingKey: async (id) => this.ensureSealingKey(id),
|
|
5144
5219
|
connectionSealingKeys: async (tenantId, vertical) => {
|
|
5145
5220
|
// LIVE connections only. A revoked connection's key is KEPT (its pending
|
|
5146
5221
|
// ciphertext must still open) but stops being projected, so a scope can no
|
|
@@ -5363,7 +5438,9 @@ export class CloudflareScopeHost {
|
|
|
5363
5438
|
return rows;
|
|
5364
5439
|
},
|
|
5365
5440
|
/** #2064: settle an intent with no outcome — one unit in the directory DO. */
|
|
5366
|
-
|
|
5441
|
+
// `async`, like every other method here: a bare stub call hands the caller workerd's RPC
|
|
5442
|
+
// promise, which is callable (for pipelining) where `Promise<boolean>` is not (#2131).
|
|
5443
|
+
settleUnrecordedOutcome: async (actor, input) => this.cp.settleUnrecordedOutcome({ actor, intentId: input.intentId, error: input.error }),
|
|
5367
5444
|
/** #2005: one change to a scope's copy marker, written around the vertical's own change. */
|
|
5368
5445
|
recordCopyMark: async (actor, entry) => {
|
|
5369
5446
|
const { tenantId, scopeId, action, ...after } = copyMarkAudit.parse(entry);
|
|
@@ -5570,6 +5647,18 @@ export class CloudflareScopeHost {
|
|
|
5570
5647
|
await this.recordAccess(actor, 'listFindingRules', { tenantId }, filter, rows.length);
|
|
5571
5648
|
return rows.map((r) => findingRuleEntry.parse(r));
|
|
5572
5649
|
},
|
|
5650
|
+
recordScopeStorage: async (_actor, readings) => this.cp.recordScopeStorage(readings),
|
|
5651
|
+
listScopeStorage: async (actor, filter) => {
|
|
5652
|
+
const rows = (await this.cp.listScopeStorage(filter)).map((r) => scopeStorageSample.parse(r));
|
|
5653
|
+
await this.recordAccess(actor, 'listScopeStorage', { tenantId: filter?.tenantId ?? null, scopeId: filter?.scopeId ?? null }, filter ?? null, rows.length);
|
|
5654
|
+
return rows;
|
|
5655
|
+
},
|
|
5656
|
+
listScopeStorageAttempts: async (actor) => {
|
|
5657
|
+
const rows = await this.cp.listScopeStorageAttempts();
|
|
5658
|
+
await this.recordAccess(actor, 'listScopeStorageAttempts', { tenantId: null }, null, rows.length);
|
|
5659
|
+
return rows;
|
|
5660
|
+
},
|
|
5661
|
+
pruneScopeStorage: async (_actor, limit) => this.cp.pruneScopeStorage(assertRowLimit('limit', limit)),
|
|
5573
5662
|
recordModelUsage: async (input) => {
|
|
5574
5663
|
const l = input.line;
|
|
5575
5664
|
return this.cp.recordModelUsage({
|
|
@@ -6790,22 +6879,29 @@ export class CloudflareScopeHost {
|
|
|
6790
6879
|
* passes — at most `batch` rows of work per scope transaction (default 500, at most 5000;
|
|
6791
6880
|
* anything else is `validation_failed`), repeated until a pass finishes. Never re-grants a revoked
|
|
6792
6881
|
* key; takes back only a key the shape declares `retired` (#2082). Returns how many
|
|
6793
|
-
* (person, entity) it topped up,
|
|
6882
|
+
* (person, entity) it topped up, how many it took retired keys from, and (only when nonzero) how
|
|
6883
|
+
* many shapes' retirements it left open for the next reconcile.
|
|
6794
6884
|
*/
|
|
6795
6885
|
async topUpEntityGrantShapesLocal(tenantId, scopeId, shapes, batch) {
|
|
6796
6886
|
const limit = shapeTopUpBatch(batch);
|
|
6797
6887
|
let toppedUp = 0;
|
|
6798
6888
|
let retired = 0;
|
|
6889
|
+
let retirementsLeftOpen = 0;
|
|
6799
6890
|
if (shapes.length === 0)
|
|
6800
6891
|
return { toppedUp, retired };
|
|
6801
6892
|
const stub = this.scopeStub(scopeId);
|
|
6802
|
-
|
|
6803
|
-
|
|
6893
|
+
// Each pass resumes where the last one stopped (#2083), until one reports nothing left. A DO
|
|
6894
|
+
// from before the cursor answers no `next`: its passes each start over, until it says `done`.
|
|
6895
|
+
let after = null;
|
|
6896
|
+
for (let more = true; more;) {
|
|
6897
|
+
const pass = await stub.topUpEntityGrantShapes(tenantId, scopeId, shapes, limit, after);
|
|
6804
6898
|
toppedUp += pass.toppedUp;
|
|
6805
6899
|
retired += pass.retired;
|
|
6806
|
-
|
|
6900
|
+
retirementsLeftOpen += pass.retirementsLeftOpen ?? 0; // a DO from before the cap reports none
|
|
6901
|
+
after = pass.next ?? null;
|
|
6902
|
+
more = pass.next === undefined ? !pass.done : pass.next !== null;
|
|
6807
6903
|
}
|
|
6808
|
-
return { toppedUp, retired };
|
|
6904
|
+
return { toppedUp, retired, ...(retirementsLeftOpen > 0 ? { retirementsLeftOpen } : {}) };
|
|
6809
6905
|
}
|
|
6810
6906
|
// -- the connector write-back's far end (#574) -----------------------------
|
|
6811
6907
|
// A CP-less dispatch vertical cannot run a connector, so the shared control
|