@substrat-run/adapter-cloudflare 0.160.0 → 0.161.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 -9
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +156 -28
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +76 -10
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +160 -117
- package/dist/host.js.map +1 -1
- package/dist/r2.d.ts.map +1 -1
- package/dist/r2.js +13 -10
- package/dist/r2.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +80 -5
- 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, 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, 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, isSizedByteStream, 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, 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, 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, auditWarningOf, switchNotFoundMessage, recordWriteSuperseded, switchSubjectOf, switchSupersededMessage, withRecorded, systemSwitchedOffMessage, tenantSystemSwitchedOffMessage, CrossVerticalRegistry, importCursorSourceOf, exportBreaksOf, exportBreakRefusal, assertPromoteReviewCurrent, promoteRaceRefusal, servingRaceRefusal, 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';
|
|
@@ -349,11 +349,11 @@ const DOOR_WORDS = {
|
|
|
349
349
|
};
|
|
350
350
|
/** #2029: the rewind hold's key for one subject — a module's id, bare as #1819 stored it, or `vertical:<slug>`. */
|
|
351
351
|
const holdKeyOf = (kind, key) => (kind === 'system' ? key : peerSubjectRef(key));
|
|
352
|
-
/** #2541: a vertical-side
|
|
353
|
-
function heldOrRefused(scopeId,
|
|
354
|
-
if (
|
|
352
|
+
/** #2541, #2544: a vertical-side read's answer, or the refusal when the scope's own object said it holds nothing (`null`). */
|
|
353
|
+
function heldOrRefused(scopeId, answer) {
|
|
354
|
+
if (answer === null)
|
|
355
355
|
throw substratError('conflict', scopeNotHeldRefusal(scopeId));
|
|
356
|
-
return
|
|
356
|
+
return answer;
|
|
357
357
|
}
|
|
358
358
|
export class CloudflareScopeHost {
|
|
359
359
|
admin;
|
|
@@ -578,7 +578,8 @@ export class CloudflareScopeHost {
|
|
|
578
578
|
grants: async () => (await this.connectionGrantsInScope(tenantId, scopeId))
|
|
579
579
|
.filter((g) => g.connectionId === open.id)
|
|
580
580
|
.map((g) => g.permission),
|
|
581
|
-
openAttachment: (attachmentId) => this.getConnectorAttachments(open.id, scopeId, { eventId })
|
|
581
|
+
openAttachment: async (attachmentId) => (await this.getConnectorAttachments(open.id, scopeId, { eventId })).open(attachmentId),
|
|
582
|
+
openAttachmentStream: async (attachmentId) => (await this.getConnectorAttachments(open.id, scopeId, { eventId })).openStream(attachmentId),
|
|
582
583
|
// #687: open a cell the scope sealed TO this connection. The keyId-indexed
|
|
583
584
|
// map goes in, never out — a connector receives the plaintext of one cell,
|
|
584
585
|
// not a private key it could mislay. Retired keys are in the map too, so a
|
|
@@ -1005,14 +1006,29 @@ export class CloudflareScopeHost {
|
|
|
1005
1006
|
* gate (the caller is the control plane, which did the K-3 check + audit on its side).
|
|
1006
1007
|
*/
|
|
1007
1008
|
async introspectScopeTables(scopeId) {
|
|
1008
|
-
return this.scopeStub(scopeId).
|
|
1009
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).introspectTablesIfHeld());
|
|
1009
1010
|
}
|
|
1011
|
+
/**
|
|
1012
|
+
* The Data view's table page (#2563): refused `conflict` with the shared sentence for a scope the
|
|
1013
|
+
* deployment does not hold, as the reads above are, never answered as an empty store. The carry
|
|
1014
|
+
* and its cleanup read a wiped copy's tombstone, which is not held by design: they use
|
|
1015
|
+
* `readTombstoneTable`.
|
|
1016
|
+
*/
|
|
1010
1017
|
async introspectScopeTable(scopeId, input) {
|
|
1018
|
+
const reply = heldOrRefused(scopeId, await this.scopeStub(scopeId).introspectTableIfHeld(input.table, input.limit, input.offset));
|
|
1019
|
+
return unwrapReply(reply);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* A table of a store this deployment may no longer HOLD, for the carry and its cleanup, which ask
|
|
1023
|
+
* a wiped copy whether it carries the `carried_away` tombstone (#2563). Not guarded: a wiped copy
|
|
1024
|
+
* answers nothing else. Not for a screen or a console; those use `introspectScopeTable`.
|
|
1025
|
+
*/
|
|
1026
|
+
async readTombstoneTable(scopeId, input) {
|
|
1011
1027
|
return unwrapReply(await this.scopeStub(scopeId).introspectTableReply(input.table, input.limit, input.offset));
|
|
1012
1028
|
}
|
|
1013
|
-
/** The SQL console's CP-less path (#219)
|
|
1029
|
+
/** The SQL console's CP-less path (#219): guarded as the table page is (#2563). */
|
|
1014
1030
|
async introspectScopeQuery(scopeId, input) {
|
|
1015
|
-
return unwrapReply(await this.scopeStub(scopeId).
|
|
1031
|
+
return unwrapReply(heldOrRefused(scopeId, await this.scopeStub(scopeId).introspectQueryIfHeld(input.sql)));
|
|
1016
1032
|
}
|
|
1017
1033
|
/**
|
|
1018
1034
|
* The K-35 denial log's CP-less path (#867) — same trust line as the reads above.
|
|
@@ -1333,39 +1349,39 @@ export class CloudflareScopeHost {
|
|
|
1333
1349
|
}
|
|
1334
1350
|
/** Facet this host's own scope's outbox (#1239) — the vertical-host read. */
|
|
1335
1351
|
async facetEventsLocal(scopeId, input) {
|
|
1336
|
-
return this.scopeStub(scopeId).
|
|
1352
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).facetEventsIfHeld(input));
|
|
1337
1353
|
}
|
|
1338
1354
|
/** What one event set off (#1237) on this host's own scope — the vertical-host read. */
|
|
1339
1355
|
async eventEffectsLocal(scopeId, input) {
|
|
1340
|
-
return this.scopeStub(scopeId).
|
|
1356
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).eventEffectsIfHeld(input));
|
|
1341
1357
|
}
|
|
1342
1358
|
/** Everything one call emitted (#1237) on this host's own scope — the vertical-host read. */
|
|
1343
1359
|
async invocationEventsLocal(scopeId, input) {
|
|
1344
|
-
return this.scopeStub(scopeId).
|
|
1360
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).invocationEventsIfHeld(input));
|
|
1345
1361
|
}
|
|
1346
1362
|
/** Every delivery that gave up (#1525) on this host's own scope — the vertical-host read. */
|
|
1347
1363
|
async deadLettersLocal(scopeId, input) {
|
|
1348
|
-
return this.scopeStub(scopeId).
|
|
1364
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).deadLettersIfHeld(input));
|
|
1349
1365
|
}
|
|
1350
1366
|
/** One entity's lifecycle replayed (#1744) on this host's own scope — the vertical-host read. */
|
|
1351
1367
|
async lifecycleFlowLocal(scopeId, input) {
|
|
1352
|
-
return this.scopeStub(scopeId).
|
|
1368
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).lifecycleFlowIfHeld(input));
|
|
1353
1369
|
}
|
|
1354
1370
|
/** Business volumes per bucket (#1750) on this host's own scope — the vertical-host read. */
|
|
1355
1371
|
async operationSeriesLocal(scopeId, input) {
|
|
1356
|
-
return this.scopeStub(scopeId).
|
|
1372
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).operationSeriesIfHeld(input));
|
|
1357
1373
|
}
|
|
1358
1374
|
/** One event's causal chain (#1237) on this host's own scope — the vertical-host read. */
|
|
1359
1375
|
async eventCauseLocal(scopeId, input) {
|
|
1360
|
-
return this.scopeStub(scopeId).
|
|
1376
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).eventCauseIfHeld(input));
|
|
1361
1377
|
}
|
|
1362
1378
|
/** One record's event history (#1235) on this host's own scope — the vertical-host read. */
|
|
1363
1379
|
async entityHistoryLocal(scopeId, input) {
|
|
1364
|
-
return this.scopeStub(scopeId).
|
|
1380
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).entityHistoryIfHeld(input));
|
|
1365
1381
|
}
|
|
1366
1382
|
/** When this host's own scope applied each migration (#1236) — the vertical-host read. */
|
|
1367
1383
|
async appliedMigrationsLocal(scopeId) {
|
|
1368
|
-
return this.scopeStub(scopeId).
|
|
1384
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).appliedMigrationsIfHeld());
|
|
1369
1385
|
}
|
|
1370
1386
|
/**
|
|
1371
1387
|
* This host's own scope's database size in bytes (#1524), the vertical-host read. A scope this
|
|
@@ -1418,7 +1434,8 @@ export class CloudflareScopeHost {
|
|
|
1418
1434
|
* so a caller cannot ask this side for more than the other would.
|
|
1419
1435
|
*/
|
|
1420
1436
|
async undrainedEventsLocal(scopeId, limit) {
|
|
1421
|
-
|
|
1437
|
+
const bounded = Math.min(Math.max(limit, 1), 1000);
|
|
1438
|
+
return undrainedEventsOf(heldOrRefused(scopeId, await this.scopeStub(scopeId).undrainedEventsReadIfHeld(bounded)));
|
|
1422
1439
|
}
|
|
1423
1440
|
/**
|
|
1424
1441
|
* Stamp `drained_at` on this host's own scope (#1334) — the delegation's other half.
|
|
@@ -1444,7 +1461,7 @@ export class CloudflareScopeHost {
|
|
|
1444
1461
|
* platform's `redrainEvents` is the door, and a count egresses nothing to record.
|
|
1445
1462
|
*/
|
|
1446
1463
|
async redrainCountLocal(scopeId, drainedBefore) {
|
|
1447
|
-
return this.scopeStub(scopeId).
|
|
1464
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).redrainCountIfHeld(drainedBefore));
|
|
1448
1465
|
}
|
|
1449
1466
|
/**
|
|
1450
1467
|
* The PITR bookmarks one scope recorded before its migration passes (#286) — the
|
|
@@ -1452,7 +1469,7 @@ export class CloudflareScopeHost {
|
|
|
1452
1469
|
* `/internal/bookmarks`; the control plane is the gate and the auditor.
|
|
1453
1470
|
*/
|
|
1454
1471
|
async migrationBookmarksLocal(scopeId) {
|
|
1455
|
-
return this.scopeStub(scopeId).
|
|
1472
|
+
return heldOrRefused(scopeId, await this.scopeStub(scopeId).migrationBookmarksIfHeld());
|
|
1456
1473
|
}
|
|
1457
1474
|
/**
|
|
1458
1475
|
* Rewind one scope to a pre-migration bookmark (#286's backout) — schema AND data,
|
|
@@ -1940,19 +1957,28 @@ export class CloudflareScopeHost {
|
|
|
1940
1957
|
throw new Error(`connector attachment ${verb} is not delegated (#574) — upload and open are the ` +
|
|
1941
1958
|
`verbs that cross the /internal seam to the serving deployment`);
|
|
1942
1959
|
};
|
|
1960
|
+
const openArgs = (attachmentId) => ({
|
|
1961
|
+
connectionId,
|
|
1962
|
+
tenantId: tenant,
|
|
1963
|
+
scopeId,
|
|
1964
|
+
vertical,
|
|
1965
|
+
attachmentId,
|
|
1966
|
+
// #726 remedy B: the delivery, resolved at the far end against the
|
|
1967
|
+
// deployment's own outbox. Absent outside a dispatch, where the ordinary
|
|
1968
|
+
// permission check still applies.
|
|
1969
|
+
eventId: forEvent?.eventId,
|
|
1970
|
+
});
|
|
1943
1971
|
return {
|
|
1944
1972
|
upload: (upload) => delegation.uploadAttachment({ connectionId, tenantId: tenant, scopeId, vertical, upload }),
|
|
1945
|
-
open: (attachmentId) => delegation.openAttachment(
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
eventId: forEvent?.eventId,
|
|
1955
|
-
}),
|
|
1973
|
+
open: (attachmentId) => delegation.openAttachment(openArgs(attachmentId)),
|
|
1974
|
+
// #2514: streamed over the seam when the embedder can. One that cannot reads the bytes whole,
|
|
1975
|
+
// as `open` does, and hands them on as a stream, so the verb still works and holds what it did.
|
|
1976
|
+
openStream: async (attachmentId) => {
|
|
1977
|
+
if (delegation.openAttachmentStream)
|
|
1978
|
+
return delegation.openAttachmentStream(openArgs(attachmentId));
|
|
1979
|
+
const opened = await delegation.openAttachment(openArgs(attachmentId));
|
|
1980
|
+
return opened && { record: opened.record, body: bytesStream(opened.body), contentType: opened.contentType };
|
|
1981
|
+
},
|
|
1956
1982
|
list: notDelegated('list'),
|
|
1957
1983
|
remove: notDelegated('remove'),
|
|
1958
1984
|
// #1575: undelegated for `list`'s reason — a connector picks a document by id.
|
|
@@ -1999,6 +2025,10 @@ export class CloudflareScopeHost {
|
|
|
1999
2025
|
const record = unwrapReply(await stub.capabilityAttachmentOpen(attachmentId, hash, tenantId, scopeId));
|
|
2000
2026
|
return record ? this.openAttachmentBytes(store, scopeId, record) : null;
|
|
2001
2027
|
},
|
|
2028
|
+
openStream: async (attachmentId) => {
|
|
2029
|
+
const record = unwrapReply(await stub.capabilityAttachmentOpen(attachmentId, hash, tenantId, scopeId));
|
|
2030
|
+
return record ? openAttachmentByteStream(store, attachmentBlobKey(scopeId, record.id), record, workerdSha256) : null;
|
|
2031
|
+
},
|
|
2002
2032
|
remove: async (attachmentId) => unwrapReply(await stub.capabilityAttachmentRefuseWrite(hash, tenantId, scopeId, 'attachments.remove', {
|
|
2003
2033
|
attachmentId,
|
|
2004
2034
|
})),
|
|
@@ -2018,6 +2048,8 @@ export class CloudflareScopeHost {
|
|
|
2018
2048
|
const asPrincipalId = ('principal' in subject
|
|
2019
2049
|
? subject.principal
|
|
2020
2050
|
: subject.connectionId);
|
|
2051
|
+
// The read gate, asked in the DO before the blob store is touched: a refused read never reaches it.
|
|
2052
|
+
const authorizeRead = async (attachmentId) => unwrapReply(await stub.attachmentAuthorizeReply(attachmentId, 'read', asPrincipalId, tenantId, scopeId, connectionId, forEvent?.eventId));
|
|
2021
2053
|
return {
|
|
2022
2054
|
upload: async (input) => {
|
|
2023
2055
|
const id = ulid();
|
|
@@ -2044,9 +2076,14 @@ export class CloudflareScopeHost {
|
|
|
2044
2076
|
},
|
|
2045
2077
|
list: async (entity) => unwrapReply(await stub.attachmentListReply(entity, asPrincipalId, tenantId, scopeId, connectionId)),
|
|
2046
2078
|
open: async (attachmentId) => {
|
|
2047
|
-
const record =
|
|
2079
|
+
const record = await authorizeRead(attachmentId);
|
|
2048
2080
|
return record ? this.openAttachmentBytes(store, scopeId, record) : null;
|
|
2049
2081
|
},
|
|
2082
|
+
// #2514: the same gate, then the bytes as R2 reads them, checked against the record as they pass.
|
|
2083
|
+
openStream: async (attachmentId) => {
|
|
2084
|
+
const record = await authorizeRead(attachmentId);
|
|
2085
|
+
return record ? openAttachmentByteStream(store, attachmentBlobKey(scopeId, record.id), record, workerdSha256) : null;
|
|
2086
|
+
},
|
|
2050
2087
|
remove: async (attachmentId) => {
|
|
2051
2088
|
const removed = unwrapReply(await stub.attachmentRemoveReply(attachmentId, asPrincipalId, tenantId, scopeId, connectionId));
|
|
2052
2089
|
if (removed)
|
|
@@ -2080,7 +2117,17 @@ export class CloudflareScopeHost {
|
|
|
2080
2117
|
return { record, body: obj.body, contentType: obj.contentType ?? record.contentType };
|
|
2081
2118
|
}
|
|
2082
2119
|
async importScope(actor, input, dump) {
|
|
2120
|
+
await this.importScopeFrom(actor, input, dump, true);
|
|
2121
|
+
}
|
|
2122
|
+
/**
|
|
2123
|
+
* `importScope`, with its role judgement (#2555) when `judge`, made before the scope is created so a
|
|
2124
|
+
* refused import leaves nothing behind. A snapshot passes false: it is the platform's copy of a live
|
|
2125
|
+
* scope of this host, made at once, so its seats are the ones that scope holds now (K-55).
|
|
2126
|
+
*/
|
|
2127
|
+
async importScopeFrom(actor, input, dump, judge) {
|
|
2083
2128
|
this.refuseUnusableScopeStores(dump.stores, input.vertical ?? null);
|
|
2129
|
+
if (judge)
|
|
2130
|
+
await this.refuseDumpRoles(input.tenantId, null, dump);
|
|
2084
2131
|
// Create the destination scope (directory row + DO + lazy migrate); the DO then
|
|
2085
2132
|
// replaces its provisioned schema with the dump wholesale (drop-then-replay), so
|
|
2086
2133
|
// the end state is the dump, at the source's frontier. Provenance is stamped from
|
|
@@ -2105,6 +2152,8 @@ export class CloudflareScopeHost {
|
|
|
2105
2152
|
if (!existing)
|
|
2106
2153
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2107
2154
|
this.refuseUnusableScopeStores(dump.stores, existing.vertical);
|
|
2155
|
+
// #2555: judged before the fence, so a refused restore moves nothing.
|
|
2156
|
+
await this.refuseDumpRoles(tenantId, scopeId, dump);
|
|
2108
2157
|
// #2314: the load replaces the scope's rows, so the orphan sweep is fenced first.
|
|
2109
2158
|
await this.fenceOrphanSweep(scopeId, tenantId);
|
|
2110
2159
|
// #1742: a dump from before a switch was pulled brings the module's grants back live, so
|
|
@@ -2171,7 +2220,7 @@ export class CloudflareScopeHost {
|
|
|
2171
2220
|
throw substratError('not_found', `unknown scope ${scopeId} in tenant ${tenantId}`);
|
|
2172
2221
|
const dump = await this.admin.exportScope(actor, tenantId, scopeId);
|
|
2173
2222
|
const snapshotId = ulid();
|
|
2174
|
-
await this.
|
|
2223
|
+
await this.importScopeFrom(actor, {
|
|
2175
2224
|
tenantId,
|
|
2176
2225
|
scopeId: snapshotId,
|
|
2177
2226
|
kind: opts?.kind ?? 'archive',
|
|
@@ -2180,7 +2229,7 @@ export class CloudflareScopeHost {
|
|
|
2180
2229
|
jurisdiction: source.jurisdiction,
|
|
2181
2230
|
expiresAt: opts?.expiresAt,
|
|
2182
2231
|
// forkedFrom/forkedAt are stamped from the dump by importScope.
|
|
2183
|
-
}, dump);
|
|
2232
|
+
}, dump, false);
|
|
2184
2233
|
// Bind the snapshot to the source's current version so it is a runnable copy at the
|
|
2185
2234
|
// same frontier (a fresh bind, so it never re-triggers the snapshot path).
|
|
2186
2235
|
if (source.verticalVersionId) {
|
|
@@ -4705,7 +4754,9 @@ export class CloudflareScopeHost {
|
|
|
4705
4754
|
// Lands PENDING — a push is not a deploy — except for a PRIVATE vertical
|
|
4706
4755
|
// (tenant-owned, not listed), whose blast radius is its own tenant: there the
|
|
4707
4756
|
// sandbox contract is the gate and the version self-admits, noted so the
|
|
4708
|
-
// publish seam can tell a staff vouch from this shortcut.
|
|
4757
|
+
// publish seam can tell a staff vouch from this shortcut. The DO judges it again on the
|
|
4758
|
+
// vertical as the insert's unit reads it, and that is what the row and the version carry
|
|
4759
|
+
// (#2528); this read answers an older DO, which takes what it is sent.
|
|
4709
4760
|
const selfAdmits = owning.owner_tenant !== null && !owning.listed;
|
|
4710
4761
|
// The manifest is retained for the serving upload (#286), not audited — a whole
|
|
4711
4762
|
// manifest per publish would drown the admin log in bundle metadata.
|
|
@@ -4751,67 +4802,33 @@ export class CloudflareScopeHost {
|
|
|
4751
4802
|
await this.recordAccess(actor, 'getVersion', {}, { versionId, verticalSlug }, hit ? 1 : 0);
|
|
4752
4803
|
return hit ? mapVersion(hit) : undefined;
|
|
4753
4804
|
},
|
|
4805
|
+
// #2528: each vertical write below is one DO call that reads the row's `before`, and runs its
|
|
4806
|
+
// guard, in the unit that writes; it refuses an unknown vertical with not_found.
|
|
4754
4807
|
setVerticalListed: async (actor, slug, listed) => {
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
//
|
|
4759
|
-
|
|
4760
|
-
// prod version has never been read by anyone but its author.
|
|
4761
|
-
if (listed) {
|
|
4762
|
-
const prod = await this.cp.readChannel(slug, 'prod');
|
|
4763
|
-
const prodVersion = prod ? await this.cp.readVersion(prod.version_id) : undefined;
|
|
4764
|
-
if (prodVersion?.admission_note === AUTO_ADMISSION_NOTE) {
|
|
4765
|
-
throw substratError('conflict', `vertical '${slug}' prod version ${prodVersion.id} is auto-admitted (private self-serve) — ` +
|
|
4766
|
-
`a staff admit must vouch for it before listing`);
|
|
4767
|
-
}
|
|
4768
|
-
}
|
|
4769
|
-
await this.cp.updateVerticalListed(slug, listed ? 1 : 0, // also resolves any pending request
|
|
4770
|
-
this.adminEntry(actor, 'setVerticalListed', { tenantId: null }, { listed: !!existing.listed }, { listed }));
|
|
4808
|
+
// Listing is the moment other tenants start trusting this code, so the version they
|
|
4809
|
+
// would install must carry a real staff vouch — an auto-admitted prod version has never
|
|
4810
|
+
// been read by anyone but its author. The DO refuses one; it also resolves any pending
|
|
4811
|
+
// publish request.
|
|
4812
|
+
await this.directory('updateVerticalListed', slug, listed ? 1 : 0, this.adminEntry(actor, 'setVerticalListed', { tenantId: null }, null, { listed }));
|
|
4771
4813
|
},
|
|
4772
4814
|
requestPublish: async (actor, slug) => {
|
|
4773
|
-
|
|
4774
|
-
if (!existing)
|
|
4775
|
-
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
4776
|
-
await this.cp.updateVerticalPublishRequest(slug, new Date().toISOString(), this.adminEntry(actor, 'requestPublish', { tenantId: null }, null, { slug }));
|
|
4815
|
+
await this.directory('updateVerticalPublishRequest', slug, new Date().toISOString(), this.adminEntry(actor, 'requestPublish', { tenantId: null }, null, { slug }));
|
|
4777
4816
|
},
|
|
4778
4817
|
setVerticalInstallsBlocked: async (actor, slug, blocked) => {
|
|
4779
|
-
|
|
4780
|
-
if (!existing)
|
|
4781
|
-
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
4782
|
-
await this.cp.updateVerticalInstallsBlocked(slug, blocked ? 1 : 0, this.adminEntry(actor, 'setVerticalInstallsBlocked', { tenantId: null }, { installsBlocked: !!existing.installs_blocked }, { installsBlocked: blocked }));
|
|
4818
|
+
await this.directory('updateVerticalInstallsBlocked', slug, blocked ? 1 : 0, this.adminEntry(actor, 'setVerticalInstallsBlocked', { tenantId: null }, null, { installsBlocked: blocked }));
|
|
4783
4819
|
},
|
|
4784
4820
|
setVerticalTenantProvisioner: async (actor, slug, granted) => {
|
|
4785
|
-
|
|
4786
|
-
if (!existing)
|
|
4787
|
-
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
4788
|
-
await this.cp.updateVerticalTenantProvisioner(slug, granted ? 1 : 0, this.adminEntry(actor, 'setVerticalTenantProvisioner', { tenantId: null }, { tenantProvisioner: !!existing.tenant_provisioner }, { tenantProvisioner: granted }));
|
|
4821
|
+
await this.directory('updateVerticalTenantProvisioner', slug, granted ? 1 : 0, this.adminEntry(actor, 'setVerticalTenantProvisioner', { tenantId: null }, null, { tenantProvisioner: granted }));
|
|
4789
4822
|
},
|
|
4790
4823
|
setVerticalEmailSender: async (actor, slug, granted) => {
|
|
4791
|
-
|
|
4792
|
-
if (!existing)
|
|
4793
|
-
throw substratError('not_found', `unknown vertical '${slug}'`);
|
|
4794
|
-
await this.cp.updateVerticalEmailSender(slug, granted ? 1 : 0, this.adminEntry(actor, 'setVerticalEmailSender', { tenantId: null }, { emailSender: !!existing.email_sender }, { emailSender: granted }));
|
|
4824
|
+
await this.directory('updateVerticalEmailSender', slug, granted ? 1 : 0, this.adminEntry(actor, 'setVerticalEmailSender', { tenantId: null }, null, { emailSender: granted }));
|
|
4795
4825
|
},
|
|
4796
4826
|
deleteVertical: async (actor, slug) => {
|
|
4797
|
-
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
//
|
|
4801
|
-
|
|
4802
|
-
// blocks — unarchive can bring it back — but the refusal names reap/restore, not
|
|
4803
|
-
// "delete", because the app itself is already gone. `reaped` is terminal history and
|
|
4804
|
-
// never blocks. Deployed dispatch scripts are NOT reaped here — they become orphans
|
|
4805
|
-
// for the cleanup script (#248).
|
|
4806
|
-
const bound = await this.cp.countScopesForVertical(slug);
|
|
4807
|
-
if (bound.live > 0) {
|
|
4808
|
-
throw substratError('conflict', `vertical '${slug}' still backs ${bound.live} scope(s) — delete or rebind them first`);
|
|
4809
|
-
}
|
|
4810
|
-
if (bound.archived > 0) {
|
|
4811
|
-
throw substratError('conflict', `vertical '${slug}' still backs ${bound.archived} archived scope(s) — reap or restore them first`);
|
|
4812
|
-
}
|
|
4813
|
-
// #2501: the period's `until` is read here, on the clock its `since` was.
|
|
4814
|
-
await this.cp.deleteVertical(slug, this.adminEntry(actor, 'deleteVertical', { tenantId: null }, { slug, source: existing.source, ownerTenant: existing.owner_tenant }, null), verticalOwnerNow());
|
|
4827
|
+
// Refused while any restorable scope is bound (the DO counts them in the delete's unit).
|
|
4828
|
+
// `reaped` is terminal history and never blocks. Deployed dispatch scripts are NOT reaped
|
|
4829
|
+
// here — they become orphans for the cleanup script (#248). #2501: the period's `until`
|
|
4830
|
+
// is read here, on the clock its `since` was.
|
|
4831
|
+
await this.directory('deleteVertical', slug, this.adminEntry(actor, 'deleteVertical', { tenantId: null }, null, null), verticalOwnerNow());
|
|
4815
4832
|
},
|
|
4816
4833
|
listVerticalOwners: async (actor, slug) => {
|
|
4817
4834
|
const periods = (await this.cp.listVerticalOwners(slug)).map((p) => verticalOwnerPeriod.parse(p));
|
|
@@ -4819,21 +4836,9 @@ export class CloudflareScopeHost {
|
|
|
4819
4836
|
return periods;
|
|
4820
4837
|
},
|
|
4821
4838
|
admitVersion: async (actor, versionId) => {
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
if (v.admission === 'admitted') {
|
|
4826
|
-
// Idempotent — except an AUTO-admitted version, which this upgrades to a
|
|
4827
|
-
// manual vouch by clearing the note (what the publish seam requires).
|
|
4828
|
-
if (v.admission_note !== AUTO_ADMISSION_NOTE)
|
|
4829
|
-
return;
|
|
4830
|
-
await this.cp.setAdmission(versionId, 'admitted', null, this.adminEntry(actor, 'admitVersion', { tenantId: null }, { admission: v.admission, note: v.admission_note }, { admission: 'admitted', note: null }));
|
|
4831
|
-
return;
|
|
4832
|
-
}
|
|
4833
|
-
if (v.admission === 'rejected') {
|
|
4834
|
-
throw substratError('conflict', `version ${versionId} was rejected — publish a new one`);
|
|
4835
|
-
}
|
|
4836
|
-
await this.cp.setAdmission(versionId, 'admitted', null, this.adminEntry(actor, 'admitVersion', { tenantId: null }, { admission: v.admission }, { admission: 'admitted' }));
|
|
4839
|
+
// Judged in the DO's unit (#2528): idempotent on an admitted version, except an AUTO-admitted
|
|
4840
|
+
// one, which it upgrades to a manual vouch; a rejected one is refused. The DO fills the row.
|
|
4841
|
+
await this.directory('admitVersion', versionId, this.adminEntry(actor, 'admitVersion', { tenantId: null }, null, null));
|
|
4837
4842
|
},
|
|
4838
4843
|
promotionImpact: async (actor, verticalSlug, channel, versionId) => {
|
|
4839
4844
|
const incoming = await this.cp.readVersion(versionId);
|
|
@@ -4849,15 +4854,8 @@ export class CloudflareScopeHost {
|
|
|
4849
4854
|
return breaks;
|
|
4850
4855
|
},
|
|
4851
4856
|
rejectVersion: async (actor, versionId, note) => {
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
throw substratError('not_found', `unknown version ${versionId}`);
|
|
4855
|
-
if (v.admission === 'admitted') {
|
|
4856
|
-
throw substratError('conflict', `version ${versionId} is already admitted — it may be bound`);
|
|
4857
|
-
}
|
|
4858
|
-
if (v.admission === 'rejected')
|
|
4859
|
-
return;
|
|
4860
|
-
await this.cp.setAdmission(versionId, 'rejected', note, this.adminEntry(actor, 'rejectVersion', { tenantId: null }, { admission: v.admission }, { admission: 'rejected', note }));
|
|
4857
|
+
// Judged in the DO's unit (#2528): an admitted version is refused, a rejected one is a no-op.
|
|
4858
|
+
await this.directory('rejectVersion', versionId, note, this.adminEntry(actor, 'rejectVersion', { tenantId: null }, null, { admission: 'rejected', note }));
|
|
4861
4859
|
},
|
|
4862
4860
|
promoteVersion: async (actor, verticalSlug, channel, versionId, acknowledge) => {
|
|
4863
4861
|
const incoming = await this.cp.readVersion(versionId);
|
|
@@ -5134,14 +5132,16 @@ export class CloudflareScopeHost {
|
|
|
5134
5132
|
await this.assertNoStrandFrom([r.serving_version_id], incoming.id, incoming, r.serving_ref ? await this.cp.provisionedVersionsOf(verticalSlug, { ref: r.serving_ref, versionId: r.serving_version_id }, null) : []);
|
|
5135
5133
|
}
|
|
5136
5134
|
}
|
|
5137
|
-
|
|
5135
|
+
// #2528: a compare-and-set on the serving ref and version the gate above judged, across
|
|
5136
|
+
// awaits another serve can run in. The DO fills the row's `before` from the state it moves.
|
|
5137
|
+
const moved = await this.directory('setVerticalServing', verticalSlug, {
|
|
5138
5138
|
ref: parsed.ref,
|
|
5139
5139
|
versionId: parsed.versionId,
|
|
5140
5140
|
doClassesJson: JSON.stringify(parsed.doClasses),
|
|
5141
5141
|
migrationTag: parsed.migrationTag,
|
|
5142
|
-
}, this.adminEntry(actor, 'setVerticalServing', { tenantId: null }, r.serving_ref
|
|
5143
|
-
|
|
5144
|
-
|
|
5142
|
+
}, this.adminEntry(actor, 'setVerticalServing', { tenantId: null }, null, { vertical: verticalSlug, ref: parsed.ref, versionId: parsed.versionId }), { ref: r.serving_ref ?? null, versionId: r.serving_version_id ?? null });
|
|
5143
|
+
if (!moved)
|
|
5144
|
+
throw servingRaceRefusal(verticalSlug);
|
|
5145
5145
|
},
|
|
5146
5146
|
versionManifest: async (actor, verticalSlug, versionId) => {
|
|
5147
5147
|
const v = await this.cp.readVersion(versionId);
|
|
@@ -5161,7 +5161,10 @@ export class CloudflareScopeHost {
|
|
|
5161
5161
|
},
|
|
5162
5162
|
recordWireAuth: async (actor, input) => {
|
|
5163
5163
|
const row = deploymentWireAuth.parse(input);
|
|
5164
|
-
await this.cp.recordWireAuth(row, this.adminEntry(actor, 'recordWireAuth', { tenantId: null, vertical: row.verticalSlug }, null, { vertical: row.verticalSlug, deploymentRef: row.deploymentRef, versionId: row.versionId, record: row.record }));
|
|
5164
|
+
const written = await this.cp.recordWireAuth(row, this.adminEntry(actor, 'recordWireAuth', { tenantId: null, vertical: row.verticalSlug }, null, { vertical: row.verticalSlug, deploymentRef: row.deploymentRef, versionId: row.versionId, record: row.record }));
|
|
5165
|
+
// Thrown here, not in the directory's object, so the code survives the RPC.
|
|
5166
|
+
if (!written)
|
|
5167
|
+
throw substratError('not_found', `no capability record for '${row.deploymentRef}': vertical '${row.verticalSlug}' is not registered`);
|
|
5165
5168
|
},
|
|
5166
5169
|
listWireAuth: async (actor, input) => {
|
|
5167
5170
|
// Judged here, before the call into the directory's object, so a refusal keeps its code; the
|
|
@@ -5458,6 +5461,9 @@ export class CloudflareScopeHost {
|
|
|
5458
5461
|
recordStoreDriverRebuildRun: async (actor, entry) => {
|
|
5459
5462
|
await this.recordAdmin(actor, 'rebuildStoreDriver', { tenantId: null }, null, entry);
|
|
5460
5463
|
},
|
|
5464
|
+
recordWireWalk: async (actor, entry) => {
|
|
5465
|
+
await this.recordAdmin(actor, 'walkWireAuth', { tenantId: null }, null, entry);
|
|
5466
|
+
},
|
|
5461
5467
|
listScopeTables: async (actor, tenantId, scopeId) => {
|
|
5462
5468
|
// K-3 cross-check on the shared directory BEFORE reaching the scope DO: a pair
|
|
5463
5469
|
// that does not resolve is unreachable, never another tenant's database.
|
|
@@ -7831,6 +7837,29 @@ export class CloudflareScopeHost {
|
|
|
7831
7837
|
const here = await this.scopeStub(scopeId).reservationHere();
|
|
7832
7838
|
return reservedRoleSetRefusal(roles, { reserved: new Set([...this.reservedKeys, ...here.reserved]), declared: new Set([...this.declaredKeys, ...here.declared]) }, dump ? dumpRoleState(dump.tables, dump.tenantId, new Date().toISOString()) : undefined);
|
|
7833
7839
|
}
|
|
7840
|
+
/**
|
|
7841
|
+
* #2555: refuse loading `dump` into a scope of `tenantId` (`dumpRoleRefusal`): the tenant's roles as
|
|
7842
|
+
* the directory stores them now, against the dump's role rows and seats, with this host's
|
|
7843
|
+
* reservations joined with the scope class's. The class is read at `scopeId` when the scope exists
|
|
7844
|
+
* (a restore), else at a live scope of the tenant (an import, whose scope is not created yet: asking
|
|
7845
|
+
* its object would create storage for a scope a refusal leaves unmade).
|
|
7846
|
+
*
|
|
7847
|
+
* Two stated residuals. The directory is read here and the dump is loaded in the scope's own call,
|
|
7848
|
+
* so a role defined between the two is not judged against the dump's seats. And an import for a
|
|
7849
|
+
* tenant with no live scope reads no class, so it is judged with this host's reservations alone: the
|
|
7850
|
+
* seat rule holds without them, and a key only the class reserves is not counted.
|
|
7851
|
+
*/
|
|
7852
|
+
async refuseDumpRoles(tenantId, scopeId, dump) {
|
|
7853
|
+
const classScope = scopeId ?? (await this.cp.listScopes({ tenantId, status: ['active', 'suspended'], limit: 1 }))[0]?.scope_id;
|
|
7854
|
+
const [rows, here] = await Promise.all([
|
|
7855
|
+
this.cp.listRoles({ tenantId }),
|
|
7856
|
+
classScope ? this.scopeStub(classScope).reservationHere() : { reserved: [], declared: [] },
|
|
7857
|
+
]);
|
|
7858
|
+
const roles = rows.map((r) => ({ key: r.role_key, permissions: JSON.parse(r.permissions), ...(r.holder ? { holder: r.holder } : {}) }));
|
|
7859
|
+
const refusal = dumpRoleRefusal(roles, { reserved: new Set([...this.reservedKeys, ...here.reserved]), declared: new Set([...this.declaredKeys, ...here.declared]) }, dump, new Date().toISOString());
|
|
7860
|
+
if (refusal)
|
|
7861
|
+
throw substratError('forbidden', refusal);
|
|
7862
|
+
}
|
|
7834
7863
|
/**
|
|
7835
7864
|
* #2446: the roles `scopeId` stores for `tenantId` and the role keys anyone but a system principal
|
|
7836
7865
|
* holds a live seat in there: what `refuseReservedRoleSet` judges a reconcile's role set against
|
|
@@ -7854,7 +7883,10 @@ export class CloudflareScopeHost {
|
|
|
7854
7883
|
const stub = this.scopeStub(scopeId);
|
|
7855
7884
|
const [here, state] = await Promise.all([stub.reservationHere(), stub.roleStateHere(tenantId)]);
|
|
7856
7885
|
const reserved = new Set([...this.reservedKeys, ...here.reserved]);
|
|
7857
|
-
const refusal = reservedRoleSetRefusal(roles, { reserved, declared: new Set([...this.declaredKeys, ...here.declared]) },
|
|
7886
|
+
const refusal = reservedRoleSetRefusal(roles, { reserved, declared: new Set([...this.declaredKeys, ...here.declared]) },
|
|
7887
|
+
// #2531: with the tombstones the scope keeps, so a system-held role a release dropped is judged
|
|
7888
|
+
// as the stored definition it was. A ScopeDO from before them answers none.
|
|
7889
|
+
{ roles: state.roles, seated: new Set(state.seated), ...(state.dropped ? { dropped: state.dropped } : {}) });
|
|
7858
7890
|
// Named by tenant and scope as well as role: a release push admitted can be refused here at every
|
|
7859
7891
|
// scope where the role is seated, and staff find each one by this line (K-55).
|
|
7860
7892
|
if (refusal)
|
|
@@ -8137,6 +8169,17 @@ export class CloudflareScopeHost {
|
|
|
8137
8169
|
* so a caller falls back rather than failing a dispatch over a missing file.
|
|
8138
8170
|
*/
|
|
8139
8171
|
async connectorAttachmentOpenLocal(connectionId, tenantId, scopeId, attachmentId, eventId) {
|
|
8172
|
+
return (await this.connectorReadSurface(connectionId, tenantId, scopeId, eventId)).open(attachmentId);
|
|
8173
|
+
}
|
|
8174
|
+
/**
|
|
8175
|
+
* `connectorAttachmentOpenLocal`, with the bytes as a stream read from R2 as the platform reads
|
|
8176
|
+
* the response (#2514). The same gates, in the same order, before the bucket is touched.
|
|
8177
|
+
*/
|
|
8178
|
+
async connectorAttachmentOpenStreamLocal(connectionId, tenantId, scopeId, attachmentId, eventId) {
|
|
8179
|
+
return (await this.connectorReadSurface(connectionId, tenantId, scopeId, eventId)).openStream(attachmentId);
|
|
8180
|
+
}
|
|
8181
|
+
/** The attachment surface the platform's reads as a connection go through, behind their gates. */
|
|
8182
|
+
async connectorReadSurface(connectionId, tenantId, scopeId, eventId) {
|
|
8140
8183
|
// Same gate as the upload leg: a held scope's bytes are not read out of its bucket.
|
|
8141
8184
|
await this.assertLive(tenantId, scopeId);
|
|
8142
8185
|
await this.migrateAndRecord(scopeId);
|
|
@@ -8144,7 +8187,7 @@ export class CloudflareScopeHost {
|
|
|
8144
8187
|
return this.buildAttachmentSurface({ connectionId }, tenantId, scopeId, store,
|
|
8145
8188
|
// #726 remedy B: the platform named a delivery; this deployment decides what that
|
|
8146
8189
|
// delivery reaches, from its own spine row. Absent ⇒ the ordinary grant check.
|
|
8147
|
-
eventId === undefined ? undefined : { eventId })
|
|
8190
|
+
eventId === undefined ? undefined : { eventId });
|
|
8148
8191
|
}
|
|
8149
8192
|
/**
|
|
8150
8193
|
* The delivery half of `grantToConnection` for a scope served HERE (#574): write
|