@substrat-run/adapter-cloudflare 0.180.0 → 0.184.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 +21 -0
- package/dist/control-plane-do.d.ts.map +1 -1
- package/dist/control-plane-do.js +23 -0
- package/dist/control-plane-do.js.map +1 -1
- package/dist/host.d.ts +23 -13
- package/dist/host.d.ts.map +1 -1
- package/dist/host.js +72 -57
- package/dist/host.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/scope-do.d.ts.map +1 -1
- package/dist/scope-do.js +62 -18
- package/dist/scope-do.js.map +1 -1
- package/package.json +12 -12
package/dist/host.js
CHANGED
|
@@ -50,15 +50,18 @@ export const TENANT_UNRECORDED_PER_PASS = 50;
|
|
|
50
50
|
*/
|
|
51
51
|
const unknownScopeForTenant = (tenantId, scopeId) => substratError('not_found', `unknown scope for tenant: (${tenantId}, ${scopeId})`);
|
|
52
52
|
/**
|
|
53
|
-
* #
|
|
54
|
-
* and no role rows
|
|
53
|
+
* #2035: the line a CP-less host writes when a door refuses a scope whose tenant is not recorded —
|
|
54
|
+
* no receipt and no role rows. The caller gets the foreign pair's refusal unchanged, so this line is
|
|
55
|
+
* the only place an operator tells a scope that has not converged from a wrong-tenant request.
|
|
56
|
+
* Server-side only, and ids and the door's name only.
|
|
55
57
|
*/
|
|
56
|
-
|
|
57
|
-
substrat: 'tenant-unrecorded',
|
|
58
|
+
const tenantUnrecordedRefusedLine = (tenantId, scopeId, door) => ({
|
|
59
|
+
substrat: 'tenant-unrecorded-refused',
|
|
58
60
|
level: 'warn',
|
|
59
61
|
tenantId,
|
|
60
62
|
scopeId,
|
|
61
|
-
|
|
63
|
+
door,
|
|
64
|
+
message: 'refused a scope whose tenant is not recorded; a reconcile, provision or load naming its tenant records it',
|
|
62
65
|
});
|
|
63
66
|
/**
|
|
64
67
|
* #2430: the line a host writes when a provision or reconcile skips delivered connection grants that
|
|
@@ -828,7 +831,7 @@ export class CloudflareScopeHost {
|
|
|
828
831
|
async drainDue(tenantId, scopeId) {
|
|
829
832
|
// Same lifecycle gate `getScope` applies (K-3): a suspended or archived scope
|
|
830
833
|
// does not get its effects driven either.
|
|
831
|
-
await this.assertLive(tenantId, scopeId);
|
|
834
|
+
await this.assertLive(tenantId, scopeId, 'drainDue');
|
|
832
835
|
await this.migrateAndRecord(scopeId);
|
|
833
836
|
// #1525: null, and honestly so — a sweep is not a call. An attempt this pass makes
|
|
834
837
|
// records no invocation, which is what distinguishes it from the first attempt the
|
|
@@ -889,14 +892,14 @@ export class CloudflareScopeHost {
|
|
|
889
892
|
this.attachmentExtractors, this.attachmentTextBounds);
|
|
890
893
|
}
|
|
891
894
|
async startJobRun(tenantId, scopeId, input) {
|
|
892
|
-
await this.assertLive(tenantId, scopeId);
|
|
895
|
+
await this.assertLive(tenantId, scopeId, 'startJobRun');
|
|
893
896
|
await this.migrateAndRecord(scopeId);
|
|
894
897
|
return jobRunOf(await startJobRun(this.jobStore(scopeId), input, ulid, () => new Date().toISOString()));
|
|
895
898
|
}
|
|
896
899
|
async runDueJobs(tenantId, scopeId, options) {
|
|
897
900
|
// Same lifecycle gate as `drainDue`: a suspended scope's runs wait rather than
|
|
898
901
|
// advance, and an archived one's never move again.
|
|
899
|
-
await this.assertLive(tenantId, scopeId);
|
|
902
|
+
await this.assertLive(tenantId, scopeId, 'runDueJobs');
|
|
900
903
|
await this.migrateAndRecord(scopeId);
|
|
901
904
|
// #1575: the kernel's extraction jobs are this host's own, bound to this scope — no
|
|
902
905
|
// deployment registers them, and none can shadow them. The backfill starts here, on
|
|
@@ -920,7 +923,7 @@ export class CloudflareScopeHost {
|
|
|
920
923
|
});
|
|
921
924
|
}
|
|
922
925
|
async jobRuns(tenantId, scopeId, filter) {
|
|
923
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
926
|
+
await this.validateScopeAccess(tenantId, scopeId, 'jobRuns');
|
|
924
927
|
await this.migrateAndRecord(scopeId);
|
|
925
928
|
return (await this.jobStore(scopeId).list(filter ?? {})).map(jobRunOf);
|
|
926
929
|
}
|
|
@@ -930,13 +933,13 @@ export class CloudflareScopeHost {
|
|
|
930
933
|
// same context build as the in-process path, so the handler cannot tell which host
|
|
931
934
|
// ran it. On a CP-less host `connectorContext` throws from the null control plane:
|
|
932
935
|
// fail closed, exactly the hole routing exists to avoid.
|
|
933
|
-
await this.assertLive(tenantId, scopeId);
|
|
936
|
+
await this.assertLive(tenantId, scopeId, 'dispatchConnector');
|
|
934
937
|
// Built on a view bound to the event, so its admin rows carry it and nothing else the
|
|
935
938
|
// host serves does (#2055).
|
|
936
939
|
await handler(await this.causedByView(event.id).connectorContext(tenantId, scopeId, options?.timeoutMs ?? 30_000, event.id), event);
|
|
937
940
|
}
|
|
938
941
|
async executorDeadLetters(tenantId, scopeId) {
|
|
939
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
942
|
+
await this.validateScopeAccess(tenantId, scopeId, 'executorDeadLetters');
|
|
940
943
|
await this.migrateAndRecord(scopeId);
|
|
941
944
|
return this.scopeStub(scopeId).executorDeadLetters();
|
|
942
945
|
}
|
|
@@ -949,18 +952,18 @@ export class CloudflareScopeHost {
|
|
|
949
952
|
return this.scopeStub(scopeId).enqueueSweepRuns(JSON.stringify(sweepRunsPayload.parse(payload)), JSON.stringify({ system: 'scope-sweeper' }));
|
|
950
953
|
}
|
|
951
954
|
async listPlatformRequests(tenantId, scopeId) {
|
|
952
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
955
|
+
await this.validateScopeAccess(tenantId, scopeId, 'listPlatformRequests');
|
|
953
956
|
await this.migrateAndRecord(scopeId);
|
|
954
957
|
// Tolerant (#1588, #1637): an undecodable row comes back naming why, never throws for the list.
|
|
955
958
|
return (await this.scopeStub(scopeId).pendingPlatformRequests()).map(platformRequestOf);
|
|
956
959
|
}
|
|
957
960
|
async listPlatformRequestHistory(tenantId, scopeId, filter) {
|
|
958
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
961
|
+
await this.validateScopeAccess(tenantId, scopeId, 'listPlatformRequestHistory');
|
|
959
962
|
await this.migrateAndRecord(scopeId);
|
|
960
963
|
return (await this.scopeStub(scopeId).platformRequestHistory(filter)).map(platformRequestOf);
|
|
961
964
|
}
|
|
962
965
|
async settlePlatformRequest(tenantId, scopeId, id, outcome) {
|
|
963
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
966
|
+
await this.validateScopeAccess(tenantId, scopeId, 'settlePlatformRequest');
|
|
964
967
|
await this.migrateAndRecord(scopeId);
|
|
965
968
|
// #2102: an outcome event travels by the verb that writes it; every other settle keeps the
|
|
966
969
|
// positional one, so it still reaches a DO class that predates the event.
|
|
@@ -1095,7 +1098,7 @@ export class CloudflareScopeHost {
|
|
|
1095
1098
|
// Checked on this side too, as `HostAdmin.mintCapability` does: a typed refusal thrown
|
|
1096
1099
|
// across the RPC arrives as a bare message.
|
|
1097
1100
|
checkBecomeInput(input, new Date().toISOString());
|
|
1098
|
-
await this.assertLive(tenantId, scopeId);
|
|
1101
|
+
await this.assertLive(tenantId, scopeId, 'mintCapabilityLocal');
|
|
1099
1102
|
await this.migrateAndRecord(scopeId);
|
|
1100
1103
|
return mintedCapability.parse(await this.scopeStub(scopeId).mintBecomeCapability(input, actor));
|
|
1101
1104
|
}
|
|
@@ -1674,7 +1677,7 @@ export class CloudflareScopeHost {
|
|
|
1674
1677
|
after = { scriptRef: last.scriptRef, moveId: last.moveId };
|
|
1675
1678
|
}
|
|
1676
1679
|
// The tenant's status too, as every door into a scope asks.
|
|
1677
|
-
await this.assertLive(tenantId, scopeId);
|
|
1680
|
+
await this.assertLive(tenantId, scopeId, 'sweepOrphanAttachments');
|
|
1678
1681
|
vouched = true;
|
|
1679
1682
|
}
|
|
1680
1683
|
else if (request.copiesSettled !== true) {
|
|
@@ -1993,18 +1996,18 @@ export class CloudflareScopeHost {
|
|
|
1993
1996
|
// gates and metadata facts live in the ScopeDO (per-scope serialization, spine event
|
|
1994
1997
|
// in the same transaction); bytes go straight to the per-tenant R2 bucket through the
|
|
1995
1998
|
// binding the vertical's worker resolved — never through the DO.
|
|
1996
|
-
await this.assertLive(tenantId, scopeId);
|
|
1999
|
+
await this.assertLive(tenantId, scopeId, 'attachments');
|
|
1997
2000
|
await this.migrateAndRecord(scopeId);
|
|
1998
2001
|
const store = await this.resolveAttachmentStore(tenantId);
|
|
1999
2002
|
return this.buildAttachmentSurface({ principal }, tenantId, scopeId, store);
|
|
2000
2003
|
}
|
|
2001
2004
|
async inspectUploadLink(tenantId, scopeId, secret, target, idempotencyKey) {
|
|
2002
|
-
await this.assertLive(tenantId, scopeId);
|
|
2005
|
+
await this.assertLive(tenantId, scopeId, 'inspectUploadLink');
|
|
2003
2006
|
await this.migrateAndRecord(scopeId);
|
|
2004
2007
|
return unwrapReply(await this.scopeStub(scopeId).uploadLinkInspectReply(secret, tenantId, scopeId, target, idempotencyKey)).view;
|
|
2005
2008
|
}
|
|
2006
2009
|
async redeemUploadLink(tenantId, scopeId, secret, input) {
|
|
2007
|
-
await this.assertLive(tenantId, scopeId);
|
|
2010
|
+
await this.assertLive(tenantId, scopeId, 'redeemUploadLink');
|
|
2008
2011
|
await this.migrateAndRecord(scopeId);
|
|
2009
2012
|
const stub = this.scopeStub(scopeId);
|
|
2010
2013
|
// The target first (the kernel judges it before the state), so a wrong path never reads as
|
|
@@ -2057,7 +2060,7 @@ export class CloudflareScopeHost {
|
|
|
2057
2060
|
throw new Error(`connection ${connectionId} is for vertical '${conn.vertical}' and scope ${scopeId} ` +
|
|
2058
2061
|
`runs '${scope.vertical ?? 'none'}'`);
|
|
2059
2062
|
}
|
|
2060
|
-
await this.validateScopeAccess(conn.tenant_id, scopeId);
|
|
2063
|
+
await this.validateScopeAccess(conn.tenant_id, scopeId, 'getConnectorAttachments');
|
|
2061
2064
|
// #574: same delegation as getConnectorScope. `upload` is the verb the reconcile
|
|
2062
2065
|
// path needs (landing the sealed PDF) and `open` the one the outbound path needs
|
|
2063
2066
|
// (sending the vertical's own document, #711). `list` and `remove` still fail
|
|
@@ -2458,7 +2461,7 @@ export class CloudflareScopeHost {
|
|
|
2458
2461
|
async getScope(principal, tenantId, scopeId, options) {
|
|
2459
2462
|
// Lifecycle gates (control-plane.md §4.1/§4.2), the K-3 fail-closed path,
|
|
2460
2463
|
// evaluated durably in the ControlPlaneDO. A throw propagates here.
|
|
2461
|
-
await this.assertLive(tenantId, scopeId);
|
|
2464
|
+
await this.assertLive(tenantId, scopeId, 'getScope');
|
|
2462
2465
|
await this.migrateAndRecord(scopeId);
|
|
2463
2466
|
return this.buildStub(tenantId, scopeId, principal, undefined, undefined, options);
|
|
2464
2467
|
}
|
|
@@ -2495,7 +2498,7 @@ export class CloudflareScopeHost {
|
|
|
2495
2498
|
throw substratError('not_found', `no scope store is bound for '${binding}'`);
|
|
2496
2499
|
}
|
|
2497
2500
|
const vertical = await this.scopeStoreVerticalOf(tenantId, scopeId);
|
|
2498
|
-
await this.assertLive(tenantId, scopeId);
|
|
2501
|
+
await this.assertLive(tenantId, scopeId, 'getScopeStore');
|
|
2499
2502
|
if (runner) {
|
|
2500
2503
|
await this.scopeStoreLifecycle.start();
|
|
2501
2504
|
return {
|
|
@@ -2587,7 +2590,7 @@ export class CloudflareScopeHost {
|
|
|
2587
2590
|
await this.scopeRecordForRead(tenantId, scopeId);
|
|
2588
2591
|
const vertical = await this.scopeStoreVerticalOf(tenantId, scopeId);
|
|
2589
2592
|
if (this.cpLess)
|
|
2590
|
-
await this.assertLive(tenantId, scopeId);
|
|
2593
|
+
await this.assertLive(tenantId, scopeId, 'readScopeStoreSizes');
|
|
2591
2594
|
const sizes = [];
|
|
2592
2595
|
for (const binding of bindings) {
|
|
2593
2596
|
const { bytes } = await this.invokeReservedStore({ tenantId, vertical, scopeId, binding }, 'substrat.size');
|
|
@@ -2792,7 +2795,7 @@ export class CloudflareScopeHost {
|
|
|
2792
2795
|
throw new Error(`connection ${connectionId} is for vertical '${conn.vertical}' and scope ${scopeId} ` +
|
|
2793
2796
|
`runs '${scope.vertical ?? 'none'}'`);
|
|
2794
2797
|
}
|
|
2795
|
-
await this.validateScopeAccess(conn.tenant_id, scopeId);
|
|
2798
|
+
await this.validateScopeAccess(conn.tenant_id, scopeId, 'getConnectorScope');
|
|
2796
2799
|
// #574: a scope served by ANOTHER deployment (the shared control plane running the
|
|
2797
2800
|
// connector pass for a dispatch vertical) — the write-back rides the delegation
|
|
2798
2801
|
// seam; migration is the serving deployment's business, exactly like provision.
|
|
@@ -2839,7 +2842,7 @@ export class CloudflareScopeHost {
|
|
|
2839
2842
|
const record = await this.resolveImpersonation(session, tenantId, scopeId);
|
|
2840
2843
|
// The same lifecycle gate the principal door applies: a suspended tenant
|
|
2841
2844
|
// refuses a support session exactly as it refuses a user.
|
|
2842
|
-
await this.assertLive(tenantId, scopeId);
|
|
2845
|
+
await this.assertLive(tenantId, scopeId, 'getImpersonatedScope');
|
|
2843
2846
|
await this.migrateAndRecord(scopeId);
|
|
2844
2847
|
return this.buildStub(tenantId, scopeId, record.principal, undefined, undefined, options, record.id);
|
|
2845
2848
|
}
|
|
@@ -2873,7 +2876,7 @@ export class CloudflareScopeHost {
|
|
|
2873
2876
|
if (!this.moduleIds.has(moduleId)) {
|
|
2874
2877
|
throw substratError('not_found', `module not registered on this host: ${moduleId}`);
|
|
2875
2878
|
}
|
|
2876
|
-
await this.assertLive(tenantId, scopeId);
|
|
2879
|
+
await this.assertLive(tenantId, scopeId, 'openSystemDoor');
|
|
2877
2880
|
await this.migrateAndRecord(scopeId);
|
|
2878
2881
|
return this.openDoor('system', moduleId, scopeId, gated, pass);
|
|
2879
2882
|
}
|
|
@@ -3025,7 +3028,7 @@ export class CloudflareScopeHost {
|
|
|
3025
3028
|
* capability row lives. The answer is re-parsed on this side of the RPC.
|
|
3026
3029
|
*/
|
|
3027
3030
|
async exchangeCapability(tenantId, scopeId, secret, options) {
|
|
3028
|
-
await this.assertLive(tenantId, scopeId);
|
|
3031
|
+
await this.assertLive(tenantId, scopeId, 'exchangeCapability');
|
|
3029
3032
|
await this.migrateAndRecord(scopeId);
|
|
3030
3033
|
const outcome = await this.scopeStub(scopeId).exchangeCapability(secret, tenantId, scopeId, options?.mode, options?.mode === 'bind' ? principalId.parse(options.presenter) : undefined);
|
|
3031
3034
|
return capabilityExchange.nullable().parse(outcome);
|
|
@@ -3039,7 +3042,7 @@ export class CloudflareScopeHost {
|
|
|
3039
3042
|
if (!plausibleSessionToken(sessionToken)) {
|
|
3040
3043
|
throw substratError('unauthenticated', 'not a capability session token');
|
|
3041
3044
|
}
|
|
3042
|
-
await this.assertLive(tenantId, scopeId);
|
|
3045
|
+
await this.assertLive(tenantId, scopeId, 'getCapabilityScope');
|
|
3043
3046
|
await this.migrateAndRecord(scopeId);
|
|
3044
3047
|
const hash = await capabilityTokenHash(sessionToken);
|
|
3045
3048
|
return this.buildStub(tenantId, scopeId, undefined, undefined, undefined, options, undefined, hash);
|
|
@@ -3214,7 +3217,7 @@ export class CloudflareScopeHost {
|
|
|
3214
3217
|
}
|
|
3215
3218
|
this.assertServedHere(record, scopeId, verb);
|
|
3216
3219
|
}
|
|
3217
|
-
await this.assertLive(tenantId, scopeId);
|
|
3220
|
+
await this.assertLive(tenantId, scopeId, 'peerScopeGate');
|
|
3218
3221
|
await this.migrateAndRecord(scopeId);
|
|
3219
3222
|
return record;
|
|
3220
3223
|
}
|
|
@@ -3244,7 +3247,7 @@ export class CloudflareScopeHost {
|
|
|
3244
3247
|
if (!plausibleSessionToken(sessionToken)) {
|
|
3245
3248
|
throw substratError('unauthenticated', 'not a capability session token');
|
|
3246
3249
|
}
|
|
3247
|
-
await this.assertLive(tenantId, scopeId);
|
|
3250
|
+
await this.assertLive(tenantId, scopeId, 'getCapabilityAttachments');
|
|
3248
3251
|
await this.migrateAndRecord(scopeId);
|
|
3249
3252
|
const store = await this.resolveAttachmentStore(tenantId);
|
|
3250
3253
|
const capabilitySession = await capabilityTokenHash(sessionToken);
|
|
@@ -3351,7 +3354,7 @@ export class CloudflareScopeHost {
|
|
|
3351
3354
|
if (await this.cp.scopeAccessRefusal(tenantId, scopeId))
|
|
3352
3355
|
return report;
|
|
3353
3356
|
}
|
|
3354
|
-
else if (lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId)) !== null) {
|
|
3357
|
+
else if (lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId, 'checkFreshness')) !== null) {
|
|
3355
3358
|
// #2016: the pair is held to the scope's own record first, so a sweep roster entry whose
|
|
3356
3359
|
// tenant disagrees with the scope is refused before the probe reads or writes anything.
|
|
3357
3360
|
// #1713: a held scope emits nothing, so judging it stale would only raise a false alarm.
|
|
@@ -3404,7 +3407,7 @@ export class CloudflareScopeHost {
|
|
|
3404
3407
|
if (await this.cp.scopeAccessRefusal(tenantId, scopeId))
|
|
3405
3408
|
return report;
|
|
3406
3409
|
}
|
|
3407
|
-
else if (lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId)) !== null) {
|
|
3410
|
+
else if (lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId, 'runDueSchedules')) !== null) {
|
|
3408
3411
|
// #1713: the lifecycle the platform delivered holds the scope. Every schedule is
|
|
3409
3412
|
// `skipped` and no cadence row moves, as under the kill switch, so a schedule that
|
|
3410
3413
|
// came due meanwhile fires once on the first pass after the scope is live again.
|
|
@@ -3475,7 +3478,7 @@ export class CloudflareScopeHost {
|
|
|
3475
3478
|
// read again before every later fire: the scope does not hold a directory lifecycle,
|
|
3476
3479
|
// so a suspension after an earlier fire would otherwise not stop this one (#1713).
|
|
3477
3480
|
if (door)
|
|
3478
|
-
await this.assertLive(tenantId, scopeId);
|
|
3481
|
+
await this.assertLive(tenantId, scopeId, 'runDueSchedules');
|
|
3479
3482
|
door ??= await this.openSystemDoor(moduleId, tenantId, scopeId, gate);
|
|
3480
3483
|
const scope = this.buildStub(tenantId, scopeId, undefined, undefined, door, counted);
|
|
3481
3484
|
if (schedule.purge) {
|
|
@@ -4040,7 +4043,7 @@ export class CloudflareScopeHost {
|
|
|
4040
4043
|
// Without one, a hosted scope is refused `unavailable` rather than switched in the
|
|
4041
4044
|
// placeholder namespace, where it would tombstone nothing a peer call reads.
|
|
4042
4045
|
if (delegated)
|
|
4043
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
4046
|
+
await this.validateScopeAccess(tenantId, scopeId, 'switchSubject');
|
|
4044
4047
|
else
|
|
4045
4048
|
await this.peerScopeGate(tenantId, scopeId, action);
|
|
4046
4049
|
}
|
|
@@ -4830,7 +4833,7 @@ export class CloudflareScopeHost {
|
|
|
4830
4833
|
// a bare message, and the caller would lose `validation_failed`. One function, so the
|
|
4831
4834
|
// two sides cannot disagree about what a valid mint is.
|
|
4832
4835
|
checkBecomeInput(input, new Date().toISOString());
|
|
4833
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
4836
|
+
await this.validateScopeAccess(tenantId, scopeId, 'mintCapability');
|
|
4834
4837
|
const rec = await this.capabilityScopeRecord(tenantId, scopeId);
|
|
4835
4838
|
// A hosted refusal precedes intent; the local migrate happens inside the audited write.
|
|
4836
4839
|
this.assertCapabilityScopeReach(rec, scopeId, 'mintCapability');
|
|
@@ -7062,7 +7065,9 @@ export class CloudflareScopeHost {
|
|
|
7062
7065
|
* without the lifecycle half pays the one DO call the pair check needs. Null with a directory,
|
|
7063
7066
|
* whose refusal already covered the lifecycle.
|
|
7064
7067
|
*/
|
|
7065
|
-
async validateScopeAccess(tenantId, scopeId
|
|
7068
|
+
async validateScopeAccess(tenantId, scopeId,
|
|
7069
|
+
/** The host method the call came through, named in `tenantUnrecordedRefusedLine` only. */
|
|
7070
|
+
door) {
|
|
7066
7071
|
const refusal = await this.cp.scopeAccessRefusal(tenantId, scopeId);
|
|
7067
7072
|
if (refusal) {
|
|
7068
7073
|
throw refusal.code
|
|
@@ -7072,14 +7077,15 @@ export class CloudflareScopeHost {
|
|
|
7072
7077
|
if (!this.cpLess)
|
|
7073
7078
|
return null;
|
|
7074
7079
|
const { verdict, lifecycle } = await this.scopeStub(scopeId).admission(tenantId);
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
//
|
|
7078
|
-
//
|
|
7079
|
-
//
|
|
7080
|
-
// Logged on every admission, so an operator sees a scope that has not converged.
|
|
7080
|
+
// #2035: a scope with no tenant record and no role rows has nothing to hold the pair against, so
|
|
7081
|
+
// it is refused like a foreign one. The platform's paths record the tenant without passing this
|
|
7082
|
+
// door — a provision or reconcile (every push runs one, #1172), a restore or snapshot, a
|
|
7083
|
+
// lifecycle delivery to a scope holding data — so a scope converges rather than staying shut.
|
|
7084
|
+
// The caller meets the foreign pair's refusal, word for word; only the log tells them apart.
|
|
7081
7085
|
if (verdict === 'unknown')
|
|
7082
|
-
console.warn(JSON.stringify(
|
|
7086
|
+
console.warn(JSON.stringify(tenantUnrecordedRefusedLine(tenantId, scopeId, door)));
|
|
7087
|
+
if (verdict === 'foreign' || verdict === 'unknown')
|
|
7088
|
+
throw unknownScopeForTenant(tenantId, scopeId);
|
|
7083
7089
|
return lifecycle;
|
|
7084
7090
|
}
|
|
7085
7091
|
/**
|
|
@@ -7097,8 +7103,8 @@ export class CloudflareScopeHost {
|
|
|
7097
7103
|
* The admin and diagnostic reads (dead letters, job runs, platform requests, delivered
|
|
7098
7104
|
* grants) keep `validateScopeAccess` alone: an operator still reads a suspended scope.
|
|
7099
7105
|
*/
|
|
7100
|
-
async assertLive(tenantId, scopeId) {
|
|
7101
|
-
const refusal = lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId), { tenantId, scopeId });
|
|
7106
|
+
async assertLive(tenantId, scopeId, door) {
|
|
7107
|
+
const refusal = lifecycleRefusal(await this.validateScopeAccess(tenantId, scopeId, door), { tenantId, scopeId });
|
|
7102
7108
|
if (refusal)
|
|
7103
7109
|
throw substratError('conflict', refusal, { reason: SCOPE_GATE_REASONS.notActive });
|
|
7104
7110
|
}
|
|
@@ -7364,7 +7370,7 @@ export class CloudflareScopeHost {
|
|
|
7364
7370
|
* directory's view stays on `HostAdmin.listConnectionGrants`.
|
|
7365
7371
|
*/
|
|
7366
7372
|
async connectionGrantsInScope(tenantId, scopeId) {
|
|
7367
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
7373
|
+
await this.validateScopeAccess(tenantId, scopeId, 'connectionGrantsInScope');
|
|
7368
7374
|
if (this.connectorDelegation) {
|
|
7369
7375
|
if (!this.connectorDelegation.grants)
|
|
7370
7376
|
throw new Error('connector grant inspection is not configured for the serving deployment');
|
|
@@ -7377,10 +7383,11 @@ export class CloudflareScopeHost {
|
|
|
7377
7383
|
// CP-less shape); a scope with a live control plane has its tenant-level grants in
|
|
7378
7384
|
// the directory instead, where the checker reads them from (`cp.tenantTuples`). Read
|
|
7379
7385
|
// only the DO and a tenant-wide grant would be reported absent on exactly the
|
|
7380
|
-
// deployments that have a control plane — the fleet — while being enforced.
|
|
7386
|
+
// deployments that have a control plane — the fleet — while being enforced. A CP-less
|
|
7387
|
+
// host has no directory to ask, and its DO already holds the projection (#2892).
|
|
7381
7388
|
const rows = [
|
|
7382
7389
|
...(await this.scopeStub(scopeId).listConnectionGrants(now)),
|
|
7383
|
-
...(await this.cp.dumpTenantTuples(tenantId))
|
|
7390
|
+
...(this.cpLess ? [] : await this.cp.dumpTenantTuples(tenantId))
|
|
7384
7391
|
.filter((r) => r.subject.startsWith('connection:') &&
|
|
7385
7392
|
r.relation.startsWith('granted:') &&
|
|
7386
7393
|
r.revoked_at === null &&
|
|
@@ -7858,7 +7865,7 @@ export class CloudflareScopeHost {
|
|
|
7858
7865
|
* count to the directory — so a scope whose first contact after a deploy is a
|
|
7859
7866
|
* subscription does not go dark in the migration fleet view.
|
|
7860
7867
|
*/
|
|
7861
|
-
await this.assertLive(tenantId, scopeId);
|
|
7868
|
+
await this.assertLive(tenantId, scopeId, 'subscribe');
|
|
7862
7869
|
await this.migrateAndRecord(scopeId);
|
|
7863
7870
|
// Asserted, not carried through from the client: the principal is the
|
|
7864
7871
|
// vertical's own resolution of its session, and the tenant and scope are the
|
|
@@ -8403,17 +8410,25 @@ export class CloudflareScopeHost {
|
|
|
8403
8410
|
* and a switched-off subject gets nothing. It can therefore only add what the running code
|
|
8404
8411
|
* declares, and never takes anything away but a recorded OFF.
|
|
8405
8412
|
*
|
|
8406
|
-
* Only a scope
|
|
8407
|
-
*
|
|
8413
|
+
* Only a scope that serves `tenantId` is seated (K-3): this never provisions a scope, and never
|
|
8414
|
+
* writes into another tenant's. Like a full reconcile, it records the platform's tenant on a scope
|
|
8415
|
+
* that holds no receipt yet (`seatsTenant`, #2035), so a push converges an ownerless vertical's
|
|
8416
|
+
* scopes too — every door refuses a scope whose tenant nothing recorded. The receipt lands in the
|
|
8417
|
+
* seat's own unit, after the recorded-off positions: until then every door still refuses the
|
|
8418
|
+
* scope, and a seat that fails records nothing.
|
|
8408
8419
|
*/
|
|
8409
8420
|
async seatScopeLocal(input) {
|
|
8410
8421
|
const stub = this.scopeStub(input.scopeId);
|
|
8411
|
-
if (!(await stub.
|
|
8422
|
+
if (!(await stub.seatsTenant(input.tenantId)))
|
|
8412
8423
|
throw unknownScopeForTenant(input.tenantId, input.scopeId);
|
|
8413
8424
|
const carry = recordedOffFromWire(input);
|
|
8414
8425
|
const connections = await this.seatableConnectionGrants(input.tenantId, input.scopeId, input.connectionGrants); // #2430
|
|
8415
8426
|
await this.migrateAndRecord(input.scopeId);
|
|
8416
|
-
const
|
|
8427
|
+
const seated = await stub.seatTuplesFor(input.tenantId, this.declaredSeatsLocal(input.scopeId, connections.seat), carry ? { scopeId: input.scopeId, at: new Date().toISOString(), ...carry } : undefined);
|
|
8428
|
+
// Reaped or recorded for another tenant since the check: K-3's refusal, naming no other tenant.
|
|
8429
|
+
if ('refused' in seated)
|
|
8430
|
+
throw unknownScopeForTenant(input.tenantId, input.scopeId);
|
|
8431
|
+
const { switchedOff } = seated;
|
|
8417
8432
|
return {
|
|
8418
8433
|
...(carry ? { switchedOff } : {}),
|
|
8419
8434
|
...(connections.skipped.length ? { skippedConnectionGrants: connections.skipped } : {}),
|
|
@@ -8601,7 +8616,7 @@ export class CloudflareScopeHost {
|
|
|
8601
8616
|
// scope (#1713): a held scope's invoke and bytes doors refuse, as every other door does.
|
|
8602
8617
|
/** Invoke ONE operation in this deployment as a CONNECTION (#574). */
|
|
8603
8618
|
async connectorInvokeLocal(connectionId, tenantId, scopeId, operation, input) {
|
|
8604
|
-
await this.assertLive(tenantId, scopeId); // #1713: a held scope runs no connector work either
|
|
8619
|
+
await this.assertLive(tenantId, scopeId, 'connectorInvokeLocal'); // #1713: a held scope runs no connector work either
|
|
8605
8620
|
await this.migrateAndRecord(scopeId);
|
|
8606
8621
|
return this.buildStub(tenantId, scopeId, undefined, connectionId).invoke(operation, input);
|
|
8607
8622
|
}
|
|
@@ -8609,7 +8624,7 @@ export class CloudflareScopeHost {
|
|
|
8609
8624
|
async connectorAttachmentUploadLocal(connectionId, tenantId, scopeId, upload) {
|
|
8610
8625
|
// The lifecycle gate every attachments door takes (#1713), ahead of the bucket: a held
|
|
8611
8626
|
// scope's provider bytes wait for the delivery's retry rather than land (#1995).
|
|
8612
|
-
await this.assertLive(tenantId, scopeId);
|
|
8627
|
+
await this.assertLive(tenantId, scopeId, 'connectorAttachmentUploadLocal');
|
|
8613
8628
|
await this.migrateAndRecord(scopeId);
|
|
8614
8629
|
const store = await this.resolveAttachmentStore(tenantId);
|
|
8615
8630
|
return this.buildAttachmentSurface({ connectionId }, tenantId, scopeId, store).upload(upload);
|
|
@@ -8634,7 +8649,7 @@ export class CloudflareScopeHost {
|
|
|
8634
8649
|
/** The attachment surface the platform's reads as a connection go through, behind their gates. */
|
|
8635
8650
|
async connectorReadSurface(connectionId, tenantId, scopeId, eventId) {
|
|
8636
8651
|
// Same gate as the upload leg: a held scope's bytes are not read out of its bucket.
|
|
8637
|
-
await this.assertLive(tenantId, scopeId);
|
|
8652
|
+
await this.assertLive(tenantId, scopeId, 'connectorReadSurface');
|
|
8638
8653
|
await this.migrateAndRecord(scopeId);
|
|
8639
8654
|
const store = await this.resolveAttachmentStore(tenantId);
|
|
8640
8655
|
return this.buildAttachmentSurface({ connectionId }, tenantId, scopeId, store,
|
|
@@ -8808,7 +8823,7 @@ export class CloudflareScopeHost {
|
|
|
8808
8823
|
const delegation = rec.vertical !== null ? this.importCursorDelegation : undefined;
|
|
8809
8824
|
if (!delegation)
|
|
8810
8825
|
this.assertServedHere(rec, scopeId, 'moveImportCursor');
|
|
8811
|
-
await this.validateScopeAccess(tenantId, scopeId);
|
|
8826
|
+
await this.validateScopeAccess(tenantId, scopeId, 'moveImportCursorAt');
|
|
8812
8827
|
// #2114: the replay's id is also its audit `operationId`, so the scheduled settle pairs and
|
|
8813
8828
|
// closes the lever's rows exactly as it does the kill switches'.
|
|
8814
8829
|
const replayId = ulid();
|