@substrat-run/adapter-cloudflare 0.163.0 → 0.165.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 +49 -28
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +282 -146
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +21 -2
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +70 -70
- package/dist/host.js.map +1 -1
- package/package.json +17 -14
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, 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, 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';
|
|
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, 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, 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';
|
|
@@ -165,9 +165,11 @@ export const SWITCH_HOLD_SNAPSHOT_MS = 2_000;
|
|
|
165
165
|
* one older than `SWITCH_HOLD_SNAPSHOT_MS`. A state read that met rewound storage therefore
|
|
166
166
|
* finished at least this long after the hold was written, so a snapshot taken before the hold
|
|
167
167
|
* is past its age by then and is re-read. Each side measures a duration on its own clock;
|
|
168
|
-
* nothing compares two clocks.
|
|
168
|
+
* nothing compares two clocks. Derived from the snapshot age, never set on its own, so no host
|
|
169
|
+
* can be given a settle that does not outlast its snapshot (`switchHoldSnapshotMs`).
|
|
169
170
|
*/
|
|
170
|
-
export const
|
|
171
|
+
export const switchHoldSettleMs = (snapshotMs) => (snapshotMs * 3) / 2;
|
|
172
|
+
export const SWITCH_HOLD_SETTLE_MS = switchHoldSettleMs(SWITCH_HOLD_SNAPSHOT_MS);
|
|
171
173
|
/**
|
|
172
174
|
* #1839: how many times a rewind waits again, after its settle, for a row a late OFF added to its
|
|
173
175
|
* claim to be `SWITCH_HOLD_SETTLE_MS` old. The settle argument above holds per ROW, so a row
|
|
@@ -479,6 +481,9 @@ export class CloudflareScopeHost {
|
|
|
479
481
|
* join this one read rather than each sending their own.
|
|
480
482
|
*/
|
|
481
483
|
holdRefresh = null;
|
|
484
|
+
/** #1819: `SWITCH_HOLD_SNAPSHOT_MS` and `SWITCH_HOLD_SETTLE_MS`, unless `switchHoldSnapshotMs` is given. */
|
|
485
|
+
holdSnapshotMs;
|
|
486
|
+
holdSettleMs;
|
|
482
487
|
/**
|
|
483
488
|
* MUST be constructed per request. Never cache an instance across requests.
|
|
484
489
|
*
|
|
@@ -509,6 +514,12 @@ export class CloudflareScopeHost {
|
|
|
509
514
|
// #2498: whatever fetch is injected, a sized stream body reaches it converted (`fetchLikeOf`).
|
|
510
515
|
this.fetchImpl = fetchLikeOf(options.fetch ?? globalFetch);
|
|
511
516
|
this.streamIdleMs = options.streamIdleTimeoutMs;
|
|
517
|
+
this.holdSnapshotMs = options.switchHoldSnapshotMs ?? SWITCH_HOLD_SNAPSHOT_MS;
|
|
518
|
+
// NaN or Infinity never expires a snapshot; zero or less settles for nothing.
|
|
519
|
+
if (!Number.isFinite(this.holdSnapshotMs) || this.holdSnapshotMs <= 0) {
|
|
520
|
+
throw new RangeError(`switchHoldSnapshotMs must be a positive, finite number of milliseconds, got ${this.holdSnapshotMs}`);
|
|
521
|
+
}
|
|
522
|
+
this.holdSettleMs = switchHoldSettleMs(this.holdSnapshotMs);
|
|
512
523
|
this.connectorCalls = options.connectorCalls ?? noopConnectorCallRecorder;
|
|
513
524
|
this.scopeLocalPermissions = options.scopeLocalPermissions ?? false;
|
|
514
525
|
this.scopeNs = options.scope;
|
|
@@ -1853,14 +1864,14 @@ export class CloudflareScopeHost {
|
|
|
1853
1864
|
// the source of truth, carrying the D1 database_id Cloudflare assigned as the ref.
|
|
1854
1865
|
const name = await tenantStoreDatabaseName(input.tenantId, input.vertical, input.binding);
|
|
1855
1866
|
const ref = await d1.create(name);
|
|
1856
|
-
const stored = await this.directory('
|
|
1867
|
+
const stored = await this.directory('putTenantStoreAndAudit', {
|
|
1857
1868
|
tenantId: input.tenantId,
|
|
1858
1869
|
vertical: input.vertical,
|
|
1859
1870
|
binding: input.binding,
|
|
1860
1871
|
kind: 'relational',
|
|
1861
1872
|
ref,
|
|
1862
1873
|
createdAt: new Date().toISOString(),
|
|
1863
|
-
});
|
|
1874
|
+
}, this.adminEntry(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref }));
|
|
1864
1875
|
if (stored.ref !== ref) {
|
|
1865
1876
|
// A concurrent provision won the ledger write. One database is canonical — the
|
|
1866
1877
|
// ledger's — so drop ours rather than orphan it (best-effort: a leaked delete
|
|
@@ -1868,7 +1879,6 @@ export class CloudflareScopeHost {
|
|
|
1868
1879
|
await d1.remove(ref).catch(() => undefined);
|
|
1869
1880
|
return { binding: input.binding, kind: 'relational', ref: stored.ref };
|
|
1870
1881
|
}
|
|
1871
|
-
await this.recordAdmin(actor, 'provisionTenantStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'relational', ref });
|
|
1872
1882
|
return { binding: input.binding, kind: 'relational', ref };
|
|
1873
1883
|
}
|
|
1874
1884
|
openTenantStore(handle) {
|
|
@@ -1914,19 +1924,18 @@ export class CloudflareScopeHost {
|
|
|
1914
1924
|
return { binding: input.binding, kind: 'blob', ref: existing.ref };
|
|
1915
1925
|
const name = await blobStoreBucketName(input.tenantId, input.vertical, input.binding);
|
|
1916
1926
|
const ref = await r2.create(name);
|
|
1917
|
-
const stored = await this.directory('
|
|
1927
|
+
const stored = await this.directory('putBlobStoreAndAudit', {
|
|
1918
1928
|
tenantId: input.tenantId,
|
|
1919
1929
|
vertical: input.vertical,
|
|
1920
1930
|
binding: input.binding,
|
|
1921
1931
|
kind: 'blob',
|
|
1922
1932
|
ref,
|
|
1923
1933
|
createdAt: new Date().toISOString(),
|
|
1924
|
-
});
|
|
1934
|
+
}, this.adminEntry(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref }));
|
|
1925
1935
|
if (stored.ref !== ref) {
|
|
1926
1936
|
await r2.remove(ref).catch(() => undefined);
|
|
1927
1937
|
return { binding: input.binding, kind: 'blob', ref: stored.ref };
|
|
1928
1938
|
}
|
|
1929
|
-
await this.recordAdmin(actor, 'provisionBlobStore', { tenantId: input.tenantId, vertical: input.vertical }, null, { binding: input.binding, kind: 'blob', ref });
|
|
1930
1939
|
return { binding: input.binding, kind: 'blob', ref };
|
|
1931
1940
|
}
|
|
1932
1941
|
/**
|
|
@@ -4792,6 +4801,9 @@ export class CloudflareScopeHost {
|
|
|
4792
4801
|
installSpec.provides = parsed.provides;
|
|
4793
4802
|
if (parsed.requires)
|
|
4794
4803
|
installSpec.requires = parsed.requires;
|
|
4804
|
+
// The declared vertical-defined connectors (vertical-connectors.md §7.2), the dashboard's door.
|
|
4805
|
+
if (parsed.connectors)
|
|
4806
|
+
installSpec.connectors = parsed.connectors;
|
|
4795
4807
|
// Declared provisioner intent (#455) — the request half of the tenant-provisioner
|
|
4796
4808
|
// capability; the grant is its own column, never part of this refreshable bag.
|
|
4797
4809
|
if (parsed.provisions)
|
|
@@ -4989,6 +5001,7 @@ export class CloudflareScopeHost {
|
|
|
4989
5001
|
ack,
|
|
4990
5002
|
channel,
|
|
4991
5003
|
baseId: outgoing?.id ?? null,
|
|
5004
|
+
permissionsChange: outgoing !== undefined && outgoing.permission_digest !== incoming.permission_digest,
|
|
4992
5005
|
migrationsChange: outgoing !== undefined && outgoing.migration_digest !== incoming.migration_digest,
|
|
4993
5006
|
read: async () => ({ base: outgoing ? await reviewed(outgoing) : null, incoming: await reviewed(incoming) }),
|
|
4994
5007
|
});
|
|
@@ -5207,11 +5220,14 @@ export class CloudflareScopeHost {
|
|
|
5207
5220
|
migrationTag: r.serving_migration_tag,
|
|
5208
5221
|
});
|
|
5209
5222
|
},
|
|
5210
|
-
setVerticalServing: async (actor, verticalSlug, state) => {
|
|
5223
|
+
setVerticalServing: async (actor, verticalSlug, state, expected) => {
|
|
5211
5224
|
const parsed = verticalServingState.parse(state);
|
|
5212
5225
|
const r = await this.cp.readVertical(verticalSlug);
|
|
5213
5226
|
if (!r)
|
|
5214
5227
|
throw substratError('not_found', `unknown vertical '${verticalSlug}'`);
|
|
5228
|
+
// #2577: the caller's upload diffed its DO-class migration from `expected`'s class set and tag.
|
|
5229
|
+
if (expected !== undefined && !migratesFrom(r, expected))
|
|
5230
|
+
throw servingRaceRefusal(verticalSlug);
|
|
5215
5231
|
// #2178: every install on the serving script runs the new version from this moment.
|
|
5216
5232
|
if (r.serving_version_id) {
|
|
5217
5233
|
const incoming = await this.cp.readVersion(parsed.versionId);
|
|
@@ -5219,14 +5235,15 @@ export class CloudflareScopeHost {
|
|
|
5219
5235
|
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) : []);
|
|
5220
5236
|
}
|
|
5221
5237
|
}
|
|
5222
|
-
// #2528: a compare-and-set on the serving
|
|
5223
|
-
// awaits another serve can run in. The DO fills the row's `before`
|
|
5238
|
+
// #2528/#2577: a compare-and-set on the serving state the gate above judged, its class set
|
|
5239
|
+
// and tag included, across awaits another serve can run in. The DO fills the row's `before`
|
|
5240
|
+
// from the state it moves.
|
|
5224
5241
|
const moved = await this.directory('setVerticalServing', verticalSlug, {
|
|
5225
5242
|
ref: parsed.ref,
|
|
5226
5243
|
versionId: parsed.versionId,
|
|
5227
5244
|
doClassesJson: JSON.stringify(parsed.doClasses),
|
|
5228
5245
|
migrationTag: parsed.migrationTag,
|
|
5229
|
-
}, this.adminEntry(actor, 'setVerticalServing', { tenantId: null }, null, { vertical: verticalSlug, ref: parsed.ref, versionId: parsed.versionId }),
|
|
5246
|
+
}, this.adminEntry(actor, 'setVerticalServing', { tenantId: null }, null, { vertical: verticalSlug, ref: parsed.ref, versionId: parsed.versionId }), servedFromRow(r));
|
|
5230
5247
|
if (!moved)
|
|
5231
5248
|
throw servingRaceRefusal(verticalSlug);
|
|
5232
5249
|
},
|
|
@@ -5491,9 +5508,7 @@ export class CloudflareScopeHost {
|
|
|
5491
5508
|
// -- store-driver releases, rollouts and holds (D-63, #2300) -------------------
|
|
5492
5509
|
recordStoreDriverRelease: async (actor, input) => {
|
|
5493
5510
|
const at = new Date().toISOString();
|
|
5494
|
-
const { record
|
|
5495
|
-
if (created)
|
|
5496
|
-
await this.recordAdmin(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, record);
|
|
5511
|
+
const { record } = await this.directory('recordStoreDriverReleaseAndAudit', { ...input, actor, at }, this.adminEntry(actor, 'uploadStoreDriverRelease', { tenantId: null }, null, null));
|
|
5497
5512
|
return record;
|
|
5498
5513
|
},
|
|
5499
5514
|
getStoreDriverRelease: async (actor, driver, sha256) => {
|
|
@@ -5526,13 +5541,11 @@ export class CloudflareScopeHost {
|
|
|
5526
5541
|
await this.cp.releaseStoreDriverHold(input);
|
|
5527
5542
|
},
|
|
5528
5543
|
beginStoreDriverRollout: async (actor, input) => {
|
|
5529
|
-
const row = await this.directory('
|
|
5530
|
-
await this.recordAdmin(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row);
|
|
5544
|
+
const row = await this.directory('beginStoreDriverRolloutAndAudit', { ...input, actor, at: new Date().toISOString() }, this.adminEntry(actor, input.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, null));
|
|
5531
5545
|
return row;
|
|
5532
5546
|
},
|
|
5533
5547
|
finishStoreDriverRollout: async (actor, input) => {
|
|
5534
|
-
const row = await this.directory('
|
|
5535
|
-
await this.recordAdmin(actor, row.action === 'rollback' ? 'rollBackStoreDriver' : 'rollOutStoreDriver', { tenantId: null }, null, row);
|
|
5548
|
+
const row = await this.directory('finishStoreDriverRolloutAndAudit', { ...input, at: new Date().toISOString() }, this.adminEntry(actor, 'rollOutStoreDriver', { tenantId: null }, null, null));
|
|
5536
5549
|
return row;
|
|
5537
5550
|
},
|
|
5538
5551
|
storeDriverRebuild: async (actor, driver) => {
|
|
@@ -5831,8 +5844,7 @@ export class CloudflareScopeHost {
|
|
|
5831
5844
|
// admin log too, so an entry written first would be overwritten by the very act
|
|
5832
5845
|
// it records, leaving the platform's most consequential write invisible.
|
|
5833
5846
|
const before = (await this.cp.listTenants({ limit: 1000 })).length;
|
|
5834
|
-
await this.
|
|
5835
|
-
await this.recordAdmin(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length });
|
|
5847
|
+
await this.directory('importDumpAndAudit', dump.tables, this.adminEntry(actor, 'restoreDirectory', { tenantId: null }, { tenants: before }, { capturedAt: dump.capturedAt, tables: dump.tables.length }));
|
|
5836
5848
|
},
|
|
5837
5849
|
activateScope: async (actor, tenantId, scopeId) => {
|
|
5838
5850
|
// Idempotent on `active`, unaudited because nothing changed. Provisioning is
|
|
@@ -6060,11 +6072,10 @@ export class CloudflareScopeHost {
|
|
|
6060
6072
|
// anyway leaves the admin log recording an access that could not happen.
|
|
6061
6073
|
await this.assertScope(input.tenantId, input.scopeId);
|
|
6062
6074
|
const session = newImpersonationSession(ulid(), actor, input, new Date().toISOString());
|
|
6063
|
-
await this.
|
|
6075
|
+
await this.directory('writeImpersonationAndAudit', impersonationRowValues(session), this.adminEntry(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session));
|
|
6064
6076
|
// BEFORE the session is usable, so the log entry precedes every row it
|
|
6065
6077
|
// will ever touch. The reason rides in `after`: it is what a review reads
|
|
6066
6078
|
// first, and a log saying a session opened but not why is half a record.
|
|
6067
|
-
await this.recordAdmin(actor, 'beginImpersonation', { tenantId: session.tenantId, scopeId: session.scopeId }, null, session);
|
|
6068
6079
|
return session;
|
|
6069
6080
|
},
|
|
6070
6081
|
endImpersonation: async (actor, id) => {
|
|
@@ -6076,9 +6087,9 @@ export class CloudflareScopeHost {
|
|
|
6076
6087
|
// already stopped it.
|
|
6077
6088
|
if (before.endedAt !== null)
|
|
6078
6089
|
return before;
|
|
6079
|
-
|
|
6090
|
+
const endedAt = new Date().toISOString();
|
|
6091
|
+
await this.directory('endImpersonationAndAudit', String(id), endedAt, this.adminEntry(actor, 'endImpersonation', { tenantId: before.tenantId, scopeId: before.scopeId }, before, { ...before, endedAt }));
|
|
6080
6092
|
const after = mapImpersonationRow((await this.cp.readImpersonation(String(id))));
|
|
6081
|
-
await this.recordAdmin(actor, 'endImpersonation', { tenantId: after.tenantId, scopeId: after.scopeId }, before, after);
|
|
6082
6093
|
return after;
|
|
6083
6094
|
},
|
|
6084
6095
|
listImpersonations: async (actor, filter) => {
|
|
@@ -6158,12 +6169,11 @@ export class CloudflareScopeHost {
|
|
|
6158
6169
|
},
|
|
6159
6170
|
registerIdentityPool: async (actor, input) => {
|
|
6160
6171
|
const parsed = identityPool.parse(input);
|
|
6161
|
-
const created = await this.directory('
|
|
6172
|
+
const created = await this.directory('registerIdentityPoolAndAudit', parsed.provider, parsed.topology, parsed.tenantId, new Date().toISOString(), this.adminEntry(actor, 'registerIdentityPool', { tenantId: parsed.tenantId }, null, parsed));
|
|
6162
6173
|
if (!created)
|
|
6163
6174
|
return; // identical registration is idempotent, unaudited
|
|
6164
6175
|
// Null tenant for a central pool: it belongs to no single tenant, which is
|
|
6165
6176
|
// what made the admin log's tenantId nullable.
|
|
6166
|
-
await this.recordAdmin(actor, 'registerIdentityPool', { tenantId: parsed.tenantId }, null, parsed);
|
|
6167
6177
|
},
|
|
6168
6178
|
getIdentityPool: async (actor, provider) => {
|
|
6169
6179
|
const r = await this.cp.readPool(provider);
|
|
@@ -6220,7 +6230,7 @@ export class CloudflareScopeHost {
|
|
|
6220
6230
|
// never seen a plaintext credential.
|
|
6221
6231
|
const sealed = await this.secretBox.seal(JSON.stringify(input.secret));
|
|
6222
6232
|
const now = new Date().toISOString();
|
|
6223
|
-
await this.
|
|
6233
|
+
await this.directory('insertConnectionAndAudit', {
|
|
6224
6234
|
id: input.id,
|
|
6225
6235
|
tenantId: input.tenantId,
|
|
6226
6236
|
vertical: input.vertical,
|
|
@@ -6235,17 +6245,16 @@ export class CloudflareScopeHost {
|
|
|
6235
6245
|
createdAt: now,
|
|
6236
6246
|
keyId: sealed.keyId,
|
|
6237
6247
|
ciphertext: sealed.ciphertext,
|
|
6238
|
-
}
|
|
6239
|
-
// METADATA ONLY — the admin log is append-only, so a credential written
|
|
6240
|
-
// here could never be removed.
|
|
6241
|
-
await this.recordAdmin(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
|
|
6248
|
+
}, this.adminEntry(actor, 'createConnection', { tenantId: input.tenantId, vertical: input.vertical }, null, {
|
|
6242
6249
|
id: input.id,
|
|
6243
6250
|
provider: input.provider,
|
|
6244
6251
|
label: input.label,
|
|
6245
6252
|
scopes: input.scopes,
|
|
6246
6253
|
externalAccountRef: input.externalAccountRef ?? null,
|
|
6247
6254
|
createdBy: input.createdBy ?? actor,
|
|
6248
|
-
});
|
|
6255
|
+
}));
|
|
6256
|
+
// METADATA ONLY — the admin log is append-only, so a credential written
|
|
6257
|
+
// here could never be removed.
|
|
6249
6258
|
},
|
|
6250
6259
|
connectionSealingKey: async (id) => this.ensureSealingKey(id),
|
|
6251
6260
|
connectionSealingKeys: async (tenantId, vertical) => {
|
|
@@ -6288,26 +6297,22 @@ export class CloudflareScopeHost {
|
|
|
6288
6297
|
throw new Error(`connection not found: ${id}`);
|
|
6289
6298
|
const sealed = await this.secretBox.seal(JSON.stringify(connectionSecret.parse(secret)));
|
|
6290
6299
|
const now = new Date().toISOString();
|
|
6291
|
-
await this.
|
|
6292
|
-
await this.recordAdmin(actor, 'updateConnectionSecret', { tenantId: row.tenant_id, vertical: row.vertical }, null, {
|
|
6300
|
+
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, {
|
|
6293
6301
|
id,
|
|
6294
6302
|
provider: row.provider,
|
|
6295
6303
|
rotatedAt: now,
|
|
6296
6304
|
expiresAt: expiresAt === undefined ? row.expires_at : expiresAt,
|
|
6297
|
-
// §3.5.1's attribution, rotate-side: the authorizing tenant principal,
|
|
6298
|
-
// never laundered into the actor column.
|
|
6299
6305
|
...(opts?.rotatedBy ? { rotatedBy: opts.rotatedBy } : {}),
|
|
6300
|
-
});
|
|
6306
|
+
}));
|
|
6301
6307
|
},
|
|
6302
6308
|
revokeConnection: async (actor, id) => {
|
|
6303
6309
|
const row = await this.cp.readConnection(id);
|
|
6304
6310
|
if (!row)
|
|
6305
6311
|
throw new Error(`connection not found: ${id}`);
|
|
6306
6312
|
const now = new Date().toISOString();
|
|
6307
|
-
const changed = await this.
|
|
6313
|
+
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 }));
|
|
6308
6314
|
if (!changed)
|
|
6309
6315
|
return; // idempotent, and a no-op is not audited
|
|
6310
|
-
await this.recordAdmin(actor, 'revokeConnection', { tenantId: row.tenant_id, vertical: row.vertical }, { status: row.status }, { id, provider: row.provider, status: 'revoked', revokedAt: now });
|
|
6311
6316
|
},
|
|
6312
6317
|
// -- a vertical's mailed connect links (connections.md §3.5.4) --------------
|
|
6313
6318
|
// Parsed here, run in the ControlPlaneDO as the kernel's statements (shared with
|
|
@@ -6405,11 +6410,10 @@ export class CloudflareScopeHost {
|
|
|
6405
6410
|
if (pool.topology === 'tenant-bound' && pool.tenant_id !== parsed.tenantId) {
|
|
6406
6411
|
throw substratError('conflict', `identity pool '${parsed.provider}' is bound to tenant ${pool.tenant_id} and cannot link into ${parsed.tenantId}`);
|
|
6407
6412
|
}
|
|
6408
|
-
const changed = await this.directory('
|
|
6413
|
+
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 }));
|
|
6409
6414
|
// Idempotent: an identity already bound is a no-op, not audited.
|
|
6410
6415
|
if (!changed)
|
|
6411
6416
|
return;
|
|
6412
|
-
await this.recordAdmin(actor, 'linkIdentity', { tenantId: parsed.tenantId, scopeId: parsed.scopeId }, null, { provider: parsed.provider, externalId: parsed.externalId, principal: parsed.principal });
|
|
6413
6417
|
// #406: an identity link is a tenant-level write, so it fans out into the tenant's
|
|
6414
6418
|
// projected scopes exactly as entitlements do (#304) — a no-op unless scope-local
|
|
6415
6419
|
// projection is on; the reconcile sweep repairs any drop.
|
|
@@ -6418,10 +6422,9 @@ export class CloudflareScopeHost {
|
|
|
6418
6422
|
unlinkIdentity: async (actor, tenantId, principal) => {
|
|
6419
6423
|
// DELETE by principal (audit is the log), so the caller who removed a member
|
|
6420
6424
|
// can sever their login without knowing the external subject. Idempotent.
|
|
6421
|
-
const changed = await this.
|
|
6425
|
+
const changed = await this.directory('unlinkIdentityAndAudit', tenantId, principal, this.adminEntry(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null));
|
|
6422
6426
|
if (!changed)
|
|
6423
6427
|
return;
|
|
6424
|
-
await this.recordAdmin(actor, 'unlinkIdentity', { tenantId, scopeId: null }, { principal }, null);
|
|
6425
6428
|
// The unlink must reach the projected scopes so the severed login stops resolving
|
|
6426
6429
|
// there — this is what makes revocation durable at request time (#406); a dropped
|
|
6427
6430
|
// fan-out is repaired by the reconcile sweep.
|
|
@@ -6513,24 +6516,11 @@ export class CloudflareScopeHost {
|
|
|
6513
6516
|
}));
|
|
6514
6517
|
},
|
|
6515
6518
|
markAccessLogDrained: async (actor, upToId, drainedAt) => {
|
|
6516
|
-
|
|
6517
|
-
if (drained > 0) {
|
|
6518
|
-
// The payload is the APPLIED state, so it belongs in `after` (contracts'
|
|
6519
|
-
// adminLogEntry: before = prior state, after = the applied payload).
|
|
6520
|
-
await this.recordAdmin(actor, 'drainAccessLog', { tenantId: null }, null, { drained, upToId, drainedAt });
|
|
6521
|
-
}
|
|
6522
|
-
return drained;
|
|
6519
|
+
return this.directory('markAccessLogDrainedAndAudit', upToId, drainedAt, this.adminEntry(actor, 'drainAccessLog', { tenantId: null }, null, null));
|
|
6523
6520
|
},
|
|
6524
6521
|
pruneAccessLog: async (actor, limit) => {
|
|
6525
6522
|
// Checked here as well as in the directory, so the refusal keeps its code across the hop.
|
|
6526
|
-
|
|
6527
|
-
if (pruned > 0) {
|
|
6528
|
-
// The payload is the APPLIED state, so it belongs in `after` (contracts'
|
|
6529
|
-
// adminLogEntry: before = prior state, after = the applied payload) — the
|
|
6530
|
-
// same shape as drainAccessLog's row above (#557).
|
|
6531
|
-
await this.recordAdmin(actor, 'pruneAccessLog', { tenantId: null }, null, { pruned });
|
|
6532
|
-
}
|
|
6533
|
-
return pruned;
|
|
6523
|
+
return this.directory('pruneAccessLogAndAudit', assertRowLimit('limit', limit), this.adminEntry(actor, 'pruneAccessLog', { tenantId: null }, null, null));
|
|
6534
6524
|
},
|
|
6535
6525
|
auditLog: async (actor, filter) => {
|
|
6536
6526
|
const rows = await this.cp.auditLog({
|
|
@@ -6656,13 +6646,11 @@ export class CloudflareScopeHost {
|
|
|
6656
6646
|
return rows.map((r) => issueEntry.parse(r));
|
|
6657
6647
|
},
|
|
6658
6648
|
setIssueStatus: async (actor, fingerprint, status) => {
|
|
6659
|
-
const change = await this.
|
|
6649
|
+
const change = await this.directory('setIssueStatusAndAudit', fingerprint, status, new Date().toISOString(), this.adminEntry(actor, 'setIssueStatus', { tenantId: null }, null, null));
|
|
6660
6650
|
if (!change)
|
|
6661
6651
|
return undefined;
|
|
6662
|
-
const before = issueEntry.parse(change.before);
|
|
6663
6652
|
const after = issueEntry.parse(change.after);
|
|
6664
|
-
// A lifecycle flip is
|
|
6665
|
-
await this.recordAdmin(actor, 'setIssueStatus', { tenantId: null, vertical: after.lastVertical }, { fingerprint, status: before.status }, { fingerprint, status: after.status });
|
|
6653
|
+
// A lifecycle flip is recorded in the same directory unit as the state write.
|
|
6666
6654
|
return after;
|
|
6667
6655
|
},
|
|
6668
6656
|
listFindings: async (actor, filter) => {
|
|
@@ -7230,12 +7218,12 @@ export class CloudflareScopeHost {
|
|
|
7230
7218
|
async switchHeld(scopeId, holdKey) {
|
|
7231
7219
|
const at = Date.now();
|
|
7232
7220
|
let error;
|
|
7233
|
-
if (!this.holdSnapshot || at - this.holdSnapshot.at >
|
|
7221
|
+
if (!this.holdSnapshot || at - this.holdSnapshot.at > this.holdSnapshotMs) {
|
|
7234
7222
|
const inflight = this.holdRefresh;
|
|
7235
7223
|
// Join a read that went out within the snapshot age of this consult: its result is then as
|
|
7236
7224
|
// fresh as one this consult would take itself, which is what the settle argument needs.
|
|
7237
7225
|
// Only the consult that sent the read reports its failure.
|
|
7238
|
-
if (inflight && at - inflight.at <=
|
|
7226
|
+
if (inflight && at - inflight.at <= this.holdSnapshotMs) {
|
|
7239
7227
|
await inflight.done;
|
|
7240
7228
|
}
|
|
7241
7229
|
else {
|
|
@@ -7477,7 +7465,7 @@ export class CloudflareScopeHost {
|
|
|
7477
7465
|
* claimed by the re-read.
|
|
7478
7466
|
*/
|
|
7479
7467
|
async settleClaim(holds, scopeId, claimId) {
|
|
7480
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
7468
|
+
await new Promise((resolve) => setTimeout(resolve, this.holdSettleMs));
|
|
7481
7469
|
for (let waits = 0;; waits++) {
|
|
7482
7470
|
const token = await holds.switchHoldToken();
|
|
7483
7471
|
const off = await this.offInStorage(scopeId);
|
|
@@ -7486,14 +7474,14 @@ export class CloudflareScopeHost {
|
|
|
7486
7474
|
this.holdSnapshot = null;
|
|
7487
7475
|
}
|
|
7488
7476
|
const youngest = await holds.switchHoldYoungestMs(scopeId, claimId);
|
|
7489
|
-
if (youngest === null || youngest >=
|
|
7477
|
+
if (youngest === null || youngest >= this.holdSettleMs)
|
|
7490
7478
|
return;
|
|
7491
7479
|
if (waits === SWITCH_HOLD_EXTRA_WAITS) {
|
|
7492
7480
|
throw new Error(`${REWIND_REFUSED}schedule switches kept being pulled off while this rewind waited for them to ` +
|
|
7493
7481
|
`settle (${waits} extra waits); nothing was rewound, and those switches stay off. Retry the rewind`);
|
|
7494
7482
|
}
|
|
7495
7483
|
// Never longer than one settle, even if the hold object's clock moved back under a restart.
|
|
7496
|
-
await new Promise((resolve) => setTimeout(resolve,
|
|
7484
|
+
await new Promise((resolve) => setTimeout(resolve, this.holdSettleMs - Math.max(0, youngest)));
|
|
7497
7485
|
}
|
|
7498
7486
|
}
|
|
7499
7487
|
// -- live reads (#938): the door ------------------------------------------
|
|
@@ -8673,6 +8661,18 @@ export class CloudflareScopeHost {
|
|
|
8673
8661
|
}
|
|
8674
8662
|
return connectionLandingsOfManifestJson(v.manifest_json);
|
|
8675
8663
|
}
|
|
8664
|
+
/**
|
|
8665
|
+
* What one pushed version declares about vertical-defined connectors (vertical-connectors.md
|
|
8666
|
+
* §6.4), from its stored manifest: the connector proxy reads it for the running version on every
|
|
8667
|
+
* call. Unaudited for the reason {@link versionImports} is.
|
|
8668
|
+
*/
|
|
8669
|
+
async versionConnectors(verticalSlug, versionId) {
|
|
8670
|
+
const v = await this.cp.readVersion(versionId);
|
|
8671
|
+
if (!v || v.vertical_slug !== verticalSlug) {
|
|
8672
|
+
throw substratError('not_found', `unknown version ${versionId} for vertical '${verticalSlug}'`);
|
|
8673
|
+
}
|
|
8674
|
+
return connectorsOfManifestJson(v.manifest_json);
|
|
8675
|
+
}
|
|
8676
8676
|
}
|
|
8677
8677
|
/** A ledger row -> the wire entry, the attribution re-nested (#1054). */
|
|
8678
8678
|
function modelUsageEntryOf(r) {
|