@forgezero/agent 0.1.35 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -36,6 +36,35 @@ Install the service explicitly:
36
36
  fz agent install --apply # writes a hardened systemd unit
37
37
  ```
38
38
 
39
+ ## Host bootstrap
40
+
41
+ The package is also the host installer. There is no separate shell setup
42
+ script and no second implementation for tenant or platform machines:
43
+
44
+ ```bash
45
+ fz bootstrap platform # interactive plan, no mutation
46
+ sudo fz bootstrap platform --apply # initial DB+API or API-only host
47
+ sudo fz bootstrap tenant --bootstrap-config ./tenant.json --apply
48
+ sudo fz bootstrap metal --bootstrap-config ./metal.json --apply
49
+ sudo fz bootstrap status
50
+ sudo fz bootstrap repair --bootstrap-config ./original.json --apply
51
+ ```
52
+
53
+ Platform bootstrap supports an elastic Community ArangoDB 3.11.14 fleet. The
54
+ first three database-capable computes establish the writable cluster; later
55
+ database joiners and API-only computes use the same command with their typed
56
+ profile and an API-issued one-time enrolment file. Nothing treats three as a
57
+ maximum.
58
+
59
+ After genesis, a tenant can designate an enrolled compute as a bootstrap
60
+ runner. The API creates a project-bound, expiring job containing only public
61
+ SSH coordinates and a pinned Ed25519 host-key fingerprint. The runner claims it
62
+ with its hybrid node identity, reads its SSH key only from a local systemd
63
+ credential, pins one vetted DNS answer, transfers this packaged CLI, and runs
64
+ the same `fz bootstrap tenant` flow on the target. Claim renewal, completion,
65
+ retry and cancellation are fenced in the compute aggregate; the API never
66
+ stores the SSH private key or a plaintext enrolment token.
67
+
39
68
  The attended Cloudflare resource phase is part of the same published command,
40
69
  but remains separate from the root host install so its management token never
41
70
  enters the API or Agent service environment:
@@ -744,7 +744,7 @@ async function postSignedNode(options, path, body) {
744
744
  }
745
745
 
746
746
  // src/version.ts
747
- var VERSION3 = "0.1.35";
747
+ var VERSION3 = "0.1.36";
748
748
 
749
749
  // src/agent-heartbeat.ts
750
750
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
@@ -68,6 +68,11 @@ export interface TenantBootstrapConfig {
68
68
  deployRoot?: string;
69
69
  software?: SoftwareRequirement[];
70
70
  installCloudflared?: boolean;
71
+ /** Local-only runner authority. The API receives only the resulting node scope. */
72
+ bootstrapRunner?: {
73
+ sshPrivateKeyFile: string;
74
+ targetTelemetryEndpoint: string;
75
+ };
71
76
  }
72
77
  export type BootstrapConfig = PlatformBootstrapConfig | TenantBootstrapConfig;
73
78
  export interface BootstrapStep {
package/dist/bootstrap.js CHANGED
@@ -1214,7 +1214,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1214
1214
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1215
1215
 
1216
1216
  // src/version.ts
1217
- var VERSION = "0.1.35";
1217
+ var VERSION = "0.1.36";
1218
1218
 
1219
1219
  // src/software.ts
1220
1220
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
@@ -1245,6 +1245,11 @@ var UBUNTU_2604_X64 = [
1245
1245
  requirement: { id: "ufw", version: "ubuntu-26.04" },
1246
1246
  check: "command -v ufw >/dev/null",
1247
1247
  install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
1248
+ },
1249
+ {
1250
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
1251
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
1252
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
1248
1253
  }
1249
1254
  ];
1250
1255
 
@@ -1786,7 +1791,12 @@ function agentUnit(options) {
1786
1791
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1787
1792
  throw new Error("migration pull and lifecycle profile must be supplied together");
1788
1793
  }
1794
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1795
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1796
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1797
+ }
1789
1798
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1799
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
1790
1800
  const warpValues = [
1791
1801
  options.warpOrganization,
1792
1802
  options.warpClientIdCredentialPath,
@@ -1852,6 +1862,9 @@ function agentUnit(options) {
1852
1862
  options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
1853
1863
  options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
1854
1864
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1865
+ options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
1866
+ options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
1867
+ options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
1855
1868
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1856
1869
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
1857
1870
  ].filter((line) => line !== null);
@@ -1859,6 +1872,8 @@ function agentUnit(options) {
1859
1872
  environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
1860
1873
  }
1861
1874
  const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
1875
+ ` : "";
1876
+ const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
1862
1877
  ` : "";
1863
1878
  const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
1864
1879
  `);
@@ -1913,7 +1928,7 @@ User=${user}
1913
1928
  Group=${VAULT_GROUP}
1914
1929
  ${deploymentGroup}
1915
1930
  LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1916
- ${gitCredential}${projectCredentials}${projectCredentials ? `
1931
+ ${gitCredential}${bootstrapCredential}${projectCredentials}${projectCredentials ? `
1917
1932
  ` : ""}${snpPrepare}ExecStart=${bin}
1918
1933
  Restart=always
1919
1934
  RestartSec=2
@@ -1968,6 +1983,10 @@ function planProvision(options) {
1968
1983
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1969
1984
  throw new Error("migration pull and lifecycle profile must be supplied together");
1970
1985
  }
1986
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1987
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1988
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1989
+ }
1971
1990
  const warpValues = [
1972
1991
  options.warpOrganization,
1973
1992
  options.warpClientIdCredentialPath,
@@ -1993,13 +2012,20 @@ function planProvision(options) {
1993
2012
  const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
1994
2013
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
1995
2014
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
2015
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
1996
2016
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
1997
2017
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
1998
2018
  return {
1999
2019
  mode,
2000
2020
  reason: reasonFor(mode),
2001
2021
  unitPath: UNIT_PATH,
2002
- unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
2022
+ unit: agentUnit({
2023
+ ...options,
2024
+ mode,
2025
+ lifecycleProfilePath,
2026
+ lifecycleHelperSocketPath,
2027
+ bootstrapSshCredentialPath
2028
+ }),
2003
2029
  auxiliaryUnits: [
2004
2030
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
2005
2031
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
@@ -2600,6 +2626,7 @@ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
2600
2626
  var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
2601
2627
  var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
2602
2628
  var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
2629
+ var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
2603
2630
  var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
2604
2631
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
2605
2632
  var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
@@ -2662,6 +2689,20 @@ function validateBootstrapConfig(value) {
2662
2689
  }
2663
2690
  if (!value.enrolTokenFile)
2664
2691
  throw new Error("tenant bootstrap requires --enrol-token-file");
2692
+ if (value.bootstrapRunner) {
2693
+ if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
2694
+ throw new Error("bootstrap runner SSH private-key file must be absolute");
2695
+ }
2696
+ let targetTelemetry;
2697
+ try {
2698
+ targetTelemetry = new URL(value.bootstrapRunner.targetTelemetryEndpoint);
2699
+ } catch {
2700
+ throw new Error("bootstrap target telemetry endpoint is malformed");
2701
+ }
2702
+ if (targetTelemetry.protocol !== "https:" || targetTelemetry.username || targetTelemetry.password || targetTelemetry.search || targetTelemetry.hash || targetTelemetry.port && targetTelemetry.port !== "443") {
2703
+ throw new Error("bootstrap target telemetry must be public HTTPS on port 443");
2704
+ }
2705
+ }
2665
2706
  return value;
2666
2707
  }
2667
2708
  if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
@@ -2723,7 +2764,11 @@ function validateBootstrapConfig(value) {
2723
2764
  }
2724
2765
  function planBootstrap(input, initialized = false) {
2725
2766
  const config = validateBootstrapConfig(structuredClone(input));
2726
- const software = config.kind === "tenant" ? [...config.software ?? [], ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []] : [
2767
+ const software = config.kind === "tenant" ? [
2768
+ ...config.software ?? [],
2769
+ ...config.bootstrapRunner && !config.software?.some(({ id }) => id === "openssh-client") ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
2770
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
2771
+ ] : [
2727
2772
  ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
2728
2773
  { id: "bun", version: "1.3.14" },
2729
2774
  { id: "nginx", version: "ubuntu-26.04" },
@@ -2958,6 +3003,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2958
3003
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
2959
3004
  host.mkdir(CREDS, 448);
2960
3005
  host.mkdir("/var/lib/forgezero", 448);
3006
+ if (config.kind === "tenant" && config.bootstrapRunner && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3007
+ await seal(host, "bootstrap-ssh-key", BOOTSTRAP_SSH_CREDENTIAL, privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key"));
3008
+ host.remove(config.bootstrapRunner.sshPrivateKeyFile);
3009
+ }
2961
3010
  if (cloudflare?.warp) {
2962
3011
  await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
2963
3012
  await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
@@ -3157,7 +3206,8 @@ function strictBootstrapDocument(value) {
3157
3206
  "cloudflareHandoff",
3158
3207
  "realm",
3159
3208
  "enrolTokenFile",
3160
- "software"
3209
+ "software",
3210
+ "bootstrapRunner"
3161
3211
  ], "bootstrap config");
3162
3212
  if (root.kind === "platform") {
3163
3213
  exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
@@ -3222,6 +3272,8 @@ function strictBootstrapDocument(value) {
3222
3272
  exactKeys(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
3223
3273
  }
3224
3274
  } else if (root.kind === "tenant") {
3275
+ if (root.bootstrapRunner !== undefined)
3276
+ exactKeys(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
3225
3277
  for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
3226
3278
  if (root[key] !== undefined && key !== "profile")
3227
3279
  throw new Error(`tenant bootstrap cannot contain ${key}`);
@@ -3316,6 +3368,9 @@ function localBootstrapHost() {
3316
3368
  generateGitIdentity: true,
3317
3369
  pullDeployments: true,
3318
3370
  pullMigrations: config.kind === "platform",
3371
+ pullBootstrap: config.kind === "tenant" && Boolean(config.bootstrapRunner),
3372
+ bootstrapSshCredentialPath: config.kind === "tenant" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
3373
+ bootstrapTargetTelemetryEndpoint: config.kind === "tenant" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
3319
3374
  lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
3320
3375
  ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
3321
3376
  warpOrganization: config.runtime.environment.cloudflare.warp.organization,
@@ -56,6 +56,9 @@ export interface InstallOptions {
56
56
  runnerLoopbackPorts?: readonly number[];
57
57
  runnerPublicTcpPorts?: readonly number[];
58
58
  pullMigrations?: boolean;
59
+ pullBootstrap?: boolean;
60
+ bootstrapSshCredentialPath?: string;
61
+ bootstrapTargetTelemetryEndpoint?: string;
59
62
  lifecycleProfilePath?: string;
60
63
  lifecycleHelperSocketPath?: string;
61
64
  warpOrganization?: string;
@@ -1,5 +1,6 @@
1
1
  // src/software.ts
2
2
  import { readFileSync } from "node:fs";
3
+ var PINNED_BUN_VERSION = "1.3.14";
3
4
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4
5
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
5
6
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -11,7 +12,8 @@ var SOFTWARE_CATALOG = [
11
12
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
12
13
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
14
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
14
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
16
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
17
  ];
16
18
  var UBUNTU_2604_X64 = [
17
19
  {
@@ -38,6 +40,11 @@ var UBUNTU_2604_X64 = [
38
40
  requirement: { id: "ufw", version: "ubuntu-26.04" },
39
41
  check: "command -v ufw >/dev/null",
40
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"
41
48
  }
42
49
  ];
43
50
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -62,7 +69,7 @@ function validateSoftwareRequirements(value, _options = {}) {
62
69
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
63
70
  throw new Error("software requirement contains an unknown field");
64
71
  }
65
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
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)) {
66
73
  throw new Error("software requirement coordinate is invalid");
67
74
  }
68
75
  const requirement = { id: row.id, version: row.version };
@@ -1,5 +1,6 @@
1
1
  // src/software.ts
2
2
  import { readFileSync } from "node:fs";
3
+ var PINNED_BUN_VERSION = "1.3.14";
3
4
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
4
5
  var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
5
6
  var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
@@ -11,7 +12,8 @@ var SOFTWARE_CATALOG = [
11
12
  { id: "nginx", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
12
13
  { id: "arangodb", version: "3.11.14", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
13
14
  { id: "cloudflared", version: "2026.7.3", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
14
- { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
+ { id: "ufw", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" },
16
+ { id: "openssh-client", version: "ubuntu-26.04", status: "active", os: "ubuntu", osVersion: "26.04", architecture: "x64", evidence: "reviewed-strategy-and-tests" }
15
17
  ];
16
18
  var UBUNTU_2604_X64 = [
17
19
  {
@@ -38,6 +40,11 @@ var UBUNTU_2604_X64 = [
38
40
  requirement: { id: "ufw", version: "ubuntu-26.04" },
39
41
  check: "command -v ufw >/dev/null",
40
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"
41
48
  }
42
49
  ];
43
50
  function observeSoftwareHost(osRelease = readFileSync("/etc/os-release", "utf8"), architecture = process.arch) {
@@ -62,7 +69,7 @@ function validateSoftwareRequirements(value, _options = {}) {
62
69
  if (Object.keys(row).some((key) => key !== "id" && key !== "version")) {
63
70
  throw new Error("software requirement contains an unknown field");
64
71
  }
65
- if (!["bun", "nginx", "arangodb", "cloudflared", "ufw"].includes(String(row.id)) || typeof row.version !== "string" || !/^[A-Za-z0-9][A-Za-z0-9.-]{0,31}$/.test(row.version)) {
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)) {
66
73
  throw new Error("software requirement coordinate is invalid");
67
74
  }
68
75
  const requirement = { id: row.id, version: row.version };