@forgezero/agent 0.1.40 → 0.1.42

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.
Files changed (64) hide show
  1. package/README.md +573 -424
  2. package/dist/agent-heartbeat.js +6 -3
  3. package/dist/agent-update-helper.js +5 -2
  4. package/dist/agent-update.js +5 -2
  5. package/dist/bootstrap.d.ts +17 -8
  6. package/dist/bootstrap.js +1504 -512
  7. package/dist/cli/agent-install.d.ts +6 -5
  8. package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
  9. package/dist/cli/maintenance.d.ts +23 -0
  10. package/dist/cli/run.d.ts +3 -1
  11. package/dist/cli/session-store.d.ts +5 -0
  12. package/dist/cloudflare-bootstrap.d.ts +73 -35
  13. package/dist/cloudflare-bootstrap.js +587 -90
  14. package/dist/cloudflare-edge.d.ts +64 -12
  15. package/dist/cloudflare-edge.js +103 -8
  16. package/dist/community-rehearsal-host.d.ts +51 -0
  17. package/dist/community-rehearsal-host.js +272 -0
  18. package/dist/credential-schema.d.ts +54 -0
  19. package/dist/credential-schema.js +47 -0
  20. package/dist/definition.d.ts +31 -5
  21. package/dist/definition.js +271 -44
  22. package/dist/deploy-file.js +294 -68
  23. package/dist/deployment-runner.js +18 -5
  24. package/dist/deployment.d.ts +13 -1
  25. package/dist/fz-agent.js +9162 -7564
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +5679 -5077
  28. package/dist/git-ssh.d.ts +5 -0
  29. package/dist/guest-enrolment.d.ts +2 -0
  30. package/dist/guest-enrolment.js +1 -0
  31. package/dist/host-maintenance.d.ts +39 -0
  32. package/dist/host-maintenance.js +135 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/mesh-connector.d.ts +16 -0
  35. package/dist/mesh-connector.js +46 -0
  36. package/dist/metal-bootstrap.js +145 -7
  37. package/dist/metal-helper-socket.js +61 -31
  38. package/dist/metal-provision.d.ts +2 -2
  39. package/dist/metal-provision.js +62 -32
  40. package/dist/operator-bootstrap.d.ts +90 -0
  41. package/dist/operator-bootstrap.js +5704 -0
  42. package/dist/otel-collector.d.ts +18 -0
  43. package/dist/pipeline.d.ts +3 -2
  44. package/dist/pipeline.js +1 -1
  45. package/dist/platform-bootstrap-runtime.d.ts +39 -21
  46. package/dist/platform-bootstrap-runtime.js +182 -59
  47. package/dist/platform-fleet-verification.d.ts +19 -0
  48. package/dist/platform-fleet-verification.js +3873 -0
  49. package/dist/platform-genesis-config.d.ts +7 -0
  50. package/dist/platform-genesis.d.ts +17 -0
  51. package/dist/provision.d.ts +76 -3
  52. package/dist/provision.js +1061 -229
  53. package/dist/recovery-host.d.ts +7 -0
  54. package/dist/recovery-host.js +124 -0
  55. package/dist/service-supervisor.d.ts +42 -0
  56. package/dist/software-helper.d.ts +4 -0
  57. package/dist/software-helper.js +865 -63
  58. package/dist/software.d.ts +14 -3
  59. package/dist/software.js +163 -37
  60. package/dist/ssh-bootstrap.d.ts +97 -0
  61. package/dist/supervised-app.d.ts +2 -0
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +175 -164
  64. package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
@@ -284,6 +284,51 @@ var AGENT_CREDENTIAL_LOCATIONS = [
284
284
  "platform-compute",
285
285
  "tenant-compute"
286
286
  ];
287
+ var CLOUDFLARE_CREDENTIAL_NAMES = {
288
+ api: "CF_API_TOKEN",
289
+ tunnel: "CF_TUNNEL_TOKEN",
290
+ connector: "CF_TUNNEL_CONNECTOR_TOKEN",
291
+ realtimePublish: "REALTIME_PUBLISH_SECRET",
292
+ realtimeTicket: "REALTIME_TICKET_SECRET"
293
+ };
294
+ var CLOUDFLARE_CREDENTIAL_SCHEMA = {
295
+ CF_TUNNEL_TOKEN: {
296
+ permissions: [
297
+ "Account:Cloudflare Tunnel Write",
298
+ "Account:Cloudflare One Connector: WARP Write",
299
+ "Account:Cloudflare One Networks Write",
300
+ "Account:Zero Trust Write",
301
+ "Zone:DNS Write"
302
+ ],
303
+ platformBootstrap: "attended-file",
304
+ platformRuntime: "vault",
305
+ tenantControl: "vault"
306
+ },
307
+ CF_API_TOKEN: {
308
+ permissions: ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"],
309
+ platformBootstrap: "attended-file",
310
+ platformRuntime: "vault-then-systemd",
311
+ tenantControl: "vault"
312
+ },
313
+ CF_TUNNEL_CONNECTOR_TOKEN: {
314
+ permissions: ["one named Tunnel connector"],
315
+ platformBootstrap: "derived",
316
+ platformRuntime: "systemd",
317
+ tenantControl: "derived"
318
+ },
319
+ REALTIME_PUBLISH_SECRET: {
320
+ permissions: ["Worker realtime publish endpoint"],
321
+ platformBootstrap: "generated-and-installed-worker-secret",
322
+ platformRuntime: "vault-then-systemd",
323
+ tenantControl: "vault"
324
+ },
325
+ REALTIME_TICKET_SECRET: {
326
+ permissions: ["Worker realtime subscription tickets"],
327
+ platformBootstrap: "generated-and-installed-worker-secret",
328
+ platformRuntime: "vault-then-systemd",
329
+ tenantControl: "vault"
330
+ }
331
+ };
287
332
  var AGENT_CREDENTIAL_POLICY = {
288
333
  operator: { vault: false, systemdFallback: false, attendedFile: true },
289
334
  metal: { vault: false, systemdFallback: true, attendedFile: false },
@@ -330,6 +375,8 @@ export {
330
375
  deploymentCredentialSchema,
331
376
  credentialBinding,
332
377
  METAL_SYSTEMD_CREDENTIALS,
378
+ CLOUDFLARE_CREDENTIAL_SCHEMA,
379
+ CLOUDFLARE_CREDENTIAL_NAMES,
333
380
  AGENT_CREDENTIAL_POLICY,
334
381
  AGENT_CREDENTIAL_LOCATIONS
335
382
  };
@@ -2,17 +2,42 @@ import type { Pipeline, PipelineStep } from './pipeline';
2
2
  import { type DeploymentChannel, type SoftwareRequirement } from './software';
3
3
  import { type CapacityCalibrationOptions } from './capacity-calibration';
4
4
  /**
5
- * Version two separates a repository's deploy recipe from the computes that use
6
- * it. A target chooses one named profile in the control plane; compute names,
7
- * counts and cluster leadership never belong in Git.
5
+ * Version three keeps fleet coordinates out of Git and replaces shell command
6
+ * strings with exact argv vectors. A target chooses one named profile in the
7
+ * control plane; compute names, counts and cluster leadership never belong in
8
+ * the repository definition.
8
9
  */
9
- export declare const PIPELINE_VERSION: 2;
10
- export declare const DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
10
+ export declare const PIPELINE_VERSION: 3;
11
+ export declare const DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v3.json";
11
12
  export interface PipelineProfile {
12
13
  software: readonly SoftwareRequirement[];
14
+ /** Optional Agent-supervised HTTP service behind one stable nginx port. */
15
+ service?: DeploymentService;
13
16
  /** Explicit opt-in, provider-neutral loopback GET probe run after target health. */
14
17
  capacityCalibration?: CapacityCalibrationOptions;
15
18
  }
19
+ export type DeploymentPortAllocation = {
20
+ mode: 'fixed';
21
+ ports: readonly number[];
22
+ } | {
23
+ mode: 'dynamic';
24
+ from: number;
25
+ to: number;
26
+ };
27
+ export interface DeploymentService {
28
+ strategy: 'direct' | 'blue-green';
29
+ /** Stable loopback ingress used by Tunnel/nginx consumers. */
30
+ publicPort: number;
31
+ /** One application port for direct, two for blue-green. */
32
+ applicationPorts: DeploymentPortAllocation;
33
+ /** Exact argv. `${FZ_APP_PORT}` and `${FZ_RELEASE}` are materialized by the Agent. */
34
+ command: readonly string[];
35
+ healthPath: string;
36
+ /** Optional node-wide active connection ceiling enforced by nginx with HTTP 503. */
37
+ maxConnections?: number;
38
+ websocket?: boolean;
39
+ drainMs?: number;
40
+ }
16
41
  export interface DeployStep extends PipelineStep {
17
42
  phase: 'build' | 'release' | 'migrate' | 'health';
18
43
  /** Run on this target, or on the one deterministic release executor. */
@@ -32,6 +57,7 @@ export interface DeployDefinition {
32
57
  export declare class DefinitionError extends Error {
33
58
  constructor(message: string);
34
59
  }
60
+ export declare function validateDeploymentService(value: unknown, where?: string): DeploymentService;
35
61
  /** Validate parsed JSON before any command from it is allowed to run. */
36
62
  export declare function parseDeployDefinition(value: unknown, options?: {
37
63
  channel?: DeploymentChannel;
@@ -1,7 +1,22 @@
1
1
  // src/software.ts
2
- import { readFileSync } from "node:fs";
2
+ import { createHash } from "node:crypto";
3
+ import {
4
+ accessSync,
5
+ chmodSync,
6
+ copyFileSync,
7
+ mkdtempSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ unlinkSync,
14
+ writeFileSync
15
+ } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
3
18
  var PINNED_BUN_VERSION = "1.3.14";
4
- var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
19
+ var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
5
20
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
6
21
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
7
22
  var OS_CATALOG = [
@@ -12,41 +27,151 @@ var SOFTWARE_CATALOG = [
12
27
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
28
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
14
29
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
30
+ { id: "cloudflare-warp", version: "2026.6.822.0-min", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
15
31
  { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
16
32
  { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
17
33
  ];
18
34
  var UBUNTU_2604_X64 = [
19
- {
20
- requirement: { id: "bun", version: "1.3.14" },
21
- check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
22
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
23
- },
24
- {
25
- requirement: { id: "nginx", version: "ubuntu-26.04" },
26
- check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
27
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
28
- },
29
- {
30
- requirement: { id: "arangodb", version: "3.11.14" },
31
- check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14' && ` + `! systemctl is-active --quiet arangodb3.service && ` + `! systemctl is-enabled --quiet arangodb3.service`,
32
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install; ` + `systemctl disable --now arangodb3.service`
33
- },
34
- {
35
- requirement: { id: "cloudflared", version: "2026.7.3" },
36
- check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
37
- install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
38
- },
39
- {
40
- requirement: { id: "ufw", version: "ubuntu-26.04" },
41
- check: "command -v ufw >/dev/null",
42
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
43
- },
44
- {
45
- requirement: { id: "openssh-client", version: "ubuntu-26.04" },
46
- check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
47
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
48
- }
35
+ { requirement: { id: "bun", version: "1.3.14" } },
36
+ { requirement: { id: "nginx", version: "ubuntu-26.04" } },
37
+ { requirement: { id: "arangodb", version: "3.11.14" } },
38
+ { requirement: { id: "cloudflared", version: "2026.7.3" } },
39
+ { requirement: { id: "cloudflare-warp", version: "2026.6.822.0-min" } },
40
+ { requirement: { id: "ufw", version: "ubuntu-26.04" } },
41
+ { requirement: { id: "openssh-client", version: "ubuntu-26.04" } }
49
42
  ];
43
+ var path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
44
+ var run = async (argv, env = {}) => {
45
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: { PATH: path, LANG: "C", LC_ALL: "C", ...env } });
46
+ const [stdout, stderr, exitCode] = await Promise.all([
47
+ new Response(child.stdout).text(),
48
+ new Response(child.stderr).text(),
49
+ child.exited
50
+ ]);
51
+ return { exitCode, output: `${stdout}${stderr}` };
52
+ };
53
+ var download = async (url, destination, sha256) => {
54
+ const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(120000) });
55
+ if (!response.ok)
56
+ throw new Error(`download failed with HTTP ${response.status}`);
57
+ const bytes = new Uint8Array(await response.arrayBuffer());
58
+ if (createHash("sha256").update(bytes).digest("hex") !== sha256)
59
+ throw new Error("download checksum mismatch");
60
+ writeFileSync(destination, bytes, { mode: 384, flag: "wx" });
61
+ };
62
+ var successful = (result, pattern) => result.exitCode === 0 && (!pattern || pattern.test(result.output));
63
+ var aptInstall = async (name) => {
64
+ const environment = { DEBIAN_FRONTEND: "noninteractive" };
65
+ const update = await run(["/usr/bin/apt-get", "update", "-qq"], environment);
66
+ return update.exitCode === 0 ? run(["/usr/bin/apt-get", "install", "-y", name], environment) : update;
67
+ };
68
+ async function executeSoftwareOperation(operation) {
69
+ const { software, version } = operation;
70
+ if (!UBUNTU_2604_X64.some(({ requirement }) => requirement.id === software && requirement.version === version)) {
71
+ return { exitCode: 2, output: "unsupported software operation" };
72
+ }
73
+ if (operation.kind === "check") {
74
+ if (software === "bun")
75
+ return run(["/usr/local/bin/bun", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /^1\.3\.14\s*$/m) ? 0 : 1 }));
76
+ if (software === "nginx") {
77
+ const binary = await run(["/usr/sbin/nginx", "-v"]);
78
+ return binary.exitCode === 0 ? run(["/usr/bin/systemctl", "is-active", "--quiet", "nginx.service"]) : binary;
79
+ }
80
+ if (software === "arangodb") {
81
+ const binary = await run(["/usr/bin/arangod", "--version"]);
82
+ if (!successful(binary, /3\.11\.14/))
83
+ return { ...binary, exitCode: 1 };
84
+ const [active, enabled] = await Promise.all([
85
+ run(["/usr/bin/systemctl", "is-active", "--quiet", "arangodb3.service"]),
86
+ run(["/usr/bin/systemctl", "is-enabled", "--quiet", "arangodb3.service"])
87
+ ]);
88
+ return active.exitCode !== 0 && enabled.exitCode !== 0 ? { exitCode: 0, output: binary.output } : { exitCode: 1, output: "vendor standalone unit remains active or enabled" };
89
+ }
90
+ if (software === "cloudflared")
91
+ return run(["/usr/local/bin/cloudflared", "--version"]).then((r) => ({ ...r, exitCode: successful(r, /2026\.7\.3/) ? 0 : 1 }));
92
+ if (software === "cloudflare-warp")
93
+ return run(["/usr/bin/warp-cli", "--version"]).then((result) => {
94
+ const match = result.output.match(/(\d{4})\.(\d+)\.(\d+)\.(\d+)/);
95
+ const observed = match?.slice(1).map(Number);
96
+ const minimum = [2026, 6, 822, 0];
97
+ const supported = observed && observed.some((part, index) => part > minimum[index] && observed.slice(0, index).every((prior, priorIndex) => prior === minimum[priorIndex])) || observed?.every((part, index) => part === minimum[index]);
98
+ return { ...result, exitCode: result.exitCode === 0 && supported ? 0 : 1 };
99
+ });
100
+ const binaries = software === "ufw" ? ["/usr/sbin/ufw"] : ["/usr/bin/ssh", "/usr/bin/scp", "/usr/bin/ssh-keyscan", "/usr/bin/ssh-keygen"];
101
+ try {
102
+ binaries.forEach((binary) => accessSync(binary));
103
+ return { exitCode: 0, output: "" };
104
+ } catch (cause) {
105
+ return { exitCode: 1, output: cause instanceof Error ? cause.message : String(cause) };
106
+ }
107
+ }
108
+ if (software === "nginx" || software === "ufw" || software === "openssh-client") {
109
+ const installed = await aptInstall(software === "openssh-client" ? "openssh-client" : software);
110
+ if (installed.exitCode !== 0 || software !== "nginx")
111
+ return installed;
112
+ return run(["/usr/bin/systemctl", "enable", "--now", "nginx.service"]);
113
+ }
114
+ const directory = mkdtempSync(join(tmpdir(), "forgezero-software-"));
115
+ try {
116
+ if (software === "cloudflare-warp") {
117
+ const key = join(directory, "cloudflare-warp-key.gpg");
118
+ await download("https://pkg.cloudflareclient.com/pubkey.gpg", key, "0f37fc298c98e88ee3c0ee68c95b69f1dba9eb477abe3167e13982105911264d");
119
+ mkdirSync("/usr/share/keyrings", { recursive: true, mode: 493 });
120
+ const dearmored = await run([
121
+ "/usr/bin/gpg",
122
+ "--batch",
123
+ "--yes",
124
+ "--dearmor",
125
+ "-o",
126
+ "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg",
127
+ key
128
+ ]);
129
+ if (dearmored.exitCode !== 0)
130
+ return dearmored;
131
+ mkdirSync("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
132
+ writeFileSync("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ resolute main
133
+ `, { mode: 420 });
134
+ return aptInstall("cloudflare-warp");
135
+ }
136
+ if (software === "bun") {
137
+ const archive = join(directory, "bun.zip");
138
+ await download("https://github.com/oven-sh/bun/releases/download/bun-v1.3.14/bun-linux-x64.zip", archive, BUN_RELEASE_SHA256);
139
+ const unpacked = join(directory, "unpacked");
140
+ mkdirSync(unpacked, { mode: 448 });
141
+ const unzipped = await run(["/usr/bin/unzip", "-q", archive, "-d", unpacked]);
142
+ if (unzipped.exitCode !== 0)
143
+ return unzipped;
144
+ mkdirSync("/usr/local/lib/forgezero/runtime", { recursive: true, mode: 493 });
145
+ copyFileSync(join(unpacked, "bun-linux-x64", "bun"), "/usr/local/lib/forgezero/runtime/bun.next");
146
+ chmodSync("/usr/local/lib/forgezero/runtime/bun.next", 493);
147
+ renameSync("/usr/local/lib/forgezero/runtime/bun.next", "/usr/local/lib/forgezero/runtime/bun");
148
+ try {
149
+ unlinkSync("/usr/local/bin/bun");
150
+ } catch {}
151
+ symlinkSync("/usr/local/lib/forgezero/runtime/bun", "/usr/local/bin/bun");
152
+ return { exitCode: 0, output: "" };
153
+ }
154
+ if (software === "cloudflared") {
155
+ const binary = join(directory, "cloudflared");
156
+ await download("https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64", binary, CLOUDFLARED_SHA256);
157
+ chmodSync(binary, 493);
158
+ copyFileSync(binary, "/usr/local/bin/cloudflared");
159
+ chmodSync("/usr/local/bin/cloudflared", 493);
160
+ return { exitCode: 0, output: "" };
161
+ }
162
+ const deb = join(directory, "arangodb.deb");
163
+ await download("https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb", deb, ARANGO_SHA256);
164
+ let installed = await run(["/usr/bin/dpkg", "-i", deb], { DEBIAN_FRONTEND: "noninteractive" });
165
+ if (installed.exitCode !== 0)
166
+ installed = await run(["/usr/bin/apt-get", "-y", "-f", "install"], { DEBIAN_FRONTEND: "noninteractive" });
167
+ if (installed.exitCode !== 0)
168
+ return installed;
169
+ await run(["/usr/bin/systemctl", "disable", "--now", "arangodb3.service"]);
170
+ return { exitCode: 0, output: "" };
171
+ } finally {
172
+ rmSync(directory, { recursive: true, force: true });
173
+ }
174
+ }
50
175
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
51
176
  const values = Object.fromEntries(osRelease.split(`
52
177
  `).flatMap((line) => {
@@ -69,7 +194,7 @@ function validateSoftwareRequirements(value, _options = {}) {
69
194
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
70
195
  throw new Error("software requirement contains an unknown field");
71
196
  }
72
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw", "openssh-client"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
197
+ 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)) {
73
198
  throw new Error("software requirement coordinate is invalid");
74
199
  }
75
200
  const requirement = { id: row.id, version: row.version };
@@ -95,15 +220,15 @@ async function ensureSoftwareRequirements(requirementsInput, options) {
95
220
  const strategy = UBUNTU_2604_X64.find(({ requirement: candidate }) => candidate.id === requirement.id && candidate.version === requirement.version);
96
221
  if (!strategy)
97
222
  throw new Error(`unsupported software requirement: ${requirement.id}@${requirement.version}`);
98
- const before = await options.exec(strategy.check);
223
+ const before = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
99
224
  if (before.exitCode === 0) {
100
225
  results.push({ ...requirement, changed: false });
101
226
  continue;
102
227
  }
103
- const installed = await options.exec(strategy.install);
228
+ const installed = await options.exec({ kind: "install", software: requirement.id, version: requirement.version });
104
229
  if (installed.exitCode !== 0)
105
230
  throw new Error(`could not install ${requirement.id}@${requirement.version}: ${installed.output.trim()}`);
106
- const after = await options.exec(strategy.check);
231
+ const after = await options.exec({ kind: "check", software: requirement.id, version: requirement.version });
107
232
  if (after.exitCode !== 0)
108
233
  throw new Error(`${requirement.id}@${requirement.version} did not pass its post-install check`);
109
234
  results.push({ ...requirement, changed: true });
@@ -227,8 +352,8 @@ async function calibrateHttpConcurrency(options, fetcher = fetch) {
227
352
  }
228
353
 
229
354
  // src/definition.ts
230
- var PIPELINE_VERSION = 2;
231
- var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v2.json";
355
+ var PIPELINE_VERSION = 3;
356
+ var DEPLOY_SCHEMA_URL = "https://www.forgezero.net/schemas/deploy-v3.json";
232
357
 
233
358
  class DefinitionError extends Error {
234
359
  constructor(message) {
@@ -248,6 +373,29 @@ var text = (value, where) => {
248
373
  }
249
374
  return value;
250
375
  };
376
+ var argv = (value, where) => {
377
+ if (!Array.isArray(value) || value.length === 0 || value.length > 256) {
378
+ throw new DefinitionError(`${where} must contain from 1 to 256 arguments.`);
379
+ }
380
+ let bytes = 0;
381
+ const parsed = value.map((argument, index) => {
382
+ if (typeof argument !== "string" || argument.length === 0 || argument.length > 16384 || argument.includes("\x00")) {
383
+ throw new DefinitionError(`${where}[${index}] must be a non-empty bounded string without NUL.`);
384
+ }
385
+ if (argument.includes("${") && !/^\$\{FZ_[A-Z0-9_]+\}$/.test(argument)) {
386
+ throw new DefinitionError(`${where}[${index}] contains unsupported interpolation; only one exact FZ coordinate is allowed.`);
387
+ }
388
+ bytes += Buffer.byteLength(argument);
389
+ if (bytes > 64 * 1024)
390
+ throw new DefinitionError(`${where} is larger than 64 KiB.`);
391
+ return argument;
392
+ });
393
+ const executable = parsed[0].split("/").at(-1).toLowerCase();
394
+ if (new Set(["sh", "bash", "dash", "zsh", "ksh", "fish", "busybox", "env"]).has(executable)) {
395
+ throw new DefinitionError(`${where}[0] may not invoke a shell or command dispatcher.`);
396
+ }
397
+ return parsed;
398
+ };
251
399
  var exactKeys = (value, allowed, where) => {
252
400
  const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
253
401
  if (unknown.length > 0)
@@ -313,6 +461,76 @@ function capacityCalibration(value, where) {
313
461
  }
314
462
  return parsed;
315
463
  }
464
+ function validateDeploymentService(value, where = "service") {
465
+ const service = record(value, where);
466
+ exactKeys(service, [
467
+ "strategy",
468
+ "publicPort",
469
+ "applicationPorts",
470
+ "command",
471
+ "healthPath",
472
+ "maxConnections",
473
+ "websocket",
474
+ "drainMs"
475
+ ], where);
476
+ if (service.strategy !== "direct" && service.strategy !== "blue-green") {
477
+ throw new DefinitionError(`${where}.strategy must be direct or blue-green.`);
478
+ }
479
+ const publicPort = Number(service.publicPort);
480
+ if (!Number.isSafeInteger(publicPort) || publicPort < 1024 || publicPort > 65535) {
481
+ throw new DefinitionError(`${where}.publicPort must be an unprivileged TCP port.`);
482
+ }
483
+ const allocation = record(service.applicationPorts, `${where}.applicationPorts`);
484
+ let applicationPorts;
485
+ const required = service.strategy === "blue-green" ? 2 : 1;
486
+ if (allocation.mode === "fixed") {
487
+ exactKeys(allocation, ["mode", "ports"], `${where}.applicationPorts`);
488
+ if (!Array.isArray(allocation.ports) || allocation.ports.length !== required || allocation.ports.some((port) => !Number.isSafeInteger(port) || Number(port) < 1024 || Number(port) > 65535) || new Set(allocation.ports).size !== allocation.ports.length || allocation.ports.includes(publicPort)) {
489
+ throw new DefinitionError(`${where}.applicationPorts needs ${required} distinct unprivileged port(s), disjoint from publicPort.`);
490
+ }
491
+ applicationPorts = { mode: "fixed", ports: allocation.ports };
492
+ } else if (allocation.mode === "dynamic") {
493
+ exactKeys(allocation, ["mode", "from", "to"], `${where}.applicationPorts`);
494
+ const from = Number(allocation.from);
495
+ const to = Number(allocation.to);
496
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 1024 || to > 65535 || to - from + 1 < required || publicPort >= from && publicPort <= to) {
497
+ throw new DefinitionError(`${where}.applicationPorts dynamic range is invalid or includes publicPort.`);
498
+ }
499
+ applicationPorts = { mode: "dynamic", from, to };
500
+ } else
501
+ throw new DefinitionError(`${where}.applicationPorts.mode must be fixed or dynamic.`);
502
+ const command = argv(service.command, `${where}.command`);
503
+ for (const argument of command) {
504
+ const coordinate = argument.match(/^\$\{(FZ_[A-Z0-9_]+)\}$/)?.[1];
505
+ if (coordinate && !["FZ_APP_PORT", "FZ_RELEASE"].includes(coordinate)) {
506
+ throw new DefinitionError(`${where}.command uses unsupported coordinate ${coordinate}.`);
507
+ }
508
+ }
509
+ if (typeof service.healthPath !== "string" || !/^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{0,255}$/.test(service.healthPath) || service.healthPath.includes("..") || service.healthPath.includes("//")) {
510
+ throw new DefinitionError(`${where}.healthPath must be one bounded absolute path.`);
511
+ }
512
+ if (service.websocket !== undefined && typeof service.websocket !== "boolean") {
513
+ throw new DefinitionError(`${where}.websocket must be a boolean.`);
514
+ }
515
+ const maxConnections = service.maxConnections === undefined ? undefined : Number(service.maxConnections);
516
+ if (maxConnections !== undefined && (!Number.isSafeInteger(maxConnections) || maxConnections < 1 || maxConnections > 1e6)) {
517
+ throw new DefinitionError(`${where}.maxConnections must be from 1 to 1000000.`);
518
+ }
519
+ const drainMs = service.drainMs === undefined ? 30000 : Number(service.drainMs);
520
+ if (!Number.isSafeInteger(drainMs) || drainMs < 0 || drainMs > 300000) {
521
+ throw new DefinitionError(`${where}.drainMs must be from 0 to 300000.`);
522
+ }
523
+ return {
524
+ strategy: service.strategy,
525
+ publicPort,
526
+ applicationPorts,
527
+ command,
528
+ healthPath: service.healthPath,
529
+ ...maxConnections === undefined ? {} : { maxConnections },
530
+ websocket: service.websocket === true,
531
+ drainMs
532
+ };
533
+ }
316
534
  function parseDeployDefinition(value, options = {}) {
317
535
  const root = record(value, "pipeline");
318
536
  exactKeys(root, ["$schema", "version", "name", "requireAttestation", "profiles", "steps"], "pipeline");
@@ -330,20 +548,28 @@ function parseDeployDefinition(value, options = {}) {
330
548
  if (profileEntries.length === 0 || profileEntries.length > 32) {
331
549
  throw new DefinitionError("pipeline.profiles must contain from 1 to 32 named profiles.");
332
550
  }
333
- if (!Array.isArray(root.steps) || root.steps.length === 0) {
334
- throw new DefinitionError("pipeline.steps must contain at least one step.");
551
+ if (!Array.isArray(root.steps) || root.steps.length === 0 || root.steps.length > 256) {
552
+ throw new DefinitionError("pipeline.steps must contain from 1 to 256 steps.");
335
553
  }
336
554
  const profiles = {};
337
555
  for (const [name2, raw] of profileEntries) {
338
556
  if (!NAME.test(name2))
339
557
  throw new DefinitionError(`pipeline profile name is invalid: ${name2}.`);
340
558
  const profile = record(raw, `profiles.${name2}`);
341
- exactKeys(profile, ["software", "capacityCalibration"], `profiles.${name2}`);
559
+ exactKeys(profile, ["software", "service", "capacityCalibration"], `profiles.${name2}`);
342
560
  if (!Array.isArray(profile.software)) {
343
561
  throw new DefinitionError(`profiles.${name2}.software must be an array.`);
344
562
  }
563
+ const software = validateSoftwareRequirements(profile.software, options);
564
+ const service = profile.service === undefined ? undefined : validateDeploymentService(profile.service, `profiles.${name2}.service`);
565
+ if (service && !software.some(({ id }) => id === "nginx")) {
566
+ throw new DefinitionError(`profiles.${name2}.service requires the reviewed nginx software strategy.`);
567
+ }
345
568
  profiles[name2] = {
346
- software: validateSoftwareRequirements(profile.software, options),
569
+ software,
570
+ ...profile.service === undefined ? {} : {
571
+ service
572
+ },
347
573
  ...profile.capacityCalibration === undefined ? {} : {
348
574
  capacityCalibration: capacityCalibration(profile.capacityCalibration, `profiles.${name2}.capacityCalibration`)
349
575
  }
@@ -352,7 +578,7 @@ function parseDeployDefinition(value, options = {}) {
352
578
  const phases = new Set(["build", "release", "migrate", "health"]);
353
579
  const steps = root.steps.map((raw, index) => {
354
580
  const step = record(raw, `steps[${index}]`);
355
- exactKeys(step, ["name", "run", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
581
+ exactKeys(step, ["name", "exec", "phase", "scope", "profiles", "secrets", "always", "timeoutMs", "when"], `steps[${index}]`);
356
582
  const phase = text(step.phase, `steps[${index}].phase`);
357
583
  if (!phases.has(phase))
358
584
  throw new DefinitionError(`steps[${index}].phase is not supported.`);
@@ -400,7 +626,7 @@ function parseDeployDefinition(value, options = {}) {
400
626
  }
401
627
  return {
402
628
  name: text(step.name, `steps[${index}].name`),
403
- run: text(step.run, `steps[${index}].run`),
629
+ exec: argv(step.exec, `steps[${index}].exec`),
404
630
  phase,
405
631
  scope: step.scope,
406
632
  profiles: selectedProfiles,
@@ -440,6 +666,7 @@ function phasePipeline(definition, phase, profile, executeRelease = false) {
440
666
  };
441
667
  }
442
668
  export {
669
+ validateDeploymentService,
443
670
  phasePipeline,
444
671
  parseDeployDefinition,
445
672
  PIPELINE_VERSION,