@forgezero/agent 0.1.41 → 0.1.43

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.
Files changed (64) hide show
  1. package/README.md +299 -86
  2. package/dist/agent-heartbeat.js +6 -3
  3. package/dist/agent-update-helper.js +5 -2
  4. package/dist/agent-update.js +5 -2
  5. package/dist/bootstrap.d.ts +17 -8
  6. package/dist/bootstrap.js +1509 -512
  7. package/dist/cli/agent-install.d.ts +6 -5
  8. package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
  9. package/dist/cli/maintenance.d.ts +23 -0
  10. package/dist/cli/run.d.ts +3 -1
  11. package/dist/cli/session-store.d.ts +5 -0
  12. package/dist/cloudflare-bootstrap.d.ts +73 -35
  13. package/dist/cloudflare-bootstrap.js +587 -90
  14. package/dist/cloudflare-edge.d.ts +64 -12
  15. package/dist/cloudflare-edge.js +103 -8
  16. package/dist/community-rehearsal-host.d.ts +51 -0
  17. package/dist/community-rehearsal-host.js +272 -0
  18. package/dist/credential-schema.d.ts +54 -0
  19. package/dist/credential-schema.js +47 -0
  20. package/dist/definition.d.ts +31 -5
  21. package/dist/definition.js +271 -44
  22. package/dist/deploy-file.js +294 -68
  23. package/dist/deployment-runner.js +18 -5
  24. package/dist/deployment.d.ts +13 -1
  25. package/dist/fz-agent.js +3932 -582
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +3636 -1263
  28. package/dist/git-ssh.d.ts +5 -0
  29. package/dist/guest-enrolment.d.ts +2 -0
  30. package/dist/guest-enrolment.js +1 -0
  31. package/dist/host-maintenance.d.ts +39 -0
  32. package/dist/host-maintenance.js +135 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/mesh-connector.d.ts +16 -0
  35. package/dist/mesh-connector.js +46 -0
  36. package/dist/metal-bootstrap.js +150 -7
  37. package/dist/metal-helper-socket.js +61 -31
  38. package/dist/metal-provision.d.ts +2 -2
  39. package/dist/metal-provision.js +62 -32
  40. package/dist/operator-bootstrap.d.ts +90 -0
  41. package/dist/operator-bootstrap.js +5709 -0
  42. package/dist/otel-collector.d.ts +18 -0
  43. package/dist/pipeline.d.ts +3 -2
  44. package/dist/pipeline.js +1 -1
  45. package/dist/platform-bootstrap-runtime.d.ts +39 -21
  46. package/dist/platform-bootstrap-runtime.js +182 -59
  47. package/dist/platform-fleet-verification.d.ts +19 -0
  48. package/dist/platform-fleet-verification.js +3873 -0
  49. package/dist/platform-genesis-config.d.ts +7 -0
  50. package/dist/platform-genesis.d.ts +17 -0
  51. package/dist/provision.d.ts +76 -3
  52. package/dist/provision.js +1061 -229
  53. package/dist/recovery-host.d.ts +7 -0
  54. package/dist/recovery-host.js +124 -0
  55. package/dist/service-supervisor.d.ts +42 -0
  56. package/dist/software-helper.d.ts +4 -0
  57. package/dist/software-helper.js +865 -63
  58. package/dist/software.d.ts +14 -3
  59. package/dist/software.js +163 -37
  60. package/dist/ssh-bootstrap.d.ts +97 -0
  61. package/dist/supervised-app.d.ts +2 -0
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +175 -164
  64. package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
@@ -94,6 +94,39 @@ async function removeCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
94
94
  network: privateDatabaseHostRoute(config.privateAddress)
95
95
  }, fetcher);
96
96
  }
97
+ var privateNetworkCidr = (value) => {
98
+ const [address, prefixText, ...extra] = value.trim().toLowerCase().split("/");
99
+ const family = isIP(address ?? "");
100
+ const prefix = Number(prefixText);
101
+ if (extra.length || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128) || !isPrivateDatabaseAddress(address)) {
102
+ throw new Error("Cloudflare WARP include must be an explicit private IPv4 or IPv6 CIDR");
103
+ }
104
+ return `${address}/${prefix}`;
105
+ };
106
+ async function ensureCloudflareWarpNetworkIncludes(config, fetcher = fetch) {
107
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
108
+ throw new Error("Cloudflare WARP policy id is invalid");
109
+ if (!Array.isArray(config.networks) || config.networks.length < 1 || config.networks.length > 256) {
110
+ throw new Error("Cloudflare WARP networks must contain 1-256 explicit CIDRs");
111
+ }
112
+ const networks = config.networks.map(privateNetworkCidr);
113
+ if (new Set(networks).size !== networks.length)
114
+ throw new Error("Cloudflare WARP networks must be unique");
115
+ const path = `/accounts/${config.accountId}/devices/policy/${config.policyId}/include`;
116
+ const entries = await cf(config, path, {}, fetcher);
117
+ const present = new Set(entries.flatMap(({ address }) => address ? [address.toLowerCase()] : []));
118
+ const additions = networks.filter((network) => !present.has(network)).map((address) => ({
119
+ address,
120
+ description: `${config.descriptionPrefix}:${address}`.slice(0, 100)
121
+ }));
122
+ if (!additions.length)
123
+ return { entries, created: 0 };
124
+ const updated = await cf(config, path, {
125
+ method: "PUT",
126
+ body: JSON.stringify([...entries, ...additions])
127
+ }, fetcher);
128
+ return { entries: updated, created: additions.length };
129
+ }
97
130
  async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
98
131
  if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
99
132
  throw new Error("Cloudflare WARP policy id is invalid");
@@ -127,13 +160,11 @@ async function removeCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
127
160
  return { entries: updated, removed: true };
128
161
  }
129
162
  async function configureCloudflareEdge(config, fetcher = fetch) {
130
- const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
131
- const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
132
163
  const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
133
164
  const dnsPath = `/zones/${config.zoneId}/dns_records`;
134
165
  const [current, records] = await Promise.all([
135
- cf(tunnelAuth, tunnelPath, {}, fetcher),
136
- cf(dnsAuth, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
166
+ cf(config, tunnelPath, {}, fetcher),
167
+ cf(config, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
137
168
  ]);
138
169
  if (records.length > 1) {
139
170
  throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
@@ -151,7 +182,7 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
151
182
  ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
152
183
  ];
153
184
  if (JSON.stringify(existing) !== JSON.stringify(desiredIngress)) {
154
- await cf(tunnelAuth, tunnelPath, {
185
+ await cf(config, tunnelPath, {
155
186
  method: "PUT",
156
187
  body: JSON.stringify({ config: { ingress: desiredIngress } })
157
188
  }, fetcher);
@@ -165,12 +196,72 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
165
196
  };
166
197
  const dnsAlreadyCorrect = existingRecord?.type === record.type && existingRecord.name?.toLowerCase() === record.name.toLowerCase() && existingRecord.content?.toLowerCase() === record.content.toLowerCase() && existingRecord.proxied === true && existingRecord.ttl === 1;
167
198
  if (!dnsAlreadyCorrect) {
168
- await cf(dnsAuth, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
169
- method: existingRecord ? "PUT" : "POST",
170
- body: JSON.stringify(record)
199
+ await cf(config, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
200
+ method: existingRecord ? "PATCH" : "POST",
201
+ body: JSON.stringify(existingRecord ? {
202
+ ...record,
203
+ ...existingRecord.comment === undefined ? {} : { comment: existingRecord.comment },
204
+ ...existingRecord.tags === undefined ? {} : { tags: existingRecord.tags },
205
+ ...existingRecord.settings === undefined ? {} : { settings: existingRecord.settings }
206
+ } : record)
171
207
  }, fetcher);
172
208
  }
173
209
  }
210
+ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
211
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
212
+ throw new Error("Cloudflare Worker script name is invalid");
213
+ }
214
+ const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
215
+ const owned = namespaces.filter(({ script }) => script === config.scriptName);
216
+ if (!owned.length)
217
+ throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
218
+ return owned;
219
+ }
220
+ async function configureCloudflareRealtimeSecrets(config, fetcher = fetch) {
221
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName) || ![config.publishSecret, config.ticketSecret].every((value) => /^[A-Za-z0-9_-]{64,128}$/.test(value)) || config.publishSecret === config.ticketSecret) {
222
+ throw new Error("Cloudflare realtime secret coordinates are invalid");
223
+ }
224
+ await cf(config, `/accounts/${config.accountId}/workers/scripts/${encodeURIComponent(config.scriptName)}/secrets-bulk`, {
225
+ method: "PATCH",
226
+ body: JSON.stringify({
227
+ secrets: {
228
+ REALTIME_PUBLISH_SECRET: {
229
+ name: "REALTIME_PUBLISH_SECRET",
230
+ text: config.publishSecret,
231
+ type: "secret_text"
232
+ },
233
+ REALTIME_TICKET_SECRET: {
234
+ name: "REALTIME_TICKET_SECRET",
235
+ text: config.ticketSecret,
236
+ type: "secret_text"
237
+ }
238
+ }
239
+ })
240
+ }, fetcher);
241
+ }
242
+ async function ensureCloudflareMeshConnector(config, fetcher = fetch) {
243
+ const name = config.name.trim();
244
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
245
+ throw new Error("Cloudflare Mesh connector name is invalid");
246
+ }
247
+ const path = `/accounts/${config.accountId}/warp_connector`;
248
+ const connectors = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
249
+ const matches = connectors.filter((connector2) => connector2.name === name && !connector2.deleted_at);
250
+ if (matches.length > 1)
251
+ throw new Error(`Cloudflare Mesh connector ${name} is ambiguous`);
252
+ const created = !matches[0];
253
+ const connector = matches[0] ?? await cf(config, path, {
254
+ method: "POST",
255
+ body: JSON.stringify({ name, ha: config.highAvailability })
256
+ }, fetcher);
257
+ if (!connector.id)
258
+ throw new Error("Cloudflare returned an invalid Mesh connector");
259
+ const connectorToken = await cf(config, `${path}/${encodeURIComponent(connector.id)}/token`, {}, fetcher);
260
+ if (!connectorToken || connectorToken.length > 16384) {
261
+ throw new Error("Cloudflare returned an invalid Mesh connector token");
262
+ }
263
+ return { connector, connectorToken, created };
264
+ }
174
265
  async function ensureCloudflareTunnel(config, fetcher = fetch) {
175
266
  const name = config.name.trim();
176
267
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
@@ -195,13 +286,15 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
195
286
 
196
287
  // src/cloudflare-bootstrap.ts
197
288
  import { constants } from "fs";
198
- import { randomUUID } from "crypto";
199
- import { chmod, lstat, mkdir, open, rename, stat, unlink } from "fs/promises";
289
+ import { randomBytes, randomUUID } from "crypto";
290
+ import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
200
291
  import { dirname, join, resolve } from "path";
292
+ import { isIP as isIP2 } from "net";
201
293
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
202
294
  var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
203
295
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
204
296
  var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
297
+ var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
205
298
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
206
299
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
207
300
  const metadata = await handle.stat();
@@ -242,33 +335,23 @@ async function readOwnerApiToken(path) {
242
335
  return token;
243
336
  }
244
337
  async function readCloudflareBootstrapTokens(files) {
245
- const entries = await Promise.all([
246
- ["apiToken", files.apiTokenFile],
247
- ["managementApiToken", files.managementApiTokenFile],
248
- ["runtimeApiToken", files.runtimeApiTokenFile],
249
- ["tunnelApiToken", files.tunnelApiTokenFile],
250
- ["dnsApiToken", files.dnsApiTokenFile],
251
- ["kvApiToken", files.kvApiTokenFile]
252
- ].map(async ([key, path]) => [key, path ? await readOwnerApiToken(path) : undefined]));
253
- const supplied = Object.fromEntries(entries.filter(([, value]) => value !== undefined));
254
- const tokens = {
255
- ...supplied.apiToken ? { apiToken: supplied.apiToken } : {},
256
- ...supplied.tunnelApiToken || supplied.managementApiToken ? {
257
- tunnelApiToken: supplied.tunnelApiToken ?? supplied.managementApiToken
258
- } : {},
259
- ...supplied.dnsApiToken || supplied.managementApiToken ? {
260
- dnsApiToken: supplied.dnsApiToken ?? supplied.managementApiToken
261
- } : {},
262
- ...supplied.kvApiToken || supplied.runtimeApiToken ? {
263
- kvApiToken: supplied.kvApiToken ?? supplied.runtimeApiToken
264
- } : {}
265
- };
266
- for (const key of ["tunnelApiToken", "dnsApiToken", "kvApiToken"]) {
267
- if (!tokens[key] && !tokens.apiToken) {
268
- throw new Error(`Cloudflare ${key} file is required when apiTokenFile is omitted`);
269
- }
338
+ const unsupported = Object.keys(files).filter((key) => ![
339
+ "tunnelTokenFile",
340
+ "apiTokenFile"
341
+ ].includes(key));
342
+ if (unsupported.length)
343
+ throw new Error(`Cloudflare bootstrap token files contain unsupported field ${unsupported[0]}`);
344
+ if (!files.tunnelTokenFile?.trim() || !files.apiTokenFile?.trim()) {
345
+ throw new Error("Cloudflare bootstrap requires exactly tunnelTokenFile and apiTokenFile");
270
346
  }
271
- return tokens;
347
+ const [tunnelToken, apiToken] = await Promise.all([
348
+ readOwnerApiToken(files.tunnelTokenFile),
349
+ readOwnerApiToken(files.apiTokenFile)
350
+ ]);
351
+ if (tunnelToken === apiToken) {
352
+ throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
353
+ }
354
+ return { tunnelToken, apiToken };
272
355
  }
273
356
  var validateId = (value, label) => {
274
357
  const normalized = value.trim().toLowerCase();
@@ -294,28 +377,89 @@ var normalizeService = (value) => {
294
377
  }
295
378
  return service.toString().replace(/\/$/, "");
296
379
  };
380
+ var privateMeshCidr = (value) => {
381
+ const [address, prefixText, ...extra] = value.trim().toLowerCase().split("/");
382
+ const family = isIP2(address ?? "");
383
+ const prefix = Number(prefixText);
384
+ const v4 = family === 4 ? address.split(".").map(Number) : undefined;
385
+ const privateAddress = family === 4 ? Boolean(v4 && (v4[0] === 10 || v4[0] === 172 && v4[1] >= 16 && v4[1] <= 31 || v4[0] === 192 && v4[1] === 168)) : family === 6 && /^f[cd][0-9a-f]:/i.test(address);
386
+ if (extra.length || !family || !privateAddress || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
387
+ throw new Error("Cloudflare Mesh route must be an explicit private IPv4 or IPv6 CIDR");
388
+ }
389
+ return `${address}/${prefix}`;
390
+ };
297
391
  function validateCloudflareBootstrapCoordinates(input) {
298
- const nodeInputs = input.nodes?.length ? input.nodes : [{
299
- nodeName: input.tunnelName,
300
- hostname: input.hostname,
301
- service: input.service,
302
- tunnelName: input.tunnelName
303
- }];
392
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
393
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
394
+ }
395
+ const unsupported = Object.keys(input).filter((key) => ![
396
+ "accountId",
397
+ "zoneId",
398
+ "kvNamespaceId",
399
+ "meshDevicePolicyId",
400
+ "realtime",
401
+ "nodes"
402
+ ].includes(key));
403
+ if (unsupported.length)
404
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
405
+ const nodeInputs = input.nodes;
406
+ if (!Array.isArray(nodeInputs))
407
+ throw new Error("Cloudflare bootstrap requires an explicit nodes array");
304
408
  if (nodeInputs.length < 1 || nodeInputs.length > 32) {
305
409
  throw new Error("Cloudflare bootstrap requires between 1 and 32 explicit nodes");
306
410
  }
307
411
  const nodes = nodeInputs.map((node) => {
412
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
413
+ throw new Error("Cloudflare bootstrap node must be an object");
414
+ }
415
+ const unknownNode = Object.keys(node).filter((key) => ![
416
+ "nodeName",
417
+ "hostname",
418
+ "service",
419
+ "tunnelName",
420
+ "mesh"
421
+ ].includes(key));
422
+ if (unknownNode.length)
423
+ throw new Error(`Cloudflare bootstrap node contains unsupported field ${unknownNode[0]}`);
308
424
  const nodeName = node.nodeName.trim().toLowerCase();
309
425
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName))
310
426
  throw new Error("Cloudflare node name is invalid");
311
427
  const hostname = node.hostname.trim().toLowerCase();
312
428
  if (!HOSTNAME.test(hostname))
313
429
  throw new Error("Cloudflare public node hostname is invalid");
430
+ let mesh;
431
+ if (node.mesh !== undefined) {
432
+ if (!node.mesh || typeof node.mesh !== "object" || Array.isArray(node.mesh)) {
433
+ throw new Error("Cloudflare Mesh coordinates must be an object");
434
+ }
435
+ const unknownMesh = Object.keys(node.mesh).filter((key) => ![
436
+ "connectorName",
437
+ "routes",
438
+ "highAvailability"
439
+ ].includes(key));
440
+ if (unknownMesh.length)
441
+ throw new Error(`Cloudflare Mesh contains unsupported field ${unknownMesh[0]}`);
442
+ if (!Array.isArray(node.mesh.routes) || node.mesh.routes.length < 1 || node.mesh.routes.length > 64) {
443
+ throw new Error("Cloudflare Mesh node requires 1-64 private routes");
444
+ }
445
+ const routes = node.mesh.routes.map(privateMeshCidr);
446
+ if (new Set(routes).size !== routes.length)
447
+ throw new Error("Cloudflare Mesh routes must be unique per node");
448
+ if (node.mesh.highAvailability !== undefined && typeof node.mesh.highAvailability !== "boolean") {
449
+ throw new Error("Cloudflare Mesh highAvailability must be a boolean");
450
+ }
451
+ mesh = {
452
+ connectorName: validateName(node.mesh.connectorName, "Cloudflare Mesh connector name"),
453
+ routes,
454
+ highAvailability: node.mesh.highAvailability === true
455
+ };
456
+ }
314
457
  return {
315
458
  nodeName,
316
459
  hostname,
317
460
  service: normalizeService(node.service),
318
- tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name")
461
+ tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name"),
462
+ ...mesh ? { mesh } : {}
319
463
  };
320
464
  });
321
465
  for (const [label, values] of [
@@ -326,14 +470,51 @@ function validateCloudflareBootstrapCoordinates(input) {
326
470
  if (new Set(values).size !== values.length)
327
471
  throw new Error(`Cloudflare fleet ${label} must be unique`);
328
472
  }
329
- const first = nodes[0];
473
+ const meshNodes = nodes.filter(({ mesh }) => mesh);
474
+ if (meshNodes.length && !input.meshDevicePolicyId?.trim()) {
475
+ throw new Error("Cloudflare Mesh requires an explicit dedicated device policy id");
476
+ }
477
+ if (!meshNodes.length && input.meshDevicePolicyId !== undefined) {
478
+ throw new Error("Cloudflare Mesh device policy is not allowed without Mesh nodes");
479
+ }
480
+ const connectorNames = meshNodes.map(({ mesh }) => mesh.connectorName);
481
+ if (new Set(connectorNames).size !== connectorNames.length) {
482
+ throw new Error("Cloudflare Mesh connector names must be unique");
483
+ }
484
+ const routeOwners = new Map;
485
+ for (const node of meshNodes)
486
+ for (const route of node.mesh.routes) {
487
+ const owner = routeOwners.get(route);
488
+ if (owner)
489
+ throw new Error(`Cloudflare Mesh route ${route} is declared by both ${owner} and ${node.nodeName}`);
490
+ routeOwners.set(route, node.nodeName);
491
+ }
492
+ let realtime;
493
+ if (input.realtime !== undefined) {
494
+ if (!input.realtime || typeof input.realtime !== "object" || Array.isArray(input.realtime) || Object.keys(input.realtime).some((key) => !["workerScriptName", "endpoint", "producer"].includes(key))) {
495
+ throw new Error("Cloudflare realtime coordinates are malformed");
496
+ }
497
+ const workerScriptName = input.realtime.workerScriptName.trim();
498
+ const producer = input.realtime.producer.trim();
499
+ let endpoint2;
500
+ try {
501
+ endpoint2 = new URL(input.realtime.endpoint);
502
+ } catch {
503
+ throw new Error("Cloudflare realtime endpoint is malformed");
504
+ }
505
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(workerScriptName) || !/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$/.test(producer) || endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.pathname !== "/" || endpoint2.search || endpoint2.hash) {
506
+ throw new Error("Cloudflare realtime Worker, endpoint, or producer is invalid");
507
+ }
508
+ realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
509
+ }
330
510
  return {
331
511
  accountId: validateId(input.accountId, "Cloudflare account id"),
332
512
  zoneId: validateId(input.zoneId, "Cloudflare zone id"),
333
- hostname: first.hostname,
334
- service: first.service,
335
- tunnelName: first.tunnelName,
336
513
  kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
514
+ ...input.meshDevicePolicyId ? {
515
+ meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
516
+ } : {},
517
+ ...realtime ? { realtime } : {},
337
518
  nodes
338
519
  };
339
520
  }
@@ -346,18 +527,50 @@ function planCloudflareBootstrap(input, outputPath) {
346
527
  outputFile: resolve(outputPath),
347
528
  coordinates,
348
529
  operations: [
530
+ "prove CF_API_TOKEN can write, read and remove one namespaced nonce in the existing KV namespace",
531
+ ...coordinates.realtime ? [
532
+ "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
533
+ ] : [],
349
534
  "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
535
+ ...coordinates.nodes.some(({ mesh }) => mesh) ? [
536
+ "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its registration token",
537
+ "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
538
+ ] : [],
350
539
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
351
540
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
352
- "write one node-specific handoff containing the connector token and owner-supplied KV-write token"
541
+ "write one node-specific handoff containing the connector token and owner-supplied KV/Worker runtime token"
353
542
  ],
354
543
  secrets: [
355
544
  "API tokens are read only from owner-only files and are never placed in argv or stdout",
356
- "the management token is never persisted; connector and KV runtime capabilities are atomically checkpointed with mode 0600",
357
- "the normal API process receives only its KV-write token and never Tunnel or DNS management authority"
545
+ "CF_TUNNEL_TOKEN is never persisted; cloudflared, Mesh, CF_API_TOKEN and generated realtime capabilities are atomically checkpointed with mode 0600",
546
+ "the normal API process receives only CF_API_TOKEN and never Tunnel, DNS or Mesh management authority"
358
547
  ]
359
548
  };
360
549
  }
550
+ async function preflightCloudflareKvRuntime(coordinates, apiToken, fetcher) {
551
+ const nonce = randomUUID();
552
+ const key = `forgezero/bootstrap-preflight/${nonce}`;
553
+ const url = `https://api.cloudflare.com/client/v4/accounts/${coordinates.accountId}` + `/storage/kv/namespaces/${coordinates.kvNamespaceId}/values/${encodeURIComponent(key)}`;
554
+ const headers = { authorization: `Bearer ${apiToken}`, "content-type": "text/plain" };
555
+ let written = false;
556
+ try {
557
+ const put = await fetcher(url, { method: "PUT", headers, body: nonce });
558
+ if (!put.ok)
559
+ throw new Error(`Cloudflare KV runtime preflight write returned HTTP ${put.status}`);
560
+ written = true;
561
+ const get = await fetcher(url, { method: "GET", headers: { authorization: `Bearer ${apiToken}` } });
562
+ if (!get.ok)
563
+ throw new Error(`Cloudflare KV runtime preflight read returned HTTP ${get.status}`);
564
+ if (await get.text() !== nonce)
565
+ throw new Error("Cloudflare KV runtime preflight read returned unexpected data");
566
+ } finally {
567
+ if (written) {
568
+ const removed = await fetcher(url, { method: "DELETE", headers: { authorization: `Bearer ${apiToken}` } });
569
+ if (!removed.ok)
570
+ throw new Error(`Cloudflare KV runtime preflight cleanup returned HTTP ${removed.status}`);
571
+ }
572
+ }
573
+ }
361
574
  async function readExistingOutput(path) {
362
575
  try {
363
576
  await lstat(path);
@@ -374,10 +587,84 @@ async function readExistingOutput(path) {
374
587
  throw new Error(`${resolve(path)} is not valid bootstrap JSON`);
375
588
  throw cause;
376
589
  }
590
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
591
+ throw new Error(`${resolve(path)} is not a ForgeZero Cloudflare bootstrap output`);
592
+ }
377
593
  const output = parsed;
378
- if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !output.resources) {
594
+ const unsupported = Object.keys(output).filter((key) => ![
595
+ "format",
596
+ "kind",
597
+ "phase",
598
+ "updatedAt",
599
+ "coordinates",
600
+ "resources",
601
+ "created"
602
+ ].includes(key));
603
+ if (unsupported.length)
604
+ throw new Error(`${resolve(path)} contains unsupported field ${unsupported[0]}`);
605
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !["edge-resources-provisioned", "complete"].includes(String(output.phase)) || typeof output.updatedAt !== "string" || Number.isNaN(Date.parse(output.updatedAt)) || !output.resources || typeof output.resources !== "object" || Array.isArray(output.resources)) {
379
606
  throw new Error(`${resolve(path)} is not a ForgeZero Cloudflare bootstrap output`);
380
607
  }
608
+ const resources = output.resources;
609
+ const unsupportedResource = Object.keys(resources).filter((key) => ![
610
+ "kvNamespaceId",
611
+ "apiToken",
612
+ "realtime",
613
+ "nodes"
614
+ ].includes(key));
615
+ if (unsupportedResource.length) {
616
+ throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
617
+ }
618
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
619
+ if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
620
+ throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
621
+ }
622
+ if (coordinates.realtime) {
623
+ const realtime = resources.realtime;
624
+ if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
625
+ throw new Error(`${resolve(path)} has malformed Cloudflare realtime resources`);
626
+ }
627
+ } else if (resources.realtime !== undefined) {
628
+ throw new Error(`${resolve(path)} contains undeclared Cloudflare realtime resources`);
629
+ }
630
+ const seen = new Set;
631
+ for (const item of resources.nodes) {
632
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
633
+ throw new Error(`${resolve(path)} has a malformed Cloudflare node resource`);
634
+ }
635
+ const node = item;
636
+ const unknownNode = Object.keys(node).filter((key) => ![
637
+ "nodeName",
638
+ "hostname",
639
+ "service",
640
+ "tunnelName",
641
+ "tunnelId",
642
+ "connectorToken",
643
+ "mesh"
644
+ ].includes(key));
645
+ const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
646
+ if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? "")) || !CONNECTOR_TOKEN.test(String(node.connectorToken ?? ""))) {
647
+ throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
648
+ }
649
+ if (expected.mesh) {
650
+ const mesh = node.mesh;
651
+ if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? "")) || !CONNECTOR_TOKEN.test(String(mesh.connectorToken ?? ""))) {
652
+ throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
653
+ }
654
+ } else if (node.mesh !== undefined) {
655
+ throw new Error(`${resolve(path)} contains an undeclared Cloudflare Mesh resource`);
656
+ }
657
+ seen.add(expected.nodeName);
658
+ }
659
+ if (output.phase === "complete" && resources.nodes.length !== coordinates.nodes.length) {
660
+ throw new Error(`${resolve(path)} completed output does not cover the declared node fleet`);
661
+ }
662
+ if (output.created !== undefined) {
663
+ if (!output.created || typeof output.created !== "object" || Array.isArray(output.created) || Object.keys(output.created).some((key) => key !== "nodes") || !Array.isArray(output.created.nodes) || output.created.nodes.some((node) => !node || typeof node !== "object" || Array.isArray(node) || Object.keys(node).some((key) => !["nodeName", "tunnel", "mesh"].includes(key)) || typeof node.nodeName !== "string" || typeof node.tunnel !== "boolean" || typeof node.mesh !== "boolean")) {
664
+ throw new Error(`${resolve(path)} has malformed Cloudflare creation evidence`);
665
+ }
666
+ }
667
+ output.coordinates = coordinates;
381
668
  return output;
382
669
  }
383
670
  function cloudflareHostHandoffPath(checkpointPath, nodeName) {
@@ -410,7 +697,12 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
410
697
  hostname: resource.hostname,
411
698
  service: resource.service,
412
699
  tunnelId: resource.tunnelId,
413
- connectorToken: resource.connectorToken
700
+ connectorToken: resource.connectorToken,
701
+ ...resource.mesh ? { mesh: {
702
+ connectorId: resource.mesh.connectorId,
703
+ connectorToken: resource.mesh.connectorToken,
704
+ routes: resource.mesh.routes
705
+ } } : {}
414
706
  };
415
707
  }
416
708
  async function readCloudflareHostHandoff(handoffPath, nodeName) {
@@ -436,9 +728,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
436
728
  "accountId",
437
729
  "zoneId",
438
730
  "kvNamespaceId",
439
- "kvRuntimeToken",
440
- "privateNetworkRuntimeToken",
441
- "warp"
731
+ "apiToken",
732
+ "mesh",
733
+ "realtime"
442
734
  ].includes(key));
443
735
  if (unknown.length)
444
736
  throw new Error(`Cloudflare host handoff contains unsupported field ${unknown[0]}`);
@@ -447,17 +739,53 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
447
739
  try {
448
740
  service = normalizeService(output.service ?? "");
449
741
  } catch {}
450
- if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !CONNECTOR_TOKEN.test(output.connectorToken ?? "") || !TOKEN.test(output.kvRuntimeToken ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
742
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !CONNECTOR_TOKEN.test(output.connectorToken ?? "") || !TOKEN.test(output.apiToken ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
451
743
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
452
744
  }
453
- if (output.privateNetworkRuntimeToken !== undefined && !TOKEN.test(output.privateNetworkRuntimeToken)) {
454
- throw new Error("Cloudflare host handoff private-network capability is malformed");
745
+ if (output.mesh !== undefined) {
746
+ if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !CONNECTOR_TOKEN.test(output.mesh.connectorToken) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
747
+ try {
748
+ return privateMeshCidr(route) !== route;
749
+ } catch {
750
+ return true;
751
+ }
752
+ })) {
753
+ throw new Error("Cloudflare host handoff contains malformed Mesh coordinates");
754
+ }
455
755
  }
456
- if (Boolean(output.warp) !== Boolean(output.privateNetworkRuntimeToken)) {
457
- throw new Error("Cloudflare host handoff private-network resources and capability disagree");
756
+ if (output.realtime !== undefined) {
757
+ let realtime;
758
+ try {
759
+ const { workerScriptName, endpoint: endpoint2, producer } = output.realtime;
760
+ realtime = validateCloudflareBootstrapCoordinates({
761
+ accountId: output.accountId,
762
+ zoneId: output.zoneId,
763
+ kvNamespaceId: output.kvNamespaceId,
764
+ realtime: { workerScriptName, endpoint: endpoint2, producer },
765
+ nodes: [{
766
+ nodeName: output.nodeName,
767
+ hostname: output.hostname,
768
+ service: output.service,
769
+ tunnelName: "handoff"
770
+ }]
771
+ }).realtime;
772
+ } catch {
773
+ throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
774
+ }
775
+ if (!REALTIME_SECRET.test(output.realtime.publishSecret) || !REALTIME_SECRET.test(output.realtime.ticketSecret) || output.realtime.publishSecret === output.realtime.ticketSecret || realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
776
+ throw new Error("Cloudflare host handoff contains malformed realtime credentials");
777
+ }
458
778
  }
459
779
  const { format: _format, kind: _kind, ...handoff } = output;
460
- return handoff;
780
+ return {
781
+ ...handoff,
782
+ nodeName: normalizedNodeName,
783
+ hostname: handoff.hostname.toLowerCase(),
784
+ service,
785
+ accountId: handoff.accountId.toLowerCase(),
786
+ zoneId: handoff.zoneId.toLowerCase(),
787
+ kvNamespaceId: handoff.kvNamespaceId.toLowerCase()
788
+ };
461
789
  }
462
790
  async function prepareOwnerOutputDirectory(absolutePath) {
463
791
  const directory = dirname(absolutePath);
@@ -513,18 +841,22 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
513
841
  accountId: output.coordinates.accountId,
514
842
  zoneId: output.coordinates.zoneId,
515
843
  kvNamespaceId: output.resources.kvNamespaceId,
516
- kvRuntimeToken: output.resources.kvRuntimeToken
844
+ apiToken: output.resources.apiToken,
845
+ ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
846
+ ...output.coordinates.realtime,
847
+ ...output.resources.realtime
848
+ } } : {},
849
+ ...node.mesh ? { mesh: {
850
+ connectorId: node.mesh.connectorId,
851
+ connectorToken: node.mesh.connectorToken,
852
+ routes: node.mesh.routes
853
+ } } : {}
517
854
  };
518
855
  await writeOwnerJson(cloudflareHostHandoffPath(checkpointPath, node.nodeName), handoff);
519
856
  }
520
857
  }
521
- var tokenFor = (tokens, key) => {
522
- const token = tokens[key]?.trim() || tokens.apiToken?.trim();
523
- if (!token || !TOKEN.test(token))
524
- throw new Error(`Cloudflare ${key} is not configured`);
525
- return token;
526
- };
527
858
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
859
+ var newRealtimeSecret = () => randomBytes(48).toString("base64url");
528
860
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
529
861
  const coordinates = validateCloudflareBootstrapCoordinates(input);
530
862
  const absoluteOutput = resolve(outputPath);
@@ -533,7 +865,46 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
533
865
  throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
534
866
  }
535
867
  await prepareOwnerOutputDirectory(absoluteOutput);
536
- const kvRuntimeToken = tokenFor(tokens, "kvApiToken");
868
+ const tunnelToken = tokens.tunnelToken.trim();
869
+ const apiToken = tokens.apiToken.trim();
870
+ if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken)) {
871
+ throw new Error("Cloudflare bootstrap tokens are malformed");
872
+ }
873
+ if (tunnelToken === apiToken) {
874
+ throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
875
+ }
876
+ await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
877
+ const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
878
+ publishSecret: newRealtimeSecret(),
879
+ ticketSecret: newRealtimeSecret()
880
+ } : undefined;
881
+ if (coordinates.realtime && realtime) {
882
+ await verifyCloudflareWorkerDurableObjects({
883
+ accountId: coordinates.accountId,
884
+ scriptName: coordinates.realtime.workerScriptName,
885
+ apiToken
886
+ }, fetcher);
887
+ await writeOwnerBootstrapOutput(absoluteOutput, {
888
+ format: 1,
889
+ kind: "forgezero-cloudflare-bootstrap",
890
+ phase: "edge-resources-provisioned",
891
+ updatedAt: new Date().toISOString(),
892
+ coordinates,
893
+ resources: {
894
+ kvNamespaceId: coordinates.kvNamespaceId,
895
+ apiToken,
896
+ realtime,
897
+ nodes: existing?.resources.nodes ?? []
898
+ }
899
+ });
900
+ await configureCloudflareRealtimeSecrets({
901
+ accountId: coordinates.accountId,
902
+ scriptName: coordinates.realtime.workerScriptName,
903
+ publishSecret: realtime.publishSecret,
904
+ ticketSecret: realtime.ticketSecret,
905
+ apiToken
906
+ }, fetcher);
907
+ }
537
908
  const nodeResources = [];
538
909
  const createdNodes = [];
539
910
  for (const node of coordinates.nodes) {
@@ -549,14 +920,33 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
549
920
  const tunnel = await ensureCloudflareTunnel({
550
921
  accountId: coordinates.accountId,
551
922
  name: node.tunnelName,
552
- apiToken: tokenFor(tokens, "tunnelApiToken")
923
+ apiToken: tunnelToken
553
924
  }, fetcher);
554
925
  created = tunnel.created;
555
- resource = { ...node, tunnelId: tunnel.tunnel.id, connectorToken: tunnel.connectorToken };
926
+ let mesh;
927
+ if (node.mesh) {
928
+ const ensured = await ensureCloudflareMeshConnector({
929
+ accountId: coordinates.accountId,
930
+ name: node.mesh.connectorName,
931
+ highAvailability: node.mesh.highAvailability === true,
932
+ apiToken: tunnelToken
933
+ }, fetcher);
934
+ mesh = {
935
+ ...node.mesh,
936
+ connectorId: ensured.connector.id,
937
+ connectorToken: ensured.connectorToken
938
+ };
939
+ }
940
+ const { mesh: _declaredMesh, ...publicNode } = node;
941
+ resource = {
942
+ ...publicNode,
943
+ tunnelId: tunnel.tunnel.id,
944
+ connectorToken: tunnel.connectorToken,
945
+ ...mesh ? { mesh } : {}
946
+ };
556
947
  }
557
948
  nodeResources.push(resource);
558
- createdNodes.push({ nodeName: node.nodeName, tunnel: created });
559
- const first2 = nodeResources[0];
949
+ createdNodes.push({ nodeName: node.nodeName, tunnel: created, mesh: Boolean(resource.mesh) });
560
950
  await writeOwnerBootstrapOutput(absoluteOutput, {
561
951
  format: 1,
562
952
  kind: "forgezero-cloudflare-bootstrap",
@@ -564,16 +954,34 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
564
954
  updatedAt: new Date().toISOString(),
565
955
  coordinates,
566
956
  resources: {
567
- tunnelId: first2.tunnelId,
568
957
  kvNamespaceId: coordinates.kvNamespaceId,
569
- hostname: first2.hostname,
570
- service: first2.service,
571
- connectorToken: first2.connectorToken,
572
- kvRuntimeToken,
958
+ apiToken,
959
+ ...realtime ? { realtime } : {},
573
960
  nodes: [...nodeResources]
574
961
  }
575
962
  });
576
963
  }
964
+ const meshResources = nodeResources.filter((node) => node.mesh);
965
+ for (const node of meshResources) {
966
+ for (const network of node.mesh.routes) {
967
+ await ensureCloudflarePrivateRoute({
968
+ accountId: coordinates.accountId,
969
+ tunnelId: node.mesh.connectorId,
970
+ network,
971
+ comment: `ForgeZero Mesh ${node.nodeName}`,
972
+ apiToken: tunnelToken
973
+ }, fetcher);
974
+ }
975
+ }
976
+ if (meshResources.length) {
977
+ await ensureCloudflareWarpNetworkIncludes({
978
+ accountId: coordinates.accountId,
979
+ policyId: coordinates.meshDevicePolicyId,
980
+ networks: meshResources.flatMap((node) => node.mesh.routes),
981
+ descriptionPrefix: "ForgeZero Mesh",
982
+ apiToken: tunnelToken
983
+ }, fetcher);
984
+ }
577
985
  for (const node of nodeResources) {
578
986
  await configureCloudflareEdge({
579
987
  accountId: coordinates.accountId,
@@ -581,12 +989,9 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
581
989
  tunnelId: node.tunnelId,
582
990
  hostname: node.hostname,
583
991
  service: node.service,
584
- apiToken: tokenFor(tokens, "tunnelApiToken"),
585
- tunnelApiToken: tokenFor(tokens, "tunnelApiToken"),
586
- dnsApiToken: tokenFor(tokens, "dnsApiToken")
992
+ apiToken: tunnelToken
587
993
  }, fetcher);
588
994
  }
589
- const first = nodeResources[0];
590
995
  const output = {
591
996
  format: 1,
592
997
  kind: "forgezero-cloudflare-bootstrap",
@@ -594,16 +999,12 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
594
999
  updatedAt: new Date().toISOString(),
595
1000
  coordinates,
596
1001
  resources: {
597
- tunnelId: first.tunnelId,
598
1002
  kvNamespaceId: coordinates.kvNamespaceId,
599
- hostname: first.hostname,
600
- service: first.service,
601
- connectorToken: first.connectorToken,
602
- kvRuntimeToken,
1003
+ apiToken,
1004
+ ...realtime ? { realtime } : {},
603
1005
  nodes: nodeResources
604
1006
  },
605
1007
  created: {
606
- tunnel: createdNodes.some(({ tunnel }) => tunnel),
607
1008
  nodes: createdNodes
608
1009
  }
609
1010
  };
@@ -622,8 +1023,8 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
622
1023
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
623
1024
  };
624
1025
  }
625
- if (!request.tokenFiles || !Object.values(request.tokenFiles).some(Boolean)) {
626
- throw new Error("Cloudflare apply requires owner-only management token file paths");
1026
+ if (!request.tokenFiles) {
1027
+ throw new Error("Cloudflare apply requires exactly two owner-only token file paths");
627
1028
  }
628
1029
  const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
629
1030
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
@@ -680,6 +1081,100 @@ async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fet
680
1081
  nodes
681
1082
  };
682
1083
  }
1084
+ async function readCloudflareBootstrapAcceptanceEvidence(acceptancePath, expectedNodes) {
1085
+ let parsed;
1086
+ try {
1087
+ parsed = JSON.parse(await readOwnerOnlyFile(acceptancePath, 65536));
1088
+ } catch (cause) {
1089
+ if (cause instanceof SyntaxError)
1090
+ throw new Error("Cloudflare acceptance evidence is not valid JSON");
1091
+ throw cause;
1092
+ }
1093
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1094
+ throw new Error("Cloudflare acceptance evidence is malformed");
1095
+ }
1096
+ const evidence = parsed;
1097
+ if (Object.keys(evidence).some((key) => ![
1098
+ "format",
1099
+ "kind",
1100
+ "checkpointFile",
1101
+ "verifiedAt",
1102
+ "nodes"
1103
+ ].includes(key)) || evidence.format !== 1 || evidence.kind !== "forgezero-cloudflare-bootstrap-acceptance" || typeof evidence.checkpointFile !== "string" || !evidence.checkpointFile || typeof evidence.verifiedAt !== "string" || Number.isNaN(Date.parse(evidence.verifiedAt)) || !Array.isArray(evidence.nodes) || evidence.nodes.length < 1 || evidence.nodes.length > 32) {
1104
+ throw new Error("Cloudflare acceptance evidence is malformed");
1105
+ }
1106
+ const nodes = evidence.nodes.map((item) => {
1107
+ if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !["nodeName", "hostname", "status"].includes(key))) {
1108
+ throw new Error("Cloudflare acceptance evidence contains a malformed node");
1109
+ }
1110
+ const node = item;
1111
+ const nodeName = node.nodeName?.trim().toLowerCase();
1112
+ const hostname = node.hostname?.trim().toLowerCase();
1113
+ if (!nodeName || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName) || !hostname || !HOSTNAME.test(hostname) || !Number.isInteger(node.status) || node.status < 200 || node.status > 299) {
1114
+ throw new Error("Cloudflare acceptance evidence contains a malformed node");
1115
+ }
1116
+ return { nodeName, hostname, status: node.status };
1117
+ });
1118
+ if (new Set(nodes.map(({ nodeName }) => nodeName)).size !== nodes.length || new Set(nodes.map(({ hostname }) => hostname)).size !== nodes.length) {
1119
+ throw new Error("Cloudflare acceptance evidence contains duplicate nodes");
1120
+ }
1121
+ if (expectedNodes) {
1122
+ const expected = expectedNodes.map(({ nodeName, hostname }) => ({
1123
+ nodeName: nodeName.trim().toLowerCase(),
1124
+ hostname: hostname.trim().toLowerCase()
1125
+ })).sort((left, right) => left.nodeName.localeCompare(right.nodeName));
1126
+ const observed = nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname })).sort((left, right) => left.nodeName.localeCompare(right.nodeName));
1127
+ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
1128
+ throw new Error("Cloudflare acceptance evidence does not cover the declared platform fleet");
1129
+ }
1130
+ }
1131
+ return {
1132
+ format: 1,
1133
+ kind: "forgezero-cloudflare-bootstrap-acceptance",
1134
+ checkpointFile: evidence.checkpointFile,
1135
+ verifiedAt: evidence.verifiedAt,
1136
+ nodes
1137
+ };
1138
+ }
1139
+ async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
1140
+ const directory = `${resolve(checkpointPath)}.hosts`;
1141
+ let entries;
1142
+ try {
1143
+ const metadata = await lstat(directory);
1144
+ const uid = ownerUid();
1145
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
1146
+ throw new Error(`Cloudflare host handoff directory ${directory} is not owner-only`);
1147
+ }
1148
+ entries = await readdir(directory);
1149
+ } catch (cause) {
1150
+ if (cause.code !== "ENOENT")
1151
+ throw cause;
1152
+ }
1153
+ if (entries) {
1154
+ const expected = output.coordinates.nodes.map(({ nodeName }) => `${nodeName}.json`).sort();
1155
+ if (JSON.stringify([...entries].sort()) !== JSON.stringify(expected)) {
1156
+ throw new Error("Cloudflare host handoff directory contains unexpected files; refusing secret cleanup");
1157
+ }
1158
+ for (const name of expected)
1159
+ await unlink(join(directory, name));
1160
+ await rmdir(directory);
1161
+ }
1162
+ await unlink(resolve(checkpointPath));
1163
+ }
1164
+ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1165
+ const checkpointPath = resolve(request.checkpointPath);
1166
+ const acceptancePath = resolve(request.acceptancePath);
1167
+ if (acceptancePath === checkpointPath || acceptancePath.startsWith(`${checkpointPath}.hosts/`)) {
1168
+ throw new Error("Cloudflare acceptance evidence must be outside the secret checkpoint and handoff directory");
1169
+ }
1170
+ const output = await readExistingOutput(checkpointPath);
1171
+ if (!output || output.phase !== "complete")
1172
+ throw new Error("Cloudflare finalization requires a completed owner checkpoint");
1173
+ const evidence = await verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher);
1174
+ await writeOwnerJson(acceptancePath, evidence);
1175
+ await removeCloudflareBootstrapSecrets(checkpointPath, output);
1176
+ return evidence;
1177
+ }
683
1178
  export {
684
1179
  writeOwnerBootstrapOutput,
685
1180
  verifyCloudflareBootstrapAcceptance,
@@ -689,7 +1184,9 @@ export {
689
1184
  readCloudflareHostHandoff,
690
1185
  readCloudflareConnectorHandoff,
691
1186
  readCloudflareBootstrapTokens,
1187
+ readCloudflareBootstrapAcceptanceEvidence,
692
1188
  planCloudflareBootstrap,
1189
+ finalizeCloudflareBootstrapAcceptance,
693
1190
  cloudflareHostHandoffPath,
694
1191
  applyCloudflareBootstrap
695
1192
  };