@forgezero/agent 0.1.103 → 0.1.107

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-agent.js CHANGED
@@ -7924,12 +7924,24 @@ function parseBootstrapBundleManifest(value) {
7924
7924
  // src/deployment.ts
7925
7925
  var RELEASE_GROUP = "forgezero-deploy";
7926
7926
  var deploymentAttestationChallenge = (revision) => createHash5("sha256").update("forgezero/deployment-attestation/v1\x00").update(revision, "utf8").digest("hex");
7927
+ var deploymentExecution = (phases) => ({
7928
+ steps: phases.flatMap((phase) => phase.steps.map((step2) => ({
7929
+ phase: phase.pipeline.slice(0, 128),
7930
+ step: step2.name.slice(0, 128),
7931
+ outcome: step2.outcome,
7932
+ exitCode: step2.exitCode,
7933
+ durationMs: Math.max(0, Math.min(step2.durationMs, 86400000)),
7934
+ ...step2.outcome === "failed" && step2.log.trim() ? { detail: step2.log.trim().slice(0, 2000) } : {}
7935
+ }))).slice(0, 128)
7936
+ });
7927
7937
 
7928
7938
  class DeploymentError extends Error {
7929
7939
  code;
7930
- constructor(code, message) {
7940
+ execution;
7941
+ constructor(code, message, execution) {
7931
7942
  super(message);
7932
7943
  this.code = code;
7944
+ this.execution = execution;
7933
7945
  this.name = "DeploymentError";
7934
7946
  }
7935
7947
  }
@@ -8280,7 +8292,7 @@ function createDeploymentManager(options) {
8280
8292
  }
8281
8293
  const assignedTopology = assigned.topology;
8282
8294
  const selectedTopology = selected.topology;
8283
- const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
8295
+ const topologyMatches = selectedTopology ? Boolean(assignedTopology && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.site) && /^[A-Za-z0-9.-]{1,253}$/.test(assignedTopology.nodeIdentity) && /^[A-Za-z0-9._:-]{1,256}$/.test(assignedTopology.localRelayReference) && /^sha256:[a-f0-9]{64}$/.test(assignedTopology.generation) && /^(?:\d{1,3}\.){3}0\/24$/.test(assignedTopology.siteCidr) && assignedTopology.localRelayAddress.length <= 64 && assignedTopology.localPeerAddresses.length <= 1023 && assignedTopology.localPeerAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.localPeerIdentities.length === assignedTopology.localPeerAddresses.length && assignedTopology.localPeerIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteRelayAddresses.length <= 1023 && assignedTopology.remoteRelayAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.remoteRelayIdentities.length === assignedTopology.remoteRelayAddresses.length && assignedTopology.remoteRelayIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && assignedTopology.remoteMemberAddresses.length <= 1024 && new Set(assignedTopology.remoteMemberAddresses).size === assignedTopology.remoteMemberAddresses.length && assignedTopology.remoteMemberAddresses.every((address) => typeof address === "string" && address.length <= 64) && assignedTopology.memberIdentities.length >= 1 && assignedTopology.memberIdentities.length <= 1024 && new Set(assignedTopology.memberIdentities).size === assignedTopology.memberIdentities.length && assignedTopology.memberIdentities.includes(assignedTopology.nodeIdentity) && assignedTopology.memberIdentities.every((identity) => /^[A-Za-z0-9.-]{1,253}$/.test(identity)) && (assignedTopology.role === "member" ? assignedTopology.remoteRelayAddresses.length === 0 : assignedTopology.remoteSiteCidrs.length === assignedTopology.remoteRelayAddresses.length) && assignedTopology.remoteSiteCidrs.every((cidr) => typeof cidr === "string" && /^(?:\d{1,3}\.){3}0\/24$/.test(cidr)) && Number.isSafeInteger(assignedTopology.healthPort) && assignedTopology.healthPort >= 1 && assignedTopology.healthPort <= 65535 && assignedTopology.routedTcpPorts.length >= 1 && assignedTopology.routedTcpPorts.length <= 64 && new Set(assignedTopology.routedTcpPorts).size === assignedTopology.routedTcpPorts.length && assignedTopology.routedTcpPorts.includes(assignedTopology.healthPort) && assignedTopology.routedTcpPorts.every((port) => Number.isSafeInteger(port) && port >= 1 && port <= 65535) && (assignedTopology.role !== "member" || assignedTopology.transport === "private-lan" && assignedTopology.warpRequired === false && assignedTopology.remoteRelayAddresses.length === 0 && assignedTopology.localPeerAddresses.length === 1) && (assignedTopology.warpRequired ? assignedTopology.role !== "member" && assignedTopology.transport === "cloudflare-warp" && selectedTopology.crossMetal.mode !== "private-lan" : assignedTopology.transport === "private-lan")) : assignedTopology === undefined;
8284
8296
  const expectedPrivate = selectedTopology ? assignedTopology?.warpRequired ? selectedTopology.crossMetal.mode === "private-lan" ? undefined : {
8285
8297
  mode: "cloudflare-warp",
8286
8298
  credential: selectedTopology.crossMetal.credential,
@@ -8318,6 +8330,7 @@ function createDeploymentManager(options) {
8318
8330
  FZ_TOPOLOGY_LOCAL_PEERS: assigned.topology.localPeerAddresses.join(","),
8319
8331
  FZ_TOPOLOGY_LOCAL_PEER_IDENTITIES: assigned.topology.localPeerIdentities.join(","),
8320
8332
  FZ_TOPOLOGY_REMOTE_RELAYS: assigned.topology.remoteRelayAddresses.join(","),
8333
+ FZ_TOPOLOGY_REMOTE_MEMBERS: assigned.topology.remoteMemberAddresses.join(","),
8321
8334
  FZ_TOPOLOGY_REMOTE_RELAY_IDENTITIES: assigned.topology.remoteRelayIdentities.join(","),
8322
8335
  FZ_TOPOLOGY_REMOTE_CIDRS: assigned.topology.remoteSiteCidrs.join(","),
8323
8336
  FZ_TOPOLOGY_MEMBERS: assigned.topology.memberIdentities.join(","),
@@ -8338,6 +8351,18 @@ function createDeploymentManager(options) {
8338
8351
  throw new DeploymentError("PIPELINE_FAILED", `component limits exceed the reserved capacity of target ${targetName}`);
8339
8352
  }
8340
8353
  }
8354
+ if (assigned?.firewallPolicy) {
8355
+ if (!options.ensureSoftware || !options.applyConnectivity) {
8356
+ throw new DeploymentError("PIPELINE_FAILED", "the supervised firewall helper is unavailable");
8357
+ }
8358
+ await options.ensureSoftware([{ id: "ufw", version: "latest" }]);
8359
+ await options.applyConnectivity({
8360
+ key: `${options.key}:${assigned.targetName}:${assigned.slot}`,
8361
+ intent: {},
8362
+ capabilities: {},
8363
+ firewallPolicy: structuredClone(assigned.firewallPolicy)
8364
+ });
8365
+ }
8341
8366
  const inputValues = resolveDeploymentInputs(plan, options.environment ?? {});
8342
8367
  const projectExec2 = options.projectExec ?? spawnExact;
8343
8368
  const materializePlanArgv = (argv2) => argv2.map((argument) => {
@@ -8416,6 +8441,7 @@ function createDeploymentManager(options) {
8416
8441
  localPeerIdentities: assigned.topology.localPeerIdentities,
8417
8442
  remoteRelayAddresses: assigned.topology.remoteRelayAddresses,
8418
8443
  remoteRelayIdentities: assigned.topology.remoteRelayIdentities,
8444
+ remoteMemberAddresses: assigned.topology.remoteMemberAddresses,
8419
8445
  remoteSiteCidrs: assigned.topology.remoteSiteCidrs,
8420
8446
  memberIdentities: assigned.topology.memberIdentities,
8421
8447
  healthPort: assigned.topology.healthPort,
@@ -8430,6 +8456,7 @@ function createDeploymentManager(options) {
8430
8456
  throw new DeploymentActionError("refused", "exec.argv requires an exact argv array");
8431
8457
  const environment = { ...phaseEnvironment2 };
8432
8458
  const bindings = resolved.credentials;
8459
+ const credentialValues = [];
8433
8460
  if (bindings !== undefined) {
8434
8461
  if (!bindings || typeof bindings !== "object" || Array.isArray(bindings))
8435
8462
  throw new DeploymentActionError("refused", "exec.argv credentials must map environment names to credential declarations");
@@ -8440,11 +8467,12 @@ function createDeploymentManager(options) {
8440
8467
  if (!credential || !options.cache)
8441
8468
  throw new DeploymentActionError("refused", `credential ${credentialName} is unavailable`);
8442
8469
  environment[environmentName] = await options.cache.get(credential.source.name);
8470
+ credentialValues.push(environment[environmentName]);
8443
8471
  }
8444
8472
  }
8445
8473
  const result = await projectExec2({ argv: materializePlanArgv(resolved.argv), cwd: release, env: environment, timeoutMs: step2.timeoutMs });
8446
8474
  if (result.exitCode !== 0)
8447
- throw new DeploymentActionError("failed", result.output.slice(0, 2000) || `command exited ${result.exitCode}`);
8475
+ throw new DeploymentActionError("failed", redact(result.output, credentialValues).slice(0, 2000) || `command exited ${result.exitCode}`);
8448
8476
  return { outputs: { exitCode: result.exitCode } };
8449
8477
  },
8450
8478
  "forgezero.health/http@1": async ({ resolved }) => {
@@ -8562,7 +8590,7 @@ function createDeploymentManager(options) {
8562
8590
  };
8563
8591
  if (!run2.ok) {
8564
8592
  const failed = run2.steps.find((step2) => step2.status === "failed" || step2.status === "manual-intervention");
8565
- throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}`);
8593
+ throw new DeploymentError("PIPELINE_FAILED", `deployment plan failed at ${failed?.id ?? "unknown step"}: ${failed?.error?.message ?? run2.status}`, deploymentExecution([phase]));
8566
8594
  }
8567
8595
  return {
8568
8596
  key: options.key,
@@ -8575,6 +8603,7 @@ function createDeploymentManager(options) {
8575
8603
  release,
8576
8604
  ok: true,
8577
8605
  phases: [phase],
8606
+ execution: deploymentExecution([phase]),
8578
8607
  resolvedTargets
8579
8608
  };
8580
8609
  }
@@ -8652,7 +8681,7 @@ function createDeploymentManager(options) {
8652
8681
  if (!result.ok) {
8653
8682
  const failedStep = result.steps.find((step2) => step2.outcome === "failed");
8654
8683
  const detail = failedStep?.log.trim().slice(0, 2000);
8655
- throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`));
8684
+ throw new DeploymentError("PIPELINE_FAILED", `${pipeline.name} failed at ${failedStep?.name ?? "unknown step"}` + (detail ? `: ${detail}` : ` (exit ${failedStep?.exitCode ?? "unknown"}).`), deploymentExecution(phases));
8656
8685
  }
8657
8686
  if (pipeline.phase === "release")
8658
8687
  await activateSelectedService();
@@ -8694,6 +8723,7 @@ function createDeploymentManager(options) {
8694
8723
  release,
8695
8724
  ok: true,
8696
8725
  phases,
8726
+ execution: deploymentExecution(phases),
8697
8727
  ...capacity ? { capacity } : {}
8698
8728
  };
8699
8729
  };
@@ -9000,7 +9030,8 @@ async function pullDeploymentOnce(options) {
9000
9030
  runKey: claim.runKey,
9001
9031
  claimToken: claim.claimToken,
9002
9032
  ok: false,
9003
- detail: reason
9033
+ detail: reason,
9034
+ ...cause instanceof DeploymentError && cause.execution ? { execution: cause.execution } : {}
9004
9035
  });
9005
9036
  if (options.telemetry) {
9006
9037
  await options.telemetry.observe("deployment.complete", acknowledge2, () => "success", remoteOutcome);
@@ -9017,6 +9048,7 @@ async function pullDeploymentOnce(options) {
9017
9048
  definitionDigest: result.definitionDigest,
9018
9049
  definitionVersion: result.definitionVersion,
9019
9050
  profile: result.profile,
9051
+ execution: result.execution,
9020
9052
  ...result.resolvedTargets ? { resolvedTargets: result.resolvedTargets.map((target) => ({
9021
9053
  ...target,
9022
9054
  profiles: [...target.profiles],
@@ -9496,7 +9528,7 @@ async function writeAndCloseProcessInput(input, value) {
9496
9528
  }
9497
9529
 
9498
9530
  // src/version.ts
9499
- var VERSION2 = "0.1.103";
9531
+ var VERSION2 = "0.1.107";
9500
9532
 
9501
9533
  // src/ssh-bootstrap.ts
9502
9534
  class SshBootstrapError extends Error {
@@ -13384,7 +13416,7 @@ async function activateContainer(requestValue, host = defaultHost2) {
13384
13416
  // src/deployment-connectivity.ts
13385
13417
  import { createHash as createHash11 } from "crypto";
13386
13418
  import { mkdirSync as mkdirSync12, renameSync as renameSync12, writeFileSync as writeFileSync13 } from "fs";
13387
- import { createConnection as createConnection2 } from "net";
13419
+ import { createConnection as createConnection2, isIP as isIP4 } from "net";
13388
13420
  import { dirname as dirname11 } from "path";
13389
13421
  var TOKEN2 = /^[A-Za-z0-9._-]{40,16384}$/;
13390
13422
  var UUID = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i;
@@ -13463,21 +13495,99 @@ function validate2(request) {
13463
13495
  }
13464
13496
  } else if (request.capabilities.private)
13465
13497
  throw new Error("unexpected private deployment capability");
13498
+ if (request.firewallPolicy) {
13499
+ const policy = request.firewallPolicy;
13500
+ if (!/^sha256:[a-f0-9]{64}$/.test(policy.generation) || !Array.isArray(policy.rules) || policy.rules.length > 256) {
13501
+ throw new Error("deployment firewall policy is malformed");
13502
+ }
13503
+ let expanded = 0;
13504
+ for (const rule of policy.rules) {
13505
+ expanded += rule.sourceAddresses.length;
13506
+ if (!/^[A-Za-z0-9_-]{1,64}$/.test(rule.ruleKey) || !["allow", "deny"].includes(rule.action) || !["tcp", "udp"].includes(rule.protocol) || rule.sourceAddresses.length < 1 || rule.sourceAddresses.length > 1024 || new Set(rule.sourceAddresses).size !== rule.sourceAddresses.length || rule.sourceAddresses.some((address) => isIP4(address) !== 4) || !Number.isSafeInteger(rule.portFrom) || rule.portFrom < 1 || rule.portFrom > 65535 || !Number.isSafeInteger(rule.portTo) || rule.portTo < rule.portFrom || rule.portTo > 65535 || !Number.isSafeInteger(rule.priority) || rule.priority < 0 || rule.priority > 1e6) {
13507
+ throw new Error("deployment firewall policy is malformed");
13508
+ }
13509
+ }
13510
+ if (expanded > 2048)
13511
+ throw new Error("deployment firewall policy expands beyond its rule limit");
13512
+ }
13513
+ }
13514
+ async function replaceTaggedUfwRules(host, comment) {
13515
+ const status = await checked7(host, ["/usr/sbin/ufw", "status", "numbered"], "firewall inventory");
13516
+ const numbers = status.split(`
13517
+ `).flatMap((line) => {
13518
+ if (!line.includes(comment))
13519
+ return [];
13520
+ const match = line.match(/^\s*\[\s*(\d{1,6})\]/);
13521
+ return match ? [Number(match[1])] : [];
13522
+ }).filter((value) => Number.isSafeInteger(value) && value > 0).sort((left, right) => right - left);
13523
+ for (const number2 of numbers)
13524
+ await checked7(host, ["/usr/sbin/ufw", "--force", "delete", String(number2)], `stale firewall rule ${number2}`);
13466
13525
  }
13467
13526
  async function applyDeploymentConnectivity(request, host = defaultHost3) {
13468
13527
  validate2(request);
13469
13528
  const id2 = idFor3(request.key);
13470
13529
  const evidence = { key: request.key };
13530
+ if (request.firewallPolicy) {
13531
+ const policy = request.firewallPolicy;
13532
+ const comment = `fz-policy-${id2}`;
13533
+ await replaceTaggedUfwRules(host, comment);
13534
+ const ordered = [...policy.rules].sort((left, right) => left.priority - right.priority || (left.action === right.action ? left.ruleKey.localeCompare(right.ruleKey) : left.action === "deny" ? -1 : 1));
13535
+ const commands = [];
13536
+ for (const rule of ordered)
13537
+ for (const source of rule.sourceAddresses)
13538
+ commands.push([
13539
+ "/usr/sbin/ufw",
13540
+ "insert",
13541
+ "1",
13542
+ rule.action,
13543
+ "from",
13544
+ source,
13545
+ "to",
13546
+ "any",
13547
+ "port",
13548
+ rule.portFrom === rule.portTo ? String(rule.portFrom) : `${rule.portFrom}:${rule.portTo}`,
13549
+ "proto",
13550
+ rule.protocol,
13551
+ "comment",
13552
+ comment
13553
+ ]);
13554
+ const ranges = new Map;
13555
+ for (const rule of ordered)
13556
+ ranges.set(`${rule.protocol}:${rule.portFrom}:${rule.portTo}`, rule);
13557
+ for (const range of ranges.values())
13558
+ commands.push([
13559
+ "/usr/sbin/ufw",
13560
+ "insert",
13561
+ "1",
13562
+ "deny",
13563
+ "to",
13564
+ "any",
13565
+ "port",
13566
+ range.portFrom === range.portTo ? String(range.portFrom) : `${range.portFrom}:${range.portTo}`,
13567
+ "proto",
13568
+ range.protocol,
13569
+ "comment",
13570
+ comment
13571
+ ]);
13572
+ for (const command3 of commands.toReversed())
13573
+ await checked7(host, command3, "deployment firewall rule");
13574
+ evidence.firewall = { generation: policy.generation, rules: commands.length, active: true };
13575
+ }
13471
13576
  const topology = request.topology;
13472
13577
  if (topology) {
13473
- const values = [topology.localRelayAddress, ...topology.localPeerAddresses, ...topology.remoteRelayAddresses];
13578
+ const values = [
13579
+ topology.localRelayAddress,
13580
+ ...topology.localPeerAddresses,
13581
+ ...topology.remoteRelayAddresses,
13582
+ ...topology.remoteMemberAddresses
13583
+ ];
13474
13584
  const identities = [
13475
13585
  topology.nodeIdentity,
13476
13586
  ...topology.localPeerIdentities,
13477
13587
  ...topology.remoteRelayIdentities,
13478
13588
  ...topology.memberIdentities
13479
13589
  ];
13480
- if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
13590
+ if (!/^[A-Za-z0-9.-]{1,253}$/.test(topology.site) || !/^(?:\d{1,3}\.){3}0\/24$/.test(topology.siteCidr) || values.some((value) => isIP4(value) !== 4) || identities.some((value) => !/^[A-Za-z0-9.-]{1,253}$/.test(value)) || topology.localPeerIdentities.length !== topology.localPeerAddresses.length || topology.remoteRelayIdentities.length !== topology.remoteRelayAddresses.length || topology.remoteMemberAddresses.length > 1024 || new Set(topology.remoteMemberAddresses).size !== topology.remoteMemberAddresses.length || topology.memberIdentities.length < 1 || topology.memberIdentities.length > 1024 || new Set(topology.memberIdentities).size !== topology.memberIdentities.length || !topology.memberIdentities.includes(topology.nodeIdentity) || !/^sha256:[a-f0-9]{64}$/.test(topology.generation) || (topology.role === "member" ? topology.remoteRelayAddresses.length !== 0 : topology.remoteSiteCidrs.length !== topology.remoteRelayAddresses.length) || topology.remoteSiteCidrs.some((value) => !/^(?:\d{1,3}\.){3}0\/24$/.test(value)) || new Set(topology.remoteSiteCidrs).size !== topology.remoteSiteCidrs.length || !Number.isSafeInteger(topology.healthPort) || topology.healthPort < 1 || topology.healthPort > 65535 || topology.routedTcpPorts.length < 1 || topology.routedTcpPorts.length > 64 || new Set(topology.routedTcpPorts).size !== topology.routedTcpPorts.length || !topology.routedTcpPorts.includes(topology.healthPort) || topology.routedTcpPorts.some((port) => !Number.isSafeInteger(port) || port < 1 || port > 65535) || topology.role === "member" && topology.transport !== "private-lan") {
13481
13591
  throw new Error("deployment topology is malformed");
13482
13592
  }
13483
13593
  if (request.intent.private?.mode === "cloudflare-warp" !== (topology.transport === "cloudflare-warp")) {
@@ -13604,28 +13714,46 @@ ${forwarding}${noOp}${starts}${starts ? `
13604
13714
  [Install]
13605
13715
  WantedBy=multi-user.target
13606
13716
  `, 420);
13607
- for (const source of [...new Set([topology.siteCidr, ...topology.remoteSiteCidrs])]) {
13717
+ const firewallComment = `fz-topology-${id2}`;
13718
+ await replaceTaggedUfwRules(host, firewallComment);
13719
+ for (const source of [...new Set([...topology.localPeerAddresses, ...topology.remoteMemberAddresses])]) {
13608
13720
  for (const port of topology.routedTcpPorts) {
13609
- await checked7(host, ["/usr/sbin/ufw", "allow", "from", source, "to", "any", "port", String(port), "proto", "tcp"], `private service ${source}:${port}`);
13721
+ await checked7(host, [
13722
+ "/usr/sbin/ufw",
13723
+ "allow",
13724
+ "from",
13725
+ source,
13726
+ "to",
13727
+ "any",
13728
+ "port",
13729
+ String(port),
13730
+ "proto",
13731
+ "tcp",
13732
+ "comment",
13733
+ firewallComment
13734
+ ], `private service ${source}:${port}`);
13610
13735
  }
13611
13736
  }
13612
13737
  if (topology.role !== "member")
13613
- for (const remoteCidr of topology.remoteSiteCidrs) {
13614
- for (const port of topology.routedTcpPorts) {
13615
- await checked7(host, [
13616
- "/usr/sbin/ufw",
13617
- "route",
13618
- "allow",
13619
- "proto",
13620
- "tcp",
13621
- "from",
13622
- topology.siteCidr,
13623
- "to",
13624
- remoteCidr,
13625
- "port",
13626
- String(port)
13627
- ], `private routed service ${topology.siteCidr}->${remoteCidr}:${port}`);
13628
- }
13738
+ for (const remoteAddress of topology.remoteMemberAddresses) {
13739
+ for (const source of topology.localPeerAddresses)
13740
+ for (const port of topology.routedTcpPorts) {
13741
+ await checked7(host, [
13742
+ "/usr/sbin/ufw",
13743
+ "route",
13744
+ "allow",
13745
+ "proto",
13746
+ "tcp",
13747
+ "from",
13748
+ source,
13749
+ "to",
13750
+ remoteAddress,
13751
+ "port",
13752
+ String(port),
13753
+ "comment",
13754
+ firewallComment
13755
+ ], `private routed service ${source}->${remoteAddress}:${port}`);
13756
+ }
13629
13757
  }
13630
13758
  await checked7(host, ["/usr/bin/systemctl", "daemon-reload"], "topology daemon reload");
13631
13759
  await checked7(host, ["/usr/bin/systemctl", "enable", "--now", routeUnit], "topology routes");
@@ -15172,7 +15300,7 @@ import { dirname as dirname14 } from "path";
15172
15300
  import { fileURLToPath as fileURLToPath2 } from "url";
15173
15301
 
15174
15302
  // src/provision.ts
15175
- import { isIP as isIP4 } from "net";
15303
+ import { isIP as isIP5 } from "net";
15176
15304
  function atLeast(version, floor) {
15177
15305
  const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
15178
15306
  const got = parse(version);
@@ -15652,7 +15780,7 @@ function agentUnit(options) {
15652
15780
  throw new Error("compute telemetry endpoint must be an absolute collector URL");
15653
15781
  }
15654
15782
  const localCollector = endpoint.protocol === "http:" && endpoint.hostname === "127.0.0.1" && endpoint.port === "4318" && endpoint.pathname === "/";
15655
- const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP4(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
15783
+ const publicCollector = endpoint.protocol === "https:" && !endpoint.username && !endpoint.password && !endpoint.search && !endpoint.hash && isIP5(endpoint.hostname) === 0 && endpoint.hostname.includes(".") && endpoint.hostname !== "localhost" && !endpoint.hostname.endsWith(".local");
15656
15784
  if (!localCollector && !publicCollector || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
15657
15785
  throw new Error("compute telemetry endpoint must be the supervised loopback collector or credential-free public HTTPS");
15658
15786
  }
@@ -16480,6 +16608,7 @@ var BOOTSTRAP_STATE_PATH = "/var/lib/forgezero/bootstrap.json";
16480
16608
  var STATE_PATH = BOOTSTRAP_STATE_PATH;
16481
16609
  var CREDS = "/etc/forgezero/creds";
16482
16610
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
16611
+ var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
16483
16612
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
16484
16613
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
16485
16614
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
@@ -16641,6 +16770,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
16641
16770
  if (nginx3.exitCode !== 0)
16642
16771
  problems.push("nginx configuration is invalid");
16643
16772
  if (state.databaseRole !== "none") {
16773
+ for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
16774
+ services[credential] = host.exists(credential);
16775
+ if (!services[credential])
16776
+ problems.push(`${credential} is missing`);
16777
+ }
16644
16778
  const unitPath = "/etc/systemd/system/forgezero-db.service";
16645
16779
  const expectsNoAgency = state.databaseAgency === "none";
16646
16780
  const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
@@ -16864,7 +16998,7 @@ async function verifyPlatformFleetHost(expectedVersion, runtime = localRuntime()
16864
16998
  // src/community-rehearsal-host.ts
16865
16999
  import { createHash as createHash12 } from "crypto";
16866
17000
  import { lstatSync as lstatSync8, mkdirSync as mkdirSync17, readFileSync as readFileSync18, rmSync as rmSync13, symlinkSync as symlinkSync8, unlinkSync as unlinkSync13, writeFileSync as writeFileSync17 } from "fs";
16867
- import { isIP as isIP5 } from "net";
17001
+ import { isIP as isIP6 } from "net";
16868
17002
  import { dirname as dirname15 } from "path";
16869
17003
  var COMMUNITY_REHEARSAL_VERSION = "3.11.14";
16870
17004
  var COMMUNITY_REHEARSAL_ROOT = "/opt/forgezero-rehearsals/community-cluster-api";
@@ -16897,7 +17031,7 @@ var validateCommunityRehearsalTopology = (value) => {
16897
17031
  throw new Error("community rehearsal requires exactly four reviewed nodes");
16898
17032
  const rows = value.map((item, index) => {
16899
17033
  const row2 = exactObject(item);
16900
- if (Object.keys(row2).some((key) => !["name", "address", "agency", "starter", "readPreferred"].includes(key)) || typeof row2.name !== "string" || !SAFE_NAME2.test(row2.name) || typeof row2.address !== "string" || isIP5(row2.address) !== 4 || typeof row2.agency !== "boolean" || typeof row2.starter !== "boolean" || typeof row2.readPreferred !== "boolean") {
17034
+ if (Object.keys(row2).some((key) => !["name", "address", "agency", "starter", "readPreferred"].includes(key)) || typeof row2.name !== "string" || !SAFE_NAME2.test(row2.name) || typeof row2.address !== "string" || isIP6(row2.address) !== 4 || typeof row2.agency !== "boolean" || typeof row2.starter !== "boolean" || typeof row2.readPreferred !== "boolean") {
16901
17035
  throw new Error(`community rehearsal topology node ${index + 1} is invalid`);
16902
17036
  }
16903
17037
  return row2;
@@ -17250,8 +17384,75 @@ async function readBoundedStdin(maxBytes) {
17250
17384
  throw new Error(`stdin request exceeds ${maxBytes} bytes`);
17251
17385
  return Buffer.from(stdout).toString("utf8");
17252
17386
  }
17387
+
17388
+ // src/database-auth-verify.ts
17389
+ import { isIP as isIP7 } from "net";
17390
+ var MAX_RESPONSE_BYTES2 = 16 * 1024;
17391
+ function databaseEndpoint(raw) {
17392
+ let endpoint;
17393
+ try {
17394
+ endpoint = new URL(raw);
17395
+ } catch {
17396
+ throw new Error("database authentication endpoint is malformed");
17397
+ }
17398
+ const hostname2 = endpoint.hostname.replace(/^\[|\]$/g, "");
17399
+ const ipVersion = isIP7(hostname2);
17400
+ const privateIp2 = ipVersion === 4 ? /^(?:10\.|192\.168\.|172\.(?:1[6-9]|2[0-9]|3[01])\.)/.test(hostname2) : ipVersion === 6 && /^(?:fc|fd)[0-9a-f]{2}:/i.test(hostname2);
17401
+ if (endpoint.protocol !== "http:" || endpoint.username || endpoint.password || endpoint.port !== "8529" || endpoint.pathname !== "/" || endpoint.search || endpoint.hash || !privateIp2)
17402
+ throw new Error("database authentication endpoint must be an exact private IP on port 8529");
17403
+ return endpoint;
17404
+ }
17405
+ function rootCredential(readCredential) {
17406
+ const password = readCredential();
17407
+ if (password.length < 48 || password.length > 256 || /[\r\n\0]/.test(password)) {
17408
+ throw new Error("ArangoDB root credential is malformed");
17409
+ }
17410
+ return password;
17411
+ }
17412
+ async function boundedBody(response) {
17413
+ const body = await response.text();
17414
+ if (Buffer.byteLength(body) > MAX_RESPONSE_BYTES2)
17415
+ throw new Error("ArangoDB authentication proof response is too large");
17416
+ return body;
17417
+ }
17418
+ async function refused2(fetcher, url, authorization) {
17419
+ const response = await fetcher(url, {
17420
+ method: "GET",
17421
+ headers: authorization ? { authorization } : undefined,
17422
+ redirect: "error"
17423
+ });
17424
+ await boundedBody(response);
17425
+ if (response.status !== 401 && response.status !== 403) {
17426
+ throw new Error("ArangoDB accepted an unauthenticated or empty-password request");
17427
+ }
17428
+ }
17429
+ async function verifyDatabaseAuthentication(rawEndpoint, options) {
17430
+ const endpoint = databaseEndpoint(rawEndpoint);
17431
+ const password = rootCredential(options.readCredential);
17432
+ const versionUrl = new URL("/_api/version", endpoint);
17433
+ const fetcher = options.fetcher ?? fetch;
17434
+ await refused2(fetcher, versionUrl);
17435
+ await refused2(fetcher, versionUrl, `Basic ${Buffer.from("root:").toString("base64")}`);
17436
+ const response = await fetcher(versionUrl, {
17437
+ method: "GET",
17438
+ headers: { authorization: `Basic ${Buffer.from(`root:${password}`).toString("base64")}` },
17439
+ redirect: "error"
17440
+ });
17441
+ const body = await boundedBody(response);
17442
+ if (!response.ok)
17443
+ throw new Error(`ArangoDB sealed root authentication failed with status ${response.status}`);
17444
+ let value;
17445
+ try {
17446
+ value = JSON.parse(body);
17447
+ } catch {
17448
+ throw new Error("ArangoDB authentication proof returned malformed JSON");
17449
+ }
17450
+ if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== "3.11.14") {
17451
+ throw new Error("ArangoDB authentication proof returned the wrong version");
17452
+ }
17453
+ }
17253
17454
  // src/deployment-topology.ts
17254
- import { isIP as isIP6 } from "net";
17455
+ import { isIP as isIP8 } from "net";
17255
17456
 
17256
17457
  class DeploymentTopologyError extends Error {
17257
17458
  constructor(message) {
@@ -17280,7 +17481,7 @@ function planDeploymentTopology(args) {
17280
17481
  const references = new Set;
17281
17482
  const slots = new Set;
17282
17483
  for (const placement of args.placements) {
17283
- if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) || isIP6(placement.privateAddress) !== 4 || !PRIVATE_V4(placement.privateAddress) || !validSiteCidr(placement.siteCidr) || !placement.privateAddress.startsWith(placement.siteCidr.slice(0, -4)) || !Number.isSafeInteger(placement.slot) || placement.slot < 0 || placement.slot > 1e6 || references.has(placement.computeReference) || slots.has(placement.slot)) {
17484
+ if (!SAFE.test(placement.computeReference) || !SAFE.test(placement.nodeIdentity) || !SAFE.test(placement.metalHostname) || isIP8(placement.privateAddress) !== 4 || !PRIVATE_V4(placement.privateAddress) || !validSiteCidr(placement.siteCidr) || !placement.privateAddress.startsWith(placement.siteCidr.slice(0, -4)) || !Number.isSafeInteger(placement.slot) || placement.slot < 0 || placement.slot > 1e6 || references.has(placement.computeReference) || slots.has(placement.slot)) {
17284
17485
  throw new DeploymentTopologyError("topology placement is malformed or duplicated");
17285
17486
  }
17286
17487
  references.add(placement.computeReference);
@@ -17332,6 +17533,7 @@ function planDeploymentTopology(args) {
17332
17533
  const relay = active.get(site);
17333
17534
  const remoteRelayAddresses = siteNames.filter((other) => other !== site).map((other) => active.get(other).privateAddress);
17334
17535
  const remoteRelayIdentities = siteNames.filter((other) => other !== site).map((other) => active.get(other).nodeIdentity);
17536
+ const remoteMemberAddresses = siteNames.filter((other) => other !== site).flatMap((other) => sites.get(other).map(({ privateAddress }) => privateAddress));
17335
17537
  const remoteSiteCidrs = siteNames.filter((other) => other !== site).map((other) => siteCidrs.get(other));
17336
17538
  return members2.map((placement, index) => {
17337
17539
  const role = index === 0 ? "relay" : index === 1 && topology.relays.standbyPerMetal === 1 ? "standby" : "member";
@@ -17349,6 +17551,7 @@ function planDeploymentTopology(args) {
17349
17551
  localPeerIdentities,
17350
17552
  remoteRelayAddresses: crossSite ? remoteRelayAddresses : [],
17351
17553
  remoteRelayIdentities: crossSite ? remoteRelayIdentities : [],
17554
+ remoteMemberAddresses: siteNames.length > 1 ? remoteMemberAddresses : [],
17352
17555
  remoteSiteCidrs: siteNames.length > 1 ? remoteSiteCidrs : [],
17353
17556
  healthPort: topology.relays.healthPort,
17354
17557
  routedTcpPorts: [...topology.relays.routedTcpPorts],
@@ -17591,6 +17794,20 @@ if (import.meta.main) {
17591
17794
  console.log("project release shape verified");
17592
17795
  process.exit(0);
17593
17796
  }
17797
+ if (command3 === "database-auth-verify") {
17798
+ const values = args.filter((arg) => arg.startsWith("--endpoint="));
17799
+ if (values.length !== 1 || args.length !== 2) {
17800
+ throw new Error("database-auth-verify requires exactly one --endpoint coordinate");
17801
+ }
17802
+ const directory = process.env.CREDENTIALS_DIRECTORY;
17803
+ if (!directory?.startsWith("/"))
17804
+ throw new Error("database-auth-verify requires a systemd credential");
17805
+ await verifyDatabaseAuthentication(values[0].slice("--endpoint=".length), {
17806
+ readCredential: () => readFileSync20(join12(directory, "arangodb-root-password"), "utf8").trim()
17807
+ });
17808
+ console.log("[database] anonymous and empty-root access refused; sealed root credential verified");
17809
+ process.exit(0);
17810
+ }
17594
17811
  const allowFileSeed = process.env.FZ_ALLOW_FILE_SEED === "1";
17595
17812
  if (command3 === "identity") {
17596
17813
  const seed = configuredAgentSeed({