@forgezero/agent 0.1.85 → 0.1.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz.js CHANGED
@@ -4634,7 +4634,7 @@ var VaultError, runtimeEnvironment = () => typeof process !== "undefined" && pro
4634
4634
  } catch {
4635
4635
  return;
4636
4636
  }
4637
- }, VERSION = "0.1.17";
4637
+ }, VERSION = "0.1.18";
4638
4638
  var init_dist = __esm(() => {
4639
4639
  init_identity();
4640
4640
  VaultError = class VaultError extends Error {
@@ -4810,8 +4810,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
4810
4810
  }
4811
4811
 
4812
4812
  // src/cli/index.ts
4813
- import { existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
4813
+ import { chmodSync as chmodSync7, existsSync as existsSync12, lstatSync as lstatSync10, mkdirSync as mkdirSync13, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
4814
4814
  import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve12 } from "path";
4815
+ import { randomBytes as randomBytes10 } from "crypto";
4815
4816
 
4816
4817
  // src/process-input.ts
4817
4818
  async function writeAndCloseProcessInput(input, value) {
@@ -4826,9 +4827,13 @@ import { hostname } from "os";
4826
4827
 
4827
4828
  // src/agent-update.ts
4828
4829
  var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
4830
+ var DEFAULT_AGENT_CANDIDATE_LINK = `${DEFAULT_AGENT_RELEASE_ROOT}/candidate`;
4829
4831
  var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
4830
4832
  var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
4831
4833
 
4834
+ // src/agent-handover.ts
4835
+ var DEFAULT_AGENT_CANDIDATE_READY_SOCKET = "/run/forgezero/candidate-ready.sock";
4836
+
4832
4837
  // src/agent-update-helper.ts
4833
4838
  var AGENT_UPDATE_GROUP = "forgezero-update";
4834
4839
  var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
@@ -4837,7 +4842,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4837
4842
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4838
4843
 
4839
4844
  // src/version.ts
4840
- var VERSION2 = "0.1.85";
4845
+ var VERSION2 = "0.1.87";
4841
4846
 
4842
4847
  // src/software.ts
4843
4848
  var PINNED_BUN_VERSION = "1.3.14";
@@ -5454,6 +5459,7 @@ var LIFECYCLE_GROUP = "forgezero-lifecycle";
5454
5459
  var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
5455
5460
  var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
5456
5461
  var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
5462
+ var AGENT_CANDIDATE_UNIT_PATH = "/etc/systemd/system/forgezero-agent-candidate.service";
5457
5463
  var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
5458
5464
  var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
5459
5465
  var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
@@ -5565,6 +5571,7 @@ Type=simple
5565
5571
  User=root
5566
5572
  Group=${AGENT_UPDATE_GROUP}
5567
5573
  Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
5574
+ Environment=FZ_AGENT_PUBLIC_SOCKET=${systemdPath(options.socketPath, "agent socket")}
5568
5575
  ExecStart=${bin} update-helper
5569
5576
  Restart=always
5570
5577
  RestartSec=2
@@ -5609,13 +5616,11 @@ WantedBy=sockets.target
5609
5616
  `;
5610
5617
  }
5611
5618
  function agentSocketProxyUnit(options) {
5612
- const backend = agentBackendSocketPath(options.socketPath);
5619
+ const backend = agentRoutingSocketPath(options.socketPath);
5613
5620
  const user = options.user ?? "forgezero";
5614
5621
  return `[Unit]
5615
5622
  Description=ForgeZero application Vault socket proxy
5616
5623
  Documentation=https://www.forgezero.net/docs/agent
5617
- Requires=forgezero-agent.service
5618
- After=forgezero-agent.service
5619
5624
 
5620
5625
  [Service]
5621
5626
  User=${user}
@@ -5636,12 +5641,26 @@ RestrictAddressFamilies=AF_UNIX
5636
5641
  `;
5637
5642
  }
5638
5643
  function agentBackendSocketPath(publicSocketPath) {
5644
+ const socket = systemdPath(publicSocketPath, "agent socket");
5645
+ const backend = `${socket}.backend.active`;
5646
+ if (Buffer.byteLength(backend) > 100)
5647
+ throw new Error("agent socket path is too long for a Unix socket");
5648
+ return backend;
5649
+ }
5650
+ function agentRoutingSocketPath(publicSocketPath) {
5639
5651
  const socket = systemdPath(publicSocketPath, "agent socket");
5640
5652
  const backend = `${socket}.backend`;
5641
5653
  if (Buffer.byteLength(backend) > 100)
5642
5654
  throw new Error("agent socket path is too long for a Unix socket");
5643
5655
  return backend;
5644
5656
  }
5657
+ function agentCandidateSocketPath(publicSocketPath) {
5658
+ const socket = systemdPath(publicSocketPath, "agent socket");
5659
+ const backend = `${socket}.backend.candidate`;
5660
+ if (Buffer.byteLength(backend) > 100)
5661
+ throw new Error("agent socket path is too long for a Unix socket");
5662
+ return backend;
5663
+ }
5645
5664
  var systemdPath = (value, label) => {
5646
5665
  if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
5647
5666
  throw new Error(`invalid ${label} path`);
@@ -5925,7 +5944,11 @@ function agentUnit(options) {
5925
5944
  }
5926
5945
  const environment = [
5927
5946
  "NODE_ENV=production",
5928
- `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
5947
+ `FZ_SOCKET_PATH=${options.backendSocketPath ?? agentBackendSocketPath(options.socketPath)}`,
5948
+ `FZ_AGENT_PUBLIC_SOCKET=${options.socketPath}`,
5949
+ `FZ_AGENT_ROUTE_SOCKET=${agentRoutingSocketPath(options.socketPath)}`,
5950
+ options.handoverCandidate ? "FZ_AGENT_HANDOVER_CANDIDATE=true" : null,
5951
+ options.handoverCandidate ? `FZ_AGENT_HANDOVER_READY_SOCKET=${DEFAULT_AGENT_CANDIDATE_READY_SOCKET}` : null,
5929
5952
  `FZ_CONTROL_SOCKET=${controlSocketPath}`,
5930
5953
  `FZ_SEED_CREDENTIAL=agent-seed`,
5931
5954
  `FZ_AGENT_MODE=${options.mode}`,
@@ -6062,6 +6085,26 @@ ${deploymentWrites}
6062
6085
  WantedBy=multi-user.target
6063
6086
  `;
6064
6087
  }
6088
+ function agentCandidateUnit(options) {
6089
+ return agentUnit({
6090
+ ...options,
6091
+ binPath: `${DEFAULT_AGENT_RELEASE_ROOT}/candidate/dist/fz-agent.js`,
6092
+ backendSocketPath: agentCandidateSocketPath(options.socketPath),
6093
+ handoverCandidate: true,
6094
+ gitCredentialPath: undefined,
6095
+ deploymentCredentials: {},
6096
+ bootstrapSshCredentialPath: undefined,
6097
+ bootstrapSshPublicKeyPath: undefined,
6098
+ pullBootstrap: false,
6099
+ pullDeployments: false,
6100
+ pullMigrations: false,
6101
+ lifecycleProfilePath: undefined,
6102
+ bootstrapTargetTelemetryEndpoint: undefined,
6103
+ repository: undefined,
6104
+ bootstrapBundlePath: undefined,
6105
+ bootstrapBundleManifestPath: undefined
6106
+ }).replace("Description=ForgeZero node agent", "Description=ForgeZero candidate node agent");
6107
+ }
6065
6108
  var renderOperation = (operation) => {
6066
6109
  if (operation.kind === "commands")
6067
6110
  return operation.commands.map(({ argv: argv2 }) => argv2.join(" ")).join(`
@@ -6197,6 +6240,7 @@ function planProvision(options) {
6197
6240
  auxiliaryUnits: [
6198
6241
  { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
6199
6242
  { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
6243
+ { path: AGENT_CANDIDATE_UNIT_PATH, unit: agentCandidateUnit(options) },
6200
6244
  { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
6201
6245
  ...options.enforceEgress ? [
6202
6246
  { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
@@ -12139,12 +12183,14 @@ var httpsOrigin = (name, raw) => {
12139
12183
  return value.origin;
12140
12184
  };
12141
12185
  function validatePlatformInitialInventory(value) {
12142
- if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
12186
+ if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "deployment"].includes(key)) || !/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(value.metalHostname) || !value.region || typeof value.region !== "object" || Array.isArray(value.region) || Object.keys(value.region).some((key) => !["key", "label", "country", "city", "confidentialCapable"].includes(key)) || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.region.key) || !/^[A-Z]{2}$/.test(value.region.country) || value.region.confidentialCapable !== true || !Array.isArray(value.computes)) {
12143
12187
  throw new Error("initial platform inventory coordinates are invalid");
12144
12188
  }
12145
12189
  safeAtom("initialInventory.region.label", value.region.label);
12146
12190
  if (value.region.city !== undefined)
12147
12191
  safeAtom("initialInventory.region.city", value.region.city);
12192
+ if (value.deployment !== undefined && (!value.deployment || typeof value.deployment !== "object" || Array.isArray(value.deployment) || Object.keys(value.deployment).some((key) => !["source", "branch", "revision", "bundleSha256"].includes(key)) || value.deployment.source !== "bootstrap-bundle" || !["dev", "main"].includes(value.deployment.branch) || !/^[a-f0-9]{40}$/.test(value.deployment.revision) || !/^[a-f0-9]{64}$/.test(value.deployment.bundleSha256)))
12193
+ throw new Error("initial platform deployment evidence is invalid");
12148
12194
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
12149
12195
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
12150
12196
  "name",
@@ -12179,7 +12225,8 @@ function validatePlatformInitialInventory(value) {
12179
12225
  databaseRole: compute.databaseRole,
12180
12226
  databaseAgency: compute.databaseAgency,
12181
12227
  confidential: true
12182
- }))
12228
+ })),
12229
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
12183
12230
  };
12184
12231
  }
12185
12232
  var systemdValue = (name, raw) => {
@@ -15143,8 +15190,10 @@ function strictBootstrapDocument(value) {
15143
15190
  ], "runtime environment");
15144
15191
  const environment = runtime.environment;
15145
15192
  if (environment.initialInventory !== undefined) {
15146
- const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
15193
+ const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
15147
15194
  exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
15195
+ if (inventory.deployment !== undefined)
15196
+ exactKeys2(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
15148
15197
  if (!Array.isArray(inventory.computes))
15149
15198
  throw new Error("initial inventory computes must be an array");
15150
15199
  for (const compute of inventory.computes)
@@ -16035,6 +16084,7 @@ function planMetalBootstrap(config) {
16035
16084
  "forgezero-metal-helper.service",
16036
16085
  "forgezero-agent-update-helper.service",
16037
16086
  "forgezero-metal-agent-egress.service",
16087
+ "forgezero-metal-agent-candidate.service",
16038
16088
  "forgezero-metal-agent.service"
16039
16089
  ],
16040
16090
  steps: [
@@ -16176,6 +16226,44 @@ RestrictAddressFamilies=AF_UNIX AF_NETLINK
16176
16226
 
16177
16227
  [Install]
16178
16228
  WantedBy=multi-user.target
16229
+ `,
16230
+ "forgezero-metal-agent-candidate.service": `[Unit]
16231
+ Description=ForgeZero candidate physical-host Agent
16232
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
16233
+ Requires=forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
16234
+
16235
+ [Service]
16236
+ Type=simple
16237
+ User=forgezero-metal
16238
+ Group=forgezero-metal
16239
+ SupplementaryGroups=forgezero-update
16240
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
16241
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
16242
+ Environment=FZ_AGENT_ROLE=metal
16243
+ Environment=FZ_AGENT_HANDOVER_CANDIDATE=true
16244
+ Environment=FZ_AGENT_HANDOVER_READY_SOCKET=/run/forgezero/candidate-ready.sock
16245
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
16246
+ Environment=FZ_API=${config.profile.apiUrl}
16247
+ Environment=NODE_ENV=production
16248
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
16249
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent-candidate
16250
+ ExecStart=/opt/forgezero/agent/candidate/dist/fz-agent.js
16251
+ Restart=on-failure
16252
+ RestartSec=2
16253
+ RuntimeDirectory=forgezero
16254
+ RuntimeDirectoryMode=0750
16255
+ RuntimeDirectoryPreserve=yes
16256
+ LimitCORE=0
16257
+ NoNewPrivileges=true
16258
+ PrivateTmp=true
16259
+ ProtectSystem=strict
16260
+ ProtectHome=true
16261
+ ProtectKernelTunables=true
16262
+ ProtectKernelModules=true
16263
+ ProtectControlGroups=true
16264
+ RestrictSUIDSGID=true
16265
+ LockPersonality=true
16266
+ ${egress}
16179
16267
  `,
16180
16268
  "forgezero-metal-agent.service": `[Unit]
16181
16269
  Description=ForgeZero identity-only physical-host Agent
@@ -17430,7 +17518,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
17430
17518
  }
17431
17519
 
17432
17520
  // src/platform-genesis-config.ts
17433
- function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
17521
+ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment) {
17434
17522
  const fleet = validatePlatformGenesisGuests(guests);
17435
17523
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
17436
17524
  throw new Error("platform genesis requires the reviewed metal hostname");
@@ -17458,7 +17546,8 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17458
17546
  confidential: guest.confidential,
17459
17547
  databaseRole: index === 0 ? "master" : "joiner",
17460
17548
  databaseAgency: "member"
17461
- }))
17549
+ })),
17550
+ ...deployment ? { deployment } : {}
17462
17551
  };
17463
17552
  return fleet.map((guest, index) => {
17464
17553
  const node = nodes[index];
@@ -17499,6 +17588,122 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17499
17588
  });
17500
17589
  }
17501
17590
 
17591
+ // src/platform-launch-profile.ts
17592
+ var PROFILES = {
17593
+ development: {
17594
+ environment: "development",
17595
+ branch: "dev",
17596
+ appOrigin: "https://dev.forgezero.net",
17597
+ apiOrigin: "https://dev-api.forgezero.net",
17598
+ zoneName: "forgezero.net",
17599
+ kvNamespaceTitle: "forgezero-dev-nodes",
17600
+ workerScriptName: "forgezero-api-edge-development",
17601
+ region: { key: "in-south", label: "India South", country: "IN" },
17602
+ agentOtlpEndpoint: "https://otel.forgezero.net",
17603
+ privateCidrs: ["10.42.0.0/24"]
17604
+ },
17605
+ production: {
17606
+ environment: "production",
17607
+ branch: "main",
17608
+ appOrigin: "https://www.forgezero.net",
17609
+ apiOrigin: "https://api.forgezero.net",
17610
+ zoneName: "forgezero.net",
17611
+ kvNamespaceTitle: "forgezero-nodes",
17612
+ workerScriptName: "forgezero-api-edge-production",
17613
+ region: { key: "in-south", label: "India South", country: "IN" },
17614
+ agentOtlpEndpoint: "https://otel.forgezero.net",
17615
+ privateCidrs: ["10.42.0.0/24"]
17616
+ }
17617
+ };
17618
+ function forgeZeroLaunchProfile(environment) {
17619
+ const profile = PROFILES[environment];
17620
+ return {
17621
+ ...profile,
17622
+ nodeHostname: (index) => environment === "development" ? `dev-api-n${index + 1}.forgezero.net` : `api-n${index + 1}.forgezero.net`
17623
+ };
17624
+ }
17625
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17626
+ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
17627
+ const fleet = validatePlatformGenesisGuests(guests);
17628
+ if (!EMAIL.test(owner.custodianEmail) || !EMAIL.test(owner.email.from)) {
17629
+ throw new Error("bootstrap custodian and From addresses must be valid email addresses");
17630
+ }
17631
+ if (owner.email.provider === "smtp" && (!owner.email.host || !Number.isInteger(owner.email.port) || owner.email.port < 1 || owner.email.port > 65535 || !owner.email.user)) {
17632
+ throw new Error("SMTP host, port and user are required");
17633
+ }
17634
+ if (bundle.manifest.branch !== profile.branch)
17635
+ throw new Error("bootstrap bundle branch does not match the launch profile");
17636
+ const coordinators = fleet.map(({ address }) => `http://${address}:8529`);
17637
+ const nodeHostnames = fleet.map((_, index) => profile.nodeHostname(index));
17638
+ const first = fleet[0];
17639
+ return validateBootstrapConfig({
17640
+ kind: "platform",
17641
+ environment: profile.environment,
17642
+ profile: "platform-db-api",
17643
+ computeReference: first.name,
17644
+ nodeHostname: nodeHostnames[0],
17645
+ apiUrl: profile.apiOrigin,
17646
+ bootstrapBundle: {
17647
+ bundleFile: bundle.bundlePath,
17648
+ manifestFile: bundle.manifestPath,
17649
+ branch: profile.branch
17650
+ },
17651
+ telemetryEndpoint: profile.agentOtlpEndpoint,
17652
+ database: {
17653
+ role: "master",
17654
+ agency: "member",
17655
+ readPreferred: false,
17656
+ serverMode: "default",
17657
+ address: first.address,
17658
+ coordinators
17659
+ },
17660
+ enrolment: { source: "genesis-derived" },
17661
+ runtime: {
17662
+ environment: {
17663
+ softwareProfile: "platform-db-api",
17664
+ databaseRole: "master",
17665
+ databaseCoordinators: coordinators,
17666
+ databaseReadPreferredCoordinators: [],
17667
+ databaseAddress: first.address,
17668
+ databaseNetworkMode: "private-lan",
17669
+ databaseReplicationFactor: 2,
17670
+ databaseWriteConcern: 2,
17671
+ databaseUser: "forgezero-api",
17672
+ nodeHostname: nodeHostnames[0],
17673
+ nodeRegion: profile.region.key,
17674
+ nodeRole: "guest",
17675
+ appOrigin: profile.appOrigin,
17676
+ apiOrigin: profile.apiOrigin,
17677
+ publicApiPort: 3000,
17678
+ sharedDirectory: "/opt/forgezero/shared",
17679
+ seedSyncPeers: fleet.slice(1).flatMap(({ address }) => [3001, 3002].map((port) => `ws://${address}:${port}/_internal/forgezero/seed-mesh/v2`)),
17680
+ seedSyncMembers: nodeHostnames,
17681
+ seedSyncEpoch: `${profile.environment}-${bundle.manifest.revision.slice(0, 16)}`,
17682
+ concurrencyLimit: 128,
17683
+ drainDeadlineMs: 30000,
17684
+ otlpEndpoint: "http://127.0.0.1:4318",
17685
+ otlpCollectorUnit: FORGEZERO_OTEL_COLLECTOR_UNIT,
17686
+ agentOtlpEndpoint: profile.agentOtlpEndpoint,
17687
+ otlpFlushIntervalMs: 1e4,
17688
+ otlpTraceSampleRatio: 0.1,
17689
+ custodianEmail: owner.custodianEmail,
17690
+ email: owner.email,
17691
+ deployProfile: profile.environment
17692
+ },
17693
+ serviceUser: "forgezero-api",
17694
+ slotsDirectory: "/opt/forgezero/slots",
17695
+ bluePort: 3001,
17696
+ greenPort: 3002,
17697
+ healthPath: "/api/health",
17698
+ keepReleases: 5
17699
+ },
17700
+ firewall: { enabled: true, sshPort: 22, privateCidrs: [...profile.privateCidrs] },
17701
+ installCloudflared: true,
17702
+ installWarp: false,
17703
+ cloudflareHandoff: { handoffFile: "/run/forgezero/cloudflare-handoff.json", nodeName: first.name }
17704
+ });
17705
+ }
17706
+
17502
17707
  // src/host-maintenance.ts
17503
17708
  import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
17504
17709
  var ROOT = "/opt/forgezero";
@@ -18052,6 +18257,8 @@ function parseOptions(argv2) {
18052
18257
  options.dataFile = argv2[++index];
18053
18258
  else if (token === "--bootstrap-config")
18054
18259
  options.bootstrapConfigPath = argv2[++index];
18260
+ else if (token === "--secrets-env-file")
18261
+ options.bootstrapSecretsEnvFile = argv2[++index];
18055
18262
  else if (token === "--output")
18056
18263
  options.outputPath = argv2[++index];
18057
18264
  else if (token === "--query")
@@ -18648,9 +18855,100 @@ async function bootstrapSecret(question) {
18648
18855
  throw new Error(`${question} was not provided`);
18649
18856
  return value;
18650
18857
  }
18651
- async function promptPlatformBootstrapSecrets(config) {
18858
+ var PLATFORM_CLUSTER_CREDENTIAL = "forgezero-platform-cluster-bootstrap-code";
18859
+ function readPlatformLaunchEnvironment(path) {
18860
+ const absolute2 = resolve12(path);
18861
+ const stat2 = lstatSync10(absolute2);
18862
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
18863
+ throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
18864
+ }
18865
+ const values = new Map;
18866
+ for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
18867
+ const line = raw.trim();
18868
+ if (!line || line.startsWith("#"))
18869
+ continue;
18870
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
18871
+ if (!match)
18872
+ throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
18873
+ if (values.has(match[1]))
18874
+ throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
18875
+ let value = match[2].trim();
18876
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
18877
+ value = value.slice(1, -1);
18878
+ }
18879
+ values.set(match[1], value);
18880
+ }
18881
+ const requiredValue = (name) => {
18882
+ const value = values.get(name)?.trim();
18883
+ if (!value)
18884
+ throw new Error(`bootstrap secret environment is missing ${name}`);
18885
+ return value;
18886
+ };
18887
+ const provider = requiredValue("FZ_EMAIL_PROVIDER");
18888
+ if (provider !== "smtp" && provider !== "jetemail")
18889
+ throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
18890
+ const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
18891
+ if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
18892
+ throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
18893
+ }
18894
+ const owner = provider === "jetemail" ? {
18895
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18896
+ email: {
18897
+ provider,
18898
+ from: requiredValue("FZ_EMAIL_FROM"),
18899
+ eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
18900
+ }
18901
+ } : {
18902
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18903
+ email: {
18904
+ provider,
18905
+ from: requiredValue("FZ_EMAIL_FROM"),
18906
+ host: requiredValue("FZ_SMTP_HOST"),
18907
+ port: smtpPort,
18908
+ user: requiredValue("FZ_SMTP_USER")
18909
+ }
18910
+ };
18911
+ return {
18912
+ owner,
18913
+ emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
18914
+ cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
18915
+ cloudflareApiToken: requiredValue("CF_API_TOKEN")
18916
+ };
18917
+ }
18918
+ async function platformLaunchClusterBootstrapCode(outputDirectory) {
18919
+ const path = join13(dirname14(outputDirectory), `${PLATFORM_CLUSTER_CREDENTIAL}.cred`);
18920
+ if (existsSync12(path)) {
18921
+ const stat2 = lstatSync10(path);
18922
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
18923
+ throw new Error(`platform cluster credential must be one owner-only regular file: ${path}`);
18924
+ }
18925
+ const child2 = Bun.spawn(["systemd-creds", "decrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, path, "-"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
18926
+ const [stdout, stderr2, exitCode2] = await Promise.all([
18927
+ new Response(child2.stdout).text(),
18928
+ new Response(child2.stderr).text(),
18929
+ child2.exited
18930
+ ]);
18931
+ if (exitCode2 !== 0)
18932
+ throw new Error(`cannot open the retained platform cluster credential: ${stderr2.trim()}`);
18933
+ const value2 = stdout.trim();
18934
+ if (!/^[a-f0-9]{64}$/i.test(value2))
18935
+ throw new Error("retained platform cluster credential is invalid");
18936
+ return value2;
18937
+ }
18938
+ mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
18939
+ const value = randomBytes10(32).toString("hex");
18940
+ const child = Bun.spawn(["systemd-creds", "encrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, "-", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
18941
+ await writeAndCloseProcessInput(child.stdin, `${value}
18942
+ `);
18943
+ const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
18944
+ if (exitCode !== 0)
18945
+ throw new Error(`cannot retain the platform cluster credential: ${stderr.trim()}`);
18946
+ chmodSync7(path, 384);
18947
+ return value;
18948
+ }
18949
+ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
18652
18950
  return validatePlatformBootstrapSecrets(config, {
18653
- clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
18951
+ clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
18654
18952
  emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
18655
18953
  ...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
18656
18954
  ...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
@@ -18843,51 +19141,53 @@ function writeMetalOperatorFiles(directory) {
18843
19141
  });
18844
19142
  return { metalConfig, remoteRequest };
18845
19143
  }
18846
- function writePlatformGenesisFleet(directory) {
19144
+ function writePlatformGenesisFleet(directory, preset) {
18847
19145
  const output = genesisOutputDirectory(directory);
18848
- const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
19146
+ const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
18849
19147
  const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
18850
- const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
19148
+ const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
18851
19149
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
18852
19150
  const fleet = metalRequest.genesis.nodes;
18853
19151
  const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile, {
18854
19152
  allowHistoricalRelease: true
18855
19153
  }).metalHostname;
18856
19154
  const checkpointPath = join13(output, "cloudflare-handoff.json");
18857
- const template = interactiveBootstrap("platform", fleet, checkpointPath);
18858
- const region = {
19155
+ const template = preset ? forgeZeroLaunchTemplate(preset.profile, fleet, preset.bundle, preset.owner) : interactiveBootstrap("platform", fleet, checkpointPath);
19156
+ const region = preset ? preset.profile.region : {
18859
19157
  label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
18860
19158
  country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
18861
19159
  city: bootstrapAnswer("Region city")
18862
19160
  };
18863
- const nodes = fleet.map((guest, index) => index === 0 ? {
18864
- nodeHostname: template.nodeHostname,
19161
+ const nodes = fleet.map((guest, index) => ({
19162
+ nodeHostname: preset ? preset.profile.nodeHostname(index) : index === 0 ? template.nodeHostname : bootstrapAnswer(`${guest.name} public node hostname`),
18865
19163
  cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18866
- } : {
18867
- nodeHostname: bootstrapAnswer(`${guest.name} public node hostname`),
18868
- cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18869
- });
18870
- const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
19164
+ }));
19165
+ const realtimeEnabled = preset ? true : bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
18871
19166
  const cloudflareConfig = writeBootstrapConfig(join13(output, "cloudflare.json"), createCloudflareBootstrapDiscoveryCommandConfig({
18872
19167
  checkpointPath,
18873
19168
  discovery: {
18874
- zoneName: bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
18875
- kvNamespaceTitle: bootstrapAnswer("Existing Worker-bound KV namespace title"),
19169
+ zoneName: preset?.profile.zoneName ?? bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
19170
+ kvNamespaceTitle: preset?.profile.kvNamespaceTitle ?? bootstrapAnswer("Existing Worker-bound KV namespace title"),
18876
19171
  ...realtimeEnabled ? {
18877
- workerScriptName: bootstrapAnswer("Existing Worker script name"),
18878
- endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
18879
- producer: bootstrapAnswer("Realtime producer identity", "platform-api")
19172
+ workerScriptName: preset?.profile.workerScriptName ?? bootstrapAnswer("Existing Worker script name"),
19173
+ endpoint: preset?.profile.apiOrigin ?? bootstrapAnswer("Stable public Worker HTTPS endpoint"),
19174
+ producer: "platform-api"
18880
19175
  } : {}
18881
19176
  },
18882
19177
  nodes: fleet.map((guest, index) => ({
18883
19178
  nodeName: guest.name,
18884
19179
  hostname: nodes[index].nodeHostname,
18885
19180
  service: "http://127.0.0.1:3000",
18886
- tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
19181
+ tunnelName: preset ? guest.name : bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
18887
19182
  }))
18888
19183
  }));
18889
19184
  const cloudflareAcceptance = join13(output, "cloudflare-acceptance.json");
18890
- const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
19185
+ const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region, preset ? {
19186
+ source: "bootstrap-bundle",
19187
+ branch: preset.profile.branch,
19188
+ revision: preset.bundle.manifest.revision,
19189
+ bundleSha256: preset.bundle.manifest.sha256
19190
+ } : undefined).map((config) => {
18891
19191
  const path = `${output}/${config.computeReference}-platform.json`;
18892
19192
  writeBootstrapConfig(path, config);
18893
19193
  return path;
@@ -18919,6 +19219,66 @@ function writePlatformGenesisFleet(directory) {
18919
19219
  const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join13(output, "platform-fleet-remote.json"), requests, { configFile: cloudflareConfig, acceptanceFile: cloudflareAcceptance });
18920
19220
  return { configs, requests, fleetRequest, cloudflareConfig, cloudflareAcceptance };
18921
19221
  }
19222
+ function selectedForgeZeroLaunchEnvironment(value) {
19223
+ if (value === "app")
19224
+ return "development";
19225
+ if (value !== "development" && value !== "production") {
19226
+ throw new Error("platform launch --profile must be development or production");
19227
+ }
19228
+ return value;
19229
+ }
19230
+ function forgeZeroLaunchOwnerInput() {
19231
+ const custodianEmail = bootstrapAnswer("First custodian email");
19232
+ const provider = bootstrapAnswer("Bootstrap email provider (smtp/jetemail)", "jetemail");
19233
+ if (provider !== "smtp" && provider !== "jetemail") {
19234
+ throw new Error("Bootstrap email provider must be smtp or jetemail");
19235
+ }
19236
+ const from = bootstrapAnswer("Bootstrap email From address");
19237
+ if (provider === "jetemail") {
19238
+ return {
19239
+ custodianEmail,
19240
+ email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
19241
+ };
19242
+ }
19243
+ return {
19244
+ custodianEmail,
19245
+ email: {
19246
+ provider,
19247
+ from,
19248
+ host: bootstrapAnswer("SMTP host"),
19249
+ port: bootstrapNumber("SMTP port", "587"),
19250
+ user: bootstrapAnswer("SMTP user")
19251
+ }
19252
+ };
19253
+ }
19254
+ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
19255
+ const profile = forgeZeroLaunchProfile(environment);
19256
+ const base = dirname14(outputDirectory);
19257
+ const metalRequestFile = join13(base, "metal", "metal-remote.json");
19258
+ const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
19259
+ for (const [path, label] of [
19260
+ [metalRequestFile, "reviewed Metal request"],
19261
+ [guestHostKeysFile, "pinned genesis guest host-key evidence"]
19262
+ ]) {
19263
+ if (!existsSync12(path))
19264
+ throw new Error(`${label} is missing at ${path}`);
19265
+ }
19266
+ const root = resolve12(projectRoot);
19267
+ const nestedApi = join13(root, "api");
19268
+ const repositoryRoot = existsSync12(join13(nestedApi, "package.json")) ? nestedApi : root;
19269
+ const bundlePath = join13(base, "api.bundle");
19270
+ const bundle = existsSync12(bundlePath) || existsSync12(`${bundlePath}.json`) ? await readBootstrapBundle(bundlePath) : await buildBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
19271
+ if (bundle.manifest.branch !== profile.branch) {
19272
+ throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
19273
+ }
19274
+ return writePlatformGenesisFleet(outputDirectory, {
19275
+ profile,
19276
+ metalRequestFile,
19277
+ guestHostKeysFile,
19278
+ bundle,
19279
+ owner: owner ?? forgeZeroLaunchOwnerInput()
19280
+ });
19281
+ }
18922
19282
  function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
18923
19283
  if (!process.stdin.isTTY)
18924
19284
  throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
@@ -19038,7 +19398,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
19038
19398
  } : {}
19039
19399
  };
19040
19400
  }
19041
- async function runAttendedPlatformFleet(fleet, apply) {
19401
+ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapCode, environmentInput) {
19042
19402
  const plan = planOperatorPlatformFleetBootstrap(fleet, "apply");
19043
19403
  const cloudflarePlan = readCloudflareBootstrapCommandPlan(fleet.cloudflareConfigFile);
19044
19404
  const coordinates = operatorPlatformFleetCoordinates(fleet);
@@ -19069,7 +19429,12 @@ async function runAttendedPlatformFleet(fleet, apply) {
19069
19429
  const firstConfig = readBootstrapConfig(first.platformConfigFile);
19070
19430
  if (firstConfig.kind !== "platform")
19071
19431
  throw new Error("remote platform fleet requires platform configs");
19072
- const secrets = await promptPlatformBootstrapSecrets(firstConfig);
19432
+ const secrets = environmentInput ? validatePlatformBootstrapSecrets(firstConfig, {
19433
+ clusterBootstrapCode: generatedClusterBootstrapCode,
19434
+ emailSecret: environmentInput.emailSecret,
19435
+ cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
19436
+ cloudflareApiToken: environmentInput.cloudflareApiToken
19437
+ }) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
19073
19438
  const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
19074
19439
  tunnelToken: secrets.cloudflareTunnelToken,
19075
19440
  apiToken: secrets.cloudflareApiToken
@@ -19104,15 +19469,21 @@ async function cmdBootstrap(options, args) {
19104
19469
  return 0;
19105
19470
  }
19106
19471
  if (operation === "platform" && args[1] === "launch") {
19107
- if (args[2] !== undefined || !options.outputPath || options.bootstrapConfigPath) {
19108
- throw new Error("Usage: fz bootstrap platform launch --output <absolute-owner-only-directory> [--apply]");
19472
+ if (args[2] !== undefined || options.bootstrapConfigPath) {
19473
+ throw new Error("Usage: fz bootstrap platform launch [--profile development|production] [--output <absolute-owner-only-directory>] [--secrets-env-file <owner-only-env>] [--apply]");
19474
+ }
19475
+ const environment = selectedForgeZeroLaunchEnvironment(options.deployProfile);
19476
+ if (environment === "production" && options.bootstrapSecretsEnvFile) {
19477
+ throw new Error("production platform launch requires interactive hidden secret prompts");
19109
19478
  }
19110
- const generated = writePlatformGenesisFleet(options.outputPath);
19479
+ const outputDirectory = options.outputPath ?? `/secure/forgezero/${environment}/genesis`;
19480
+ const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
19481
+ const generated = await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner);
19111
19482
  const fleet = readOperatorPlatformBootstrapFleetRequest(generated.fleetRequest);
19112
- if (options.apply && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
19483
+ if (options.apply && !environmentInput && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
19113
19484
  throw new Error("attended platform bootstrap was not confirmed");
19114
19485
  }
19115
- return await runAttendedPlatformFleet(fleet, options.apply);
19486
+ return await runAttendedPlatformFleet(fleet, options.apply, options.apply ? await platformLaunchClusterBootstrapCode(outputDirectory) : undefined, environmentInput);
19116
19487
  }
19117
19488
  if (operation === "platform" && args[1] === "fleet") {
19118
19489
  const mode = args[2];
@@ -19922,8 +20293,9 @@ function usage() {
19922
20293
  fz bootstrap platform bundle
19923
20294
  Build one clean exact-revision API Git bundle for genesis
19924
20295
  fz bootstrap platform launch
19925
- Prompt, review and run the complete attended platform
19926
- bootstrap; --output is the owner-only working directory
20296
+ One profile-driven attended launch. Public topology,
20297
+ bundle and edge names are derived; the owner supplies
20298
+ only email settings, hidden provider tokens and APPLY
19927
20299
  fz bootstrap platform remote <apply|status>
19928
20300
  Run typed bootstrap from the operator laptop through
19929
20301
  a pinned host; apply copies and verifies that bundle
@@ -20025,7 +20397,7 @@ function usage() {
20025
20397
  --root <path> Project root for project/deploy commands
20026
20398
  --name <name> Project or deploy name during init
20027
20399
  --purpose <text> Product outcome during project init
20028
- --profile <name> Initial deploy profile (default app)
20400
+ --profile <name> Platform launch environment or initial deploy profile
20029
20401
  --software <key@ver> Initial tested software coordinate; repeatable
20030
20402
  --channel <name> Catalog view: production or development (shows testing)
20031
20403
  --attestation Require hardware attestation for every deploy step