@forgezero/agent 0.1.38 → 0.1.39
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/README.md +37 -4
- package/dist/agent-heartbeat.d.ts +13 -0
- package/dist/agent-heartbeat.js +9 -1
- package/dist/bootstrap.d.ts +36 -4
- package/dist/bootstrap.js +516 -59
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -1
- package/dist/cloudflare-bootstrap.d.ts +31 -2
- package/dist/cloudflare-bootstrap.js +168 -27
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/fz-agent.js +6217 -20
- package/dist/fz.js +10766 -241
- package/dist/metal-bootstrap.d.ts +1 -0
- package/dist/metal-bootstrap.js +5 -3
- package/dist/platform-bootstrap-runtime.d.ts +0 -28
- package/dist/platform-bootstrap-runtime.js +1 -71
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +3 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
|
|
1
|
+
import { verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
|
|
2
2
|
export interface CloudflareBootstrapCommandDependencies {
|
|
3
3
|
run?: CloudflareBootstrapPhaseRunner;
|
|
4
4
|
write?: (text: string) => void;
|
|
@@ -9,3 +9,7 @@ export interface CloudflareBootstrapCommandDependencies {
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function readCloudflareBootstrapCommandConfig(path: string, mode: 'plan' | 'apply'): AttendedCloudflareBootstrapRequest;
|
|
11
11
|
export declare function runCloudflareBootstrapCommand(configPath: string, apply: boolean, dependencies?: CloudflareBootstrapCommandDependencies): Promise<CloudflareBootstrapEvidence>;
|
|
12
|
+
export declare function runCloudflareBootstrapVerificationCommand(checkpointPath: string, dependencies?: {
|
|
13
|
+
verify?: typeof verifyCloudflareBootstrapAcceptance;
|
|
14
|
+
write?: (text: string) => void;
|
|
15
|
+
}): Promise<CloudflareBootstrapAcceptanceEvidence>;
|
|
@@ -144,10 +144,28 @@ export interface CloudflareBootstrapEvidence {
|
|
|
144
144
|
nodes: ReadonlyArray<{
|
|
145
145
|
nodeName: string;
|
|
146
146
|
hostname: string;
|
|
147
|
+
/** Owner-only, node-specific input for `fz bootstrap platform`. */
|
|
148
|
+
handoffFile?: string;
|
|
147
149
|
tunnelId?: string;
|
|
148
150
|
applicationId?: string;
|
|
149
151
|
}>;
|
|
150
152
|
}
|
|
153
|
+
/** Secret-free proof that the already-provisioned edge reaches live APIs. */
|
|
154
|
+
export interface CloudflareBootstrapAcceptanceEvidence {
|
|
155
|
+
format: 1;
|
|
156
|
+
kind: 'forgezero-cloudflare-bootstrap-acceptance';
|
|
157
|
+
checkpointFile: string;
|
|
158
|
+
verifiedAt: string;
|
|
159
|
+
nodes: ReadonlyArray<{
|
|
160
|
+
nodeName: string;
|
|
161
|
+
hostname: string;
|
|
162
|
+
status: number;
|
|
163
|
+
}>;
|
|
164
|
+
publicDomains: ReadonlyArray<{
|
|
165
|
+
hostname: string;
|
|
166
|
+
status: number;
|
|
167
|
+
}>;
|
|
168
|
+
}
|
|
151
169
|
export interface CloudflareBootstrapDependencies {
|
|
152
170
|
fetcher?: typeof fetch;
|
|
153
171
|
workerRunner?: CloudflareWorkerCommandRunner;
|
|
@@ -191,6 +209,8 @@ export interface CloudflareHostHandoff extends CloudflareConnectorHandoff {
|
|
|
191
209
|
deviceProfileId: string;
|
|
192
210
|
};
|
|
193
211
|
}
|
|
212
|
+
/** Deterministic private output path; safe to include in secret-free evidence. */
|
|
213
|
+
export declare function cloudflareHostHandoffPath(checkpointPath: string, nodeName: string): string;
|
|
194
214
|
/**
|
|
195
215
|
* Select one node's connector credential directly from the completed fleet
|
|
196
216
|
* checkpoint. Host bootstrap can pipe this value into `systemd-creds` without
|
|
@@ -199,8 +219,7 @@ export interface CloudflareHostHandoff extends CloudflareConnectorHandoff {
|
|
|
199
219
|
*/
|
|
200
220
|
export declare function readCloudflareConnectorHandoff(checkpointPath: string, nodeName: string): Promise<CloudflareConnectorHandoff>;
|
|
201
221
|
/** Complete secret-bearing handoff consumed once by root host bootstrap. */
|
|
202
|
-
export declare function readCloudflareHostHandoff(
|
|
203
|
-
/** Atomically persist sensitive bootstrap material without making it runtime configuration. */
|
|
222
|
+
export declare function readCloudflareHostHandoff(handoffPath: string, nodeName: string): Promise<CloudflareHostHandoff>;
|
|
204
223
|
export declare function writeOwnerBootstrapOutput(path: string, output: CloudflareBootstrapOutput): Promise<void>;
|
|
205
224
|
/**
|
|
206
225
|
* Attended, resumable Cloudflare bootstrap. The output is checkpointed after
|
|
@@ -215,4 +234,14 @@ export declare function applyCloudflareBootstrap(input: CloudflareBootstrapCoord
|
|
|
215
234
|
* a deliberately secret-free evidence record to the outer bootstrap journal.
|
|
216
235
|
*/
|
|
217
236
|
export declare function runAttendedCloudflareBootstrap(request: AttendedCloudflareBootstrapRequest, dependencies?: CloudflareBootstrapDependencies): Promise<CloudflareBootstrapEvidence>;
|
|
237
|
+
/**
|
|
238
|
+
* Prove the post-bootstrap data path from an operator machine.
|
|
239
|
+
*
|
|
240
|
+
* This deliberately reads only the completed owner checkpoint. The Access
|
|
241
|
+
* service credential is used in request headers for each origin and is never
|
|
242
|
+
* returned, logged, copied to a host, or accepted on argv. Public Worker
|
|
243
|
+
* domains are then checked without privileged headers, proving the same route
|
|
244
|
+
* a client uses after KV heartbeats are active.
|
|
245
|
+
*/
|
|
246
|
+
export declare function verifyCloudflareBootstrapAcceptance(checkpointPath: string, fetcher?: typeof fetch): Promise<CloudflareBootstrapAcceptanceEvidence>;
|
|
218
247
|
export {};
|
|
@@ -411,6 +411,22 @@ import { dirname, join, resolve } from "path";
|
|
|
411
411
|
import { tmpdir } from "os";
|
|
412
412
|
import { randomUUID } from "crypto";
|
|
413
413
|
import { isIP as isIP2 } from "net";
|
|
414
|
+
var acceptanceFetch = async (url, label, fetcher, headers) => {
|
|
415
|
+
let response;
|
|
416
|
+
try {
|
|
417
|
+
response = await fetcher(url, {
|
|
418
|
+
method: "GET",
|
|
419
|
+
headers,
|
|
420
|
+
redirect: "manual",
|
|
421
|
+
signal: AbortSignal.timeout(5000)
|
|
422
|
+
});
|
|
423
|
+
} catch {
|
|
424
|
+
throw new Error(`${label} is unreachable`);
|
|
425
|
+
}
|
|
426
|
+
if (!response.ok)
|
|
427
|
+
throw new Error(`${label} returned HTTP ${response.status}`);
|
|
428
|
+
return response.status;
|
|
429
|
+
};
|
|
414
430
|
var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
415
431
|
async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
|
|
416
432
|
const metadata = await handle.stat();
|
|
@@ -739,6 +755,12 @@ async function readExistingOutput(path) {
|
|
|
739
755
|
}
|
|
740
756
|
return output;
|
|
741
757
|
}
|
|
758
|
+
function cloudflareHostHandoffPath(checkpointPath, nodeName) {
|
|
759
|
+
const normalized = nodeName.trim().toLowerCase();
|
|
760
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalized))
|
|
761
|
+
throw new Error("Cloudflare host handoff node name is invalid");
|
|
762
|
+
return join(`${resolve(checkpointPath)}.hosts`, `${normalized}.json`);
|
|
763
|
+
}
|
|
742
764
|
async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
|
|
743
765
|
const output = await readExistingOutput(resolve(checkpointPath));
|
|
744
766
|
if (!output || output.phase !== "complete") {
|
|
@@ -766,40 +788,66 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
|
|
|
766
788
|
connectorToken: resource.connectorToken
|
|
767
789
|
};
|
|
768
790
|
}
|
|
769
|
-
async function readCloudflareHostHandoff(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
791
|
+
async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
792
|
+
let parsed;
|
|
793
|
+
try {
|
|
794
|
+
parsed = JSON.parse(await readOwnerOnlyFile(handoffPath, 65536));
|
|
795
|
+
} catch (cause) {
|
|
796
|
+
if (cause instanceof SyntaxError)
|
|
797
|
+
throw new Error("Cloudflare host handoff is not valid JSON");
|
|
798
|
+
throw cause;
|
|
775
799
|
}
|
|
776
|
-
|
|
800
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
801
|
+
throw new Error("Cloudflare host handoff is malformed");
|
|
802
|
+
const output = parsed;
|
|
803
|
+
const unknown = Object.keys(output).filter((key) => ![
|
|
804
|
+
"format",
|
|
805
|
+
"kind",
|
|
806
|
+
"nodeName",
|
|
807
|
+
"hostname",
|
|
808
|
+
"service",
|
|
809
|
+
"tunnelId",
|
|
810
|
+
"connectorToken",
|
|
811
|
+
"accountId",
|
|
812
|
+
"zoneId",
|
|
813
|
+
"kvNamespaceId",
|
|
814
|
+
"kvRuntimeToken",
|
|
815
|
+
"privateNetworkRuntimeToken",
|
|
816
|
+
"warp"
|
|
817
|
+
].includes(key));
|
|
818
|
+
if (unknown.length)
|
|
819
|
+
throw new Error(`Cloudflare host handoff contains unsupported field ${unknown[0]}`);
|
|
820
|
+
if (output.warp) {
|
|
821
|
+
const unknownWarp = Object.keys(output.warp).filter((key) => ![
|
|
822
|
+
"organization",
|
|
823
|
+
"clientId",
|
|
824
|
+
"clientSecret",
|
|
825
|
+
"virtualNetworkId",
|
|
826
|
+
"deviceProfileId"
|
|
827
|
+
].includes(key));
|
|
828
|
+
if (unknownWarp.length)
|
|
829
|
+
throw new Error(`Cloudflare host handoff WARP contains unsupported field ${unknownWarp[0]}`);
|
|
830
|
+
}
|
|
831
|
+
const normalizedNodeName = nodeName.trim().toLowerCase();
|
|
832
|
+
let service;
|
|
833
|
+
try {
|
|
834
|
+
service = new URL(output.service ?? "");
|
|
835
|
+
} catch {}
|
|
836
|
+
if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "") || !/^[A-Za-z0-9._-]{40,80}$/.test(output.kvRuntimeToken ?? "") || !output.hostname || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(output.hostname) || !service || service.protocol !== "http:" || service.hostname !== "127.0.0.1" || !service.port || service.pathname !== "/" || service.username || service.password || service.search || service.hash || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(output.tunnelId ?? "") || !/^[A-Za-z0-9._-]{40,16384}$/.test(output.connectorToken ?? "")) {
|
|
837
|
+
throw new Error("Cloudflare host handoff is malformed or belongs to another node");
|
|
838
|
+
}
|
|
839
|
+
const network = output.privateNetworkRuntimeToken;
|
|
777
840
|
if (network !== undefined && !/^[A-Za-z0-9._-]{40,80}$/.test(network)) {
|
|
778
841
|
throw new Error("Cloudflare host handoff private-network capability is malformed");
|
|
779
842
|
}
|
|
780
|
-
|
|
781
|
-
const access = output.resources.access;
|
|
782
|
-
if (Boolean(privateNetwork) !== Boolean(network)) {
|
|
843
|
+
if (Boolean(output.warp) !== Boolean(network)) {
|
|
783
844
|
throw new Error("Cloudflare host handoff private-network resources and capability disagree");
|
|
784
845
|
}
|
|
785
|
-
if (
|
|
846
|
+
if (output.warp && (!output.warp.clientId || !output.warp.clientSecret || !/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(output.warp.organization) || !/^[0-9a-f-]{36}$/i.test(output.warp.virtualNetworkId) || !output.warp.deviceProfileId)) {
|
|
786
847
|
throw new Error("Cloudflare host handoff WARP enrollment is malformed");
|
|
787
848
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
accountId: output.coordinates.accountId,
|
|
791
|
-
zoneId: output.coordinates.zoneId,
|
|
792
|
-
kvNamespaceId: output.resources.kvNamespaceId,
|
|
793
|
-
kvRuntimeToken: kv.value,
|
|
794
|
-
...network ? { privateNetworkRuntimeToken: network } : {},
|
|
795
|
-
...privateNetwork && access ? { warp: {
|
|
796
|
-
organization: privateNetwork.warpOrganization,
|
|
797
|
-
clientId: access.clientId,
|
|
798
|
-
clientSecret: access.clientSecret,
|
|
799
|
-
virtualNetworkId: privateNetwork.virtualNetworkId,
|
|
800
|
-
deviceProfileId: privateNetwork.deviceProfileId
|
|
801
|
-
} } : {}
|
|
802
|
-
};
|
|
849
|
+
const { format: _format, kind: _kind, ...handoff } = output;
|
|
850
|
+
return handoff;
|
|
803
851
|
}
|
|
804
852
|
async function prepareOwnerOutputDirectory(absolutePath) {
|
|
805
853
|
const directory = dirname(absolutePath);
|
|
@@ -811,7 +859,7 @@ async function prepareOwnerOutputDirectory(absolutePath) {
|
|
|
811
859
|
}
|
|
812
860
|
return directory;
|
|
813
861
|
}
|
|
814
|
-
async function
|
|
862
|
+
async function writeOwnerJson(path, output) {
|
|
815
863
|
const absolute = resolve(path);
|
|
816
864
|
const directory = await prepareOwnerOutputDirectory(absolute);
|
|
817
865
|
const temporary = `${absolute}.${randomUUID()}.tmp`;
|
|
@@ -839,6 +887,42 @@ async function writeOwnerBootstrapOutput(path, output) {
|
|
|
839
887
|
});
|
|
840
888
|
}
|
|
841
889
|
}
|
|
890
|
+
async function writeOwnerBootstrapOutput(path, output) {
|
|
891
|
+
await writeOwnerJson(path, output);
|
|
892
|
+
}
|
|
893
|
+
async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
894
|
+
const kv = output.resources.runtimeTokens?.kv?.value;
|
|
895
|
+
if (!kv || !/^[A-Za-z0-9._-]{40,80}$/.test(kv)) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const network = output.resources.runtimeTokens?.privateNetwork?.value;
|
|
899
|
+
const privateNetwork = output.resources.privateNetwork;
|
|
900
|
+
const access = output.resources.access;
|
|
901
|
+
for (const node of output.resources.nodes) {
|
|
902
|
+
const handoff = {
|
|
903
|
+
format: 1,
|
|
904
|
+
kind: "forgezero-cloudflare-host-handoff",
|
|
905
|
+
nodeName: node.nodeName,
|
|
906
|
+
hostname: node.hostname,
|
|
907
|
+
service: node.service,
|
|
908
|
+
tunnelId: node.tunnelId,
|
|
909
|
+
connectorToken: node.connectorToken,
|
|
910
|
+
accountId: output.coordinates.accountId,
|
|
911
|
+
zoneId: output.coordinates.zoneId,
|
|
912
|
+
kvNamespaceId: output.resources.kvNamespaceId,
|
|
913
|
+
kvRuntimeToken: kv,
|
|
914
|
+
...network ? { privateNetworkRuntimeToken: network } : {},
|
|
915
|
+
...privateNetwork && access ? { warp: {
|
|
916
|
+
organization: privateNetwork.warpOrganization,
|
|
917
|
+
clientId: access.clientId,
|
|
918
|
+
clientSecret: access.clientSecret,
|
|
919
|
+
virtualNetworkId: privateNetwork.virtualNetworkId,
|
|
920
|
+
deviceProfileId: privateNetwork.deviceProfileId
|
|
921
|
+
} } : {}
|
|
922
|
+
};
|
|
923
|
+
await writeOwnerJson(cloudflareHostHandoffPath(checkpointPath, node.nodeName), handoff);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
842
926
|
var tokenFor = (tokens, key) => {
|
|
843
927
|
const token = tokens[key]?.trim() || tokens.apiToken?.trim();
|
|
844
928
|
if (!token)
|
|
@@ -1139,6 +1223,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
1139
1223
|
}
|
|
1140
1224
|
};
|
|
1141
1225
|
await writeOwnerBootstrapOutput(absoluteOutput, output);
|
|
1226
|
+
await writeCloudflareHostHandoffs(absoluteOutput, output);
|
|
1142
1227
|
return output;
|
|
1143
1228
|
}
|
|
1144
1229
|
async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
|
|
@@ -1177,13 +1262,68 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
|
|
|
1177
1262
|
nodes: output.resources.nodes.map(({ nodeName, hostname, tunnelId, applicationId }) => ({
|
|
1178
1263
|
nodeName,
|
|
1179
1264
|
hostname,
|
|
1265
|
+
...output.resources.runtimeTokens?.kv ? {
|
|
1266
|
+
handoffFile: cloudflareHostHandoffPath(plan.outputFile, nodeName)
|
|
1267
|
+
} : {},
|
|
1180
1268
|
tunnelId,
|
|
1181
1269
|
applicationId
|
|
1182
1270
|
}))
|
|
1183
1271
|
};
|
|
1184
1272
|
}
|
|
1273
|
+
async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fetch) {
|
|
1274
|
+
const absolute = resolve(checkpointPath);
|
|
1275
|
+
const output = await readExistingOutput(absolute);
|
|
1276
|
+
if (!output || output.phase !== "complete")
|
|
1277
|
+
throw new Error("Cloudflare acceptance requires a completed owner checkpoint");
|
|
1278
|
+
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
1279
|
+
const access = output.resources.access;
|
|
1280
|
+
if (!access?.clientId?.trim() || !access.clientSecret?.trim()) {
|
|
1281
|
+
throw new Error("Cloudflare acceptance checkpoint is missing the Access service credential");
|
|
1282
|
+
}
|
|
1283
|
+
if (!output.resources.worker?.deployed || output.resources.worker.scriptName !== coordinates.workerScriptName || JSON.stringify(output.resources.worker.publicDomains) !== JSON.stringify(coordinates.publicDomains)) {
|
|
1284
|
+
throw new Error("Cloudflare acceptance checkpoint does not prove the expected Worker deployment");
|
|
1285
|
+
}
|
|
1286
|
+
if (output.resources.nodes.length !== coordinates.nodes.length) {
|
|
1287
|
+
throw new Error("Cloudflare acceptance checkpoint does not cover the declared node fleet");
|
|
1288
|
+
}
|
|
1289
|
+
const nodeNames = new Set;
|
|
1290
|
+
const hostnames = new Set;
|
|
1291
|
+
for (const node of output.resources.nodes) {
|
|
1292
|
+
const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
|
|
1293
|
+
if (!expected || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(node.tunnelId)) {
|
|
1294
|
+
throw new Error("Cloudflare acceptance checkpoint has an unbound node resource");
|
|
1295
|
+
}
|
|
1296
|
+
if (nodeNames.has(node.nodeName) || hostnames.has(node.hostname)) {
|
|
1297
|
+
throw new Error("Cloudflare acceptance checkpoint has duplicate node coordinates");
|
|
1298
|
+
}
|
|
1299
|
+
nodeNames.add(node.nodeName);
|
|
1300
|
+
hostnames.add(node.hostname);
|
|
1301
|
+
}
|
|
1302
|
+
const accessHeaders = {
|
|
1303
|
+
"CF-Access-Client-Id": access.clientId,
|
|
1304
|
+
"CF-Access-Client-Secret": access.clientSecret
|
|
1305
|
+
};
|
|
1306
|
+
const nodes = await Promise.all(output.resources.nodes.map(async ({ nodeName, hostname }) => ({
|
|
1307
|
+
nodeName,
|
|
1308
|
+
hostname,
|
|
1309
|
+
status: await acceptanceFetch(`https://${hostname}/api/health`, `Cloudflare origin ${nodeName}`, fetcher, accessHeaders)
|
|
1310
|
+
})));
|
|
1311
|
+
const publicDomains = await Promise.all(output.resources.worker.publicDomains.map(async (hostname) => ({
|
|
1312
|
+
hostname,
|
|
1313
|
+
status: await acceptanceFetch(`https://${hostname}/api/health`, `Cloudflare public domain ${hostname}`, fetcher)
|
|
1314
|
+
})));
|
|
1315
|
+
return {
|
|
1316
|
+
format: 1,
|
|
1317
|
+
kind: "forgezero-cloudflare-bootstrap-acceptance",
|
|
1318
|
+
checkpointFile: absolute,
|
|
1319
|
+
verifiedAt: new Date().toISOString(),
|
|
1320
|
+
nodes,
|
|
1321
|
+
publicDomains
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1185
1324
|
export {
|
|
1186
1325
|
writeOwnerBootstrapOutput,
|
|
1326
|
+
verifyCloudflareBootstrapAcceptance,
|
|
1187
1327
|
validateCloudflareBootstrapCoordinates,
|
|
1188
1328
|
runAttendedCloudflareBootstrap,
|
|
1189
1329
|
readOwnerApiToken,
|
|
@@ -1192,5 +1332,6 @@ export {
|
|
|
1192
1332
|
readCloudflareBootstrapTokens,
|
|
1193
1333
|
planCloudflareBootstrap,
|
|
1194
1334
|
deployCloudflareWorker,
|
|
1335
|
+
cloudflareHostHandoffPath,
|
|
1195
1336
|
applyCloudflareBootstrap
|
|
1196
1337
|
};
|
|
@@ -38,6 +38,8 @@ export interface DeploymentPullOptions {
|
|
|
38
38
|
clearTimer?: (handle: unknown) => void;
|
|
39
39
|
onEvent?: (event: string, detail?: unknown) => void;
|
|
40
40
|
telemetry?: AgentOperationTelemetry;
|
|
41
|
+
/** Fail-closed desired-state gate. False means do not contact the claim route. */
|
|
42
|
+
canClaim?: () => boolean;
|
|
41
43
|
}
|
|
42
44
|
export type PullResult = {
|
|
43
45
|
status: 'idle';
|