@forgezero/agent 0.1.52 → 0.1.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz.js CHANGED
@@ -4811,9 +4811,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
4813
  init_dist();
4814
- import { existsSync as existsSync9, lstatSync as lstatSync8, mkdirSync as mkdirSync9, readFileSync as readFileSync12, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync10 } from "fs";
4814
+ import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync12 } from "fs";
4815
4815
  import { randomBytes as randomBytes10 } from "crypto";
4816
- import { dirname as dirname10, isAbsolute as isAbsolute4, resolve as resolve7 } from "path";
4816
+ import { basename as basename3, dirname as dirname12, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
4817
4817
  import { fileURLToPath as fileURLToPath3 } from "url";
4818
4818
  import { hostname } from "os";
4819
4819
 
@@ -4830,7 +4830,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4830
4830
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4831
4831
 
4832
4832
  // src/version.ts
4833
- var VERSION2 = "0.1.52";
4833
+ var VERSION2 = "0.1.55";
4834
4834
 
4835
4835
  // src/software.ts
4836
4836
  var PINNED_BUN_VERSION = "1.3.14";
@@ -4840,6 +4840,7 @@ var OS_CATALOG = [
4840
4840
  ];
4841
4841
  var SOFTWARE_CATALOG = [
4842
4842
  { id: "bun", version: "1.3.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4843
+ { id: "docker", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4843
4844
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4844
4845
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4845
4846
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
@@ -4847,6 +4848,14 @@ var SOFTWARE_CATALOG = [
4847
4848
  { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
4848
4849
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
4849
4850
  ];
4851
+ var DOCKER_DAEMON_CONFIG = `${JSON.stringify({
4852
+ "data-root": "/var/lib/docker",
4853
+ "storage-driver": "overlay2",
4854
+ "live-restore": true,
4855
+ "log-driver": "local",
4856
+ "log-opts": { "max-size": "10m", "max-file": "3" }
4857
+ }, null, 2)}
4858
+ `;
4850
4859
  function validateSoftwareRequirements(value, _options = {}) {
4851
4860
  if (!Array.isArray(value) || value.length > 32)
4852
4861
  throw new Error("software requirements must be an array of at most 32 entries");
@@ -4858,7 +4867,7 @@ function validateSoftwareRequirements(value, _options = {}) {
4858
4867
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
4859
4868
  throw new Error("software requirement contains an unknown field");
4860
4869
  }
4861
- if (!["bun", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4870
+ if (!["bun", "docker", "nginx", "arangodb", "cloudflared", "cloudflare-warp", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
4862
4871
  throw new Error("software requirement coordinate is invalid");
4863
4872
  }
4864
4873
  const requirement = { id: row.id, version: row.version };
@@ -5254,6 +5263,39 @@ var defaultHost = {
5254
5263
  now: Date.now
5255
5264
  };
5256
5265
 
5266
+ // src/container-supervisor.ts
5267
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, readdirSync as readdirSync2, realpathSync as realpathSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
5268
+ import { dirname as dirname2, resolve as resolve2, sep as sep2 } from "path";
5269
+ var defaultHost2 = {
5270
+ realpath: realpathSync2,
5271
+ exists: existsSync2,
5272
+ read: (path) => readFileSync2(path, "utf8"),
5273
+ write(path, content, mode) {
5274
+ mkdirSync2(dirname2(path), { recursive: true, mode: 493 });
5275
+ const next = `${path}.next`;
5276
+ writeFileSync2(next, content, { mode });
5277
+ renameSync2(next, path);
5278
+ },
5279
+ remove: (path) => rmSync2(path, { force: true }),
5280
+ list: (path) => existsSync2(path) ? readdirSync2(path) : [],
5281
+ mkdir: (path, mode) => mkdirSync2(path, { recursive: true, mode }),
5282
+ async exec(argv2) {
5283
+ const child = Bun.spawn([...argv2], { stdout: "pipe", stderr: "pipe", env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C" } });
5284
+ const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
5285
+ return { exitCode, output: `${stdout}${stderr}` };
5286
+ },
5287
+ async health(port, path, timeoutMs, method, expectedStatus) {
5288
+ try {
5289
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, { method, redirect: "manual", signal: AbortSignal.timeout(timeoutMs) });
5290
+ return expectedStatus.includes(response.status);
5291
+ } catch {
5292
+ return false;
5293
+ }
5294
+ },
5295
+ sleep: (ms) => Bun.sleep(ms),
5296
+ now: Date.now
5297
+ };
5298
+
5257
5299
  // src/software-helper.ts
5258
5300
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
5259
5301
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
@@ -6236,17 +6278,17 @@ import { randomBytes as randomBytes5 } from "crypto";
6236
6278
  import {
6237
6279
  chmodSync,
6238
6280
  copyFileSync,
6239
- existsSync as existsSync2,
6281
+ existsSync as existsSync3,
6240
6282
  lstatSync,
6241
- mkdirSync as mkdirSync2,
6242
- readFileSync as readFileSync2,
6243
- realpathSync as realpathSync2,
6244
- renameSync as renameSync2,
6245
- rmSync as rmSync2,
6283
+ mkdirSync as mkdirSync3,
6284
+ readFileSync as readFileSync3,
6285
+ realpathSync as realpathSync3,
6286
+ renameSync as renameSync3,
6287
+ rmSync as rmSync3,
6246
6288
  symlinkSync,
6247
- writeFileSync as writeFileSync2
6289
+ writeFileSync as writeFileSync3
6248
6290
  } from "fs";
6249
- import { dirname as dirname2 } from "path";
6291
+ import { dirname as dirname3 } from "path";
6250
6292
  function parseAssignments(value) {
6251
6293
  if (!value?.trim())
6252
6294
  return;
@@ -6338,28 +6380,28 @@ var runProvisionOperation = async (operation) => {
6338
6380
  }
6339
6381
  if (operation.kind === "install-runtime") {
6340
6382
  const release = `/opt/forgezero/agent/versions/${operation.version}`;
6341
- mkdirSync2(`${release}/dist`, { recursive: true, mode: 493 });
6342
- mkdirSync2(dirname2(operation.binary), { recursive: true, mode: 493 });
6383
+ mkdirSync3(`${release}/dist`, { recursive: true, mode: 493 });
6384
+ mkdirSync3(dirname3(operation.binary), { recursive: true, mode: 493 });
6343
6385
  copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
6344
6386
  chmodSync(`${release}/dist/fz-agent.js`, 493);
6345
- const gitSshSource = `${dirname2(operation.source)}/fz-git-ssh.js`;
6346
- if (!existsSync2(gitSshSource))
6387
+ const gitSshSource = `${dirname3(operation.source)}/fz-git-ssh.js`;
6388
+ if (!existsSync3(gitSshSource))
6347
6389
  return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
6348
6390
  copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
6349
6391
  chmodSync(`${release}/dist/fz-git-ssh.js`, 493);
6350
6392
  const pending = "/opt/forgezero/agent/current.next";
6351
- rmSync2(pending, { force: true });
6393
+ rmSync3(pending, { force: true });
6352
6394
  symlinkSync(`versions/${operation.version}`, pending);
6353
- renameSync2(pending, "/opt/forgezero/agent/current");
6354
- rmSync2(operation.binary, { force: true });
6395
+ renameSync3(pending, "/opt/forgezero/agent/current");
6396
+ rmSync3(operation.binary, { force: true });
6355
6397
  symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
6356
6398
  const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
6357
- rmSync2(gitSshBinary, { force: true });
6399
+ rmSync3(gitSshBinary, { force: true });
6358
6400
  symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
6359
6401
  return { stdout: "", exitCode: 0 };
6360
6402
  }
6361
6403
  if (operation.kind === "ensure-seed") {
6362
- if (existsSync2(operation.credential) && lstatSync(operation.credential).size > 0)
6404
+ if (existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
6363
6405
  return { stdout: "", exitCode: 0 };
6364
6406
  const seed = randomBytes5(32).toString("base64url");
6365
6407
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
@@ -6371,9 +6413,9 @@ var runProvisionOperation = async (operation) => {
6371
6413
  const key = "/run/forgezero-git-deploy-key";
6372
6414
  const publicKey = `${key}.pub`;
6373
6415
  try {
6374
- if (!existsSync2(operation.credential) || lstatSync(operation.credential).size < 1) {
6375
- rmSync2(key, { force: true });
6376
- rmSync2(publicKey, { force: true });
6416
+ if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
6417
+ rmSync3(key, { force: true });
6418
+ rmSync3(publicKey, { force: true });
6377
6419
  let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
6378
6420
  if (result.exitCode !== 0)
6379
6421
  return result;
@@ -6382,8 +6424,8 @@ var runProvisionOperation = async (operation) => {
6382
6424
  return result;
6383
6425
  chmodSync(operation.credential, 256);
6384
6426
  }
6385
- if (!existsSync2(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
6386
- if (!existsSync2(key)) {
6427
+ if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
6428
+ if (!existsSync3(key)) {
6387
6429
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
6388
6430
  if (decrypted.exitCode !== 0)
6389
6431
  return decrypted;
@@ -6391,25 +6433,25 @@ var runProvisionOperation = async (operation) => {
6391
6433
  const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
6392
6434
  if (derived.exitCode !== 0)
6393
6435
  return derived;
6394
- writeFileSync2(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
6436
+ writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
6395
6437
  `, { mode: 292 });
6396
6438
  }
6397
6439
  return { stdout: "", exitCode: 0 };
6398
6440
  } finally {
6399
- rmSync2(key, { force: true });
6400
- rmSync2(publicKey, { force: true });
6441
+ rmSync3(key, { force: true });
6442
+ rmSync3(publicKey, { force: true });
6401
6443
  }
6402
6444
  }
6403
6445
  if (operation.kind === "ensure-bootstrap-ssh-identity") {
6404
6446
  const key = "/run/forgezero-bootstrap-ssh-key";
6405
6447
  const generatedPublicKey = `${key}.pub`;
6406
6448
  try {
6407
- if (!existsSync2(operation.credential) || lstatSync(operation.credential).size < 1) {
6408
- rmSync2(key, { force: true });
6409
- rmSync2(generatedPublicKey, { force: true });
6449
+ if (!existsSync3(operation.credential) || lstatSync(operation.credential).size < 1) {
6450
+ rmSync3(key, { force: true });
6451
+ rmSync3(generatedPublicKey, { force: true });
6410
6452
  let result;
6411
6453
  if (operation.source) {
6412
- const source = existsSync2(operation.source) ? lstatSync(operation.source) : undefined;
6454
+ const source = existsSync3(operation.source) ? lstatSync(operation.source) : undefined;
6413
6455
  if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
6414
6456
  return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
6415
6457
  }
@@ -6429,8 +6471,8 @@ var runProvisionOperation = async (operation) => {
6429
6471
  return result;
6430
6472
  chmodSync(operation.credential, 256);
6431
6473
  }
6432
- if (!existsSync2(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
6433
- if (!existsSync2(key)) {
6474
+ if (!existsSync3(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
6475
+ if (!existsSync3(key)) {
6434
6476
  const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
6435
6477
  if (decrypted.exitCode !== 0)
6436
6478
  return decrypted;
@@ -6440,28 +6482,28 @@ var runProvisionOperation = async (operation) => {
6440
6482
  if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
6441
6483
  return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
6442
6484
  }
6443
- mkdirSync2(dirname2(operation.publicKey), { recursive: true, mode: 493 });
6444
- writeFileSync2(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
6485
+ mkdirSync3(dirname3(operation.publicKey), { recursive: true, mode: 493 });
6486
+ writeFileSync3(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
6445
6487
  `, { mode: 292 });
6446
6488
  chmodSync(operation.publicKey, 292);
6447
6489
  }
6448
6490
  if (operation.source)
6449
- rmSync2(operation.source, { force: true });
6491
+ rmSync3(operation.source, { force: true });
6450
6492
  return { stdout: "", exitCode: 0 };
6451
6493
  } finally {
6452
- rmSync2(key, { force: true });
6453
- rmSync2(generatedPublicKey, { force: true });
6494
+ rmSync3(key, { force: true });
6495
+ rmSync3(generatedPublicKey, { force: true });
6454
6496
  }
6455
6497
  }
6456
6498
  if (operation.kind === "ensure-enrolment") {
6457
- if (existsSync2(operation.state) && lstatSync(operation.state).size > 0 || existsSync2(operation.credential) && lstatSync(operation.credential).size > 0)
6499
+ if (existsSync3(operation.state) && lstatSync(operation.state).size > 0 || existsSync3(operation.credential) && lstatSync(operation.credential).size > 0)
6458
6500
  return { stdout: "", exitCode: 0 };
6459
- if (!existsSync2(operation.source))
6501
+ if (!existsSync3(operation.source))
6460
6502
  return { stdout: "enrolment source is missing", exitCode: 1 };
6461
6503
  const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
6462
6504
  if (result.exitCode === 0) {
6463
6505
  chmodSync(operation.credential, 256);
6464
- rmSync2(operation.source, { force: true });
6506
+ rmSync3(operation.source, { force: true });
6465
6507
  }
6466
6508
  return result;
6467
6509
  }
@@ -6476,7 +6518,7 @@ var runProvisionOperation = async (operation) => {
6476
6518
  return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
6477
6519
  }
6478
6520
  if (operation.kind === "verify-file")
6479
- return existsSync2(operation.path) && lstatSync(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
6521
+ return existsSync3(operation.path) && lstatSync(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
6480
6522
  if (operation.kind === "verify-egress") {
6481
6523
  const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
6482
6524
  if (active.exitCode !== 0)
@@ -6492,31 +6534,31 @@ var runProvisionOperation = async (operation) => {
6492
6534
  if (operation.kind === "verify-resolved-stub") {
6493
6535
  try {
6494
6536
  const expected = "/run/systemd/resolve/stub-resolv.conf";
6495
- return realpathSync2("/etc/resolv.conf") === expected && realpathSync2(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
6537
+ return realpathSync3("/etc/resolv.conf") === expected && realpathSync3(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
6496
6538
  } catch {
6497
6539
  return { stdout: "resolver stub missing", exitCode: 1 };
6498
6540
  }
6499
6541
  }
6500
6542
  if (operation.kind === "install-warp") {
6501
- const os = readFileSync2("/etc/os-release", "utf8");
6543
+ const os = readFileSync3("/etc/os-release", "utf8");
6502
6544
  if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
6503
6545
  return { stdout: "unsupported WARP host OS", exitCode: 1 };
6504
6546
  const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
6505
6547
  if (!response.ok)
6506
6548
  return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
6507
- mkdirSync2("/usr/share/keyrings", { recursive: true, mode: 493 });
6508
- mkdirSync2("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
6509
- mkdirSync2("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
6549
+ mkdirSync3("/usr/share/keyrings", { recursive: true, mode: 493 });
6550
+ mkdirSync3("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
6551
+ mkdirSync3("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
6510
6552
  const key = "/run/cloudflare-warp-key.gpg";
6511
- writeFileSync2(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
6553
+ writeFileSync3(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
6512
6554
  let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
6513
- rmSync2(key, { force: true });
6555
+ rmSync3(key, { force: true });
6514
6556
  if (result.exitCode !== 0)
6515
6557
  return result;
6516
6558
  const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
6517
6559
  if (!codename)
6518
6560
  return { stdout: "Ubuntu codename missing", exitCode: 1 };
6519
- writeFileSync2("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
6561
+ writeFileSync3("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
6520
6562
  `, { mode: 420 });
6521
6563
  result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
6522
6564
  return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
@@ -9901,8 +9943,8 @@ async function runUnlock(args) {
9901
9943
  }
9902
9944
 
9903
9945
  // src/project-context.ts
9904
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync as renameSync3, writeFileSync as writeFileSync3 } from "fs";
9905
- import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
9946
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, renameSync as renameSync4, writeFileSync as writeFileSync4 } from "fs";
9947
+ import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
9906
9948
  var PROJECT_CONTEXT_VERSION = 1;
9907
9949
  var GENERATED = "<!-- Generated by @forgezero/agent project context. Edit .forgezero/project.json, then run `fz project sync`. -->";
9908
9950
 
@@ -9981,14 +10023,14 @@ function defaultProjectContext(root = process.cwd()) {
9981
10023
  let name = root.split("/").filter(Boolean).at(-1) ?? "project";
9982
10024
  let verify = ["npm test"];
9983
10025
  const manifestPath = join3(root, "package.json");
9984
- if (existsSync3(manifestPath)) {
10026
+ if (existsSync4(manifestPath)) {
9985
10027
  try {
9986
- const pkg = JSON.parse(readFileSync3(manifestPath, "utf8"));
10028
+ const pkg = JSON.parse(readFileSync4(manifestPath, "utf8"));
9987
10029
  name = pkg.name ?? name;
9988
- const runner = existsSync3(join3(root, "bun.lock")) ? "bun run" : "npm run";
10030
+ const runner = existsSync4(join3(root, "bun.lock")) ? "bun run" : "npm run";
9989
10031
  verify = ["check", "test", "build"].filter((script) => pkg.scripts?.[script]).map((script) => `${runner} ${script}`);
9990
10032
  if (verify.length === 0)
9991
- verify = [existsSync3(join3(root, "bun.lock")) ? "bun test" : "npm test"];
10033
+ verify = [existsSync4(join3(root, "bun.lock")) ? "bun test" : "npm test"];
9992
10034
  } catch {}
9993
10035
  }
9994
10036
  return {
@@ -10083,22 +10125,22 @@ ${adapter("Cursor").replace(`${GENERATED}
10083
10125
  ];
10084
10126
  }
10085
10127
  var atomicWrite = (path, content) => {
10086
- mkdirSync3(dirname3(path), { recursive: true });
10128
+ mkdirSync4(dirname4(path), { recursive: true });
10087
10129
  const next = `${path}.${process.pid}.next`;
10088
- writeFileSync3(next, content, { mode: 420 });
10089
- renameSync3(next, path);
10130
+ writeFileSync4(next, content, { mode: 420 });
10131
+ renameSync4(next, path);
10090
10132
  };
10091
10133
  function initializeProjectContext(rootInput, manifestInput = defaultProjectContext(rootInput), options = {}) {
10092
- const root = resolve2(rootInput);
10134
+ const root = resolve3(rootInput);
10093
10135
  const manifest = parseProjectContext(manifestInput);
10094
10136
  const manifestPath = join3(root, ".forgezero", "project.json");
10095
10137
  const files = projectContextFiles(manifest);
10096
10138
  const collisions = [manifestPath, ...files.map((file) => join3(root, file.path))].filter((path) => {
10097
- if (!existsSync3(path))
10139
+ if (!existsSync4(path))
10098
10140
  return false;
10099
10141
  if (path === manifestPath)
10100
10142
  return true;
10101
- return !readFileSync3(path, "utf8").startsWith(GENERATED);
10143
+ return !readFileSync4(path, "utf8").startsWith(GENERATED);
10102
10144
  });
10103
10145
  if (collisions.length && !options.force) {
10104
10146
  throw new ProjectContextError(`refusing to replace existing project context: ${collisions.join(", ")}`);
@@ -10110,15 +10152,15 @@ function initializeProjectContext(rootInput, manifestInput = defaultProjectConte
10110
10152
  return files;
10111
10153
  }
10112
10154
  function syncProjectContext(rootInput) {
10113
- const root = resolve2(rootInput);
10155
+ const root = resolve3(rootInput);
10114
10156
  const manifestPath = join3(root, ".forgezero", "project.json");
10115
- if (!existsSync3(manifestPath))
10157
+ if (!existsSync4(manifestPath))
10116
10158
  throw new ProjectContextError("No .forgezero/project.json. Run `fz project init`.");
10117
- const manifest = parseProjectContext(JSON.parse(readFileSync3(manifestPath, "utf8")));
10159
+ const manifest = parseProjectContext(JSON.parse(readFileSync4(manifestPath, "utf8")));
10118
10160
  const files = projectContextFiles(manifest);
10119
10161
  for (const file of files) {
10120
10162
  const path = join3(root, file.path);
10121
- if (existsSync3(path) && !readFileSync3(path, "utf8").startsWith(GENERATED)) {
10163
+ if (existsSync4(path) && !readFileSync4(path, "utf8").startsWith(GENERATED)) {
10122
10164
  throw new ProjectContextError(`refusing to replace non-generated adapter: ${file.path}`);
10123
10165
  }
10124
10166
  atomicWrite(path, file.content);
@@ -10126,30 +10168,30 @@ function syncProjectContext(rootInput) {
10126
10168
  return files;
10127
10169
  }
10128
10170
  function checkProjectContext(rootInput) {
10129
- const root = resolve2(rootInput);
10171
+ const root = resolve3(rootInput);
10130
10172
  const manifestPath = join3(root, ".forgezero", "project.json");
10131
- if (!existsSync3(manifestPath))
10173
+ if (!existsSync4(manifestPath))
10132
10174
  return { ok: false, problems: ["missing .forgezero/project.json"] };
10133
10175
  let manifest;
10134
10176
  try {
10135
- manifest = parseProjectContext(JSON.parse(readFileSync3(manifestPath, "utf8")));
10177
+ manifest = parseProjectContext(JSON.parse(readFileSync4(manifestPath, "utf8")));
10136
10178
  } catch (cause) {
10137
10179
  return { ok: false, problems: [cause instanceof Error ? cause.message : String(cause)] };
10138
10180
  }
10139
10181
  const problems = [];
10140
10182
  for (const source of manifest.truth) {
10141
- if (!existsSync3(join3(root, source.path)))
10183
+ if (!existsSync4(join3(root, source.path)))
10142
10184
  problems.push(`missing truth source: ${source.path}`);
10143
10185
  }
10144
10186
  for (const path of manifest.readFirst) {
10145
- if (!existsSync3(join3(root, path)))
10187
+ if (!existsSync4(join3(root, path)))
10146
10188
  problems.push(`missing read-first file: ${path}`);
10147
10189
  }
10148
10190
  for (const file of projectContextFiles(manifest)) {
10149
10191
  const path = join3(root, file.path);
10150
- if (!existsSync3(path))
10192
+ if (!existsSync4(path))
10151
10193
  problems.push(`missing generated adapter: ${file.path}`);
10152
- else if (readFileSync3(path, "utf8") !== file.content)
10194
+ else if (readFileSync4(path, "utf8") !== file.content)
10153
10195
  problems.push(`drifted generated adapter: ${file.path}`);
10154
10196
  }
10155
10197
  return { ok: problems.length === 0, problems };
@@ -10157,7 +10199,7 @@ function checkProjectContext(rootInput) {
10157
10199
 
10158
10200
  // src/deploy-file.ts
10159
10201
  import { createHash } from "crypto";
10160
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
10202
+ import { existsSync as existsSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
10161
10203
  import { basename, join as join4 } from "path";
10162
10204
  var DEPLOY_FILE = ".fz/deploy.json";
10163
10205
  var DEPLOY_TODO_PREFIX = "ForgeZero pipeline TODO:";
@@ -10178,17 +10220,17 @@ var safeName = (value) => {
10178
10220
  };
10179
10221
  function packageHints(root) {
10180
10222
  const packagePath = join4(root, "package.json");
10181
- if (!existsSync4(packagePath))
10182
- return { bun: existsSync4(join4(root, "bun.lock")) };
10223
+ if (!existsSync5(packagePath))
10224
+ return { bun: existsSync5(join4(root, "bun.lock")) };
10183
10225
  try {
10184
- const manifest = JSON.parse(readFileSync4(packagePath, "utf8"));
10226
+ const manifest = JSON.parse(readFileSync5(packagePath, "utf8"));
10185
10227
  return {
10186
10228
  name: typeof manifest.name === "string" ? manifest.name : undefined,
10187
10229
  build: typeof manifest.scripts?.build === "string" ? ["bun", "run", "build"] : undefined,
10188
- bun: existsSync4(join4(root, "bun.lock")) || existsSync4(join4(root, "bun.lockb"))
10230
+ bun: existsSync5(join4(root, "bun.lock")) || existsSync5(join4(root, "bun.lockb"))
10189
10231
  };
10190
10232
  } catch {
10191
- return { bun: existsSync4(join4(root, "bun.lock")) };
10233
+ return { bun: existsSync5(join4(root, "bun.lock")) };
10192
10234
  }
10193
10235
  }
10194
10236
  var blocker = (instruction) => ["fz-agent", "pipeline-todo", `${DEPLOY_TODO_PREFIX} ${instruction}`];
@@ -10225,9 +10267,9 @@ function defaultDeployFile(root, options = {}) {
10225
10267
  }
10226
10268
  function inspectDeployFile(root, options = {}) {
10227
10269
  const path = join4(root, DEPLOY_FILE);
10228
- if (!existsSync4(path))
10270
+ if (!existsSync5(path))
10229
10271
  throw new Error(`${DEPLOY_FILE} does not exist; run \`fz deploy init\`.`);
10230
- const raw = JSON.parse(readFileSync4(path, "utf8"));
10272
+ const raw = JSON.parse(readFileSync5(path, "utf8"));
10231
10273
  const definition = parseDeployDefinition(raw, options);
10232
10274
  const problems = definition.steps.filter((step2) => step2.exec.some((argument) => argument.includes(DEPLOY_TODO_PREFIX))).map((step2) => `${step2.name} still contains the safe initialization blocker`);
10233
10275
  const profiles = Object.keys(definition.profiles).sort();
@@ -10251,17 +10293,1060 @@ function inspectDeployFile(root, options = {}) {
10251
10293
  }
10252
10294
  function initializeDeployFile(root, options = {}) {
10253
10295
  const path = join4(root, DEPLOY_FILE);
10254
- if (existsSync4(path) && !options.force) {
10296
+ if (existsSync5(path) && !options.force) {
10255
10297
  throw new Error(`${DEPLOY_FILE} already exists; use --force only when replacing it deliberately.`);
10256
10298
  }
10257
10299
  const raw = defaultDeployFile(root, options);
10258
10300
  parseDeployDefinition(raw, { channel: options.channel });
10259
- mkdirSync4(join4(root, ".fz"), { recursive: true });
10260
- writeFileSync4(path, `${JSON.stringify(raw, null, 2)}
10301
+ mkdirSync5(join4(root, ".fz"), { recursive: true });
10302
+ writeFileSync5(path, `${JSON.stringify(raw, null, 2)}
10261
10303
  `, { mode: 420 });
10262
10304
  return inspectDeployFile(root, { channel: options.channel });
10263
10305
  }
10264
10306
 
10307
+ // src/deploy-compiler.ts
10308
+ import { existsSync as existsSync6, lstatSync as lstatSync2, mkdirSync as mkdirSync6, readFileSync as readFileSync6, renameSync as renameSync5, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
10309
+ import { dirname as dirname5, isAbsolute, relative, resolve as resolve4, sep as sep3 } from "path";
10310
+ import { pathToFileURL } from "url";
10311
+
10312
+ // src/deploy-plan.ts
10313
+ import { createHash as createHash2 } from "crypto";
10314
+
10315
+ // src/deploy-actions.ts
10316
+ var row = (value, where) => {
10317
+ if (!value || typeof value !== "object" || Array.isArray(value))
10318
+ throw new Error(`${where} must be an object`);
10319
+ return value;
10320
+ };
10321
+ var exact = (value, keys, where) => {
10322
+ const unknown = Object.keys(value).filter((key) => !keys.includes(key));
10323
+ if (unknown.length)
10324
+ throw new Error(`${where} contains unknown field(s): ${unknown.join(", ")}`);
10325
+ for (const key of keys)
10326
+ if (value[key] === undefined)
10327
+ throw new Error(`${where} requires ${key}`);
10328
+ };
10329
+ var component = (value, components, where) => {
10330
+ if (typeof value !== "string" || !components[value])
10331
+ throw new Error(`${where}.component must name an existing component`);
10332
+ return components[value];
10333
+ };
10334
+ var exactArgv = (value, where) => {
10335
+ if (!Array.isArray(value) || value.length < 1 || value.length > 256 || value.some((entry) => typeof entry !== "string" || entry.length < 1 || entry.length > 16384 || entry.includes("\x00")))
10336
+ throw new Error(`${where} requires a bounded exact argv array`);
10337
+ const executable = value[0].split("/").at(-1).toLowerCase();
10338
+ if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
10339
+ throw new Error(`${where} cannot invoke a command dispatcher`);
10340
+ };
10341
+ function validateBuiltInAction(definition, context, where) {
10342
+ if (!definition.uses.startsWith("forgezero."))
10343
+ return;
10344
+ const withValue = row(definition.with, `${where}.with`);
10345
+ if (definition.uses === "forgezero.software/ensure@1") {
10346
+ exact(withValue, ["requirements"], `${where}.with`);
10347
+ if (!Array.isArray(withValue.requirements) || withValue.requirements.length < 1 || withValue.requirements.length > 64 || withValue.requirements.some((name) => typeof name !== "string" || !context.requirements[name]))
10348
+ throw new Error(`${where}.with.requirements must name existing requirements`);
10349
+ return;
10350
+ }
10351
+ if (definition.uses === "forgezero.exec/argv@1") {
10352
+ const allowed = ["component", "argv", "credentials"];
10353
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
10354
+ if (unknown.length)
10355
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
10356
+ component(withValue.component, context.components, `${where}.with`);
10357
+ exactArgv(withValue.argv, `${where}.with.argv`);
10358
+ if (withValue.credentials !== undefined) {
10359
+ const bindings = row(withValue.credentials, `${where}.with.credentials`);
10360
+ for (const [environment, credentialName] of Object.entries(bindings))
10361
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(environment) || typeof credentialName !== "string" || !context.credentials[credentialName])
10362
+ throw new Error(`${where}.with.credentials contains an invalid binding`);
10363
+ }
10364
+ return;
10365
+ }
10366
+ if (["forgezero.oci/build@1", "forgezero.oci/pull@1"].includes(definition.uses)) {
10367
+ const allowed = definition.uses.endsWith("/build@1") ? ["component", "noCache"] : ["component"];
10368
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
10369
+ if (unknown.length)
10370
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
10371
+ const selected = component(withValue.component, context.components, `${where}.with`);
10372
+ if (selected.kind !== "application" || selected.runtime.kind !== "container")
10373
+ throw new Error(`${where}.with.component must use a container runtime`);
10374
+ if (withValue.noCache !== undefined && typeof withValue.noCache !== "boolean")
10375
+ throw new Error(`${where}.with.noCache must be a boolean`);
10376
+ return;
10377
+ }
10378
+ if (["forgezero.service/deploy@1", "forgezero.service/promote@1", "forgezero.health/http@1", "forgezero.storage/verify@1"].includes(definition.uses)) {
10379
+ const allowed = definition.uses === "forgezero.service/deploy@1" || definition.uses === "forgezero.service/promote@1" ? ["component", "imageDigest"] : ["component"];
10380
+ const unknown = Object.keys(withValue).filter((key) => !allowed.includes(key));
10381
+ if (unknown.length)
10382
+ throw new Error(`${where}.with contains unknown field(s): ${unknown.join(", ")}`);
10383
+ component(withValue.component, context.components, `${where}.with`);
10384
+ return;
10385
+ }
10386
+ throw new Error(`${where}.uses is an unknown ForgeZero action`);
10387
+ }
10388
+ // src/deploy-providers.ts
10389
+ var plain = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
10390
+ var exact2 = (row2, keys, provider) => {
10391
+ const unknown = Object.keys(row2).filter((key) => !keys.includes(key));
10392
+ if (unknown.length)
10393
+ throw new Error(`${provider} config contains unknown field(s): ${unknown.join(", ")}`);
10394
+ for (const key of keys)
10395
+ if (row2[key] === undefined)
10396
+ throw new Error(`${provider} config requires ${key}`);
10397
+ };
10398
+ var absolute = (value) => typeof value === "string" && /^\/(?:[A-Za-z0-9._-]+\/?)*$/.test(value) && !value.includes("..");
10399
+ function validateBuiltInProvider(requirement2, where) {
10400
+ const row2 = plain(requirement2.config);
10401
+ if (!requirement2.provider.startsWith("forgezero."))
10402
+ return;
10403
+ const expected = {
10404
+ "forgezero.bun": { capability: "runtime.javascript", versions: ["1.3.14"], keys: ["installScope"] },
10405
+ "forgezero.docker": { capability: "runtime.container", versions: ["ubuntu-26.04"], keys: ["installScope", "storageDriver", "dataRoot", "liveRestore", "logDriver", "defaultNetwork", "rootless"] },
10406
+ "forgezero.nginx": { capability: "proxy.http", versions: ["ubuntu-26.04"], keys: ["installScope", "websocket", "maximumBodyMiB", "workerProcesses", "workerConnections"] },
10407
+ "forgezero.arangodb": { capability: "database.arangodb", versions: ["3.11.14"], keys: ["installScope", "runtime", "dataPath", "bind"] },
10408
+ "forgezero.cloudflared": { capability: "network.public-tunnel", versions: ["2026.7.3"], keys: ["installScope", "mode"] },
10409
+ "forgezero.warp": { capability: "network.private", versions: ["2026.6.822.0-min"], keys: ["installScope", "mode"] },
10410
+ "forgezero.ufw": { capability: "security.firewall", versions: ["ubuntu-26.04"], keys: ["installScope", "defaultIncoming", "defaultOutgoing"] },
10411
+ "forgezero.openssh": { capability: "transport.ssh", versions: ["ubuntu-26.04"], keys: ["installScope", "mode"] },
10412
+ "forgezero.systemd": { capability: "service.manager", versions: ["ubuntu-26.04"], keys: ["installScope", "credentials", "serviceSandbox"] }
10413
+ };
10414
+ const contract = expected[requirement2.provider];
10415
+ if (!contract)
10416
+ throw new Error(`${where} uses unknown built-in provider ${requirement2.provider}`);
10417
+ if (requirement2.contract !== 1 || requirement2.capability !== contract.capability || !contract.versions.includes(requirement2.version))
10418
+ throw new Error(`${where} provider coordinate is unsupported`);
10419
+ exact2(row2, contract.keys, requirement2.provider);
10420
+ if (row2.installScope !== "host")
10421
+ throw new Error(`${where}.config.installScope must be host`);
10422
+ if (requirement2.provider === "forgezero.docker") {
10423
+ if (row2.storageDriver !== "overlay2" || row2.dataRoot !== "/var/lib/docker" || row2.liveRestore !== true || row2.logDriver !== "local" || row2.defaultNetwork !== "bridge" || row2.rootless !== false)
10424
+ throw new Error(`${where} Docker config is unsupported by the fixed host strategy`);
10425
+ }
10426
+ if (requirement2.provider === "forgezero.nginx" && (typeof row2.websocket !== "boolean" || !Number.isInteger(row2.maximumBodyMiB) || Number(row2.maximumBodyMiB) < 1 || Number(row2.maximumBodyMiB) > 1024 || !(row2.workerProcesses === "auto" || Number.isInteger(row2.workerProcesses)) || !Number.isInteger(row2.workerConnections)))
10427
+ throw new Error(`${where} Nginx config is invalid`);
10428
+ if (requirement2.provider === "forgezero.arangodb" && (row2.runtime !== "native" || !absolute(row2.dataPath) || row2.bind !== "private"))
10429
+ throw new Error(`${where} ArangoDB config is invalid`);
10430
+ }
10431
+
10432
+ // src/deploy-plan.ts
10433
+ var DEPLOY_PLAN_FORMAT = "forgezero-deployment-plan";
10434
+ var DEPLOY_PLAN_VERSION = 1;
10435
+
10436
+ class DeploymentPlanError extends Error {
10437
+ constructor(message) {
10438
+ super(message);
10439
+ this.name = "DeploymentPlanError";
10440
+ }
10441
+ }
10442
+ var NAME3 = /^[a-z][A-Za-z0-9-]{0,62}$/;
10443
+ var COORDINATE = /^[a-z][a-z0-9.-]{0,127}$/;
10444
+ var ACTION = /^[a-z][a-z0-9.-]{0,127}\/[a-z][a-z0-9.-]{0,127}@[1-9][0-9]{0,5}$/;
10445
+ var VERSION3 = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,63}$/;
10446
+ var ABSOLUTE_PATH = /^\/(?:[A-Za-z0-9._-]+\/?)*$/;
10447
+ var HEALTH_PATH = /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/;
10448
+ var fail2 = (where, message) => {
10449
+ throw new DeploymentPlanError(`${where} ${message}`);
10450
+ };
10451
+ var object = (value, where) => {
10452
+ if (!value || typeof value !== "object" || Array.isArray(value))
10453
+ fail2(where, "must be an object.");
10454
+ return value;
10455
+ };
10456
+ var exact3 = (value, allowed, where) => {
10457
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
10458
+ if (unknown.length)
10459
+ fail2(where, `contains unknown field(s): ${unknown.join(", ")}.`);
10460
+ };
10461
+ var named = (value, where) => {
10462
+ if (typeof value !== "string" || !NAME3.test(value))
10463
+ fail2(where, "must be a lowercase typed name.");
10464
+ return value;
10465
+ };
10466
+ var boundedString = (value, where, maximum = 256) => {
10467
+ if (typeof value !== "string" || value.length < 1 || value.length > maximum || value.includes("\x00"))
10468
+ fail2(where, `must be a non-empty string of at most ${maximum} characters.`);
10469
+ return value;
10470
+ };
10471
+ var integer = (value, where, minimum, maximum) => {
10472
+ if (!Number.isInteger(value) || value < minimum || value > maximum)
10473
+ fail2(where, `must be an integer from ${minimum} to ${maximum}.`);
10474
+ return value;
10475
+ };
10476
+ var number = (value, where, minimum, maximum) => {
10477
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum)
10478
+ fail2(where, `must be a finite number from ${minimum} to ${maximum}.`);
10479
+ return value;
10480
+ };
10481
+ function canonicalJson(value) {
10482
+ if (value === null || typeof value === "boolean" || typeof value === "string")
10483
+ return JSON.stringify(value);
10484
+ if (typeof value === "number") {
10485
+ if (!Number.isFinite(value))
10486
+ throw new DeploymentPlanError("deployment data contains a non-finite number.");
10487
+ return JSON.stringify(value);
10488
+ }
10489
+ if (Array.isArray(value))
10490
+ return `[${value.map(canonicalJson).join(",")}]`;
10491
+ if (value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
10492
+ return `{${Object.entries(value).filter(([, entry]) => entry !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
10493
+ }
10494
+ throw new DeploymentPlanError("deployment data must contain only plain JSON values.");
10495
+ }
10496
+ function deploymentSourceDigest(source) {
10497
+ return `sha256:${createHash2("sha256").update(canonicalJson(source)).digest("hex")}`;
10498
+ }
10499
+ function reference(value, where) {
10500
+ const row2 = object(value, where);
10501
+ exact3(row2, ["$ref"], where);
10502
+ const coordinate = boundedString(row2.$ref, `${where}.$ref`, 256);
10503
+ if (!/^(inputs\.[a-z][A-Za-z0-9-]{0,62}|steps\.[a-z][A-Za-z0-9-]{0,62}\.(status|outputs\.[a-z][A-Za-z0-9-]{0,62})|components\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|targets\.[a-z][A-Za-z0-9-]{0,62}\.[a-z][A-Za-z0-9-]{0,62}|deployment\.[a-z][A-Za-z0-9-]{0,62})$/.test(coordinate)) {
10504
+ fail2(where, "contains an unsupported reference.");
10505
+ }
10506
+ return { $ref: coordinate };
10507
+ }
10508
+ function operand(value, where) {
10509
+ if (value === null || ["string", "number", "boolean"].includes(typeof value)) {
10510
+ if (typeof value === "number" && !Number.isFinite(value))
10511
+ fail2(where, "must be finite.");
10512
+ return value;
10513
+ }
10514
+ return reference(value, where);
10515
+ }
10516
+ function condition(value, where, depth = 0) {
10517
+ if (depth > 16)
10518
+ fail2(where, "is nested too deeply.");
10519
+ const row2 = object(value, where);
10520
+ if (Object.keys(row2).length !== 1)
10521
+ fail2(where, "must contain exactly one operator.");
10522
+ const [operator, body] = Object.entries(row2)[0];
10523
+ if (operator === "all" || operator === "any") {
10524
+ if (!Array.isArray(body) || body.length < 1 || body.length > 32)
10525
+ fail2(where, `${operator} must contain 1 to 32 conditions.`);
10526
+ const entries = body;
10527
+ return { [operator]: entries.map((entry, index) => condition(entry, `${where}.${operator}[${index}]`, depth + 1)) };
10528
+ }
10529
+ if (operator === "not")
10530
+ return { not: condition(body, `${where}.not`, depth + 1) };
10531
+ if (["equals", "notEquals", "greaterThan", "greaterThanOrEqual", "lessThan", "lessThanOrEqual", "contains", "startsWith"].includes(operator)) {
10532
+ if (!Array.isArray(body) || body.length !== 2)
10533
+ fail2(where, `${operator} must contain two operands.`);
10534
+ const entries = body;
10535
+ return { [operator]: [operand(entries[0], `${where}.${operator}[0]`), operand(entries[1], `${where}.${operator}[1]`)] };
10536
+ }
10537
+ if (operator === "exists")
10538
+ return { exists: reference(body, `${where}.exists`) };
10539
+ if (["succeeded", "failed", "changed"].includes(operator))
10540
+ return { [operator]: named(body, `${where}.${operator}`) };
10541
+ return fail2(where, `uses unsupported operator ${operator}.`);
10542
+ }
10543
+ function jsonValue(value, where, depth = 0) {
10544
+ if (depth > 16)
10545
+ fail2(where, "is nested too deeply.");
10546
+ if (value === null || typeof value === "string" || typeof value === "boolean")
10547
+ return value;
10548
+ if (typeof value === "number") {
10549
+ if (!Number.isFinite(value))
10550
+ fail2(where, "must be finite.");
10551
+ return value;
10552
+ }
10553
+ if (Array.isArray(value)) {
10554
+ if (value.length > 1000)
10555
+ fail2(where, "contains more than 1000 entries.");
10556
+ return value.map((entry, index) => jsonValue(entry, `${where}[${index}]`, depth + 1));
10557
+ }
10558
+ const row2 = object(value, where);
10559
+ if (Object.keys(row2).length > 1000)
10560
+ fail2(where, "contains more than 1000 fields.");
10561
+ return Object.fromEntries(Object.entries(row2).map(([key, entry]) => [key, jsonValue(entry, `${where}.${key}`, depth + 1)]));
10562
+ }
10563
+ function validateInput(value, where) {
10564
+ const row2 = object(value, where);
10565
+ const type = row2.type;
10566
+ const common = ["type", "required", "default"];
10567
+ if (row2.required !== undefined && typeof row2.required !== "boolean")
10568
+ fail2(`${where}.required`, "must be a boolean.");
10569
+ if (type === "string") {
10570
+ exact3(row2, [...common, "minimumLength", "maximumLength", "pattern"], where);
10571
+ if (row2.default !== undefined)
10572
+ boundedString(row2.default, `${where}.default`, 16384);
10573
+ if (row2.minimumLength !== undefined)
10574
+ integer(row2.minimumLength, `${where}.minimumLength`, 0, 16384);
10575
+ if (row2.maximumLength !== undefined)
10576
+ integer(row2.maximumLength, `${where}.maximumLength`, 1, 16384);
10577
+ if (row2.pattern !== undefined) {
10578
+ const pattern = boundedString(row2.pattern, `${where}.pattern`, 512);
10579
+ try {
10580
+ new RegExp(pattern);
10581
+ } catch {
10582
+ fail2(`${where}.pattern`, "must be a valid regular expression.");
10583
+ }
10584
+ }
10585
+ } else if (type === "integer" || type === "number") {
10586
+ exact3(row2, [...common, "minimum", "maximum"], where);
10587
+ if (row2.default !== undefined)
10588
+ number(row2.default, `${where}.default`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
10589
+ if (row2.minimum !== undefined)
10590
+ number(row2.minimum, `${where}.minimum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
10591
+ if (row2.maximum !== undefined)
10592
+ number(row2.maximum, `${where}.maximum`, -Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
10593
+ if (type === "integer" && row2.default !== undefined && !Number.isInteger(row2.default))
10594
+ fail2(`${where}.default`, "must be an integer.");
10595
+ } else if (type === "boolean") {
10596
+ exact3(row2, common, where);
10597
+ if (row2.default !== undefined && typeof row2.default !== "boolean")
10598
+ fail2(`${where}.default`, "must be a boolean.");
10599
+ } else if (type === "enum") {
10600
+ exact3(row2, [...common, "values"], where);
10601
+ if (!Array.isArray(row2.values) || row2.values.length < 1 || row2.values.length > 64)
10602
+ fail2(`${where}.values`, "must contain 1 to 64 values.");
10603
+ const values = row2.values.map((entry, index) => boundedString(entry, `${where}.values[${index}]`, 128));
10604
+ if (new Set(values).size !== values.length)
10605
+ fail2(`${where}.values`, "must be unique.");
10606
+ if (row2.default !== undefined && !values.includes(row2.default))
10607
+ fail2(`${where}.default`, "must be an enum value.");
10608
+ } else if (type === "hostname") {
10609
+ exact3(row2, common, where);
10610
+ if (row2.default !== undefined && !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/i.test(String(row2.default)))
10611
+ fail2(`${where}.default`, "must be a hostname.");
10612
+ } else
10613
+ fail2(`${where}.type`, "is unsupported.");
10614
+ return row2;
10615
+ }
10616
+ function validateRequirement(value, where) {
10617
+ const row2 = object(value, where);
10618
+ exact3(row2, ["capability", "provider", "contract", "version", "config", "when"], where);
10619
+ if (!COORDINATE.test(String(row2.capability)))
10620
+ fail2(`${where}.capability`, "is invalid.");
10621
+ if (!COORDINATE.test(String(row2.provider)))
10622
+ fail2(`${where}.provider`, "is invalid.");
10623
+ integer(row2.contract, `${where}.contract`, 1, 65535);
10624
+ if (typeof row2.version !== "string" || !VERSION3.test(row2.version))
10625
+ fail2(`${where}.version`, "is invalid.");
10626
+ jsonValue(row2.config, `${where}.config`);
10627
+ if (row2.when !== undefined)
10628
+ condition(row2.when, `${where}.when`);
10629
+ const requirement2 = row2;
10630
+ validateBuiltInProvider(requirement2, where);
10631
+ return requirement2;
10632
+ }
10633
+ function validateArgv(value, where) {
10634
+ if (!Array.isArray(value) || value.length < 1 || value.length > 256)
10635
+ fail2(where, "must contain 1 to 256 arguments.");
10636
+ let bytes = 0;
10637
+ const values = value.map((entry, index) => {
10638
+ const result = boundedString(entry, `${where}[${index}]`, 16384);
10639
+ bytes += Buffer.byteLength(result);
10640
+ return result;
10641
+ });
10642
+ if (bytes > 64 * 1024)
10643
+ fail2(where, "is larger than 64 KiB.");
10644
+ const executable = values[0].split("/").at(-1).toLowerCase();
10645
+ if (["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"].includes(executable))
10646
+ fail2(`${where}[0]`, "may not invoke a command dispatcher.");
10647
+ return values;
10648
+ }
10649
+ function validateResources(value, where) {
10650
+ const row2 = object(value, where);
10651
+ exact3(row2, ["cpu", "memory", "pids", "io"], where);
10652
+ if (row2.cpu !== undefined) {
10653
+ const cpu = object(row2.cpu, `${where}.cpu`);
10654
+ exact3(cpu, ["limit", "weight", "pinning"], `${where}.cpu`);
10655
+ number(cpu.limit, `${where}.cpu.limit`, 0.01, 1024);
10656
+ if (cpu.weight !== undefined)
10657
+ integer(cpu.weight, `${where}.cpu.weight`, 1, 1e4);
10658
+ if (cpu.pinning !== undefined && !["automatic", "dedicated"].includes(String(cpu.pinning)))
10659
+ fail2(`${where}.cpu.pinning`, "is unsupported.");
10660
+ }
10661
+ if (row2.memory !== undefined) {
10662
+ const memory = object(row2.memory, `${where}.memory`);
10663
+ exact3(memory, ["limitMiB", "reservationMiB", "swap"], `${where}.memory`);
10664
+ integer(memory.limitMiB, `${where}.memory.limitMiB`, 16, 16777216);
10665
+ if (memory.reservationMiB !== undefined && integer(memory.reservationMiB, `${where}.memory.reservationMiB`, 1, memory.limitMiB) > memory.limitMiB)
10666
+ fail2(`${where}.memory.reservationMiB`, "must not exceed limitMiB.");
10667
+ if (memory.swap !== undefined && !["disabled", "bounded"].includes(String(memory.swap)))
10668
+ fail2(`${where}.memory.swap`, "is unsupported.");
10669
+ }
10670
+ if (row2.pids !== undefined) {
10671
+ const pids = object(row2.pids, `${where}.pids`);
10672
+ exact3(pids, ["limit"], `${where}.pids`);
10673
+ integer(pids.limit, `${where}.pids.limit`, 1, 1048576);
10674
+ }
10675
+ if (row2.io !== undefined) {
10676
+ const io = object(row2.io, `${where}.io`);
10677
+ exact3(io, ["weight", "readBps", "writeBps", "readIops", "writeIops"], `${where}.io`);
10678
+ for (const key of Object.keys(io))
10679
+ integer(io[key], `${where}.io.${key}`, 1, Number.MAX_SAFE_INTEGER);
10680
+ }
10681
+ }
10682
+ function validateStorage(value, where) {
10683
+ const row2 = object(value, where);
10684
+ if (row2.class === "ephemeral") {
10685
+ exact3(row2, ["class", "path", "type", "sizeMiB"], where);
10686
+ if (row2.type !== "tmpfs")
10687
+ fail2(`${where}.type`, "must be tmpfs.");
10688
+ integer(row2.sizeMiB, `${where}.sizeMiB`, 1, 1048576);
10689
+ } else if (row2.class === "persistent") {
10690
+ exact3(row2, ["class", "name", "path", "minimumFreePercent"], where);
10691
+ named(row2.name, `${where}.name`);
10692
+ if (row2.minimumFreePercent !== undefined)
10693
+ integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
10694
+ } else if (row2.class === "database") {
10695
+ exact3(row2, ["class", "name", "path", "minimumFreePercent", "minimumIops", "latencyTargetMs"], where);
10696
+ named(row2.name, `${where}.name`);
10697
+ integer(row2.minimumFreePercent, `${where}.minimumFreePercent`, 1, 95);
10698
+ if (row2.minimumIops !== undefined)
10699
+ integer(row2.minimumIops, `${where}.minimumIops`, 1, 1e8);
10700
+ if (row2.latencyTargetMs !== undefined)
10701
+ number(row2.latencyTargetMs, `${where}.latencyTargetMs`, 0.01, 60000);
10702
+ } else
10703
+ fail2(`${where}.class`, "is unsupported.");
10704
+ if (typeof row2.path !== "string" || !ABSOLUTE_PATH.test(row2.path) || row2.path.includes(".."))
10705
+ fail2(`${where}.path`, "must be a normalized absolute path.");
10706
+ }
10707
+ function validateNetwork(value, where) {
10708
+ const row2 = object(value, where);
10709
+ exact3(row2, ["ingress", "private", "public", "container"], where);
10710
+ if (row2.ingress !== undefined) {
10711
+ const ingress = object(row2.ingress, `${where}.ingress`);
10712
+ exact3(ingress, ["exposure", "stablePort"], `${where}.ingress`);
10713
+ if (!["loopback", "private", "public"].includes(String(ingress.exposure)))
10714
+ fail2(`${where}.ingress.exposure`, "is unsupported.");
10715
+ if (ingress.stablePort !== undefined)
10716
+ integer(ingress.stablePort, `${where}.ingress.stablePort`, 1, 65535);
10717
+ }
10718
+ if (row2.private !== undefined) {
10719
+ const privateNetwork = object(row2.private, `${where}.private`);
10720
+ exact3(privateNetwork, ["mode", "preferences"], `${where}.private`);
10721
+ if (!["private-lan", "cloudflare-warp", "auto"].includes(String(privateNetwork.mode)))
10722
+ fail2(`${where}.private.mode`, "is unsupported.");
10723
+ if (privateNetwork.preferences !== undefined && (!Array.isArray(privateNetwork.preferences) || privateNetwork.preferences.length < 1 || privateNetwork.preferences.length > 2 || privateNetwork.preferences.some((entry) => !["private-lan", "cloudflare-warp"].includes(String(entry))) || new Set(privateNetwork.preferences).size !== privateNetwork.preferences.length))
10724
+ fail2(`${where}.private.preferences`, "is invalid.");
10725
+ }
10726
+ if (row2.public !== undefined) {
10727
+ const publicNetwork = object(row2.public, `${where}.public`);
10728
+ exact3(publicNetwork, ["mode", "optional"], `${where}.public`);
10729
+ if (!["disabled", "cloudflare-tunnel"].includes(String(publicNetwork.mode)))
10730
+ fail2(`${where}.public.mode`, "is unsupported.");
10731
+ if (publicNetwork.optional !== undefined && typeof publicNetwork.optional !== "boolean")
10732
+ fail2(`${where}.public.optional`, "must be a boolean.");
10733
+ }
10734
+ if (row2.container !== undefined) {
10735
+ const container = object(row2.container, `${where}.container`);
10736
+ exact3(container, ["mode", "network"], `${where}.container`);
10737
+ if (!["bridge", "host"].includes(String(container.mode)))
10738
+ fail2(`${where}.container.mode`, "is unsupported.");
10739
+ if (container.network !== undefined)
10740
+ named(container.network, `${where}.container.network`);
10741
+ }
10742
+ }
10743
+ function validateComponent(value, where, targetNames, requirements) {
10744
+ const row2 = object(value, where);
10745
+ if (!targetNames.has(String(row2.target)))
10746
+ fail2(`${where}.target`, "must name an existing target.");
10747
+ if (row2.kind === "application") {
10748
+ exact3(row2, ["kind", "target", "runtime", "service", "resources", "storage", "network", "rollout"], where);
10749
+ const runtime = object(row2.runtime, `${where}.runtime`);
10750
+ const runtimeRequirement = requirements.get(String(runtime.requirement));
10751
+ if (!runtimeRequirement)
10752
+ fail2(`${where}.runtime.requirement`, "must name an existing requirement.");
10753
+ if (runtime.kind === "native") {
10754
+ exact3(runtime, ["kind", "provider", "requirement", "argv"], `${where}.runtime`);
10755
+ validateArgv(runtime.argv, `${where}.runtime.argv`);
10756
+ } else if (runtime.kind === "container") {
10757
+ exact3(runtime, ["kind", "provider", "requirement", "image", "entrypoint", "security"], `${where}.runtime`);
10758
+ const image = object(runtime.image, `${where}.runtime.image`);
10759
+ exact3(image, ["source"], `${where}.runtime.image`);
10760
+ const source = object(image.source, `${where}.runtime.image.source`);
10761
+ if (source.kind === "build") {
10762
+ exact3(source, ["kind", "context", "dockerfile"], `${where}.runtime.image.source`);
10763
+ boundedString(source.context, `${where}.runtime.image.source.context`, 512);
10764
+ boundedString(source.dockerfile, `${where}.runtime.image.source.dockerfile`, 512);
10765
+ } else if (source.kind === "registry") {
10766
+ exact3(source, ["kind", "reference"], `${where}.runtime.image.source`);
10767
+ if (!/^[-a-zA-Z0-9./:_@]+$/.test(boundedString(source.reference, `${where}.runtime.image.source.reference`, 512)))
10768
+ fail2(`${where}.runtime.image.source.reference`, "is invalid.");
10769
+ } else
10770
+ fail2(`${where}.runtime.image.source.kind`, "is unsupported.");
10771
+ if (runtime.entrypoint !== undefined)
10772
+ validateArgv(runtime.entrypoint, `${where}.runtime.entrypoint`);
10773
+ const security = object(runtime.security, `${where}.runtime.security`);
10774
+ exact3(security, ["privileged", "noNewPrivileges", "root", "dropCapabilities"], `${where}.runtime.security`);
10775
+ if (security.privileged !== false || security.noNewPrivileges !== true)
10776
+ fail2(`${where}.runtime.security`, "must disable privileged mode and enable noNewPrivileges.");
10777
+ if (!["read-only", "writable"].includes(String(security.root)))
10778
+ fail2(`${where}.runtime.security.root`, "is unsupported.");
10779
+ if (!Array.isArray(security.dropCapabilities) || !security.dropCapabilities.includes("ALL"))
10780
+ fail2(`${where}.runtime.security.dropCapabilities`, "must include ALL.");
10781
+ } else
10782
+ fail2(`${where}.runtime.kind`, "is unsupported.");
10783
+ if (!COORDINATE.test(String(runtime.provider)))
10784
+ fail2(`${where}.runtime.provider`, "is invalid.");
10785
+ if (runtimeRequirement && runtime.provider !== runtimeRequirement.provider)
10786
+ fail2(`${where}.runtime.provider`, "must match its requirement provider.");
10787
+ if (runtimeRequirement && runtime.kind === "container" && runtimeRequirement.capability !== "runtime.container")
10788
+ fail2(`${where}.runtime.requirement`, "must provide runtime.container.");
10789
+ if (runtimeRequirement && runtime.kind === "native" && !runtimeRequirement.capability.startsWith("runtime."))
10790
+ fail2(`${where}.runtime.requirement`, "must provide a runtime capability.");
10791
+ const service = object(row2.service, `${where}.service`);
10792
+ exact3(service, ["protocol", "port", "health", "websocket", "maximumConnections"], `${where}.service`);
10793
+ if (service.protocol !== "http")
10794
+ fail2(`${where}.service.protocol`, "must be http.");
10795
+ integer(service.port, `${where}.service.port`, 1, 65535);
10796
+ const health = object(service.health, `${where}.service.health`);
10797
+ exact3(health, ["protocol", "method", "path", "expectedStatus", "timeoutMs", "intervalMs", "attempts"], `${where}.service.health`);
10798
+ if (health.protocol !== "http" || !["GET", "HEAD"].includes(String(health.method)) || typeof health.path !== "string" || !HEALTH_PATH.test(health.path) || health.path.includes("..") || health.path.includes("//"))
10799
+ fail2(`${where}.service.health`, "is invalid.");
10800
+ if (!Array.isArray(health.expectedStatus) || health.expectedStatus.length < 1 || health.expectedStatus.length > 16 || health.expectedStatus.some((status) => !Number.isInteger(status) || status < 100 || status > 599))
10801
+ fail2(`${where}.service.health.expectedStatus`, "is invalid.");
10802
+ integer(health.timeoutMs, `${where}.service.health.timeoutMs`, 100, 300000);
10803
+ if (health.intervalMs !== undefined)
10804
+ integer(health.intervalMs, `${where}.service.health.intervalMs`, 100, 300000);
10805
+ if (health.attempts !== undefined)
10806
+ integer(health.attempts, `${where}.service.health.attempts`, 1, 1000);
10807
+ if (service.websocket !== undefined && typeof service.websocket !== "boolean")
10808
+ fail2(`${where}.service.websocket`, "must be a boolean.");
10809
+ if (service.maximumConnections !== undefined)
10810
+ integer(service.maximumConnections, `${where}.service.maximumConnections`, 1, 1e7);
10811
+ validateResources(row2.resources, `${where}.resources`);
10812
+ if (runtime.kind === "container") {
10813
+ const resources = object(row2.resources, `${where}.resources`);
10814
+ for (const required of ["cpu", "memory", "pids"])
10815
+ if (resources[required] === undefined)
10816
+ fail2(`${where}.resources.${required}`, "is required for a bounded container.");
10817
+ if (row2.network === undefined)
10818
+ fail2(`${where}.network`, "is required for a container.");
10819
+ const network = object(row2.network, `${where}.network`);
10820
+ if (network.container === undefined)
10821
+ fail2(`${where}.network.container`, "must select bridge or reviewed host networking.");
10822
+ }
10823
+ if (row2.storage !== undefined) {
10824
+ if (!Array.isArray(row2.storage) || row2.storage.length > 32)
10825
+ fail2(`${where}.storage`, "must contain at most 32 mounts.");
10826
+ row2.storage.forEach((entry, index) => validateStorage(entry, `${where}.storage[${index}]`));
10827
+ }
10828
+ if (row2.network !== undefined)
10829
+ validateNetwork(row2.network, `${where}.network`);
10830
+ const rollout = object(row2.rollout, `${where}.rollout`);
10831
+ exact3(rollout, ["strategy", "proxy", "drainMs", "automaticRollback"], `${where}.rollout`);
10832
+ if (!["direct", "blue-green", "rolling", "canary"].includes(String(rollout.strategy)))
10833
+ fail2(`${where}.rollout.strategy`, "is unsupported.");
10834
+ if (rollout.proxy !== undefined)
10835
+ named(rollout.proxy, `${where}.rollout.proxy`);
10836
+ if (rollout.strategy !== "direct") {
10837
+ const proxyRequirement = requirements.get(String(rollout.proxy));
10838
+ if (!proxyRequirement || proxyRequirement.capability !== "proxy.http")
10839
+ fail2(`${where}.rollout.proxy`, "must name a proxy.http requirement for managed rollout.");
10840
+ }
10841
+ if (rollout.drainMs !== undefined)
10842
+ integer(rollout.drainMs, `${where}.rollout.drainMs`, 0, 600000);
10843
+ if (rollout.automaticRollback !== undefined && typeof rollout.automaticRollback !== "boolean")
10844
+ fail2(`${where}.rollout.automaticRollback`, "must be a boolean.");
10845
+ } else if (row2.kind === "database") {
10846
+ exact3(row2, ["kind", "target", "runtime", "topology", "resources", "storage", "network"], where);
10847
+ const runtime = object(row2.runtime, `${where}.runtime`);
10848
+ exact3(runtime, ["provider", "requirement", "mode"], `${where}.runtime`);
10849
+ const databaseRequirement = requirements.get(String(runtime.requirement));
10850
+ if (!databaseRequirement)
10851
+ fail2(`${where}.runtime.requirement`, "must name an existing requirement.");
10852
+ if (databaseRequirement && (runtime.provider !== databaseRequirement.provider || databaseRequirement.capability !== "database.arangodb"))
10853
+ fail2(`${where}.runtime.requirement`, "must match a database.arangodb provider requirement.");
10854
+ if (databaseRequirement && databaseRequirement.config.runtime !== runtime.mode)
10855
+ fail2(`${where}.runtime.mode`, "must match the provider runtime mode.");
10856
+ if (runtime.mode !== "native")
10857
+ fail2(`${where}.runtime.mode`, "must be native in deployment plan v1.");
10858
+ const topology = object(row2.topology, `${where}.topology`);
10859
+ if (topology.mode === "standalone")
10860
+ exact3(topology, ["mode"], `${where}.topology`);
10861
+ else if (topology.mode === "cluster") {
10862
+ exact3(topology, ["mode", "bootstrapMembers", "replicationFactor", "writeConcern", "additionalJoiners"], `${where}.topology`);
10863
+ integer(topology.bootstrapMembers, `${where}.topology.bootstrapMembers`, 3, 9);
10864
+ integer(topology.replicationFactor, `${where}.topology.replicationFactor`, 1, 16);
10865
+ integer(topology.writeConcern, `${where}.topology.writeConcern`, 1, topology.replicationFactor);
10866
+ if (!["allowed", "disabled"].includes(String(topology.additionalJoiners)))
10867
+ fail2(`${where}.topology.additionalJoiners`, "is unsupported.");
10868
+ } else
10869
+ fail2(`${where}.topology.mode`, "is unsupported.");
10870
+ if (row2.resources !== undefined)
10871
+ validateResources(row2.resources, `${where}.resources`);
10872
+ validateStorage(row2.storage, `${where}.storage`);
10873
+ if (row2.storage.class !== "database")
10874
+ fail2(`${where}.storage.class`, "must be database.");
10875
+ validateNetwork(row2.network, `${where}.network`);
10876
+ } else
10877
+ fail2(`${where}.kind`, "is unsupported.");
10878
+ return row2;
10879
+ }
10880
+ function validateStep(value, where, targets, context) {
10881
+ const row2 = object(value, where);
10882
+ exact3(row2, ["uses", "with", "if", "scope", "timeoutMs", "retry", "failure", "compensate"], where);
10883
+ if (typeof row2.uses !== "string" || !ACTION.test(row2.uses))
10884
+ fail2(`${where}.uses`, "must be a versioned action coordinate.");
10885
+ jsonValue(row2.with, `${where}.with`);
10886
+ if (row2.if !== undefined)
10887
+ condition(row2.if, `${where}.if`);
10888
+ const scope = object(row2.scope, `${where}.scope`);
10889
+ if (scope.kind === "release-executor")
10890
+ exact3(scope, ["kind"], `${where}.scope`);
10891
+ else if (scope.kind === "elected-one") {
10892
+ exact3(scope, ["kind", "group"], `${where}.scope`);
10893
+ named(scope.group, `${where}.scope.group`);
10894
+ } else if (scope.kind === "each-target") {
10895
+ exact3(scope, ["kind", "target"], `${where}.scope`);
10896
+ if (!targets.has(String(scope.target)))
10897
+ fail2(`${where}.scope.target`, "must name an existing target.");
10898
+ } else if (scope.kind === "target-batches") {
10899
+ exact3(scope, ["kind", "target", "size"], `${where}.scope`);
10900
+ if (!targets.has(String(scope.target)))
10901
+ fail2(`${where}.scope.target`, "must name an existing target.");
10902
+ integer(scope.size, `${where}.scope.size`, 1, 32);
10903
+ } else
10904
+ fail2(`${where}.scope.kind`, "is unsupported.");
10905
+ if (row2.timeoutMs !== undefined)
10906
+ integer(row2.timeoutMs, `${where}.timeoutMs`, 1, 86400000);
10907
+ if (row2.retry !== undefined) {
10908
+ const retry = object(row2.retry, `${where}.retry`);
10909
+ exact3(retry, ["attempts", "backoff", "retryOn"], `${where}.retry`);
10910
+ integer(retry.attempts, `${where}.retry.attempts`, 1, 20);
10911
+ const backoff = object(retry.backoff, `${where}.retry.backoff`);
10912
+ exact3(backoff, ["kind", "initialMs", "maximumMs"], `${where}.retry.backoff`);
10913
+ if (!["fixed", "linear", "exponential"].includes(String(backoff.kind)))
10914
+ fail2(`${where}.retry.backoff.kind`, "is unsupported.");
10915
+ integer(backoff.initialMs, `${where}.retry.backoff.initialMs`, 0, 300000);
10916
+ integer(backoff.maximumMs, `${where}.retry.backoff.maximumMs`, backoff.initialMs, 3600000);
10917
+ if (retry.retryOn !== undefined && (!Array.isArray(retry.retryOn) || retry.retryOn.length > 8 || retry.retryOn.some((entry) => !["network", "timeout", "provider-unavailable", "rate-limited", "conflict"].includes(String(entry))) || new Set(retry.retryOn).size !== retry.retryOn.length))
10918
+ fail2(`${where}.retry.retryOn`, "is invalid.");
10919
+ }
10920
+ if (row2.failure !== undefined) {
10921
+ const failure = object(row2.failure, `${where}.failure`);
10922
+ exact3(failure, ["policy"], `${where}.failure`);
10923
+ if (!["continue", "stop", "rollback-stage", "rollback-deployment", "compensate", "manual-intervention"].includes(String(failure.policy)))
10924
+ fail2(`${where}.failure.policy`, "is unsupported.");
10925
+ }
10926
+ if (row2.compensate !== undefined) {
10927
+ const compensate = object(row2.compensate, `${where}.compensate`);
10928
+ exact3(compensate, ["uses", "with"], `${where}.compensate`);
10929
+ if (typeof compensate.uses !== "string" || !ACTION.test(compensate.uses))
10930
+ fail2(`${where}.compensate.uses`, "must be a versioned action coordinate.");
10931
+ jsonValue(compensate.with, `${where}.compensate.with`);
10932
+ if (row2.failure?.policy !== "compensate")
10933
+ fail2(`${where}.compensate`, "requires failure.policy compensate.");
10934
+ }
10935
+ const definition = row2;
10936
+ validateBuiltInAction(definition, context, where);
10937
+ if (definition.compensate)
10938
+ validateBuiltInAction({ uses: definition.compensate.uses, with: definition.compensate.with, scope: definition.scope }, context, `${where}.compensate`);
10939
+ return definition;
10940
+ }
10941
+ function topologicalStages(stages, workflow) {
10942
+ const names = Object.keys(stages);
10943
+ const nameSet = new Set(names);
10944
+ for (const name of names)
10945
+ for (const dependency of stages[name].dependsOn ?? []) {
10946
+ if (!nameSet.has(dependency))
10947
+ fail2(`workflows.${workflow}.stages.${name}.dependsOn`, `names missing stage ${dependency}.`);
10948
+ if (dependency === name)
10949
+ fail2(`workflows.${workflow}.stages.${name}.dependsOn`, "cannot depend on itself.");
10950
+ }
10951
+ const visiting = new Set;
10952
+ const visited = new Set;
10953
+ const ordered = [];
10954
+ const visit = (name) => {
10955
+ if (visiting.has(name))
10956
+ fail2(`workflows.${workflow}.stages`, "contains a dependency cycle.");
10957
+ if (visited.has(name))
10958
+ return;
10959
+ visiting.add(name);
10960
+ for (const dependency of stages[name].dependsOn ?? [])
10961
+ visit(dependency);
10962
+ visiting.delete(name);
10963
+ visited.add(name);
10964
+ ordered.push(name);
10965
+ };
10966
+ for (const name of names.sort())
10967
+ visit(name);
10968
+ return ordered;
10969
+ }
10970
+ function validateStage(value, where, targets, context) {
10971
+ const row2 = object(value, where);
10972
+ exact3(row2, ["dependsOn", "if", "strategy", "steps"], where);
10973
+ if (row2.dependsOn !== undefined && (!Array.isArray(row2.dependsOn) || row2.dependsOn.length > 64 || row2.dependsOn.some((entry) => typeof entry !== "string" || !NAME3.test(entry)) || new Set(row2.dependsOn).size !== row2.dependsOn.length))
10974
+ fail2(`${where}.dependsOn`, "is invalid.");
10975
+ if (row2.if !== undefined)
10976
+ condition(row2.if, `${where}.if`);
10977
+ const strategy = object(row2.strategy, `${where}.strategy`);
10978
+ exact3(strategy, ["mode", "maximumConcurrency", "batchSize", "minimumHealthy", "increments", "observationMs"], `${where}.strategy`);
10979
+ if (!["sequential", "parallel", "rolling", "blue-green", "canary"].includes(String(strategy.mode)))
10980
+ fail2(`${where}.strategy.mode`, "is unsupported.");
10981
+ if (strategy.maximumConcurrency !== undefined)
10982
+ integer(strategy.maximumConcurrency, `${where}.strategy.maximumConcurrency`, 1, 1024);
10983
+ if (strategy.batchSize !== undefined)
10984
+ integer(strategy.batchSize, `${where}.strategy.batchSize`, 1, 1024);
10985
+ if (strategy.minimumHealthy !== undefined)
10986
+ integer(strategy.minimumHealthy, `${where}.strategy.minimumHealthy`, 0, 1024);
10987
+ if (strategy.increments !== undefined && (!Array.isArray(strategy.increments) || strategy.increments.length < 1 || strategy.increments.length > 20 || strategy.increments.some((entry) => !Number.isInteger(entry) || entry < 1 || entry > 100) || strategy.increments.at(-1) !== 100))
10988
+ fail2(`${where}.strategy.increments`, "must end at 100 and contain bounded percentages.");
10989
+ if (strategy.observationMs !== undefined)
10990
+ integer(strategy.observationMs, `${where}.strategy.observationMs`, 1000, 86400000);
10991
+ if (strategy.mode === "canary" && strategy.increments === undefined)
10992
+ fail2(`${where}.strategy.increments`, "is required for canary mode.");
10993
+ if (strategy.mode === "rolling" && strategy.batchSize === undefined)
10994
+ fail2(`${where}.strategy.batchSize`, "is required for rolling mode.");
10995
+ const steps = object(row2.steps, `${where}.steps`);
10996
+ const entries = Object.entries(steps);
10997
+ if (entries.length < 1 || entries.length > 256)
10998
+ fail2(`${where}.steps`, "must contain 1 to 256 steps.");
10999
+ for (const [name, stepValue] of entries) {
11000
+ named(name, `${where}.steps key`);
11001
+ validateStep(stepValue, `${where}.steps.${name}`, targets, context);
11002
+ }
11003
+ return row2;
11004
+ }
11005
+ function compileDeployment(sourceValue) {
11006
+ const source = object(sourceValue, "deployment");
11007
+ exact3(source, ["apiVersion", "kind", "metadata", "spec"], "deployment");
11008
+ if (source.apiVersion !== "deploy.forgezero.net/v1")
11009
+ fail2("deployment.apiVersion", "must be deploy.forgezero.net/v1.");
11010
+ if (source.kind !== "Deployment")
11011
+ fail2("deployment.kind", "must be Deployment.");
11012
+ const metadata = object(source.metadata, "deployment.metadata");
11013
+ exact3(metadata, ["name", "description"], "deployment.metadata");
11014
+ const name = named(metadata.name, "deployment.metadata.name");
11015
+ if (metadata.description !== undefined)
11016
+ boundedString(metadata.description, "deployment.metadata.description", 1024);
11017
+ const spec = object(source.spec, "deployment.spec");
11018
+ exact3(spec, ["security", "inputs", "credentials", "targets", "requirements", "components", "workflows"], "deployment.spec");
11019
+ if (spec.security !== undefined) {
11020
+ const security = object(spec.security, "deployment.spec.security");
11021
+ exact3(security, ["attestation"], "deployment.spec.security");
11022
+ if (security.attestation !== undefined && !["required", "preferred", "disabled"].includes(String(security.attestation)))
11023
+ fail2("deployment.spec.security.attestation", "is unsupported.");
11024
+ }
11025
+ const inputs = object(spec.inputs ?? {}, "deployment.spec.inputs");
11026
+ if (Object.keys(inputs).length > 128)
11027
+ fail2("deployment.spec.inputs", "contains more than 128 inputs.");
11028
+ for (const [inputName, value] of Object.entries(inputs)) {
11029
+ named(inputName, "deployment.spec.inputs key");
11030
+ validateInput(value, `deployment.spec.inputs.${inputName}`);
11031
+ }
11032
+ const credentials = object(spec.credentials ?? {}, "deployment.spec.credentials");
11033
+ if (Object.keys(credentials).length > 128)
11034
+ fail2("deployment.spec.credentials", "contains more than 128 credentials.");
11035
+ for (const [credentialName, value] of Object.entries(credentials)) {
11036
+ named(credentialName, "deployment.spec.credentials key");
11037
+ const credential = object(value, `deployment.spec.credentials.${credentialName}`);
11038
+ exact3(credential, ["schema", "source"], `deployment.spec.credentials.${credentialName}`);
11039
+ if (!ACTION.test(String(credential.schema)))
11040
+ fail2(`deployment.spec.credentials.${credentialName}.schema`, "must be a versioned schema coordinate.");
11041
+ const credentialSource = object(credential.source, `deployment.spec.credentials.${credentialName}.source`);
11042
+ exact3(credentialSource, ["kind", "name", "fallback"], `deployment.spec.credentials.${credentialName}.source`);
11043
+ if (!["vault", "systemd"].includes(String(credentialSource.kind)))
11044
+ fail2(`deployment.spec.credentials.${credentialName}.source.kind`, "is unsupported.");
11045
+ named(credentialSource.name, `deployment.spec.credentials.${credentialName}.source.name`);
11046
+ if (credentialSource.fallback !== undefined && (credentialSource.kind !== "vault" || credentialSource.fallback !== "systemd"))
11047
+ fail2(`deployment.spec.credentials.${credentialName}.source.fallback`, "may only be systemd behind Vault.");
11048
+ }
11049
+ const targets = object(spec.targets, "deployment.spec.targets");
11050
+ const targetEntries = Object.entries(targets);
11051
+ if (targetEntries.length < 1 || targetEntries.length > 64)
11052
+ fail2("deployment.spec.targets", "must contain 1 to 64 targets.");
11053
+ const targetNames = new Set(targetEntries.map(([targetName]) => named(targetName, "deployment.spec.targets key")));
11054
+ for (const [targetName, value] of targetEntries) {
11055
+ const target = object(value, `deployment.spec.targets.${targetName}`);
11056
+ exact3(target, ["kind", "selector", "cardinality", "placement"], `deployment.spec.targets.${targetName}`);
11057
+ if (target.kind !== "compute")
11058
+ fail2(`deployment.spec.targets.${targetName}.kind`, "must be compute.");
11059
+ const selector = object(target.selector, `deployment.spec.targets.${targetName}.selector`);
11060
+ exact3(selector, ["profiles", "confidentialCompute", "labels"], `deployment.spec.targets.${targetName}.selector`);
11061
+ if (!Array.isArray(selector.profiles) || selector.profiles.length < 1 || selector.profiles.length > 32 || selector.profiles.some((profile) => typeof profile !== "string" || !NAME3.test(profile)) || new Set(selector.profiles).size !== selector.profiles.length)
11062
+ fail2(`deployment.spec.targets.${targetName}.selector.profiles`, "is invalid.");
11063
+ if (selector.confidentialCompute !== undefined && !["required", "preferred", "disabled"].includes(String(selector.confidentialCompute)))
11064
+ fail2(`deployment.spec.targets.${targetName}.selector.confidentialCompute`, "is unsupported.");
11065
+ if (selector.labels !== undefined)
11066
+ jsonValue(selector.labels, `deployment.spec.targets.${targetName}.selector.labels`);
11067
+ const cardinality = object(target.cardinality, `deployment.spec.targets.${targetName}.cardinality`);
11068
+ exact3(cardinality, ["minimum", "desired", "maximum"], `deployment.spec.targets.${targetName}.cardinality`);
11069
+ const minimum = integer(cardinality.minimum, `deployment.spec.targets.${targetName}.cardinality.minimum`, 1, 1024);
11070
+ const maximum = integer(cardinality.maximum, `deployment.spec.targets.${targetName}.cardinality.maximum`, minimum, 1024);
11071
+ if (typeof cardinality.desired === "number")
11072
+ integer(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`, minimum, maximum);
11073
+ else
11074
+ reference(cardinality.desired, `deployment.spec.targets.${targetName}.cardinality.desired`);
11075
+ if (target.placement !== undefined)
11076
+ jsonValue(target.placement, `deployment.spec.targets.${targetName}.placement`);
11077
+ }
11078
+ const requirements = object(spec.requirements ?? {}, "deployment.spec.requirements");
11079
+ if (Object.keys(requirements).length > 64)
11080
+ fail2("deployment.spec.requirements", "contains more than 64 requirements.");
11081
+ const requirementDefinitions = new Map;
11082
+ const requiredProviders = new Map;
11083
+ for (const [requirementName, value] of Object.entries(requirements)) {
11084
+ named(requirementName, "deployment.spec.requirements key");
11085
+ const requirementValue = validateRequirement(value, `deployment.spec.requirements.${requirementName}`);
11086
+ requirementDefinitions.set(requirementName, requirementValue);
11087
+ const key = `${requirementValue.provider}@${requirementValue.contract}:${requirementValue.capability}`;
11088
+ requiredProviders.set(key, { provider: requirementValue.provider, contract: requirementValue.contract, capability: requirementValue.capability });
11089
+ }
11090
+ const components = object(spec.components, "deployment.spec.components");
11091
+ const componentEntries = Object.entries(components);
11092
+ if (componentEntries.length < 1 || componentEntries.length > 128)
11093
+ fail2("deployment.spec.components", "must contain 1 to 128 components.");
11094
+ for (const [componentName, value] of componentEntries) {
11095
+ named(componentName, "deployment.spec.components key");
11096
+ validateComponent(value, `deployment.spec.components.${componentName}`, targetNames, requirementDefinitions);
11097
+ }
11098
+ const actionContext = {
11099
+ components,
11100
+ requirements,
11101
+ credentials
11102
+ };
11103
+ const workflows = object(spec.workflows, "deployment.spec.workflows");
11104
+ const workflowEntries = Object.entries(workflows);
11105
+ if (workflowEntries.length < 1 || workflowEntries.length > 32)
11106
+ fail2("deployment.spec.workflows", "must contain 1 to 32 workflows.");
11107
+ const plannedWorkflows = [];
11108
+ for (const [workflowName, value] of workflowEntries.sort(([left], [right]) => left.localeCompare(right))) {
11109
+ named(workflowName, "deployment.spec.workflows key");
11110
+ const workflowValue = object(value, `deployment.spec.workflows.${workflowName}`);
11111
+ exact3(workflowValue, ["concurrency", "stages"], `deployment.spec.workflows.${workflowName}`);
11112
+ if (workflowValue.concurrency !== undefined) {
11113
+ const concurrency = object(workflowValue.concurrency, `deployment.spec.workflows.${workflowName}.concurrency`);
11114
+ exact3(concurrency, ["group", "limit"], `deployment.spec.workflows.${workflowName}.concurrency`);
11115
+ boundedString(concurrency.group, `deployment.spec.workflows.${workflowName}.concurrency.group`, 256);
11116
+ integer(concurrency.limit, `deployment.spec.workflows.${workflowName}.concurrency.limit`, 1, 1024);
11117
+ }
11118
+ const stages = object(workflowValue.stages, `deployment.spec.workflows.${workflowName}.stages`);
11119
+ if (Object.keys(stages).length < 1 || Object.keys(stages).length > 64)
11120
+ fail2(`deployment.spec.workflows.${workflowName}.stages`, "must contain 1 to 64 stages.");
11121
+ for (const [stageName, stageValue] of Object.entries(stages)) {
11122
+ named(stageName, `deployment.spec.workflows.${workflowName}.stages key`);
11123
+ validateStage(stageValue, `deployment.spec.workflows.${workflowName}.stages.${stageName}`, targetNames, actionContext);
11124
+ }
11125
+ const stepIds = new Set;
11126
+ const plannedStages = topologicalStages(stages, workflowName).map((stageName) => {
11127
+ const stageValue = stages[stageName];
11128
+ const plannedSteps = Object.entries(stageValue.steps).map(([stepName, stepValue]) => {
11129
+ if (stepIds.has(stepName))
11130
+ fail2(`deployment.spec.workflows.${workflowName}`, `contains duplicate step id ${stepName}.`);
11131
+ stepIds.add(stepName);
11132
+ return { id: stepName, ...stepValue };
11133
+ });
11134
+ return { id: stageName, ...stageValue.dependsOn ? { dependsOn: [...stageValue.dependsOn].sort() } : {}, ...stageValue.if ? { if: stageValue.if } : {}, strategy: stageValue.strategy, steps: plannedSteps };
11135
+ });
11136
+ plannedWorkflows.push({ id: workflowName, ...workflowValue.concurrency ? { concurrency: workflowValue.concurrency } : {}, stages: plannedStages });
11137
+ }
11138
+ const typedSource = source;
11139
+ return {
11140
+ format: DEPLOY_PLAN_FORMAT,
11141
+ version: DEPLOY_PLAN_VERSION,
11142
+ sourceDigest: deploymentSourceDigest(typedSource),
11143
+ name,
11144
+ ...metadata.description ? { description: metadata.description } : {},
11145
+ requiredProviders: [...requiredProviders.values()].sort((left, right) => `${left.provider}:${left.contract}:${left.capability}`.localeCompare(`${right.provider}:${right.contract}:${right.capability}`)),
11146
+ spec: {
11147
+ ...spec.security ? { security: spec.security } : {},
11148
+ inputs,
11149
+ credentials,
11150
+ targets,
11151
+ requirements,
11152
+ components,
11153
+ workflows: plannedWorkflows
11154
+ }
11155
+ };
11156
+ }
11157
+ function deploymentPlanDigest(plan) {
11158
+ return `sha256:${createHash2("sha256").update(canonicalJson(plan)).digest("hex")}`;
11159
+ }
11160
+ function parseDeploymentPlan(value) {
11161
+ const row2 = object(value, "plan");
11162
+ exact3(row2, ["format", "version", "sourceDigest", "name", "description", "requiredProviders", "spec"], "plan");
11163
+ if (row2.format !== DEPLOY_PLAN_FORMAT || row2.version !== DEPLOY_PLAN_VERSION)
11164
+ fail2("plan", "uses an unsupported format or version.");
11165
+ if (typeof row2.sourceDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(row2.sourceDigest))
11166
+ fail2("plan.sourceDigest", "is invalid.");
11167
+ named(row2.name, "plan.name");
11168
+ if (row2.description !== undefined)
11169
+ boundedString(row2.description, "plan.description", 1024);
11170
+ if (!Array.isArray(row2.requiredProviders) || row2.requiredProviders.length > 64)
11171
+ fail2("plan.requiredProviders", "must be a bounded array.");
11172
+ for (const [index, provider] of row2.requiredProviders.entries()) {
11173
+ const item = object(provider, `plan.requiredProviders[${index}]`);
11174
+ exact3(item, ["provider", "contract", "capability"], `plan.requiredProviders[${index}]`);
11175
+ if (!COORDINATE.test(String(item.provider)) || !COORDINATE.test(String(item.capability)))
11176
+ fail2(`plan.requiredProviders[${index}]`, "contains an invalid coordinate.");
11177
+ integer(item.contract, `plan.requiredProviders[${index}].contract`, 1, 65535);
11178
+ }
11179
+ const spec = object(row2.spec, "plan.spec");
11180
+ const workflows = spec.workflows;
11181
+ if (!Array.isArray(workflows))
11182
+ fail2("plan.spec.workflows", "must be an array.");
11183
+ const sourceWorkflows = {};
11184
+ for (const [workflowIndex, workflow] of workflows.entries()) {
11185
+ const item = object(workflow, `plan.spec.workflows[${workflowIndex}]`);
11186
+ exact3(item, ["id", "concurrency", "stages"], `plan.spec.workflows[${workflowIndex}]`);
11187
+ const workflowId = named(item.id, `plan.spec.workflows[${workflowIndex}].id`);
11188
+ if (sourceWorkflows[workflowId])
11189
+ fail2("plan.spec.workflows", `contains duplicate workflow ${workflowId}.`);
11190
+ if (!Array.isArray(item.stages))
11191
+ fail2(`plan.spec.workflows[${workflowIndex}].stages`, "must be an array.");
11192
+ const sourceStages = {};
11193
+ for (const [stageIndex, stage] of item.stages.entries()) {
11194
+ const stageItem = object(stage, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
11195
+ exact3(stageItem, ["id", "dependsOn", "if", "strategy", "steps"], `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}]`);
11196
+ const stageId = named(stageItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].id`);
11197
+ if (sourceStages[stageId])
11198
+ fail2(`plan.spec.workflows[${workflowIndex}].stages`, `contains duplicate stage ${stageId}.`);
11199
+ if (!Array.isArray(stageItem.steps))
11200
+ fail2(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, "must be an array.");
11201
+ const sourceSteps = {};
11202
+ for (const [stepIndex, step3] of stageItem.steps.entries()) {
11203
+ const stepItem = object(step3, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}]`);
11204
+ const stepId = named(stepItem.id, `plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps[${stepIndex}].id`);
11205
+ if (sourceSteps[stepId])
11206
+ fail2(`plan.spec.workflows[${workflowIndex}].stages[${stageIndex}].steps`, `contains duplicate step ${stepId}.`);
11207
+ const { id: _id3, ...stepWithoutId } = stepItem;
11208
+ sourceSteps[stepId] = stepWithoutId;
11209
+ }
11210
+ const { id: _id2, steps: _steps, ...stageWithoutId } = stageItem;
11211
+ sourceStages[stageId] = { ...stageWithoutId, steps: sourceSteps };
11212
+ }
11213
+ const { id: _id, stages: _stages, ...workflowWithoutId } = item;
11214
+ sourceWorkflows[workflowId] = { ...workflowWithoutId, stages: sourceStages };
11215
+ }
11216
+ const { workflows: _workflows, ...specWithoutWorkflows } = spec;
11217
+ const reconstructed = {
11218
+ apiVersion: "deploy.forgezero.net/v1",
11219
+ kind: "Deployment",
11220
+ metadata: { name: row2.name, ...row2.description ? { description: row2.description } : {} },
11221
+ spec: { ...specWithoutWorkflows, workflows: sourceWorkflows }
11222
+ };
11223
+ const compiled = compileDeployment(reconstructed);
11224
+ return { ...compiled, sourceDigest: row2.sourceDigest };
11225
+ }
11226
+
11227
+ // src/deploy-compiler.ts
11228
+ var DEPLOY_SOURCE_FILE = "forgezero.deploy.ts";
11229
+ var DEPLOY_PLAN_FILE = ".fz/deploy.plan.json";
11230
+ function safeName2(value) {
11231
+ const result = value.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 63);
11232
+ return /^[a-z]/.test(result) ? result : `app-${result || "service"}`;
11233
+ }
11234
+ function defaultTypeScriptDeployment(name) {
11235
+ return `import { actions, application, defineDeployment, input, providers, stage, target, workflow } from '@forgezero/agent/deploy';
11236
+
11237
+ export default defineDeployment({
11238
+ apiVersion: 'deploy.forgezero.net/v1',
11239
+ kind: 'Deployment',
11240
+ metadata: { name: '${safeName2(name)}' },
11241
+ spec: {
11242
+ security: { attestation: 'preferred' },
11243
+ inputs: { replicas: input.integer({ minimum: 1, maximum: 32, default: 1 }) },
11244
+ targets: {
11245
+ app: target.compute({
11246
+ selector: { profiles: ['app'] },
11247
+ cardinality: { minimum: 1, desired: input.ref('replicas'), maximum: 32 }
11248
+ })
11249
+ },
11250
+ requirements: { bun: providers.bun.require() },
11251
+ components: {
11252
+ app: application({
11253
+ target: 'app',
11254
+ runtime: { kind: 'native', provider: 'forgezero.bun', requirement: 'bun', argv: ['/usr/local/bin/bun', 'run', 'start'] },
11255
+ service: { protocol: 'http', port: 3000, health: { protocol: 'http', method: 'GET', path: '/health', expectedStatus: [200], timeoutMs: 5_000 } },
11256
+ resources: {},
11257
+ rollout: { strategy: 'direct' }
11258
+ })
11259
+ },
11260
+ workflows: {
11261
+ deploy: workflow({
11262
+ stages: {
11263
+ build: stage({ strategy: { mode: 'sequential' }, steps: {
11264
+ build: actions.exec.argv(
11265
+ { component: 'app', argv: ['/usr/local/bin/fz-agent', 'pipeline-todo', 'replace with the project build argv'] },
11266
+ { scope: { kind: 'each-target', target: 'app' }, timeoutMs: 600_000 }
11267
+ )
11268
+ } }),
11269
+ release: stage({ dependsOn: ['build'], strategy: { mode: 'sequential' }, steps: {
11270
+ start: actions.exec.argv(
11271
+ { component: 'app', argv: ['/usr/local/bin/fz-agent', 'pipeline-todo', 'replace with the project release argv'] },
11272
+ { scope: { kind: 'each-target', target: 'app' }, timeoutMs: 120_000 }
11273
+ )
11274
+ } }),
11275
+ verify: stage({ dependsOn: ['release'], strategy: { mode: 'parallel' }, steps: {
11276
+ health: actions.service.health({ component: 'app' }, { scope: { kind: 'each-target', target: 'app' }, timeoutMs: 30_000 })
11277
+ } })
11278
+ }
11279
+ })
11280
+ }
11281
+ }
11282
+ });
11283
+ `;
11284
+ }
11285
+ function initializeTypeScriptDeployment(rootValue, options) {
11286
+ const path = localPath(rootValue, DEPLOY_SOURCE_FILE, "deployment source");
11287
+ if (existsSync6(path) && !options.force)
11288
+ throw new Error(`${DEPLOY_SOURCE_FILE} already exists; use --force only when replacing it deliberately`);
11289
+ writeFileSync6(path, defaultTypeScriptDeployment(options.name), { mode: 420, flag: options.force ? "w" : "wx" });
11290
+ return path;
11291
+ }
11292
+ function inside(root, path) {
11293
+ const nested = relative(root, path);
11294
+ return nested === "" || !nested.startsWith(`..${sep3}`) && nested !== ".." && !isAbsolute(nested);
11295
+ }
11296
+ function localPath(rootValue, value, label) {
11297
+ const root = resolve4(rootValue);
11298
+ const path = resolve4(root, value);
11299
+ if (!inside(root, path))
11300
+ throw new Error(`${label} must remain inside the project root`);
11301
+ return path;
11302
+ }
11303
+ async function loadDeploymentSource(root, sourceFile = DEPLOY_SOURCE_FILE) {
11304
+ const source = localPath(root, sourceFile, "deployment source");
11305
+ if (!existsSync6(source))
11306
+ throw new Error(`${sourceFile} does not exist`);
11307
+ const status = lstatSync2(source);
11308
+ if (!status.isFile() || status.isSymbolicLink() || status.size > 2 * 1024 * 1024)
11309
+ throw new Error("deployment source must be one bounded regular file");
11310
+ const module = await import(`${pathToFileURL(source).href}?forgezero=${status.mtimeMs}`);
11311
+ if (module.default === undefined)
11312
+ throw new Error(`${sourceFile} must export one default deployment definition`);
11313
+ return module.default;
11314
+ }
11315
+ async function compileDeploymentProject(rootValue, options = {}) {
11316
+ const root = resolve4(rootValue);
11317
+ const sourceFile = options.sourceFile ?? DEPLOY_SOURCE_FILE;
11318
+ const outputFile = options.outputFile ?? DEPLOY_PLAN_FILE;
11319
+ const source = localPath(root, sourceFile, "deployment source");
11320
+ const output = localPath(root, outputFile, "deployment output");
11321
+ const plan = compileDeployment(await loadDeploymentSource(root, sourceFile));
11322
+ const bytes = `${canonicalJson(plan)}
11323
+ `;
11324
+ parseDeploymentPlan(JSON.parse(bytes));
11325
+ const previous = existsSync6(output) ? readFileSync6(output, "utf8") : undefined;
11326
+ const changed = previous !== bytes;
11327
+ if (options.write !== false && changed) {
11328
+ mkdirSync6(dirname5(output), { recursive: true, mode: 493 });
11329
+ const temporary = `${output}.new-${process.pid}`;
11330
+ try {
11331
+ writeFileSync6(temporary, bytes, { mode: 420, flag: "wx" });
11332
+ renameSync5(temporary, output);
11333
+ } finally {
11334
+ rmSync4(temporary, { force: true });
11335
+ }
11336
+ }
11337
+ return { source, output, plan, digest: deploymentPlanDigest(plan), changed };
11338
+ }
11339
+ function inspectCompiledDeployment(rootValue, outputFile = DEPLOY_PLAN_FILE) {
11340
+ const path = localPath(rootValue, outputFile, "deployment output");
11341
+ if (!existsSync6(path))
11342
+ throw new Error(`${outputFile} does not exist; run \`fz deploy compile\``);
11343
+ const status = lstatSync2(path);
11344
+ if (!status.isFile() || status.isSymbolicLink() || status.size > 4194304)
11345
+ throw new Error("deployment plan must be one bounded regular file");
11346
+ const plan = parseDeploymentPlan(JSON.parse(readFileSync6(path, "utf8")));
11347
+ return { path, plan, digest: deploymentPlanDigest(plan) };
11348
+ }
11349
+
10265
11350
  // ../vault/dist/config.js
10266
11351
  class ConfigError extends Error {
10267
11352
  code;
@@ -10351,15 +11436,15 @@ function loadConfig(options = {}) {
10351
11436
  // src/cli/session-store.ts
10352
11437
  import {
10353
11438
  chmodSync as chmodSync2,
10354
- existsSync as existsSync5,
10355
- lstatSync as lstatSync2,
10356
- mkdirSync as mkdirSync5,
10357
- readFileSync as readFileSync5,
10358
- renameSync as renameSync4,
11439
+ existsSync as existsSync7,
11440
+ lstatSync as lstatSync3,
11441
+ mkdirSync as mkdirSync7,
11442
+ readFileSync as readFileSync7,
11443
+ renameSync as renameSync6,
10359
11444
  unlinkSync,
10360
- writeFileSync as writeFileSync5
11445
+ writeFileSync as writeFileSync7
10361
11446
  } from "fs";
10362
- import { dirname as dirname4, join as join5 } from "path";
11447
+ import { dirname as dirname6, join as join5 } from "path";
10363
11448
  import { homedir } from "os";
10364
11449
  var EMPTY2 = () => ({ version: 1, sessions: {} });
10365
11450
  function canonicalApi(value) {
@@ -10381,9 +11466,9 @@ function defaultSessionPath(env = process.env) {
10381
11466
  return join5(state, "forgezero", "sessions.json");
10382
11467
  }
10383
11468
  function assertPrivate(path, kind) {
10384
- if (!existsSync5(path))
11469
+ if (!existsSync7(path))
10385
11470
  return;
10386
- const stat = lstatSync2(path);
11471
+ const stat = lstatSync3(path);
10387
11472
  if (stat.isSymbolicLink())
10388
11473
  throw new Error(`Refusing symlinked CLI session ${kind}: ${path}`);
10389
11474
  if (kind === "directory" ? !stat.isDirectory() : !stat.isFile()) {
@@ -10399,13 +11484,13 @@ function assertPrivate(path, kind) {
10399
11484
  }
10400
11485
  }
10401
11486
  function parse(path) {
10402
- if (!existsSync5(path))
11487
+ if (!existsSync7(path))
10403
11488
  return EMPTY2();
10404
- assertPrivate(dirname4(path), "directory");
11489
+ assertPrivate(dirname6(path), "directory");
10405
11490
  assertPrivate(path, "file");
10406
11491
  let value;
10407
11492
  try {
10408
- value = JSON.parse(readFileSync5(path, "utf8"));
11493
+ value = JSON.parse(readFileSync7(path, "utf8"));
10409
11494
  } catch {
10410
11495
  throw new Error(`CLI session file is not valid JSON: ${path}`);
10411
11496
  }
@@ -10416,19 +11501,19 @@ function parse(path) {
10416
11501
  return raw;
10417
11502
  }
10418
11503
  function persist(path, value) {
10419
- const directory = dirname4(path);
10420
- if (!existsSync5(directory))
10421
- mkdirSync5(directory, { recursive: true, mode: 448 });
11504
+ const directory = dirname6(path);
11505
+ if (!existsSync7(directory))
11506
+ mkdirSync7(directory, { recursive: true, mode: 448 });
10422
11507
  assertPrivate(directory, "directory");
10423
11508
  if (process.platform !== "win32")
10424
11509
  chmodSync2(directory, 448);
10425
11510
  const temporary = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
10426
11511
  try {
10427
- writeFileSync5(temporary, `${JSON.stringify(value, null, 2)}
11512
+ writeFileSync7(temporary, `${JSON.stringify(value, null, 2)}
10428
11513
  `, { mode: 384, flag: "wx" });
10429
11514
  if (process.platform !== "win32")
10430
11515
  chmodSync2(temporary, 384);
10431
- renameSync4(temporary, path);
11516
+ renameSync6(temporary, path);
10432
11517
  } finally {
10433
11518
  try {
10434
11519
  unlinkSync(temporary);
@@ -10465,18 +11550,18 @@ function removeSession(api, realm, path = defaultSessionPath()) {
10465
11550
  }
10466
11551
 
10467
11552
  // src/bootstrap.ts
10468
- import { createHash as createHash2, createHmac, randomBytes as randomBytes7 } from "crypto";
11553
+ import { createHash as createHash3, createHmac, randomBytes as randomBytes7 } from "crypto";
10469
11554
  import {
10470
11555
  chmodSync as chmodSync3,
10471
- existsSync as existsSync6,
10472
- lstatSync as lstatSync3,
10473
- mkdirSync as mkdirSync6,
10474
- readFileSync as readFileSync6,
10475
- renameSync as renameSync5,
10476
- rmSync as rmSync3,
10477
- writeFileSync as writeFileSync6
11556
+ existsSync as existsSync8,
11557
+ lstatSync as lstatSync4,
11558
+ mkdirSync as mkdirSync8,
11559
+ readFileSync as readFileSync8,
11560
+ renameSync as renameSync7,
11561
+ rmSync as rmSync5,
11562
+ writeFileSync as writeFileSync8
10478
11563
  } from "fs";
10479
- import { dirname as dirname6 } from "path";
11564
+ import { dirname as dirname8 } from "path";
10480
11565
  import { fileURLToPath } from "url";
10481
11566
 
10482
11567
  // src/platform-bootstrap-runtime.ts
@@ -10854,7 +11939,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
10854
11939
  import { constants } from "fs";
10855
11940
  import { randomBytes as randomBytes6, randomUUID } from "crypto";
10856
11941
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
10857
- import { dirname as dirname5, join as join6, resolve as resolve3 } from "path";
11942
+ import { dirname as dirname7, join as join6, resolve as resolve5 } from "path";
10858
11943
  import { isIP as isIP3 } from "net";
10859
11944
 
10860
11945
  // src/cloudflare-edge.ts
@@ -11098,16 +12183,16 @@ async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
11098
12183
  throw new Error(`${path} has an invalid size`);
11099
12184
  }
11100
12185
  async function readOwnerOnlyFile(path, maximumBytes) {
11101
- const absolute = resolve3(path);
12186
+ const absolute2 = resolve5(path);
11102
12187
  let handle;
11103
12188
  try {
11104
- handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
11105
- await assertOwnerOnlyHandle(absolute, handle, maximumBytes);
12189
+ handle = await open(absolute2, constants.O_RDONLY | constants.O_NOFOLLOW);
12190
+ await assertOwnerOnlyHandle(absolute2, handle, maximumBytes);
11106
12191
  return await handle.readFile({ encoding: "utf8" });
11107
12192
  } catch (cause) {
11108
- if (cause instanceof Error && cause.message.startsWith(absolute))
12193
+ if (cause instanceof Error && cause.message.startsWith(absolute2))
11109
12194
  throw cause;
11110
- throw new Error(`cannot securely read owner-only file ${absolute}`);
12195
+ throw new Error(`cannot securely read owner-only file ${absolute2}`);
11111
12196
  } finally {
11112
12197
  await handle?.close();
11113
12198
  }
@@ -11115,7 +12200,7 @@ async function readOwnerOnlyFile(path, maximumBytes) {
11115
12200
  async function readOwnerApiToken(path) {
11116
12201
  const token = (await readOwnerOnlyFile(path, 4096)).trim();
11117
12202
  if (!TOKEN.test(token))
11118
- throw new Error(`${resolve3(path)} must contain exactly one Cloudflare API token`);
12203
+ throw new Error(`${resolve5(path)} must contain exactly one Cloudflare API token`);
11119
12204
  return token;
11120
12205
  }
11121
12206
  async function readCloudflareBootstrapTokens(files) {
@@ -11308,7 +12393,7 @@ function planCloudflareBootstrap(input, outputPath) {
11308
12393
  format: 1,
11309
12394
  kind: "forgezero-cloudflare-bootstrap-plan",
11310
12395
  mode: "attended-token-file",
11311
- outputFile: resolve3(outputPath),
12396
+ outputFile: resolve5(outputPath),
11312
12397
  coordinates,
11313
12398
  operations: [
11314
12399
  "prove CF_API_TOKEN can write, read and remove one namespaced nonce in the existing KV namespace",
@@ -11368,11 +12453,11 @@ async function readExistingOutput(path) {
11368
12453
  parsed = JSON.parse(await readOwnerOnlyFile(path, 1048576));
11369
12454
  } catch (cause) {
11370
12455
  if (cause instanceof SyntaxError)
11371
- throw new Error(`${resolve3(path)} is not valid bootstrap JSON`);
12456
+ throw new Error(`${resolve5(path)} is not valid bootstrap JSON`);
11372
12457
  throw cause;
11373
12458
  }
11374
12459
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
11375
- throw new Error(`${resolve3(path)} is not a ForgeZero Cloudflare bootstrap output`);
12460
+ throw new Error(`${resolve5(path)} is not a ForgeZero Cloudflare bootstrap output`);
11376
12461
  }
11377
12462
  const output = parsed;
11378
12463
  const unsupported = Object.keys(output).filter((key) => ![
@@ -11385,9 +12470,9 @@ async function readExistingOutput(path) {
11385
12470
  "created"
11386
12471
  ].includes(key));
11387
12472
  if (unsupported.length)
11388
- throw new Error(`${resolve3(path)} contains unsupported field ${unsupported[0]}`);
12473
+ throw new Error(`${resolve5(path)} contains unsupported field ${unsupported[0]}`);
11389
12474
  if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !["edge-resources-provisioned", "complete"].includes(String(output.phase)) || typeof output.updatedAt !== "string" || Number.isNaN(Date.parse(output.updatedAt)) || !output.resources || typeof output.resources !== "object" || Array.isArray(output.resources)) {
11390
- throw new Error(`${resolve3(path)} is not a ForgeZero Cloudflare bootstrap output`);
12475
+ throw new Error(`${resolve5(path)} is not a ForgeZero Cloudflare bootstrap output`);
11391
12476
  }
11392
12477
  const resources = output.resources;
11393
12478
  const unsupportedResource = Object.keys(resources).filter((key) => ![
@@ -11397,24 +12482,24 @@ async function readExistingOutput(path) {
11397
12482
  "nodes"
11398
12483
  ].includes(key));
11399
12484
  if (unsupportedResource.length) {
11400
- throw new Error(`${resolve3(path)} resources contain unsupported field ${unsupportedResource[0]}`);
12485
+ throw new Error(`${resolve5(path)} resources contain unsupported field ${unsupportedResource[0]}`);
11401
12486
  }
11402
12487
  const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
11403
12488
  if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
11404
- throw new Error(`${resolve3(path)} has malformed Cloudflare bootstrap resources`);
12489
+ throw new Error(`${resolve5(path)} has malformed Cloudflare bootstrap resources`);
11405
12490
  }
11406
12491
  if (coordinates.realtime) {
11407
12492
  const realtime = resources.realtime;
11408
12493
  if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
11409
- throw new Error(`${resolve3(path)} has malformed Cloudflare realtime resources`);
12494
+ throw new Error(`${resolve5(path)} has malformed Cloudflare realtime resources`);
11410
12495
  }
11411
12496
  } else if (resources.realtime !== undefined) {
11412
- throw new Error(`${resolve3(path)} contains undeclared Cloudflare realtime resources`);
12497
+ throw new Error(`${resolve5(path)} contains undeclared Cloudflare realtime resources`);
11413
12498
  }
11414
12499
  const seen = new Set;
11415
12500
  for (const item of resources.nodes) {
11416
12501
  if (!item || typeof item !== "object" || Array.isArray(item)) {
11417
- throw new Error(`${resolve3(path)} has a malformed Cloudflare node resource`);
12502
+ throw new Error(`${resolve5(path)} has a malformed Cloudflare node resource`);
11418
12503
  }
11419
12504
  const node = item;
11420
12505
  const unknownNode = Object.keys(node).filter((key) => ![
@@ -11428,24 +12513,24 @@ async function readExistingOutput(path) {
11428
12513
  ].includes(key));
11429
12514
  const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
11430
12515
  if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? "")) || !CONNECTOR_TOKEN.test(String(node.connectorToken ?? ""))) {
11431
- throw new Error(`${resolve3(path)} has a malformed or unbound Cloudflare node resource`);
12516
+ throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare node resource`);
11432
12517
  }
11433
12518
  if (expected.mesh) {
11434
12519
  const mesh = node.mesh;
11435
12520
  if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? "")) || !CONNECTOR_TOKEN.test(String(mesh.connectorToken ?? ""))) {
11436
- throw new Error(`${resolve3(path)} has a malformed or unbound Cloudflare Mesh resource`);
12521
+ throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare Mesh resource`);
11437
12522
  }
11438
12523
  } else if (node.mesh !== undefined) {
11439
- throw new Error(`${resolve3(path)} contains an undeclared Cloudflare Mesh resource`);
12524
+ throw new Error(`${resolve5(path)} contains an undeclared Cloudflare Mesh resource`);
11440
12525
  }
11441
12526
  seen.add(expected.nodeName);
11442
12527
  }
11443
12528
  if (output.phase === "complete" && resources.nodes.length !== coordinates.nodes.length) {
11444
- throw new Error(`${resolve3(path)} completed output does not cover the declared node fleet`);
12529
+ throw new Error(`${resolve5(path)} completed output does not cover the declared node fleet`);
11445
12530
  }
11446
12531
  if (output.created !== undefined) {
11447
12532
  if (!output.created || typeof output.created !== "object" || Array.isArray(output.created) || Object.keys(output.created).some((key) => key !== "nodes") || !Array.isArray(output.created.nodes) || output.created.nodes.some((node) => !node || typeof node !== "object" || Array.isArray(node) || Object.keys(node).some((key) => !["nodeName", "tunnel", "mesh"].includes(key)) || typeof node.nodeName !== "string" || typeof node.tunnel !== "boolean" || typeof node.mesh !== "boolean")) {
11448
- throw new Error(`${resolve3(path)} has malformed Cloudflare creation evidence`);
12533
+ throw new Error(`${resolve5(path)} has malformed Cloudflare creation evidence`);
11449
12534
  }
11450
12535
  }
11451
12536
  output.coordinates = coordinates;
@@ -11455,7 +12540,7 @@ function cloudflareHostHandoffPath(checkpointPath, nodeName) {
11455
12540
  const normalized = nodeName.trim().toLowerCase();
11456
12541
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalized))
11457
12542
  throw new Error("Cloudflare host handoff node name is invalid");
11458
- return join6(`${resolve3(checkpointPath)}.hosts`, `${normalized}.json`);
12543
+ return join6(`${resolve5(checkpointPath)}.hosts`, `${normalized}.json`);
11459
12544
  }
11460
12545
  async function readCloudflareHostHandoff(handoffPath, nodeName) {
11461
12546
  let parsed;
@@ -11540,7 +12625,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
11540
12625
  };
11541
12626
  }
11542
12627
  async function prepareOwnerOutputDirectory(absolutePath) {
11543
- const directory = dirname5(absolutePath);
12628
+ const directory = dirname7(absolutePath);
11544
12629
  await mkdir(directory, { recursive: true, mode: 448 });
11545
12630
  const metadata = await stat(directory);
11546
12631
  const uid = ownerUid();
@@ -11550,9 +12635,9 @@ async function prepareOwnerOutputDirectory(absolutePath) {
11550
12635
  return directory;
11551
12636
  }
11552
12637
  async function writeOwnerJson(path, output) {
11553
- const absolute = resolve3(path);
11554
- const directory = await prepareOwnerOutputDirectory(absolute);
11555
- const temporary = `${absolute}.${randomUUID()}.tmp`;
12638
+ const absolute2 = resolve5(path);
12639
+ const directory = await prepareOwnerOutputDirectory(absolute2);
12640
+ const temporary = `${absolute2}.${randomUUID()}.tmp`;
11556
12641
  let handle;
11557
12642
  try {
11558
12643
  handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
@@ -11561,8 +12646,8 @@ async function writeOwnerJson(path, output) {
11561
12646
  await handle.sync();
11562
12647
  await handle.close();
11563
12648
  handle = undefined;
11564
- await rename(temporary, absolute);
11565
- await chmod(absolute, 384);
12649
+ await rename(temporary, absolute2);
12650
+ await chmod(absolute2, 384);
11566
12651
  const directoryHandle = await open(directory, constants.O_RDONLY);
11567
12652
  try {
11568
12653
  await directoryHandle.sync();
@@ -11611,7 +12696,7 @@ var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(r
11611
12696
  var newRealtimeSecret = () => randomBytes6(48).toString("base64url");
11612
12697
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
11613
12698
  const coordinates = validateCloudflareBootstrapCoordinates(input);
11614
- const absoluteOutput = resolve3(outputPath);
12699
+ const absoluteOutput = resolve5(outputPath);
11615
12700
  const existing = await readExistingOutput(absoluteOutput);
11616
12701
  if (existing && !sameCoordinates(existing.coordinates, coordinates)) {
11617
12702
  throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
@@ -11806,8 +12891,8 @@ var acceptanceFetch = async (url, label, fetcher) => {
11806
12891
  return response.status;
11807
12892
  };
11808
12893
  async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fetch) {
11809
- const absolute = resolve3(checkpointPath);
11810
- const output = await readExistingOutput(absolute);
12894
+ const absolute2 = resolve5(checkpointPath);
12895
+ const output = await readExistingOutput(absolute2);
11811
12896
  if (!output || output.phase !== "complete")
11812
12897
  throw new Error("Cloudflare acceptance requires a completed owner checkpoint");
11813
12898
  const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
@@ -11828,13 +12913,13 @@ async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fet
11828
12913
  return {
11829
12914
  format: 1,
11830
12915
  kind: "forgezero-cloudflare-bootstrap-acceptance",
11831
- checkpointFile: absolute,
12916
+ checkpointFile: absolute2,
11832
12917
  verifiedAt: new Date().toISOString(),
11833
12918
  nodes
11834
12919
  };
11835
12920
  }
11836
12921
  async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
11837
- const directory = `${resolve3(checkpointPath)}.hosts`;
12922
+ const directory = `${resolve5(checkpointPath)}.hosts`;
11838
12923
  let entries;
11839
12924
  try {
11840
12925
  const metadata = await lstat(directory);
@@ -11856,11 +12941,11 @@ async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
11856
12941
  await unlink(join6(directory, name));
11857
12942
  await rmdir(directory);
11858
12943
  }
11859
- await unlink(resolve3(checkpointPath));
12944
+ await unlink(resolve5(checkpointPath));
11860
12945
  }
11861
12946
  async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
11862
- const checkpointPath = resolve3(request.checkpointPath);
11863
- const acceptancePath = resolve3(request.acceptancePath);
12947
+ const checkpointPath = resolve5(request.checkpointPath);
12948
+ const acceptancePath = resolve5(request.acceptancePath);
11864
12949
  if (acceptancePath === checkpointPath || acceptancePath.startsWith(`${checkpointPath}.hosts/`)) {
11865
12950
  throw new Error("Cloudflare acceptance evidence must be outside the secret checkpoint and handoff directory");
11866
12951
  }
@@ -12496,7 +13581,7 @@ function bootstrapIdentity(config) {
12496
13581
  };
12497
13582
  }
12498
13583
  function bootstrapIdentityDigest(config) {
12499
- return createHash2("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
13584
+ return createHash3("sha256").update(JSON.stringify(bootstrapIdentity(config))).digest("hex");
12500
13585
  }
12501
13586
  function parseStoredState(raw) {
12502
13587
  let value;
@@ -13130,11 +14215,11 @@ function strictBootstrapDocument(value) {
13130
14215
  return value;
13131
14216
  }
13132
14217
  function readBootstrapConfig(path) {
13133
- const metadata = lstatSync3(path);
14218
+ const metadata = lstatSync4(path);
13134
14219
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
13135
14220
  throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
13136
14221
  }
13137
- return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync6(path, "utf8"))));
14222
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync8(path, "utf8"))));
13138
14223
  }
13139
14224
  function localBootstrapHost() {
13140
14225
  const execute = async (argv2, options = {}) => {
@@ -13152,19 +14237,19 @@ function localBootstrapHost() {
13152
14237
  };
13153
14238
  return {
13154
14239
  uid: () => process.getuid?.() ?? -1,
13155
- exists: existsSync6,
13156
- read: (path) => readFileSync6(path, "utf8"),
14240
+ exists: existsSync8,
14241
+ read: (path) => readFileSync8(path, "utf8"),
13157
14242
  write(path, content, mode) {
13158
- mkdirSync6(dirname6(path), { recursive: true, mode: 493 });
14243
+ mkdirSync8(dirname8(path), { recursive: true, mode: 493 });
13159
14244
  const temporary = `${path}.next.${process.pid}`;
13160
- writeFileSync6(temporary, content, { mode });
14245
+ writeFileSync8(temporary, content, { mode });
13161
14246
  chmodSync3(temporary, mode);
13162
- renameSync5(temporary, path);
14247
+ renameSync7(temporary, path);
13163
14248
  },
13164
- mkdir: (path, mode) => mkdirSync6(path, { recursive: true, mode }),
13165
- remove: (path) => rmSync3(path, { force: true }),
14249
+ mkdir: (path, mode) => mkdirSync8(path, { recursive: true, mode }),
14250
+ remove: (path) => rmSync5(path, { force: true }),
13166
14251
  inspect(path) {
13167
- const value = lstatSync3(path);
14252
+ const value = lstatSync4(path);
13168
14253
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
13169
14254
  },
13170
14255
  exec: execute,
@@ -13186,7 +14271,7 @@ function localBootstrapHost() {
13186
14271
  async installAgent(config, enrolTokenSourcePath) {
13187
14272
  const capabilities = await readCapabilities(localRunner);
13188
14273
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
13189
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync6("/var/lib/forgezero/enrolment.json");
14274
+ const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync8("/var/lib/forgezero/enrolment.json");
13190
14275
  if (config.kind === "platform") {
13191
14276
  const lifecycle = config.database.role === "none" ? {
13192
14277
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -13198,8 +14283,8 @@ function localBootstrapHost() {
13198
14283
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
13199
14284
  databasePorts: [8529]
13200
14285
  };
13201
- mkdirSync6(dirname6(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
13202
- writeFileSync6(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
14286
+ mkdirSync8(dirname8(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
14287
+ writeFileSync8(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
13203
14288
  `, { mode: 256 });
13204
14289
  }
13205
14290
  const plan = planInstall({
@@ -13240,8 +14325,8 @@ function localBootstrapHost() {
13240
14325
  } : {}
13241
14326
  });
13242
14327
  for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
13243
- mkdirSync6(dirname6(unit.path), { recursive: true, mode: 493 });
13244
- writeFileSync6(unit.path, unit.unit, { mode: 420 });
14328
+ mkdirSync8(dirname8(unit.path), { recursive: true, mode: 493 });
14329
+ writeFileSync8(unit.path, unit.unit, { mode: 420 });
13245
14330
  }
13246
14331
  await applyPlan(plan, localRunner);
13247
14332
  return plan;
@@ -13250,8 +14335,8 @@ function localBootstrapHost() {
13250
14335
  }
13251
14336
 
13252
14337
  // src/cli/cloudflare-bootstrap.ts
13253
- import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync7 } from "fs";
13254
- import { dirname as dirname7, resolve as resolve4 } from "path";
14338
+ import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
14339
+ import { dirname as dirname9, resolve as resolve6 } from "path";
13255
14340
  var exactKeys3 = (value, allowed, label) => {
13256
14341
  const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
13257
14342
  if (unknown.length)
@@ -13263,22 +14348,22 @@ var record2 = (value, label) => {
13263
14348
  return value;
13264
14349
  };
13265
14350
  function readOwnerConfig(path) {
13266
- const absolute = resolve4(path);
14351
+ const absolute2 = resolve6(path);
13267
14352
  let descriptor;
13268
14353
  try {
13269
- descriptor = openSync(absolute, constants2.O_RDONLY | constants2.O_NOFOLLOW);
14354
+ descriptor = openSync(absolute2, constants2.O_RDONLY | constants2.O_NOFOLLOW);
13270
14355
  const metadata = fstatSync(descriptor);
13271
14356
  const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
13272
14357
  if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
13273
- throw new Error(`${absolute} must be one operator-owned 0600 regular file`);
14358
+ throw new Error(`${absolute2} must be one operator-owned 0600 regular file`);
13274
14359
  }
13275
- return JSON.parse(readFileSync7(descriptor, "utf8"));
14360
+ return JSON.parse(readFileSync9(descriptor, "utf8"));
13276
14361
  } catch (cause) {
13277
14362
  if (cause instanceof SyntaxError)
13278
- throw new Error(`${absolute} is not valid Cloudflare bootstrap JSON`);
13279
- if (cause instanceof Error && cause.message.startsWith(absolute))
14363
+ throw new Error(`${absolute2} is not valid Cloudflare bootstrap JSON`);
14364
+ if (cause instanceof Error && cause.message.startsWith(absolute2))
13280
14365
  throw cause;
13281
- throw new Error(`cannot securely read Cloudflare bootstrap config ${absolute}`);
14366
+ throw new Error(`cannot securely read Cloudflare bootstrap config ${absolute2}`);
13282
14367
  } finally {
13283
14368
  if (descriptor !== undefined)
13284
14369
  closeSync(descriptor);
@@ -13286,7 +14371,7 @@ function readOwnerConfig(path) {
13286
14371
  }
13287
14372
  function readCloudflareBootstrapCommandConfig(path, mode) {
13288
14373
  const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
13289
- const baseDirectory = dirname7(resolve4(path));
14374
+ const baseDirectory = dirname9(resolve6(path));
13290
14375
  exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates", "tokenFiles"], "Cloudflare bootstrap config");
13291
14376
  if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
13292
14377
  throw new Error("Cloudflare bootstrap config format/kind is invalid");
@@ -13334,8 +14419,8 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
13334
14419
  }
13335
14420
  }
13336
14421
  tokenFiles = {
13337
- tunnelTokenFile: resolve4(baseDirectory, source.tunnelTokenFile),
13338
- apiTokenFile: resolve4(baseDirectory, source.apiTokenFile)
14422
+ tunnelTokenFile: resolve6(baseDirectory, source.tunnelTokenFile),
14423
+ apiTokenFile: resolve6(baseDirectory, source.apiTokenFile)
13339
14424
  };
13340
14425
  }
13341
14426
  if (mode === "apply" && !tokenFiles) {
@@ -13344,7 +14429,7 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
13344
14429
  return {
13345
14430
  mode,
13346
14431
  coordinates,
13347
- checkpointPath: resolve4(baseDirectory, input.checkpointPath),
14432
+ checkpointPath: resolve6(baseDirectory, input.checkpointPath),
13348
14433
  ...tokenFiles ? { tokenFiles } : {}
13349
14434
  };
13350
14435
  }
@@ -13356,14 +14441,14 @@ async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {
13356
14441
  return evidence;
13357
14442
  }
13358
14443
  async function runCloudflareBootstrapVerificationCommand(checkpointPath, dependencies = {}) {
13359
- const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(resolve4(checkpointPath));
14444
+ const evidence = await (dependencies.verify ?? verifyCloudflareBootstrapAcceptance)(resolve6(checkpointPath));
13360
14445
  (dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
13361
14446
  `);
13362
14447
  return evidence;
13363
14448
  }
13364
14449
  function readCloudflareBootstrapFinalizeConfig(path) {
13365
14450
  const input = record2(readOwnerConfig(path), "Cloudflare bootstrap finalize config");
13366
- const baseDirectory = dirname7(resolve4(path));
14451
+ const baseDirectory = dirname9(resolve6(path));
13367
14452
  exactKeys3(input, ["format", "kind", "checkpointPath", "acceptancePath"], "Cloudflare bootstrap finalize config");
13368
14453
  if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-finalize") {
13369
14454
  throw new Error("Cloudflare bootstrap finalize config format/kind is invalid");
@@ -13374,8 +14459,8 @@ function readCloudflareBootstrapFinalizeConfig(path) {
13374
14459
  }
13375
14460
  }
13376
14461
  return {
13377
- checkpointPath: resolve4(baseDirectory, input.checkpointPath),
13378
- acceptancePath: resolve4(baseDirectory, input.acceptancePath)
14462
+ checkpointPath: resolve6(baseDirectory, input.checkpointPath),
14463
+ acceptancePath: resolve6(baseDirectory, input.acceptancePath)
13379
14464
  };
13380
14465
  }
13381
14466
  async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies = {}) {
@@ -13387,31 +14472,31 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
13387
14472
  }
13388
14473
 
13389
14474
  // src/metal-bootstrap.ts
13390
- import { createHash as createHash3, randomBytes as randomBytes8 } from "crypto";
14475
+ import { createHash as createHash4, randomBytes as randomBytes8 } from "crypto";
13391
14476
  import {
13392
14477
  chmodSync as chmodSync4,
13393
14478
  chownSync,
13394
14479
  copyFileSync as copyFileSync2,
13395
- existsSync as existsSync7,
13396
- lstatSync as lstatSync4,
13397
- mkdirSync as mkdirSync8,
13398
- readFileSync as readFileSync8,
13399
- realpathSync as realpathSync3,
13400
- renameSync as renameSync6,
14480
+ existsSync as existsSync9,
14481
+ lstatSync as lstatSync5,
14482
+ mkdirSync as mkdirSync10,
14483
+ readFileSync as readFileSync10,
14484
+ realpathSync as realpathSync4,
14485
+ renameSync as renameSync8,
13401
14486
  statSync,
13402
14487
  symlinkSync as symlinkSync2,
13403
14488
  unlinkSync as unlinkSync2,
13404
- writeFileSync as writeFileSync8
14489
+ writeFileSync as writeFileSync10
13405
14490
  } from "fs";
13406
- import { dirname as dirname9, isAbsolute as isAbsolute2, join as join9, resolve as resolve5 } from "path";
14491
+ import { dirname as dirname11, isAbsolute as isAbsolute3, join as join9, resolve as resolve7 } from "path";
13407
14492
  import { isIP as isIP5 } from "net";
13408
14493
 
13409
14494
  // src/metal-isolation.ts
13410
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync7 } from "fs";
14495
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync9 } from "fs";
13411
14496
  import { join as join8 } from "path";
13412
14497
 
13413
14498
  // src/metal-provision.ts
13414
- import { dirname as dirname8, isAbsolute, join as join7 } from "path";
14499
+ import { dirname as dirname10, isAbsolute as isAbsolute2, join as join7 } from "path";
13415
14500
  import { isIP as isIP4 } from "net";
13416
14501
 
13417
14502
  // src/ubuntu.ts
@@ -13460,7 +14545,7 @@ function validateMetalProfile(profile) {
13460
14545
  if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
13461
14546
  throw new MetalProvisionError("invalid guest address range");
13462
14547
  for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
13463
- if (!isAbsolute(path))
14548
+ if (!isAbsolute2(path))
13464
14549
  throw new MetalProvisionError("metal paths must be absolute");
13465
14550
  }
13466
14551
  new URL(profile.apiUrl);
@@ -13599,16 +14684,16 @@ async function applyMetalIsolation(profile, exec = defaultExec) {
13599
14684
  validateMetalProfile(profile);
13600
14685
  await requireGuestsInSlice(exec);
13601
14686
  const unitDir = profile.unitDir;
13602
- mkdirSync7(unitDir, { recursive: true });
13603
- writeFileSync7(join8(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
14687
+ mkdirSync9(unitDir, { recursive: true });
14688
+ writeFileSync9(join8(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
13604
14689
  for (const unit of ["system.slice", "user.slice"]) {
13605
14690
  const directory = join8(unitDir, `${unit}.d`);
13606
- mkdirSync7(directory, { recursive: true });
13607
- writeFileSync7(join8(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
14691
+ mkdirSync9(directory, { recursive: true });
14692
+ writeFileSync9(join8(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
13608
14693
  }
13609
14694
  const initDirectory = join8(unitDir, "init.scope.d");
13610
- mkdirSync7(initDirectory, { recursive: true });
13611
- writeFileSync7(join8(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
14695
+ mkdirSync9(initDirectory, { recursive: true });
14696
+ writeFileSync9(join8(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
13612
14697
  await checked3(exec, ["systemctl", "daemon-reload"]);
13613
14698
  await requireGuestsInSlice(exec);
13614
14699
  const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
@@ -13727,7 +14812,7 @@ function validateMetalBootstrapConfig(config) {
13727
14812
  throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
13728
14813
  }
13729
14814
  const image = config.profile.images[Object.keys(config.profile.images)[0]];
13730
- if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve5(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
14815
+ if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve7(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
13731
14816
  throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
13732
14817
  }
13733
14818
  if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunReleaseSha256 !== SUPPORTED_BUN_RELEASE_SHA256 || config.profile.agentVersion !== VERSION2) {
@@ -13762,11 +14847,11 @@ function validateMetalBootstrapConfig(config) {
13762
14847
  return config;
13763
14848
  }
13764
14849
  function validateOwnerOnlyPath(path, requireRootOwner) {
13765
- if (!isAbsolute2(path) || resolve5(path) !== path || path.includes("/../")) {
14850
+ if (!isAbsolute3(path) || resolve7(path) !== path || path.includes("/../")) {
13766
14851
  throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
13767
14852
  }
13768
- const metadata = lstatSync4(path);
13769
- if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync3(path) !== path) {
14853
+ const metadata = lstatSync5(path);
14854
+ if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync4(path) !== path) {
13770
14855
  throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
13771
14856
  }
13772
14857
  if ((metadata.mode & 63) !== 0)
@@ -13783,7 +14868,7 @@ function readMetalBootstrapConfig(path) {
13783
14868
  throw new MetalBootstrapError("metal bootstrap config size is invalid");
13784
14869
  let parsed;
13785
14870
  try {
13786
- parsed = JSON.parse(readFileSync8(path, "utf8"));
14871
+ parsed = JSON.parse(readFileSync10(path, "utf8"));
13787
14872
  } catch {
13788
14873
  throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
13789
14874
  }
@@ -13816,15 +14901,15 @@ function planMetalBootstrap(config) {
13816
14901
  };
13817
14902
  }
13818
14903
  var atomicWrite2 = (path, body, mode) => {
13819
- mkdirSync8(dirname9(path), { recursive: true, mode: 493 });
14904
+ mkdirSync10(dirname11(path), { recursive: true, mode: 493 });
13820
14905
  const temporary = `${path}.next-${process.pid}`;
13821
- writeFileSync8(temporary, body, { mode, flag: "wx" });
14906
+ writeFileSync10(temporary, body, { mode, flag: "wx" });
13822
14907
  chmodSync4(temporary, mode);
13823
14908
  chownSync(temporary, 0, 0);
13824
- renameSync6(temporary, path);
14909
+ renameSync8(temporary, path);
13825
14910
  };
13826
14911
  var validateAgentSourcePath = (source) => {
13827
- if (!isAbsolute2(source) || !lstatSync4(source).isFile() || lstatSync4(source).isSymbolicLink()) {
14912
+ if (!isAbsolute3(source) || !lstatSync5(source).isFile() || lstatSync5(source).isSymbolicLink()) {
13828
14913
  throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
13829
14914
  }
13830
14915
  };
@@ -13988,11 +15073,11 @@ WantedBy=multi-user.target
13988
15073
  var installAgentBinary = (source, version) => {
13989
15074
  validateAgentSourcePath(source);
13990
15075
  const release = `/opt/forgezero/agent/versions/${version}/dist`;
13991
- mkdirSync8(release, { recursive: true, mode: 493 });
15076
+ mkdirSync10(release, { recursive: true, mode: 493 });
13992
15077
  copyFileSync2(source, join9(release, "fz-agent.js"));
13993
15078
  chmodSync4(join9(release, "fz-agent.js"), 493);
13994
15079
  chownSync(join9(release, "fz-agent.js"), 0, 0);
13995
- mkdirSync8("/opt/forgezero/agent", { recursive: true, mode: 493 });
15080
+ mkdirSync10("/opt/forgezero/agent", { recursive: true, mode: 493 });
13996
15081
  for (const [link, target] of [
13997
15082
  ["/opt/forgezero/agent/current.next", `versions/${version}`],
13998
15083
  [AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
@@ -14002,7 +15087,7 @@ var installAgentBinary = (source, version) => {
14002
15087
  } catch {}
14003
15088
  symlinkSync2(target, link);
14004
15089
  if (link.endsWith("current.next"))
14005
- renameSync6(link, "/opt/forgezero/agent/current");
15090
+ renameSync8(link, "/opt/forgezero/agent/current");
14006
15091
  }
14007
15092
  };
14008
15093
  var preflight = async (config, exec) => {
@@ -14024,10 +15109,10 @@ var preflight = async (config, exec) => {
14024
15109
  ]);
14025
15110
  await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
14026
15111
  await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
14027
- if (!existsSync7("/dev/kvm"))
15112
+ if (!existsSync9("/dev/kvm"))
14028
15113
  throw new MetalBootstrapError("/dev/kvm is required");
14029
15114
  if (config.profile.confidential) {
14030
- if (!existsSync7("/dev/sev"))
15115
+ if (!existsSync9("/dev/sev"))
14031
15116
  throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
14032
15117
  await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
14033
15118
  }
@@ -14039,16 +15124,16 @@ var assertSupportedMetalHost = () => {
14039
15124
  if (process.platform !== "linux" || process.arch !== "x64") {
14040
15125
  throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
14041
15126
  }
14042
- const release = readFileSync8("/etc/os-release", "utf8");
15127
+ const release = readFileSync10("/etc/os-release", "utf8");
14043
15128
  if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
14044
15129
  throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
14045
15130
  }
14046
15131
  };
14047
15132
  var ensurePinnedGuestImage = async (config, exec) => {
14048
15133
  const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
14049
- if (existsSync7(image.path))
15134
+ if (existsSync9(image.path))
14050
15135
  return;
14051
- mkdirSync8(dirname9(image.path), { recursive: true, mode: 493 });
15136
+ mkdirSync10(dirname11(image.path), { recursive: true, mode: 493 });
14052
15137
  const temporary = `${image.path}.next-${process.pid}`;
14053
15138
  try {
14054
15139
  await runChecked(exec, [
@@ -14067,7 +15152,7 @@ var ensurePinnedGuestImage = async (config, exec) => {
14067
15152
  throw new MetalBootstrapError("downloaded guest image digest mismatch");
14068
15153
  chmodSync4(temporary, 292);
14069
15154
  chownSync(temporary, 0, 0);
14070
- renameSync6(temporary, image.path);
15155
+ renameSync8(temporary, image.path);
14071
15156
  } catch (cause) {
14072
15157
  try {
14073
15158
  unlinkSync2(temporary);
@@ -14080,11 +15165,11 @@ async function applyMetalBootstrap(config, options) {
14080
15165
  if ((options.getuid ?? process.getuid)?.() !== 0)
14081
15166
  throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
14082
15167
  assertSupportedMetalHost();
14083
- if (existsSync7(STATE_PATH2) && !options.repair)
15168
+ if (existsSync9(STATE_PATH2) && !options.repair)
14084
15169
  throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
14085
15170
  if (config.agentSeedFile)
14086
15171
  validateOwnerOnlyPath(config.agentSeedFile, true);
14087
- if (config.agentSeedFile && existsSync7(SEED_CREDENTIAL_PATH)) {
15172
+ if (config.agentSeedFile && existsSync9(SEED_CREDENTIAL_PATH)) {
14088
15173
  throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
14089
15174
  }
14090
15175
  validateAgentSourcePath(options.agentSourcePath);
@@ -14111,9 +15196,9 @@ async function applyMetalBootstrap(config, options) {
14111
15196
  await preflight(config, exec);
14112
15197
  await ensureAccount(exec);
14113
15198
  installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
14114
- mkdirSync8("/etc/forgezero/creds", { recursive: true, mode: 448 });
14115
- mkdirSync8(config.profile.stateDir, { recursive: true, mode: 448 });
14116
- mkdirSync8(config.profile.seedDir, { recursive: true, mode: 448 });
15199
+ mkdirSync10("/etc/forgezero/creds", { recursive: true, mode: 448 });
15200
+ mkdirSync10(config.profile.stateDir, { recursive: true, mode: 448 });
15201
+ mkdirSync10(config.profile.seedDir, { recursive: true, mode: 448 });
14117
15202
  const persistedProfile = {
14118
15203
  ...config.profile,
14119
15204
  metalHostname: config.metalHostname,
@@ -14122,8 +15207,8 @@ async function applyMetalBootstrap(config, options) {
14122
15207
  };
14123
15208
  atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
14124
15209
  `, 384);
14125
- if (!existsSync7(SEED_CREDENTIAL_PATH)) {
14126
- const seed = config.agentSeedFile ? readFileSync8(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
15210
+ if (!existsSync9(SEED_CREDENTIAL_PATH)) {
15211
+ const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes8(32).toString("base64url");
14127
15212
  if (seed.length < 32 || /[\0\r\n]/.test(seed))
14128
15213
  throw new MetalBootstrapError("metal Agent seed is invalid");
14129
15214
  await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
@@ -14177,7 +15262,7 @@ async function applyMetalBootstrap(config, options) {
14177
15262
  initializedAt: new Date().toISOString(),
14178
15263
  role: "metal",
14179
15264
  metalHostname: config.metalHostname,
14180
- profileSha256: createHash3("sha256").update(JSON.stringify(config.profile)).digest("hex")
15265
+ profileSha256: createHash4("sha256").update(JSON.stringify(config.profile)).digest("hex")
14181
15266
  };
14182
15267
  atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
14183
15268
  `, 384);
@@ -14185,7 +15270,7 @@ async function applyMetalBootstrap(config, options) {
14185
15270
  }
14186
15271
  var socketReady = (path) => {
14187
15272
  try {
14188
- return lstatSync4(path).isSocket();
15273
+ return lstatSync5(path).isSocket();
14189
15274
  } catch {
14190
15275
  return false;
14191
15276
  }
@@ -14194,17 +15279,17 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14194
15279
  const problems = [];
14195
15280
  let profileValid = false, imageVerified = false, metalHostname, profileSha256;
14196
15281
  let profileMode = null;
14197
- if (existsSync7(PROFILE_PATH)) {
15282
+ if (existsSync9(PROFILE_PATH)) {
14198
15283
  try {
14199
15284
  const metadata = statSync(PROFILE_PATH);
14200
15285
  profileMode = metadata.mode & 511;
14201
15286
  if (profileMode !== 384 || metadata.uid !== 0)
14202
15287
  problems.push("metal profile is not root-owned mode 0600");
14203
- const persisted = JSON.parse(readFileSync8(PROFILE_PATH, "utf8"));
15288
+ const persisted = JSON.parse(readFileSync10(PROFILE_PATH, "utf8"));
14204
15289
  const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
14205
15290
  validateMetalProfile(profile);
14206
15291
  profileValid = true;
14207
- profileSha256 = createHash3("sha256").update(JSON.stringify(profile)).digest("hex");
15292
+ profileSha256 = createHash4("sha256").update(JSON.stringify(profile)).digest("hex");
14208
15293
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
14209
15294
  problems.push("persisted metal host coordinates are invalid");
14210
15295
  }
@@ -14233,7 +15318,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14233
15318
  problems.push("local OTLP metrics receiver did not accept a proof request");
14234
15319
  }
14235
15320
  const image = profile.images[Object.keys(profile.images)[0]];
14236
- if (existsSync7(image.path)) {
15321
+ if (existsSync9(image.path)) {
14237
15322
  const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
14238
15323
  imageVerified = digest === image.sha256;
14239
15324
  }
@@ -14244,9 +15329,9 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14244
15329
  }
14245
15330
  } else
14246
15331
  problems.push("metal profile is missing");
14247
- if (existsSync7(STATE_PATH2)) {
15332
+ if (existsSync9(STATE_PATH2)) {
14248
15333
  try {
14249
- const state = JSON.parse(readFileSync8(STATE_PATH2, "utf8"));
15334
+ const state = JSON.parse(readFileSync10(STATE_PATH2, "utf8"));
14250
15335
  metalHostname = state.metalHostname;
14251
15336
  if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
14252
15337
  problems.push("metal initialized state does not bind the current profile");
@@ -14262,7 +15347,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14262
15347
  "forgezero-metal-agent-egress.service",
14263
15348
  "forgezero-metal-agent.service"
14264
15349
  ]) {
14265
- if (!existsSync7(join9(UNIT_DIRECTORY, unit)))
15350
+ if (!existsSync9(join9(UNIT_DIRECTORY, unit)))
14266
15351
  units[unit] = "missing";
14267
15352
  else
14268
15353
  units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
@@ -14276,7 +15361,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14276
15361
  if (!updateSocketReady)
14277
15362
  problems.push("Agent update helper socket is not ready");
14278
15363
  return {
14279
- initialized: existsSync7(STATE_PATH2),
15364
+ initialized: existsSync9(STATE_PATH2),
14280
15365
  profileValid,
14281
15366
  profileMode,
14282
15367
  imageVerified,
@@ -14289,8 +15374,8 @@ async function metalBootstrapStatus(exec = defaultExec2) {
14289
15374
  }
14290
15375
 
14291
15376
  // src/operator-bootstrap.ts
14292
- import { createHash as createHash4, randomBytes as randomBytes9 } from "crypto";
14293
- import { lstatSync as lstatSync5, mkdtempSync, readFileSync as readFileSync9, rmSync as rmSync4, writeFileSync as writeFileSync9 } from "fs";
15377
+ import { createHash as createHash5, randomBytes as randomBytes9 } from "crypto";
15378
+ import { lstatSync as lstatSync6, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
14294
15379
  import { isIP as isIP6 } from "net";
14295
15380
  import { tmpdir } from "os";
14296
15381
  import { basename as basename2, join as join10 } from "path";
@@ -14384,22 +15469,22 @@ var exactKeys5 = (value, keys, label) => {
14384
15469
  var ownerFile = (path, limit, label) => {
14385
15470
  if (!path.startsWith("/") || /[\r\n]/.test(path))
14386
15471
  throw new Error(`${label} path must be absolute`);
14387
- const metadata = lstatSync5(path);
15472
+ const metadata = lstatSync6(path);
14388
15473
  const uid = process.getuid?.() ?? metadata.uid;
14389
15474
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > limit) {
14390
15475
  throw new Error(`${label} must be an owner-only regular file with one link and at most ${limit} bytes`);
14391
15476
  }
14392
- return readFileSync9(path);
15477
+ return readFileSync11(path);
14393
15478
  };
14394
15479
  var publicIdentity = (path) => {
14395
15480
  if (!path.startsWith("/") || /[\r\n]/.test(path))
14396
15481
  throw new Error("SSH public-key path must be absolute");
14397
- const metadata = lstatSync5(path);
15482
+ const metadata = lstatSync6(path);
14398
15483
  const uid = process.getuid?.() ?? metadata.uid;
14399
15484
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== uid || metadata.nlink !== 1 || metadata.size > 16384) {
14400
15485
  throw new Error("SSH public key must be a caller-owned regular file with one link");
14401
15486
  }
14402
- const value = readFileSync9(path, "utf8").trim();
15487
+ const value = readFileSync11(path, "utf8").trim();
14403
15488
  if (!/^ssh-(?:ed25519|rsa) [A-Za-z0-9+/]+={0,3}(?: [^\r\n]+)?$/.test(value)) {
14404
15489
  throw new Error("SSH public key is malformed");
14405
15490
  }
@@ -14408,7 +15493,7 @@ var publicIdentity = (path) => {
14408
15493
  var socketPath = (path) => {
14409
15494
  if (!path.startsWith("/") || /[\r\n]/.test(path))
14410
15495
  throw new Error("SSH agent socket path must be absolute");
14411
- const metadata = lstatSync5(path);
15496
+ const metadata = lstatSync6(path);
14412
15497
  const uid = process.getuid?.() ?? metadata.uid;
14413
15498
  if (!metadata.isSocket() || metadata.isSymbolicLink() || metadata.uid !== uid) {
14414
15499
  throw new Error("SSH agent socket must be a caller-owned Unix socket");
@@ -14422,7 +15507,7 @@ var fingerprint = (key) => {
14422
15507
  if (!bytes.length || bytes.toString("base64").replace(/=+$/, "") !== encoded.replace(/=+$/, "")) {
14423
15508
  throw new Error("SSH host key is malformed");
14424
15509
  }
14425
- return `SHA256:${createHash4("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
15510
+ return `SHA256:${createHash5("sha256").update(bytes).digest("base64").replace(/=+$/, "")}`;
14426
15511
  };
14427
15512
  var validateHop = (value, label) => {
14428
15513
  const hop = exactKeys5(value, ["address", "port", "user", "hostKey", "hostKeySha256"], label);
@@ -14603,7 +15688,7 @@ function writeKnownHosts(request, directory) {
14603
15688
  if (request.target.jump)
14604
15689
  lines.push(`${hostLabel(request.target.jump.address, request.target.jump.port)} ${request.target.jump.hostKey}`);
14605
15690
  const path = join10(directory, "known_hosts");
14606
- writeFileSync9(path, `${lines.join(`
15691
+ writeFileSync11(path, `${lines.join(`
14607
15692
  `)}
14608
15693
  `, { mode: 384, flag: "wx" });
14609
15694
  return path;
@@ -14678,7 +15763,7 @@ function stageConfig(config, directory) {
14678
15763
  for (const [name, source] of secretSources(config)) {
14679
15764
  const bytes = ownerFile(source, SECRET_LIMIT, name);
14680
15765
  const local = join10(directory, name);
14681
- writeFileSync9(local, bytes, { mode: 384, flag: "wx" });
15766
+ writeFileSync11(local, bytes, { mode: 384, flag: "wx" });
14682
15767
  staged.push(name);
14683
15768
  const remotePath = `${REMOTE_STAGE}/${name}`;
14684
15769
  if (name === "cluster-code")
@@ -14693,7 +15778,7 @@ function stageConfig(config, directory) {
14693
15778
  rewritten.cloudflareHandoff.handoffFile = remotePath;
14694
15779
  }
14695
15780
  const path = join10(directory, "platform-config.json");
14696
- writeFileSync9(path, `${JSON.stringify(rewritten, null, 2)}
15781
+ writeFileSync11(path, `${JSON.stringify(rewritten, null, 2)}
14697
15782
  `, { mode: 384, flag: "wx" });
14698
15783
  return { path, files: staged };
14699
15784
  }
@@ -14702,7 +15787,7 @@ function stageMetalConfig(config, directory) {
14702
15787
  const files = [];
14703
15788
  if (config.agentSeedFile) {
14704
15789
  const name = "metal-agent-seed";
14705
- writeFileSync9(join10(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
15790
+ writeFileSync11(join10(directory, name), ownerFile(config.agentSeedFile, SECRET_LIMIT, name), {
14706
15791
  mode: 384,
14707
15792
  flag: "wx"
14708
15793
  });
@@ -14710,7 +15795,7 @@ function stageMetalConfig(config, directory) {
14710
15795
  files.push(name);
14711
15796
  }
14712
15797
  const path = join10(directory, "metal-config.json");
14713
- writeFileSync9(path, `${JSON.stringify(rewritten, null, 2)}
15798
+ writeFileSync11(path, `${JSON.stringify(rewritten, null, 2)}
14714
15799
  `, { mode: 384, flag: "wx" });
14715
15800
  return { path, files };
14716
15801
  }
@@ -14719,10 +15804,10 @@ async function verifiedBunArchive(directory, fetcher) {
14719
15804
  if (!response.ok)
14720
15805
  throw new Error("pinned Bun download failed");
14721
15806
  const bytes = new Uint8Array(await response.arrayBuffer());
14722
- if (createHash4("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
15807
+ if (createHash5("sha256").update(bytes).digest("hex") !== BUN_RELEASE_SHA256)
14723
15808
  throw new Error("pinned Bun checksum mismatch");
14724
15809
  const path = join10(directory, "bun.zip");
14725
- writeFileSync9(path, bytes, { mode: 384, flag: "wx" });
15810
+ writeFileSync11(path, bytes, { mode: 384, flag: "wx" });
14726
15811
  return path;
14727
15812
  }
14728
15813
  async function installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options) {
@@ -14732,7 +15817,7 @@ async function installPackagedAgent(request, knownHosts, exec, directory, remote
14732
15817
  [options.fzGitSshPath ?? fileURLToPath2(new URL("./fz-git-ssh.js", import.meta.url)), "fz-git-ssh.js"]
14733
15818
  ];
14734
15819
  for (const [artifact] of artifacts) {
14735
- if (!readFileSync9(artifact).length)
15820
+ if (!readFileSync11(artifact).length)
14736
15821
  throw new Error(`packaged artifact is empty: ${basename2(artifact)}`);
14737
15822
  }
14738
15823
  await remote(exec, request, knownHosts, ["/usr/bin/mkdir", "-m", "0700", remoteTemp], "remote staging");
@@ -14803,7 +15888,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
14803
15888
  return;
14804
15889
  });
14805
15890
  }
14806
- rmSync4(directory, { recursive: true, force: true });
15891
+ rmSync6(directory, { recursive: true, force: true });
14807
15892
  }
14808
15893
  }
14809
15894
  async function applyOperatorMetalBootstrap(request, mode, options = {}) {
@@ -14855,7 +15940,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
14855
15940
  return;
14856
15941
  });
14857
15942
  }
14858
- rmSync4(directory, { recursive: true, force: true });
15943
+ rmSync6(directory, { recursive: true, force: true });
14859
15944
  }
14860
15945
  }
14861
15946
 
@@ -14910,7 +15995,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
14910
15995
  }
14911
15996
 
14912
15997
  // src/host-maintenance.ts
14913
- import { lstatSync as lstatSync6, readFileSync as readFileSync10 } from "fs";
15998
+ import { lstatSync as lstatSync7, readFileSync as readFileSync12 } from "fs";
14914
15999
  var ROOT = "/opt/forgezero";
14915
16000
  var SLOT_FILE = `${ROOT}/.forge-slot`;
14916
16001
  var SHARED_ENV = `${ROOT}/shared/.env`;
@@ -14918,9 +16003,9 @@ var JWT = "/etc/forgezero/creds/arangodb-jwt.cred";
14918
16003
  var FZ = "/usr/local/bin/fz";
14919
16004
  var localRuntime = () => ({
14920
16005
  uid: () => process.getuid?.() ?? -1,
14921
- read: (path) => readFileSync10(path, "utf8"),
16006
+ read: (path) => readFileSync12(path, "utf8"),
14922
16007
  inspect(path) {
14923
- const value = lstatSync6(path);
16008
+ const value = lstatSync7(path);
14924
16009
  return {
14925
16010
  regular: value.isFile(),
14926
16011
  symbolic: value.isSymbolicLink(),
@@ -15041,8 +16126,8 @@ async function applyHostMaintenance(request, runtime = localRuntime()) {
15041
16126
  }
15042
16127
 
15043
16128
  // src/cli/maintenance.ts
15044
- import { existsSync as existsSync8, lstatSync as lstatSync7, readFileSync as readFileSync11, realpathSync as realpathSync4 } from "fs";
15045
- import { isAbsolute as isAbsolute3, join as join11, relative, resolve as resolve6 } from "path";
16129
+ import { existsSync as existsSync10, lstatSync as lstatSync8, readFileSync as readFileSync13, realpathSync as realpathSync5 } from "fs";
16130
+ import { isAbsolute as isAbsolute4, join as join11, relative as relative2, resolve as resolve8 } from "path";
15046
16131
  var API_OPERATION_ENTRYPOINTS = {
15047
16132
  "dev-reset": ["src", "server", "maintenance", "dev-reset.ts"],
15048
16133
  "db-backup": ["src", "server", "maintenance", "snapshot-backup.ts"],
@@ -15112,12 +16197,12 @@ function unsupportedRepositoryCliOption(argv2) {
15112
16197
  }
15113
16198
  function manifestName(root) {
15114
16199
  const manifestPath = join11(root, "package.json");
15115
- if (!existsSync8(manifestPath)) {
16200
+ if (!existsSync10(manifestPath)) {
15116
16201
  throw new Error(`No package.json exists at repository root ${root}.`);
15117
16202
  }
15118
16203
  let parsed;
15119
16204
  try {
15120
- parsed = JSON.parse(readFileSync11(manifestPath, "utf8"));
16205
+ parsed = JSON.parse(readFileSync13(manifestPath, "utf8"));
15121
16206
  } catch (cause) {
15122
16207
  throw new Error(`Cannot read ${manifestPath}: ${cause instanceof Error ? cause.message : String(cause)}`);
15123
16208
  }
@@ -15128,21 +16213,21 @@ function manifestName(root) {
15128
16213
  }
15129
16214
  function checkedEntrypoint(root, parts) {
15130
16215
  const candidate = join11(root, ...parts);
15131
- if (!existsSync8(candidate) || !lstatSync7(candidate).isFile()) {
16216
+ if (!existsSync10(candidate) || !lstatSync8(candidate).isFile()) {
15132
16217
  throw new Error(`The reviewed operation entrypoint is missing: ${candidate}`);
15133
16218
  }
15134
- if (lstatSync7(candidate).isSymbolicLink()) {
16219
+ if (lstatSync8(candidate).isSymbolicLink()) {
15135
16220
  throw new Error(`The reviewed operation entrypoint must not be a symbolic link: ${candidate}`);
15136
16221
  }
15137
- const actual = realpathSync4(candidate);
15138
- const within = relative(root, actual);
15139
- if (within.startsWith("..") || isAbsolute3(within)) {
16222
+ const actual = realpathSync5(candidate);
16223
+ const within = relative2(root, actual);
16224
+ if (within.startsWith("..") || isAbsolute4(within)) {
15140
16225
  throw new Error(`The reviewed operation entrypoint escapes repository root ${root}.`);
15141
16226
  }
15142
16227
  return actual;
15143
16228
  }
15144
16229
  function resolveRepositoryOperation(operation, requestedRoot) {
15145
- const root = realpathSync4(resolve6(requestedRoot));
16230
+ const root = realpathSync5(resolve8(requestedRoot));
15146
16231
  const name = manifestName(root);
15147
16232
  if (operation in API_OPERATION_ENTRYPOINTS) {
15148
16233
  const parts = API_OPERATION_ENTRYPOINTS[operation];
@@ -15155,7 +16240,7 @@ function resolveRepositoryOperation(operation, requestedRoot) {
15155
16240
  };
15156
16241
  }
15157
16242
  if (name === "forgezero") {
15158
- const apiRoot = realpathSync4(join11(root, "api"));
16243
+ const apiRoot = realpathSync5(join11(root, "api"));
15159
16244
  if (manifestName(apiRoot) !== "@forgezero/api") {
15160
16245
  throw new Error(`${apiRoot} is not the ForgeZero API package.`);
15161
16246
  }
@@ -15443,7 +16528,7 @@ async function api(options, path, init) {
15443
16528
  return { status: response.status, body };
15444
16529
  }
15445
16530
  function sleep(ms) {
15446
- return new Promise((resolve8) => setTimeout(resolve8, ms));
16531
+ return new Promise((resolve10) => setTimeout(resolve10, ms));
15447
16532
  }
15448
16533
  function browserCommand(url) {
15449
16534
  if (process.platform === "darwin")
@@ -15528,7 +16613,7 @@ function requireSession() {
15528
16613
  function readOwnerOnlySecret(path, label) {
15529
16614
  let metadata;
15530
16615
  try {
15531
- metadata = lstatSync8(path);
16616
+ metadata = lstatSync9(path);
15532
16617
  } catch {
15533
16618
  throw new Error(`The ${label} file ${path} is unreadable.`);
15534
16619
  }
@@ -15542,7 +16627,7 @@ function readOwnerOnlySecret(path, label) {
15542
16627
  if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
15543
16628
  throw new Error(`The ${label} file ${path} is not owned by the current user.`);
15544
16629
  }
15545
- const value = readFileSync12(path, "utf8").trim();
16630
+ const value = readFileSync14(path, "utf8").trim();
15546
16631
  if (!value)
15547
16632
  throw new Error(`The ${label} file ${path} is empty.`);
15548
16633
  return value;
@@ -15666,13 +16751,13 @@ async function cmdRoutes(options, search) {
15666
16751
  throw new Error("This API does not expose the CLI action catalog yet. Update the ForgeZero API first.");
15667
16752
  }
15668
16753
  const needle = search?.trim().toLowerCase();
15669
- const actions = needle ? body.actions.filter((action) => `${action.method} ${action.path} ${action.label} ${action.page}`.toLowerCase().includes(needle)) : body.actions;
16754
+ const actions2 = needle ? body.actions.filter((action) => `${action.method} ${action.path} ${action.label} ${action.page}`.toLowerCase().includes(needle)) : body.actions;
15670
16755
  if (options.json) {
15671
- out.line(JSON.stringify({ api: options.api, realm: options.realm, actions }, null, 2));
16756
+ out.line(JSON.stringify({ api: options.api, realm: options.realm, actions: actions2 }, null, 2));
15672
16757
  return 0;
15673
16758
  }
15674
16759
  out.line(`Authorized UI actions for ${options.realm}${needle ? ` matching "${search}"` : ""}:`);
15675
- for (const action of actions) {
16760
+ for (const action of actions2) {
15676
16761
  out.line(` ${action.method.padEnd(6)} ${action.path}${action.freshProof ? " [fresh proof]" : ""}`);
15677
16762
  out.step(`${action.label} \xB7 page ${action.page}`);
15678
16763
  }
@@ -15723,7 +16808,7 @@ async function cmdApi(options, args) {
15723
16808
  let body = undefined;
15724
16809
  if (options.data !== undefined && options.dataFile)
15725
16810
  throw new Error("Use only one of --data or --data-file.");
15726
- const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync12(options.dataFile, "utf8") : options.data;
16811
+ const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync14(options.dataFile, "utf8") : options.data;
15727
16812
  if (encoded !== undefined)
15728
16813
  body = JSON.parse(encoded);
15729
16814
  const result = await api(options, `${url.pathname}${url.search}`, { method, body });
@@ -15857,22 +16942,22 @@ async function cmdAgent(options, args) {
15857
16942
  out.line(auxiliary.unit);
15858
16943
  }
15859
16944
  out.line(" --- then ---");
15860
- for (const step2 of plan.steps)
15861
- out.step(`${step2.command}`);
16945
+ for (const step3 of plan.steps)
16946
+ out.step(`${step3.command}`);
15862
16947
  return 0;
15863
16948
  }
15864
16949
  try {
15865
- writeFileSync10(plan.unitPath, plan.unit, { mode: 420 });
16950
+ writeFileSync12(plan.unitPath, plan.unit, { mode: 420 });
15866
16951
  out.ok(`Wrote ${plan.unitPath}`);
15867
16952
  for (const auxiliary of plan.auxiliaryUnits) {
15868
- mkdirSync9(dirname10(auxiliary.path), { recursive: true, mode: 493 });
15869
- writeFileSync10(auxiliary.path, auxiliary.unit, { mode: 420 });
16953
+ mkdirSync11(dirname12(auxiliary.path), { recursive: true, mode: 493 });
16954
+ writeFileSync12(auxiliary.path, auxiliary.unit, { mode: 420 });
15870
16955
  out.ok(`Wrote ${auxiliary.path}`);
15871
16956
  }
15872
16957
  if (options.enrol) {
15873
- if (existsSync9(enrolTokenSourcePath)) {
16958
+ if (existsSync11(enrolTokenSourcePath)) {
15874
16959
  const source = statSync2(enrolTokenSourcePath);
15875
- const token = readFileSync12(enrolTokenSourcePath, "utf8").trim();
16960
+ const token = readFileSync14(enrolTokenSourcePath, "utf8").trim();
15876
16961
  if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
15877
16962
  throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
15878
16963
  }
@@ -15885,18 +16970,18 @@ async function cmdAgent(options, args) {
15885
16970
  if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
15886
16971
  throw new Error("A valid fze_ enrolment token was not provided.");
15887
16972
  }
15888
- writeFileSync10(enrolTokenSourcePath, `${token}
16973
+ writeFileSync12(enrolTokenSourcePath, `${token}
15889
16974
  `, { mode: 384, flag: "wx" });
15890
16975
  }
15891
16976
  }
15892
16977
  const transcript = await applyPlan(plan, localRunner);
15893
- for (const step2 of transcript)
15894
- out.ok(step2.label);
16978
+ for (const step3 of transcript)
16979
+ out.ok(step3.label);
15895
16980
  out.ok("Agent service and socket verified");
15896
16981
  out.line();
15897
16982
  out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
15898
16983
  out.line();
15899
- out.line(` ${readFileSync12(gitPublicKeyPath, "utf8").trim()}`);
16984
+ out.line(` ${readFileSync14(gitPublicKeyPath, "utf8").trim()}`);
15900
16985
  out.line();
15901
16986
  return 0;
15902
16987
  } catch (cause) {
@@ -15985,19 +17070,19 @@ function interactiveMetalBootstrap() {
15985
17070
  });
15986
17071
  }
15987
17072
  function writeBootstrapConfig(path, config) {
15988
- if (!isAbsolute4(path) || resolve7(path) !== path)
17073
+ if (!isAbsolute5(path) || resolve9(path) !== path)
15989
17074
  throw new Error("--output must be a canonical absolute path");
15990
- mkdirSync9(dirname10(path), { recursive: true, mode: 448 });
15991
- writeFileSync10(path, `${JSON.stringify(config, null, 2)}
17075
+ mkdirSync11(dirname12(path), { recursive: true, mode: 448 });
17076
+ writeFileSync12(path, `${JSON.stringify(config, null, 2)}
15992
17077
  `, { mode: 384, flag: "wx" });
15993
17078
  return path;
15994
17079
  }
15995
17080
  function genesisOutputDirectory(path) {
15996
- if (!isAbsolute4(path) || resolve7(path) !== path)
17081
+ if (!isAbsolute5(path) || resolve9(path) !== path)
15997
17082
  throw new Error("--output must be a canonical absolute directory");
15998
- if (!existsSync9(path))
15999
- mkdirSync9(path, { recursive: true, mode: 448 });
16000
- const metadata = lstatSync8(path);
17083
+ if (!existsSync11(path))
17084
+ mkdirSync11(path, { recursive: true, mode: 448 });
17085
+ const metadata = lstatSync9(path);
16001
17086
  const uid = process.getuid?.();
16002
17087
  if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
16003
17088
  throw new Error("platform genesis output must be an owner-only directory owned by the current user");
@@ -16006,8 +17091,8 @@ function genesisOutputDirectory(path) {
16006
17091
  }
16007
17092
  function ensureGenesisClusterSecret(directory) {
16008
17093
  const path = `${directory}/cluster-bootstrap.code`;
16009
- if (!existsSync9(path))
16010
- writeFileSync10(path, `${randomBytes10(32).toString("hex")}
17094
+ if (!existsSync11(path))
17095
+ writeFileSync12(path, `${randomBytes10(32).toString("hex")}
16011
17096
  `, { mode: 384, flag: "wx" });
16012
17097
  if (!/^[a-f0-9]{64}$/i.test(readOwnerOnlySecret(path, "cluster bootstrap code"))) {
16013
17098
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
@@ -16264,8 +17349,8 @@ async function cmdBootstrap(options, args) {
16264
17349
  return 0;
16265
17350
  }
16266
17351
  const installedBootstrapKind = () => resolveInstalledBootstrapKind({
16267
- metal: existsSync9(METAL_BOOTSTRAP_STATE_PATH),
16268
- compute: existsSync9(BOOTSTRAP_STATE_PATH)
17352
+ metal: existsSync11(METAL_BOOTSTRAP_STATE_PATH),
17353
+ compute: existsSync11(BOOTSTRAP_STATE_PATH)
16269
17354
  });
16270
17355
  if (operation === "status") {
16271
17356
  if (installedBootstrapKind() === "metal") {
@@ -16389,7 +17474,7 @@ async function cmdUnlock(options) {
16389
17474
  if (options.key || options.userExplicit) {
16390
17475
  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.");
16391
17476
  }
16392
- const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync12(0, "utf8").trim() : "";
17477
+ const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync14(0, "utf8").trim() : "";
16393
17478
  const phrase = phraseText ? phraseText.split(/\s+/) : [];
16394
17479
  if (phrase.length !== 24) {
16395
17480
  throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
@@ -16605,7 +17690,7 @@ function projectFromCheckout(options) {
16605
17690
  try {
16606
17691
  return loadConfig({
16607
17692
  cwd: options.projectRoot,
16608
- readFile: (path) => existsSync9(path) ? readFileSync12(path, "utf8") : undefined
17693
+ readFile: (path) => existsSync11(path) ? readFileSync14(path, "utf8") : undefined
16609
17694
  }).config.project;
16610
17695
  } catch (cause) {
16611
17696
  throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
@@ -16653,24 +17738,69 @@ async function cmdDeploy(options, args) {
16653
17738
  if (options.optionError)
16654
17739
  throw new Error(options.optionError);
16655
17740
  if (operation === "init") {
16656
- const created = initializeDeployFile(options.projectRoot, {
16657
- name: options.projectName,
16658
- profile: options.deployProfile,
16659
- software: softwareCoordinates(options.deploySoftware),
16660
- requireAttestation: options.requireAttestation,
16661
- channel: options.deployChannel,
16662
- force: options.force
16663
- });
17741
+ if (options.deploySoftware.length > 0 || options.deployProfile !== "app" || options.requireAttestation) {
17742
+ const created = initializeDeployFile(options.projectRoot, {
17743
+ name: options.projectName,
17744
+ profile: options.deployProfile,
17745
+ software: softwareCoordinates(options.deploySoftware),
17746
+ requireAttestation: options.requireAttestation,
17747
+ channel: options.deployChannel,
17748
+ force: options.force
17749
+ });
17750
+ if (options.json)
17751
+ out.line(JSON.stringify(created.summary, null, 2));
17752
+ else
17753
+ out.ok(`Initialized legacy .fz/deploy.json (${created.summary.digest}).`);
17754
+ return 0;
17755
+ }
17756
+ initializeTypeScriptDeployment(options.projectRoot, { name: options.projectName ?? basename3(resolve9(options.projectRoot)), force: options.force });
17757
+ const compiled = await compileDeploymentProject(options.projectRoot);
16664
17758
  if (options.json)
16665
- out.line(JSON.stringify(created.summary, null, 2));
17759
+ out.line(JSON.stringify({ source: compiled.source, output: compiled.output, digest: compiled.digest }, null, 2));
16666
17760
  else {
16667
- out.ok(`Initialized .fz/deploy.json (${created.summary.digest}).`);
16668
- out.warn("Release and health use safe blockers until you replace them with project-specific commands.");
17761
+ out.ok(`Initialized ${DEPLOY_SOURCE_FILE} and ${DEPLOY_PLAN_FILE} (${compiled.digest}).`);
17762
+ out.warn("Build and release use safe blockers until you replace them with project-specific typed actions.");
16669
17763
  out.step("Run `fz deploy check`, commit the file, then push; the verified Git commit is the live sync.");
16670
17764
  }
16671
17765
  return 0;
16672
17766
  }
17767
+ if (operation === "compile") {
17768
+ const compiled = await compileDeploymentProject(options.projectRoot);
17769
+ const problems = canonicalJson(compiled.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : [];
17770
+ const summary = { source: compiled.source, output: compiled.output, digest: compiled.digest, changed: compiled.changed, ready: problems.length === 0, problems };
17771
+ if (options.json)
17772
+ out.line(JSON.stringify(summary, null, 2));
17773
+ else {
17774
+ out.ok(`Compiled ${DEPLOY_SOURCE_FILE} \u2192 ${DEPLOY_PLAN_FILE} (${compiled.digest}).`);
17775
+ for (const problem of problems)
17776
+ out.warn(problem);
17777
+ }
17778
+ return problems.length === 0 ? 0 : 1;
17779
+ }
16673
17780
  if (operation === "check" || operation === "sync") {
17781
+ if (existsSync11(resolve9(options.projectRoot, DEPLOY_SOURCE_FILE))) {
17782
+ const expected = await compileDeploymentProject(options.projectRoot, { write: false });
17783
+ const actual = inspectCompiledDeployment(options.projectRoot);
17784
+ const current = canonicalJson(expected.plan) === canonicalJson(actual.plan);
17785
+ const problems = [
17786
+ ...current ? [] : [`${DEPLOY_PLAN_FILE} is stale; run \`fz deploy compile\``],
17787
+ ...canonicalJson(expected.plan).includes("pipeline-todo") ? ["typed plan contains safe initialization blockers"] : []
17788
+ ];
17789
+ const summary = { source: expected.source, output: actual.path, digest: actual.digest, version: actual.plan.version, name: actual.plan.name, ready: problems.length === 0, problems };
17790
+ if (options.json)
17791
+ out.line(JSON.stringify(summary, null, 2));
17792
+ else {
17793
+ out.step(`${summary.name} \xB7 plan v${summary.version} \xB7 ${summary.digest}`);
17794
+ for (const problem of problems)
17795
+ out.fail(problem);
17796
+ if (summary.ready) {
17797
+ out.ok("TypeScript definition and canonical execution plan are current.");
17798
+ if (operation === "sync")
17799
+ out.step("Commit both files. ForgeZero executes only the reviewed plan.");
17800
+ }
17801
+ }
17802
+ return summary.ready ? 0 : 1;
17803
+ }
16674
17804
  const inspected = inspectDeployFile(options.projectRoot, { channel: options.deployChannel });
16675
17805
  if (options.json)
16676
17806
  out.line(JSON.stringify(inspected.summary, null, 2));
@@ -16724,7 +17854,7 @@ async function cmdDeploy(options, args) {
16724
17854
  branch: options.branch,
16725
17855
  cloneUrl: required(options.cloneUrl, "--clone-url"),
16726
17856
  sourceAuth,
16727
- ...options.knownHostsFile ? { knownHosts: readFileSync12(options.knownHostsFile, "utf8") } : {},
17857
+ ...options.knownHostsFile ? { knownHosts: readFileSync14(options.knownHostsFile, "utf8") } : {},
16728
17858
  projectKey
16729
17859
  } });
16730
17860
  const pipelineKey2 = String(created.pipelineKey);
@@ -16784,7 +17914,7 @@ async function cmdDeploy(options, args) {
16784
17914
  out.line(JSON.stringify(await deploymentRequest(options, "/rotate", { method: "POST", body: { pipelineKey } }), null, 2));
16785
17915
  return 0;
16786
17916
  }
16787
- out.fail("Usage: fz deploy init|check|sync|catalog|list|connect|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
17917
+ out.fail("Usage: fz deploy init|compile|check|sync|catalog|list|connect|attach|runs|release|enable|disable|target-enable|target-disable|rotate");
16788
17918
  return 2;
16789
17919
  } catch (cause) {
16790
17920
  out.fail(cause instanceof Error ? cause.message : String(cause));
@@ -16847,8 +17977,9 @@ function usage() {
16847
17977
  fz project init Create vendor-neutral, Git-persisted AI context
16848
17978
  fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
16849
17979
  fz project check Fail when truth sources or generated adapters drift
16850
- fz deploy init Create a typed, fail-safe .fz/deploy.json
16851
- fz deploy check Validate commands, profiles and software coordinates
17980
+ fz deploy init Create forgezero.deploy.ts and its fail-safe canonical plan
17981
+ fz deploy compile Compile TypeScript into .fz/deploy.plan.json
17982
+ fz deploy check Validate source, plan, actions, providers and topology
16852
17983
  fz deploy sync Prove readiness and print the Git synchronization rule
16853
17984
  fz deploy catalog List selectable tested OS/software coordinates
16854
17985
  fz deploy list List pipelines for .fz/config.json's project