@forgezero/agent 0.1.124 → 0.1.128

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
@@ -4049,7 +4049,7 @@ function combineKEMS(realSeedLen, realMsgLen, expandSeed, combiner, ...kems) {
4049
4049
  var x25519kem = /* @__PURE__ */ ecdhKem(x25519);
4050
4050
  var ml_kem768_x25519 = /* @__PURE__ */ (() => combineKEMS(32, 32, expandSeedXof(shake256), (pk, ct, ss) => sha3_256(concatBytes2(ss[0], ss[1], ct[1], pk[1], asciiToBytes("\\.//^\\"))), ml_kem768, x25519kem))();
4051
4051
 
4052
- // ../../node_modules/.bun/@forgezero+runtime@0.1.17+cd212ec15d36da0a/node_modules/@forgezero/runtime/dist/identity.js
4052
+ // ../runtime/dist/identity.js
4053
4053
  var ENCODER = new TextEncoder;
4054
4054
  var b64 = toBase64Url;
4055
4055
  var un64 = fromBase64Url;
@@ -4196,7 +4196,7 @@ function signRequest(keys, nodeKey, args) {
4196
4196
  };
4197
4197
  }
4198
4198
 
4199
- // ../../node_modules/.bun/@forgezero+vault@0.1.22+3f759277d50bc01a/node_modules/@forgezero/vault/dist/index.js
4199
+ // ../vault/dist/index.js
4200
4200
  var DEFAULT_SOCKET = "/run/forgezero/vault.sock";
4201
4201
 
4202
4202
  // src/socket.ts
@@ -4692,7 +4692,7 @@ import { chmodSync as chmodSync4, existsSync as existsSync4, lstatSync as lstatS
4692
4692
  import { createHash as createHash5, randomUUID } from "crypto";
4693
4693
  import { dirname as dirname3, isAbsolute as isAbsolute2, join as join2 } from "path";
4694
4694
 
4695
- // ../../node_modules/.bun/@forgezero+runtime@0.1.17+cd212ec15d36da0a/node_modules/@forgezero/runtime/dist/queue.js
4695
+ // ../runtime/dist/queue.js
4696
4696
  class QueueStoppedError extends Error {
4697
4697
  constructor() {
4698
4698
  super("queue: stopped before this task could run");
@@ -9640,7 +9640,7 @@ async function writeAndCloseProcessInput(input, value) {
9640
9640
  }
9641
9641
 
9642
9642
  // src/version.ts
9643
- var VERSION2 = "0.1.124";
9643
+ var VERSION2 = "0.1.128";
9644
9644
 
9645
9645
  // src/ssh-bootstrap.ts
9646
9646
  class SshBootstrapError extends Error {
@@ -9655,7 +9655,7 @@ var defaultExec = async (argv2, options = {}) => {
9655
9655
  stdin: options.stdin === undefined ? "ignore" : "pipe",
9656
9656
  stdout: "pipe",
9657
9657
  stderr: "pipe",
9658
- env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C" }
9658
+ env: { PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin", LANG: "C", LC_ALL: "C", ...options.env }
9659
9659
  });
9660
9660
  if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
9661
9661
  await writeAndCloseProcessInput(child.stdin, options.stdin);
@@ -9682,7 +9682,7 @@ var validateClaim = (claim) => {
9682
9682
  }
9683
9683
  if (operatingSystem?.id !== "ubuntu" || !["24.04", "26.04"].includes(operatingSystem.version) || operatingSystem.architecture !== "x64")
9684
9684
  throw new SshBootstrapError("CLAIM_INVALID");
9685
- if (!claim.target.credential || claim.target.credential.source !== "runner" && claim.target.credential.source !== "vault" || claim.target.credential.source === "vault" && (typeof claim.target.credential.privateKey !== "string" || claim.target.credential.privateKey.length > 32768 || !claim.target.credential.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") || !claim.target.credential.privateKey.trimEnd().endsWith("-----END OPENSSH PRIVATE KEY-----"))) {
9685
+ if (!/^ssh-(?:ed25519|rsa|ecdsa-[^\s]+) [A-Za-z0-9+/]+={0,3}(?:\s+.*)?$/.test(claim.runnerSshPublicKey) || !claim.target.credential || claim.target.credential.source !== "runner" && claim.target.credential.source !== "vault" || claim.target.credential.source === "vault" && (claim.target.credential.kind === "private-key" && (typeof claim.target.credential.privateKey !== "string" || claim.target.credential.privateKey.length > 32768 || !claim.target.credential.privateKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----") || !claim.target.credential.privateKey.trimEnd().endsWith("-----END OPENSSH PRIVATE KEY-----")) || claim.target.credential.kind === "password" && (typeof claim.target.credential.password !== "string" || claim.target.credential.password.length < 1 || claim.target.credential.password.length > 4096))) {
9686
9686
  throw new SshBootstrapError("CLAIM_INVALID");
9687
9687
  }
9688
9688
  };
@@ -9693,14 +9693,7 @@ var target = (claim, pinned) => {
9693
9693
  var sshOptions = (claim, pinned, knownHosts, keyPath) => [
9694
9694
  "-F",
9695
9695
  "/dev/null",
9696
- "-o",
9697
- "BatchMode=yes",
9698
- "-o",
9699
- "IdentitiesOnly=yes",
9700
- "-o",
9701
- `IdentityFile=${keyPath}`,
9702
- "-o",
9703
- "IdentityAgent=none",
9696
+ ...claim.target.credential.source === "vault" && claim.target.credential.kind === "password" ? ["-o", "BatchMode=no", "-o", "IdentitiesOnly=yes", "-o", "PubkeyAuthentication=no", "-o", "PreferredAuthentications=password,keyboard-interactive", "-o", "NumberOfPasswordPrompts=1"] : ["-o", "BatchMode=yes", "-o", "IdentitiesOnly=yes", "-o", `IdentityFile=${keyPath}`, "-o", "IdentityAgent=none"],
9704
9697
  "-o",
9705
9698
  `UserKnownHostsFile=${knownHosts}`,
9706
9699
  "-o",
@@ -9787,18 +9780,30 @@ async function executeSshBootstrap(claim, options) {
9787
9780
  const api = new URL(options.platformApiUrl);
9788
9781
  if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash)
9789
9782
  throw new SshBootstrapError("TARGET_API_INVALID");
9790
- const exec = options.exec ?? defaultExec;
9783
+ const baseExec = options.exec ?? defaultExec;
9791
9784
  const directory = mkdtempSync2(join3(tmpdir2(), "forgezero-ssh-bootstrap-"));
9792
9785
  chmodSync7(directory, 448);
9786
+ let exec = baseExec;
9793
9787
  let sshKeyPath = options.sshKeyPath;
9794
9788
  let remoteDirectory = "";
9795
9789
  let pinned;
9796
9790
  try {
9797
- if (claim.target.credential.source === "vault") {
9791
+ if (claim.target.credential.source === "vault" && claim.target.credential.kind === "private-key") {
9798
9792
  sshKeyPath = join3(directory, "identity");
9799
9793
  writeFileSync5(sshKeyPath, `${claim.target.credential.privateKey.trim()}
9800
9794
  `, { mode: 384, flag: "wx" });
9801
9795
  }
9796
+ if (claim.target.credential.source === "vault" && claim.target.credential.kind === "password") {
9797
+ const askpass = join3(directory, "askpass");
9798
+ writeFileSync5(askpass, `#!/usr/bin/env bun
9799
+ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
9800
+ `, { mode: 448, flag: "wx" });
9801
+ const password = claim.target.credential.password;
9802
+ exec = (argv2, command = {}) => baseExec(argv2, {
9803
+ ...command,
9804
+ env: { ...command.env, DISPLAY: "forgezero:0", SSH_ASKPASS: askpass, SSH_ASKPASS_REQUIRE: "force", FZ_SSH_PASSWORD: password }
9805
+ });
9806
+ }
9802
9807
  const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
9803
9808
  const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
9804
9809
  if (!resolved || resolved.protocol !== "ssh")
@@ -9809,6 +9814,21 @@ async function executeSshBootstrap(claim, options) {
9809
9814
  remoteDirectory = await checked(exec, ["ssh", ...ssh, target(claim, pinned), "--", "mktemp", "-d", "/tmp/forgezero-bootstrap.XXXXXXXX"], "SSH_UNREACHABLE");
9810
9815
  if (!/^\/tmp\/forgezero-bootstrap\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
9811
9816
  throw new SshBootstrapError("REMOTE_PATH_INVALID");
9817
+ const destination = target(claim, pinned);
9818
+ const passwd = await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/getent", "passwd", claim.target.user]);
9819
+ const home = passwd.split(":")[5] ?? "";
9820
+ if (!home.startsWith("/") || /[\r\n]/.test(home))
9821
+ throw new SshBootstrapError("SSH_IDENTITY_INSTALL_FAILED");
9822
+ await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-d", "-m", "0700", `${home}/.ssh`]);
9823
+ const existing = await remote(exec, ssh, destination, "SSH_IDENTITY_READ_FAILED", ["/usr/bin/cat", `${home}/.ssh/authorized_keys`]).catch(() => "");
9824
+ const authorized = [...new Set([...existing.split(`
9825
+ `).map((line) => line.trim()).filter(Boolean), claim.runnerSshPublicKey])].join(`
9826
+ `) + `
9827
+ `;
9828
+ const authorizedPath = join3(directory, "authorized_keys");
9829
+ writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
9830
+ await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
9831
+ await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-m", "0600", `${remoteDirectory}/authorized_keys`, `${home}/.ssh/authorized_keys`]);
9812
9832
  const config = {
9813
9833
  kind: "enrolled-compute",
9814
9834
  operatingSystem: claim.target.operatingSystem,
@@ -9844,7 +9864,6 @@ async function executeSshBootstrap(claim, options) {
9844
9864
  join3(directory, name),
9845
9865
  `${target(claim, pinned)}:${remoteDirectory}/${name}`
9846
9866
  ], "SCP_FAILED", { secret: name === "enrol.token" });
9847
- const destination = target(claim, pinned);
9848
9867
  const version = await remote(exec, ssh, destination, "REMOTE_BUN_CHECK_FAILED", ["/usr/local/bin/bun", "--version"]).catch(() => "");
9849
9868
  if (version !== PINNED_BUN_VERSION) {
9850
9869
  await remote(exec, ssh, destination, "REMOTE_PREREQUISITES_FAILED", [
@@ -10093,6 +10112,7 @@ var validateMetalClaim = (claim) => {
10093
10112
  realm: claim.tenantKey,
10094
10113
  projectKey: claim.projectKey,
10095
10114
  environmentKey: claim.environmentKey,
10115
+ runnerSshPublicKey: claim.runnerSshPublicKey,
10096
10116
  target: claim.target,
10097
10117
  enrolmentToken: `fze_${"A".repeat(43)}`
10098
10118
  });
@@ -10104,18 +10124,30 @@ async function executeMetalAdmission(claim, options) {
10104
10124
  if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash || telemetry.protocol !== "https:" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
10105
10125
  throw new SshBootstrapError("TARGET_COORDINATE_INVALID");
10106
10126
  }
10107
- const exec = options.exec ?? defaultExec;
10127
+ const baseExec = options.exec ?? defaultExec;
10108
10128
  const directory = mkdtempSync2(join3(tmpdir2(), "forgezero-metal-admission-"));
10109
10129
  chmodSync7(directory, 448);
10130
+ let exec = baseExec;
10110
10131
  let sshKeyPath = options.sshKeyPath;
10111
10132
  let remoteDirectory = "";
10112
10133
  let pinned;
10113
10134
  try {
10114
- if (claim.target.credential.source === "vault") {
10135
+ if (claim.target.credential.source === "vault" && claim.target.credential.kind === "private-key") {
10115
10136
  sshKeyPath = join3(directory, "identity");
10116
10137
  writeFileSync5(sshKeyPath, `${claim.target.credential.privateKey.trim()}
10117
10138
  `, { mode: 384, flag: "wx" });
10118
10139
  }
10140
+ if (claim.target.credential.source === "vault" && claim.target.credential.kind === "password") {
10141
+ const askpass = join3(directory, "askpass");
10142
+ writeFileSync5(askpass, `#!/usr/bin/env bun
10143
+ process.stdout.write(process.env.FZ_SSH_PASSWORD ?? "")
10144
+ `, { mode: 448, flag: "wx" });
10145
+ const password = claim.target.credential.password;
10146
+ exec = (argv2, command = {}) => baseExec(argv2, {
10147
+ ...command,
10148
+ env: { ...command.env, DISPLAY: "forgezero:0", SSH_ASKPASS: askpass, SSH_ASKPASS_REQUIRE: "force", FZ_SSH_PASSWORD: password }
10149
+ });
10150
+ }
10119
10151
  const sourceHost = claim.target.host.includes(":") ? `[${claim.target.host}]` : claim.target.host;
10120
10152
  const resolved = await resolvePinnedGitTarget(`ssh://${claim.target.user}@${sourceHost}:${claim.target.port}/`, options.resolveHost);
10121
10153
  if (!resolved || resolved.protocol !== "ssh")
@@ -10127,6 +10159,20 @@ async function executeMetalAdmission(claim, options) {
10127
10159
  remoteDirectory = await checked(exec, ["ssh", ...ssh, destination, "--", "mktemp", "-d", "/tmp/forgezero-metal-admission.XXXXXXXX"], "SSH_UNREACHABLE");
10128
10160
  if (!/^\/tmp\/forgezero-metal-admission\.[A-Za-z0-9]{8}$/.test(remoteDirectory))
10129
10161
  throw new SshBootstrapError("REMOTE_PATH_INVALID");
10162
+ const passwd = await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/getent", "passwd", claim.target.user]);
10163
+ const home = passwd.split(":")[5] ?? "";
10164
+ if (!home.startsWith("/") || /[\r\n]/.test(home))
10165
+ throw new SshBootstrapError("SSH_IDENTITY_INSTALL_FAILED");
10166
+ await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-d", "-m", "0700", `${home}/.ssh`]);
10167
+ const existing = await remote(exec, ssh, destination, "SSH_IDENTITY_READ_FAILED", ["/usr/bin/cat", `${home}/.ssh/authorized_keys`]).catch(() => "");
10168
+ const authorized = [...new Set([...existing.split(`
10169
+ `).map((line) => line.trim()).filter(Boolean), claim.runnerSshPublicKey])].join(`
10170
+ `) + `
10171
+ `;
10172
+ const authorizedPath = join3(directory, "authorized_keys");
10173
+ writeFileSync5(authorizedPath, authorized, { mode: 384, flag: "wx" });
10174
+ await checked(exec, ["scp", ...scpOptions(claim, pinned, knownHosts, sshKeyPath), authorizedPath, `${destination}:${remoteDirectory}/authorized_keys`], "SSH_IDENTITY_INSTALL_FAILED");
10175
+ await remote(exec, ssh, destination, "SSH_IDENTITY_INSTALL_FAILED", ["/usr/bin/install", "-m", "0600", `${remoteDirectory}/authorized_keys`, `${home}/.ssh/authorized_keys`]);
10130
10176
  const fzCliPath = options.fzCliPath ?? fileURLToPath(new URL("./fz.js", import.meta.url));
10131
10177
  const fzAgentPath = options.fzAgentPath ?? fileURLToPath(new URL("./fz-agent.js", import.meta.url));
10132
10178
  for (const [source, name] of [[fzCliPath, "fz.js"], [fzAgentPath, "fz-agent.js"]]) {
@@ -11363,7 +11409,7 @@ function requestDeploymentCommand(input, socketPath = DEFAULT_DEPLOYMENT_RUNNER_
11363
11409
  // src/snp-attestation.ts
11364
11410
  import { existsSync as existsSync10 } from "fs";
11365
11411
 
11366
- // ../../node_modules/.bun/@forgezero+runtime@0.1.17+cd212ec15d36da0a/node_modules/@forgezero/runtime/dist/snp.js
11412
+ // ../runtime/dist/snp.js
11367
11413
  var REPORT_BYTES = 1184;
11368
11414
 
11369
11415
  // src/snp-attestation.ts
@@ -15302,6 +15348,26 @@ var nonEmpty = (path2) => {
15302
15348
  if (!existsSync20(path2) || !statSync5(path2).isFile() || statSync5(path2).size < 1)
15303
15349
  throw new Error(`required file missing: ${path2}`);
15304
15350
  };
15351
+ function validateRecoveryBootstrapState(value, expected) {
15352
+ if (!value || typeof value !== "object" || Array.isArray(value))
15353
+ throw new Error("bootstrap state is malformed");
15354
+ const state = value;
15355
+ if (state.format !== 2 || state.kind !== "platform" || state.environment !== expected.profile || state.databaseRole !== expected.role || state.profile !== expected.software || state.apiUrl !== expected.apiOrigin || state.nodeHostname !== expected.edgeHostname || expected.databaseAddress !== undefined && state.databaseAddress !== expected.databaseAddress)
15356
+ throw new Error("bootstrap state does not match reviewed recovery coordinates");
15357
+ }
15358
+ var exactBootstrapState = (expected) => {
15359
+ const path2 = "/var/lib/forgezero/bootstrap.json";
15360
+ nonEmpty(path2);
15361
+ if (statSync5(path2).size > 64 * 1024)
15362
+ throw new Error("bootstrap state is oversized");
15363
+ let value;
15364
+ try {
15365
+ value = JSON.parse(readFileSync15(path2, "utf8"));
15366
+ } catch {
15367
+ throw new Error("bootstrap state is malformed");
15368
+ }
15369
+ validateRecoveryBootstrapState(value, expected);
15370
+ };
15305
15371
  var option = (args, name) => {
15306
15372
  const value = args.find((arg) => arg.startsWith(`--${name}=`))?.slice(name.length + 3);
15307
15373
  if (!value || /[\0\r\n]/.test(value))
@@ -15325,15 +15391,11 @@ async function runRecoveryHost(args, run2 = execute2) {
15325
15391
  const apiOrigin = option(args, "api-origin");
15326
15392
  const edgeHostname = option(args, "edge-hostname");
15327
15393
  const databaseAddress = args.find((arg) => arg.startsWith("--db-address="))?.slice(13);
15328
- exactState("/opt/forgezero/.forge-state", `profile=${profile}`);
15329
- exactState("/opt/forgezero/.forge-state", `db_role=${role}`);
15330
- exactState("/opt/forgezero/.forge-state", `software_profile=${software}`);
15331
- if (databaseAddress)
15332
- exactState("/opt/forgezero/.forge-state", `db_address=${databaseAddress}`);
15333
- exactState("/opt/forgezero/shared/.env", "ARANGO_DB=fz");
15334
- exactState("/opt/forgezero/shared/.env", "FZ_DATABASE_MODE=platform");
15335
- exactState("/opt/forgezero/shared/.env", `API_ORIGIN=${apiOrigin}`);
15336
- exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME=${edgeHostname}`);
15394
+ exactBootstrapState({ profile, role, software, apiOrigin, edgeHostname, ...databaseAddress ? { databaseAddress } : {} });
15395
+ exactState("/opt/forgezero/shared/.env", 'ARANGO_DB="fz"');
15396
+ exactState("/opt/forgezero/shared/.env", 'FZ_DATABASE_MODE="platform"');
15397
+ exactState("/opt/forgezero/shared/.env", `API_ORIGIN="${apiOrigin}"`);
15398
+ exactState("/opt/forgezero/shared/.env", `FZ_NODE_HOSTNAME="${edgeHostname}"`);
15337
15399
  nonEmpty("/etc/forgezero/creds/arangodb-jwt.cred");
15338
15400
  const hasDatabase = role !== "none";
15339
15401
  if (hasDatabase) {
@@ -15377,7 +15439,7 @@ async function runRecoveryHost(args, run2 = execute2) {
15377
15439
  throw new Error("maintenance accepts only fz db argv");
15378
15440
  const slot = activeSlot();
15379
15441
  const properties = [
15380
- "--property=User=forgezero",
15442
+ "--property=User=forgezero-api",
15381
15443
  `--property=WorkingDirectory=/opt/forgezero/slots/${slot}`,
15382
15444
  "--property=Environment=NODE_ENV=production",
15383
15445
  "--property=EnvironmentFile=/opt/forgezero/shared/.env",
@@ -17125,6 +17187,8 @@ var COMMUNITY_REHEARSAL_COORDINATOR_PORT = 18529;
17125
17187
  var COMMUNITY_REHEARSAL_DATABASE_PORT_RANGE = "18528:18539";
17126
17188
  var COMMUNITY_REHEARSAL_API_PORT = 18787;
17127
17189
  var COMMUNITY_REHEARSAL_FIREWALL_COMMENT = "fz-community-rehearsal";
17190
+ var COMMUNITY_REHEARSAL_EGRESS_TABLE = "forgezero_agent_egress";
17191
+ var COMMUNITY_REHEARSAL_EGRESS_CHAIN = "output";
17128
17192
  var RELEASE = /^[a-f0-9]{64}$/;
17129
17193
  var SAFE_NAME2 = /^[a-z][a-z0-9-]{0,62}$/;
17130
17194
  async function ensureCommunityRehearsalArango(execute3 = executeSoftwareOperation) {
@@ -17288,6 +17352,7 @@ function planCommunityRehearsalPrepare(request) {
17288
17352
  { kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-community-api.service"], accepted: [0, 5] },
17289
17353
  { kind: "exec", argv: ["/usr/bin/systemctl", "stop", "forgezero-rehearsal-db.service"], accepted: [0, 5] },
17290
17354
  { kind: "clear-firewall" },
17355
+ { kind: "prepare-egress", addresses: topology.map(({ address }) => address) },
17291
17356
  ...topology.map(({ address }) => ({
17292
17357
  kind: "exec",
17293
17358
  argv: [
@@ -17350,6 +17415,7 @@ function planCommunityRehearsalCleanup() {
17350
17415
  { kind: "remove-tree", path: COMMUNITY_DATABASE_ROOT },
17351
17416
  { kind: "remove-tree", path: COMMUNITY_REHEARSAL_ROOT },
17352
17417
  { kind: "remove-tree", path: COMMUNITY_CREDENTIAL_PARENT },
17418
+ { kind: "clear-egress" },
17353
17419
  { kind: "clear-firewall" },
17354
17420
  { kind: "exec", argv: ["/usr/bin/systemctl", "daemon-reload"] },
17355
17421
  { kind: "exec", argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-community-api.service", "forgezero-rehearsal-db.service"], accepted: [0, 1, 5] }
@@ -17372,6 +17438,10 @@ async function applyOperations(operations) {
17372
17438
  rmSync13(operation.path, { recursive: true, force: true });
17373
17439
  else if (operation.kind === "clear-firewall")
17374
17440
  await clearCommunityRehearsalFirewall();
17441
+ else if (operation.kind === "prepare-egress")
17442
+ await prepareCommunityRehearsalEgress(operation.addresses);
17443
+ else if (operation.kind === "clear-egress")
17444
+ await clearCommunityRehearsalEgress();
17375
17445
  else if (operation.kind === "write") {
17376
17446
  mkdirSync17(dirname15(operation.path), { recursive: true });
17377
17447
  writeFileSync17(operation.path, operation.content, { mode: operation.mode });
@@ -17402,6 +17472,49 @@ function communityRehearsalFirewallRuleNumbers(status) {
17402
17472
  return match ? [Number(match[1])] : [];
17403
17473
  }).filter((number2) => Number.isSafeInteger(number2) && number2 > 0).sort((left, right) => right - left);
17404
17474
  }
17475
+ function communityRehearsalEgressRuleHandles(ruleset) {
17476
+ return ruleset.split(/\r?\n/).flatMap((line) => {
17477
+ if (!line.includes(`comment "${COMMUNITY_REHEARSAL_FIREWALL_COMMENT}"`))
17478
+ return [];
17479
+ const match = line.match(/# handle (\d+)\s*$/);
17480
+ return match ? [Number(match[1])] : [];
17481
+ }).filter((number2) => Number.isSafeInteger(number2) && number2 > 0).sort((left, right) => right - left);
17482
+ }
17483
+ function communityRehearsalEgressRuleArguments(uid, addresses) {
17484
+ if (!/^\d+$/.test(uid) || addresses.some((address) => isIP6(address) !== 4) || new Set(addresses).size !== addresses.length) {
17485
+ throw new Error("rehearsal egress arguments require one uid and unique IPv4 addresses");
17486
+ }
17487
+ return addresses.flatMap((address) => [
17488
+ [
17489
+ "meta",
17490
+ "skuid",
17491
+ uid,
17492
+ "ct",
17493
+ "state",
17494
+ "established,related",
17495
+ "ip",
17496
+ "daddr",
17497
+ address,
17498
+ "accept",
17499
+ "comment",
17500
+ COMMUNITY_REHEARSAL_FIREWALL_COMMENT
17501
+ ],
17502
+ [
17503
+ "meta",
17504
+ "skuid",
17505
+ uid,
17506
+ "ip",
17507
+ "daddr",
17508
+ address,
17509
+ "tcp",
17510
+ "dport",
17511
+ String(COMMUNITY_REHEARSAL_COORDINATOR_PORT),
17512
+ "accept",
17513
+ "comment",
17514
+ COMMUNITY_REHEARSAL_FIREWALL_COMMENT
17515
+ ]
17516
+ ]);
17517
+ }
17405
17518
  async function clearCommunityRehearsalFirewall() {
17406
17519
  const status = await exec(["/usr/sbin/ufw", "status", "numbered"]);
17407
17520
  if (status.exitCode !== 0)
@@ -17413,6 +17526,71 @@ async function clearCommunityRehearsalFirewall() {
17413
17526
  }
17414
17527
  }
17415
17528
  }
17529
+ var rehearsalEgressRules = async () => {
17530
+ const result = await exec([
17531
+ "/usr/sbin/nft",
17532
+ "-a",
17533
+ "list",
17534
+ "chain",
17535
+ "inet",
17536
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
17537
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN
17538
+ ]);
17539
+ if (result.exitCode === 0)
17540
+ return { exists: true, output: result.output };
17541
+ if (/No such file or directory|does not exist/i.test(result.output))
17542
+ return { exists: false, output: result.output };
17543
+ throw new Error(`unable to inspect rehearsal egress rules: ${result.output.trim()}`);
17544
+ };
17545
+ async function clearCommunityRehearsalEgress() {
17546
+ const listed = await rehearsalEgressRules();
17547
+ if (!listed.exists)
17548
+ return;
17549
+ for (const handle of communityRehearsalEgressRuleHandles(listed.output)) {
17550
+ const removed = await exec([
17551
+ "/usr/sbin/nft",
17552
+ "delete",
17553
+ "rule",
17554
+ "inet",
17555
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
17556
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN,
17557
+ "handle",
17558
+ String(handle)
17559
+ ]);
17560
+ if (removed.exitCode !== 0) {
17561
+ throw new Error(`unable to remove rehearsal egress rule ${handle}: ${removed.output.trim()}`);
17562
+ }
17563
+ }
17564
+ }
17565
+ async function prepareCommunityRehearsalEgress(addresses) {
17566
+ await clearCommunityRehearsalEgress();
17567
+ if (!(await rehearsalEgressRules()).exists)
17568
+ return;
17569
+ const identity = await exec(["/usr/bin/id", "-u", "forgezero"]);
17570
+ const uid = identity.output.trim();
17571
+ if (identity.exitCode !== 0 || !/^\d+$/.test(uid))
17572
+ throw new Error("unable to resolve the forgezero service uid");
17573
+ for (const rule of communityRehearsalEgressRuleArguments(uid, addresses)) {
17574
+ const inserted = await exec([
17575
+ "/usr/sbin/nft",
17576
+ "insert",
17577
+ "rule",
17578
+ "inet",
17579
+ COMMUNITY_REHEARSAL_EGRESS_TABLE,
17580
+ COMMUNITY_REHEARSAL_EGRESS_CHAIN,
17581
+ ...rule
17582
+ ]);
17583
+ if (inserted.exitCode !== 0) {
17584
+ await clearCommunityRehearsalEgress();
17585
+ throw new Error(`unable to install a rehearsal egress rule: ${inserted.output.trim()}`);
17586
+ }
17587
+ }
17588
+ const installed = communityRehearsalEgressRuleHandles((await rehearsalEgressRules()).output);
17589
+ if (installed.length !== addresses.length * 2) {
17590
+ await clearCommunityRehearsalEgress();
17591
+ throw new Error("rehearsal egress rule verification failed");
17592
+ }
17593
+ }
17416
17594
  var responseFor = async (request) => {
17417
17595
  const node = request.topology.find((entry) => entry.name === request.node);
17418
17596
  const base = `http://${node.address}:${COMMUNITY_REHEARSAL_API_PORT}`;