@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/fz.js CHANGED
@@ -4811,8 +4811,7 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
4813
  init_dist();
4814
- import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync12 } from "fs";
4815
- import { randomBytes as randomBytes10 } from "crypto";
4814
+ import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
4816
4815
  import { basename as basename3, dirname as dirname12, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
4817
4816
  import { fileURLToPath as fileURLToPath3 } from "url";
4818
4817
  import { hostname } from "os";
@@ -4830,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4830
4829
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4831
4830
 
4832
4831
  // src/version.ts
4833
- var VERSION2 = "0.1.58";
4832
+ var VERSION2 = "0.1.59";
4834
4833
 
4835
4834
  // src/software.ts
4836
4835
  var PINNED_BUN_VERSION = "1.3.14";
@@ -6099,10 +6098,11 @@ function planProvision(options) {
6099
6098
  const warpEnabled = warpValues.every(Boolean);
6100
6099
  if (warpValues.some(Boolean) && !warpEnabled)
6101
6100
  throw new Error("WARP configuration must be supplied together");
6102
- const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
6103
- if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
6104
- throw new Error("direct enrolment paths must be supplied together");
6105
- const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
6101
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
6102
+ if (Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath) || options.enrolTokenSourcePath && !enrolmentEnabled) {
6103
+ throw new Error("direct enrolment credential and state paths must be supplied together");
6104
+ }
6105
+ const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
6106
6106
  const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
6107
6107
  const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
6108
6108
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
@@ -6230,7 +6230,12 @@ function planProvision(options) {
6230
6230
  ] : [],
6231
6231
  ...enrolmentEnabled ? [
6232
6232
  step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
6233
- step("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
6233
+ ...enrolTokenSourcePath ? [step("encrypted one-time enrolment capability", {
6234
+ kind: "ensure-enrolment",
6235
+ state: enrolStatePath,
6236
+ source: enrolTokenSourcePath,
6237
+ credential: enrolTokenCredentialPath
6238
+ })] : []
6234
6239
  ] : [],
6235
6240
  ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
6236
6241
  { path: deployRoot, mode: 493, owner: "root", group: "root" },
@@ -11550,7 +11555,7 @@ function removeSession(api, realm, path = defaultSessionPath()) {
11550
11555
  }
11551
11556
 
11552
11557
  // src/bootstrap.ts
11553
- import { createHash as createHash3, createHmac, randomBytes as randomBytes7 } from "crypto";
11558
+ import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes6 } from "crypto";
11554
11559
  import {
11555
11560
  chmodSync as chmodSync3,
11556
11561
  existsSync as existsSync8,
@@ -11761,9 +11766,10 @@ function renderPlatformSharedEnvironment(input) {
11761
11766
  }
11762
11767
  function platformApiCredentialSpecs(options) {
11763
11768
  const optional = [
11764
- ["bootstrap-smtp-password", options.emailProvider === "smtp"],
11765
- ["bootstrap-jetemail-api-key", options.emailProvider === "jetemail"],
11769
+ ["fz_smtp.password", options.emailProvider === "smtp"],
11770
+ ["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
11766
11771
  ["CF_API_TOKEN", options.cloudflareKv],
11772
+ ["CF_TUNNEL_TOKEN", options.cloudflareKv],
11767
11773
  ["REALTIME_PUBLISH_SECRET", options.realtime],
11768
11774
  ["REALTIME_TICKET_SECRET", options.realtime]
11769
11775
  ];
@@ -11937,7 +11943,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
11937
11943
 
11938
11944
  // src/cloudflare-bootstrap.ts
11939
11945
  import { constants } from "fs";
11940
- import { randomBytes as randomBytes6, randomUUID } from "crypto";
11946
+ import { createHmac, randomUUID } from "crypto";
11941
11947
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
11942
11948
  import { dirname as dirname7, join as join6, resolve as resolve5 } from "path";
11943
11949
  import { isIP as isIP3 } from "net";
@@ -12218,13 +12224,23 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
12218
12224
  }
12219
12225
  return { tunnel, connectorToken, created };
12220
12226
  }
12227
+ async function retrieveCloudflareConnectorTokens(config, fetcher = fetch) {
12228
+ 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)) {
12229
+ throw new Error("Cloudflare connector retrieval coordinates are invalid");
12230
+ }
12231
+ const connectorToken = await cf(config, `/accounts/${config.accountId}/cfd_tunnel/${encodeURIComponent(config.tunnelId)}/token`, {}, fetcher);
12232
+ const meshConnectorToken = config.meshConnectorId ? await cf(config, `/accounts/${config.accountId}/warp_connector/${encodeURIComponent(config.meshConnectorId)}/token`, {}, fetcher) : undefined;
12233
+ for (const value of [connectorToken, meshConnectorToken]) {
12234
+ if (value !== undefined && (!value || value.length > 16384))
12235
+ throw new Error("Cloudflare returned an invalid connector token");
12236
+ }
12237
+ return { connectorToken, ...meshConnectorToken ? { meshConnectorToken } : {} };
12238
+ }
12221
12239
 
12222
12240
  // src/cloudflare-bootstrap.ts
12223
12241
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
12224
- var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
12225
12242
  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;
12226
12243
  var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
12227
- var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
12228
12244
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
12229
12245
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
12230
12246
  const metadata = await handle.stat();
@@ -12258,26 +12274,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
12258
12274
  await handle?.close();
12259
12275
  }
12260
12276
  }
12261
- async function readOwnerApiToken(path) {
12262
- const token = (await readOwnerOnlyFile(path, 4096)).trim();
12263
- if (!TOKEN.test(token))
12264
- throw new Error(`${resolve5(path)} must contain exactly one Cloudflare API token`);
12265
- return token;
12266
- }
12267
- async function readCloudflareBootstrapTokens(files) {
12268
- const unsupported = Object.keys(files).filter((key) => ![
12269
- "tunnelTokenFile",
12270
- "apiTokenFile"
12271
- ].includes(key));
12277
+ function validateCloudflareBootstrapTokens(input) {
12278
+ if (!input || typeof input !== "object" || Array.isArray(input))
12279
+ throw new Error("Cloudflare bootstrap tokens must be an object");
12280
+ const source = input;
12281
+ const unsupported = Object.keys(source).filter((key) => !["tunnelToken", "apiToken"].includes(key));
12272
12282
  if (unsupported.length)
12273
- throw new Error(`Cloudflare bootstrap token files contain unsupported field ${unsupported[0]}`);
12274
- if (!files.tunnelTokenFile?.trim() || !files.apiTokenFile?.trim()) {
12275
- throw new Error("Cloudflare bootstrap requires exactly tunnelTokenFile and apiTokenFile");
12276
- }
12277
- const [tunnelToken, apiToken] = await Promise.all([
12278
- readOwnerApiToken(files.tunnelTokenFile),
12279
- readOwnerApiToken(files.apiTokenFile)
12280
- ]);
12283
+ throw new Error(`Cloudflare bootstrap tokens contain unsupported field ${unsupported[0]}`);
12284
+ const tunnelToken = typeof source.tunnelToken === "string" ? source.tunnelToken.trim() : "";
12285
+ const apiToken = typeof source.apiToken === "string" ? source.apiToken.trim() : "";
12286
+ if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken))
12287
+ throw new Error("Cloudflare bootstrap requires two valid API tokens");
12281
12288
  if (tunnelToken === apiToken) {
12282
12289
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
12283
12290
  }
@@ -12453,7 +12460,7 @@ function planCloudflareBootstrap(input, outputPath) {
12453
12460
  return {
12454
12461
  format: 1,
12455
12462
  kind: "forgezero-cloudflare-bootstrap-plan",
12456
- mode: "attended-token-file",
12463
+ mode: "attended-hidden-input",
12457
12464
  outputFile: resolve5(outputPath),
12458
12465
  coordinates,
12459
12466
  operations: [
@@ -12461,19 +12468,19 @@ function planCloudflareBootstrap(input, outputPath) {
12461
12468
  ...coordinates.realtime ? [
12462
12469
  "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
12463
12470
  ] : [],
12464
- "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
12471
+ "create or reuse one remotely-managed Tunnel per node and checkpoint only its non-secret id",
12465
12472
  ...coordinates.nodes.some(({ mesh }) => mesh) ? [
12466
- "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its registration token",
12473
+ "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint only its non-secret id",
12467
12474
  "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
12468
12475
  ] : [],
12469
12476
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
12470
12477
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
12471
- "write one node-specific handoff containing the connector token and owner-supplied KV/Worker runtime token"
12478
+ "write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
12472
12479
  ],
12473
12480
  secrets: [
12474
- "API tokens are read only from owner-only files and are never placed in argv or stdout",
12475
- "CF_TUNNEL_TOKEN is never persisted; cloudflared, Mesh, CF_API_TOKEN and generated realtime capabilities are atomically checkpointed with mode 0600",
12476
- "the normal API process receives only CF_API_TOKEN and never Tunnel, DNS or Mesh management authority"
12481
+ "API tokens are accepted only from a hidden prompt and are never placed in JSON, files, argv or stdout",
12482
+ "checkpoint and host handoffs contain no connector, management, runtime or realtime secret",
12483
+ "the target Agent retrieves its connector tokens and seals both Vault fallback tokens directly"
12477
12484
  ]
12478
12485
  };
12479
12486
  }
@@ -12538,25 +12545,15 @@ async function readExistingOutput(path) {
12538
12545
  const resources = output.resources;
12539
12546
  const unsupportedResource = Object.keys(resources).filter((key) => ![
12540
12547
  "kvNamespaceId",
12541
- "apiToken",
12542
- "realtime",
12543
12548
  "nodes"
12544
12549
  ].includes(key));
12545
12550
  if (unsupportedResource.length) {
12546
12551
  throw new Error(`${resolve5(path)} resources contain unsupported field ${unsupportedResource[0]}`);
12547
12552
  }
12548
12553
  const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
12549
- if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
12554
+ if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
12550
12555
  throw new Error(`${resolve5(path)} has malformed Cloudflare bootstrap resources`);
12551
12556
  }
12552
- if (coordinates.realtime) {
12553
- const realtime = resources.realtime;
12554
- if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
12555
- throw new Error(`${resolve5(path)} has malformed Cloudflare realtime resources`);
12556
- }
12557
- } else if (resources.realtime !== undefined) {
12558
- throw new Error(`${resolve5(path)} contains undeclared Cloudflare realtime resources`);
12559
- }
12560
12557
  const seen = new Set;
12561
12558
  for (const item of resources.nodes) {
12562
12559
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -12569,16 +12566,15 @@ async function readExistingOutput(path) {
12569
12566
  "service",
12570
12567
  "tunnelName",
12571
12568
  "tunnelId",
12572
- "connectorToken",
12573
12569
  "mesh"
12574
12570
  ].includes(key));
12575
12571
  const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
12576
- 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 ?? ""))) {
12572
+ 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 ?? ""))) {
12577
12573
  throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare node resource`);
12578
12574
  }
12579
12575
  if (expected.mesh) {
12580
12576
  const mesh = node.mesh;
12581
- 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 ?? ""))) {
12577
+ 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 ?? ""))) {
12582
12578
  throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare Mesh resource`);
12583
12579
  }
12584
12580
  } else if (node.mesh !== undefined) {
@@ -12622,11 +12618,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
12622
12618
  "hostname",
12623
12619
  "service",
12624
12620
  "tunnelId",
12625
- "connectorToken",
12626
12621
  "accountId",
12627
12622
  "zoneId",
12628
12623
  "kvNamespaceId",
12629
- "apiToken",
12630
12624
  "mesh",
12631
12625
  "realtime"
12632
12626
  ].includes(key));
@@ -12637,11 +12631,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
12637
12631
  try {
12638
12632
  service = normalizeService(output.service ?? "");
12639
12633
  } catch {}
12640
- 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 ?? "")) {
12634
+ 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 ?? "")) {
12641
12635
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
12642
12636
  }
12643
12637
  if (output.mesh !== undefined) {
12644
- 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) => {
12638
+ 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) => {
12645
12639
  try {
12646
12640
  return privateMeshCidr(route) !== route;
12647
12641
  } catch {
@@ -12670,7 +12664,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
12670
12664
  } catch {
12671
12665
  throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
12672
12666
  }
12673
- 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) {
12667
+ if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
12674
12668
  throw new Error("Cloudflare host handoff contains malformed realtime credentials");
12675
12669
  }
12676
12670
  }
@@ -12735,18 +12729,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
12735
12729
  hostname: node.hostname,
12736
12730
  service: node.service,
12737
12731
  tunnelId: node.tunnelId,
12738
- connectorToken: node.connectorToken,
12739
12732
  accountId: output.coordinates.accountId,
12740
12733
  zoneId: output.coordinates.zoneId,
12741
12734
  kvNamespaceId: output.resources.kvNamespaceId,
12742
- apiToken: output.resources.apiToken,
12743
- ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
12744
- ...output.coordinates.realtime,
12745
- ...output.resources.realtime
12746
- } } : {},
12735
+ ...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
12747
12736
  ...node.mesh ? { mesh: {
12748
12737
  connectorId: node.mesh.connectorId,
12749
- connectorToken: node.mesh.connectorToken,
12750
12738
  routes: node.mesh.routes
12751
12739
  } } : {}
12752
12740
  };
@@ -12754,7 +12742,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
12754
12742
  }
12755
12743
  }
12756
12744
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
12757
- var newRealtimeSecret = () => randomBytes6(48).toString("base64url");
12745
+ var deriveCloudflareRealtimeSecrets = (apiToken) => ({
12746
+ publishSecret: createHmac("sha512", apiToken).update("forgezero/realtime/publish/v1").digest("base64url"),
12747
+ ticketSecret: createHmac("sha512", apiToken).update("forgezero/realtime/ticket/v1").digest("base64url")
12748
+ });
12758
12749
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
12759
12750
  const coordinates = validateCloudflareBootstrapCoordinates(input);
12760
12751
  const absoluteOutput = resolve5(outputPath);
@@ -12772,10 +12763,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12772
12763
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
12773
12764
  }
12774
12765
  await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
12775
- const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
12776
- publishSecret: newRealtimeSecret(),
12777
- ticketSecret: newRealtimeSecret()
12778
- } : undefined;
12766
+ const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
12779
12767
  if (coordinates.realtime && realtime) {
12780
12768
  await verifyCloudflareWorkerDurableObjects({
12781
12769
  accountId: coordinates.accountId,
@@ -12790,8 +12778,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12790
12778
  coordinates,
12791
12779
  resources: {
12792
12780
  kvNamespaceId: coordinates.kvNamespaceId,
12793
- apiToken,
12794
- realtime,
12795
12781
  nodes: existing?.resources.nodes ?? []
12796
12782
  }
12797
12783
  });
@@ -12810,7 +12796,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12810
12796
  let resource;
12811
12797
  let created = false;
12812
12798
  if (checkpointed) {
12813
- if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId) || !CONNECTOR_TOKEN.test(checkpointed.connectorToken)) {
12799
+ if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)) {
12814
12800
  throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
12815
12801
  }
12816
12802
  resource = checkpointed;
@@ -12831,15 +12817,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12831
12817
  }, fetcher);
12832
12818
  mesh = {
12833
12819
  ...node.mesh,
12834
- connectorId: ensured.connector.id,
12835
- connectorToken: ensured.connectorToken
12820
+ connectorId: ensured.connector.id
12836
12821
  };
12837
12822
  }
12838
12823
  const { mesh: _declaredMesh, ...publicNode } = node;
12839
12824
  resource = {
12840
12825
  ...publicNode,
12841
12826
  tunnelId: tunnel.tunnel.id,
12842
- connectorToken: tunnel.connectorToken,
12843
12827
  ...mesh ? { mesh } : {}
12844
12828
  };
12845
12829
  }
@@ -12853,8 +12837,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12853
12837
  coordinates,
12854
12838
  resources: {
12855
12839
  kvNamespaceId: coordinates.kvNamespaceId,
12856
- apiToken,
12857
- ...realtime ? { realtime } : {},
12858
12840
  nodes: [...nodeResources]
12859
12841
  }
12860
12842
  });
@@ -12898,8 +12880,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
12898
12880
  coordinates,
12899
12881
  resources: {
12900
12882
  kvNamespaceId: coordinates.kvNamespaceId,
12901
- apiToken,
12902
- ...realtime ? { realtime } : {},
12903
12883
  nodes: nodeResources
12904
12884
  },
12905
12885
  created: {
@@ -12921,10 +12901,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
12921
12901
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
12922
12902
  };
12923
12903
  }
12924
- if (!request.tokenFiles) {
12925
- throw new Error("Cloudflare apply requires exactly two owner-only token file paths");
12904
+ if (!request.tokens) {
12905
+ throw new Error("Cloudflare apply requires exactly two attended API tokens");
12926
12906
  }
12927
- const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
12907
+ const tokens = validateCloudflareBootstrapTokens(request.tokens);
12928
12908
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
12929
12909
  return {
12930
12910
  format: 1,
@@ -13182,6 +13162,78 @@ var PLATFORM_BOOTSTRAP_PROFILES = [
13182
13162
  "platform-db-api",
13183
13163
  "platform-api"
13184
13164
  ];
13165
+ function validateCloudflareBootstrapSecretPair(input) {
13166
+ if (!input || typeof input !== "object" || Array.isArray(input))
13167
+ throw new Error("Cloudflare credential input must be an object");
13168
+ const source = input;
13169
+ const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : "";
13170
+ const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : "";
13171
+ if (!/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareTunnelToken) || !/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareApiToken) || cloudflareTunnelToken === cloudflareApiToken) {
13172
+ throw new Error("Cloudflare bootstrap credentials must be distinct valid API tokens");
13173
+ }
13174
+ return { cloudflareTunnelToken, cloudflareApiToken };
13175
+ }
13176
+ function validateEnrolledComputeBootstrapSecrets(config, input) {
13177
+ if (!input || typeof input !== "object" || Array.isArray(input))
13178
+ throw new Error("compute activation credential input must be an object");
13179
+ const source = input;
13180
+ const allowed = ["enrolmentToken", "cloudflareTunnelToken", "cloudflareApiToken"];
13181
+ const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
13182
+ if (unknown.length)
13183
+ throw new Error(`compute activation credential input contains unsupported field ${unknown[0]}`);
13184
+ const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : "";
13185
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken))
13186
+ throw new Error("compute enrolment token is malformed");
13187
+ const cloudflareConfigured = Boolean(config.cloudflareHandoff);
13188
+ if (cloudflareConfigured !== Boolean(source.cloudflareTunnelToken && source.cloudflareApiToken)) {
13189
+ throw new Error("Cloudflare compute activation requires CF_TUNNEL_TOKEN and CF_API_TOKEN together");
13190
+ }
13191
+ return {
13192
+ enrolmentToken,
13193
+ ...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
13194
+ };
13195
+ }
13196
+ function validatePlatformBootstrapSecrets(config, input) {
13197
+ if (!input || typeof input !== "object" || Array.isArray(input))
13198
+ throw new Error("bootstrap credential input must be an object");
13199
+ const source = input;
13200
+ const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
13201
+ const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
13202
+ if (unknown.length)
13203
+ throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
13204
+ const clusterBootstrapCode = typeof source.clusterBootstrapCode === "string" ? source.clusterBootstrapCode.trim() : "";
13205
+ const emailSecret = typeof source.emailSecret === "string" ? source.emailSecret.trim() : "";
13206
+ const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : undefined;
13207
+ const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
13208
+ const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
13209
+ const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
13210
+ if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
13211
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
13212
+ if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
13213
+ throw new Error("bootstrap email credential is malformed");
13214
+ if (config.enrolment.source === "api-token" !== Boolean(enrolmentToken) || enrolmentToken && !/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken)) {
13215
+ throw new Error("platform enrolment source and attended token disagree");
13216
+ }
13217
+ if (Boolean(config.runtime.environment.backup) !== Boolean(backupS3Secret)) {
13218
+ throw new Error("backup configuration and its attended credential must be supplied together");
13219
+ }
13220
+ if (backupS3Secret && (backupS3Secret.length > 16384 || /[\r\n\0]/.test(backupS3Secret))) {
13221
+ throw new Error("backup credential is malformed");
13222
+ }
13223
+ const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
13224
+ if (cloudflareConfigured !== Boolean(cloudflareTunnelToken && cloudflareApiToken)) {
13225
+ throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
13226
+ }
13227
+ if (cloudflareTunnelToken)
13228
+ validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
13229
+ return {
13230
+ clusterBootstrapCode,
13231
+ emailSecret,
13232
+ ...enrolmentToken ? { enrolmentToken } : {},
13233
+ ...backupS3Secret ? { backupS3Secret } : {},
13234
+ ...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
13235
+ };
13236
+ }
13185
13237
  var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
13186
13238
  function resolveInstalledBootstrapKind(states) {
13187
13239
  if (states.compute && states.metal) {
@@ -13197,6 +13249,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
13197
13249
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
13198
13250
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
13199
13251
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
13252
+ var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
13200
13253
  var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
13201
13254
  var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
13202
13255
  var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
@@ -13205,7 +13258,6 @@ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
13205
13258
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
13206
13259
  var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
13207
13260
  var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
13208
- var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
13209
13261
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
13210
13262
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
13211
13263
  var CONTROL_SOCKET = "/run/forgezero/control.sock";
@@ -13283,8 +13335,6 @@ function validateBootstrapConfig(value) {
13283
13335
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
13284
13336
  throw new Error("tenant API must be public HTTPS or loopback HTTP");
13285
13337
  }
13286
- if (!value.enrolTokenFile)
13287
- throw new Error("enrolled-compute activation requires an enrolment-token file");
13288
13338
  if (value.bootstrapRunner) {
13289
13339
  if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
13290
13340
  throw new Error("bootstrap runner SSH private-key file must be absolute");
@@ -13328,8 +13378,6 @@ function validateBootstrapConfig(value) {
13328
13378
  if (!/^[a-z0-9](?:[a-z0-9:_-]{0,126}[a-z0-9])?$/.test(value.computeReference)) {
13329
13379
  throw new Error("platform compute reference is malformed");
13330
13380
  }
13331
- if (!value.database.bootstrapSecretFile)
13332
- throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
13333
13381
  const write = value.database.coordinators.map(privateOrigin);
13334
13382
  if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
13335
13383
  throw new Error("database coordinators must contain 1-16 unique private origins");
@@ -13344,12 +13392,6 @@ function validateBootstrapConfig(value) {
13344
13392
  }
13345
13393
  if (runtime.deployProfile !== value.environment)
13346
13394
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
13347
- if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
13348
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
13349
- }
13350
- if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
13351
- throw new Error("Bootstrap email credential file must be an absolute single-line path");
13352
- }
13353
13395
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
13354
13396
  throw new Error("runtime database topology disagrees with bootstrap topology");
13355
13397
  }
@@ -13360,9 +13402,8 @@ function validateBootstrapConfig(value) {
13360
13402
  if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
13361
13403
  throw new Error("enabled firewall requires unique private cluster CIDRs");
13362
13404
  }
13363
- 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) {
13364
- throw new Error("platform enrolment source and token file disagree");
13365
- }
13405
+ if (!["genesis-derived", "api-token"].includes(value.enrolment.source))
13406
+ throw new Error("platform enrolment source is invalid");
13366
13407
  value.runtime.environment = runtime;
13367
13408
  return value;
13368
13409
  }
@@ -13576,7 +13617,7 @@ async function waitForCloudflaredTunnel(host, expectedTunnelId) {
13576
13617
  var derive = (root, label) => {
13577
13618
  if (!/^[a-f0-9]{64}$/i.test(root))
13578
13619
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
13579
- return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
13620
+ return createHmac2("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
13580
13621
  };
13581
13622
  async function seal(host, name, destination2, value) {
13582
13623
  if (host.exists(destination2))
@@ -13866,15 +13907,10 @@ async function bootstrapStatus(host = localBootstrapHost()) {
13866
13907
  problems.push("durable Agent enrolment state is missing");
13867
13908
  return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
13868
13909
  }
13869
- async function applyBootstrap(input, host = localBootstrapHost()) {
13910
+ async function applyBootstrap(input, host = localBootstrapHost(), secrets, dependencies = {}) {
13870
13911
  const config = validateBootstrapConfig(structuredClone(input));
13871
13912
  if (host.uid() !== 0)
13872
13913
  throw new Error("fz bootstrap --apply must run as root");
13873
- if (config.kind === "enrolled-compute") {
13874
- const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
13875
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
13876
- throw new Error("tenant enrolment token is malformed");
13877
- }
13878
13914
  let installed;
13879
13915
  if (host.exists(STATE_PATH))
13880
13916
  installed = parseStoredState(host.read(STATE_PATH));
@@ -13932,11 +13968,21 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
13932
13968
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
13933
13969
  }
13934
13970
  }
13935
- const platformPrivate = config.kind === "platform" ? {
13936
- root: privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret"),
13937
- 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,
13938
- backup: config.runtime.credentialFiles?.backupS3Secret && !host.exists(`${CREDS}/backup-s3-secret.cred`) ? privateFile(host, config.runtime.credentialFiles.backupS3Secret, "backup S3 credential") : undefined
13939
- } : undefined;
13971
+ const platformPrivate = config.kind === "platform" ? (() => {
13972
+ if (!secrets)
13973
+ throw new Error("platform apply requires attended credentials on stdin");
13974
+ const checked3 = validatePlatformBootstrapSecrets(config, secrets);
13975
+ return {
13976
+ root: checked3.clusterBootstrapCode,
13977
+ email: checked3.emailSecret,
13978
+ enrolmentToken: checked3.enrolmentToken,
13979
+ backup: checked3.backupS3Secret,
13980
+ cloudflareTunnelToken: checked3.cloudflareTunnelToken,
13981
+ cloudflareApiToken: checked3.cloudflareApiToken
13982
+ };
13983
+ })() : undefined;
13984
+ const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
13985
+ const cloudflarePrivate = cloudflare ? config.kind === "platform" ? { cloudflareTunnelToken: platformPrivate.cloudflareTunnelToken, cloudflareApiToken: platformPrivate.cloudflareApiToken } : validateCloudflareBootstrapSecretPair(enrolledPrivate) : undefined;
13940
13986
  if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
13941
13987
  privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
13942
13988
  }
@@ -13945,17 +13991,31 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
13945
13991
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
13946
13992
  host.mkdir(CREDS, 448);
13947
13993
  host.mkdir("/var/lib/forgezero", 448);
13948
- if (cloudflare && !host.exists(CF_API_CREDENTIAL)) {
13949
- await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflare.apiToken);
13994
+ if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
13995
+ 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;
13996
+ await seal(host, "enrol-token", ENROL_CREDENTIAL, enrolmentToken);
13997
+ }
13998
+ const connectorCapabilities = cloudflare ? await retrieveCloudflareConnectorTokens({
13999
+ accountId: cloudflare.accountId,
14000
+ tunnelId: cloudflare.tunnelId,
14001
+ ...cloudflare.mesh ? { meshConnectorId: cloudflare.mesh.connectorId } : {},
14002
+ apiToken: cloudflarePrivate.cloudflareTunnelToken
14003
+ }, dependencies.fetcher ?? fetch) : undefined;
14004
+ if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
14005
+ await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
13950
14006
  }
13951
- if (cloudflare?.realtime && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
13952
- await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, cloudflare.realtime.publishSecret);
14007
+ if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
14008
+ await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
13953
14009
  }
13954
- if (cloudflare?.realtime && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
13955
- await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, cloudflare.realtime.ticketSecret);
14010
+ const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
14011
+ if (realtimeSecrets && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
14012
+ await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, realtimeSecrets.publishSecret);
13956
14013
  }
13957
- if (cloudflare?.mesh && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
13958
- await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, cloudflare.mesh.connectorToken);
14014
+ if (realtimeSecrets && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
14015
+ await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, realtimeSecrets.ticketSecret);
14016
+ }
14017
+ if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
14018
+ await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
13959
14019
  }
13960
14020
  await host.installAgent(config);
13961
14021
  if (config.kind === "platform" && config.firewall.enabled) {
@@ -13982,22 +14042,15 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
13982
14042
  }
13983
14043
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
13984
14044
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
13985
- const credentialFiles = config.runtime.credentialFiles ?? {};
13986
- const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
13987
- if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
13988
- throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
13989
- }
14045
+ const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
13990
14046
  for (const [name, source] of Object.entries({
13991
14047
  ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
13992
- "backup-s3-secret": platformPrivate.backup
14048
+ "backup.s3.secretAccessKey": platformPrivate.backup
13993
14049
  })) {
13994
14050
  if (source) {
13995
14051
  const destination2 = `${CREDS}/${name}.cred`;
13996
14052
  if (!host.exists(destination2)) {
13997
14053
  await seal(host, name, destination2, source);
13998
- const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
13999
- if (sourcePath)
14000
- host.remove(sourcePath);
14001
14054
  }
14002
14055
  }
14003
14056
  }
@@ -14067,7 +14120,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
14067
14120
  if (config.database.role === "master") {
14068
14121
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
14069
14122
  if (!host.exists(invite)) {
14070
- host.write(invite, `plt_${randomBytes7(24).toString("hex")}
14123
+ host.write(invite, `plt_${randomBytes6(24).toString("hex")}
14071
14124
  `, 384);
14072
14125
  await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
14073
14126
  }
@@ -14087,13 +14140,6 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
14087
14140
  verifiedAt: new Date().toISOString()
14088
14141
  };
14089
14142
  host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
14090
- `, 384);
14091
- }
14092
- if (!alreadyEnrolled) {
14093
- 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}`)}`;
14094
- if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
14095
- throw new Error("platform enrolment token is malformed");
14096
- host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
14097
14143
  `, 384);
14098
14144
  }
14099
14145
  await checked2(host, [
@@ -14105,15 +14151,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
14105
14151
  "deploy",
14106
14152
  ...config.database.role === "master" ? ["--release-executor"] : []
14107
14153
  ], "initial Agent deployment");
14108
- if (!alreadyEnrolled) {
14109
- await host.installAgent(config, PLATFORM_ENROL_SOURCE);
14110
- if (config.enrolment.source === "api-token")
14111
- host.remove(config.enrolment.tokenFile);
14112
- }
14113
14154
  }
14114
14155
  if (config.cloudflareHandoff) {
14115
- if (!host.exists(TUNNEL_CREDENTIAL) && cloudflare) {
14116
- await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
14156
+ if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
14157
+ await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, connectorCapabilities.connectorToken);
14117
14158
  }
14118
14159
  if (!host.exists(TUNNEL_CREDENTIAL))
14119
14160
  throw new Error("sealed cloudflared connector credential is missing");
@@ -14187,7 +14228,6 @@ function strictBootstrapDocument(value) {
14187
14228
  "installWarp",
14188
14229
  "cloudflareHandoff",
14189
14230
  "realm",
14190
- "enrolTokenFile",
14191
14231
  "software",
14192
14232
  "deploymentCredentials",
14193
14233
  "bootstrapRunner"
@@ -14196,8 +14236,8 @@ function strictBootstrapDocument(value) {
14196
14236
  exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
14197
14237
  if (root.cloudflareHandoff !== undefined)
14198
14238
  exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
14199
- exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
14200
- exactKeys2(root.enrolment, ["source", "tokenFile"], "platform enrolment config");
14239
+ exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
14240
+ exactKeys2(root.enrolment, ["source"], "platform enrolment config");
14201
14241
  const runtime = exactKeys2(root.runtime, [
14202
14242
  "environment",
14203
14243
  "serviceUser",
@@ -14205,8 +14245,7 @@ function strictBootstrapDocument(value) {
14205
14245
  "bluePort",
14206
14246
  "greenPort",
14207
14247
  "healthPath",
14208
- "keepReleases",
14209
- "credentialFiles"
14248
+ "keepReleases"
14210
14249
  ], "runtime config");
14211
14250
  exactKeys2(runtime.environment, [
14212
14251
  "softwareProfile",
@@ -14244,8 +14283,6 @@ function strictBootstrapDocument(value) {
14244
14283
  "cloudflare",
14245
14284
  "realtime"
14246
14285
  ], "runtime environment");
14247
- if (runtime.credentialFiles !== undefined)
14248
- exactKeys2(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
14249
14286
  const environment = runtime.environment;
14250
14287
  if (environment.email !== undefined) {
14251
14288
  const email = exactKeys2(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
@@ -14329,10 +14366,10 @@ function localBootstrapHost() {
14329
14366
  throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
14330
14367
  return result;
14331
14368
  },
14332
- async installAgent(config, enrolTokenSourcePath) {
14369
+ async installAgent(config) {
14333
14370
  const capabilities = await readCapabilities(localRunner);
14334
14371
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
14335
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync8("/var/lib/forgezero/enrolment.json");
14372
+ const hasBinding = config.kind === "enrolled-compute" || existsSync8(ENROL_CREDENTIAL) || existsSync8("/var/lib/forgezero/enrolment.json");
14336
14373
  if (config.kind === "platform") {
14337
14374
  const lifecycle = config.database.role === "none" ? {
14338
14375
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -14375,8 +14412,7 @@ function localBootstrapHost() {
14375
14412
  telemetryEndpoint: config.telemetryEndpoint,
14376
14413
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
14377
14414
  sourceBinPath: PACKAGED_AGENT_BIN,
14378
- ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
14379
- enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
14415
+ ...hasBinding ? {
14380
14416
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
14381
14417
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
14382
14418
  apiUrl: config.apiUrl,
@@ -14399,12 +14435,11 @@ function localBootstrapHost() {
14399
14435
  import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
14400
14436
  import { dirname as dirname9, resolve as resolve6 } from "path";
14401
14437
  async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
14402
- const tokens = await readCloudflareBootstrapTokens(input.tokenFiles);
14403
14438
  return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
14404
14439
  zoneName: input.zoneName,
14405
14440
  kvNamespaceTitle: input.kvNamespaceTitle,
14406
14441
  ...input.workerScriptName ? { workerScriptName: input.workerScriptName } : {},
14407
- ...tokens
14442
+ ...input.tokens
14408
14443
  }, dependencies.fetcher);
14409
14444
  }
14410
14445
  var exactKeys3 = (value, allowed, label) => {
@@ -14421,20 +14456,11 @@ function createCloudflareBootstrapCommandConfig(input) {
14421
14456
  if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
14422
14457
  throw new Error("Cloudflare bootstrap checkpointPath is required");
14423
14458
  }
14424
- for (const key of ["tunnelTokenFile", "apiTokenFile"]) {
14425
- if (typeof input.tokenFiles?.[key] !== "string" || !input.tokenFiles[key].trim()) {
14426
- throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
14427
- }
14428
- }
14429
14459
  return {
14430
14460
  format: 1,
14431
14461
  kind: "forgezero-cloudflare-bootstrap-request",
14432
14462
  checkpointPath: input.checkpointPath.trim(),
14433
- coordinates: validateCloudflareBootstrapCoordinates(input.coordinates),
14434
- tokenFiles: {
14435
- tunnelTokenFile: input.tokenFiles.tunnelTokenFile.trim(),
14436
- apiTokenFile: input.tokenFiles.apiTokenFile.trim()
14437
- }
14463
+ coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
14438
14464
  };
14439
14465
  }
14440
14466
  function readOwnerConfig(path) {
@@ -14462,7 +14488,7 @@ function readOwnerConfig(path) {
14462
14488
  function readCloudflareBootstrapCommandConfig(path, mode) {
14463
14489
  const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
14464
14490
  const baseDirectory = dirname9(resolve6(path));
14465
- exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates", "tokenFiles"], "Cloudflare bootstrap config");
14491
+ exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"], "Cloudflare bootstrap config");
14466
14492
  if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
14467
14493
  throw new Error("Cloudflare bootstrap config format/kind is invalid");
14468
14494
  }
@@ -14498,33 +14524,16 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
14498
14524
  }
14499
14525
  }
14500
14526
  const coordinates = validateCloudflareBootstrapCoordinates(coordinateSource);
14501
- let tokenFiles;
14502
- if (input.tokenFiles !== undefined) {
14503
- const source = record2(input.tokenFiles, "Cloudflare bootstrap tokenFiles");
14504
- const keys = ["tunnelTokenFile", "apiTokenFile"];
14505
- exactKeys3(source, keys, "Cloudflare bootstrap tokenFiles");
14506
- for (const key of keys) {
14507
- if (typeof source[key] !== "string" || !source[key].trim()) {
14508
- throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
14509
- }
14510
- }
14511
- tokenFiles = {
14512
- tunnelTokenFile: resolve6(baseDirectory, source.tunnelTokenFile),
14513
- apiTokenFile: resolve6(baseDirectory, source.apiTokenFile)
14514
- };
14515
- }
14516
- if (mode === "apply" && !tokenFiles) {
14517
- throw new Error("Cloudflare apply config requires exactly two owner-only tokenFiles paths");
14518
- }
14519
14527
  return {
14520
14528
  mode,
14521
14529
  coordinates,
14522
- checkpointPath: resolve6(baseDirectory, input.checkpointPath),
14523
- ...tokenFiles ? { tokenFiles } : {}
14530
+ checkpointPath: resolve6(baseDirectory, input.checkpointPath)
14524
14531
  };
14525
14532
  }
14526
- async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {}) {
14533
+ async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
14527
14534
  const request = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
14535
+ if (apply)
14536
+ request.tokens = tokens;
14528
14537
  const evidence = await (dependencies.run ?? runAttendedCloudflareBootstrap)(request);
14529
14538
  (dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
14530
14539
  `);
@@ -14562,7 +14571,7 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
14562
14571
  }
14563
14572
 
14564
14573
  // src/metal-bootstrap.ts
14565
- import { createHash as createHash4, randomBytes as randomBytes8 } from "crypto";
14574
+ import { createHash as createHash4, randomBytes as randomBytes7 } from "crypto";
14566
14575
  import {
14567
14576
  chmodSync as chmodSync4,
14568
14577
  chownSync,
@@ -15298,7 +15307,7 @@ async function applyMetalBootstrap(config, options) {
15298
15307
  atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
15299
15308
  `, 384);
15300
15309
  if (!existsSync9(SEED_CREDENTIAL_PATH)) {
15301
- const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
15310
+ const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes7(32).toString("base64url");
15302
15311
  if (seed.length < 32 || /[\0\r\n]/.test(seed))
15303
15312
  throw new MetalBootstrapError("metal Agent seed is invalid");
15304
15313
  await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
@@ -15464,7 +15473,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
15464
15473
  }
15465
15474
 
15466
15475
  // src/operator-bootstrap.ts
15467
- import { createHash as createHash5, randomBytes as randomBytes9 } from "crypto";
15476
+ import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
15468
15477
  import { lstatSync as lstatSync6, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
15469
15478
  import { isIP as isIP6 } from "net";
15470
15479
  import { tmpdir } from "os";
@@ -15752,12 +15761,15 @@ function planOperatorMetalBootstrap(request, mode) {
15752
15761
  };
15753
15762
  }
15754
15763
  var secretSources = (config) => [
15755
- ["cluster-code", config.database.bootstrapSecretFile],
15756
- ...config.enrolment.tokenFile ? [["enrol-token", config.enrolment.tokenFile]] : [],
15757
- ...config.runtime.credentialFiles?.emailSecret ? [["email-secret", config.runtime.credentialFiles.emailSecret]] : [],
15758
- ...config.runtime.credentialFiles?.backupS3Secret ? [["backup-s3-secret", config.runtime.credentialFiles.backupS3Secret]] : [],
15759
15764
  ...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
15760
15765
  ];
15766
+ var attendedSecretNames = (config) => [
15767
+ "cluster-bootstrap-code",
15768
+ config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
15769
+ ...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
15770
+ ...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
15771
+ ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
15772
+ ];
15761
15773
  function planOperatorPlatformBootstrap(request, mode) {
15762
15774
  const config = readBootstrapConfig(request.platformConfigFile);
15763
15775
  if (config.kind !== "platform")
@@ -15779,7 +15791,7 @@ function planOperatorPlatformBootstrap(request, mode) {
15779
15791
  `run typed fz bootstrap platform${mode === "prepare" ? " prepare" : ""} --apply`,
15780
15792
  "remove transient local and remote staging data"
15781
15793
  ],
15782
- secretInputs: secretSources(config).map(([name]) => name)
15794
+ secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
15783
15795
  };
15784
15796
  }
15785
15797
  var defaultExec3 = async (argv2, options = {}) => {
@@ -15896,15 +15908,7 @@ function stageConfig(config, directory) {
15896
15908
  writeFileSync11(local, bytes, { mode: 384, flag: "wx" });
15897
15909
  staged.push(name);
15898
15910
  const remotePath = `${REMOTE_STAGE}/${name}`;
15899
- if (name === "cluster-code")
15900
- rewritten.database.bootstrapSecretFile = remotePath;
15901
- else if (name === "enrol-token")
15902
- rewritten.enrolment.tokenFile = remotePath;
15903
- else if (name === "email-secret")
15904
- rewritten.runtime.credentialFiles.emailSecret = remotePath;
15905
- else if (name === "backup-s3-secret")
15906
- rewritten.runtime.credentialFiles.backupS3Secret = remotePath;
15907
- else if (name === "cloudflare-handoff")
15911
+ if (name === "cloudflare-handoff")
15908
15912
  rewritten.cloudflareHandoff.handoffFile = remotePath;
15909
15913
  }
15910
15914
  const path = join10(directory, "platform-config.json");
@@ -15988,7 +15992,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
15988
15992
  socketPath(request.target.agentSocket);
15989
15993
  const exec = options.exec ?? defaultExec3;
15990
15994
  const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-bootstrap-"));
15991
- const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
15995
+ const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
15992
15996
  let knownHosts = "";
15993
15997
  try {
15994
15998
  knownHosts = writeKnownHosts(request, directory);
@@ -15999,6 +16003,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
15999
16003
  const config = readBootstrapConfig(request.platformConfigFile);
16000
16004
  if (config.kind !== "platform")
16001
16005
  throw new Error("operator bootstrap requires a platform config");
16006
+ const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
16002
16007
  const staged = stageConfig(config, directory);
16003
16008
  await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
16004
16009
  await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
@@ -16009,8 +16014,11 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
16009
16014
  const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform"];
16010
16015
  if (mode === "prepare")
16011
16016
  command.push("prepare");
16017
+ else
16018
+ command.push("credentials-stdin");
16012
16019
  command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
16013
- const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap");
16020
+ const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
16021
+ ` : undefined);
16014
16022
  return { plan, output };
16015
16023
  } finally {
16016
16024
  if (knownHosts) {
@@ -16027,7 +16035,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
16027
16035
  socketPath(request.target.agentSocket);
16028
16036
  const exec = options.exec ?? defaultExec3;
16029
16037
  const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-metal-"));
16030
- const remoteTemp = `/tmp/forgezero-operator-${randomBytes9(12).toString("hex")}`;
16038
+ const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
16031
16039
  let knownHosts = "";
16032
16040
  try {
16033
16041
  knownHosts = writeKnownHosts(request, directory);
@@ -16102,8 +16110,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
16102
16110
  serverMode: "default",
16103
16111
  address: guest.address,
16104
16112
  ...role === "joiner" ? { master } : {},
16105
- coordinators,
16106
- bootstrapSecretFile: template.database.bootstrapSecretFile
16113
+ coordinators
16107
16114
  };
16108
16115
  config.enrolment = { source: "genesis-derived" };
16109
16116
  config.installCloudflared = true;
@@ -16985,8 +16992,10 @@ async function cmdStatus(options) {
16985
16992
  async function cmdAgent(options, args) {
16986
16993
  if (args[0] === "activate") {
16987
16994
  try {
16988
- if (args[1] !== undefined)
16989
- throw new Error("internal Agent activation accepts no positional arguments");
16995
+ const credentialStdin = args[1] === "credentials-stdin";
16996
+ if (args[1] !== undefined && !credentialStdin || args[2] !== undefined) {
16997
+ throw new Error("internal Agent activation accepts only credentials-stdin");
16998
+ }
16990
16999
  if (!options.bootstrapConfigPath)
16991
17000
  throw new Error("internal Agent activation requires --bootstrap-config");
16992
17001
  const config = readBootstrapConfig(options.bootstrapConfigPath);
@@ -16997,7 +17006,19 @@ async function cmdAgent(options, args) {
16997
17006
  out.line(JSON.stringify(plan2, null, 2));
16998
17007
  return 0;
16999
17008
  }
17000
- out.line(JSON.stringify(await applyBootstrap(config), null, 2));
17009
+ if (!credentialStdin)
17010
+ throw new Error("internal Agent activation requires credentials-stdin");
17011
+ const body = await Bun.stdin.text();
17012
+ if (body.length < 2 || body.length > 65536)
17013
+ throw new Error("activation credential stdin has an invalid size");
17014
+ let parsed;
17015
+ try {
17016
+ parsed = JSON.parse(body);
17017
+ } catch {
17018
+ throw new Error("activation credential stdin is not valid JSON");
17019
+ }
17020
+ const secrets = validateEnrolledComputeBootstrapSecrets(config, parsed);
17021
+ out.line(JSON.stringify(await applyBootstrap(config, undefined, secrets), null, 2));
17001
17022
  return 0;
17002
17023
  } catch (cause) {
17003
17024
  out.fail(cause instanceof Error ? cause.message : String(cause));
@@ -17008,7 +17029,6 @@ async function cmdAgent(options, args) {
17008
17029
  out.fail("Usage: fz agent install [--apply]");
17009
17030
  return 1;
17010
17031
  }
17011
- const enrolTokenSourcePath = "/run/forgezero-enrol-token";
17012
17032
  const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
17013
17033
  const enrolStatePath = "/var/lib/forgezero/enrolment.json";
17014
17034
  const gitCredentialPath = process.env.FZ_GIT_CREDENTIAL_PATH ?? "/etc/forgezero/creds/git-deploy-key.cred";
@@ -17048,7 +17068,6 @@ async function cmdAgent(options, args) {
17048
17068
  telemetryEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
17049
17069
  ...options.enrol ? {
17050
17070
  pullDeployments: true,
17051
- enrolTokenSourcePath,
17052
17071
  enrolTokenCredentialPath,
17053
17072
  enrolStatePath,
17054
17073
  deployRoot: process.env.FZ_DEPLOY_ROOT ?? "/opt/forgezero"
@@ -17086,23 +17105,20 @@ async function cmdAgent(options, args) {
17086
17105
  out.ok(`Wrote ${auxiliary.path}`);
17087
17106
  }
17088
17107
  if (options.enrol) {
17089
- if (existsSync11(enrolTokenSourcePath)) {
17090
- const source = statSync2(enrolTokenSourcePath);
17091
- const token = readFileSync14(enrolTokenSourcePath, "utf8").trim();
17092
- if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
17093
- throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
17094
- }
17095
- if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
17096
- throw new Error("The preloaded enrolment token is malformed.");
17097
- }
17098
- } else {
17099
- const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", "ForgeZero one-time enrolment token:"], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
17100
- const token = (await new Response(prompt.stdout).text()).trim();
17101
- if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
17108
+ if (!existsSync11(enrolStatePath) && !existsSync11(enrolTokenCredentialPath)) {
17109
+ const token = await bootstrapSecret("ForgeZero one-time enrolment token");
17110
+ if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
17102
17111
  throw new Error("A valid fze_ enrolment token was not provided.");
17112
+ mkdirSync11(dirname12(enrolTokenCredentialPath), { recursive: true, mode: 448 });
17113
+ const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
17114
+ if (sealing.stdin && typeof sealing.stdin !== "number") {
17115
+ sealing.stdin.write(`${token}
17116
+ `);
17117
+ sealing.stdin.end();
17103
17118
  }
17104
- writeFileSync12(enrolTokenSourcePath, `${token}
17105
- `, { mode: 384, flag: "wx" });
17119
+ const error = await new Response(sealing.stderr).text();
17120
+ if (await sealing.exited !== 0)
17121
+ throw new Error(`could not seal enrolment credential: ${error.trim()}`);
17106
17122
  }
17107
17123
  }
17108
17124
  const transcript = await applyPlan(plan, localRunner);
@@ -17116,11 +17132,6 @@ async function cmdAgent(options, args) {
17116
17132
  out.line();
17117
17133
  return 0;
17118
17134
  } catch (cause) {
17119
- if (options.enrol) {
17120
- try {
17121
- unlinkSync3(enrolTokenSourcePath);
17122
- } catch {}
17123
- }
17124
17135
  out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
17125
17136
  out.step("Run this as root, or use --json and hand the unit to your provisioner.");
17126
17137
  return 1;
@@ -17141,6 +17152,27 @@ var bootstrapNumber = (question, fallback) => {
17141
17152
  throw new Error(`${question} must be an integer`);
17142
17153
  return value;
17143
17154
  };
17155
+ async function bootstrapSecret(question) {
17156
+ if (!process.stdin.isTTY)
17157
+ throw new Error(`${question} requires an interactive terminal`);
17158
+ const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", `${question}:`], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
17159
+ const value = (await new Response(prompt.stdout).text()).trim();
17160
+ if (await prompt.exited !== 0 || !value)
17161
+ throw new Error(`${question} was not provided`);
17162
+ return value;
17163
+ }
17164
+ async function promptPlatformBootstrapSecrets(config) {
17165
+ return validatePlatformBootstrapSecrets(config, {
17166
+ clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
17167
+ emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
17168
+ ...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
17169
+ ...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
17170
+ ...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
17171
+ cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
17172
+ cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
17173
+ } : {}
17174
+ });
17175
+ }
17144
17176
  var bootstrapList = (question, fallback) => bootstrapAnswer(question, fallback).split(",").map((value) => value.trim()).filter(Boolean);
17145
17177
  var parseCpuPools = (value) => value.split(";").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
17146
17178
  const [key, cpus, physicalCores, memoryNodes] = entry.split("|").map((field) => field?.trim());
@@ -17211,9 +17243,9 @@ function writeBootstrapConfig(path, config) {
17211
17243
  async function interactiveCloudflareBootstrap() {
17212
17244
  if (!process.stdin.isTTY)
17213
17245
  throw new Error("interactive Cloudflare config generation requires a terminal");
17214
- const tokenFiles = {
17215
- tunnelTokenFile: bootstrapAnswer("Owner-only CF_TUNNEL_TOKEN file", "./CF_TUNNEL_TOKEN"),
17216
- apiTokenFile: bootstrapAnswer("Owner-only CF_API_TOKEN file", "./CF_API_TOKEN")
17246
+ const tokens = {
17247
+ tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
17248
+ apiToken: await bootstrapSecret("CF_API_TOKEN")
17217
17249
  };
17218
17250
  const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
17219
17251
  const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
@@ -17223,7 +17255,7 @@ async function interactiveCloudflareBootstrap() {
17223
17255
  zoneName,
17224
17256
  kvNamespaceTitle,
17225
17257
  ...workerScriptName ? { workerScriptName } : {},
17226
- tokenFiles
17258
+ tokens
17227
17259
  });
17228
17260
  const nodeCount = bootstrapNumber("Number of public API nodes", "3");
17229
17261
  if (nodeCount < 1 || nodeCount > 32)
@@ -17261,8 +17293,7 @@ async function interactiveCloudflareBootstrap() {
17261
17293
  }
17262
17294
  } : {},
17263
17295
  nodes
17264
- },
17265
- tokenFiles
17296
+ }
17266
17297
  });
17267
17298
  }
17268
17299
  function genesisOutputDirectory(path) {
@@ -17277,20 +17308,9 @@ function genesisOutputDirectory(path) {
17277
17308
  }
17278
17309
  return path;
17279
17310
  }
17280
- function ensureGenesisClusterSecret(directory) {
17281
- const path = `${directory}/cluster-bootstrap.code`;
17282
- if (!existsSync11(path))
17283
- writeFileSync12(path, `${randomBytes10(32).toString("hex")}
17284
- `, { mode: 384, flag: "wx" });
17285
- if (!/^[a-f0-9]{64}$/i.test(readOwnerOnlySecret(path, "cluster bootstrap code"))) {
17286
- throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
17287
- }
17288
- return path;
17289
- }
17290
17311
  function writePlatformGenesisFleet(directory) {
17291
17312
  const output = genesisOutputDirectory(directory);
17292
- const secret = ensureGenesisClusterSecret(output);
17293
- const template = interactiveBootstrap("platform", { bootstrapSecretFile: secret });
17313
+ const template = interactiveBootstrap("platform", true);
17294
17314
  const fleet = PLATFORM_GENESIS_FLEETS[template.environment];
17295
17315
  const nodes = fleet.map((guest, index) => index === 0 ? {
17296
17316
  nodeHostname: template.nodeHostname,
@@ -17304,9 +17324,9 @@ function writePlatformGenesisFleet(directory) {
17304
17324
  writeBootstrapConfig(path, config);
17305
17325
  return path;
17306
17326
  });
17307
- return { secret, configs };
17327
+ return { configs };
17308
17328
  }
17309
- function interactiveBootstrap(kind, genesis) {
17329
+ function interactiveBootstrap(kind, genesis = false) {
17310
17330
  if (!process.stdin.isTTY)
17311
17331
  throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
17312
17332
  const environment = bootstrapAnswer("Environment (production/development)", "production");
@@ -17340,18 +17360,14 @@ function interactiveBootstrap(kind, genesis) {
17340
17360
  const smtpPort = emailProvider === "smtp" ? Number(bootstrapAnswer("SMTP port", "587")) : undefined;
17341
17361
  const smtpUser = emailProvider === "smtp" ? bootstrapAnswer("SMTP user") : undefined;
17342
17362
  const jetemailEu = emailProvider === "jetemail" ? bootstrapAnswer("Use JetEmail EU processing? (yes/no)", "no") === "yes" : undefined;
17343
- const emailSecret = bootstrapAnswer(emailProvider === "smtp" ? "Root-only SMTP password file" : "Root-only JetEmail API-key file");
17344
17363
  const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
17345
17364
  const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
17346
17365
  const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
17347
17366
  const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
17348
- const backupS3Secret = backupEndpoint ? bootstrapAnswer("Root-only backup S3 secret file") : undefined;
17349
17367
  const cloudflareHandoffFile = bootstrapAnswer(genesis ? `${computeReference} node-specific Cloudflare host handoff` : "Node-specific Cloudflare host handoff (blank for no public edge)", genesis ? undefined : "");
17350
17368
  const cloudflareNodeName = cloudflareHandoffFile ? bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
17351
17369
  const databaseNetworkMode = "private-lan";
17352
- const bootstrapSecretFile = genesis?.bootstrapSecretFile ?? bootstrapAnswer("Root-only shared cluster bootstrap-code file");
17353
17370
  const enrolmentSource = genesis ? "genesis-derived" : bootstrapAnswer("Enrolment source (genesis-derived/api-token)", "genesis-derived");
17354
- const platformEnrolTokenFile = enrolmentSource === "api-token" ? bootstrapAnswer("Root-only API-issued platform enrolment-token file") : undefined;
17355
17371
  return {
17356
17372
  kind,
17357
17373
  environment,
@@ -17368,10 +17384,9 @@ function interactiveBootstrap(kind, genesis) {
17368
17384
  serverMode,
17369
17385
  address,
17370
17386
  master,
17371
- coordinators,
17372
- bootstrapSecretFile
17387
+ coordinators
17373
17388
  },
17374
- enrolment: { source: enrolmentSource, ...platformEnrolTokenFile ? { tokenFile: platformEnrolTokenFile } : {} },
17389
+ enrolment: { source: enrolmentSource },
17375
17390
  runtime: {
17376
17391
  environment: {
17377
17392
  softwareProfile: profile,
@@ -17414,11 +17429,7 @@ function interactiveBootstrap(kind, genesis) {
17414
17429
  bluePort: 3001,
17415
17430
  greenPort: 3002,
17416
17431
  healthPath: "/api/health",
17417
- keepReleases: 5,
17418
- credentialFiles: {
17419
- emailSecret,
17420
- backupS3Secret
17421
- }
17432
+ keepReleases: 5
17422
17433
  },
17423
17434
  firewall: {
17424
17435
  enabled: bootstrapAnswer("Enable host firewall? (yes/no)", "yes") === "yes",
@@ -17468,7 +17479,11 @@ async function cmdBootstrap(options, args) {
17468
17479
  out.step("Review the pinned target and secret names, then repeat with --apply to contact the host.");
17469
17480
  return 0;
17470
17481
  }
17471
- out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode), null, 2));
17482
+ const platformConfig = readBootstrapConfig(request.platformConfigFile);
17483
+ if (platformConfig.kind !== "platform")
17484
+ throw new Error("remote platform bootstrap requires a platform config");
17485
+ const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(platformConfig) : undefined;
17486
+ out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode, { secrets: secrets2 }), null, 2));
17472
17487
  return 0;
17473
17488
  }
17474
17489
  if (operation === "metal" && args[1] === "remote") {
@@ -17509,7 +17524,7 @@ async function cmdBootstrap(options, args) {
17509
17524
  steps: [
17510
17525
  "probe every declared public node",
17511
17526
  "write secret-free acceptance evidence",
17512
- "remove the capability-bearing checkpoint and node handoffs"
17527
+ "remove the completed non-secret checkpoint and node handoffs"
17513
17528
  ]
17514
17529
  }, null, 2));
17515
17530
  out.step("Review the evidence destination, then repeat with --apply to finalize and remove bootstrap capabilities.");
@@ -17520,7 +17535,11 @@ async function cmdBootstrap(options, args) {
17520
17535
  }
17521
17536
  if (args[2] !== undefined)
17522
17537
  throw new Error("Cloudflare bootstrap operation must be plan/apply, verify or finalize");
17523
- await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply);
17538
+ const tokens = options.apply ? {
17539
+ tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
17540
+ apiToken: await bootstrapSecret("CF_API_TOKEN")
17541
+ } : undefined;
17542
+ await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply, tokens);
17524
17543
  return 0;
17525
17544
  }
17526
17545
  if (operation === "platform" && args[1] === "prepare") {
@@ -17542,6 +17561,9 @@ async function cmdBootstrap(options, args) {
17542
17561
  out.line(JSON.stringify(await preparePlatformBootstrap(config2), null, 2));
17543
17562
  return 0;
17544
17563
  }
17564
+ const credentialStdin = operation === "platform" && args[1] === "credentials-stdin";
17565
+ if (credentialStdin && args[2] !== undefined)
17566
+ throw new Error("platform credential stdin accepts no additional positional arguments");
17545
17567
  const installedBootstrapKind = () => resolveInstalledBootstrapKind({
17546
17568
  metal: existsSync11(METAL_BOOTSTRAP_STATE_PATH),
17547
17569
  compute: existsSync11(BOOTSTRAP_STATE_PATH)
@@ -17602,7 +17624,21 @@ async function cmdBootstrap(options, args) {
17602
17624
  out.step("Review the plan, then repeat with --apply as root.");
17603
17625
  return 0;
17604
17626
  }
17605
- const result = await applyBootstrap(config);
17627
+ const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config) : undefined;
17628
+ let stdinSecrets = secrets;
17629
+ if (config.kind === "platform" && credentialStdin) {
17630
+ const body = await Bun.stdin.text();
17631
+ if (body.length < 2 || body.length > 65536)
17632
+ throw new Error("bootstrap credential stdin has an invalid size");
17633
+ let parsed;
17634
+ try {
17635
+ parsed = JSON.parse(body);
17636
+ } catch {
17637
+ throw new Error("bootstrap credential stdin is not valid JSON");
17638
+ }
17639
+ stdinSecrets = validatePlatformBootstrapSecrets(config, parsed);
17640
+ }
17641
+ const result = await applyBootstrap(config, undefined, stdinSecrets);
17606
17642
  out.line(JSON.stringify(result, null, 2));
17607
17643
  return 0;
17608
17644
  } catch (cause) {
@@ -18155,13 +18191,13 @@ function usage() {
18155
18191
  strict three-node platform genesis directory; never apply locally
18156
18192
  fz bootstrap platform cloudflare
18157
18193
  Plan/apply attended Tunnel and DNS reconciliation
18158
- using supplied management and KV/Worker runtime token files
18194
+ using hidden management and KV/Worker runtime token prompts
18159
18195
  fz bootstrap platform cloudflare verify
18160
18196
  From the operator laptop, prove every ordinary public
18161
18197
  node /api/health using the 0600 checkpoint
18162
18198
  fz bootstrap platform cloudflare finalize
18163
18199
  Persist secret-free public-node acceptance, then remove
18164
- the capability-bearing checkpoint and node handoffs
18200
+ the completed non-secret checkpoint and node handoffs
18165
18201
  fz bootstrap metal Plan/install an identity-only physical provisioner
18166
18202
  fz bootstrap metal remote <apply|genesis|rehearsal|status>
18167
18203
  Install/bootstrap one pinned blank metal target, then