@forgezero/agent 0.1.57 → 0.1.59

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/bootstrap.js CHANGED
@@ -22,7 +22,7 @@ function privateDatabaseHostRoute(value) {
22
22
  return `${address}/${isIP(address) === 4 ? 32 : 128}`;
23
23
  }
24
24
  var endpoint = "https://api.cloudflare.com/client/v4";
25
- async function cf(config, path, init = {}, fetcher = fetch) {
25
+ async function cfEnvelope(config, path, init = {}, fetcher = fetch) {
26
26
  const response = await fetcher(`${endpoint}${path}`, {
27
27
  ...init,
28
28
  headers: {
@@ -35,7 +35,68 @@ async function cf(config, path, init = {}, fetcher = fetch) {
35
35
  if (!response.ok || body.success !== true) {
36
36
  throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
37
37
  }
38
- return body.result;
38
+ return body;
39
+ }
40
+ async function cf(config, path, init = {}, fetcher = fetch) {
41
+ return (await cfEnvelope(config, path, init, fetcher)).result;
42
+ }
43
+ async function cfPages(config, path, perPage, fetcher) {
44
+ const output = [];
45
+ for (let page = 1;page <= 100; page += 1) {
46
+ const separator = path.includes("?") ? "&" : "?";
47
+ const envelope = await cfEnvelope(config, `${path}${separator}page=${page}&per_page=${perPage}`, {}, fetcher);
48
+ const result = Array.isArray(envelope.result) ? envelope.result : [];
49
+ output.push(...result);
50
+ const totalPages = envelope.result_info?.total_pages;
51
+ if (Number.isInteger(totalPages) ? page >= totalPages : result.length < perPage)
52
+ return output;
53
+ }
54
+ throw new Error("Cloudflare pagination exceeded the reviewed 100-page bound");
55
+ }
56
+ var exactHexId = (value, label) => {
57
+ const normalized = String(value ?? "").trim().toLowerCase();
58
+ if (!/^[a-f0-9]{32}$/.test(normalized))
59
+ throw new Error(`${label} is malformed`);
60
+ return normalized;
61
+ };
62
+ async function discoverCloudflareBootstrapResources(config, fetcher = fetch) {
63
+ const zoneName = config.zoneName.trim().toLowerCase().replace(/\.$/, "");
64
+ const kvNamespaceTitle = config.kvNamespaceTitle.trim();
65
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(zoneName)) {
66
+ throw new Error("Cloudflare zone name is invalid");
67
+ }
68
+ if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
69
+ throw new Error("Cloudflare KV namespace title is invalid");
70
+ }
71
+ const [managementStatus, runtimeStatus] = await Promise.all([
72
+ cf({ apiToken: config.tunnelToken }, "/user/tokens/verify", {}, fetcher),
73
+ cf({ apiToken: config.apiToken }, "/user/tokens/verify", {}, fetcher)
74
+ ]);
75
+ if (managementStatus.status !== "active")
76
+ throw new Error("CF_TUNNEL_TOKEN is not active");
77
+ if (runtimeStatus.status !== "active")
78
+ throw new Error("CF_API_TOKEN is not active");
79
+ const zones = await cfPages({ apiToken: config.tunnelToken }, `/zones?name=${encodeURIComponent(zoneName)}&match=all&status=active`, 50, fetcher);
80
+ const matchingZones = zones.filter(({ name }) => name?.trim().toLowerCase() === zoneName);
81
+ if (matchingZones.length !== 1) {
82
+ throw new Error(`Cloudflare zone ${zoneName} must resolve to exactly one active zone`);
83
+ }
84
+ const zoneId = exactHexId(matchingZones[0].id, "Cloudflare zone id");
85
+ const accountId = exactHexId(matchingZones[0].account?.id, "Cloudflare account id");
86
+ const namespaces = await cfPages({ apiToken: config.apiToken }, `/accounts/${accountId}/storage/kv/namespaces?order=title&direction=asc`, 1000, fetcher);
87
+ const matchingNamespaces = namespaces.filter(({ title }) => title === kvNamespaceTitle);
88
+ if (matchingNamespaces.length !== 1) {
89
+ throw new Error(`Cloudflare KV namespace ${kvNamespaceTitle} must resolve to exactly one namespace`);
90
+ }
91
+ const kvNamespaceId = exactHexId(matchingNamespaces[0].id, "Cloudflare KV namespace id");
92
+ if (config.workerScriptName) {
93
+ await verifyCloudflareWorkerDurableObjects({
94
+ accountId,
95
+ scriptName: config.workerScriptName,
96
+ apiToken: config.apiToken
97
+ }, fetcher);
98
+ }
99
+ return { accountId, zoneId, kvNamespaceId };
39
100
  }
40
101
  async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
41
102
  const [address, prefixText, ...extra] = config.network.split("/");
@@ -211,7 +272,7 @@ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
211
272
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
212
273
  throw new Error("Cloudflare Worker script name is invalid");
213
274
  }
214
- const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
275
+ const namespaces = await cfPages(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces`, 1000, fetcher);
215
276
  const owned = namespaces.filter(({ script }) => script === config.scriptName);
216
277
  if (!owned.length)
217
278
  throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
@@ -283,18 +344,28 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
283
344
  }
284
345
  return { tunnel, connectorToken, created };
285
346
  }
347
+ async function retrieveCloudflareConnectorTokens(config, fetcher = fetch) {
348
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId) || !/^[0-9a-f-]{36}$/i.test(config.tunnelId) || config.meshConnectorId && !/^[0-9a-f-]{36}$/i.test(config.meshConnectorId)) {
349
+ throw new Error("Cloudflare connector retrieval coordinates are invalid");
350
+ }
351
+ const connectorToken = await cf(config, `/accounts/${config.accountId}/cfd_tunnel/${encodeURIComponent(config.tunnelId)}/token`, {}, fetcher);
352
+ const meshConnectorToken = config.meshConnectorId ? await cf(config, `/accounts/${config.accountId}/warp_connector/${encodeURIComponent(config.meshConnectorId)}/token`, {}, fetcher) : undefined;
353
+ for (const value of [connectorToken, meshConnectorToken]) {
354
+ if (value !== undefined && (!value || value.length > 16384))
355
+ throw new Error("Cloudflare returned an invalid connector token");
356
+ }
357
+ return { connectorToken, ...meshConnectorToken ? { meshConnectorToken } : {} };
358
+ }
286
359
 
287
360
  // src/cloudflare-bootstrap.ts
288
361
  import { constants } from "fs";
289
- import { randomBytes, randomUUID } from "crypto";
362
+ import { createHmac, randomUUID } from "crypto";
290
363
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
291
364
  import { dirname, join, resolve } from "path";
292
365
  import { isIP as isIP2 } from "net";
293
366
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
294
- var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
295
367
  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;
296
368
  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}$/;
298
369
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
299
370
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
300
371
  const metadata = await handle.stat();
@@ -328,26 +399,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
328
399
  await handle?.close();
329
400
  }
330
401
  }
331
- async function readOwnerApiToken(path) {
332
- const token = (await readOwnerOnlyFile(path, 4096)).trim();
333
- if (!TOKEN.test(token))
334
- throw new Error(`${resolve(path)} must contain exactly one Cloudflare API token`);
335
- return token;
336
- }
337
- async function readCloudflareBootstrapTokens(files) {
338
- const unsupported = Object.keys(files).filter((key) => ![
339
- "tunnelTokenFile",
340
- "apiTokenFile"
341
- ].includes(key));
402
+ function validateCloudflareBootstrapTokens(input) {
403
+ if (!input || typeof input !== "object" || Array.isArray(input))
404
+ throw new Error("Cloudflare bootstrap tokens must be an object");
405
+ const source = input;
406
+ const unsupported = Object.keys(source).filter((key) => !["tunnelToken", "apiToken"].includes(key));
342
407
  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");
346
- }
347
- const [tunnelToken, apiToken] = await Promise.all([
348
- readOwnerApiToken(files.tunnelTokenFile),
349
- readOwnerApiToken(files.apiTokenFile)
350
- ]);
408
+ throw new Error(`Cloudflare bootstrap tokens contain unsupported field ${unsupported[0]}`);
409
+ const tunnelToken = typeof source.tunnelToken === "string" ? source.tunnelToken.trim() : "";
410
+ const apiToken = typeof source.apiToken === "string" ? source.apiToken.trim() : "";
411
+ if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken))
412
+ throw new Error("Cloudflare bootstrap requires two valid API tokens");
351
413
  if (tunnelToken === apiToken) {
352
414
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
353
415
  }
@@ -523,7 +585,7 @@ function planCloudflareBootstrap(input, outputPath) {
523
585
  return {
524
586
  format: 1,
525
587
  kind: "forgezero-cloudflare-bootstrap-plan",
526
- mode: "attended-token-file",
588
+ mode: "attended-hidden-input",
527
589
  outputFile: resolve(outputPath),
528
590
  coordinates,
529
591
  operations: [
@@ -531,19 +593,19 @@ function planCloudflareBootstrap(input, outputPath) {
531
593
  ...coordinates.realtime ? [
532
594
  "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
533
595
  ] : [],
534
- "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
596
+ "create or reuse one remotely-managed Tunnel per node and checkpoint only its non-secret id",
535
597
  ...coordinates.nodes.some(({ mesh }) => mesh) ? [
536
- "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its registration token",
598
+ "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint only its non-secret id",
537
599
  "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
538
600
  ] : [],
539
601
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
540
602
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
541
- "write one node-specific handoff containing the connector token and owner-supplied KV/Worker runtime token"
603
+ "write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
542
604
  ],
543
605
  secrets: [
544
- "API tokens are read only from owner-only files and are never placed in argv or stdout",
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"
606
+ "API tokens are accepted only from a hidden prompt and are never placed in JSON, files, argv or stdout",
607
+ "checkpoint and host handoffs contain no connector, management, runtime or realtime secret",
608
+ "the target Agent retrieves its connector tokens and seals both Vault fallback tokens directly"
547
609
  ]
548
610
  };
549
611
  }
@@ -608,25 +670,15 @@ async function readExistingOutput(path) {
608
670
  const resources = output.resources;
609
671
  const unsupportedResource = Object.keys(resources).filter((key) => ![
610
672
  "kvNamespaceId",
611
- "apiToken",
612
- "realtime",
613
673
  "nodes"
614
674
  ].includes(key));
615
675
  if (unsupportedResource.length) {
616
676
  throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
617
677
  }
618
678
  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) {
679
+ if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
620
680
  throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
621
681
  }
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
682
  const seen = new Set;
631
683
  for (const item of resources.nodes) {
632
684
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -639,16 +691,15 @@ async function readExistingOutput(path) {
639
691
  "service",
640
692
  "tunnelName",
641
693
  "tunnelId",
642
- "connectorToken",
643
694
  "mesh"
644
695
  ].includes(key));
645
696
  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 ?? ""))) {
697
+ 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 ?? ""))) {
647
698
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
648
699
  }
649
700
  if (expected.mesh) {
650
701
  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 ?? ""))) {
702
+ 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 ?? ""))) {
652
703
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
653
704
  }
654
705
  } else if (node.mesh !== undefined) {
@@ -689,7 +740,7 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
689
740
  throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
690
741
  }
691
742
  const resource = matches[0];
692
- if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId) || !CONNECTOR_TOKEN.test(resource.connectorToken)) {
743
+ if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId)) {
693
744
  throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
694
745
  }
695
746
  return {
@@ -697,10 +748,8 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
697
748
  hostname: resource.hostname,
698
749
  service: resource.service,
699
750
  tunnelId: resource.tunnelId,
700
- connectorToken: resource.connectorToken,
701
751
  ...resource.mesh ? { mesh: {
702
752
  connectorId: resource.mesh.connectorId,
703
- connectorToken: resource.mesh.connectorToken,
704
753
  routes: resource.mesh.routes
705
754
  } } : {}
706
755
  };
@@ -724,11 +773,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
724
773
  "hostname",
725
774
  "service",
726
775
  "tunnelId",
727
- "connectorToken",
728
776
  "accountId",
729
777
  "zoneId",
730
778
  "kvNamespaceId",
731
- "apiToken",
732
779
  "mesh",
733
780
  "realtime"
734
781
  ].includes(key));
@@ -739,11 +786,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
739
786
  try {
740
787
  service = normalizeService(output.service ?? "");
741
788
  } catch {}
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 ?? "")) {
789
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
743
790
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
744
791
  }
745
792
  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) => {
793
+ if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
747
794
  try {
748
795
  return privateMeshCidr(route) !== route;
749
796
  } catch {
@@ -772,7 +819,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
772
819
  } catch {
773
820
  throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
774
821
  }
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) {
822
+ if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
776
823
  throw new Error("Cloudflare host handoff contains malformed realtime credentials");
777
824
  }
778
825
  }
@@ -837,18 +884,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
837
884
  hostname: node.hostname,
838
885
  service: node.service,
839
886
  tunnelId: node.tunnelId,
840
- connectorToken: node.connectorToken,
841
887
  accountId: output.coordinates.accountId,
842
888
  zoneId: output.coordinates.zoneId,
843
889
  kvNamespaceId: output.resources.kvNamespaceId,
844
- apiToken: output.resources.apiToken,
845
- ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
846
- ...output.coordinates.realtime,
847
- ...output.resources.realtime
848
- } } : {},
890
+ ...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
849
891
  ...node.mesh ? { mesh: {
850
892
  connectorId: node.mesh.connectorId,
851
- connectorToken: node.mesh.connectorToken,
852
893
  routes: node.mesh.routes
853
894
  } } : {}
854
895
  };
@@ -856,7 +897,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
856
897
  }
857
898
  }
858
899
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
859
- var newRealtimeSecret = () => randomBytes(48).toString("base64url");
900
+ var deriveCloudflareRealtimeSecrets = (apiToken) => ({
901
+ publishSecret: createHmac("sha512", apiToken).update("forgezero/realtime/publish/v1").digest("base64url"),
902
+ ticketSecret: createHmac("sha512", apiToken).update("forgezero/realtime/ticket/v1").digest("base64url")
903
+ });
860
904
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
861
905
  const coordinates = validateCloudflareBootstrapCoordinates(input);
862
906
  const absoluteOutput = resolve(outputPath);
@@ -874,10 +918,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
874
918
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
875
919
  }
876
920
  await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
877
- const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
878
- publishSecret: newRealtimeSecret(),
879
- ticketSecret: newRealtimeSecret()
880
- } : undefined;
921
+ const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
881
922
  if (coordinates.realtime && realtime) {
882
923
  await verifyCloudflareWorkerDurableObjects({
883
924
  accountId: coordinates.accountId,
@@ -892,8 +933,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
892
933
  coordinates,
893
934
  resources: {
894
935
  kvNamespaceId: coordinates.kvNamespaceId,
895
- apiToken,
896
- realtime,
897
936
  nodes: existing?.resources.nodes ?? []
898
937
  }
899
938
  });
@@ -912,7 +951,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
912
951
  let resource;
913
952
  let created = false;
914
953
  if (checkpointed) {
915
- if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId) || !CONNECTOR_TOKEN.test(checkpointed.connectorToken)) {
954
+ if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)) {
916
955
  throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
917
956
  }
918
957
  resource = checkpointed;
@@ -933,15 +972,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
933
972
  }, fetcher);
934
973
  mesh = {
935
974
  ...node.mesh,
936
- connectorId: ensured.connector.id,
937
- connectorToken: ensured.connectorToken
975
+ connectorId: ensured.connector.id
938
976
  };
939
977
  }
940
978
  const { mesh: _declaredMesh, ...publicNode } = node;
941
979
  resource = {
942
980
  ...publicNode,
943
981
  tunnelId: tunnel.tunnel.id,
944
- connectorToken: tunnel.connectorToken,
945
982
  ...mesh ? { mesh } : {}
946
983
  };
947
984
  }
@@ -955,8 +992,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
955
992
  coordinates,
956
993
  resources: {
957
994
  kvNamespaceId: coordinates.kvNamespaceId,
958
- apiToken,
959
- ...realtime ? { realtime } : {},
960
995
  nodes: [...nodeResources]
961
996
  }
962
997
  });
@@ -1000,8 +1035,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
1000
1035
  coordinates,
1001
1036
  resources: {
1002
1037
  kvNamespaceId: coordinates.kvNamespaceId,
1003
- apiToken,
1004
- ...realtime ? { realtime } : {},
1005
1038
  nodes: nodeResources
1006
1039
  },
1007
1040
  created: {
@@ -1023,10 +1056,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
1023
1056
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
1024
1057
  };
1025
1058
  }
1026
- if (!request.tokenFiles) {
1027
- throw new Error("Cloudflare apply requires exactly two owner-only token file paths");
1059
+ if (!request.tokens) {
1060
+ throw new Error("Cloudflare apply requires exactly two attended API tokens");
1028
1061
  }
1029
- const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
1062
+ const tokens = validateCloudflareBootstrapTokens(request.tokens);
1030
1063
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
1031
1064
  return {
1032
1065
  format: 1,
@@ -1177,7 +1210,7 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1177
1210
  }
1178
1211
 
1179
1212
  // src/bootstrap.ts
1180
- import { createHash, createHmac, randomBytes as randomBytes3 } from "crypto";
1213
+ import { createHash, createHmac as createHmac2, randomBytes as randomBytes2 } from "crypto";
1181
1214
  import {
1182
1215
  chmodSync as chmodSync2,
1183
1216
  existsSync as existsSync4,
@@ -1207,7 +1240,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1207
1240
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1208
1241
 
1209
1242
  // src/version.ts
1210
- var VERSION = "0.1.57";
1243
+ var VERSION = "0.1.59";
1211
1244
 
1212
1245
  // src/software.ts
1213
1246
  var PINNED_BUN_VERSION = "1.3.14";
@@ -2117,10 +2150,11 @@ function planProvision(options) {
2117
2150
  const warpEnabled = warpValues.every(Boolean);
2118
2151
  if (warpValues.some(Boolean) && !warpEnabled)
2119
2152
  throw new Error("WARP configuration must be supplied together");
2120
- const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
2121
- if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
2122
- throw new Error("direct enrolment paths must be supplied together");
2123
- const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
2153
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
2154
+ if (Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath) || options.enrolTokenSourcePath && !enrolmentEnabled) {
2155
+ throw new Error("direct enrolment credential and state paths must be supplied together");
2156
+ }
2157
+ const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
2124
2158
  const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
2125
2159
  const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
2126
2160
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
@@ -2248,7 +2282,12 @@ function planProvision(options) {
2248
2282
  ] : [],
2249
2283
  ...enrolmentEnabled ? [
2250
2284
  step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
2251
- step("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
2285
+ ...enrolTokenSourcePath ? [step("encrypted one-time enrolment capability", {
2286
+ kind: "ensure-enrolment",
2287
+ state: enrolStatePath,
2288
+ source: enrolTokenSourcePath,
2289
+ credential: enrolTokenCredentialPath
2290
+ })] : []
2252
2291
  ] : [],
2253
2292
  ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
2254
2293
  { path: deployRoot, mode: 493, owner: "root", group: "root" },
@@ -2292,7 +2331,7 @@ function planProvision(options) {
2292
2331
  }
2293
2332
 
2294
2333
  // src/cli/agent-install.ts
2295
- import { randomBytes as randomBytes2 } from "crypto";
2334
+ import { randomBytes } from "crypto";
2296
2335
  import {
2297
2336
  chmodSync,
2298
2337
  copyFileSync,
@@ -2388,7 +2427,7 @@ var runProvisionOperation = async (operation) => {
2388
2427
  if (operation.kind === "ensure-seed") {
2389
2428
  if (existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
2390
2429
  return { stdout: "", exitCode: 0 };
2391
- const seed = randomBytes2(32).toString("base64url");
2430
+ const seed = randomBytes(32).toString("base64url");
2392
2431
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
2393
2432
  if (result.exitCode === 0)
2394
2433
  chmodSync(operation.credential, 256);
@@ -2781,9 +2820,10 @@ function renderPlatformSharedEnvironment(input) {
2781
2820
  }
2782
2821
  function platformApiCredentialSpecs(options) {
2783
2822
  const optional = [
2784
- ["bootstrap-smtp-password", options.emailProvider === "smtp"],
2785
- ["bootstrap-jetemail-api-key", options.emailProvider === "jetemail"],
2823
+ ["fz_smtp.password", options.emailProvider === "smtp"],
2824
+ ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
2786
2825
  ["CF_API_TOKEN", options.cloudflareKv],
2826
+ ["CF_TUNNEL_TOKEN", options.cloudflareKv],
2787
2827
  ["REALTIME_PUBLISH_SECRET", options.realtime],
2788
2828
  ["REALTIME_TICKET_SECRET", options.realtime]
2789
2829
  ];
@@ -3118,6 +3158,78 @@ var PLATFORM_BOOTSTRAP_PROFILES = [
3118
3158
  "platform-db-api",
3119
3159
  "platform-api"
3120
3160
  ];
3161
+ function validateCloudflareBootstrapSecretPair(input) {
3162
+ if (!input || typeof input !== "object" || Array.isArray(input))
3163
+ throw new Error("Cloudflare credential input must be an object");
3164
+ const source = input;
3165
+ const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : "";
3166
+ const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : "";
3167
+ if (!/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareTunnelToken) || !/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareApiToken) || cloudflareTunnelToken === cloudflareApiToken) {
3168
+ throw new Error("Cloudflare bootstrap credentials must be distinct valid API tokens");
3169
+ }
3170
+ return { cloudflareTunnelToken, cloudflareApiToken };
3171
+ }
3172
+ function validateEnrolledComputeBootstrapSecrets(config, input) {
3173
+ if (!input || typeof input !== "object" || Array.isArray(input))
3174
+ throw new Error("compute activation credential input must be an object");
3175
+ const source = input;
3176
+ const allowed = ["enrolmentToken", "cloudflareTunnelToken", "cloudflareApiToken"];
3177
+ const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3178
+ if (unknown.length)
3179
+ throw new Error(`compute activation credential input contains unsupported field ${unknown[0]}`);
3180
+ const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : "";
3181
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken))
3182
+ throw new Error("compute enrolment token is malformed");
3183
+ const cloudflareConfigured = Boolean(config.cloudflareHandoff);
3184
+ if (cloudflareConfigured !== Boolean(source.cloudflareTunnelToken && source.cloudflareApiToken)) {
3185
+ throw new Error("Cloudflare compute activation requires CF_TUNNEL_TOKEN and CF_API_TOKEN together");
3186
+ }
3187
+ return {
3188
+ enrolmentToken,
3189
+ ...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
3190
+ };
3191
+ }
3192
+ function validatePlatformBootstrapSecrets(config, input) {
3193
+ if (!input || typeof input !== "object" || Array.isArray(input))
3194
+ throw new Error("bootstrap credential input must be an object");
3195
+ const source = input;
3196
+ const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
3197
+ const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
3198
+ if (unknown.length)
3199
+ throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
3200
+ const clusterBootstrapCode = typeof source.clusterBootstrapCode === "string" ? source.clusterBootstrapCode.trim() : "";
3201
+ const emailSecret = typeof source.emailSecret === "string" ? source.emailSecret.trim() : "";
3202
+ const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : undefined;
3203
+ const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
3204
+ const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
3205
+ const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
3206
+ if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
3207
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3208
+ if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
3209
+ throw new Error("bootstrap email credential is malformed");
3210
+ if (config.enrolment.source === "api-token" !== Boolean(enrolmentToken) || enrolmentToken && !/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken)) {
3211
+ throw new Error("platform enrolment source and attended token disagree");
3212
+ }
3213
+ if (Boolean(config.runtime.environment.backup) !== Boolean(backupS3Secret)) {
3214
+ throw new Error("backup configuration and its attended credential must be supplied together");
3215
+ }
3216
+ if (backupS3Secret && (backupS3Secret.length > 16384 || /[\r\n\0]/.test(backupS3Secret))) {
3217
+ throw new Error("backup credential is malformed");
3218
+ }
3219
+ const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
3220
+ if (cloudflareConfigured !== Boolean(cloudflareTunnelToken && cloudflareApiToken)) {
3221
+ throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
3222
+ }
3223
+ if (cloudflareTunnelToken)
3224
+ validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
3225
+ return {
3226
+ clusterBootstrapCode,
3227
+ emailSecret,
3228
+ ...enrolmentToken ? { enrolmentToken } : {},
3229
+ ...backupS3Secret ? { backupS3Secret } : {},
3230
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
3231
+ };
3232
+ }
3121
3233
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
3122
3234
  function resolveInstalledBootstrapKind(states) {
3123
3235
  if (states.compute && states.metal) {
@@ -3133,6 +3245,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
3133
3245
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
3134
3246
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
3135
3247
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
3248
+ var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
3136
3249
  var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
3137
3250
  var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
3138
3251
  var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
@@ -3141,7 +3254,6 @@ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
3141
3254
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
3142
3255
  var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
3143
3256
  var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
3144
- var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
3145
3257
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
3146
3258
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
3147
3259
  var CONTROL_SOCKET = "/run/forgezero/control.sock";
@@ -3219,8 +3331,6 @@ function validateBootstrapConfig(value) {
3219
3331
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
3220
3332
  throw new Error("tenant API must be public HTTPS or loopback HTTP");
3221
3333
  }
3222
- if (!value.enrolTokenFile)
3223
- throw new Error("enrolled-compute activation requires an enrolment-token file");
3224
3334
  if (value.bootstrapRunner) {
3225
3335
  if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
3226
3336
  throw new Error("bootstrap runner SSH private-key file must be absolute");
@@ -3264,8 +3374,6 @@ function validateBootstrapConfig(value) {
3264
3374
  if (!/^[a-z0-9](?:[a-z0-9:_-]{0,126}[a-z0-9])?$/.test(value.computeReference)) {
3265
3375
  throw new Error("platform compute reference is malformed");
3266
3376
  }
3267
- if (!value.database.bootstrapSecretFile)
3268
- throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
3269
3377
  const write = value.database.coordinators.map(privateOrigin);
3270
3378
  if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
3271
3379
  throw new Error("database coordinators must contain 1-16 unique private origins");
@@ -3280,12 +3388,6 @@ function validateBootstrapConfig(value) {
3280
3388
  }
3281
3389
  if (runtime.deployProfile !== value.environment)
3282
3390
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
3283
- if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
3284
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3285
- }
3286
- if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
3287
- throw new Error("Bootstrap email credential file must be an absolute single-line path");
3288
- }
3289
3391
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
3290
3392
  throw new Error("runtime database topology disagrees with bootstrap topology");
3291
3393
  }
@@ -3296,9 +3398,8 @@ function validateBootstrapConfig(value) {
3296
3398
  if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
3297
3399
  throw new Error("enabled firewall requires unique private cluster CIDRs");
3298
3400
  }
3299
- if (!["genesis-derived", "api-token"].includes(value.enrolment.source) || value.enrolment.source === "api-token" && !value.enrolment.tokenFile || value.enrolment.source === "genesis-derived" && value.enrolment.tokenFile) {
3300
- throw new Error("platform enrolment source and token file disagree");
3301
- }
3401
+ if (!["genesis-derived", "api-token"].includes(value.enrolment.source))
3402
+ throw new Error("platform enrolment source is invalid");
3302
3403
  value.runtime.environment = runtime;
3303
3404
  return value;
3304
3405
  }
@@ -3512,7 +3613,7 @@ async function waitForCloudflaredTunnel(host, expectedTunnelId) {
3512
3613
  var derive = (root, label) => {
3513
3614
  if (!/^[a-f0-9]{64}$/i.test(root))
3514
3615
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3515
- return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
3616
+ return createHmac2("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
3516
3617
  };
3517
3618
  async function seal(host, name, destination2, value) {
3518
3619
  if (host.exists(destination2))
@@ -3802,15 +3903,10 @@ async function bootstrapStatus(host = localBootstrapHost()) {
3802
3903
  problems.push("durable Agent enrolment state is missing");
3803
3904
  return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
3804
3905
  }
3805
- async function applyBootstrap(input, host = localBootstrapHost()) {
3906
+ async function applyBootstrap(input, host = localBootstrapHost(), secrets, dependencies = {}) {
3806
3907
  const config = validateBootstrapConfig(structuredClone(input));
3807
3908
  if (host.uid() !== 0)
3808
3909
  throw new Error("fz bootstrap --apply must run as root");
3809
- if (config.kind === "enrolled-compute") {
3810
- const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
3811
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
3812
- throw new Error("tenant enrolment token is malformed");
3813
- }
3814
3910
  let installed;
3815
3911
  if (host.exists(STATE_PATH))
3816
3912
  installed = parseStoredState(host.read(STATE_PATH));
@@ -3868,11 +3964,21 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3868
3964
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
3869
3965
  }
3870
3966
  }
3871
- const platformPrivate = config.kind === "platform" ? {
3872
- root: privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret"),
3873
- email: config.runtime.credentialFiles?.emailSecret && !host.exists(`${CREDS}/${config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : "bootstrap-smtp-password"}.cred`) ? privateFile(host, config.runtime.credentialFiles.emailSecret, "bootstrap email credential") : undefined,
3874
- backup: config.runtime.credentialFiles?.backupS3Secret && !host.exists(`${CREDS}/backup-s3-secret.cred`) ? privateFile(host, config.runtime.credentialFiles.backupS3Secret, "backup S3 credential") : undefined
3875
- } : undefined;
3967
+ const platformPrivate = config.kind === "platform" ? (() => {
3968
+ if (!secrets)
3969
+ throw new Error("platform apply requires attended credentials on stdin");
3970
+ const checked3 = validatePlatformBootstrapSecrets(config, secrets);
3971
+ return {
3972
+ root: checked3.clusterBootstrapCode,
3973
+ email: checked3.emailSecret,
3974
+ enrolmentToken: checked3.enrolmentToken,
3975
+ backup: checked3.backupS3Secret,
3976
+ cloudflareTunnelToken: checked3.cloudflareTunnelToken,
3977
+ cloudflareApiToken: checked3.cloudflareApiToken
3978
+ };
3979
+ })() : undefined;
3980
+ const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
3981
+ const cloudflarePrivate = cloudflare ? config.kind === "platform" ? { cloudflareTunnelToken: platformPrivate.cloudflareTunnelToken, cloudflareApiToken: platformPrivate.cloudflareApiToken } : validateCloudflareBootstrapSecretPair(enrolledPrivate) : undefined;
3876
3982
  if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3877
3983
  privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
3878
3984
  }
@@ -3881,17 +3987,31 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3881
3987
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
3882
3988
  host.mkdir(CREDS, 448);
3883
3989
  host.mkdir("/var/lib/forgezero", 448);
3884
- if (cloudflare && !host.exists(CF_API_CREDENTIAL)) {
3885
- await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflare.apiToken);
3990
+ if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
3991
+ const enrolmentToken = config.kind === "platform" ? config.enrolment.source === "api-token" ? platformPrivate.enrolmentToken : `fze_${derive(derive(platformPrivate.root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}` : enrolledPrivate.enrolmentToken;
3992
+ await seal(host, "enrol-token", ENROL_CREDENTIAL, enrolmentToken);
3886
3993
  }
3887
- if (cloudflare?.realtime && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
3888
- await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, cloudflare.realtime.publishSecret);
3994
+ const connectorCapabilities = cloudflare ? await retrieveCloudflareConnectorTokens({
3995
+ accountId: cloudflare.accountId,
3996
+ tunnelId: cloudflare.tunnelId,
3997
+ ...cloudflare.mesh ? { meshConnectorId: cloudflare.mesh.connectorId } : {},
3998
+ apiToken: cloudflarePrivate.cloudflareTunnelToken
3999
+ }, dependencies.fetcher ?? fetch) : undefined;
4000
+ if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
4001
+ await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
3889
4002
  }
3890
- if (cloudflare?.realtime && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
3891
- await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, cloudflare.realtime.ticketSecret);
4003
+ if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
4004
+ await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
3892
4005
  }
3893
- if (cloudflare?.mesh && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
3894
- await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, cloudflare.mesh.connectorToken);
4006
+ const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
4007
+ if (realtimeSecrets && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
4008
+ await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, realtimeSecrets.publishSecret);
4009
+ }
4010
+ if (realtimeSecrets && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
4011
+ await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, realtimeSecrets.ticketSecret);
4012
+ }
4013
+ if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
4014
+ await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
3895
4015
  }
3896
4016
  await host.installAgent(config);
3897
4017
  if (config.kind === "platform" && config.firewall.enabled) {
@@ -3918,22 +4038,15 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3918
4038
  }
3919
4039
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
3920
4040
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
3921
- const credentialFiles = config.runtime.credentialFiles ?? {};
3922
- const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
3923
- if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
3924
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3925
- }
4041
+ const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
3926
4042
  for (const [name, source] of Object.entries({
3927
4043
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
3928
- "backup-s3-secret": platformPrivate.backup
4044
+ "backup.s3.secretAccessKey": platformPrivate.backup
3929
4045
  })) {
3930
4046
  if (source) {
3931
4047
  const destination2 = `${CREDS}/${name}.cred`;
3932
4048
  if (!host.exists(destination2)) {
3933
4049
  await seal(host, name, destination2, source);
3934
- const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
3935
- if (sourcePath)
3936
- host.remove(sourcePath);
3937
4050
  }
3938
4051
  }
3939
4052
  }
@@ -4003,7 +4116,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4003
4116
  if (config.database.role === "master") {
4004
4117
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
4005
4118
  if (!host.exists(invite)) {
4006
- host.write(invite, `plt_${randomBytes3(24).toString("hex")}
4119
+ host.write(invite, `plt_${randomBytes2(24).toString("hex")}
4007
4120
  `, 384);
4008
4121
  await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
4009
4122
  }
@@ -4023,13 +4136,6 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4023
4136
  verifiedAt: new Date().toISOString()
4024
4137
  };
4025
4138
  host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
4026
- `, 384);
4027
- }
4028
- if (!alreadyEnrolled) {
4029
- const enrolToken = config.enrolment.source === "api-token" ? privateFile(host, config.enrolment.tokenFile, "platform enrolment token") : `fze_${derive(derive(root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}`;
4030
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
4031
- throw new Error("platform enrolment token is malformed");
4032
- host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
4033
4139
  `, 384);
4034
4140
  }
4035
4141
  await checked2(host, [
@@ -4041,15 +4147,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4041
4147
  "deploy",
4042
4148
  ...config.database.role === "master" ? ["--release-executor"] : []
4043
4149
  ], "initial Agent deployment");
4044
- if (!alreadyEnrolled) {
4045
- await host.installAgent(config, PLATFORM_ENROL_SOURCE);
4046
- if (config.enrolment.source === "api-token")
4047
- host.remove(config.enrolment.tokenFile);
4048
- }
4049
4150
  }
4050
4151
  if (config.cloudflareHandoff) {
4051
- if (!host.exists(TUNNEL_CREDENTIAL) && cloudflare) {
4052
- await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
4152
+ if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
4153
+ await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, connectorCapabilities.connectorToken);
4053
4154
  }
4054
4155
  if (!host.exists(TUNNEL_CREDENTIAL))
4055
4156
  throw new Error("sealed cloudflared connector credential is missing");
@@ -4123,7 +4224,6 @@ function strictBootstrapDocument(value) {
4123
4224
  "installWarp",
4124
4225
  "cloudflareHandoff",
4125
4226
  "realm",
4126
- "enrolTokenFile",
4127
4227
  "software",
4128
4228
  "deploymentCredentials",
4129
4229
  "bootstrapRunner"
@@ -4132,8 +4232,8 @@ function strictBootstrapDocument(value) {
4132
4232
  exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
4133
4233
  if (root.cloudflareHandoff !== undefined)
4134
4234
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
4135
- exactKeys(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
4136
- exactKeys(root.enrolment, ["source", "tokenFile"], "platform enrolment config");
4235
+ exactKeys(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
4236
+ exactKeys(root.enrolment, ["source"], "platform enrolment config");
4137
4237
  const runtime = exactKeys(root.runtime, [
4138
4238
  "environment",
4139
4239
  "serviceUser",
@@ -4141,8 +4241,7 @@ function strictBootstrapDocument(value) {
4141
4241
  "bluePort",
4142
4242
  "greenPort",
4143
4243
  "healthPath",
4144
- "keepReleases",
4145
- "credentialFiles"
4244
+ "keepReleases"
4146
4245
  ], "runtime config");
4147
4246
  exactKeys(runtime.environment, [
4148
4247
  "softwareProfile",
@@ -4180,8 +4279,6 @@ function strictBootstrapDocument(value) {
4180
4279
  "cloudflare",
4181
4280
  "realtime"
4182
4281
  ], "runtime environment");
4183
- if (runtime.credentialFiles !== undefined)
4184
- exactKeys(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
4185
4282
  const environment = runtime.environment;
4186
4283
  if (environment.email !== undefined) {
4187
4284
  const email = exactKeys(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
@@ -4265,10 +4362,10 @@ function localBootstrapHost() {
4265
4362
  throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
4266
4363
  return result;
4267
4364
  },
4268
- async installAgent(config, enrolTokenSourcePath) {
4365
+ async installAgent(config) {
4269
4366
  const capabilities = await readCapabilities(localRunner);
4270
4367
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
4271
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync4("/var/lib/forgezero/enrolment.json");
4368
+ const hasBinding = config.kind === "enrolled-compute" || existsSync4(ENROL_CREDENTIAL) || existsSync4("/var/lib/forgezero/enrolment.json");
4272
4369
  if (config.kind === "platform") {
4273
4370
  const lifecycle = config.database.role === "none" ? {
4274
4371
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -4311,8 +4408,7 @@ function localBootstrapHost() {
4311
4408
  telemetryEndpoint: config.telemetryEndpoint,
4312
4409
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
4313
4410
  sourceBinPath: PACKAGED_AGENT_BIN,
4314
- ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
4315
- enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
4411
+ ...hasBinding ? {
4316
4412
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
4317
4413
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
4318
4414
  apiUrl: config.apiUrl,
@@ -4331,6 +4427,8 @@ function localBootstrapHost() {
4331
4427
  };
4332
4428
  }
4333
4429
  export {
4430
+ validatePlatformBootstrapSecrets,
4431
+ validateEnrolledComputeBootstrapSecrets,
4334
4432
  validateBootstrapConfig,
4335
4433
  resolveInstalledBootstrapKind,
4336
4434
  readBootstrapConfig,