@forgezero/agent 0.1.34 → 0.1.36

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
@@ -4857,9 +4857,9 @@ async function spawnWith(command, env, report = () => {}) {
4857
4857
 
4858
4858
  // src/cli/index.ts
4859
4859
  init_dist();
4860
- import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
4861
- import { dirname as dirname3 } from "path";
4862
- import { fileURLToPath } from "url";
4860
+ import { existsSync as existsSync6, lstatSync as lstatSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
4861
+ import { dirname as dirname8 } from "path";
4862
+ import { fileURLToPath as fileURLToPath2 } from "url";
4863
4863
  import { hostname } from "os";
4864
4864
 
4865
4865
  // src/agent-update.ts
@@ -4875,7 +4875,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4875
4875
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4876
4876
 
4877
4877
  // src/version.ts
4878
- var VERSION2 = "0.1.34";
4878
+ var VERSION2 = "0.1.36";
4879
4879
 
4880
4880
  // src/software.ts
4881
4881
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
@@ -4889,7 +4889,8 @@ var SOFTWARE_CATALOG = [
4889
4889
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4890
4890
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4891
4891
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4892
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4892
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4893
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4893
4894
  ];
4894
4895
  var UBUNTU_2604_X64 = [
4895
4896
  {
@@ -4916,6 +4917,11 @@ var UBUNTU_2604_X64 = [
4916
4917
  requirement: { id: "ufw", version: "ubuntu-26.04" },
4917
4918
  check: "command -v ufw >/dev/null",
4918
4919
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
4920
+ },
4921
+ {
4922
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
4923
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
4924
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
4919
4925
  }
4920
4926
  ];
4921
4927
  function validateSoftwareRequirements(value, _options = {}) {
@@ -4929,7 +4935,7 @@ function validateSoftwareRequirements(value, _options = {}) {
4929
4935
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
4930
4936
  throw new Error("software requirement contains an unknown field");
4931
4937
  }
4932
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4938
+ if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4933
4939
  throw new Error("software requirement coordinate is invalid");
4934
4940
  }
4935
4941
  const requirement = { id: row.id, version: row.version };
@@ -5482,7 +5488,12 @@ function agentUnit(options) {
5482
5488
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
5483
5489
  throw new Error("migration pull and lifecycle profile must be supplied together");
5484
5490
  }
5491
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
5492
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
5493
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
5494
+ }
5485
5495
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
5496
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5486
5497
  const warpValues = [
5487
5498
  options.warpOrganization,
5488
5499
  options.warpClientIdCredentialPath,
@@ -5548,6 +5559,9 @@ function agentUnit(options) {
5548
5559
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
5549
5560
  options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
5550
5561
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
5562
+ options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
5563
+ options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
5564
+ options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
5551
5565
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
5552
5566
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
5553
5567
  ].filter((line) => line !== null);
@@ -5555,6 +5569,8 @@ function agentUnit(options) {
5555
5569
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
5556
5570
  }
5557
5571
  const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
5572
+ ` : "";
5573
+ const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
5558
5574
  ` : "";
5559
5575
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
5560
5576
  `);
@@ -5609,7 +5625,7 @@ User=${user}
5609
5625
  Group=${VAULT_GROUP}
5610
5626
  ${deploymentGroup}
5611
5627
  LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
5612
- ${gitCredential}${projectCredentials}${projectCredentials ? `
5628
+ ${gitCredential}${bootstrapCredential}${projectCredentials}${projectCredentials ? `
5613
5629
  ` : ""}${snpPrepare}ExecStart=${bin}
5614
5630
  Restart=always
5615
5631
  RestartSec=2
@@ -5664,6 +5680,10 @@ function planProvision(options) {
5664
5680
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
5665
5681
  throw new Error("migration pull and lifecycle profile must be supplied together");
5666
5682
  }
5683
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
5684
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
5685
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
5686
+ }
5667
5687
  const warpValues = [
5668
5688
  options.warpOrganization,
5669
5689
  options.warpClientIdCredentialPath,
@@ -5689,13 +5709,20 @@ function planProvision(options) {
5689
5709
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
5690
5710
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
5691
5711
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
5712
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
5692
5713
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
5693
5714
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
5694
5715
  return {
5695
5716
  mode,
5696
5717
  reason: reasonFor(mode),
5697
5718
  unitPath: UNIT_PATH,
5698
- unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
5719
+ unit: agentUnit({
5720
+ ...options,
5721
+ mode,
5722
+ lifecycleProfilePath,
5723
+ lifecycleHelperSocketPath,
5724
+ bootstrapSshCredentialPath
5725
+ }),
5699
5726
  auxiliaryUnits: [
5700
5727
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
5701
5728
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
@@ -5806,7 +5833,7 @@ function planProvision(options) {
5806
5833
  },
5807
5834
  {
5808
5835
  label: "encrypted one-time enrolment capability",
5809
- command: `test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
5836
+ command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
5810
5837
  }
5811
5838
  ] : [],
5812
5839
  ...deploymentEnabled ? [{
@@ -10090,249 +10117,3583 @@ function removeSession(api, realm, path = defaultSessionPath()) {
10090
10117
  persist(path, file);
10091
10118
  }
10092
10119
 
10093
- // src/cli/index.ts
10094
- var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
10095
- var DEFAULT_MODE = RECOMMENDED_MODE;
10096
- var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
10097
- function parseOptions(argv) {
10098
- const options = {
10099
- api: process.env.FZ_API ?? "http://localhost:8787",
10100
- apiExplicit: Boolean(process.env.FZ_API),
10101
- app: process.env.FZ_APP,
10102
- realm: process.env.FZ_REALM ?? "platform",
10103
- realmExplicit: Boolean(process.env.FZ_REALM),
10104
- json: false,
10105
- apply: false,
10106
- enrol: false,
10107
- socket: process.env.SSH_AUTH_SOCK,
10108
- mode: DEFAULT_MODE,
10109
- user: process.env.FZ_USER ?? "operator",
10110
- userExplicit: Boolean(process.env.FZ_USER),
10111
- phraseStdin: false,
10112
- preserveEnv: false,
10113
- projectRoot: process.cwd(),
10114
- deployProfile: "app",
10115
- deploySoftware: [],
10116
- deployChannel: "production",
10117
- requireAttestation: false,
10118
- force: false,
10119
- noBrowser: false,
10120
- provider: "github",
10121
- branch: "main",
10122
- authMode: "public",
10123
- targets: [],
10124
- wait: false,
10125
- timeoutSeconds: 900,
10126
- queries: []
10127
- };
10128
- const positional = [];
10129
- for (let index = 0;index < argv.length; index += 1) {
10130
- const token = argv[index];
10131
- if (token === "--api") {
10132
- options.api = argv[++index] ?? options.api;
10133
- options.apiExplicit = true;
10134
- } else if (token === "--app")
10135
- options.app = argv[++index];
10136
- else if (token === "--realm") {
10137
- options.realm = argv[++index] ?? options.realm;
10138
- options.realmExplicit = true;
10139
- } else if (token === "--socket")
10140
- options.socket = argv[++index];
10141
- else if (token === "--json")
10142
- options.json = true;
10143
- else if (token === "--apply")
10144
- options.apply = true;
10145
- else if (token === "--enrol")
10146
- options.enrol = true;
10147
- else if (token === "--preserve-env")
10148
- options.preserveEnv = true;
10149
- else if (token === "--root")
10150
- options.projectRoot = argv[++index] ?? options.projectRoot;
10151
- else if (token === "--name")
10152
- options.projectName = argv[++index];
10153
- else if (token === "--purpose")
10154
- options.projectPurpose = argv[++index];
10155
- else if (token === "--profile")
10156
- options.deployProfile = argv[++index] ?? options.deployProfile;
10157
- else if (token === "--software")
10158
- options.deploySoftware.push(argv[++index] ?? "");
10159
- else if (token === "--channel") {
10160
- const channel = argv[++index];
10161
- if (channel === "development" || channel === "production")
10162
- options.deployChannel = channel;
10163
- else
10164
- options.optionError = "--channel must be production or development.";
10165
- } else if (token === "--attestation")
10166
- options.requireAttestation = true;
10167
- else if (token === "--force")
10168
- options.force = true;
10169
- else if (token === "--no-browser")
10170
- options.noBrowser = true;
10171
- else if (token === "--project")
10172
- options.projectKey = argv[++index];
10173
- else if (token === "--pipeline")
10174
- options.pipelineKey = argv[++index];
10175
- else if (token === "--revision")
10176
- options.revision = argv[++index];
10177
- else if (token === "--label")
10178
- options.label = argv[++index];
10179
- else if (token === "--provider") {
10180
- const value = argv[++index];
10181
- if (value === "github" || value === "gitlab" || value === "generic")
10182
- options.provider = value;
10183
- else
10184
- options.optionError = "--provider must be github, gitlab, or generic.";
10185
- } else if (token === "--repository")
10186
- options.repository = argv[++index];
10187
- else if (token === "--branch")
10188
- options.branch = argv[++index] ?? options.branch;
10189
- else if (token === "--clone-url")
10190
- options.cloneUrl = argv[++index];
10191
- else if (token === "--auth") {
10192
- const value = argv[++index];
10193
- if (value === "public" || value === "node-ssh" || value === "vault-token")
10194
- options.authMode = value;
10195
- else
10196
- options.optionError = "--auth must be public, node-ssh, or vault-token.";
10197
- } else if (token === "--git-secret")
10198
- options.gitSecret = argv[++index];
10199
- else if (token === "--git-username")
10200
- options.gitUsername = argv[++index];
10201
- else if (token === "--known-hosts")
10202
- options.knownHostsFile = argv[++index];
10203
- else if (token === "--target")
10204
- options.targets.push(argv[++index] ?? "");
10205
- else if (token === "--target-key")
10206
- options.targetKey = argv[++index];
10207
- else if (token === "--wait")
10208
- options.wait = true;
10209
- else if (token === "--timeout") {
10210
- const value = Number(argv[++index] ?? "");
10211
- if (Number.isFinite(value) && value > 0 && value <= 86400)
10212
- options.timeoutSeconds = value;
10213
- else
10214
- options.optionError = "--timeout must be between 1 and 86400 seconds.";
10215
- } else if (token === "--data")
10216
- options.data = argv[++index];
10217
- else if (token === "--data-file")
10218
- options.dataFile = argv[++index];
10219
- else if (token === "--query")
10220
- options.queries.push(argv[++index] ?? "");
10221
- else if (token === "--key")
10222
- options.key = argv[++index];
10223
- else if (token === "--mode")
10224
- options.mode = argv[++index] ?? options.mode;
10225
- else if (token === "--user") {
10226
- options.user = argv[++index] ?? options.user;
10227
- options.userExplicit = true;
10228
- } else if (token === "--token-file")
10229
- options.tokenFile = argv[++index];
10230
- else if (token === "--phrase-file")
10231
- options.phraseFile = argv[++index];
10232
- else if (token === "--phrase-stdin")
10233
- options.phraseStdin = true;
10234
- else
10235
- positional.push(token);
10120
+ // src/bootstrap.ts
10121
+ import { createHmac, randomBytes as randomBytes5 } from "crypto";
10122
+ import {
10123
+ chmodSync as chmodSync2,
10124
+ existsSync as existsSync4,
10125
+ lstatSync as lstatSync2,
10126
+ mkdirSync as mkdirSync4,
10127
+ readFileSync as readFileSync4,
10128
+ renameSync as renameSync3,
10129
+ rmSync,
10130
+ writeFileSync as writeFileSync4
10131
+ } from "fs";
10132
+ import { dirname as dirname4 } from "path";
10133
+ import { fileURLToPath } from "url";
10134
+
10135
+ // src/platform-bootstrap-runtime.ts
10136
+ var safeAtom = (name, value) => {
10137
+ if (!value || /[\0\r\n]/.test(value))
10138
+ throw new Error(`${name} must be non-empty and single-line.`);
10139
+ return value;
10140
+ };
10141
+ var boundedInteger = (name, value, minimum, maximum) => {
10142
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
10143
+ throw new Error(`${name} must be an integer from ${minimum} through ${maximum}.`);
10236
10144
  }
10237
- return { command: positional[0] ?? "help", args: positional.slice(1), options };
10238
- }
10239
- var out = {
10240
- line: (text3 = "") => process.stdout.write(`${text3}
10241
- `),
10242
- step: (text3) => process.stdout.write(` ${text3}
10243
- `),
10244
- warn: (text3) => process.stderr.write(` ! ${text3}
10245
- `),
10246
- fail: (text3) => process.stderr.write(` \u2717 ${text3}
10247
- `),
10248
- ok: (text3) => process.stdout.write(` \u2713 ${text3}
10249
- `)
10145
+ return value;
10250
10146
  };
10251
- var sessionCookie = null;
10252
- var storedSession = null;
10253
- function captureSession(response) {
10254
- const setCookie = response.headers.get("set-cookie");
10255
- if (!setCookie)
10256
- return;
10257
- const cookie = setCookie.split(";")[0];
10258
- sessionCookie = cookie;
10259
- if (storedSession && (cookie !== storedSession.cookie || Date.now() - storedSession.lastUsedAtTs >= 60000)) {
10260
- storedSession = { ...storedSession, cookie, lastUsedAtTs: Date.now() };
10261
- saveSession(storedSession);
10147
+ var privateCoordinator = (raw) => {
10148
+ const url = new URL(raw);
10149
+ if (url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
10150
+ throw new Error("ArangoDB coordinator URLs must be credential-free private HTTP origins.");
10262
10151
  }
10263
- }
10264
- function requestHeaders(apiBase, cookie) {
10265
- return {
10266
- "content-type": "application/json",
10267
- origin: new URL(apiBase).origin,
10268
- ...cookie ? { cookie } : {}
10269
- };
10270
- }
10271
- async function api(options, path, init) {
10272
- const base = options.realm === "platform" ? "/api" : `/api/t/${encodeURIComponent(options.realm)}`;
10273
- const response = await fetch(`${options.api}${base}${path}`, {
10274
- method: init?.method ?? "GET",
10275
- headers: { ...requestHeaders(options.api, sessionCookie), "user-agent": `forgezero-cli/${VERSION2}` },
10276
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10277
- });
10278
- captureSession(response);
10279
- let body = null;
10280
- try {
10281
- body = await response.json();
10282
- } catch {
10283
- body = null;
10152
+ const host = url.hostname.replace(/^\[|\]$/g, "");
10153
+ const privateHost = host === "localhost" || host === "::1" || host.startsWith("fd") || host.startsWith("fc") || /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
10154
+ if (!privateHost || url.port && url.port !== "8529") {
10155
+ throw new Error("ArangoDB coordinators must use private addresses and port 8529.");
10284
10156
  }
10285
- const challenge = body?.security;
10286
- if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && custodyIdentity && !path.startsWith("/security/step-up/")) {
10287
- const proved = await proveWithAgent(options, challenge.requestKey);
10288
- if (proved) {
10289
- const replay = await fetch(`${options.api}${base}${path}`, {
10290
- method: init?.method ?? "GET",
10291
- headers: {
10292
- ...requestHeaders(options.api, sessionCookie),
10293
- "x-security-request-key": challenge.requestKey
10294
- },
10295
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10296
- });
10297
- captureSession(replay);
10298
- let replayed = null;
10299
- try {
10300
- replayed = await replay.json();
10301
- } catch {
10302
- replayed = null;
10303
- }
10304
- return { status: replay.status, body: replayed };
10305
- }
10157
+ return url.origin;
10158
+ };
10159
+ var httpsOrigin = (name, raw) => {
10160
+ const value = new URL(raw);
10161
+ if (value.protocol !== "https:" || value.username || value.password || value.search || value.hash || value.pathname !== "/") {
10162
+ throw new Error(`${name} must be a credential-free HTTPS origin.`);
10306
10163
  }
10307
- if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && storedSession && !path.startsWith("/security/step-up/")) {
10308
- const proved = await proveWithBrowser(options, challenge.requestKey, base);
10309
- if (proved) {
10310
- const replay = await fetch(`${options.api}${base}${path}`, {
10311
- method: init?.method ?? "GET",
10312
- headers: {
10313
- ...requestHeaders(options.api, sessionCookie),
10314
- "user-agent": `forgezero-cli/${VERSION2}`,
10315
- "x-security-request-key": challenge.requestKey
10316
- },
10317
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10318
- });
10319
- captureSession(replay);
10320
- let replayed = null;
10321
- try {
10322
- replayed = await replay.json();
10323
- } catch {}
10324
- return { status: replay.status, body: replayed };
10164
+ return value.origin;
10165
+ };
10166
+ var systemdValue = (name, raw) => {
10167
+ if (/[\0\r\n]/.test(raw))
10168
+ throw new Error(`${name} must be single-line.`);
10169
+ const value = raw;
10170
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
10171
+ };
10172
+ function validatePlatformSharedEnvironment(input, options = {}) {
10173
+ if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
10174
+ throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
10175
+ }
10176
+ if (input.databaseCoordinators.length < 1 || input.databaseCoordinators.length > 16) {
10177
+ throw new Error("databaseCoordinators must contain 1 through 16 endpoints.");
10178
+ }
10179
+ const coordinators = input.databaseCoordinators.map(privateCoordinator);
10180
+ if (new Set(coordinators).size !== coordinators.length)
10181
+ throw new Error("databaseCoordinators must be unique.");
10182
+ if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
10183
+ throw new Error("Database networking must be private-lan or cloudflare-warp.");
10184
+ }
10185
+ boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
10186
+ boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
10187
+ if (input.databaseWriteConcern > input.databaseReplicationFactor) {
10188
+ throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
10189
+ }
10190
+ boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
10191
+ boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
10192
+ boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
10193
+ boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
10194
+ boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
10195
+ if (!Number.isFinite(input.otlpTraceSampleRatio) || input.otlpTraceSampleRatio < 0 || input.otlpTraceSampleRatio > 1) {
10196
+ throw new Error("otlpTraceSampleRatio must be from 0 through 1.");
10197
+ }
10198
+ if (input.otlpEndpoint !== "http://127.0.0.1:4318")
10199
+ throw new Error("OTLP must use the exact local collector endpoint.");
10200
+ validateCollectorUnit(input.otlpCollectorUnit);
10201
+ httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint);
10202
+ for (const [name, value] of Object.entries({
10203
+ nodeHostname: input.nodeHostname,
10204
+ nodeRegion: input.nodeRegion,
10205
+ databaseUser: input.databaseUser,
10206
+ sharedDirectory: input.sharedDirectory,
10207
+ seedSyncEpoch: input.seedSyncEpoch,
10208
+ repository: input.repository,
10209
+ branch: input.branch,
10210
+ deployProfile: input.deployProfile
10211
+ }))
10212
+ safeAtom(name, value);
10213
+ if (!input.sharedDirectory.startsWith("/"))
10214
+ throw new Error("sharedDirectory must be absolute.");
10215
+ for (const peer of input.seedSyncPeers) {
10216
+ const url = new URL(peer);
10217
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
10218
+ throw new Error("Seed peers must be WebSocket URLs.");
10219
+ if (url.username || url.password || url.hash)
10220
+ throw new Error("Seed peers cannot contain credentials or fragments.");
10221
+ }
10222
+ if (input.smtp) {
10223
+ safeAtom("smtp.host", input.smtp.host);
10224
+ boundedInteger("smtp.port", input.smtp.port, 1, 65535);
10225
+ safeAtom("smtp.from", input.smtp.from);
10226
+ if (input.smtp.user)
10227
+ safeAtom("smtp.user", input.smtp.user);
10228
+ }
10229
+ if (input.backup) {
10230
+ httpsOrigin("backup.endpoint", input.backup.endpoint);
10231
+ for (const [name, value] of Object.entries(input.backup))
10232
+ safeAtom(`backup.${name}`, value);
10233
+ }
10234
+ if (input.cloudflare) {
10235
+ if (![input.cloudflare.accountId, input.cloudflare.zoneId, input.cloudflare.kvNamespaceId].every((item) => /^[a-f0-9]{32}$/i.test(item)) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.cloudflare.tunnelId)) {
10236
+ throw new Error("Cloudflare account, zone, KV and Tunnel ids are malformed.");
10237
+ }
10238
+ const service = new URL(input.cloudflare.tunnelService);
10239
+ if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
10240
+ throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
10241
+ if (input.cloudflare.warp) {
10242
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(input.cloudflare.warp.organization) || !/^[0-9a-f-]{36}$/i.test(input.cloudflare.warp.virtualNetworkId) || !/^[A-Za-z0-9_-]{1,128}$/.test(input.cloudflare.warp.deviceProfileId)) {
10243
+ throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
10244
+ }
10325
10245
  }
10326
10246
  }
10327
- if (response.status === 401 && storedSession) {
10328
- removeSession(storedSession.api, storedSession.realm);
10329
- storedSession = null;
10330
- sessionCookie = null;
10247
+ if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
10248
+ throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
10331
10249
  }
10332
- return { status: response.status, body };
10250
+ return {
10251
+ ...input,
10252
+ databaseCoordinators: coordinators,
10253
+ appOrigin: httpsOrigin("appOrigin", input.appOrigin),
10254
+ apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
10255
+ agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
10256
+ };
10333
10257
  }
10334
- function sleep(ms) {
10335
- return new Promise((resolve2) => setTimeout(resolve2, ms));
10258
+ function renderPlatformSharedEnvironment(input) {
10259
+ const value = validatePlatformSharedEnvironment(input);
10260
+ const appHost = new URL(value.appOrigin).hostname.split(".").slice(-2).join(".");
10261
+ const apiHost = new URL(value.apiOrigin).hostname.split(".").slice(-2).join(".");
10262
+ const entries = {
10263
+ ARANGO_URL: value.databaseCoordinators[0],
10264
+ ARANGO_URLS: value.databaseCoordinators.join(","),
10265
+ ARANGO_DB: "fz",
10266
+ FZ_DATABASE_MODE: "platform",
10267
+ ARANGO_USER: value.databaseUser,
10268
+ ARANGO_REPLICATION_FACTOR: String(value.databaseReplicationFactor),
10269
+ ARANGO_WRITE_CONCERN: String(value.databaseWriteConcern),
10270
+ FZ_DB_ROLE: value.databaseRole,
10271
+ FZ_SOFTWARE_PROFILE: value.softwareProfile,
10272
+ FZ_ROLE: value.nodeRole,
10273
+ FZ_DB_ADDRESS: value.databaseAddress ?? "",
10274
+ FZ_DB_MASTER: value.databaseMaster ?? "",
10275
+ FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
10276
+ FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
10277
+ FZ_SEED_SYNC_MEMBERS: String(value.seedSyncMembers),
10278
+ FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
10279
+ FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
10280
+ FZ_SHARED_DIR: value.sharedDirectory,
10281
+ FZ_PUBLIC_API_PORT: String(value.publicApiPort),
10282
+ ORIGIN: value.appOrigin,
10283
+ API_ORIGIN: value.apiOrigin,
10284
+ HOST: "127.0.0.1",
10285
+ APP_ORIGINS: value.appOrigin,
10286
+ TRUST_CLOUDFLARE_IP: "1",
10287
+ SESSION_COOKIE_SAMESITE: appHost === apiHost ? "lax" : "none",
10288
+ SESSION_COOKIE_DOMAIN: "",
10289
+ FZ_NODE_HOSTNAME: value.nodeHostname,
10290
+ FZ_NODE_REGION: value.nodeRegion,
10291
+ FZ_CONCURRENCY_LIMIT: String(value.concurrencyLimit),
10292
+ FZ_DRAIN_DEADLINE_MS: String(value.drainDeadlineMs),
10293
+ OTEL_EXPORTER_OTLP_ENDPOINT: value.otlpEndpoint,
10294
+ FZ_OTLP_COLLECTOR_UNIT: value.otlpCollectorUnit,
10295
+ OTEL_SERVICE_NAME: "forgezero-api",
10296
+ FZ_OTLP_FLUSH_INTERVAL_MS: String(value.otlpFlushIntervalMs),
10297
+ FZ_OTLP_TRACE_SAMPLE_RATIO: String(value.otlpTraceSampleRatio),
10298
+ FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
10299
+ FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
10300
+ FZ_PROFILE: value.deployProfile,
10301
+ FZ_REPO: value.repository,
10302
+ FZ_BRANCH: value.branch,
10303
+ FZ_SMTP_HOST: value.smtp?.host ?? "",
10304
+ FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
10305
+ FZ_SMTP_USER: value.smtp?.user ?? "",
10306
+ FZ_SMTP_FROM: value.smtp?.from ?? "",
10307
+ BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
10308
+ BACKUP_S3_REGION: value.backup?.region ?? "",
10309
+ BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
10310
+ BACKUP_S3_ACCESS_KEY_ID: value.backup?.accessKeyId ?? "",
10311
+ FZ_CF_ACCOUNT_ID: value.cloudflare?.accountId ?? "",
10312
+ FZ_CF_ZONE_ID: value.cloudflare?.zoneId ?? "",
10313
+ FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
10314
+ FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
10315
+ FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
10316
+ FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
10317
+ FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
10318
+ FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
10319
+ };
10320
+ return `# Generated by fz bootstrap platform. Non-secret coordinates only.
10321
+ ` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
10322
+ `) + `
10323
+ `;
10324
+ }
10325
+ function platformApiCredentialSpecs(options) {
10326
+ const optional = [
10327
+ ["bootstrap-smtp-password", options.smtp],
10328
+ ["cloudflare-kv-token", options.cloudflareKv],
10329
+ ["cloudflare-network-token", options.cloudflareNetwork]
10330
+ ];
10331
+ return [
10332
+ { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
10333
+ { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
10334
+ ...optional.filter(([, present]) => present).map(([name]) => ({
10335
+ name,
10336
+ encryptedPath: `/etc/forgezero/creds/${name}.cred`,
10337
+ required: false
10338
+ }))
10339
+ ];
10340
+ }
10341
+ function renderPlatformApiUnits(input) {
10342
+ for (const path of [input.sharedDirectory, input.sharedEnvironmentFile, input.slotsDirectory]) {
10343
+ if (!path.startsWith("/") || /[\r\n]/.test(path))
10344
+ throw new Error("Runtime paths must be absolute and single-line.");
10345
+ }
10346
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
10347
+ throw new Error("Invalid service user.");
10348
+ validateCollectorUnit(input.collectorUnit);
10349
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
10350
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
10351
+ if (input.bluePort === input.greenPort)
10352
+ throw new Error("Blue and green ports must differ.");
10353
+ const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
10354
+ `);
10355
+ const template = `[Unit]
10356
+ Description=ForgeZero (%i slot)
10357
+ After=network-online.target ${input.collectorUnit}
10358
+ Wants=network-online.target ${input.collectorUnit}
10359
+
10360
+ [Service]
10361
+ Type=simple
10362
+ User=${input.serviceUser}
10363
+ WorkingDirectory=${input.slotsDirectory}/%i
10364
+ Environment=NODE_ENV=production
10365
+ Environment=FZ_SLOT=%i
10366
+ EnvironmentFile=${input.sharedEnvironmentFile}
10367
+ ${credentials}
10368
+ ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
10369
+ Restart=always
10370
+ RestartSec=2
10371
+ TimeoutStopSec=35s
10372
+ LimitCORE=0
10373
+ UMask=0077
10374
+ NoNewPrivileges=yes
10375
+ PrivateTmp=yes
10376
+ PrivateDevices=yes
10377
+ ProtectSystem=strict
10378
+ ProtectHome=yes
10379
+ ReadOnlyPaths=${input.sharedDirectory}
10380
+ ProtectKernelTunables=yes
10381
+ ProtectKernelModules=yes
10382
+ ProtectControlGroups=yes
10383
+ RestrictSUIDSGID=yes
10384
+ RestrictRealtime=yes
10385
+ LockPersonality=yes
10386
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
10387
+
10388
+ [Install]
10389
+ WantedBy=multi-user.target
10390
+ `;
10391
+ return { template, dropIns: {
10392
+ blue: `[Service]
10393
+ Environment=PORT=${input.bluePort}
10394
+ `,
10395
+ green: `[Service]
10396
+ Environment=PORT=${input.greenPort}
10397
+ `
10398
+ } };
10399
+ }
10400
+ function renderPlatformNginx(input) {
10401
+ boundedInteger("publicPort", input.publicPort, 1024, 65535);
10402
+ boundedInteger("initialSlotPort", input.initialSlotPort, 1024, 65535);
10403
+ if (input.publicPort === input.initialSlotPort)
10404
+ throw new Error("Edge and slot ports must differ.");
10405
+ return {
10406
+ upstream: `upstream forgezero { server 127.0.0.1:${input.initialSlotPort}; }
10407
+ `,
10408
+ site: `server {
10409
+ listen 127.0.0.1:${input.publicPort};
10410
+ server_name _;
10411
+ location ^~ /api/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
10412
+ location ^~ /v1/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
10413
+ location / { return 404; }
10414
+ }
10415
+ `
10416
+ };
10417
+ }
10418
+ function renderPlatformActivationFiles(input) {
10419
+ if (!input.root.startsWith("/") || /[\0\r\n]/.test(input.root))
10420
+ throw new Error("Activation root must be absolute and single-line.");
10421
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
10422
+ throw new Error("Invalid activation service user.");
10423
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
10424
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
10425
+ if (input.bluePort === input.greenPort)
10426
+ throw new Error("Activation slot ports must differ.");
10427
+ boundedInteger("keepReleases", input.keepReleases, 2, 100);
10428
+ if (!/^\/[A-Za-z0-9/_-]{1,128}$/.test(input.healthPath) || input.healthPath.includes("..")) {
10429
+ throw new Error("Activation health path is malformed.");
10430
+ }
10431
+ const environment = [
10432
+ `FZ_DIR=${input.root}`,
10433
+ `FZ_USER=${input.serviceUser}`,
10434
+ `FZ_BLUE_PORT=${input.bluePort}`,
10435
+ `FZ_GREEN_PORT=${input.greenPort}`,
10436
+ `FZ_HEALTH_PATH=${input.healthPath}`,
10437
+ `FZ_KEEP_RELEASES=${input.keepReleases}`
10438
+ ].join(`
10439
+ `) + `
10440
+ `;
10441
+ const helper = `#!/usr/bin/env bash
10442
+ set -Eeuo pipefail
10443
+ source /etc/forgezero/deploy.env
10444
+ [[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
10445
+ release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
10446
+ install -d -o root -g root -m 0755 "$slots"
10447
+ case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
10448
+ [[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
10449
+ slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
10450
+ if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
10451
+ target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
10452
+ chown -R root:"$FZ_USER" "$release"; chmod -R a-w "$release"; find "$release" -type d -exec chmod a+rx {} +; find "$release" -type f -exec chmod a+r {} +
10453
+ ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
10454
+ healthy=0; for _ in $(seq 1 30); do curl -fsS --max-time 2 "http://127.0.0.1:\${port}\${FZ_HEALTH_PATH}" >/dev/null 2>&1 && { healthy=1; break; }; sleep 1; done
10455
+ if (( ! healthy )); then systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; exit 1; fi
10456
+ upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
10457
+ printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
10458
+ if ! nginx -t || ! nginx -s reload; then [[ -s "$backup" ]] && cp "$backup" "$upstream" || rm -f "$upstream"; rm -f "$backup"; systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; nginx -t >/dev/null 2>&1 && nginx -s reload || true; exit 1; fi
10459
+ rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"; [[ -n "$previous_slot" && "$previous_slot" != "$target" ]] && systemctl stop "forgezero@\${previous_slot}.service" || true
10460
+ mapfile -t old < <(find "$releases" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\\n' | sort -rn | tail -n "+$((FZ_KEEP_RELEASES + 1))" | cut -d' ' -f2-)
10461
+ for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
10462
+ printf 'promoted %s on %s\\n' "$release" "$target"
10463
+ `;
10464
+ return {
10465
+ environment,
10466
+ helper,
10467
+ sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
10468
+ `
10469
+ };
10470
+ }
10471
+ function validateCollectorUnit(unit) {
10472
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit))
10473
+ throw new Error("Invalid OTLP collector service unit.");
10474
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-db)\.service$/.test(unit)) {
10475
+ throw new Error("OTLP collector must be independently supervised.");
10476
+ }
10477
+ }
10478
+ function planLocalOtlpProof(endpoint, collectorUnit) {
10479
+ if (endpoint !== "http://127.0.0.1:4318")
10480
+ throw new Error("OTLP proof requires exact loopback endpoint http://127.0.0.1:4318.");
10481
+ validateCollectorUnit(collectorUnit);
10482
+ return {
10483
+ unitCheck: { command: "systemctl", argv: ["is-active", "--quiet", collectorUnit] },
10484
+ receiverCheck: {
10485
+ command: "curl",
10486
+ acceptedStatus: "2xx",
10487
+ argv: [
10488
+ "--silent",
10489
+ "--show-error",
10490
+ "--max-time",
10491
+ "5",
10492
+ "--output",
10493
+ "/dev/null",
10494
+ "--write-out",
10495
+ "%{http_code}",
10496
+ "--request",
10497
+ "POST",
10498
+ "--header",
10499
+ "Content-Type: application/json",
10500
+ "--data-binary",
10501
+ "{}",
10502
+ `${endpoint}/v1/metrics`
10503
+ ]
10504
+ }
10505
+ };
10506
+ }
10507
+
10508
+ // src/cloudflare-bootstrap.ts
10509
+ import { constants } from "fs";
10510
+ import { chmod, lstat, mkdir, mkdtemp, open, rename, rm, stat, unlink } from "fs/promises";
10511
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
10512
+ import { tmpdir } from "os";
10513
+ import { randomUUID } from "crypto";
10514
+ import { isIP as isIP3 } from "net";
10515
+
10516
+ // src/cloudflare-edge.ts
10517
+ import { isIP as isIP2 } from "net";
10518
+ function isPrivateDatabaseAddress(value) {
10519
+ const address = value.trim().toLowerCase();
10520
+ const family = isIP2(address);
10521
+ if (family === 4) {
10522
+ const [a, b] = address.split(".").map(Number);
10523
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
10524
+ }
10525
+ if (family === 6) {
10526
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
10527
+ return Number.isFinite(first) && (first & 65024) === 64512;
10528
+ }
10529
+ return false;
10530
+ }
10531
+ function privateDatabaseHostRoute(value) {
10532
+ const address = value.trim().toLowerCase();
10533
+ if (!isPrivateDatabaseAddress(address)) {
10534
+ throw new Error("database address must be an RFC 1918 IPv4 or unique-local IPv6 address");
10535
+ }
10536
+ return `${address}/${isIP2(address) === 4 ? 32 : 128}`;
10537
+ }
10538
+ var endpoint = "https://api.cloudflare.com/client/v4";
10539
+ async function cf(config, path, init = {}, fetcher = fetch) {
10540
+ const response = await fetcher(`${endpoint}${path}`, {
10541
+ ...init,
10542
+ headers: {
10543
+ authorization: `Bearer ${config.apiToken}`,
10544
+ "content-type": "application/json",
10545
+ ...init.headers
10546
+ }
10547
+ });
10548
+ const body = await response.json();
10549
+ if (!response.ok || body.success !== true) {
10550
+ throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
10551
+ }
10552
+ return body.result;
10553
+ }
10554
+ async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
10555
+ const [address, prefixText, ...extra] = config.network.split("/");
10556
+ const family = isIP2(address ?? "");
10557
+ const prefix = Number(prefixText);
10558
+ if (extra.length > 0 || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
10559
+ throw new Error("Cloudflare private route must be an explicit IPv4 or IPv6 CIDR");
10560
+ }
10561
+ const path = `/accounts/${config.accountId}/teamnet/routes`;
10562
+ const routes = await cf(config, path, {}, fetcher);
10563
+ const current = routes.find((route2) => !route2.deleted_at && route2.network === config.network && (route2.virtual_network_id ?? "") === (config.virtualNetworkId ?? ""));
10564
+ if (current) {
10565
+ if (current.tunnel_id !== config.tunnelId) {
10566
+ throw new Error(`private route ${config.network} already belongs to another Tunnel`);
10567
+ }
10568
+ return { route: current, created: false };
10569
+ }
10570
+ const route = await cf(config, path, {
10571
+ method: "POST",
10572
+ body: JSON.stringify({
10573
+ network: config.network,
10574
+ tunnel_id: config.tunnelId,
10575
+ comment: config.comment.slice(0, 100),
10576
+ ...config.virtualNetworkId ? { virtual_network_id: config.virtualNetworkId } : {}
10577
+ })
10578
+ }, fetcher);
10579
+ return { route, created: true };
10580
+ }
10581
+ async function ensureCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
10582
+ return ensureCloudflarePrivateRoute({
10583
+ ...config,
10584
+ network: privateDatabaseHostRoute(config.privateAddress)
10585
+ }, fetcher);
10586
+ }
10587
+ async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
10588
+ if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
10589
+ throw new Error("Cloudflare WARP policy id is invalid");
10590
+ const route = privateDatabaseHostRoute(config.privateAddress);
10591
+ const policy = config.policyId ? `/${config.policyId}` : "";
10592
+ const path = `/accounts/${config.accountId}/devices/policy${policy}/include`;
10593
+ const entries = await cf(config, path, {}, fetcher);
10594
+ if (entries.some((entry) => entry.address === route))
10595
+ return { entries, created: false };
10596
+ const next = [...entries, { address: route, description: config.description.slice(0, 100) }];
10597
+ const updated = await cf(config, path, {
10598
+ method: "PUT",
10599
+ body: JSON.stringify(next)
10600
+ }, fetcher);
10601
+ return { entries: updated, created: true };
10602
+ }
10603
+ async function configureCloudflareEdge(config, fetcher = fetch) {
10604
+ const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
10605
+ const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
10606
+ const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
10607
+ const current = await cf(tunnelAuth, tunnelPath, {}, fetcher);
10608
+ const existing = current.config?.ingress ?? [];
10609
+ const catchAll = existing.filter((rule) => !("hostname" in rule));
10610
+ const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
10611
+ await cf(tunnelAuth, tunnelPath, {
10612
+ method: "PUT",
10613
+ body: JSON.stringify({ config: { ingress: [
10614
+ { hostname: config.hostname, service: config.service },
10615
+ ...otherHosts,
10616
+ ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
10617
+ ] } })
10618
+ }, fetcher);
10619
+ const dnsPath = `/zones/${config.zoneId}/dns_records`;
10620
+ const records = await cf(dnsAuth, `${dnsPath}?type=CNAME&name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher);
10621
+ if (records.length > 1) {
10622
+ throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
10623
+ }
10624
+ const record2 = {
10625
+ type: "CNAME",
10626
+ name: config.hostname,
10627
+ content: `${config.tunnelId}.cfargotunnel.com`,
10628
+ proxied: true,
10629
+ ttl: 1
10630
+ };
10631
+ await cf(dnsAuth, records[0] ? `${dnsPath}/${records[0].id}` : dnsPath, {
10632
+ method: records[0] ? "PUT" : "POST",
10633
+ body: JSON.stringify(record2)
10634
+ }, fetcher);
10635
+ }
10636
+ var exactAccountPermissionGroup = async (config, name, fetcher) => {
10637
+ const groups = await cf(config, `/accounts/${config.accountId}/tokens/permission_groups?name=${encodeURIComponent(name)}` + "&scope=com.cloudflare.api.account", {}, fetcher);
10638
+ const matches = groups.filter((group) => group.name === name && group.scopes?.includes("com.cloudflare.api.account") && Boolean(group.id && /^[a-f0-9]{32}$/i.test(group.id)));
10639
+ if (matches.length !== 1) {
10640
+ throw new Error(`Cloudflare account token permission group ${name} is ${matches.length === 0 ? "missing" : "ambiguous"}`);
10641
+ }
10642
+ return { id: matches[0].id, name };
10643
+ };
10644
+ async function createCloudflareAccountRuntimeToken(config, fetcher = fetch) {
10645
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId))
10646
+ throw new Error("Cloudflare account id is invalid");
10647
+ const name = config.name.trim();
10648
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,119}$/.test(name)) {
10649
+ throw new Error("Cloudflare account runtime-token name is invalid");
10650
+ }
10651
+ const permissionNames = [...new Set(config.permissionNames)];
10652
+ if (permissionNames.length === 0)
10653
+ throw new Error("Cloudflare account runtime token needs a permission group");
10654
+ const permissionGroups = await Promise.all(permissionNames.map((permissionName) => exactAccountPermissionGroup(config, permissionName, fetcher)));
10655
+ const body = {
10656
+ name,
10657
+ policies: [{
10658
+ effect: "allow",
10659
+ permission_groups: permissionGroups.map(({ id: id2 }) => ({ id: id2 })),
10660
+ resources: { [`com.cloudflare.api.account.${config.accountId}`]: "*" }
10661
+ }]
10662
+ };
10663
+ const created = await cf(config, `/accounts/${config.accountId}/tokens`, { method: "POST", body: JSON.stringify(body) }, fetcher);
10664
+ if (!created.id || !/^[a-f0-9]{32}$/i.test(created.id) || !created.value || !/^[A-Za-z0-9._-]{40,80}$/.test(created.value)) {
10665
+ throw new Error("Cloudflare did not return the one-time account runtime-token id and value");
10666
+ }
10667
+ return { id: created.id, value: created.value, name, permissionNames };
10668
+ }
10669
+ async function ensureCloudflareAccessServiceToken(config, fetcher = fetch) {
10670
+ const name = config.name.trim();
10671
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
10672
+ throw new Error("Cloudflare Access service-token name is invalid");
10673
+ }
10674
+ const path = `/accounts/${config.accountId}/access/service_tokens`;
10675
+ const tokens = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10676
+ const matches = tokens.filter((token) => token.name === name);
10677
+ if (matches.length > 1)
10678
+ throw new Error(`Cloudflare Access service token ${name} is ambiguous`);
10679
+ if (matches[0]) {
10680
+ if (!config.existing || config.existing.tokenId !== matches[0].id || config.existing.clientId !== matches[0].client_id || !config.existing.clientSecret) {
10681
+ throw new Error(`Cloudflare Access service token ${name} exists but its one-time client secret was not supplied`);
10682
+ }
10683
+ return { credentials: config.existing, created: false };
10684
+ }
10685
+ const created = await cf(config, path, {
10686
+ method: "POST",
10687
+ body: JSON.stringify({ name, duration: config.duration ?? "8760h" })
10688
+ }, fetcher);
10689
+ if (!created.id || !created.client_id || !created.client_secret) {
10690
+ throw new Error("Cloudflare did not return the new Access service-token secret");
10691
+ }
10692
+ return {
10693
+ credentials: {
10694
+ tokenId: created.id,
10695
+ clientId: created.client_id,
10696
+ clientSecret: created.client_secret
10697
+ },
10698
+ created: true
10699
+ };
10700
+ }
10701
+ async function ensureCloudflareAccessPolicy(config, fetcher = fetch) {
10702
+ const path = `/accounts/${config.accountId}/access/policies`;
10703
+ const policies = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10704
+ const matches = policies.filter((policy2) => policy2.name === config.name);
10705
+ if (matches.length > 1)
10706
+ throw new Error(`Cloudflare Access policy ${config.name} is ambiguous`);
10707
+ const desired = {
10708
+ name: config.name,
10709
+ decision: "non_identity",
10710
+ include: [{ service_token: { token_id: config.serviceTokenId } }]
10711
+ };
10712
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, {
10713
+ method: matches[0] ? "PUT" : "POST",
10714
+ body: JSON.stringify(desired)
10715
+ }, fetcher);
10716
+ return { policy, created: !matches[0] };
10717
+ }
10718
+ async function ensureCloudflareAccessApplication(config, fetcher = fetch) {
10719
+ const path = `/accounts/${config.accountId}/access/apps`;
10720
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10721
+ const matches = applications.filter((application2) => application2.domain === config.hostname || application2.self_hosted_domains?.includes(config.hostname));
10722
+ if (matches.length > 1)
10723
+ throw new Error(`Cloudflare Access application for ${config.hostname} is ambiguous`);
10724
+ const desired = {
10725
+ name: config.name,
10726
+ type: "self_hosted",
10727
+ domain: config.hostname,
10728
+ session_duration: "24h",
10729
+ service_auth_401_redirect: true,
10730
+ policies: [{ id: config.policyId, precedence: 1 }]
10731
+ };
10732
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
10733
+ return { application, created: !matches[0] };
10734
+ }
10735
+ async function ensureCloudflareWarpEnrollmentApplication(config, fetcher = fetch) {
10736
+ const name = config.name.trim();
10737
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
10738
+ throw new Error("Cloudflare WARP enrollment application name is invalid");
10739
+ }
10740
+ const path = `/accounts/${config.accountId}/access/apps`;
10741
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10742
+ const matches = applications.filter((application2) => application2.type === "warp" || application2.name === name);
10743
+ if (matches.length > 1)
10744
+ throw new Error(`Cloudflare WARP enrollment application ${name} is ambiguous`);
10745
+ if (matches[0] && (matches[0].type !== "warp" || matches[0].name !== name)) {
10746
+ throw new Error(`Cloudflare Access application ${name} is not the owned WARP enrollment application`);
10747
+ }
10748
+ const desired = {
10749
+ name,
10750
+ type: "warp",
10751
+ policies: [{ id: config.policyId, precedence: 1 }]
10752
+ };
10753
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
10754
+ if (!application.id || application.type && application.type !== "warp") {
10755
+ throw new Error("Cloudflare did not return the WARP enrollment application");
10756
+ }
10757
+ return { application, created: !matches[0] };
10758
+ }
10759
+ async function ensureCloudflareVirtualNetwork(config, fetcher = fetch) {
10760
+ const name = config.name.trim();
10761
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
10762
+ throw new Error("Cloudflare VNET name is invalid");
10763
+ const path = `/accounts/${config.accountId}/teamnet/virtual_networks`;
10764
+ const networks = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10765
+ const matches = networks.filter((network) => !network.deleted_at && network.name === name);
10766
+ if (matches.length > 1)
10767
+ throw new Error(`Cloudflare VNET ${name} is ambiguous`);
10768
+ if (matches[0])
10769
+ return { virtualNetwork: matches[0], created: false };
10770
+ const virtualNetwork = await cf(config, path, {
10771
+ method: "POST",
10772
+ body: JSON.stringify({ name, comment: config.comment.slice(0, 256), is_default_network: false })
10773
+ }, fetcher);
10774
+ if (!virtualNetwork.id || !/^[0-9a-f-]{36}$/i.test(virtualNetwork.id)) {
10775
+ throw new Error("Cloudflare did not return the VNET id");
10776
+ }
10777
+ return { virtualNetwork, created: true };
10778
+ }
10779
+ async function ensureCloudflareWarpDevicePolicy(config, fetcher = fetch) {
10780
+ const name = config.name.trim();
10781
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
10782
+ throw new Error("Cloudflare WARP device profile name is invalid");
10783
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.serviceTokenId))
10784
+ throw new Error("Cloudflare service-token id is invalid");
10785
+ if (!/^[0-9a-f-]{36}$/i.test(config.virtualNetworkId))
10786
+ throw new Error("Cloudflare VNET id is invalid");
10787
+ const precedence = config.precedence ?? 100;
10788
+ if (!Number.isInteger(precedence) || precedence < 1 || precedence > 999999) {
10789
+ throw new Error("Cloudflare WARP device profile precedence is invalid");
10790
+ }
10791
+ const match = `identity.service_token_uuid == "${config.serviceTokenId}"`;
10792
+ const listPath = `/accounts/${config.accountId}/devices/policies`;
10793
+ const path = `/accounts/${config.accountId}/devices/policy`;
10794
+ const policies = await cf(config, `${listPath}?per_page=1000`, {}, fetcher);
10795
+ const matches = policies.filter((policy2) => policy2.name === name);
10796
+ if (matches.length > 1)
10797
+ throw new Error(`Cloudflare WARP device profile ${name} is ambiguous`);
10798
+ if (matches[0]?.match && matches[0].match !== match) {
10799
+ throw new Error(`Cloudflare WARP device profile ${name} belongs to another enrollment identity`);
10800
+ }
10801
+ const desired = {
10802
+ name,
10803
+ match,
10804
+ precedence,
10805
+ description: "ForgeZero non-interactive compute enrollment",
10806
+ enabled: true,
10807
+ allow_mode_switch: false,
10808
+ allowed_to_leave: false,
10809
+ auto_connect: 0,
10810
+ switch_locked: true,
10811
+ service_mode_v2: { mode: "warp" },
10812
+ virtual_networks: { allowed: [config.virtualNetworkId], default: config.virtualNetworkId }
10813
+ };
10814
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PATCH" : "POST", body: JSON.stringify(desired) }, fetcher);
10815
+ if (!policy.id)
10816
+ throw new Error("Cloudflare did not return the WARP device profile id");
10817
+ return { policy, created: !matches[0] };
10818
+ }
10819
+ async function configureCloudflareWorkerAccessSecrets(config, fetcher = fetch) {
10820
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(config.scriptName)) {
10821
+ throw new Error("Cloudflare Worker script name is invalid");
10822
+ }
10823
+ await cf(config, `/accounts/${config.accountId}/workers/scripts/${config.scriptName}/secrets-bulk`, {
10824
+ method: "PATCH",
10825
+ body: JSON.stringify({
10826
+ secrets: {
10827
+ CF_ACCESS_CLIENT_ID: {
10828
+ name: "CF_ACCESS_CLIENT_ID",
10829
+ type: "secret_text",
10830
+ text: config.credentials.clientId
10831
+ },
10832
+ CF_ACCESS_CLIENT_SECRET: {
10833
+ name: "CF_ACCESS_CLIENT_SECRET",
10834
+ type: "secret_text",
10835
+ text: config.credentials.clientSecret
10836
+ }
10837
+ }
10838
+ })
10839
+ }, fetcher);
10840
+ }
10841
+ async function ensureCloudflareKvNamespace(config, fetcher = fetch) {
10842
+ const title = config.title.trim();
10843
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(title)) {
10844
+ throw new Error("Cloudflare KV namespace title is invalid");
10845
+ }
10846
+ const path = `/accounts/${config.accountId}/storage/kv/namespaces`;
10847
+ const namespaces = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10848
+ const matches = namespaces.filter((namespace2) => namespace2.title === title);
10849
+ if (matches.length > 1)
10850
+ throw new Error(`Cloudflare KV namespace ${title} is ambiguous`);
10851
+ if (matches[0])
10852
+ return { namespace: matches[0], created: false };
10853
+ const namespace = await cf(config, path, {
10854
+ method: "POST",
10855
+ body: JSON.stringify({ title })
10856
+ }, fetcher);
10857
+ return { namespace, created: true };
10858
+ }
10859
+ async function ensureCloudflareTunnel(config, fetcher = fetch) {
10860
+ const name = config.name.trim();
10861
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
10862
+ throw new Error("Cloudflare Tunnel name is invalid");
10863
+ }
10864
+ const path = `/accounts/${config.accountId}/cfd_tunnel`;
10865
+ const tunnels = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
10866
+ const matches = tunnels.filter((tunnel2) => tunnel2.name === name && !tunnel2.deleted_at);
10867
+ if (matches.length > 1)
10868
+ throw new Error(`Cloudflare Tunnel ${name} is ambiguous`);
10869
+ const created = !matches[0];
10870
+ const tunnel = matches[0] ?? await cf(config, path, {
10871
+ method: "POST",
10872
+ body: JSON.stringify({ name, config_src: "cloudflare" })
10873
+ }, fetcher);
10874
+ const connectorToken = await cf(config, `${path}/${encodeURIComponent(tunnel.id)}/token`, {}, fetcher);
10875
+ if (!connectorToken || connectorToken.length > 16384) {
10876
+ throw new Error("Cloudflare returned an invalid Tunnel connector token");
10877
+ }
10878
+ return { tunnel, connectorToken, created };
10879
+ }
10880
+
10881
+ // src/cloudflare-bootstrap.ts
10882
+ var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
10883
+ async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
10884
+ const metadata = await handle.stat();
10885
+ if (!metadata.isFile())
10886
+ throw new Error(`${path} must be a regular file`);
10887
+ if (metadata.nlink !== 1)
10888
+ throw new Error(`${path} must not have multiple hard links`);
10889
+ const uid = ownerUid();
10890
+ if (uid !== undefined && uid !== 0 && metadata.uid !== uid)
10891
+ throw new Error(`${path} must be owned by the current operator`);
10892
+ if ((metadata.mode & 63) !== 0)
10893
+ throw new Error(`${path} must not be accessible by group or other users`);
10894
+ if ((metadata.mode & 256) === 0)
10895
+ throw new Error(`${path} must be readable by its owner`);
10896
+ if (metadata.size < 1 || metadata.size > maximumBytes)
10897
+ throw new Error(`${path} has an invalid size`);
10898
+ }
10899
+ async function readOwnerOnlyFile(path, maximumBytes) {
10900
+ const absolute = resolve2(path);
10901
+ let handle;
10902
+ try {
10903
+ handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
10904
+ await assertOwnerOnlyHandle(absolute, handle, maximumBytes);
10905
+ return await handle.readFile({ encoding: "utf8" });
10906
+ } catch (cause) {
10907
+ if (cause instanceof Error && cause.message.startsWith(absolute))
10908
+ throw cause;
10909
+ throw new Error(`cannot securely read owner-only file ${absolute}`);
10910
+ } finally {
10911
+ await handle?.close();
10912
+ }
10913
+ }
10914
+ async function readOwnerApiToken(path) {
10915
+ const token = (await readOwnerOnlyFile(path, 4096)).trim();
10916
+ if (!/^[A-Za-z0-9._-]{40,80}$/.test(token)) {
10917
+ throw new Error(`${resolve2(path)} must contain exactly one Cloudflare API token`);
10918
+ }
10919
+ return token;
10920
+ }
10921
+ async function readCloudflareBootstrapTokens(files) {
10922
+ const entries = await Promise.all([
10923
+ ["apiToken", files.apiTokenFile],
10924
+ ["tunnelApiToken", files.tunnelApiTokenFile],
10925
+ ["dnsApiToken", files.dnsApiTokenFile],
10926
+ ["kvApiToken", files.kvApiTokenFile],
10927
+ ["accessApiToken", files.accessApiTokenFile],
10928
+ ["workerApiToken", files.workerApiTokenFile]
10929
+ ].map(async ([key, path]) => [key, path ? await readOwnerApiToken(path) : undefined]));
10930
+ const tokens = Object.fromEntries(entries.filter(([, value]) => value !== undefined));
10931
+ const unified = tokens.apiToken;
10932
+ for (const key of ["tunnelApiToken", "dnsApiToken", "kvApiToken", "accessApiToken", "workerApiToken"]) {
10933
+ if (!tokens[key] && !unified)
10934
+ throw new Error(`Cloudflare ${key} file is required when --token-file is omitted`);
10935
+ }
10936
+ return tokens;
10937
+ }
10938
+ var validateId = (value, label) => {
10939
+ const normalized = value.trim().toLowerCase();
10940
+ if (!/^[a-f0-9]{32}$/.test(normalized))
10941
+ throw new Error(`${label} must be a 32-character hexadecimal id`);
10942
+ return normalized;
10943
+ };
10944
+ var validateName = (value, label, maximum, allowSpaces = true) => {
10945
+ const normalized = value.trim();
10946
+ const pattern = allowSpaces ? /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/ : /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10947
+ if (!normalized || normalized.length > maximum || !pattern.test(normalized)) {
10948
+ throw new Error(`${label} is invalid`);
10949
+ }
10950
+ return normalized;
10951
+ };
10952
+ var privateAddress = (value) => {
10953
+ const address = value.trim().toLowerCase();
10954
+ const family = isIP3(address);
10955
+ if (family === 4) {
10956
+ const [a, b] = address.split(".").map(Number);
10957
+ if (a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31)
10958
+ return address;
10959
+ }
10960
+ if (family === 6) {
10961
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
10962
+ if (Number.isFinite(first) && (first & 65024) === 64512)
10963
+ return address;
10964
+ }
10965
+ throw new Error("Cloudflare private database address must be RFC 1918 IPv4 or unique-local IPv6");
10966
+ };
10967
+ function validateCloudflareBootstrapCoordinates(input) {
10968
+ const nodeInputs = input.nodes?.length ? input.nodes : [{
10969
+ nodeName: input.tunnelName,
10970
+ hostname: input.hostname,
10971
+ service: input.service,
10972
+ tunnelName: input.tunnelName,
10973
+ applicationName: input.applicationName
10974
+ }];
10975
+ if (nodeInputs.length < 1 || nodeInputs.length > 32) {
10976
+ throw new Error("Cloudflare bootstrap requires between 1 and 32 explicit nodes");
10977
+ }
10978
+ const nodes = nodeInputs.map((node) => {
10979
+ const nodeName = node.nodeName.trim().toLowerCase();
10980
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName))
10981
+ throw new Error("Cloudflare node name is invalid");
10982
+ const hostname = node.hostname.trim().toLowerCase();
10983
+ if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(hostname)) {
10984
+ throw new Error("Cloudflare public node hostname is invalid");
10985
+ }
10986
+ const serviceUrl = new URL(node.service);
10987
+ if (serviceUrl.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(serviceUrl.hostname) || !serviceUrl.port || serviceUrl.pathname !== "/" || serviceUrl.search || serviceUrl.hash) {
10988
+ throw new Error("Cloudflare Tunnel service must be an explicit loopback HTTP port");
10989
+ }
10990
+ return {
10991
+ nodeName,
10992
+ hostname,
10993
+ service: serviceUrl.toString().replace(/\/$/, ""),
10994
+ tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name", 100, false),
10995
+ applicationName: validateName(node.applicationName, "Cloudflare Access application name", 100),
10996
+ ...node.privateAddress ? { privateAddress: privateAddress(node.privateAddress) } : {}
10997
+ };
10998
+ });
10999
+ for (const [label, values] of [
11000
+ ["node name", nodes.map(({ nodeName }) => nodeName)],
11001
+ ["hostname", nodes.map(({ hostname }) => hostname)],
11002
+ ["Tunnel name", nodes.map(({ tunnelName }) => tunnelName)],
11003
+ ["Access application name", nodes.map(({ applicationName }) => applicationName)]
11004
+ ]) {
11005
+ if (new Set(values).size !== values.length)
11006
+ throw new Error(`Cloudflare fleet ${label} must be unique`);
11007
+ }
11008
+ const first = nodes[0];
11009
+ const workerScriptName = input.workerScriptName.trim();
11010
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(workerScriptName))
11011
+ throw new Error("Cloudflare Worker script name is invalid");
11012
+ const workerCompatibilityDate = input.workerCompatibilityDate.trim();
11013
+ if (!/^20\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(workerCompatibilityDate)) {
11014
+ throw new Error("Cloudflare Worker compatibility date is invalid");
11015
+ }
11016
+ const publicDomains = [...new Set(input.publicDomains.map((domain) => domain.trim().toLowerCase()))];
11017
+ if (publicDomains.length < 1 || publicDomains.length > 10 || publicDomains.some((domain) => !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain) || nodes.some(({ hostname }) => hostname === domain))) {
11018
+ throw new Error("Cloudflare Worker public domains are invalid or include the private origin hostname");
11019
+ }
11020
+ const workerDirectory = resolve2(input.workerDirectory);
11021
+ const workerMain = input.workerMain.trim();
11022
+ if (!workerMain || workerMain.startsWith("/") || workerMain.split(/[\\/]/).includes("..")) {
11023
+ throw new Error("Cloudflare Worker main must be a project-relative path");
11024
+ }
11025
+ const runtimeTokenNamePrefix = input.runtimeTokenNamePrefix.trim();
11026
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(runtimeTokenNamePrefix)) {
11027
+ throw new Error("Cloudflare runtime-token name prefix is invalid");
11028
+ }
11029
+ if (input.createPrivateNetworkRuntimeToken && !input.createRuntimeTokens) {
11030
+ throw new Error("private-network runtime token requires runtime-token creation");
11031
+ }
11032
+ const privateNetwork = input.privateNetwork ? {
11033
+ warpOrganization: validateName(input.privateNetwork.warpOrganization, "Cloudflare WARP organization", 63, false).toLowerCase(),
11034
+ virtualNetworkName: validateName(input.privateNetwork.virtualNetworkName, "Cloudflare VNET name", 100),
11035
+ deviceProfileName: validateName(input.privateNetwork.deviceProfileName, "Cloudflare WARP device profile name", 100),
11036
+ enrollmentApplicationName: validateName(input.privateNetwork.enrollmentApplicationName, "Cloudflare WARP enrollment application name", 100),
11037
+ ...input.privateNetwork.deviceProfilePrecedence !== undefined ? { deviceProfilePrecedence: input.privateNetwork.deviceProfilePrecedence } : {}
11038
+ } : undefined;
11039
+ if (privateNetwork && (!input.createPrivateNetworkRuntimeToken || nodes.every((node) => !node.privateAddress))) {
11040
+ throw new Error("Cloudflare private network requires its runtime token and at least one DB node private address");
11041
+ }
11042
+ if (!privateNetwork && nodes.some((node) => node.privateAddress)) {
11043
+ throw new Error("Cloudflare node private addresses require privateNetwork coordinates");
11044
+ }
11045
+ return {
11046
+ accountId: validateId(input.accountId, "Cloudflare account id"),
11047
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
11048
+ hostname: first.hostname,
11049
+ service: first.service,
11050
+ tunnelName: first.tunnelName,
11051
+ kvNamespaceTitle: validateName(input.kvNamespaceTitle, "Cloudflare KV namespace title", 128, false),
11052
+ workerScriptName,
11053
+ serviceTokenName: validateName(input.serviceTokenName, "Cloudflare Access service-token name", 100),
11054
+ policyName: validateName(input.policyName, "Cloudflare Access policy name", 100),
11055
+ applicationName: first.applicationName,
11056
+ workerDirectory,
11057
+ workerMain,
11058
+ workerCompatibilityDate,
11059
+ publicDomains,
11060
+ createRuntimeTokens: input.createRuntimeTokens,
11061
+ createPrivateNetworkRuntimeToken: input.createPrivateNetworkRuntimeToken,
11062
+ runtimeTokenNamePrefix,
11063
+ nodes,
11064
+ ...privateNetwork ? { privateNetwork } : {}
11065
+ };
11066
+ }
11067
+ function planCloudflareBootstrap(input, outputPath) {
11068
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
11069
+ return {
11070
+ format: 1,
11071
+ kind: "forgezero-cloudflare-bootstrap-plan",
11072
+ mode: "attended-token-file",
11073
+ outputFile: resolve2(outputPath),
11074
+ coordinates,
11075
+ operations: [
11076
+ "create or reuse one Workers KV namespace",
11077
+ "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
11078
+ ...coordinates.createRuntimeTokens ? [
11079
+ "create exact-account least-privilege runtime tokens and checkpoint their one-time values"
11080
+ ] : [],
11081
+ "deploy the shared Worker once with the created NODES binding and stable custom domains",
11082
+ "create or reuse one shared Access service token/policy and one self-hosted application per node",
11083
+ ...coordinates.privateNetwork ? [
11084
+ "create or reuse the VNET, WARP enrollment application, locked service-token device profile and exact DB host routes"
11085
+ ] : [],
11086
+ "write the Access client id and secret to the existing Worker as encrypted secrets",
11087
+ "reconcile each node ingress rule and proxied CNAME only after all Access applications are ready"
11088
+ ],
11089
+ secrets: [
11090
+ "API tokens are read only from owner-only files and are never written to output",
11091
+ "the output contains connector, Access and requested runtime credentials and is atomically written with mode 0600",
11092
+ "the normal API process does not receive or import the management token files"
11093
+ ]
11094
+ };
11095
+ }
11096
+ var defaultWorkerCommandRunner = async ({ command, cwd, env }) => {
11097
+ const child = Bun.spawn([...command], {
11098
+ cwd,
11099
+ env: { ...env },
11100
+ stdin: "ignore",
11101
+ stdout: "pipe",
11102
+ stderr: "pipe"
11103
+ });
11104
+ const [exitCode, stdout, stderr] = await Promise.all([
11105
+ child.exited,
11106
+ new Response(child.stdout).text(),
11107
+ new Response(child.stderr).text()
11108
+ ]);
11109
+ return { exitCode, stdout, stderr };
11110
+ };
11111
+ var inheritedWorkerEnvironment = () => {
11112
+ const allowed = [
11113
+ "PATH",
11114
+ "HOME",
11115
+ "TMPDIR",
11116
+ "XDG_CONFIG_HOME",
11117
+ "XDG_CACHE_HOME",
11118
+ "SSL_CERT_FILE",
11119
+ "SSL_CERT_DIR",
11120
+ "NODE_EXTRA_CA_CERTS",
11121
+ "HTTPS_PROXY",
11122
+ "HTTP_PROXY",
11123
+ "NO_PROXY"
11124
+ ];
11125
+ return Object.fromEntries(allowed.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
11126
+ };
11127
+ var redact = (text3, secrets) => {
11128
+ let safe = text3.slice(0, 4096);
11129
+ for (const secret of secrets)
11130
+ if (secret)
11131
+ safe = safe.split(secret).join("[REDACTED]");
11132
+ return safe.trim();
11133
+ };
11134
+ async function deployCloudflareWorker(coordinates, kvNamespaceId, apiToken, runner = defaultWorkerCommandRunner) {
11135
+ const validated = validateCloudflareBootstrapCoordinates(coordinates);
11136
+ const workerDirectoryMetadata = await stat(validated.workerDirectory);
11137
+ if (!workerDirectoryMetadata.isDirectory())
11138
+ throw new Error("Cloudflare Worker directory is not a directory");
11139
+ const workerMain = resolve2(validated.workerDirectory, validated.workerMain);
11140
+ const workerMainMetadata = await stat(workerMain);
11141
+ if (!workerMainMetadata.isFile())
11142
+ throw new Error("Cloudflare Worker main is not a regular file");
11143
+ const wrangler = resolve2(validated.workerDirectory, "node_modules/.bin/wrangler");
11144
+ const wranglerMetadata = await stat(wrangler);
11145
+ if (!wranglerMetadata.isFile())
11146
+ throw new Error("Cloudflare Wrangler is not installed in the Worker project");
11147
+ if (!/^[a-f0-9]{32}$/.test(kvNamespaceId))
11148
+ throw new Error("Cloudflare KV namespace id is invalid");
11149
+ const temporaryDirectory = await mkdtemp(join5(tmpdir(), "fz-wrangler-"));
11150
+ await chmod(temporaryDirectory, 448);
11151
+ const configurationPath = join5(temporaryDirectory, "wrangler.json");
11152
+ try {
11153
+ const handle = await open(configurationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
11154
+ try {
11155
+ await handle.writeFile(`${JSON.stringify({
11156
+ name: validated.workerScriptName,
11157
+ main: workerMain,
11158
+ compatibility_date: validated.workerCompatibilityDate,
11159
+ workers_dev: false,
11160
+ routes: validated.publicDomains.map((pattern) => ({ pattern, custom_domain: true })),
11161
+ observability: { enabled: true },
11162
+ kv_namespaces: [{ binding: "NODES", id: kvNamespaceId }]
11163
+ }, null, 2)}
11164
+ `);
11165
+ await handle.sync();
11166
+ } finally {
11167
+ await handle.close();
11168
+ }
11169
+ const result = await runner({
11170
+ command: [wrangler, "deploy", "--config", configurationPath],
11171
+ cwd: validated.workerDirectory,
11172
+ env: {
11173
+ ...inheritedWorkerEnvironment(),
11174
+ XDG_CONFIG_HOME: temporaryDirectory,
11175
+ XDG_CACHE_HOME: temporaryDirectory,
11176
+ WRANGLER_LOG_PATH: join5(temporaryDirectory, "wrangler.log"),
11177
+ CLOUDFLARE_ACCOUNT_ID: validated.accountId,
11178
+ CLOUDFLARE_API_TOKEN: apiToken,
11179
+ WRANGLER_SEND_METRICS: "false"
11180
+ }
11181
+ });
11182
+ if (result.exitCode !== 0) {
11183
+ const detail = redact(result.stderr || result.stdout || "no Wrangler diagnostic", [apiToken]);
11184
+ throw new Error(`Cloudflare Worker deployment failed with exit ${result.exitCode}: ${detail}`);
11185
+ }
11186
+ } finally {
11187
+ await rm(temporaryDirectory, { recursive: true, force: true });
11188
+ }
11189
+ }
11190
+ async function readExistingOutput(path) {
11191
+ try {
11192
+ await lstat(path);
11193
+ } catch (cause) {
11194
+ if (cause.code === "ENOENT")
11195
+ return;
11196
+ throw cause;
11197
+ }
11198
+ const text3 = await readOwnerOnlyFile(path, 1048576);
11199
+ let output;
11200
+ try {
11201
+ output = JSON.parse(text3);
11202
+ } catch {
11203
+ throw new Error(`${resolve2(path)} is not valid bootstrap JSON`);
11204
+ }
11205
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !output.resources) {
11206
+ throw new Error(`${resolve2(path)} is not a ForgeZero Cloudflare bootstrap output`);
11207
+ }
11208
+ return output;
11209
+ }
11210
+ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
11211
+ const output = await readExistingOutput(resolve2(checkpointPath));
11212
+ if (!output || output.phase !== "complete") {
11213
+ throw new Error("Cloudflare connector handoff requires a completed bootstrap checkpoint");
11214
+ }
11215
+ const normalizedNodeName = nodeName.trim().toLowerCase();
11216
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalizedNodeName)) {
11217
+ throw new Error("Cloudflare connector handoff node name is invalid");
11218
+ }
11219
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
11220
+ const expected = coordinates.nodes.find((node) => node.nodeName === normalizedNodeName);
11221
+ const matches = output.resources.nodes?.filter((node) => node.nodeName === normalizedNodeName) ?? [];
11222
+ if (!expected || matches.length !== 1) {
11223
+ throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
11224
+ }
11225
+ const resource = matches[0];
11226
+ if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !resource.applicationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(resource.tunnelId) || !/^[A-Za-z0-9._-]{40,16384}$/.test(resource.connectorToken)) {
11227
+ throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
11228
+ }
11229
+ return {
11230
+ nodeName: resource.nodeName,
11231
+ hostname: resource.hostname,
11232
+ service: resource.service,
11233
+ tunnelId: resource.tunnelId,
11234
+ connectorToken: resource.connectorToken
11235
+ };
11236
+ }
11237
+ async function readCloudflareHostHandoff(checkpointPath, nodeName) {
11238
+ const connector = await readCloudflareConnectorHandoff(checkpointPath, nodeName);
11239
+ const output = await readExistingOutput(resolve2(checkpointPath));
11240
+ const kv = output?.resources.runtimeTokens?.kv;
11241
+ if (!output || output.phase !== "complete" || !/^[a-f0-9]{32}$/i.test(output.coordinates.accountId) || !/^[a-f0-9]{32}$/i.test(output.coordinates.zoneId) || !/^[a-f0-9]{32}$/i.test(output.resources.kvNamespaceId) || !kv || !/^[A-Za-z0-9._-]{40,80}$/.test(kv.value)) {
11242
+ throw new Error("Cloudflare host handoff is missing the exact-account KV runtime capability");
11243
+ }
11244
+ const network = output.resources.runtimeTokens?.privateNetwork?.value;
11245
+ if (network !== undefined && !/^[A-Za-z0-9._-]{40,80}$/.test(network)) {
11246
+ throw new Error("Cloudflare host handoff private-network capability is malformed");
11247
+ }
11248
+ const privateNetwork = output.resources.privateNetwork;
11249
+ const access = output.resources.access;
11250
+ if (Boolean(privateNetwork) !== Boolean(network)) {
11251
+ throw new Error("Cloudflare host handoff private-network resources and capability disagree");
11252
+ }
11253
+ if (privateNetwork && (!access?.clientId || !access.clientSecret || !/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(privateNetwork.warpOrganization) || !/^[0-9a-f-]{36}$/i.test(privateNetwork.virtualNetworkId) || !privateNetwork.deviceProfileId)) {
11254
+ throw new Error("Cloudflare host handoff WARP enrollment is malformed");
11255
+ }
11256
+ return {
11257
+ ...connector,
11258
+ accountId: output.coordinates.accountId,
11259
+ zoneId: output.coordinates.zoneId,
11260
+ kvNamespaceId: output.resources.kvNamespaceId,
11261
+ kvRuntimeToken: kv.value,
11262
+ ...network ? { privateNetworkRuntimeToken: network } : {},
11263
+ ...privateNetwork && access ? { warp: {
11264
+ organization: privateNetwork.warpOrganization,
11265
+ clientId: access.clientId,
11266
+ clientSecret: access.clientSecret,
11267
+ virtualNetworkId: privateNetwork.virtualNetworkId,
11268
+ deviceProfileId: privateNetwork.deviceProfileId
11269
+ } } : {}
11270
+ };
11271
+ }
11272
+ async function prepareOwnerOutputDirectory(absolutePath) {
11273
+ const directory = dirname3(absolutePath);
11274
+ await mkdir(directory, { recursive: true, mode: 448 });
11275
+ const directoryMetadata = await stat(directory);
11276
+ const uid = ownerUid();
11277
+ if (!directoryMetadata.isDirectory() || uid !== undefined && directoryMetadata.uid !== uid || (directoryMetadata.mode & 18) !== 0) {
11278
+ throw new Error(`bootstrap output directory ${directory} must be operator-owned and not group/other writable`);
11279
+ }
11280
+ return directory;
11281
+ }
11282
+ async function writeOwnerBootstrapOutput(path, output) {
11283
+ const absolute = resolve2(path);
11284
+ const directory = await prepareOwnerOutputDirectory(absolute);
11285
+ const temporary = `${absolute}.${randomUUID()}.tmp`;
11286
+ let handle;
11287
+ try {
11288
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
11289
+ await handle.writeFile(`${JSON.stringify(output, null, 2)}
11290
+ `, { encoding: "utf8" });
11291
+ await handle.sync();
11292
+ await handle.close();
11293
+ handle = undefined;
11294
+ await rename(temporary, absolute);
11295
+ await chmod(absolute, 384);
11296
+ const directoryHandle = await open(directory, constants.O_RDONLY);
11297
+ try {
11298
+ await directoryHandle.sync();
11299
+ } finally {
11300
+ await directoryHandle.close();
11301
+ }
11302
+ } finally {
11303
+ await handle?.close();
11304
+ await unlink(temporary).catch((cause) => {
11305
+ if (cause.code !== "ENOENT")
11306
+ throw cause;
11307
+ });
11308
+ }
11309
+ }
11310
+ var tokenFor = (tokens, key) => {
11311
+ const token = tokens[key]?.trim() || tokens.apiToken?.trim();
11312
+ if (!token)
11313
+ throw new Error(`Cloudflare ${key} is not configured`);
11314
+ return token;
11315
+ };
11316
+ var initialManagementToken = (tokens) => {
11317
+ const token = tokens.apiToken?.trim();
11318
+ if (!token) {
11319
+ throw new Error("initial Cloudflare --token-file is required to create account-owned runtime tokens");
11320
+ }
11321
+ return token;
11322
+ };
11323
+ var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
11324
+ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch, workerRunner = defaultWorkerCommandRunner) {
11325
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
11326
+ const absoluteOutput = resolve2(outputPath);
11327
+ const existing = await readExistingOutput(absoluteOutput);
11328
+ if (existing && !sameCoordinates(existing.coordinates, coordinates)) {
11329
+ throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
11330
+ }
11331
+ await prepareOwnerOutputDirectory(absoluteOutput);
11332
+ const namespace = await ensureCloudflareKvNamespace({
11333
+ accountId: coordinates.accountId,
11334
+ title: coordinates.kvNamespaceTitle,
11335
+ apiToken: tokenFor(tokens, "kvApiToken")
11336
+ }, fetcher);
11337
+ const nodeResources = [];
11338
+ const createdNodes = [];
11339
+ let resources;
11340
+ for (const node of coordinates.nodes) {
11341
+ const checkpointed = existing?.resources.nodes?.find(({ nodeName }) => nodeName === node.nodeName);
11342
+ let created = false;
11343
+ if (checkpointed) {
11344
+ nodeResources.push(checkpointed);
11345
+ } else {
11346
+ const tunnel = await ensureCloudflareTunnel({
11347
+ accountId: coordinates.accountId,
11348
+ name: node.tunnelName,
11349
+ apiToken: tokenFor(tokens, "tunnelApiToken")
11350
+ }, fetcher);
11351
+ created = tunnel.created;
11352
+ nodeResources.push({
11353
+ nodeName: node.nodeName,
11354
+ hostname: node.hostname,
11355
+ service: node.service,
11356
+ tunnelName: node.tunnelName,
11357
+ tunnelId: tunnel.tunnel.id,
11358
+ connectorToken: tunnel.connectorToken
11359
+ });
11360
+ }
11361
+ createdNodes.push({ nodeName: node.nodeName, tunnel: created, application: false });
11362
+ const firstNode = nodeResources[0];
11363
+ resources = {
11364
+ tunnelId: firstNode.tunnelId,
11365
+ kvNamespaceId: namespace.namespace.id,
11366
+ hostname: firstNode.hostname,
11367
+ service: firstNode.service,
11368
+ connectorToken: firstNode.connectorToken,
11369
+ nodes: [...nodeResources],
11370
+ ...existing?.resources.access ? { access: existing.resources.access } : {},
11371
+ ...existing?.resources.runtimeTokens ? { runtimeTokens: existing.resources.runtimeTokens } : {},
11372
+ ...existing?.resources.worker ? { worker: existing.resources.worker } : {},
11373
+ ...existing?.resources.privateNetwork ? { privateNetwork: existing.resources.privateNetwork } : {}
11374
+ };
11375
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11376
+ format: 1,
11377
+ kind: "forgezero-cloudflare-bootstrap",
11378
+ phase: resources.access ? "access-token-provisioned" : "edge-resources-provisioned",
11379
+ updatedAt: new Date().toISOString(),
11380
+ coordinates,
11381
+ resources
11382
+ });
11383
+ }
11384
+ if (!resources)
11385
+ throw new Error("Cloudflare fleet has no nodes");
11386
+ if (coordinates.createRuntimeTokens && !resources.runtimeTokens) {
11387
+ resources = {
11388
+ ...resources,
11389
+ runtimeTokens: { kv: await createCloudflareAccountRuntimeToken({
11390
+ accountId: coordinates.accountId,
11391
+ name: `${coordinates.runtimeTokenNamePrefix}-kv-runtime`,
11392
+ permissionNames: ["Workers KV Storage Write"],
11393
+ apiToken: initialManagementToken(tokens)
11394
+ }, fetcher) }
11395
+ };
11396
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11397
+ format: 1,
11398
+ kind: "forgezero-cloudflare-bootstrap",
11399
+ phase: "runtime-tokens-created",
11400
+ updatedAt: new Date().toISOString(),
11401
+ coordinates,
11402
+ resources
11403
+ });
11404
+ }
11405
+ if (coordinates.createPrivateNetworkRuntimeToken && resources.runtimeTokens && !resources.runtimeTokens.privateNetwork) {
11406
+ resources = {
11407
+ ...resources,
11408
+ runtimeTokens: {
11409
+ ...resources.runtimeTokens,
11410
+ privateNetwork: await createCloudflareAccountRuntimeToken({
11411
+ accountId: coordinates.accountId,
11412
+ name: `${coordinates.runtimeTokenNamePrefix}-private-network-runtime`,
11413
+ permissionNames: ["Cloudflare One Networks Write", "Zero Trust Write"],
11414
+ apiToken: initialManagementToken(tokens)
11415
+ }, fetcher)
11416
+ }
11417
+ };
11418
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11419
+ format: 1,
11420
+ kind: "forgezero-cloudflare-bootstrap",
11421
+ phase: "runtime-tokens-created",
11422
+ updatedAt: new Date().toISOString(),
11423
+ coordinates,
11424
+ resources
11425
+ });
11426
+ }
11427
+ if (!resources.worker) {
11428
+ await deployCloudflareWorker(coordinates, namespace.namespace.id, tokenFor(tokens, "workerApiToken"), workerRunner);
11429
+ resources = {
11430
+ ...resources,
11431
+ worker: {
11432
+ scriptName: coordinates.workerScriptName,
11433
+ publicDomains: coordinates.publicDomains,
11434
+ deployed: true
11435
+ }
11436
+ };
11437
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11438
+ format: 1,
11439
+ kind: "forgezero-cloudflare-bootstrap",
11440
+ phase: "worker-deployed",
11441
+ updatedAt: new Date().toISOString(),
11442
+ coordinates,
11443
+ resources
11444
+ });
11445
+ }
11446
+ const serviceToken = await ensureCloudflareAccessServiceToken({
11447
+ accountId: coordinates.accountId,
11448
+ name: coordinates.serviceTokenName,
11449
+ apiToken: tokenFor(tokens, "accessApiToken"),
11450
+ existing: resources.access
11451
+ }, fetcher);
11452
+ resources = { ...resources, access: serviceToken.credentials };
11453
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11454
+ format: 1,
11455
+ kind: "forgezero-cloudflare-bootstrap",
11456
+ phase: "access-token-provisioned",
11457
+ updatedAt: new Date().toISOString(),
11458
+ coordinates,
11459
+ resources
11460
+ });
11461
+ const policy = await ensureCloudflareAccessPolicy({
11462
+ accountId: coordinates.accountId,
11463
+ name: coordinates.policyName,
11464
+ serviceTokenId: serviceToken.credentials.tokenId,
11465
+ apiToken: tokenFor(tokens, "accessApiToken")
11466
+ }, fetcher);
11467
+ if (!policy.policy.id)
11468
+ throw new Error("Cloudflare did not return the Access policy id");
11469
+ resources = {
11470
+ ...resources,
11471
+ access: { ...serviceToken.credentials, policyId: policy.policy.id }
11472
+ };
11473
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11474
+ format: 1,
11475
+ kind: "forgezero-cloudflare-bootstrap",
11476
+ phase: "access-token-provisioned",
11477
+ updatedAt: new Date().toISOString(),
11478
+ coordinates,
11479
+ resources
11480
+ });
11481
+ let privateNetworkCreated = false;
11482
+ if (coordinates.privateNetwork && !resources.privateNetwork) {
11483
+ const managementToken = initialManagementToken(tokens);
11484
+ const virtualNetwork = await ensureCloudflareVirtualNetwork({
11485
+ accountId: coordinates.accountId,
11486
+ name: coordinates.privateNetwork.virtualNetworkName,
11487
+ comment: "ForgeZero private database network",
11488
+ apiToken: managementToken
11489
+ }, fetcher);
11490
+ const enrollment = await ensureCloudflareWarpEnrollmentApplication({
11491
+ accountId: coordinates.accountId,
11492
+ name: coordinates.privateNetwork.enrollmentApplicationName,
11493
+ policyId: policy.policy.id,
11494
+ apiToken: tokenFor(tokens, "accessApiToken")
11495
+ }, fetcher);
11496
+ const deviceProfile = await ensureCloudflareWarpDevicePolicy({
11497
+ accountId: coordinates.accountId,
11498
+ name: coordinates.privateNetwork.deviceProfileName,
11499
+ serviceTokenId: serviceToken.credentials.tokenId,
11500
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11501
+ precedence: coordinates.privateNetwork.deviceProfilePrecedence,
11502
+ apiToken: managementToken
11503
+ }, fetcher);
11504
+ const routes = [];
11505
+ for (const node of coordinates.nodes.filter((item) => item.privateAddress)) {
11506
+ const resource = resources.nodes.find((item) => item.nodeName === node.nodeName);
11507
+ const route = await ensureCloudflarePrivateDatabaseRoute({
11508
+ accountId: coordinates.accountId,
11509
+ tunnelId: resource.tunnelId,
11510
+ privateAddress: node.privateAddress,
11511
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11512
+ comment: `ForgeZero ${node.nodeName} database`,
11513
+ apiToken: managementToken
11514
+ }, fetcher);
11515
+ await ensureCloudflareWarpDatabaseInclude({
11516
+ accountId: coordinates.accountId,
11517
+ policyId: deviceProfile.policy.id,
11518
+ privateAddress: node.privateAddress,
11519
+ description: `ForgeZero ${node.nodeName} database`,
11520
+ apiToken: managementToken
11521
+ }, fetcher);
11522
+ routes.push({ nodeName: node.nodeName, routeId: route.route.id, privateAddress: node.privateAddress });
11523
+ }
11524
+ resources = {
11525
+ ...resources,
11526
+ privateNetwork: {
11527
+ warpOrganization: coordinates.privateNetwork.warpOrganization,
11528
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11529
+ deviceProfileId: deviceProfile.policy.id,
11530
+ enrollmentApplicationId: enrollment.application.id,
11531
+ routes
11532
+ }
11533
+ };
11534
+ privateNetworkCreated = virtualNetwork.created || enrollment.created || deviceProfile.created || routes.length > 0;
11535
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11536
+ format: 1,
11537
+ kind: "forgezero-cloudflare-bootstrap",
11538
+ phase: "access-token-provisioned",
11539
+ updatedAt: new Date().toISOString(),
11540
+ coordinates,
11541
+ resources
11542
+ });
11543
+ }
11544
+ for (const node of coordinates.nodes) {
11545
+ const application = await ensureCloudflareAccessApplication({
11546
+ accountId: coordinates.accountId,
11547
+ name: node.applicationName,
11548
+ hostname: node.hostname,
11549
+ policyId: policy.policy.id,
11550
+ apiToken: tokenFor(tokens, "accessApiToken")
11551
+ }, fetcher);
11552
+ if (!application.application.id)
11553
+ throw new Error(`Cloudflare did not return the Access application id for ${node.nodeName}`);
11554
+ resources = {
11555
+ ...resources,
11556
+ nodes: resources.nodes.map((resource) => resource.nodeName === node.nodeName ? { ...resource, applicationId: application.application.id } : resource),
11557
+ access: {
11558
+ ...resources.access,
11559
+ ...node.nodeName === coordinates.nodes[0].nodeName ? { applicationId: application.application.id } : {}
11560
+ }
11561
+ };
11562
+ const createdNode = createdNodes.find(({ nodeName }) => nodeName === node.nodeName);
11563
+ createdNode.application = application.created;
11564
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11565
+ format: 1,
11566
+ kind: "forgezero-cloudflare-bootstrap",
11567
+ phase: "access-token-provisioned",
11568
+ updatedAt: new Date().toISOString(),
11569
+ coordinates,
11570
+ resources
11571
+ });
11572
+ }
11573
+ await configureCloudflareWorkerAccessSecrets({
11574
+ accountId: coordinates.accountId,
11575
+ scriptName: coordinates.workerScriptName,
11576
+ credentials: serviceToken.credentials,
11577
+ apiToken: tokenFor(tokens, "workerApiToken")
11578
+ }, fetcher);
11579
+ for (const node of resources.nodes) {
11580
+ await configureCloudflareEdge({
11581
+ accountId: coordinates.accountId,
11582
+ zoneId: coordinates.zoneId,
11583
+ tunnelId: node.tunnelId,
11584
+ hostname: node.hostname,
11585
+ service: node.service,
11586
+ apiToken: tokenFor(tokens, "tunnelApiToken"),
11587
+ tunnelApiToken: tokenFor(tokens, "tunnelApiToken"),
11588
+ dnsApiToken: tokenFor(tokens, "dnsApiToken")
11589
+ }, fetcher);
11590
+ }
11591
+ const output = {
11592
+ format: 1,
11593
+ kind: "forgezero-cloudflare-bootstrap",
11594
+ phase: "complete",
11595
+ updatedAt: new Date().toISOString(),
11596
+ coordinates,
11597
+ resources,
11598
+ created: {
11599
+ tunnel: createdNodes.some(({ tunnel }) => tunnel),
11600
+ kvNamespace: namespace.created,
11601
+ serviceToken: serviceToken.created,
11602
+ policy: policy.created,
11603
+ application: createdNodes.some(({ application }) => application),
11604
+ privateNetwork: privateNetworkCreated,
11605
+ workerDeployed: true,
11606
+ nodes: createdNodes
11607
+ }
11608
+ };
11609
+ await writeOwnerBootstrapOutput(absoluteOutput, output);
11610
+ return output;
11611
+ }
11612
+ async function runAttendedCloudflareBootstrap(request2, dependencies = {}) {
11613
+ const plan = planCloudflareBootstrap(request2.coordinates, request2.checkpointPath);
11614
+ if (request2.mode === "plan") {
11615
+ return {
11616
+ format: 1,
11617
+ kind: "forgezero-cloudflare-bootstrap-evidence",
11618
+ phase: "planned",
11619
+ checkpointFile: plan.outputFile,
11620
+ workerScriptName: plan.coordinates.workerScriptName,
11621
+ publicDomains: plan.coordinates.publicDomains,
11622
+ nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
11623
+ };
11624
+ }
11625
+ if (!request2.tokenFiles || !Object.values(request2.tokenFiles).some(Boolean)) {
11626
+ throw new Error("Cloudflare apply requires owner-only management token file paths");
11627
+ }
11628
+ if (plan.coordinates.createRuntimeTokens && !request2.tokenFiles.apiTokenFile) {
11629
+ throw new Error("Cloudflare runtime-token creation requires the initial management token file");
11630
+ }
11631
+ const tokens = await readCloudflareBootstrapTokens(request2.tokenFiles);
11632
+ const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch, dependencies.workerRunner);
11633
+ return {
11634
+ format: 1,
11635
+ kind: "forgezero-cloudflare-bootstrap-evidence",
11636
+ phase: "complete",
11637
+ checkpointFile: plan.outputFile,
11638
+ kvNamespaceId: output.resources.kvNamespaceId,
11639
+ workerScriptName: output.coordinates.workerScriptName,
11640
+ publicDomains: output.resources.worker?.publicDomains ?? output.coordinates.publicDomains,
11641
+ runtimeTokenIds: {
11642
+ kv: output.resources.runtimeTokens?.kv.id,
11643
+ privateNetwork: output.resources.runtimeTokens?.privateNetwork?.id
11644
+ },
11645
+ nodes: output.resources.nodes.map(({ nodeName, hostname, tunnelId, applicationId }) => ({
11646
+ nodeName,
11647
+ hostname,
11648
+ tunnelId,
11649
+ applicationId
11650
+ }))
11651
+ };
11652
+ }
11653
+
11654
+ // src/bootstrap.ts
11655
+ init_dist();
11656
+ var PLATFORM_BOOTSTRAP_PROFILES = [
11657
+ "platform-db-api",
11658
+ "platform-api"
11659
+ ];
11660
+ var STATE_PATH = "/var/lib/forgezero/bootstrap.json";
11661
+ var CREDS = "/etc/forgezero/creds";
11662
+ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
11663
+ var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
11664
+ var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
11665
+ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
11666
+ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
11667
+ var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
11668
+ var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
11669
+ var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
11670
+ var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
11671
+ var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
11672
+ var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
11673
+ var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
11674
+ var privateOrigin = (value) => {
11675
+ let url;
11676
+ try {
11677
+ url = new URL(value);
11678
+ } catch {
11679
+ throw new Error(`database coordinator is malformed: ${value}`);
11680
+ }
11681
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
11682
+ throw new Error(`database coordinators must be credential-free HTTP(S) origins: ${value}`);
11683
+ }
11684
+ const host = url.hostname.replace(/^\[|\]$/g, "");
11685
+ const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
11686
+ const privateV4 = v4 && v4.every((part) => part >= 0 && part <= 255) && (v4[0] === 10 || v4[0] === 172 && v4[1] >= 16 && v4[1] <= 31 || v4[0] === 192 && v4[1] === 168);
11687
+ const privateV6 = host === "::1" || /^f[cd][0-9a-f]:/i.test(host);
11688
+ if (!privateV4 && !privateV6 && host !== "127.0.0.1" && host !== "localhost") {
11689
+ throw new Error(`database coordinator must use a private address: ${value}`);
11690
+ }
11691
+ return url.origin;
11692
+ };
11693
+ var privateCidr = (value) => {
11694
+ const match = value.match(/^(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\/(\d|[12]\d|3[0-2])$/);
11695
+ if (!match || match[1].split(".").some((part) => Number(part) > 255))
11696
+ throw new Error(`private firewall CIDR is malformed: ${value}`);
11697
+ return value;
11698
+ };
11699
+ var privateFile = (host, path, label) => {
11700
+ if (!host.exists(path))
11701
+ throw new Error(`${label} file is missing: ${path}`);
11702
+ const metadata = host.inspect?.(path);
11703
+ if (metadata && (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 16 * 1024)) {
11704
+ throw new Error(`${label} must be a root-owned, owner-only regular file with one link and at most 16 KiB`);
11705
+ }
11706
+ const value = host.read(path).trim();
11707
+ if (!value)
11708
+ throw new Error(`${label} file is empty: ${path}`);
11709
+ return value;
11710
+ };
11711
+ function validateBootstrapConfig(value) {
11712
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?){2,}$/.test(value.nodeHostname)) {
11713
+ throw new Error("node hostname must be a lowercase public FQDN");
11714
+ }
11715
+ let telemetry;
11716
+ try {
11717
+ telemetry = new URL(value.telemetryEndpoint);
11718
+ } catch {
11719
+ throw new Error("telemetry endpoint is malformed");
11720
+ }
11721
+ if (telemetry.protocol !== "https:" || telemetry.port && telemetry.port !== "443" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
11722
+ throw new Error("telemetry endpoint must be public HTTPS on port 443 without credentials, query or fragment");
11723
+ }
11724
+ if (value.kind === "tenant") {
11725
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
11726
+ throw new Error("tenant realm is malformed");
11727
+ if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
11728
+ throw new Error("tenant API must be public HTTPS or loopback HTTP");
11729
+ }
11730
+ if (!value.enrolTokenFile)
11731
+ throw new Error("tenant bootstrap requires --enrol-token-file");
11732
+ if (value.bootstrapRunner) {
11733
+ if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
11734
+ throw new Error("bootstrap runner SSH private-key file must be absolute");
11735
+ }
11736
+ let targetTelemetry;
11737
+ try {
11738
+ targetTelemetry = new URL(value.bootstrapRunner.targetTelemetryEndpoint);
11739
+ } catch {
11740
+ throw new Error("bootstrap target telemetry endpoint is malformed");
11741
+ }
11742
+ if (targetTelemetry.protocol !== "https:" || targetTelemetry.username || targetTelemetry.password || targetTelemetry.search || targetTelemetry.hash || targetTelemetry.port && targetTelemetry.port !== "443") {
11743
+ throw new Error("bootstrap target telemetry must be public HTTPS on port 443");
11744
+ }
11745
+ }
11746
+ return value;
11747
+ }
11748
+ if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
11749
+ throw new Error("unsupported platform software profile");
11750
+ if (!["production", "development"].includes(value.environment))
11751
+ throw new Error("platform environment must be production or development");
11752
+ let api;
11753
+ try {
11754
+ api = new URL(value.apiUrl);
11755
+ } catch {
11756
+ throw new Error("platform API URL is malformed");
11757
+ }
11758
+ if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash) {
11759
+ throw new Error("platform API must be public HTTPS without credentials, query or fragment");
11760
+ }
11761
+ const expected = value.profile === "platform-api" ? { role: "none", mode: "default" } : { role: undefined, mode: "default" };
11762
+ if (expected.role && value.database.role !== expected.role || value.database.serverMode !== expected.mode || value.profile === "platform-db-api" && !["master", "joiner"].includes(value.database.role)) {
11763
+ throw new Error("platform profile, database role and Coordinator mode disagree");
11764
+ }
11765
+ if (value.database.role !== "none" && !value.database.address)
11766
+ throw new Error("database nodes require a private address");
11767
+ if (value.database.role === "joiner" && !value.database.master)
11768
+ throw new Error("database joiners require the master starter address");
11769
+ if (!/^(?:dev-)?fz-n[1-9][0-9]{0,2}$/.test(value.computeReference))
11770
+ throw new Error("platform compute reference is malformed");
11771
+ if (!value.database.bootstrapSecretFile)
11772
+ throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
11773
+ const write = value.database.coordinators.map(privateOrigin);
11774
+ if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
11775
+ throw new Error("database coordinators must contain 1-16 unique private origins");
11776
+ }
11777
+ value.database.coordinators = write;
11778
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
11779
+ allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
11780
+ });
11781
+ if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.repository !== value.repository || runtime.branch !== value.branch || runtime.databaseCoordinators.join(",") !== write.join(",")) {
11782
+ throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
11783
+ }
11784
+ if (runtime.deployProfile !== value.environment)
11785
+ throw new Error("runtime deployment profile disagrees with bootstrap environment");
11786
+ if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
11787
+ throw new Error("runtime database topology disagrees with bootstrap topology");
11788
+ }
11789
+ if (!Number.isSafeInteger(value.firewall.sshPort) || value.firewall.sshPort < 1 || value.firewall.sshPort > 65535) {
11790
+ throw new Error("firewall SSH port is invalid");
11791
+ }
11792
+ value.firewall.privateCidrs = value.firewall.privateCidrs.map(privateCidr);
11793
+ if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
11794
+ throw new Error("enabled firewall requires unique private cluster CIDRs");
11795
+ }
11796
+ if (Number(value.computeReference.match(/n(\d+)$/)?.[1]) > 3 && !value.platformEnrolTokenFile) {
11797
+ throw new Error("post-genesis platform computes require an API-issued enrolment token file");
11798
+ }
11799
+ if (value.cloudflareHandoff && (!value.installCloudflared || !value.cloudflareHandoff.checkpointFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
11800
+ throw new Error("Cloudflare handoff requires cloudflared installation, a checkpoint and a valid node name");
11801
+ }
11802
+ value.runtime.environment = runtime;
11803
+ return value;
11804
+ }
11805
+ function planBootstrap(input, initialized = false) {
11806
+ const config = validateBootstrapConfig(structuredClone(input));
11807
+ const software = config.kind === "tenant" ? [
11808
+ ...config.software ?? [],
11809
+ ...config.bootstrapRunner && !config.software?.some(({ id: id2 }) => id2 === "openssh-client") ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
11810
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
11811
+ ] : [
11812
+ ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
11813
+ { id: "bun", version: "1.3.14" },
11814
+ { id: "nginx", version: "ubuntu-26.04" },
11815
+ ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
11816
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
11817
+ ];
11818
+ return {
11819
+ kind: config.kind,
11820
+ mode: initialized ? "repair" : "install",
11821
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
11822
+ software,
11823
+ statePath: STATE_PATH,
11824
+ steps: [
11825
+ { id: "agent", label: "Install and verify the common Agent supervision boundary", mutation: true },
11826
+ { id: "software", label: "Install exact Agent-owned software coordinates in order", mutation: true },
11827
+ { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
11828
+ ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
11829
+ ...config.kind === "platform" ? [
11830
+ { id: "runtime", label: "Install the shared environment, API slots, edge and activation boundary", mutation: true },
11831
+ { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
11832
+ { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
11833
+ ] : [],
11834
+ ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
11835
+ { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
11836
+ { id: "status", label: "Verify services and immutable profile", mutation: false }
11837
+ ]
11838
+ };
11839
+ }
11840
+ var checked = async (host, argv, label, options) => {
11841
+ const result = await host.exec(argv, options);
11842
+ if (result.exitCode !== 0)
11843
+ throw new Error(`${label} failed: ${result.output.trim()}`);
11844
+ return result.output;
11845
+ };
11846
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
11847
+ var unitEscape = (value) => {
11848
+ if (/[^A-Za-z0-9_./:@,+-]/.test(value))
11849
+ throw new Error(`unsafe systemd coordinate: ${value}`);
11850
+ return value;
11851
+ };
11852
+ function databaseUnit(config) {
11853
+ const db = config.database;
11854
+ const join6 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
11855
+ return `[Unit]
11856
+ Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role})
11857
+ After=network-online.target
11858
+ Wants=network-online.target
11859
+
11860
+ [Service]
11861
+ Type=simple
11862
+ User=arangodb
11863
+ Group=arangodb
11864
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
11865
+ ExecStart=/usr/bin/arangodb --starter.mode=cluster --starter.address=${unitEscape(db.address)} --starter.host=${unitEscape(db.address)} --starter.data-dir=/var/lib/forgezero-cluster --auth.jwt-secret=%d/arangodb-jwt${join6}
11866
+ Restart=always
11867
+ RestartSec=5
11868
+ UMask=0077
11869
+ NoNewPrivileges=true
11870
+ PrivateTmp=true
11871
+ PrivateDevices=true
11872
+ ProtectSystem=strict
11873
+ ProtectHome=true
11874
+ ReadWritePaths=/var/lib/forgezero-cluster
11875
+ ProtectKernelTunables=true
11876
+ ProtectKernelModules=true
11877
+ ProtectControlGroups=true
11878
+ RestrictSUIDSGID=true
11879
+ LockPersonality=true
11880
+
11881
+ [Install]
11882
+ WantedBy=multi-user.target
11883
+ `;
11884
+ }
11885
+ function databaseVerifyUnit(config) {
11886
+ const { address } = config.database;
11887
+ return `[Unit]
11888
+ Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
11889
+ Requires=forgezero-db.service
11890
+ After=forgezero-db.service
11891
+ PartOf=forgezero-db.service
11892
+
11893
+ [Service]
11894
+ Type=oneshot
11895
+ User=arangodb
11896
+ Group=arangodb
11897
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
11898
+ ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default")quit(0);last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
11899
+ RemainAfterExit=yes
11900
+ TimeoutStartSec=200
11901
+ NoNewPrivileges=true
11902
+ PrivateTmp=true
11903
+ PrivateDevices=true
11904
+ ProtectSystem=strict
11905
+ ProtectHome=true
11906
+
11907
+ [Install]
11908
+ WantedBy=multi-user.target
11909
+ `;
11910
+ }
11911
+ function tunnelUnit() {
11912
+ return `[Unit]
11913
+ Description=ForgeZero Cloudflare Tunnel connector
11914
+ After=network-online.target
11915
+ Wants=network-online.target
11916
+
11917
+ [Service]
11918
+ Type=simple
11919
+ DynamicUser=yes
11920
+ LoadCredentialEncrypted=cloudflared-token:${TUNNEL_CREDENTIAL}
11921
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate run --token-file %d/cloudflared-token
11922
+ Restart=always
11923
+ RestartSec=5
11924
+ NoNewPrivileges=true
11925
+ PrivateTmp=true
11926
+ ProtectSystem=strict
11927
+ ProtectHome=true
11928
+
11929
+ [Install]
11930
+ WantedBy=multi-user.target
11931
+ `;
11932
+ }
11933
+ var derive = (root, label) => {
11934
+ if (!/^[a-f0-9]{64}$/i.test(root))
11935
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
11936
+ return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
11937
+ };
11938
+ async function seal(host, name, destination, value) {
11939
+ if (host.exists(destination))
11940
+ return;
11941
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
11942
+ if (result.exitCode !== 0)
11943
+ throw new Error(`could not seal ${name}: ${result.output.trim()}`);
11944
+ }
11945
+ function stateFor(config) {
11946
+ return `${JSON.stringify({
11947
+ format: 1,
11948
+ kind: config.kind,
11949
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
11950
+ nodeHostname: config.nodeHostname,
11951
+ apiUrl: config.apiUrl,
11952
+ ...config.kind === "platform" ? {
11953
+ environment: config.environment,
11954
+ databaseRole: config.database.role,
11955
+ databaseServerMode: config.database.serverMode,
11956
+ databaseAddress: config.database.address,
11957
+ databaseCoordinators: config.database.coordinators,
11958
+ databaseModeEvidence: config.database.role === "none" ? undefined : DB_MODE_EVIDENCE,
11959
+ collectorUnit: config.runtime.environment.otlpCollectorUnit,
11960
+ cloudflared: Boolean(config.cloudflareHandoff)
11961
+ } : { realm: config.realm }
11962
+ }, null, 2)}
11963
+ `;
11964
+ }
11965
+ async function bootstrapStatus(host = localBootstrapHost()) {
11966
+ if (!host.exists(STATE_PATH))
11967
+ return { initialized: false, services: {}, problems: ["bootstrap state is missing"] };
11968
+ let state;
11969
+ try {
11970
+ state = JSON.parse(host.read(STATE_PATH));
11971
+ } catch {
11972
+ return { initialized: false, services: {}, problems: ["bootstrap state is malformed"] };
11973
+ }
11974
+ const units = ["forgezero-agent.service", "forgezero-agent.socket"];
11975
+ if (state.kind === "platform")
11976
+ units.push("nginx.service");
11977
+ if (state.kind === "platform" && state.collectorUnit)
11978
+ units.push(state.collectorUnit);
11979
+ if (state.kind === "platform" && state.cloudflared)
11980
+ units.push("cloudflared.service");
11981
+ if (state.kind === "platform" && state.databaseRole !== "none")
11982
+ units.push("forgezero-db.service", "forgezero-db-verify.service");
11983
+ const services = {};
11984
+ const problems = [];
11985
+ for (const unit of units) {
11986
+ const result = await host.exec(["systemctl", "is-active", "--quiet", unit]);
11987
+ services[unit] = result.exitCode === 0;
11988
+ if (result.exitCode !== 0)
11989
+ problems.push(`${unit} is not active`);
11990
+ }
11991
+ if (state.kind === "platform") {
11992
+ const [blue, green] = await Promise.all([
11993
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
11994
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
11995
+ ]);
11996
+ services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
11997
+ if (!services["forgezero@active.service"])
11998
+ problems.push("neither API slot is active");
11999
+ }
12000
+ if (!host.exists("/var/lib/forgezero/enrolment.json"))
12001
+ problems.push("durable Agent enrolment state is missing");
12002
+ return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
12003
+ }
12004
+ async function applyBootstrap(input, host = localBootstrapHost()) {
12005
+ const config = validateBootstrapConfig(structuredClone(input));
12006
+ if (host.uid() !== 0)
12007
+ throw new Error("fz bootstrap --apply must run as root");
12008
+ if (config.kind === "tenant") {
12009
+ const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
12010
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
12011
+ throw new Error("tenant enrolment token is malformed");
12012
+ }
12013
+ let cloudflare;
12014
+ if (config.kind === "platform" && config.cloudflareHandoff) {
12015
+ cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.checkpointFile, config.cloudflareHandoff.nodeName);
12016
+ const expected = config.runtime.environment.cloudflare;
12017
+ if (cloudflare.hostname !== config.nodeHostname)
12018
+ throw new Error("Cloudflare checkpoint hostname disagrees with platform node hostname");
12019
+ if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
12020
+ throw new Error("Cloudflare checkpoint disagrees with immutable platform runtime coordinates");
12021
+ }
12022
+ const discovered = {
12023
+ accountId: cloudflare.accountId,
12024
+ zoneId: cloudflare.zoneId,
12025
+ kvNamespaceId: cloudflare.kvNamespaceId,
12026
+ tunnelId: cloudflare.tunnelId,
12027
+ tunnelService: cloudflare.service,
12028
+ ...cloudflare.warp ? { warp: {
12029
+ organization: cloudflare.warp.organization,
12030
+ virtualNetworkId: cloudflare.warp.virtualNetworkId,
12031
+ deviceProfileId: cloudflare.warp.deviceProfileId
12032
+ } } : {}
12033
+ };
12034
+ if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
12035
+ throw new Error("Cloudflare checkpoint disagrees with immutable WARP/runtime coordinates");
12036
+ }
12037
+ config.runtime.environment.cloudflare = expected ?? discovered;
12038
+ if (config.runtime.environment.databaseNetworkMode === "cloudflare-warp" !== Boolean(cloudflare.warp)) {
12039
+ throw new Error("Cloudflare checkpoint private-network mode disagrees with the platform database network mode");
12040
+ }
12041
+ }
12042
+ const plan = planBootstrap(config, host.exists(STATE_PATH));
12043
+ const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
12044
+ host.mkdir(CREDS, 448);
12045
+ host.mkdir("/var/lib/forgezero", 448);
12046
+ if (config.kind === "tenant" && config.bootstrapRunner && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
12047
+ await seal(host, "bootstrap-ssh-key", BOOTSTRAP_SSH_CREDENTIAL, privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key"));
12048
+ host.remove(config.bootstrapRunner.sshPrivateKeyFile);
12049
+ }
12050
+ if (cloudflare?.warp) {
12051
+ await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
12052
+ await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
12053
+ }
12054
+ await host.installAgent(config, alreadyEnrolled && config.kind === "platform" ? PLATFORM_ENROL_SOURCE : undefined);
12055
+ if (config.kind === "platform" && config.firewall.enabled) {
12056
+ await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
12057
+ } else
12058
+ await host.ensureSoftware(plan.software);
12059
+ if (config.kind === "platform" && config.firewall.enabled) {
12060
+ await checked(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
12061
+ await checked(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
12062
+ await checked(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
12063
+ for (const cidr of config.firewall.privateCidrs) {
12064
+ if (config.database.role !== "none")
12065
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
12066
+ for (const port of [config.runtime.bluePort, config.runtime.greenPort])
12067
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
12068
+ }
12069
+ await checked(host, ["ufw", "--force", "enable"], "firewall activation");
12070
+ await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 !== "ufw"));
12071
+ }
12072
+ if (config.kind === "platform") {
12073
+ const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
12074
+ if (!host.exists(JWT_CREDENTIAL)) {
12075
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
12076
+ }
12077
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
12078
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
12079
+ const credentialFiles = config.runtime.credentialFiles ?? {};
12080
+ for (const [name, source] of Object.entries({
12081
+ "bootstrap-smtp-password": credentialFiles.smtpPassword,
12082
+ "backup-s3-secret": credentialFiles.backupS3Secret
12083
+ })) {
12084
+ if (source) {
12085
+ const destination = `${CREDS}/${name}.cred`;
12086
+ if (!host.exists(destination)) {
12087
+ await seal(host, name, destination, privateFile(host, source, name));
12088
+ host.remove(source);
12089
+ }
12090
+ }
12091
+ }
12092
+ if (cloudflare) {
12093
+ await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
12094
+ if (cloudflare.privateNetworkRuntimeToken)
12095
+ await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
12096
+ }
12097
+ const runtime = config.runtime;
12098
+ const envPath = `${runtime.environment.sharedDirectory}/.env`;
12099
+ const credentials = platformApiCredentialSpecs({
12100
+ smtp: Boolean(credentialFiles.smtpPassword),
12101
+ cloudflareKv: Boolean(cloudflare),
12102
+ cloudflareNetwork: Boolean(cloudflare?.privateNetworkRuntimeToken)
12103
+ });
12104
+ const units = renderPlatformApiUnits({
12105
+ serviceUser: runtime.serviceUser,
12106
+ sharedDirectory: runtime.environment.sharedDirectory,
12107
+ sharedEnvironmentFile: envPath,
12108
+ slotsDirectory: runtime.slotsDirectory,
12109
+ bluePort: runtime.bluePort,
12110
+ greenPort: runtime.greenPort,
12111
+ collectorUnit: runtime.environment.otlpCollectorUnit,
12112
+ credentials
12113
+ });
12114
+ const edge = renderPlatformNginx({ publicPort: runtime.environment.publicApiPort, initialSlotPort: runtime.bluePort });
12115
+ const activation = renderPlatformActivationFiles({
12116
+ root: config.deployRoot ?? "/opt/forgezero",
12117
+ serviceUser: runtime.serviceUser,
12118
+ bluePort: runtime.bluePort,
12119
+ greenPort: runtime.greenPort,
12120
+ healthPath: runtime.healthPath,
12121
+ keepReleases: runtime.keepReleases
12122
+ });
12123
+ await checked(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
12124
+ await checked(host, ["id", runtime.serviceUser], "existing API service account");
12125
+ });
12126
+ host.mkdir(runtime.environment.sharedDirectory, 488);
12127
+ host.mkdir(runtime.slotsDirectory, 493);
12128
+ host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
12129
+ await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
12130
+ host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
12131
+ host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
12132
+ host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
12133
+ host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
12134
+ host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
12135
+ host.write("/etc/forgezero/deploy.env", activation.environment, 420);
12136
+ host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
12137
+ host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
12138
+ await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
12139
+ const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
12140
+ await checked(host, telemetry.unitCheck.argv, "OTLP collector supervision");
12141
+ const otlpStatus = (await checked(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
12142
+ if (!/^2\d\d$/.test(otlpStatus))
12143
+ throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
12144
+ await checked(host, ["nginx", "-t"], "nginx configuration");
12145
+ await checked(host, ["systemctl", "daemon-reload"], "systemd reload");
12146
+ await checked(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
12147
+ if (config.database.role === "master") {
12148
+ const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
12149
+ if (!host.exists(invite)) {
12150
+ host.write(invite, `plt_${randomBytes5(24).toString("hex")}
12151
+ `, 384);
12152
+ await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
12153
+ }
12154
+ }
12155
+ if (config.database.role !== "none") {
12156
+ host.mkdir("/var/lib/forgezero-cluster", 448);
12157
+ await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
12158
+ host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
12159
+ host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
12160
+ await checked(host, ["systemctl", "daemon-reload"], "database unit reload");
12161
+ await checked(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
12162
+ const evidence = {
12163
+ expectedMode: "default",
12164
+ role: "COORDINATOR",
12165
+ unit: "forgezero-db-verify.service",
12166
+ verifiedAt: new Date().toISOString()
12167
+ };
12168
+ host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
12169
+ `, 384);
12170
+ }
12171
+ if (!alreadyEnrolled) {
12172
+ const enrolToken = config.platformEnrolTokenFile ? privateFile(host, config.platformEnrolTokenFile, "platform enrolment token") : `fze_${derive(derive(root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}`;
12173
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
12174
+ throw new Error("platform enrolment token is malformed");
12175
+ host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
12176
+ `, 384);
12177
+ }
12178
+ await checked(host, [
12179
+ "runuser",
12180
+ "-u",
12181
+ "forgezero-agent",
12182
+ "--",
12183
+ "/usr/local/bin/fz-agent",
12184
+ "deploy",
12185
+ ...config.database.role === "master" ? ["--release-executor"] : []
12186
+ ], "initial Agent deployment");
12187
+ if (!alreadyEnrolled) {
12188
+ await host.installAgent(config, PLATFORM_ENROL_SOURCE);
12189
+ if (config.platformEnrolTokenFile)
12190
+ host.remove(config.platformEnrolTokenFile);
12191
+ }
12192
+ }
12193
+ if (config.kind === "platform" && cloudflare) {
12194
+ if (!host.exists(TUNNEL_CREDENTIAL)) {
12195
+ await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
12196
+ }
12197
+ host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
12198
+ await checked(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
12199
+ await checked(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
12200
+ }
12201
+ host.write(STATE_PATH, stateFor(config), 384);
12202
+ const status = await bootstrapStatus(host);
12203
+ if (!status.initialized)
12204
+ throw new Error(`bootstrap verification failed: ${status.problems.join("; ")}`);
12205
+ let launch;
12206
+ if (config.kind === "platform" && config.database.role === "master") {
12207
+ const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
12208
+ if (!host.exists(invitePath))
12209
+ throw new Error("platform invite is missing after deployment");
12210
+ const token = host.read(invitePath).trim();
12211
+ if (!/^plt_[a-f0-9]{48}$/.test(token))
12212
+ throw new Error("platform invite is malformed");
12213
+ launch = {
12214
+ command: `sudo -u ${shellQuote(config.runtime.serviceUser)} env FZ_SHARED_DIR=${shellQuote(config.runtime.environment.sharedDirectory)} /usr/local/bin/fz genesis --mode 2-of-3 --api ${shellQuote(config.apiUrl)} --app ${shellQuote(config.runtime.environment.appOrigin)}`,
12215
+ inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
12216
+ };
12217
+ }
12218
+ return { plan, applied: true, status, ...launch ? { launch } : {} };
12219
+ }
12220
+ var exactKeys2 = (value, allowed, label) => {
12221
+ if (!value || typeof value !== "object" || Array.isArray(value))
12222
+ throw new Error(`${label} must be an object`);
12223
+ const record2 = value;
12224
+ const unknown = Object.keys(record2).filter((key) => !allowed.includes(key));
12225
+ if (unknown.length)
12226
+ throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
12227
+ return record2;
12228
+ };
12229
+ function strictBootstrapDocument(value) {
12230
+ const root = exactKeys2(value, [
12231
+ "kind",
12232
+ "environment",
12233
+ "profile",
12234
+ "computeReference",
12235
+ "nodeHostname",
12236
+ "apiUrl",
12237
+ "repository",
12238
+ "branch",
12239
+ "deployRoot",
12240
+ "telemetryEndpoint",
12241
+ "database",
12242
+ "platformEnrolTokenFile",
12243
+ "runtime",
12244
+ "firewall",
12245
+ "installCloudflared",
12246
+ "cloudflareHandoff",
12247
+ "realm",
12248
+ "enrolTokenFile",
12249
+ "software",
12250
+ "bootstrapRunner"
12251
+ ], "bootstrap config");
12252
+ if (root.kind === "platform") {
12253
+ exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
12254
+ if (root.cloudflareHandoff !== undefined)
12255
+ exactKeys2(root.cloudflareHandoff, ["checkpointFile", "nodeName"], "Cloudflare handoff");
12256
+ exactKeys2(root.database, ["role", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
12257
+ const runtime = exactKeys2(root.runtime, [
12258
+ "environment",
12259
+ "serviceUser",
12260
+ "slotsDirectory",
12261
+ "bluePort",
12262
+ "greenPort",
12263
+ "healthPath",
12264
+ "keepReleases",
12265
+ "credentialFiles"
12266
+ ], "runtime config");
12267
+ exactKeys2(runtime.environment, [
12268
+ "softwareProfile",
12269
+ "databaseRole",
12270
+ "databaseCoordinators",
12271
+ "databaseAddress",
12272
+ "databaseMaster",
12273
+ "databaseNetworkMode",
12274
+ "databaseReplicationFactor",
12275
+ "databaseWriteConcern",
12276
+ "nodeHostname",
12277
+ "nodeRegion",
12278
+ "databaseUser",
12279
+ "nodeRole",
12280
+ "appOrigin",
12281
+ "apiOrigin",
12282
+ "publicApiPort",
12283
+ "sharedDirectory",
12284
+ "seedSyncPeers",
12285
+ "seedSyncMembers",
12286
+ "seedSyncEpoch",
12287
+ "concurrencyLimit",
12288
+ "drainDeadlineMs",
12289
+ "otlpEndpoint",
12290
+ "otlpCollectorUnit",
12291
+ "agentOtlpEndpoint",
12292
+ "custodianEmail",
12293
+ "smtp",
12294
+ "repository",
12295
+ "branch",
12296
+ "deployProfile",
12297
+ "otlpFlushIntervalMs",
12298
+ "otlpTraceSampleRatio",
12299
+ "backup",
12300
+ "cloudflare"
12301
+ ], "runtime environment");
12302
+ if (runtime.credentialFiles !== undefined)
12303
+ exactKeys2(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
12304
+ const environment = runtime.environment;
12305
+ if (environment.smtp !== undefined)
12306
+ exactKeys2(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
12307
+ if (environment.backup !== undefined)
12308
+ exactKeys2(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
12309
+ if (environment.cloudflare !== undefined)
12310
+ exactKeys2(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService", "warp"], "Cloudflare runtime config");
12311
+ if (environment.cloudflare && typeof environment.cloudflare === "object" && environment.cloudflare.warp !== undefined) {
12312
+ exactKeys2(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
12313
+ }
12314
+ } else if (root.kind === "tenant") {
12315
+ if (root.bootstrapRunner !== undefined)
12316
+ exactKeys2(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
12317
+ for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
12318
+ if (root[key] !== undefined && key !== "profile")
12319
+ throw new Error(`tenant bootstrap cannot contain ${key}`);
12320
+ }
12321
+ } else
12322
+ throw new Error("bootstrap config kind must be platform or tenant");
12323
+ return value;
12324
+ }
12325
+ function readBootstrapConfig(path) {
12326
+ const metadata = lstatSync2(path);
12327
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
12328
+ throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
12329
+ }
12330
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync4(path, "utf8"))));
12331
+ }
12332
+ function localBootstrapHost() {
12333
+ const execute = async (argv, options = {}) => {
12334
+ const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
12335
+ if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
12336
+ child.stdin.write(options.stdin);
12337
+ child.stdin.end();
12338
+ }
12339
+ const [stdout, stderr, exitCode] = await Promise.all([
12340
+ new Response(child.stdout).text(),
12341
+ new Response(child.stderr).text(),
12342
+ child.exited
12343
+ ]);
12344
+ return { exitCode, output: `${stdout}${stderr}` };
12345
+ };
12346
+ return {
12347
+ uid: () => process.getuid?.() ?? -1,
12348
+ exists: existsSync4,
12349
+ read: (path) => readFileSync4(path, "utf8"),
12350
+ write(path, content, mode) {
12351
+ mkdirSync4(dirname4(path), { recursive: true, mode: 493 });
12352
+ const temporary = `${path}.next.${process.pid}`;
12353
+ writeFileSync4(temporary, content, { mode });
12354
+ chmodSync2(temporary, mode);
12355
+ renameSync3(temporary, path);
12356
+ },
12357
+ mkdir: (path, mode) => mkdirSync4(path, { recursive: true, mode }),
12358
+ remove: (path) => rmSync(path, { force: true }),
12359
+ inspect(path) {
12360
+ const value = lstatSync2(path);
12361
+ return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
12362
+ },
12363
+ exec: execute,
12364
+ async ensureSoftware(requirements) {
12365
+ const result = await execute([
12366
+ "runuser",
12367
+ "-u",
12368
+ "forgezero-agent",
12369
+ "--",
12370
+ "/usr/local/bin/fz-agent",
12371
+ "software-ensure",
12372
+ ...requirements.map(({ id: id2, version }) => `--require=${id2}@${version}`)
12373
+ ]);
12374
+ if (result.exitCode !== 0)
12375
+ throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
12376
+ return result;
12377
+ },
12378
+ async installAgent(config, enrolTokenSourcePath) {
12379
+ const capabilities = await readCapabilities(localRunner);
12380
+ const deployRoot = config.deployRoot ?? "/opt/forgezero";
12381
+ if (config.kind === "platform") {
12382
+ const lifecycle = config.database.role === "none" ? {
12383
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
12384
+ apiHealthUrl: "http://127.0.0.1:3000/api/health"
12385
+ } : {
12386
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
12387
+ databaseUnit: "forgezero-db.service",
12388
+ apiHealthUrl: "http://127.0.0.1:3000/api/health",
12389
+ databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
12390
+ databasePorts: [8529]
12391
+ };
12392
+ mkdirSync4(dirname4(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
12393
+ writeFileSync4(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
12394
+ `, { mode: 256 });
12395
+ }
12396
+ const plan = planInstall({
12397
+ capabilities,
12398
+ socketPath: DEFAULT_SOCKET,
12399
+ seedPath: "/var/lib/forgezero/node.seed",
12400
+ controlSocketPath: "/run/forgezero/control.sock",
12401
+ repository: config.repository,
12402
+ branch: config.branch,
12403
+ profile: config.kind === "platform" ? config.profile : config.profile,
12404
+ deployRoot,
12405
+ publicApiUrl: config.apiUrl,
12406
+ gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
12407
+ gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
12408
+ generateGitIdentity: true,
12409
+ pullDeployments: true,
12410
+ pullMigrations: config.kind === "platform",
12411
+ pullBootstrap: config.kind === "tenant" && Boolean(config.bootstrapRunner),
12412
+ bootstrapSshCredentialPath: config.kind === "tenant" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
12413
+ bootstrapTargetTelemetryEndpoint: config.kind === "tenant" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
12414
+ lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
12415
+ ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
12416
+ warpOrganization: config.runtime.environment.cloudflare.warp.organization,
12417
+ warpClientIdCredentialPath: WARP_CLIENT_ID_CREDENTIAL,
12418
+ warpClientSecretCredentialPath: WARP_CLIENT_SECRET_CREDENTIAL,
12419
+ cloudflareAccountId: config.runtime.environment.cloudflare.accountId,
12420
+ cloudflareTunnelId: config.runtime.environment.cloudflare.tunnelId,
12421
+ cloudflareVirtualNetworkId: config.runtime.environment.cloudflare.warp.virtualNetworkId,
12422
+ cloudflareWarpPolicyId: config.runtime.environment.cloudflare.warp.deviceProfileId
12423
+ } : {},
12424
+ enforceEgress: true,
12425
+ nodeHostname: config.nodeHostname,
12426
+ telemetryEndpoint: config.telemetryEndpoint,
12427
+ binPath: "/usr/local/lib/forgezero/agent/fz-agent",
12428
+ sourceBinPath: PACKAGED_AGENT_BIN,
12429
+ ...config.kind === "tenant" || enrolTokenSourcePath ? {
12430
+ enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
12431
+ enrolTokenCredentialPath: ENROL_CREDENTIAL,
12432
+ enrolStatePath: "/var/lib/forgezero/enrolment.json",
12433
+ apiUrl: config.apiUrl,
12434
+ project: config.kind === "tenant" ? config.realm : "platform",
12435
+ environment: config.kind === "tenant" ? undefined : config.environment,
12436
+ nodeLabel: config.kind === "tenant" ? config.nodeHostname : config.computeReference
12437
+ } : {}
12438
+ });
12439
+ for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
12440
+ mkdirSync4(dirname4(unit.path), { recursive: true, mode: 493 });
12441
+ writeFileSync4(unit.path, unit.unit, { mode: 420 });
12442
+ }
12443
+ await applyPlan(plan, localRunner);
12444
+ return plan;
12445
+ }
12446
+ };
12447
+ }
12448
+
12449
+ // src/cli/cloudflare-bootstrap.ts
12450
+ import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync5 } from "fs";
12451
+ import { dirname as dirname5, resolve as resolve3 } from "path";
12452
+ var exactKeys3 = (value, allowed, label) => {
12453
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
12454
+ if (unknown.length)
12455
+ throw new Error(`${label} contains unsupported field ${unknown[0]}`);
12456
+ };
12457
+ var record2 = (value, label) => {
12458
+ if (!value || typeof value !== "object" || Array.isArray(value))
12459
+ throw new Error(`${label} must be an object`);
12460
+ return value;
12461
+ };
12462
+ function readOwnerConfig(path) {
12463
+ const absolute = resolve3(path);
12464
+ let descriptor;
12465
+ try {
12466
+ descriptor = openSync(absolute, constants2.O_RDONLY | constants2.O_NOFOLLOW);
12467
+ const metadata = fstatSync(descriptor);
12468
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
12469
+ if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
12470
+ throw new Error(`${absolute} must be one operator-owned 0600 regular file`);
12471
+ }
12472
+ return JSON.parse(readFileSync5(descriptor, "utf8"));
12473
+ } catch (cause) {
12474
+ if (cause instanceof SyntaxError)
12475
+ throw new Error(`${absolute} is not valid Cloudflare bootstrap JSON`);
12476
+ if (cause instanceof Error && cause.message.startsWith(absolute))
12477
+ throw cause;
12478
+ throw new Error(`cannot securely read Cloudflare bootstrap config ${absolute}`);
12479
+ } finally {
12480
+ if (descriptor !== undefined)
12481
+ closeSync(descriptor);
12482
+ }
12483
+ }
12484
+ function readCloudflareBootstrapCommandConfig(path, mode) {
12485
+ const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
12486
+ const baseDirectory = dirname5(resolve3(path));
12487
+ exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates", "tokenFiles"], "Cloudflare bootstrap config");
12488
+ if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
12489
+ throw new Error("Cloudflare bootstrap config format/kind is invalid");
12490
+ }
12491
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
12492
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
12493
+ }
12494
+ const coordinateSource = record2(input.coordinates, "Cloudflare bootstrap coordinates");
12495
+ exactKeys3(coordinateSource, [
12496
+ "accountId",
12497
+ "zoneId",
12498
+ "hostname",
12499
+ "service",
12500
+ "tunnelName",
12501
+ "kvNamespaceTitle",
12502
+ "workerScriptName",
12503
+ "serviceTokenName",
12504
+ "policyName",
12505
+ "applicationName",
12506
+ "workerDirectory",
12507
+ "workerMain",
12508
+ "workerCompatibilityDate",
12509
+ "publicDomains",
12510
+ "createRuntimeTokens",
12511
+ "createPrivateNetworkRuntimeToken",
12512
+ "runtimeTokenNamePrefix",
12513
+ "nodes",
12514
+ "privateNetwork"
12515
+ ], "Cloudflare bootstrap coordinates");
12516
+ if (coordinateSource.nodes !== undefined) {
12517
+ if (!Array.isArray(coordinateSource.nodes))
12518
+ throw new Error("Cloudflare bootstrap coordinates.nodes must be an array");
12519
+ for (const node of coordinateSource.nodes) {
12520
+ exactKeys3(record2(node, "Cloudflare bootstrap node"), [
12521
+ "nodeName",
12522
+ "hostname",
12523
+ "service",
12524
+ "tunnelName",
12525
+ "applicationName",
12526
+ "privateAddress"
12527
+ ], "Cloudflare bootstrap node");
12528
+ }
12529
+ }
12530
+ if (coordinateSource.privateNetwork !== undefined) {
12531
+ exactKeys3(record2(coordinateSource.privateNetwork, "Cloudflare bootstrap privateNetwork"), [
12532
+ "warpOrganization",
12533
+ "virtualNetworkName",
12534
+ "deviceProfileName",
12535
+ "enrollmentApplicationName",
12536
+ "deviceProfilePrecedence"
12537
+ ], "Cloudflare bootstrap privateNetwork");
12538
+ }
12539
+ if (typeof coordinateSource.workerDirectory !== "string" || !coordinateSource.workerDirectory.trim()) {
12540
+ throw new Error("Cloudflare bootstrap coordinates.workerDirectory is required");
12541
+ }
12542
+ const coordinates = validateCloudflareBootstrapCoordinates({
12543
+ ...coordinateSource,
12544
+ workerDirectory: resolve3(baseDirectory, coordinateSource.workerDirectory)
12545
+ });
12546
+ let tokenFiles;
12547
+ if (input.tokenFiles !== undefined) {
12548
+ const source = record2(input.tokenFiles, "Cloudflare bootstrap tokenFiles");
12549
+ const keys = [
12550
+ "apiTokenFile",
12551
+ "tunnelApiTokenFile",
12552
+ "dnsApiTokenFile",
12553
+ "kvApiTokenFile",
12554
+ "accessApiTokenFile",
12555
+ "workerApiTokenFile"
12556
+ ];
12557
+ exactKeys3(source, keys, "Cloudflare bootstrap tokenFiles");
12558
+ for (const key of keys) {
12559
+ if (source[key] !== undefined && (typeof source[key] !== "string" || !source[key].trim())) {
12560
+ throw new Error(`Cloudflare bootstrap tokenFiles.${key} must be a non-empty file path`);
12561
+ }
12562
+ }
12563
+ tokenFiles = Object.fromEntries(keys.flatMap((key) => typeof source[key] === "string" ? [[key, resolve3(baseDirectory, source[key])]] : []));
12564
+ }
12565
+ if (mode === "apply" && (!tokenFiles || !Object.values(tokenFiles).some(Boolean))) {
12566
+ throw new Error("Cloudflare apply config requires tokenFiles with owner-only file paths");
12567
+ }
12568
+ return {
12569
+ mode,
12570
+ coordinates,
12571
+ checkpointPath: resolve3(baseDirectory, input.checkpointPath),
12572
+ ...tokenFiles ? { tokenFiles } : {}
12573
+ };
12574
+ }
12575
+ async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {}) {
12576
+ const request2 = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
12577
+ const evidence = await (dependencies.run ?? runAttendedCloudflareBootstrap)(request2);
12578
+ (dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
12579
+ `);
12580
+ return evidence;
12581
+ }
12582
+
12583
+ // src/metal-bootstrap.ts
12584
+ import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
12585
+ import {
12586
+ chmodSync as chmodSync3,
12587
+ chownSync,
12588
+ copyFileSync,
12589
+ existsSync as existsSync5,
12590
+ lstatSync as lstatSync3,
12591
+ mkdirSync as mkdirSync6,
12592
+ readFileSync as readFileSync6,
12593
+ realpathSync,
12594
+ renameSync as renameSync4,
12595
+ statSync,
12596
+ symlinkSync,
12597
+ unlinkSync as unlinkSync2,
12598
+ writeFileSync as writeFileSync6
12599
+ } from "fs";
12600
+ import { dirname as dirname7, isAbsolute as isAbsolute2, join as join8, resolve as resolve4 } from "path";
12601
+ import { isIP as isIP5 } from "net";
12602
+
12603
+ // src/metal-isolation.ts
12604
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
12605
+ import { join as join7 } from "path";
12606
+
12607
+ // src/metal-provision.ts
12608
+ import { dirname as dirname6, isAbsolute, join as join6 } from "path";
12609
+ import { isIP as isIP4 } from "net";
12610
+
12611
+ // src/ubuntu.ts
12612
+ var SUPPORTED_GUEST_IMAGE = Object.freeze({
12613
+ key: "ubuntu-resolute-20260731",
12614
+ family: "ubuntu-26.04",
12615
+ version: "2026-07-31",
12616
+ label: "Ubuntu 26.04 LTS Resolute",
12617
+ url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
12618
+ sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
12619
+ });
12620
+
12621
+ // src/metal-provision.ts
12622
+ var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
12623
+ var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
12624
+ var IPV4_PREFIX = /^(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
12625
+ var LINUX_LIST = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/;
12626
+
12627
+ class MetalProvisionError extends Error {
12628
+ }
12629
+ function membersOfLinuxList(value, label) {
12630
+ if (!LINUX_LIST.test(value))
12631
+ throw new MetalProvisionError(`invalid ${label} list`);
12632
+ const members = [];
12633
+ for (const part of value.split(",")) {
12634
+ const [startText, endText = startText] = part.split("-");
12635
+ const start = Number(startText);
12636
+ const end = Number(endText);
12637
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > 65535) {
12638
+ throw new MetalProvisionError(`invalid ${label} list`);
12639
+ }
12640
+ for (let value2 = start;value2 <= end; value2 += 1)
12641
+ members.push(value2);
12642
+ }
12643
+ if (new Set(members).size !== members.length)
12644
+ throw new MetalProvisionError(`${label} list overlaps itself`);
12645
+ return members;
12646
+ }
12647
+ function validateMetalProfile(profile) {
12648
+ if (!SAFE_NAME.test(profile.volumeGroup))
12649
+ throw new MetalProvisionError("invalid volume group");
12650
+ if (!DEVICE.test(profile.bridge))
12651
+ throw new MetalProvisionError("invalid bridge");
12652
+ if (!IPV4_PREFIX.test(profile.subnetPrefix))
12653
+ throw new MetalProvisionError("invalid subnet prefix");
12654
+ if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
12655
+ throw new MetalProvisionError("invalid guest address range");
12656
+ for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
12657
+ if (!isAbsolute(path))
12658
+ throw new MetalProvisionError("metal paths must be absolute");
12659
+ }
12660
+ new URL(profile.apiUrl);
12661
+ let telemetryEndpoint;
12662
+ try {
12663
+ telemetryEndpoint = new URL(profile.agentTelemetryEndpoint);
12664
+ } catch {
12665
+ throw new MetalProvisionError("Agent telemetry endpoint must be an absolute public HTTPS URL");
12666
+ }
12667
+ if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP4(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
12668
+ throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
12669
+ const imageKeys = Object.keys(profile.images);
12670
+ if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
12671
+ throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
12672
+ }
12673
+ if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
12674
+ throw new MetalProvisionError("at least one exclusive CPU pool is required");
12675
+ }
12676
+ const keys = new Set;
12677
+ const assigned = new Set;
12678
+ const assignedMemory = new Set;
12679
+ let poolsWithMemory = 0;
12680
+ for (const pool of profile.cpuPools) {
12681
+ if (!SAFE_NAME.test(pool.key) || keys.has(pool.key))
12682
+ throw new MetalProvisionError("invalid or duplicate CPU pool key");
12683
+ keys.add(pool.key);
12684
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
12685
+ if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
12686
+ throw new MetalProvisionError("invalid CPU pool physical-core count");
12687
+ }
12688
+ for (const cpu of cpus) {
12689
+ if (assigned.has(cpu))
12690
+ throw new MetalProvisionError("CPU pools overlap");
12691
+ assigned.add(cpu);
12692
+ }
12693
+ if (pool.memoryNodes) {
12694
+ poolsWithMemory += 1;
12695
+ for (const node of membersOfLinuxList(pool.memoryNodes, "memory-node")) {
12696
+ if (assignedMemory.has(node))
12697
+ throw new MetalProvisionError("guest memory-node pools overlap");
12698
+ assignedMemory.add(node);
12699
+ }
12700
+ }
12701
+ }
12702
+ if (poolsWithMemory !== 0 && poolsWithMemory !== profile.cpuPools.length) {
12703
+ throw new MetalProvisionError("every CPU pool must name memory nodes when NUMA isolation is enabled");
12704
+ }
12705
+ const housekeeping = membersOfLinuxList(profile.housekeepingCpus, "housekeeping CPU");
12706
+ if (housekeeping.some((cpu) => assigned.has(cpu))) {
12707
+ throw new MetalProvisionError("housekeeping CPUs overlap guest CPU pools");
12708
+ }
12709
+ if (profile.housekeepingMemoryNodes) {
12710
+ const housekeepingMemory = membersOfLinuxList(profile.housekeepingMemoryNodes, "housekeeping memory-node");
12711
+ if (housekeepingMemory.some((node) => assignedMemory.has(node))) {
12712
+ throw new MetalProvisionError("housekeeping memory nodes overlap guest memory-node pools");
12713
+ }
12714
+ } else if (assignedMemory.size > 0) {
12715
+ throw new MetalProvisionError("NUMA-isolated guest pools require housekeeping memory nodes");
12716
+ }
12717
+ }
12718
+
12719
+ // src/metal-isolation.ts
12720
+ var members = (list) => list.split(",").flatMap((part) => {
12721
+ const [first, last = first] = part.split("-").map(Number);
12722
+ return Array.from({ length: last - first + 1 }, (_, index) => first + index);
12723
+ });
12724
+ var compact = (values) => {
12725
+ const sorted = [...new Set(values)].sort((left, right) => left - right);
12726
+ const ranges = [];
12727
+ for (let index = 0;index < sorted.length; ) {
12728
+ const first = sorted[index];
12729
+ let last = first;
12730
+ while (sorted[index + 1] === last + 1)
12731
+ last = sorted[++index];
12732
+ ranges.push(first === last ? String(first) : `${first}-${last}`);
12733
+ index += 1;
12734
+ }
12735
+ return ranges.join(",");
12736
+ };
12737
+ var memoryDirective = (nodes) => nodes ? `AllowedMemoryNodes=${nodes}
12738
+ ` : "";
12739
+ function metalGuestSliceUnit(profile) {
12740
+ validateMetalProfile(profile);
12741
+ const cpus = compact(profile.cpuPools.flatMap((pool) => members(pool.cpus)));
12742
+ const nodes = compact(profile.cpuPools.flatMap((pool) => pool.memoryNodes ? members(pool.memoryNodes) : [])) || undefined;
12743
+ return `[Unit]
12744
+ Description=ForgeZero exclusive guest CPU and memory boundary
12745
+
12746
+ [Slice]
12747
+ AllowedCPUs=${cpus}
12748
+ ${memoryDirective(nodes)}`;
12749
+ }
12750
+ function metalHousekeepingDropIn(profile, kind) {
12751
+ validateMetalProfile(profile);
12752
+ return `[${kind === "slice" ? "Slice" : "Scope"}]
12753
+ AllowedCPUs=${profile.housekeepingCpus}
12754
+ ${memoryDirective(profile.housekeepingMemoryNodes)}`;
12755
+ }
12756
+ var defaultExec = async (argv) => {
12757
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
12758
+ const [exitCode, stdout, stderr] = await Promise.all([
12759
+ child.exited,
12760
+ new Response(child.stdout).text(),
12761
+ new Response(child.stderr).text()
12762
+ ]);
12763
+ return { exitCode, stdout, stderr };
12764
+ };
12765
+ var checked2 = async (exec, argv) => {
12766
+ const result = await exec(argv);
12767
+ if (result.exitCode !== 0)
12768
+ throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
12769
+ return result;
12770
+ };
12771
+ var requireGuestsInSlice = async (exec) => {
12772
+ const active = await checked2(exec, [
12773
+ "systemctl",
12774
+ "list-units",
12775
+ "--type=service",
12776
+ "--state=running",
12777
+ "--plain",
12778
+ "--no-legend",
12779
+ "forgezero-guest@*.service"
12780
+ ]);
12781
+ for (const line of active.stdout.split(`
12782
+ `)) {
12783
+ const service = line.trim().split(/\s+/)[0];
12784
+ if (!service)
12785
+ continue;
12786
+ const cgroup = await checked2(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
12787
+ if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
12788
+ throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
12789
+ }
12790
+ }
12791
+ };
12792
+ async function applyMetalIsolation(profile, exec = defaultExec) {
12793
+ validateMetalProfile(profile);
12794
+ await requireGuestsInSlice(exec);
12795
+ const unitDir = profile.unitDir;
12796
+ mkdirSync5(unitDir, { recursive: true });
12797
+ writeFileSync5(join7(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
12798
+ for (const unit of ["system.slice", "user.slice"]) {
12799
+ const directory = join7(unitDir, `${unit}.d`);
12800
+ mkdirSync5(directory, { recursive: true });
12801
+ writeFileSync5(join7(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
12802
+ }
12803
+ const initDirectory = join7(unitDir, "init.scope.d");
12804
+ mkdirSync5(initDirectory, { recursive: true });
12805
+ writeFileSync5(join7(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
12806
+ await checked2(exec, ["systemctl", "daemon-reload"]);
12807
+ await requireGuestsInSlice(exec);
12808
+ const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
12809
+ if (profile.housekeepingMemoryNodes)
12810
+ properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
12811
+ for (const unit of ["system.slice", "user.slice", "init.scope"]) {
12812
+ await checked2(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
12813
+ }
12814
+ }
12815
+
12816
+ // src/metal-bootstrap.ts
12817
+ var PROFILE_PATH = "/etc/forgezero/metal.json";
12818
+ var STATE_PATH2 = "/etc/forgezero/metal.initialized.json";
12819
+ var SEED_CREDENTIAL_PATH = "/etc/forgezero/creds/metal-agent-seed.cred";
12820
+ var UNIT_DIRECTORY = "/etc/systemd/system";
12821
+ var AGENT_PATH = "/usr/local/bin/fz-agent";
12822
+ var HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
12823
+ var UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
12824
+ var MAX_CONFIG_BYTES = 256 * 1024;
12825
+ var SUPPORTED_BUN_VERSION = "1.3.14";
12826
+ var SUPPORTED_BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
12827
+
12828
+ class MetalBootstrapError extends Error {
12829
+ }
12830
+ var defaultExec2 = async (argv, stdin) => {
12831
+ const child = Bun.spawn([...argv], {
12832
+ stdin: stdin === undefined ? undefined : new Blob([stdin]),
12833
+ stdout: "pipe",
12834
+ stderr: "pipe",
12835
+ env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
12836
+ });
12837
+ const [exitCode, stdout, stderr] = await Promise.all([
12838
+ child.exited,
12839
+ new Response(child.stdout).text(),
12840
+ new Response(child.stderr).text()
12841
+ ]);
12842
+ return { exitCode, stdout, stderr };
12843
+ };
12844
+ var runChecked = async (exec, argv, stdin) => {
12845
+ const result = await exec(argv, stdin);
12846
+ if (result.exitCode !== 0) {
12847
+ throw new MetalBootstrapError(`${argv.join(" ")} failed: ${(result.stderr || result.stdout).trim() || `exit ${result.exitCode}`}`);
12848
+ }
12849
+ return result;
12850
+ };
12851
+ var exactKeys4 = (value, allowed, label) => {
12852
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
12853
+ if (unknown.length)
12854
+ throw new MetalBootstrapError(`${label} contains unknown fields: ${unknown.join(", ")}`);
12855
+ };
12856
+ var validUnit = (unit) => {
12857
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit)) {
12858
+ throw new MetalBootstrapError("hostTelemetryUnit must be a systemd .service unit");
12859
+ }
12860
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-metal-helper|forgezero-db)\.service$/.test(unit)) {
12861
+ throw new MetalBootstrapError("the OTLP collector must be independently supervised");
12862
+ }
12863
+ };
12864
+ function validateMetalBootstrapConfig(config) {
12865
+ if (!config || typeof config !== "object")
12866
+ throw new MetalBootstrapError("metal bootstrap config must be an object");
12867
+ exactKeys4(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
12868
+ if (config.kind !== "metal")
12869
+ throw new MetalBootstrapError("metal bootstrap config kind must be metal");
12870
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(config.metalHostname)) {
12871
+ throw new MetalBootstrapError("invalid metal inventory hostname");
12872
+ }
12873
+ if (!config.profile || typeof config.profile !== "object")
12874
+ throw new MetalBootstrapError("metal profile must be an object");
12875
+ exactKeys4(config.profile, [
12876
+ "volumeGroup",
12877
+ "bridge",
12878
+ "subnetPrefix",
12879
+ "addressStart",
12880
+ "addressEnd",
12881
+ "gateway",
12882
+ "nameservers",
12883
+ "stateDir",
12884
+ "seedDir",
12885
+ "unitDir",
12886
+ "apiUrl",
12887
+ "agentTelemetryEndpoint",
12888
+ "images",
12889
+ "cpuPools",
12890
+ "housekeepingCpus",
12891
+ "housekeepingMemoryNodes",
12892
+ "bunVersion",
12893
+ "bunInstallerSha256",
12894
+ "agentVersion",
12895
+ "confidential"
12896
+ ], "metal profile");
12897
+ if (Array.isArray(config.profile.cpuPools)) {
12898
+ for (const pool of config.profile.cpuPools)
12899
+ exactKeys4(pool, ["key", "cpus", "physicalCores", "memoryNodes"], "metal CPU pool");
12900
+ }
12901
+ validateMetalProfile(config.profile);
12902
+ if (config.profile.stateDir !== "/etc/forgezero/metal-guests" || config.profile.seedDir !== "/var/lib/forgezero/seed" || config.profile.unitDir !== UNIT_DIRECTORY || config.profile.legacyStateDir !== undefined) {
12903
+ throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
12904
+ }
12905
+ const image = config.profile.images[Object.keys(config.profile.images)[0]];
12906
+ if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve4(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
12907
+ throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
12908
+ }
12909
+ if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunInstallerSha256 !== SUPPORTED_BUN_INSTALLER_SHA256 || config.profile.agentVersion !== VERSION2) {
12910
+ throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
12911
+ }
12912
+ let api;
12913
+ try {
12914
+ api = new URL(config.profile.apiUrl);
12915
+ } catch {
12916
+ throw new MetalBootstrapError("metal API must be a public HTTPS origin");
12917
+ }
12918
+ if (api.protocol !== "https:" || api.username || api.password || api.pathname !== "/" || api.search || api.hash || isIP5(api.hostname) !== 0 || !api.hostname.includes(".") || api.hostname.endsWith(".local")) {
12919
+ throw new MetalBootstrapError("metal API must be a credential-free public HTTPS origin");
12920
+ }
12921
+ if (config.profile.nameservers?.some((address) => isIP5(address) === 0)) {
12922
+ throw new MetalBootstrapError("metal nameservers must be literal IP addresses");
12923
+ }
12924
+ if (isIP5(`${config.profile.subnetPrefix}.1`) !== 4 || isIP5(config.profile.gateway) !== 4 || !config.profile.gateway.startsWith(`${config.profile.subnetPrefix}.`)) {
12925
+ throw new MetalBootstrapError("metal gateway must be an IPv4 address in the reviewed subnet");
12926
+ }
12927
+ if (config.profile.confidential && (!Number.isSafeInteger(config.profile.confidential.cbitpos) || config.profile.confidential.cbitpos < 1 || config.profile.confidential.cbitpos > 63 || !Number.isSafeInteger(config.profile.confidential.reducedPhysBits) || config.profile.confidential.reducedPhysBits < 0 || config.profile.confidential.reducedPhysBits > 63 || !/^0x[0-9a-fA-F]{1,16}$/.test(config.profile.confidential.policy)))
12928
+ throw new MetalBootstrapError("invalid confidential-compute coordinate");
12929
+ if (config.hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
12930
+ throw new MetalBootstrapError("metal host OTLP must use exact loopback http://127.0.0.1:4318");
12931
+ }
12932
+ validUnit(config.hostTelemetryUnit);
12933
+ if (config.agentSeedFile)
12934
+ validateOwnerOnlyPath(config.agentSeedFile, false);
12935
+ return config;
12936
+ }
12937
+ function validateOwnerOnlyPath(path, requireRootOwner) {
12938
+ if (!isAbsolute2(path) || resolve4(path) !== path || path.includes("/../")) {
12939
+ throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
12940
+ }
12941
+ const metadata = lstatSync3(path);
12942
+ if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync(path) !== path) {
12943
+ throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
12944
+ }
12945
+ if ((metadata.mode & 63) !== 0)
12946
+ throw new MetalBootstrapError("private bootstrap file must be owner-only");
12947
+ const caller = typeof process.getuid === "function" ? process.getuid() : -1;
12948
+ if (requireRootOwner && metadata.uid !== 0 || !requireRootOwner && metadata.uid !== 0 && metadata.uid !== caller) {
12949
+ throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
12950
+ }
12951
+ }
12952
+ function readMetalBootstrapConfig(path) {
12953
+ validateOwnerOnlyPath(path, false);
12954
+ const metadata = statSync(path);
12955
+ if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
12956
+ throw new MetalBootstrapError("metal bootstrap config size is invalid");
12957
+ let parsed;
12958
+ try {
12959
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
12960
+ } catch {
12961
+ throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
12962
+ }
12963
+ return validateMetalBootstrapConfig(parsed);
12964
+ }
12965
+ function planMetalBootstrap(config) {
12966
+ validateMetalBootstrapConfig(config);
12967
+ return {
12968
+ kind: "metal",
12969
+ metalHostname: config.metalHostname,
12970
+ profilePath: PROFILE_PATH,
12971
+ units: [
12972
+ "forgezero-metal-helper.service",
12973
+ "forgezero-agent-update-helper.service",
12974
+ "forgezero-metal-agent-egress.service",
12975
+ "forgezero-metal-agent.service"
12976
+ ],
12977
+ steps: [
12978
+ "install fixed KVM/LVM/cloud-init/nftables prerequisites",
12979
+ "verify bridge, volume group, pinned guest image, KVM and optional SEV device",
12980
+ "install the published Agent binary and root-owned immutable metal profile",
12981
+ "seal or generate the metal identity seed as a systemd credential",
12982
+ "apply reviewed CPU/NUMA host and guest isolation",
12983
+ "install helper, update, egress and identity-only Agent units",
12984
+ "enable units and prove services, local sockets, egress policy and OTLP collector"
12985
+ ],
12986
+ requiresRoot: true,
12987
+ consumes: config.agentSeedFile ? [config.agentSeedFile] : []
12988
+ };
12989
+ }
12990
+ var atomicWrite2 = (path, body, mode) => {
12991
+ mkdirSync6(dirname7(path), { recursive: true, mode: 493 });
12992
+ const temporary = `${path}.next-${process.pid}`;
12993
+ writeFileSync6(temporary, body, { mode, flag: "wx" });
12994
+ chmodSync3(temporary, mode);
12995
+ chownSync(temporary, 0, 0);
12996
+ renameSync4(temporary, path);
12997
+ };
12998
+ var validateAgentSourcePath = (source) => {
12999
+ if (!isAbsolute2(source) || !lstatSync3(source).isFile() || lstatSync3(source).isSymbolicLink()) {
13000
+ throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
13001
+ }
13002
+ };
13003
+ var ensureAccount = async (exec) => {
13004
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-metal"])).exitCode !== 0) {
13005
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-metal"]);
13006
+ }
13007
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-update"])).exitCode !== 0) {
13008
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-update"]);
13009
+ }
13010
+ if ((await exec(["/usr/bin/id", "forgezero-metal"])).exitCode !== 0) {
13011
+ await runChecked(exec, [
13012
+ "/usr/sbin/useradd",
13013
+ "--system",
13014
+ "--no-create-home",
13015
+ "--shell",
13016
+ "/usr/sbin/nologin",
13017
+ "--gid",
13018
+ "forgezero-metal",
13019
+ "forgezero-metal"
13020
+ ]);
13021
+ }
13022
+ await runChecked(exec, ["/usr/sbin/usermod", "-a", "-G", "forgezero-update", "forgezero-metal"]);
13023
+ };
13024
+ function renderMetalUnits(config) {
13025
+ validateMetalBootstrapConfig(config);
13026
+ const egress = systemdAgentEgressDirectives([4318]);
13027
+ const updateEgress = systemdAgentEgressDirectives();
13028
+ return {
13029
+ "forgezero-metal-helper.service": `[Unit]
13030
+ Description=ForgeZero constrained physical-host helper
13031
+ After=local-fs.target
13032
+
13033
+ [Service]
13034
+ Type=simple
13035
+ User=root
13036
+ Group=forgezero-metal
13037
+ UMask=0007
13038
+ RuntimeDirectory=forgezero-metal
13039
+ RuntimeDirectoryMode=0770
13040
+ ExecStart=${AGENT_PATH} metal-helper --profile=${PROFILE_PATH}
13041
+ Restart=on-failure
13042
+ RestartSec=5
13043
+ TimeoutStopSec=10min
13044
+ LimitCORE=0
13045
+ PrivateTmp=true
13046
+ ProtectHome=true
13047
+ RestrictAddressFamilies=AF_UNIX
13048
+
13049
+ [Install]
13050
+ WantedBy=multi-user.target
13051
+ `,
13052
+ "forgezero-agent-update-helper.service": `[Unit]
13053
+ Description=ForgeZero verified Agent update helper
13054
+ After=network-online.target
13055
+ Wants=network-online.target
13056
+
13057
+ [Service]
13058
+ Type=simple
13059
+ User=root
13060
+ Group=forgezero-update
13061
+ Environment=FZ_AGENT_UPDATE_SOCKET=${UPDATE_SOCKET}
13062
+ ExecStart=${AGENT_PATH} update-helper
13063
+ Restart=always
13064
+ RestartSec=2
13065
+ RuntimeDirectory=forgezero-update
13066
+ RuntimeDirectoryMode=0750
13067
+ UMask=0007
13068
+ LimitCORE=0
13069
+ NoNewPrivileges=true
13070
+ PrivateTmp=true
13071
+ ProtectSystem=strict
13072
+ ProtectHome=true
13073
+ ProtectKernelTunables=true
13074
+ ProtectKernelModules=true
13075
+ ProtectControlGroups=true
13076
+ RestrictSUIDSGID=true
13077
+ RestrictRealtime=true
13078
+ LockPersonality=true
13079
+ ReadWritePaths=/opt/forgezero/agent /var/lib/forgezero
13080
+ ${updateEgress}
13081
+
13082
+ [Install]
13083
+ WantedBy=multi-user.target
13084
+ `,
13085
+ "forgezero-metal-agent-egress.service": `[Unit]
13086
+ Description=ForgeZero physical Agent host egress policy
13087
+ After=systemd-resolved.service nftables.service
13088
+ Requires=systemd-resolved.service
13089
+ Before=forgezero-metal-agent.service
13090
+
13091
+ [Service]
13092
+ Type=notify
13093
+ NotifyAccess=all
13094
+ User=root
13095
+ Group=root
13096
+ ExecStart=${AGENT_PATH} egress-policy --user=forgezero-metal --loopback-user=forgezero-metal --loopback-tcp-port=4318 --public-tcp-port=443
13097
+ Restart=on-failure
13098
+ RestartSec=2
13099
+ LimitCORE=0
13100
+ NoNewPrivileges=true
13101
+ PrivateTmp=true
13102
+ ProtectSystem=strict
13103
+ ProtectHome=true
13104
+ ProtectKernelTunables=true
13105
+ ProtectKernelModules=true
13106
+ ProtectControlGroups=true
13107
+ RestrictSUIDSGID=true
13108
+ RestrictRealtime=true
13109
+ MemoryDenyWriteExecute=true
13110
+ LockPersonality=true
13111
+ CapabilityBoundingSet=CAP_NET_ADMIN
13112
+ RestrictAddressFamilies=AF_UNIX AF_NETLINK
13113
+
13114
+ [Install]
13115
+ WantedBy=multi-user.target
13116
+ `,
13117
+ "forgezero-metal-agent.service": `[Unit]
13118
+ Description=ForgeZero identity-only physical-host Agent
13119
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
13120
+ Wants=network-online.target ${config.hostTelemetryUnit}
13121
+ Requires=forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
13122
+ BindsTo=forgezero-metal-agent-egress.service
13123
+
13124
+ [Service]
13125
+ Type=simple
13126
+ User=forgezero-metal
13127
+ Group=forgezero-metal
13128
+ SupplementaryGroups=forgezero-update
13129
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
13130
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
13131
+ Environment=FZ_AGENT_ROLE=metal
13132
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
13133
+ Environment=FZ_API=${config.profile.apiUrl}
13134
+ Environment=FZ_METAL_HELPER_SOCKET=${HELPER_SOCKET}
13135
+ Environment=FZ_DRAIN_DEADLINE_MS=120000
13136
+ Environment=NODE_ENV=production
13137
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
13138
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent
13139
+ ExecStart=${AGENT_PATH}
13140
+ Restart=on-failure
13141
+ RestartSec=5
13142
+ TimeoutStopSec=130s
13143
+ LimitCORE=0
13144
+ NoNewPrivileges=true
13145
+ PrivateTmp=true
13146
+ ProtectSystem=strict
13147
+ ProtectHome=true
13148
+ ProtectKernelTunables=true
13149
+ ProtectKernelModules=true
13150
+ ProtectControlGroups=true
13151
+ RestrictSUIDSGID=true
13152
+ LockPersonality=true
13153
+ ${egress}
13154
+
13155
+ [Install]
13156
+ WantedBy=multi-user.target
13157
+ `
13158
+ };
13159
+ }
13160
+ var installAgentBinary = (source, version) => {
13161
+ validateAgentSourcePath(source);
13162
+ const release = `/opt/forgezero/agent/versions/${version}/dist`;
13163
+ mkdirSync6(release, { recursive: true, mode: 493 });
13164
+ copyFileSync(source, join8(release, "fz-agent.js"));
13165
+ chmodSync3(join8(release, "fz-agent.js"), 493);
13166
+ chownSync(join8(release, "fz-agent.js"), 0, 0);
13167
+ mkdirSync6("/opt/forgezero/agent", { recursive: true, mode: 493 });
13168
+ for (const [link, target] of [
13169
+ ["/opt/forgezero/agent/current.next", `versions/${version}`],
13170
+ [AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
13171
+ ]) {
13172
+ try {
13173
+ unlinkSync2(link);
13174
+ } catch {}
13175
+ symlinkSync(target, link);
13176
+ if (link.endsWith("current.next"))
13177
+ renameSync4(link, "/opt/forgezero/agent/current");
13178
+ }
13179
+ };
13180
+ var preflight = async (config, exec) => {
13181
+ await runChecked(exec, ["/usr/bin/systemctl", "is-active", "--quiet", config.hostTelemetryUnit]);
13182
+ await runChecked(exec, [
13183
+ "/usr/bin/curl",
13184
+ "--silent",
13185
+ "--show-error",
13186
+ "--fail",
13187
+ "--max-time",
13188
+ "5",
13189
+ "--request",
13190
+ "POST",
13191
+ "--header",
13192
+ "Content-Type: application/json",
13193
+ "--data-binary",
13194
+ "{}",
13195
+ `${config.hostTelemetryEndpoint}/v1/metrics`
13196
+ ]);
13197
+ await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
13198
+ await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
13199
+ if (!existsSync5("/dev/kvm"))
13200
+ throw new MetalBootstrapError("/dev/kvm is required");
13201
+ if (config.profile.confidential) {
13202
+ if (!existsSync5("/dev/sev"))
13203
+ throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
13204
+ await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
13205
+ }
13206
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", config.profile.images[Object.keys(config.profile.images)[0]].path])).stdout.split(/\s+/)[0];
13207
+ if (digest !== config.profile.images[Object.keys(config.profile.images)[0]].sha256)
13208
+ throw new MetalBootstrapError("pinned guest image digest mismatch");
13209
+ };
13210
+ var assertSupportedMetalHost = () => {
13211
+ if (process.platform !== "linux" || process.arch !== "x64") {
13212
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
13213
+ }
13214
+ const release = readFileSync6("/etc/os-release", "utf8");
13215
+ if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
13216
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
13217
+ }
13218
+ };
13219
+ var ensurePinnedGuestImage = async (config, exec) => {
13220
+ const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
13221
+ if (existsSync5(image.path))
13222
+ return;
13223
+ mkdirSync6(dirname7(image.path), { recursive: true, mode: 493 });
13224
+ const temporary = `${image.path}.next-${process.pid}`;
13225
+ try {
13226
+ await runChecked(exec, [
13227
+ "/usr/bin/curl",
13228
+ "--fail",
13229
+ "--location",
13230
+ "--proto",
13231
+ "=https",
13232
+ "--tlsv1.2",
13233
+ "--output",
13234
+ temporary,
13235
+ SUPPORTED_GUEST_IMAGE.url
13236
+ ]);
13237
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
13238
+ if (digest !== image.sha256)
13239
+ throw new MetalBootstrapError("downloaded guest image digest mismatch");
13240
+ chmodSync3(temporary, 292);
13241
+ chownSync(temporary, 0, 0);
13242
+ renameSync4(temporary, image.path);
13243
+ } catch (cause) {
13244
+ try {
13245
+ unlinkSync2(temporary);
13246
+ } catch {}
13247
+ throw cause;
13248
+ }
13249
+ };
13250
+ async function applyMetalBootstrap(config, options) {
13251
+ validateMetalBootstrapConfig(config);
13252
+ if ((options.getuid ?? process.getuid)?.() !== 0)
13253
+ throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
13254
+ assertSupportedMetalHost();
13255
+ if (existsSync5(STATE_PATH2) && !options.repair)
13256
+ throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
13257
+ if (config.agentSeedFile)
13258
+ validateOwnerOnlyPath(config.agentSeedFile, true);
13259
+ if (config.agentSeedFile && existsSync5(SEED_CREDENTIAL_PATH)) {
13260
+ throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
13261
+ }
13262
+ validateAgentSourcePath(options.agentSourcePath);
13263
+ const exec = options.exec ?? defaultExec2;
13264
+ await runChecked(exec, ["/usr/bin/apt-get", "update"]);
13265
+ await runChecked(exec, [
13266
+ "/usr/bin/apt-get",
13267
+ "install",
13268
+ "-y",
13269
+ "--no-install-recommends",
13270
+ "qemu-system-x86",
13271
+ "qemu-utils",
13272
+ "cloud-image-utils",
13273
+ "lvm2",
13274
+ "nftables",
13275
+ "curl"
13276
+ ]);
13277
+ await ensurePinnedGuestImage(config, exec);
13278
+ await preflight(config, exec);
13279
+ await ensureAccount(exec);
13280
+ installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
13281
+ mkdirSync6("/etc/forgezero/creds", { recursive: true, mode: 448 });
13282
+ mkdirSync6(config.profile.stateDir, { recursive: true, mode: 448 });
13283
+ mkdirSync6(config.profile.seedDir, { recursive: true, mode: 448 });
13284
+ const persistedProfile = {
13285
+ ...config.profile,
13286
+ metalHostname: config.metalHostname,
13287
+ hostTelemetryEndpoint: config.hostTelemetryEndpoint,
13288
+ hostTelemetryUnit: config.hostTelemetryUnit
13289
+ };
13290
+ atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
13291
+ `, 384);
13292
+ if (!existsSync5(SEED_CREDENTIAL_PATH)) {
13293
+ const seed = config.agentSeedFile ? readFileSync6(config.agentSeedFile, "utf8").trim() : randomBytes6(32).toString("base64url");
13294
+ if (seed.length < 32 || /[\0\r\n]/.test(seed))
13295
+ throw new MetalBootstrapError("metal Agent seed is invalid");
13296
+ await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
13297
+ `);
13298
+ chmodSync3(SEED_CREDENTIAL_PATH, 256);
13299
+ chownSync(SEED_CREDENTIAL_PATH, 0, 0);
13300
+ if (config.agentSeedFile)
13301
+ unlinkSync2(config.agentSeedFile);
13302
+ }
13303
+ await applyMetalIsolation(config.profile, (argv) => exec(argv));
13304
+ for (const [unit, body] of Object.entries(renderMetalUnits(config)))
13305
+ atomicWrite2(join8(UNIT_DIRECTORY, unit), body, 420);
13306
+ await runChecked(exec, ["/usr/bin/systemctl", "daemon-reload"]);
13307
+ await runChecked(exec, [
13308
+ "/usr/bin/systemctl",
13309
+ "enable",
13310
+ "--now",
13311
+ "forgezero-agent-update-helper.service",
13312
+ "forgezero-metal-helper.service",
13313
+ "forgezero-metal-agent-egress.service",
13314
+ "forgezero-metal-agent.service"
13315
+ ]);
13316
+ for (let attempt = 0;attempt < 100 && (!socketReady(HELPER_SOCKET) || !socketReady(UPDATE_SOCKET)); attempt += 1) {
13317
+ await Bun.sleep(100);
13318
+ }
13319
+ const status = await metalBootstrapStatus(exec);
13320
+ const operationalProblems = options.repair ? status.problems.filter((problem) => problem !== "metal initialized state does not bind the current profile") : status.problems;
13321
+ if (operationalProblems.length)
13322
+ throw new MetalBootstrapError(`metal bootstrap verification failed: ${operationalProblems.join("; ")}`);
13323
+ const identity = await runChecked(exec, [
13324
+ "/usr/bin/systemd-run",
13325
+ "--pipe",
13326
+ "--wait",
13327
+ "--quiet",
13328
+ "--collect",
13329
+ `--property=LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}`,
13330
+ "--setenv=FZ_SEED_CREDENTIAL=metal-agent-seed",
13331
+ AGENT_PATH,
13332
+ "identity"
13333
+ ]);
13334
+ const publicIdentity = identity.stdout.trim();
13335
+ if (!publicIdentity || /[\r\n]/.test(publicIdentity))
13336
+ throw new MetalBootstrapError("metal public identity proof was invalid");
13337
+ const state = {
13338
+ initializedAt: new Date().toISOString(),
13339
+ role: "metal",
13340
+ metalHostname: config.metalHostname,
13341
+ profileSha256: createHash3("sha256").update(JSON.stringify(config.profile)).digest("hex")
13342
+ };
13343
+ atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
13344
+ `, 384);
13345
+ return { ...status, initialized: true, problems: [], metalHostname: config.metalHostname, publicIdentity };
13346
+ }
13347
+ var socketReady = (path) => {
13348
+ try {
13349
+ return lstatSync3(path).isSocket();
13350
+ } catch {
13351
+ return false;
13352
+ }
13353
+ };
13354
+ async function metalBootstrapStatus(exec = defaultExec2) {
13355
+ const problems = [];
13356
+ let profileValid = false, imageVerified = false, metalHostname, profileSha256;
13357
+ let profileMode = null;
13358
+ if (existsSync5(PROFILE_PATH)) {
13359
+ try {
13360
+ const metadata = statSync(PROFILE_PATH);
13361
+ profileMode = metadata.mode & 511;
13362
+ if (profileMode !== 384 || metadata.uid !== 0)
13363
+ problems.push("metal profile is not root-owned mode 0600");
13364
+ const persisted = JSON.parse(readFileSync6(PROFILE_PATH, "utf8"));
13365
+ const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
13366
+ validateMetalProfile(profile);
13367
+ profileValid = true;
13368
+ profileSha256 = createHash3("sha256").update(JSON.stringify(profile)).digest("hex");
13369
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
13370
+ problems.push("persisted metal host coordinates are invalid");
13371
+ }
13372
+ try {
13373
+ validUnit(hostTelemetryUnit);
13374
+ } catch {
13375
+ problems.push("persisted metal OTLP collector unit is invalid");
13376
+ }
13377
+ if ((await exec(["/usr/bin/systemctl", "is-active", "--quiet", hostTelemetryUnit])).exitCode !== 0) {
13378
+ problems.push(`${hostTelemetryUnit} is inactive`);
13379
+ } else if ((await exec([
13380
+ "/usr/bin/curl",
13381
+ "--silent",
13382
+ "--show-error",
13383
+ "--fail",
13384
+ "--max-time",
13385
+ "5",
13386
+ "--request",
13387
+ "POST",
13388
+ "--header",
13389
+ "Content-Type: application/json",
13390
+ "--data-binary",
13391
+ "{}",
13392
+ `${hostTelemetryEndpoint}/v1/metrics`
13393
+ ])).exitCode !== 0) {
13394
+ problems.push("local OTLP metrics receiver did not accept a proof request");
13395
+ }
13396
+ const image = profile.images[Object.keys(profile.images)[0]];
13397
+ if (existsSync5(image.path)) {
13398
+ const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
13399
+ imageVerified = digest === image.sha256;
13400
+ }
13401
+ if (!imageVerified)
13402
+ problems.push("pinned guest image is absent or has the wrong digest");
13403
+ } catch (cause) {
13404
+ problems.push(`metal profile invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
13405
+ }
13406
+ } else
13407
+ problems.push("metal profile is missing");
13408
+ if (existsSync5(STATE_PATH2)) {
13409
+ try {
13410
+ const state = JSON.parse(readFileSync6(STATE_PATH2, "utf8"));
13411
+ metalHostname = state.metalHostname;
13412
+ if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
13413
+ problems.push("metal initialized state does not bind the current profile");
13414
+ }
13415
+ } catch {
13416
+ problems.push("metal initialized state is invalid");
13417
+ }
13418
+ }
13419
+ const units = {};
13420
+ for (const unit of [
13421
+ "forgezero-metal-helper.service",
13422
+ "forgezero-agent-update-helper.service",
13423
+ "forgezero-metal-agent-egress.service",
13424
+ "forgezero-metal-agent.service"
13425
+ ]) {
13426
+ if (!existsSync5(join8(UNIT_DIRECTORY, unit)))
13427
+ units[unit] = "missing";
13428
+ else
13429
+ units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
13430
+ if (units[unit] !== "active")
13431
+ problems.push(`${unit} is ${units[unit]}`);
13432
+ }
13433
+ const helperSocketReady = socketReady(HELPER_SOCKET);
13434
+ const updateSocketReady = socketReady(UPDATE_SOCKET);
13435
+ if (!helperSocketReady)
13436
+ problems.push("metal helper socket is not ready");
13437
+ if (!updateSocketReady)
13438
+ problems.push("Agent update helper socket is not ready");
13439
+ return {
13440
+ initialized: existsSync5(STATE_PATH2),
13441
+ profileValid,
13442
+ profileMode,
13443
+ imageVerified,
13444
+ units,
13445
+ helperSocketReady,
13446
+ updateSocketReady,
13447
+ metalHostname,
13448
+ problems
13449
+ };
13450
+ }
13451
+
13452
+ // src/cli/index.ts
13453
+ var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
13454
+ var DEFAULT_MODE = RECOMMENDED_MODE;
13455
+ var PACKAGED_AGENT_BIN2 = fileURLToPath2(new URL("./fz-agent.js", import.meta.url));
13456
+ function parseOptions(argv) {
13457
+ const options = {
13458
+ api: process.env.FZ_API ?? "http://localhost:8787",
13459
+ apiExplicit: Boolean(process.env.FZ_API),
13460
+ app: process.env.FZ_APP,
13461
+ realm: process.env.FZ_REALM ?? "platform",
13462
+ realmExplicit: Boolean(process.env.FZ_REALM),
13463
+ json: false,
13464
+ apply: false,
13465
+ enrol: false,
13466
+ socket: process.env.SSH_AUTH_SOCK,
13467
+ mode: DEFAULT_MODE,
13468
+ user: process.env.FZ_USER ?? "operator",
13469
+ userExplicit: Boolean(process.env.FZ_USER),
13470
+ phraseStdin: false,
13471
+ preserveEnv: false,
13472
+ projectRoot: process.cwd(),
13473
+ deployProfile: "app",
13474
+ deploySoftware: [],
13475
+ deployChannel: "production",
13476
+ requireAttestation: false,
13477
+ force: false,
13478
+ noBrowser: false,
13479
+ provider: "github",
13480
+ branch: "main",
13481
+ authMode: "public",
13482
+ targets: [],
13483
+ wait: false,
13484
+ timeoutSeconds: 900,
13485
+ queries: []
13486
+ };
13487
+ const positional = [];
13488
+ for (let index = 0;index < argv.length; index += 1) {
13489
+ const token = argv[index];
13490
+ if (token === "--api") {
13491
+ options.api = argv[++index] ?? options.api;
13492
+ options.apiExplicit = true;
13493
+ } else if (token === "--app")
13494
+ options.app = argv[++index];
13495
+ else if (token === "--realm") {
13496
+ options.realm = argv[++index] ?? options.realm;
13497
+ options.realmExplicit = true;
13498
+ } else if (token === "--socket")
13499
+ options.socket = argv[++index];
13500
+ else if (token === "--json")
13501
+ options.json = true;
13502
+ else if (token === "--apply")
13503
+ options.apply = true;
13504
+ else if (token === "--enrol")
13505
+ options.enrol = true;
13506
+ else if (token === "--preserve-env")
13507
+ options.preserveEnv = true;
13508
+ else if (token === "--root")
13509
+ options.projectRoot = argv[++index] ?? options.projectRoot;
13510
+ else if (token === "--name")
13511
+ options.projectName = argv[++index];
13512
+ else if (token === "--purpose")
13513
+ options.projectPurpose = argv[++index];
13514
+ else if (token === "--profile")
13515
+ options.deployProfile = argv[++index] ?? options.deployProfile;
13516
+ else if (token === "--software")
13517
+ options.deploySoftware.push(argv[++index] ?? "");
13518
+ else if (token === "--channel") {
13519
+ const channel = argv[++index];
13520
+ if (channel === "development" || channel === "production")
13521
+ options.deployChannel = channel;
13522
+ else
13523
+ options.optionError = "--channel must be production or development.";
13524
+ } else if (token === "--attestation")
13525
+ options.requireAttestation = true;
13526
+ else if (token === "--force")
13527
+ options.force = true;
13528
+ else if (token === "--no-browser")
13529
+ options.noBrowser = true;
13530
+ else if (token === "--project")
13531
+ options.projectKey = argv[++index];
13532
+ else if (token === "--pipeline")
13533
+ options.pipelineKey = argv[++index];
13534
+ else if (token === "--revision")
13535
+ options.revision = argv[++index];
13536
+ else if (token === "--label")
13537
+ options.label = argv[++index];
13538
+ else if (token === "--provider") {
13539
+ const value = argv[++index];
13540
+ if (value === "github" || value === "gitlab" || value === "generic")
13541
+ options.provider = value;
13542
+ else
13543
+ options.optionError = "--provider must be github, gitlab, or generic.";
13544
+ } else if (token === "--repository")
13545
+ options.repository = argv[++index];
13546
+ else if (token === "--branch")
13547
+ options.branch = argv[++index] ?? options.branch;
13548
+ else if (token === "--clone-url")
13549
+ options.cloneUrl = argv[++index];
13550
+ else if (token === "--auth") {
13551
+ const value = argv[++index];
13552
+ if (value === "public" || value === "node-ssh" || value === "vault-token")
13553
+ options.authMode = value;
13554
+ else
13555
+ options.optionError = "--auth must be public, node-ssh, or vault-token.";
13556
+ } else if (token === "--git-secret")
13557
+ options.gitSecret = argv[++index];
13558
+ else if (token === "--git-username")
13559
+ options.gitUsername = argv[++index];
13560
+ else if (token === "--known-hosts")
13561
+ options.knownHostsFile = argv[++index];
13562
+ else if (token === "--target")
13563
+ options.targets.push(argv[++index] ?? "");
13564
+ else if (token === "--target-key")
13565
+ options.targetKey = argv[++index];
13566
+ else if (token === "--wait")
13567
+ options.wait = true;
13568
+ else if (token === "--timeout") {
13569
+ const value = Number(argv[++index] ?? "");
13570
+ if (Number.isFinite(value) && value > 0 && value <= 86400)
13571
+ options.timeoutSeconds = value;
13572
+ else
13573
+ options.optionError = "--timeout must be between 1 and 86400 seconds.";
13574
+ } else if (token === "--data")
13575
+ options.data = argv[++index];
13576
+ else if (token === "--data-file")
13577
+ options.dataFile = argv[++index];
13578
+ else if (token === "--bootstrap-config")
13579
+ options.bootstrapConfigPath = argv[++index];
13580
+ else if (token === "--query")
13581
+ options.queries.push(argv[++index] ?? "");
13582
+ else if (token === "--key")
13583
+ options.key = argv[++index];
13584
+ else if (token === "--mode")
13585
+ options.mode = argv[++index] ?? options.mode;
13586
+ else if (token === "--user") {
13587
+ options.user = argv[++index] ?? options.user;
13588
+ options.userExplicit = true;
13589
+ } else if (token === "--token-file")
13590
+ options.tokenFile = argv[++index];
13591
+ else if (token === "--phrase-file")
13592
+ options.phraseFile = argv[++index];
13593
+ else if (token === "--phrase-stdin")
13594
+ options.phraseStdin = true;
13595
+ else
13596
+ positional.push(token);
13597
+ }
13598
+ return { command: positional[0] ?? "help", args: positional.slice(1), options };
13599
+ }
13600
+ var out = {
13601
+ line: (text3 = "") => process.stdout.write(`${text3}
13602
+ `),
13603
+ step: (text3) => process.stdout.write(` ${text3}
13604
+ `),
13605
+ warn: (text3) => process.stderr.write(` ! ${text3}
13606
+ `),
13607
+ fail: (text3) => process.stderr.write(` \u2717 ${text3}
13608
+ `),
13609
+ ok: (text3) => process.stdout.write(` \u2713 ${text3}
13610
+ `)
13611
+ };
13612
+ var sessionCookie = null;
13613
+ var storedSession = null;
13614
+ function captureSession(response) {
13615
+ const setCookie = response.headers.get("set-cookie");
13616
+ if (!setCookie)
13617
+ return;
13618
+ const cookie = setCookie.split(";")[0];
13619
+ sessionCookie = cookie;
13620
+ if (storedSession && (cookie !== storedSession.cookie || Date.now() - storedSession.lastUsedAtTs >= 60000)) {
13621
+ storedSession = { ...storedSession, cookie, lastUsedAtTs: Date.now() };
13622
+ saveSession(storedSession);
13623
+ }
13624
+ }
13625
+ function requestHeaders(apiBase, cookie) {
13626
+ return {
13627
+ "content-type": "application/json",
13628
+ origin: new URL(apiBase).origin,
13629
+ ...cookie ? { cookie } : {}
13630
+ };
13631
+ }
13632
+ async function api(options, path, init) {
13633
+ const base = options.realm === "platform" ? "/api" : `/api/t/${encodeURIComponent(options.realm)}`;
13634
+ const response = await fetch(`${options.api}${base}${path}`, {
13635
+ method: init?.method ?? "GET",
13636
+ headers: { ...requestHeaders(options.api, sessionCookie), "user-agent": `forgezero-cli/${VERSION2}` },
13637
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13638
+ });
13639
+ captureSession(response);
13640
+ let body = null;
13641
+ try {
13642
+ body = await response.json();
13643
+ } catch {
13644
+ body = null;
13645
+ }
13646
+ const challenge = body?.security;
13647
+ if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && custodyIdentity && !path.startsWith("/security/step-up/")) {
13648
+ const proved = await proveWithAgent(options, challenge.requestKey);
13649
+ if (proved) {
13650
+ const replay = await fetch(`${options.api}${base}${path}`, {
13651
+ method: init?.method ?? "GET",
13652
+ headers: {
13653
+ ...requestHeaders(options.api, sessionCookie),
13654
+ "x-security-request-key": challenge.requestKey
13655
+ },
13656
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13657
+ });
13658
+ captureSession(replay);
13659
+ let replayed = null;
13660
+ try {
13661
+ replayed = await replay.json();
13662
+ } catch {
13663
+ replayed = null;
13664
+ }
13665
+ return { status: replay.status, body: replayed };
13666
+ }
13667
+ }
13668
+ if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && storedSession && !path.startsWith("/security/step-up/")) {
13669
+ const proved = await proveWithBrowser(options, challenge.requestKey, base);
13670
+ if (proved) {
13671
+ const replay = await fetch(`${options.api}${base}${path}`, {
13672
+ method: init?.method ?? "GET",
13673
+ headers: {
13674
+ ...requestHeaders(options.api, sessionCookie),
13675
+ "user-agent": `forgezero-cli/${VERSION2}`,
13676
+ "x-security-request-key": challenge.requestKey
13677
+ },
13678
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13679
+ });
13680
+ captureSession(replay);
13681
+ let replayed = null;
13682
+ try {
13683
+ replayed = await replay.json();
13684
+ } catch {}
13685
+ return { status: replay.status, body: replayed };
13686
+ }
13687
+ }
13688
+ if (response.status === 401 && storedSession) {
13689
+ removeSession(storedSession.api, storedSession.realm);
13690
+ storedSession = null;
13691
+ sessionCookie = null;
13692
+ }
13693
+ return { status: response.status, body };
13694
+ }
13695
+ function sleep(ms) {
13696
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
10336
13697
  }
10337
13698
  function browserCommand(url) {
10338
13699
  if (process.platform === "darwin")
@@ -10438,7 +13799,7 @@ function requireSession() {
10438
13799
  function readOwnerOnlySecret(path, label) {
10439
13800
  let metadata;
10440
13801
  try {
10441
- metadata = lstatSync2(path);
13802
+ metadata = lstatSync4(path);
10442
13803
  } catch {
10443
13804
  throw new Error(`The ${label} file ${path} is unreadable.`);
10444
13805
  }
@@ -10452,7 +13813,7 @@ function readOwnerOnlySecret(path, label) {
10452
13813
  if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
10453
13814
  throw new Error(`The ${label} file ${path} is not owned by the current user.`);
10454
13815
  }
10455
- const value = readFileSync4(path, "utf8").trim();
13816
+ const value = readFileSync7(path, "utf8").trim();
10456
13817
  if (!value)
10457
13818
  throw new Error(`The ${label} file ${path} is empty.`);
10458
13819
  return value;
@@ -10625,7 +13986,7 @@ async function cmdApi(options, args) {
10625
13986
  let body = undefined;
10626
13987
  if (options.data !== undefined && options.dataFile)
10627
13988
  throw new Error("Use only one of --data or --data-file.");
10628
- const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync4(options.dataFile, "utf8") : options.data;
13989
+ const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync7(options.dataFile, "utf8") : options.data;
10629
13990
  if (encoded !== undefined)
10630
13991
  body = JSON.parse(encoded);
10631
13992
  const result = await api(options, `${url.pathname}${url.search}`, { method, body });
@@ -10750,7 +14111,7 @@ async function cmdAgent(options, args) {
10750
14111
  deployRoot: process.env.FZ_DEPLOY_ROOT ?? "/opt/forgezero"
10751
14112
  } : {},
10752
14113
  binPath: process.env.FZ_AGENT_BIN ?? "/usr/local/lib/forgezero/agent/fz-agent",
10753
- sourceBinPath: process.env.FZ_AGENT_SOURCE_BIN ?? PACKAGED_AGENT_BIN,
14114
+ sourceBinPath: process.env.FZ_AGENT_SOURCE_BIN ?? PACKAGED_AGENT_BIN2,
10754
14115
  user: process.env.FZ_AGENT_USER,
10755
14116
  apiUrl: options.api,
10756
14117
  project: process.env.FZ_PROJECT,
@@ -10774,17 +14135,17 @@ async function cmdAgent(options, args) {
10774
14135
  return 0;
10775
14136
  }
10776
14137
  try {
10777
- writeFileSync4(plan.unitPath, plan.unit, { mode: 420 });
14138
+ writeFileSync7(plan.unitPath, plan.unit, { mode: 420 });
10778
14139
  out.ok(`Wrote ${plan.unitPath}`);
10779
14140
  for (const auxiliary of plan.auxiliaryUnits) {
10780
- mkdirSync4(dirname3(auxiliary.path), { recursive: true, mode: 493 });
10781
- writeFileSync4(auxiliary.path, auxiliary.unit, { mode: 420 });
14141
+ mkdirSync7(dirname8(auxiliary.path), { recursive: true, mode: 493 });
14142
+ writeFileSync7(auxiliary.path, auxiliary.unit, { mode: 420 });
10782
14143
  out.ok(`Wrote ${auxiliary.path}`);
10783
14144
  }
10784
14145
  if (options.enrol) {
10785
- if (existsSync4(enrolTokenSourcePath)) {
10786
- const source = statSync(enrolTokenSourcePath);
10787
- const token = readFileSync4(enrolTokenSourcePath, "utf8").trim();
14146
+ if (existsSync6(enrolTokenSourcePath)) {
14147
+ const source = statSync2(enrolTokenSourcePath);
14148
+ const token = readFileSync7(enrolTokenSourcePath, "utf8").trim();
10788
14149
  if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
10789
14150
  throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
10790
14151
  }
@@ -10797,7 +14158,7 @@ async function cmdAgent(options, args) {
10797
14158
  if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
10798
14159
  throw new Error("A valid fze_ enrolment token was not provided.");
10799
14160
  }
10800
- writeFileSync4(enrolTokenSourcePath, `${token}
14161
+ writeFileSync7(enrolTokenSourcePath, `${token}
10801
14162
  `, { mode: 384, flag: "wx" });
10802
14163
  }
10803
14164
  }
@@ -10808,13 +14169,13 @@ async function cmdAgent(options, args) {
10808
14169
  out.line();
10809
14170
  out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
10810
14171
  out.line();
10811
- out.line(` ${readFileSync4(gitPublicKeyPath, "utf8").trim()}`);
14172
+ out.line(` ${readFileSync7(gitPublicKeyPath, "utf8").trim()}`);
10812
14173
  out.line();
10813
14174
  return 0;
10814
14175
  } catch (cause) {
10815
14176
  if (options.enrol) {
10816
14177
  try {
10817
- unlinkSync2(enrolTokenSourcePath);
14178
+ unlinkSync3(enrolTokenSourcePath);
10818
14179
  } catch {}
10819
14180
  }
10820
14181
  out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
@@ -10822,6 +14183,203 @@ async function cmdAgent(options, args) {
10822
14183
  return 1;
10823
14184
  }
10824
14185
  }
14186
+ var bootstrapAnswer = (question, fallback) => {
14187
+ const suffix = fallback ? ` [${fallback}]` : "";
14188
+ const value = globalThis.prompt(`${question}${suffix}:`)?.trim();
14189
+ if (value)
14190
+ return value;
14191
+ if (fallback !== undefined)
14192
+ return fallback;
14193
+ throw new Error(`${question} is required (or use --bootstrap-config).`);
14194
+ };
14195
+ function interactiveBootstrap(kind) {
14196
+ if (!process.stdin.isTTY)
14197
+ throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
14198
+ if (kind === "tenant") {
14199
+ const runnerKey = bootstrapAnswer("Bootstrap runner SSH private-key file (blank to disable)", "");
14200
+ return {
14201
+ kind,
14202
+ apiUrl: bootstrapAnswer("Platform API URL"),
14203
+ realm: bootstrapAnswer("Tenant realm slug"),
14204
+ nodeHostname: bootstrapAnswer("Node hostname", hostname()),
14205
+ telemetryEndpoint: bootstrapAnswer("Public HTTPS OTLP endpoint"),
14206
+ enrolTokenFile: bootstrapAnswer("Root-only enrolment token file"),
14207
+ profile: bootstrapAnswer("Deploy profile", "app"),
14208
+ ...runnerKey ? { bootstrapRunner: {
14209
+ sshPrivateKeyFile: runnerKey,
14210
+ targetTelemetryEndpoint: bootstrapAnswer("Public HTTPS OTLP endpoint for bootstrapped targets")
14211
+ } } : {}
14212
+ };
14213
+ }
14214
+ const environment = bootstrapAnswer("Environment (production/development)", "production");
14215
+ const profile = bootstrapAnswer("Profile (platform-db-api/platform-api)", "platform-db-api");
14216
+ const role = profile === "platform-api" ? "none" : bootstrapAnswer("Database role (master/joiner)", "master");
14217
+ const serverMode = "default";
14218
+ const address = role === "none" ? undefined : bootstrapAnswer("Private database address");
14219
+ const master = role === "joiner" ? bootstrapAnswer("Master starter private address") : undefined;
14220
+ const coordinators = bootstrapAnswer("Writable Coordinator origins (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
14221
+ const computeReference = bootstrapAnswer("API-owned compute reference", environment === "production" ? "fz-n1" : "dev-fz-n1");
14222
+ const nodeHostname = bootstrapAnswer("Public node hostname");
14223
+ const apiUrl = bootstrapAnswer("Platform API URL");
14224
+ const appOrigin = bootstrapAnswer("Public App origin");
14225
+ const repository = bootstrapAnswer("API Git repository");
14226
+ const branch = bootstrapAnswer("Deployment branch", environment === "production" ? "main" : "dev");
14227
+ const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
14228
+ const collectorUnit = bootstrapAnswer("Independent local OTLP collector unit", "otelcol.service");
14229
+ const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
14230
+ const writeConcern = Number(bootstrapAnswer("Database write concern", "2"));
14231
+ const seedPeers = bootstrapAnswer("Seed WebSocket peers (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
14232
+ const smtpHost = bootstrapAnswer("Bootstrap SMTP host (blank to disable)", "");
14233
+ const smtpPort = smtpHost ? Number(bootstrapAnswer("Bootstrap SMTP port", "587")) : undefined;
14234
+ const smtpUser = smtpHost ? bootstrapAnswer("Bootstrap SMTP user (blank if none)", "") : undefined;
14235
+ const smtpFrom = smtpHost ? bootstrapAnswer("Bootstrap SMTP From address") : undefined;
14236
+ const smtpPassword = smtpHost ? bootstrapAnswer("Root-only SMTP password file") : undefined;
14237
+ const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
14238
+ const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
14239
+ const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
14240
+ const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
14241
+ const backupS3Secret = backupEndpoint ? bootstrapAnswer("Root-only backup S3 secret file") : undefined;
14242
+ const cloudflareCheckpoint = bootstrapAnswer("Completed Cloudflare bootstrap checkpoint (blank for no public edge)", "");
14243
+ const cloudflareNodeName = cloudflareCheckpoint ? bootstrapAnswer("Cloudflare checkpoint node name", computeReference) : undefined;
14244
+ const databaseNetworkMode = cloudflareCheckpoint ? bootstrapAnswer("Database network (private-lan/cloudflare-warp)", "private-lan") : "private-lan";
14245
+ const bootstrapSecretFile = bootstrapAnswer("Root-only shared cluster bootstrap-code file");
14246
+ const index = Number(computeReference.match(/n(\d+)$/)?.[1] ?? 0);
14247
+ const platformEnrolTokenFile = index > 3 ? bootstrapAnswer("Root-only API-issued platform enrolment-token file") : undefined;
14248
+ return {
14249
+ kind,
14250
+ environment,
14251
+ profile,
14252
+ computeReference,
14253
+ nodeHostname,
14254
+ apiUrl,
14255
+ repository,
14256
+ branch,
14257
+ telemetryEndpoint,
14258
+ database: {
14259
+ role,
14260
+ serverMode,
14261
+ address,
14262
+ master,
14263
+ coordinators,
14264
+ bootstrapSecretFile
14265
+ },
14266
+ platformEnrolTokenFile,
14267
+ runtime: {
14268
+ environment: {
14269
+ softwareProfile: profile,
14270
+ databaseRole: role,
14271
+ databaseCoordinators: coordinators,
14272
+ databaseAddress: address,
14273
+ databaseMaster: master,
14274
+ databaseNetworkMode,
14275
+ databaseReplicationFactor: replicationFactor,
14276
+ databaseWriteConcern: writeConcern,
14277
+ databaseUser: "forgezero-api",
14278
+ nodeHostname,
14279
+ nodeRegion: bootstrapAnswer("Published node region"),
14280
+ nodeRole: "guest",
14281
+ appOrigin,
14282
+ apiOrigin: apiUrl,
14283
+ publicApiPort: Number(bootstrapAnswer("Loopback public API port", "3000")),
14284
+ sharedDirectory: "/opt/forgezero/shared",
14285
+ seedSyncPeers: seedPeers,
14286
+ seedSyncMembers: Number(bootstrapAnswer("Seed quorum members", "3")),
14287
+ seedSyncEpoch: bootstrapAnswer("Seed epoch"),
14288
+ concurrencyLimit: Number(bootstrapAnswer("Initial concurrency limit", "128")),
14289
+ drainDeadlineMs: Number(bootstrapAnswer("Drain deadline milliseconds", "30000")),
14290
+ otlpEndpoint: "http://127.0.0.1:4318",
14291
+ otlpCollectorUnit: collectorUnit,
14292
+ agentOtlpEndpoint: telemetryEndpoint,
14293
+ otlpFlushIntervalMs: 1e4,
14294
+ otlpTraceSampleRatio: 0.1,
14295
+ custodianEmail: role === "master" ? bootstrapAnswer("First custodian email") : undefined,
14296
+ ...smtpHost && smtpPort && smtpFrom ? { smtp: { host: smtpHost, port: smtpPort, user: smtpUser || undefined, from: smtpFrom } } : {},
14297
+ ...backupEndpoint && backupRegion && backupBucket && backupAccessKeyId ? {
14298
+ backup: { endpoint: backupEndpoint, region: backupRegion, bucket: backupBucket, accessKeyId: backupAccessKeyId }
14299
+ } : {},
14300
+ repository,
14301
+ branch,
14302
+ deployProfile: environment
14303
+ },
14304
+ serviceUser: "forgezero-api",
14305
+ slotsDirectory: "/opt/forgezero/slots",
14306
+ bluePort: 3001,
14307
+ greenPort: 3002,
14308
+ healthPath: "/api/health",
14309
+ keepReleases: 5,
14310
+ credentialFiles: {
14311
+ smtpPassword,
14312
+ backupS3Secret
14313
+ }
14314
+ },
14315
+ firewall: {
14316
+ enabled: bootstrapAnswer("Enable host firewall? (yes/no)", "yes") === "yes",
14317
+ sshPort: Number(bootstrapAnswer("SSH port", "22")),
14318
+ privateCidrs: bootstrapAnswer("Private cluster CIDRs (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean)
14319
+ },
14320
+ installCloudflared: cloudflareCheckpoint ? true : bootstrapAnswer("Install cloudflared binary only? (yes/no)", "no") === "yes",
14321
+ ...cloudflareCheckpoint && cloudflareNodeName ? {
14322
+ cloudflareHandoff: { checkpointFile: cloudflareCheckpoint, nodeName: cloudflareNodeName }
14323
+ } : {}
14324
+ };
14325
+ }
14326
+ async function cmdBootstrap(options, args) {
14327
+ try {
14328
+ const operation = args[0] ?? "status";
14329
+ if (operation === "platform" && args[1] === "cloudflare") {
14330
+ if (!options.bootstrapConfigPath) {
14331
+ throw new Error("Cloudflare bootstrap requires --bootstrap-config <owner-only-cloudflare-json>");
14332
+ }
14333
+ await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply);
14334
+ return 0;
14335
+ }
14336
+ if (operation === "status") {
14337
+ if (existsSync6("/etc/forgezero/metal.initialized.json")) {
14338
+ const status2 = await metalBootstrapStatus();
14339
+ out.line(JSON.stringify(status2, null, 2));
14340
+ return status2.initialized && status2.problems.length === 0 ? 0 : 1;
14341
+ }
14342
+ const status = await bootstrapStatus();
14343
+ out.line(JSON.stringify(status, null, 2));
14344
+ return status.initialized ? 0 : 1;
14345
+ }
14346
+ if (operation === "metal") {
14347
+ if (!options.bootstrapConfigPath) {
14348
+ throw new Error("metal bootstrap requires --bootstrap-config <owner-only-metal-json>");
14349
+ }
14350
+ const config2 = readMetalBootstrapConfig(options.bootstrapConfigPath);
14351
+ if (!options.apply) {
14352
+ out.line(JSON.stringify(planMetalBootstrap(config2), null, 2));
14353
+ out.step("Review the plan, then repeat with --apply as root.");
14354
+ return 0;
14355
+ }
14356
+ const result2 = await applyMetalBootstrap(config2, { agentSourcePath: PACKAGED_AGENT_BIN2 });
14357
+ out.line(JSON.stringify(result2, null, 2));
14358
+ return 0;
14359
+ }
14360
+ if (!["platform", "tenant", "repair"].includes(operation)) {
14361
+ throw new Error("Usage: fz bootstrap platform [cloudflare]|tenant|metal|status|repair [--bootstrap-config <path>] [--apply]");
14362
+ }
14363
+ const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
14364
+ throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
14365
+ })() : interactiveBootstrap(operation);
14366
+ if (operation !== "repair" && config.kind !== operation) {
14367
+ throw new Error(`bootstrap config kind ${config.kind} does not match requested ${operation}`);
14368
+ }
14369
+ const plan = planBootstrap(config, operation === "repair");
14370
+ if (!options.apply) {
14371
+ out.line(JSON.stringify(plan, null, 2));
14372
+ out.step("Review the plan, then repeat with --apply as root.");
14373
+ return 0;
14374
+ }
14375
+ const result = await applyBootstrap(config);
14376
+ out.line(JSON.stringify(result, null, 2));
14377
+ return 0;
14378
+ } catch (cause) {
14379
+ out.fail(cause instanceof Error ? cause.message : String(cause));
14380
+ return 1;
14381
+ }
14382
+ }
10825
14383
  async function cmdGenesis(options) {
10826
14384
  try {
10827
14385
  if (options.realm !== "platform") {
@@ -10840,10 +14398,10 @@ async function cmdGenesis(options) {
10840
14398
  ` + ` On the server: the token file is read automatically, or set FZ_SHARED_DIR
10841
14399
  ` + ` From a laptop: copy it to a chmod 600 file and pass --token-file <path>
10842
14400
 
10843
- ` + " The token is what setup.sh created. It is single use.");
14401
+ ` + " The token is what `fz bootstrap platform` created. It is single use.");
10844
14402
  }
10845
14403
  if (!/^plt_[A-Za-z0-9_-]{32,100}$/.test(token)) {
10846
- throw new Error("The platform invitation is malformed; expected the one-use plt_ token from setup.sh.");
14404
+ throw new Error("The platform invitation is malformed; expected the one-use plt_ token from `fz bootstrap platform`.");
10847
14405
  }
10848
14406
  const session = await authorizeDeviceSession(options, (approval, app) => {
10849
14407
  const ceremony = new URL("/custody/ceremony", app);
@@ -10880,7 +14438,7 @@ async function cmdUnlock(options) {
10880
14438
  if (options.key || options.userExplicit) {
10881
14439
  throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
10882
14440
  }
10883
- const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync4(0, "utf8").trim() : "";
14441
+ const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync7(0, "utf8").trim() : "";
10884
14442
  const phrase = phraseText ? phraseText.split(/\s+/) : [];
10885
14443
  if (phrase.length !== 24) {
10886
14444
  throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
@@ -10999,7 +14557,7 @@ function projectFromCheckout(options) {
10999
14557
  try {
11000
14558
  return loadConfig({
11001
14559
  cwd: options.projectRoot,
11002
- readFile: (path) => existsSync4(path) ? readFileSync4(path, "utf8") : undefined
14560
+ readFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : undefined
11003
14561
  }).config.project;
11004
14562
  } catch (cause) {
11005
14563
  throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
@@ -11118,7 +14676,7 @@ async function cmdDeploy(options, args) {
11118
14676
  branch: options.branch,
11119
14677
  cloneUrl: required(options.cloneUrl, "--clone-url"),
11120
14678
  sourceAuth,
11121
- ...options.knownHostsFile ? { knownHosts: readFileSync4(options.knownHostsFile, "utf8") } : {},
14679
+ ...options.knownHostsFile ? { knownHosts: readFileSync7(options.knownHostsFile, "utf8") } : {},
11122
14680
  projectKey
11123
14681
  } });
11124
14682
  const pipelineKey2 = String(created.pipelineKey);
@@ -11215,6 +14773,14 @@ function usage() {
11215
14773
  fz agent install Install the node agent as a systemd service, so
11216
14774
  applications on this box read secrets through a
11217
14775
  local socket instead of holding an API key
14776
+ fz bootstrap platform Install/repair a typed elastic platform compute
14777
+ fz bootstrap platform cloudflare
14778
+ Plan/apply token-file-only KV, Worker, Access, DNS
14779
+ and one Tunnel per explicit platform API node
14780
+ fz bootstrap tenant Install/enrol reusable tenant compute tooling
14781
+ fz bootstrap metal Plan/install an identity-only physical provisioner
14782
+ fz bootstrap status Verify persisted profile and supervised units
14783
+ fz bootstrap repair Reapply an explicitly supplied reviewed config
11218
14784
  fz project init Create vendor-neutral, Git-persisted AI context
11219
14785
  fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
11220
14786
  fz project check Fail when truth sources or generated adapters drift
@@ -11252,6 +14818,8 @@ function usage() {
11252
14818
  --apply Write the unit rather than printing it (root)
11253
14819
  --enrol Bind this machine with a one-time token prompted
11254
14820
  securely by systemd (tenant-owned compute)
14821
+ --bootstrap-config <p> Private JSON coordinates for unattended bootstrap;
14822
+ secret values remain in separate owner-only files
11255
14823
  --root <path> Project root for project/deploy commands
11256
14824
  --name <name> Project or deploy name during init
11257
14825
  --purpose <text> Product outcome during project init
@@ -11330,6 +14898,9 @@ async function runCli() {
11330
14898
  case "agent":
11331
14899
  code = await cmdAgent(options, args);
11332
14900
  break;
14901
+ case "bootstrap":
14902
+ code = await cmdBootstrap(options, args);
14903
+ break;
11333
14904
  case "project":
11334
14905
  code = cmdProject(options, args);
11335
14906
  break;
@@ -11359,7 +14930,7 @@ async function runCli() {
11359
14930
  usage();
11360
14931
  code = 1;
11361
14932
  }
11362
- if (args.length > 0 && code === 0 && !["agent", "project", "deploy", "api", "ui", "routes"].includes(command)) {
14933
+ if (args.length > 0 && code === 0 && !["agent", "bootstrap", "project", "deploy", "api", "ui", "routes"].includes(command)) {
11363
14934
  out.warn(`Ignored: ${args.join(" ")}`);
11364
14935
  }
11365
14936
  process.exit(code);