@forgezero/agent 0.1.58 → 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
@@ -344,18 +344,28 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
344
344
  }
345
345
  return { tunnel, connectorToken, created };
346
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
+ }
347
359
 
348
360
  // src/cloudflare-bootstrap.ts
349
361
  import { constants } from "fs";
350
- import { randomBytes, randomUUID } from "crypto";
362
+ import { createHmac, randomUUID } from "crypto";
351
363
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
352
364
  import { dirname, join, resolve } from "path";
353
365
  import { isIP as isIP2 } from "net";
354
366
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
355
- var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
356
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;
357
368
  var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
358
- var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
359
369
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
360
370
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
361
371
  const metadata = await handle.stat();
@@ -389,26 +399,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
389
399
  await handle?.close();
390
400
  }
391
401
  }
392
- async function readOwnerApiToken(path) {
393
- const token = (await readOwnerOnlyFile(path, 4096)).trim();
394
- if (!TOKEN.test(token))
395
- throw new Error(`${resolve(path)} must contain exactly one Cloudflare API token`);
396
- return token;
397
- }
398
- async function readCloudflareBootstrapTokens(files) {
399
- const unsupported = Object.keys(files).filter((key) => ![
400
- "tunnelTokenFile",
401
- "apiTokenFile"
402
- ].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));
403
407
  if (unsupported.length)
404
- throw new Error(`Cloudflare bootstrap token files contain unsupported field ${unsupported[0]}`);
405
- if (!files.tunnelTokenFile?.trim() || !files.apiTokenFile?.trim()) {
406
- throw new Error("Cloudflare bootstrap requires exactly tunnelTokenFile and apiTokenFile");
407
- }
408
- const [tunnelToken, apiToken] = await Promise.all([
409
- readOwnerApiToken(files.tunnelTokenFile),
410
- readOwnerApiToken(files.apiTokenFile)
411
- ]);
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");
412
413
  if (tunnelToken === apiToken) {
413
414
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
414
415
  }
@@ -584,7 +585,7 @@ function planCloudflareBootstrap(input, outputPath) {
584
585
  return {
585
586
  format: 1,
586
587
  kind: "forgezero-cloudflare-bootstrap-plan",
587
- mode: "attended-token-file",
588
+ mode: "attended-hidden-input",
588
589
  outputFile: resolve(outputPath),
589
590
  coordinates,
590
591
  operations: [
@@ -592,19 +593,19 @@ function planCloudflareBootstrap(input, outputPath) {
592
593
  ...coordinates.realtime ? [
593
594
  "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
594
595
  ] : [],
595
- "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",
596
597
  ...coordinates.nodes.some(({ mesh }) => mesh) ? [
597
- "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",
598
599
  "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
599
600
  ] : [],
600
601
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
601
602
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
602
- "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"
603
604
  ],
604
605
  secrets: [
605
- "API tokens are read only from owner-only files and are never placed in argv or stdout",
606
- "CF_TUNNEL_TOKEN is never persisted; cloudflared, Mesh, CF_API_TOKEN and generated realtime capabilities are atomically checkpointed with mode 0600",
607
- "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"
608
609
  ]
609
610
  };
610
611
  }
@@ -669,25 +670,15 @@ async function readExistingOutput(path) {
669
670
  const resources = output.resources;
670
671
  const unsupportedResource = Object.keys(resources).filter((key) => ![
671
672
  "kvNamespaceId",
672
- "apiToken",
673
- "realtime",
674
673
  "nodes"
675
674
  ].includes(key));
676
675
  if (unsupportedResource.length) {
677
676
  throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
678
677
  }
679
678
  const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
680
- 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) {
681
680
  throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
682
681
  }
683
- if (coordinates.realtime) {
684
- const realtime = resources.realtime;
685
- if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
686
- throw new Error(`${resolve(path)} has malformed Cloudflare realtime resources`);
687
- }
688
- } else if (resources.realtime !== undefined) {
689
- throw new Error(`${resolve(path)} contains undeclared Cloudflare realtime resources`);
690
- }
691
682
  const seen = new Set;
692
683
  for (const item of resources.nodes) {
693
684
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -700,16 +691,15 @@ async function readExistingOutput(path) {
700
691
  "service",
701
692
  "tunnelName",
702
693
  "tunnelId",
703
- "connectorToken",
704
694
  "mesh"
705
695
  ].includes(key));
706
696
  const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
707
- 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 ?? ""))) {
708
698
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
709
699
  }
710
700
  if (expected.mesh) {
711
701
  const mesh = node.mesh;
712
- 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 ?? ""))) {
713
703
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
714
704
  }
715
705
  } else if (node.mesh !== undefined) {
@@ -750,7 +740,7 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
750
740
  throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
751
741
  }
752
742
  const resource = matches[0];
753
- 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)) {
754
744
  throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
755
745
  }
756
746
  return {
@@ -758,10 +748,8 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
758
748
  hostname: resource.hostname,
759
749
  service: resource.service,
760
750
  tunnelId: resource.tunnelId,
761
- connectorToken: resource.connectorToken,
762
751
  ...resource.mesh ? { mesh: {
763
752
  connectorId: resource.mesh.connectorId,
764
- connectorToken: resource.mesh.connectorToken,
765
753
  routes: resource.mesh.routes
766
754
  } } : {}
767
755
  };
@@ -785,11 +773,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
785
773
  "hostname",
786
774
  "service",
787
775
  "tunnelId",
788
- "connectorToken",
789
776
  "accountId",
790
777
  "zoneId",
791
778
  "kvNamespaceId",
792
- "apiToken",
793
779
  "mesh",
794
780
  "realtime"
795
781
  ].includes(key));
@@ -800,11 +786,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
800
786
  try {
801
787
  service = normalizeService(output.service ?? "");
802
788
  } catch {}
803
- 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 ?? "")) {
804
790
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
805
791
  }
806
792
  if (output.mesh !== undefined) {
807
- 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) => {
808
794
  try {
809
795
  return privateMeshCidr(route) !== route;
810
796
  } catch {
@@ -833,7 +819,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
833
819
  } catch {
834
820
  throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
835
821
  }
836
- 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) {
837
823
  throw new Error("Cloudflare host handoff contains malformed realtime credentials");
838
824
  }
839
825
  }
@@ -898,18 +884,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
898
884
  hostname: node.hostname,
899
885
  service: node.service,
900
886
  tunnelId: node.tunnelId,
901
- connectorToken: node.connectorToken,
902
887
  accountId: output.coordinates.accountId,
903
888
  zoneId: output.coordinates.zoneId,
904
889
  kvNamespaceId: output.resources.kvNamespaceId,
905
- apiToken: output.resources.apiToken,
906
- ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
907
- ...output.coordinates.realtime,
908
- ...output.resources.realtime
909
- } } : {},
890
+ ...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
910
891
  ...node.mesh ? { mesh: {
911
892
  connectorId: node.mesh.connectorId,
912
- connectorToken: node.mesh.connectorToken,
913
893
  routes: node.mesh.routes
914
894
  } } : {}
915
895
  };
@@ -917,7 +897,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
917
897
  }
918
898
  }
919
899
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
920
- 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
+ });
921
904
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
922
905
  const coordinates = validateCloudflareBootstrapCoordinates(input);
923
906
  const absoluteOutput = resolve(outputPath);
@@ -935,10 +918,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
935
918
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
936
919
  }
937
920
  await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
938
- const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
939
- publishSecret: newRealtimeSecret(),
940
- ticketSecret: newRealtimeSecret()
941
- } : undefined;
921
+ const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
942
922
  if (coordinates.realtime && realtime) {
943
923
  await verifyCloudflareWorkerDurableObjects({
944
924
  accountId: coordinates.accountId,
@@ -953,8 +933,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
953
933
  coordinates,
954
934
  resources: {
955
935
  kvNamespaceId: coordinates.kvNamespaceId,
956
- apiToken,
957
- realtime,
958
936
  nodes: existing?.resources.nodes ?? []
959
937
  }
960
938
  });
@@ -973,7 +951,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
973
951
  let resource;
974
952
  let created = false;
975
953
  if (checkpointed) {
976
- 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)) {
977
955
  throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
978
956
  }
979
957
  resource = checkpointed;
@@ -994,15 +972,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
994
972
  }, fetcher);
995
973
  mesh = {
996
974
  ...node.mesh,
997
- connectorId: ensured.connector.id,
998
- connectorToken: ensured.connectorToken
975
+ connectorId: ensured.connector.id
999
976
  };
1000
977
  }
1001
978
  const { mesh: _declaredMesh, ...publicNode } = node;
1002
979
  resource = {
1003
980
  ...publicNode,
1004
981
  tunnelId: tunnel.tunnel.id,
1005
- connectorToken: tunnel.connectorToken,
1006
982
  ...mesh ? { mesh } : {}
1007
983
  };
1008
984
  }
@@ -1016,8 +992,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
1016
992
  coordinates,
1017
993
  resources: {
1018
994
  kvNamespaceId: coordinates.kvNamespaceId,
1019
- apiToken,
1020
- ...realtime ? { realtime } : {},
1021
995
  nodes: [...nodeResources]
1022
996
  }
1023
997
  });
@@ -1061,8 +1035,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
1061
1035
  coordinates,
1062
1036
  resources: {
1063
1037
  kvNamespaceId: coordinates.kvNamespaceId,
1064
- apiToken,
1065
- ...realtime ? { realtime } : {},
1066
1038
  nodes: nodeResources
1067
1039
  },
1068
1040
  created: {
@@ -1084,10 +1056,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
1084
1056
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
1085
1057
  };
1086
1058
  }
1087
- if (!request.tokenFiles) {
1088
- 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");
1089
1061
  }
1090
- const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
1062
+ const tokens = validateCloudflareBootstrapTokens(request.tokens);
1091
1063
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
1092
1064
  return {
1093
1065
  format: 1,
@@ -1238,7 +1210,7 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1238
1210
  }
1239
1211
 
1240
1212
  // src/bootstrap.ts
1241
- import { createHash, createHmac, randomBytes as randomBytes3 } from "crypto";
1213
+ import { createHash, createHmac as createHmac2, randomBytes as randomBytes2 } from "crypto";
1242
1214
  import {
1243
1215
  chmodSync as chmodSync2,
1244
1216
  existsSync as existsSync4,
@@ -1268,7 +1240,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1268
1240
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1269
1241
 
1270
1242
  // src/version.ts
1271
- var VERSION = "0.1.58";
1243
+ var VERSION = "0.1.59";
1272
1244
 
1273
1245
  // src/software.ts
1274
1246
  var PINNED_BUN_VERSION = "1.3.14";
@@ -2178,10 +2150,11 @@ function planProvision(options) {
2178
2150
  const warpEnabled = warpValues.every(Boolean);
2179
2151
  if (warpValues.some(Boolean) && !warpEnabled)
2180
2152
  throw new Error("WARP configuration must be supplied together");
2181
- const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
2182
- if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
2183
- throw new Error("direct enrolment paths must be supplied together");
2184
- 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;
2185
2158
  const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
2186
2159
  const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
2187
2160
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
@@ -2309,7 +2282,12 @@ function planProvision(options) {
2309
2282
  ] : [],
2310
2283
  ...enrolmentEnabled ? [
2311
2284
  step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
2312
- 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
+ })] : []
2313
2291
  ] : [],
2314
2292
  ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
2315
2293
  { path: deployRoot, mode: 493, owner: "root", group: "root" },
@@ -2353,7 +2331,7 @@ function planProvision(options) {
2353
2331
  }
2354
2332
 
2355
2333
  // src/cli/agent-install.ts
2356
- import { randomBytes as randomBytes2 } from "crypto";
2334
+ import { randomBytes } from "crypto";
2357
2335
  import {
2358
2336
  chmodSync,
2359
2337
  copyFileSync,
@@ -2449,7 +2427,7 @@ var runProvisionOperation = async (operation) => {
2449
2427
  if (operation.kind === "ensure-seed") {
2450
2428
  if (existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
2451
2429
  return { stdout: "", exitCode: 0 };
2452
- const seed = randomBytes2(32).toString("base64url");
2430
+ const seed = randomBytes(32).toString("base64url");
2453
2431
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
2454
2432
  if (result.exitCode === 0)
2455
2433
  chmodSync(operation.credential, 256);
@@ -2842,9 +2820,10 @@ function renderPlatformSharedEnvironment(input) {
2842
2820
  }
2843
2821
  function platformApiCredentialSpecs(options) {
2844
2822
  const optional = [
2845
- ["bootstrap-smtp-password", options.emailProvider === "smtp"],
2846
- ["bootstrap-jetemail-api-key", options.emailProvider === "jetemail"],
2823
+ ["fz_smtp.password", options.emailProvider === "smtp"],
2824
+ ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
2847
2825
  ["CF_API_TOKEN", options.cloudflareKv],
2826
+ ["CF_TUNNEL_TOKEN", options.cloudflareKv],
2848
2827
  ["REALTIME_PUBLISH_SECRET", options.realtime],
2849
2828
  ["REALTIME_TICKET_SECRET", options.realtime]
2850
2829
  ];
@@ -3179,6 +3158,78 @@ var PLATFORM_BOOTSTRAP_PROFILES = [
3179
3158
  "platform-db-api",
3180
3159
  "platform-api"
3181
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
+ }
3182
3233
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
3183
3234
  function resolveInstalledBootstrapKind(states) {
3184
3235
  if (states.compute && states.metal) {
@@ -3194,6 +3245,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
3194
3245
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
3195
3246
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
3196
3247
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
3248
+ var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
3197
3249
  var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
3198
3250
  var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
3199
3251
  var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
@@ -3202,7 +3254,6 @@ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
3202
3254
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
3203
3255
  var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
3204
3256
  var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
3205
- var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
3206
3257
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
3207
3258
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
3208
3259
  var CONTROL_SOCKET = "/run/forgezero/control.sock";
@@ -3280,8 +3331,6 @@ function validateBootstrapConfig(value) {
3280
3331
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
3281
3332
  throw new Error("tenant API must be public HTTPS or loopback HTTP");
3282
3333
  }
3283
- if (!value.enrolTokenFile)
3284
- throw new Error("enrolled-compute activation requires an enrolment-token file");
3285
3334
  if (value.bootstrapRunner) {
3286
3335
  if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
3287
3336
  throw new Error("bootstrap runner SSH private-key file must be absolute");
@@ -3325,8 +3374,6 @@ function validateBootstrapConfig(value) {
3325
3374
  if (!/^[a-z0-9](?:[a-z0-9:_-]{0,126}[a-z0-9])?$/.test(value.computeReference)) {
3326
3375
  throw new Error("platform compute reference is malformed");
3327
3376
  }
3328
- if (!value.database.bootstrapSecretFile)
3329
- throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
3330
3377
  const write = value.database.coordinators.map(privateOrigin);
3331
3378
  if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
3332
3379
  throw new Error("database coordinators must contain 1-16 unique private origins");
@@ -3341,12 +3388,6 @@ function validateBootstrapConfig(value) {
3341
3388
  }
3342
3389
  if (runtime.deployProfile !== value.environment)
3343
3390
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
3344
- if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
3345
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3346
- }
3347
- if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
3348
- throw new Error("Bootstrap email credential file must be an absolute single-line path");
3349
- }
3350
3391
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
3351
3392
  throw new Error("runtime database topology disagrees with bootstrap topology");
3352
3393
  }
@@ -3357,9 +3398,8 @@ function validateBootstrapConfig(value) {
3357
3398
  if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
3358
3399
  throw new Error("enabled firewall requires unique private cluster CIDRs");
3359
3400
  }
3360
- 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) {
3361
- throw new Error("platform enrolment source and token file disagree");
3362
- }
3401
+ if (!["genesis-derived", "api-token"].includes(value.enrolment.source))
3402
+ throw new Error("platform enrolment source is invalid");
3363
3403
  value.runtime.environment = runtime;
3364
3404
  return value;
3365
3405
  }
@@ -3573,7 +3613,7 @@ async function waitForCloudflaredTunnel(host, expectedTunnelId) {
3573
3613
  var derive = (root, label) => {
3574
3614
  if (!/^[a-f0-9]{64}$/i.test(root))
3575
3615
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
3576
- return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
3616
+ return createHmac2("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
3577
3617
  };
3578
3618
  async function seal(host, name, destination2, value) {
3579
3619
  if (host.exists(destination2))
@@ -3863,15 +3903,10 @@ async function bootstrapStatus(host = localBootstrapHost()) {
3863
3903
  problems.push("durable Agent enrolment state is missing");
3864
3904
  return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
3865
3905
  }
3866
- async function applyBootstrap(input, host = localBootstrapHost()) {
3906
+ async function applyBootstrap(input, host = localBootstrapHost(), secrets, dependencies = {}) {
3867
3907
  const config = validateBootstrapConfig(structuredClone(input));
3868
3908
  if (host.uid() !== 0)
3869
3909
  throw new Error("fz bootstrap --apply must run as root");
3870
- if (config.kind === "enrolled-compute") {
3871
- const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
3872
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
3873
- throw new Error("tenant enrolment token is malformed");
3874
- }
3875
3910
  let installed;
3876
3911
  if (host.exists(STATE_PATH))
3877
3912
  installed = parseStoredState(host.read(STATE_PATH));
@@ -3929,11 +3964,21 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3929
3964
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
3930
3965
  }
3931
3966
  }
3932
- const platformPrivate = config.kind === "platform" ? {
3933
- root: privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret"),
3934
- 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,
3935
- backup: config.runtime.credentialFiles?.backupS3Secret && !host.exists(`${CREDS}/backup-s3-secret.cred`) ? privateFile(host, config.runtime.credentialFiles.backupS3Secret, "backup S3 credential") : undefined
3936
- } : 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;
3937
3982
  if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3938
3983
  privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
3939
3984
  }
@@ -3942,17 +3987,31 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3942
3987
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
3943
3988
  host.mkdir(CREDS, 448);
3944
3989
  host.mkdir("/var/lib/forgezero", 448);
3945
- if (cloudflare && !host.exists(CF_API_CREDENTIAL)) {
3946
- 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);
3993
+ }
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);
3947
4002
  }
3948
- if (cloudflare?.realtime && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
3949
- await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, cloudflare.realtime.publishSecret);
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);
3950
4005
  }
3951
- if (cloudflare?.realtime && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
3952
- await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, cloudflare.realtime.ticketSecret);
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);
3953
4009
  }
3954
- if (cloudflare?.mesh && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
3955
- await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, cloudflare.mesh.connectorToken);
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);
3956
4015
  }
3957
4016
  await host.installAgent(config);
3958
4017
  if (config.kind === "platform" && config.firewall.enabled) {
@@ -3979,22 +4038,15 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3979
4038
  }
3980
4039
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
3981
4040
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
3982
- const credentialFiles = config.runtime.credentialFiles ?? {};
3983
- const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
3984
- if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
3985
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3986
- }
4041
+ const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
3987
4042
  for (const [name, source] of Object.entries({
3988
4043
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
3989
- "backup-s3-secret": platformPrivate.backup
4044
+ "backup.s3.secretAccessKey": platformPrivate.backup
3990
4045
  })) {
3991
4046
  if (source) {
3992
4047
  const destination2 = `${CREDS}/${name}.cred`;
3993
4048
  if (!host.exists(destination2)) {
3994
4049
  await seal(host, name, destination2, source);
3995
- const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
3996
- if (sourcePath)
3997
- host.remove(sourcePath);
3998
4050
  }
3999
4051
  }
4000
4052
  }
@@ -4064,7 +4116,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4064
4116
  if (config.database.role === "master") {
4065
4117
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
4066
4118
  if (!host.exists(invite)) {
4067
- host.write(invite, `plt_${randomBytes3(24).toString("hex")}
4119
+ host.write(invite, `plt_${randomBytes2(24).toString("hex")}
4068
4120
  `, 384);
4069
4121
  await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
4070
4122
  }
@@ -4084,13 +4136,6 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4084
4136
  verifiedAt: new Date().toISOString()
4085
4137
  };
4086
4138
  host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
4087
- `, 384);
4088
- }
4089
- if (!alreadyEnrolled) {
4090
- 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}`)}`;
4091
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
4092
- throw new Error("platform enrolment token is malformed");
4093
- host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
4094
4139
  `, 384);
4095
4140
  }
4096
4141
  await checked2(host, [
@@ -4102,15 +4147,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
4102
4147
  "deploy",
4103
4148
  ...config.database.role === "master" ? ["--release-executor"] : []
4104
4149
  ], "initial Agent deployment");
4105
- if (!alreadyEnrolled) {
4106
- await host.installAgent(config, PLATFORM_ENROL_SOURCE);
4107
- if (config.enrolment.source === "api-token")
4108
- host.remove(config.enrolment.tokenFile);
4109
- }
4110
4150
  }
4111
4151
  if (config.cloudflareHandoff) {
4112
- if (!host.exists(TUNNEL_CREDENTIAL) && cloudflare) {
4113
- 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);
4114
4154
  }
4115
4155
  if (!host.exists(TUNNEL_CREDENTIAL))
4116
4156
  throw new Error("sealed cloudflared connector credential is missing");
@@ -4184,7 +4224,6 @@ function strictBootstrapDocument(value) {
4184
4224
  "installWarp",
4185
4225
  "cloudflareHandoff",
4186
4226
  "realm",
4187
- "enrolTokenFile",
4188
4227
  "software",
4189
4228
  "deploymentCredentials",
4190
4229
  "bootstrapRunner"
@@ -4193,8 +4232,8 @@ function strictBootstrapDocument(value) {
4193
4232
  exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
4194
4233
  if (root.cloudflareHandoff !== undefined)
4195
4234
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
4196
- exactKeys(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
4197
- 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");
4198
4237
  const runtime = exactKeys(root.runtime, [
4199
4238
  "environment",
4200
4239
  "serviceUser",
@@ -4202,8 +4241,7 @@ function strictBootstrapDocument(value) {
4202
4241
  "bluePort",
4203
4242
  "greenPort",
4204
4243
  "healthPath",
4205
- "keepReleases",
4206
- "credentialFiles"
4244
+ "keepReleases"
4207
4245
  ], "runtime config");
4208
4246
  exactKeys(runtime.environment, [
4209
4247
  "softwareProfile",
@@ -4241,8 +4279,6 @@ function strictBootstrapDocument(value) {
4241
4279
  "cloudflare",
4242
4280
  "realtime"
4243
4281
  ], "runtime environment");
4244
- if (runtime.credentialFiles !== undefined)
4245
- exactKeys(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
4246
4282
  const environment = runtime.environment;
4247
4283
  if (environment.email !== undefined) {
4248
4284
  const email = exactKeys(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
@@ -4326,10 +4362,10 @@ function localBootstrapHost() {
4326
4362
  throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
4327
4363
  return result;
4328
4364
  },
4329
- async installAgent(config, enrolTokenSourcePath) {
4365
+ async installAgent(config) {
4330
4366
  const capabilities = await readCapabilities(localRunner);
4331
4367
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
4332
- 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");
4333
4369
  if (config.kind === "platform") {
4334
4370
  const lifecycle = config.database.role === "none" ? {
4335
4371
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -4372,8 +4408,7 @@ function localBootstrapHost() {
4372
4408
  telemetryEndpoint: config.telemetryEndpoint,
4373
4409
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
4374
4410
  sourceBinPath: PACKAGED_AGENT_BIN,
4375
- ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
4376
- enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
4411
+ ...hasBinding ? {
4377
4412
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
4378
4413
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
4379
4414
  apiUrl: config.apiUrl,
@@ -4392,6 +4427,8 @@ function localBootstrapHost() {
4392
4427
  };
4393
4428
  }
4394
4429
  export {
4430
+ validatePlatformBootstrapSecrets,
4431
+ validateEnrolledComputeBootstrapSecrets,
4395
4432
  validateBootstrapConfig,
4396
4433
  resolveInstalledBootstrapKind,
4397
4434
  readBootstrapConfig,