@forgezero/agent 0.1.85 → 0.1.86

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
@@ -11,7 +11,7 @@ The node agent deploys on a compute; tenant mode also holds one complete project
11
11
 
12
12
  ## Package overview
13
13
 
14
- Every managed compute. It owns node identity, attestation and repository pipelines. Tenant mode additionally holds one whole project across its environments in RAM and answers over a unix socket; platform mode never replicates the API vault beside the process that already holds every unlocked realm seed. Supported runtimes: bun, node. Package root: @forgezero/agent. The sections below show the actual named imports emitted by each declaration entry point; wildcard imports are intentionally not used in the documentation.
14
+ Every managed compute. It owns node identity, attestation and repository pipelines. An SNP compute may hold its assigned whole project across environments in RAM after current attestation. Ordinary compute is API-only unless an administrator explicitly records the versioned non-TEE risk acceptance for that concrete machine. Platform mode never replicates realm-wide API authority beside the process that already holds unlocked realm seeds. Supported runtimes: bun, node. Package root: @forgezero/agent. The sections below show the actual named imports emitted by each declaration entry point; wildcard imports are intentionally not used in the documentation.
15
15
 
16
16
  ```text
17
17
  bun add -g @forgezero/agent
@@ -38,7 +38,7 @@ Every row links to the detailed explanation and named-import/example area below.
38
38
  | fz | The operator command: keys, status, genesis, unlock, run, and agent install. | Bun/Node host | [Reference + usage](#fz) |
39
39
  | fz run | The fallback for software that cannot read the local socket: start a process with scoped vault values in its environment. | Bun/Node host | [Reference + usage](#fz-run) |
40
40
  | fz agent install | Provision the same hardened agent service locally that the platform provisions remotely. | Bun/Node host | [Reference + usage](#fz-agent-install) |
41
- | fz-agent | The service entry point. Identity and private Git material arrive only as systemd credentials. A platform bootstrap may use a statically configured local control socket; an enrolled tenant guest builds each deployment manager from its signed server claim and reads all commands from the checked-out .fz definition. The signed enrolment binding constructs the exact project Vault replica scope, and an SNP guest completes its first live attestation before Vault synchronization begins. | Bun/Node host | [Reference + usage](#fz-agent) |
41
+ | fz-agent | The service entry point. Identity and private Git material arrive only as systemd credentials. A platform bootstrap may use a statically configured local control socket; an enrolled tenant guest builds each deployment manager from its signed server claim and reads all commands from the checked-out .fz definition. The signed enrolment binding constructs the exact project scope. Vault synchronization succeeds only when the API sees current SNP evidence or an audited versioned non-TEE acceptance; ordinary compute otherwise remains API-only and a revocation clears its resident cache. | Bun/Node host | [Reference + usage](#fz-agent) |
42
42
  | fz-agent (socket) | identity, sign, attest, get, sync and held — the whole interface an application sees. | Bun/Node host | [Reference + usage](#fz-agent-socket) |
43
43
  | @forgezero/agent/migration-pull | PQ-authenticated outbound lifecycle claims with fenced renewal, awaited async execution, idempotent acknowledgement and bounded shutdown drain. The executor reports local facts only. | Bun/Node host | [Reference + usage](#forgezero-agent-migration-pull) |
44
44
  | @forgezero/agent/lifecycle-helper | The root-owned fixed-operation boundary for migration work. Signed controller claims can request only private reachability probes, local health checks, or stops of units named in a root-owned profile; they cannot carry commands. | Bun/Node host | [Reference + usage](#forgezero-agent-lifecycle-helper) |
@@ -79,7 +79,7 @@ Every row links to the detailed explanation and named-import/example area below.
79
79
  | @forgezero/agent/software | Exact managed-software inventory, requirement and evidence contracts. | Bun/Node host | [Reference + usage](#forgezero-agent-software) |
80
80
  | @forgezero/agent/software-helper | Root-owned fixed installation/update boundary for declared software. | Bun/Node host | [Reference + usage](#forgezero-agent-software-helper) |
81
81
  | @forgezero/agent/ubuntu | The closed guest-image view: pinned Ubuntu 26.04 LTS x64 supports ordinary or SEV-SNP compute, while pinned Ubuntu 24.04 LTS x64 is limited to ordinary non-confidential compute. Every other OS remains rejected until its own pinned strategy and acceptance evidence are added. | Bun/Node host | [Reference + usage](#forgezero-agent-ubuntu) |
82
- | fz-agent (replica) | The project-scoped vault replica: resident in RAM, never on disk, invalidated by cursor rather than by timer, and refusing a read it can no longer vouch for. | Bun/Node host | [Reference + usage](#fz-agent-replica) |
82
+ | fz-agent (replica) | The authorized project-scoped vault replica: resident in RAM, never on disk, invalidated by cursor rather than by timer, cleared when local authority is revoked, and refusing a read it can no longer vouch for. | Bun/Node host | [Reference + usage](#fz-agent-replica) |
83
83
 
84
84
  ## Commands
85
85
 
@@ -89,8 +89,8 @@ fz deploy init --runtime containerd --os ubuntu-24.04 --replicas 1 --cpu-cores 2
89
89
  fz deploy init --runtime kata-snp --os ubuntu-26.04 --replicas 1 --cpu-cores 2 --memory-mib 4096 --storage-gib 40 --reuse require — Create one Kata QEMU SEV-SNP definition on existing eligible capacity. Use reuse prefer only with the explicit provisioning and spend-ceiling flags shown by fz deploy init --help.
90
90
  fz deploy compile — Compile and validate TypeScript deployment intent into .fz/deploy.plan.json.
91
91
  fz deploy check — Refuse stale plans, unsafe provider/action coordinates and unresolved deployment blockers.
92
- fz bootstrap platform bundle --root /absolute/api --branch dev --output /secure/forgezero/development/api.bundle Build one verified release-generation-one source bundle without a Git credential.
93
- fz bootstrap platform remote apply --bootstrap-config /secure/forgezero/development/dev-fz-n1-remote.json — Dry-plan one pinned operator-to-guest bootstrap; add --apply only after review.
92
+ fz bootstrap platform launch --profile development --apply Run the one attended ForgeZero launch: public topology and bundle are derived; answer only email settings, hidden provider tokens and APPLY.
93
+ fz bootstrap platform fleet apply --bootstrap-config /secure/forgezero/development/genesis/platform-fleet-remote.json — Resume or diagnose an already generated fleet plan; this is not the normal first-launch path.
94
94
  fz bootstrap platform --help — Inspect attended platform bootstrap inputs before applying them.
95
95
  fz bootstrap metal --help — Inspect the identity-only physical provisioner bootstrap.
96
96
  fz host transactions|schema-plan|schema-apply — Plan fixed supervised database maintenance; add --apply only after reviewing the exact argv and credential names.
@@ -103,8 +103,8 @@ fz deploy init --runtime containerd --os ubuntu-24.04 --replicas 1 --cpu-cores 2
103
103
  fz deploy init --runtime kata-snp --os ubuntu-26.04 --replicas 1 --cpu-cores 2 --memory-mib 4096 --storage-gib 40 --reuse require
104
104
  fz deploy compile
105
105
  fz deploy check
106
- fz bootstrap platform bundle --root /absolute/api --branch dev --output /secure/forgezero/development/api.bundle
107
- fz bootstrap platform remote apply --bootstrap-config /secure/forgezero/development/dev-fz-n1-remote.json
106
+ fz bootstrap platform launch --profile development --apply
107
+ fz bootstrap platform fleet apply --bootstrap-config /secure/forgezero/development/genesis/platform-fleet-remote.json
108
108
  fz bootstrap platform --help
109
109
  fz bootstrap metal --help
110
110
  fz host transactions|schema-plan|schema-apply
@@ -167,7 +167,7 @@ fz agent install --help
167
167
  <a id="fz-agent"></a>
168
168
  ## fz-agent
169
169
 
170
- The service entry point. Identity and private Git material arrive only as systemd credentials. A platform bootstrap may use a statically configured local control socket; an enrolled tenant guest builds each deployment manager from its signed server claim and reads all commands from the checked-out .fz definition. The signed enrolment binding constructs the exact project Vault replica scope, and an SNP guest completes its first live attestation before Vault synchronization begins. This entry requires a Bun/Node host runtime. Run the command with `--help` before applying it.
170
+ The service entry point. Identity and private Git material arrive only as systemd credentials. A platform bootstrap may use a statically configured local control socket; an enrolled tenant guest builds each deployment manager from its signed server claim and reads all commands from the checked-out .fz definition. The signed enrolment binding constructs the exact project scope. Vault synchronization succeeds only when the API sees current SNP evidence or an audited versioned non-TEE acceptance; ordinary compute otherwise remains API-only and a revocation clears its resident cache. This entry requires a Bun/Node host runtime. Run the command with `--help` before applying it.
171
171
 
172
172
  ```text
173
173
  fz-agent --help
@@ -755,14 +755,13 @@ import type {
755
755
  } from '@forgezero/agent/operator-bootstrap';
756
756
  ```
757
757
 
758
- ## @forgezero/agent/operator-bootstrap — Run the same typed platform bootstrap from an operator laptop
758
+ ## @forgezero/agent/operator-bootstrap — Launch the platform from an operator laptop
759
759
 
760
- The request contains the pinned SSH transport, reviewed platform config, and one verified local Git bundle copied identically to every genesis compute. No repository key is created. Hidden credential values are sealed immediately as systemd credentials.
760
+ The selected ForgeZero profile and pinned Metal evidence derive every public coordinate. The CLI builds the clean API bundle, retains cluster material only as an owner-host encrypted systemd credential for safe resume, deploys all three computes, seals hidden credentials, seeds inventory and prints the invitation. Low-level file commands are reserved for resume and repair.
761
761
 
762
762
  ```text
763
- fz bootstrap platform remote apply --bootstrap-config /secure/forgezero/development/dev-fz-n1-remote.json
764
- # Review, then allow the SSH-agent approval prompt:
765
- fz bootstrap platform remote apply --bootstrap-config /secure/forgezero/development/dev-fz-n1-remote.json --apply
763
+ # The command prints the derived plan before its final APPLY confirmation:
764
+ fz bootstrap platform launch --profile development --apply
766
765
  ```
767
766
 
768
767
  <a id="forgezero-agent-host-maintenance"></a>
@@ -1809,7 +1808,7 @@ export const selectedCapability = SUPPORTED_GUEST_IMAGE;
1809
1808
  <a id="fz-agent-replica"></a>
1810
1809
  ## fz-agent (replica)
1811
1810
 
1812
- The project-scoped vault replica: resident in RAM, never on disk, invalidated by cursor rather than by timer, and refusing a read it can no longer vouch for. This is a managed runtime interface label, not an import or standalone executable.
1811
+ The authorized project-scoped vault replica: resident in RAM, never on disk, invalidated by cursor rather than by timer, cleared when local authority is revoked, and refusing a read it can no longer vouch for. This is a managed runtime interface label, not an import or standalone executable.
1813
1812
 
1814
1813
  ```text
1815
1814
  fz-agent (replica)
@@ -691,9 +691,11 @@ import {
691
691
 
692
692
  class SignedNodeHttpError extends Error {
693
693
  status;
694
- constructor(status, message) {
694
+ code;
695
+ constructor(status, message, code) {
695
696
  super(message);
696
697
  this.status = status;
698
+ this.code = code;
697
699
  this.name = "SignedNodeHttpError";
698
700
  }
699
701
  }
@@ -740,7 +742,7 @@ async function postSignedNode(options, path, body) {
740
742
  if (!response.ok) {
741
743
  const failure = payload;
742
744
  const reason2 = failure ? failure.error?.message ?? failure.message : undefined;
743
- throw new SignedNodeHttpError(response.status, reason2 || `signed node request returned HTTP ${response.status}`);
745
+ throw new SignedNodeHttpError(response.status, reason2 || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
744
746
  }
745
747
  try {
746
748
  return await openResponse(recipient.secretKey, signature, payload);
@@ -750,7 +752,7 @@ async function postSignedNode(options, path, body) {
750
752
  }
751
753
 
752
754
  // src/version.ts
753
- var VERSION3 = "0.1.85";
755
+ var VERSION3 = "0.1.86";
754
756
 
755
757
  // src/agent-heartbeat.ts
756
758
  function readAgentHostMetrics() {
package/dist/bootstrap.js CHANGED
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1416
1416
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1417
1417
 
1418
1418
  // src/version.ts
1419
- var VERSION = "0.1.85";
1419
+ var VERSION = "0.1.86";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3002,12 +3002,14 @@ var httpsOrigin = (name, raw) => {
3002
3002
  return value.origin;
3003
3003
  };
3004
3004
  function validatePlatformInitialInventory(value) {
3005
- 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)) {
3005
+ 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)) {
3006
3006
  throw new Error("initial platform inventory coordinates are invalid");
3007
3007
  }
3008
3008
  safeAtom("initialInventory.region.label", value.region.label);
3009
3009
  if (value.region.city !== undefined)
3010
3010
  safeAtom("initialInventory.region.city", value.region.city);
3011
+ 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)))
3012
+ throw new Error("initial platform deployment evidence is invalid");
3011
3013
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
3012
3014
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
3013
3015
  "name",
@@ -3042,7 +3044,8 @@ function validatePlatformInitialInventory(value) {
3042
3044
  databaseRole: compute.databaseRole,
3043
3045
  databaseAgency: compute.databaseAgency,
3044
3046
  confidential: true
3045
- }))
3047
+ })),
3048
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
3046
3049
  };
3047
3050
  }
3048
3051
  var systemdValue = (name, raw) => {
@@ -4770,8 +4773,10 @@ function strictBootstrapDocument(value) {
4770
4773
  ], "runtime environment");
4771
4774
  const environment = runtime.environment;
4772
4775
  if (environment.initialInventory !== undefined) {
4773
- const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
4776
+ const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
4774
4777
  exactKeys(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
4778
+ if (inventory.deployment !== undefined)
4779
+ exactKeys(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
4775
4780
  if (!Array.isArray(inventory.computes))
4776
4781
  throw new Error("initial inventory computes must be an array");
4777
4782
  for (const compute of inventory.computes)
@@ -129,9 +129,11 @@ import {
129
129
 
130
130
  class SignedNodeHttpError extends Error {
131
131
  status;
132
- constructor(status, message) {
132
+ code;
133
+ constructor(status, message, code) {
133
134
  super(message);
134
135
  this.status = status;
136
+ this.code = code;
135
137
  this.name = "SignedNodeHttpError";
136
138
  }
137
139
  }
@@ -178,7 +180,7 @@ async function postSignedNode(options, path, body) {
178
180
  if (!response.ok) {
179
181
  const failure = payload;
180
182
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
181
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
183
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
182
184
  }
183
185
  try {
184
186
  return await openResponse(recipient.secretKey, signature, payload);
@@ -216,8 +218,17 @@ function tenantNodeApiUrl(apiUrl, tenantSlug) {
216
218
  return url.toString().replace(/\/$/, "");
217
219
  }
218
220
  function createNodeVaultCache(options) {
219
- const post = (operation, body) => postSignedNode(options, `v1/node/vault/${operation}`, body);
220
- return createSecretCache({
221
+ let cache;
222
+ const post = async (operation, body) => {
223
+ try {
224
+ return await postSignedNode(options, `v1/node/vault/${operation}`, body);
225
+ } catch (cause) {
226
+ if (cause instanceof SignedNodeHttpError && cause.code === "VAULT_API_ONLY")
227
+ cache?.clear();
228
+ throw cause;
229
+ }
230
+ };
231
+ cache = createSecretCache({
221
232
  ttlMs: options.ttlMs,
222
233
  maxStaleMs: options.maxStaleMs,
223
234
  list: async () => {
@@ -246,6 +257,7 @@ function createNodeVaultCache(options) {
246
257
  };
247
258
  }
248
259
  });
260
+ return cache;
249
261
  }
250
262
  function startNodeVaultSync(cache, options = {}) {
251
263
  const interval = Math.max(1000, options.intervalMs ?? 30000);
package/dist/fz-agent.js CHANGED
@@ -4326,9 +4326,11 @@ function createSecretCache(options) {
4326
4326
  // src/signed-node-http.ts
4327
4327
  class SignedNodeHttpError extends Error {
4328
4328
  status;
4329
- constructor(status, message) {
4329
+ code;
4330
+ constructor(status, message, code) {
4330
4331
  super(message);
4331
4332
  this.status = status;
4333
+ this.code = code;
4332
4334
  this.name = "SignedNodeHttpError";
4333
4335
  }
4334
4336
  }
@@ -4375,7 +4377,7 @@ async function postSignedNode(options, path, body) {
4375
4377
  if (!response.ok) {
4376
4378
  const failure = payload;
4377
4379
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
4378
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
4380
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
4379
4381
  }
4380
4382
  try {
4381
4383
  return await openResponse(recipient.secretKey, signature, payload);
@@ -4413,8 +4415,17 @@ function tenantNodeApiUrl(apiUrl, tenantSlug) {
4413
4415
  return url.toString().replace(/\/$/, "");
4414
4416
  }
4415
4417
  function createNodeVaultCache(options) {
4416
- const post = (operation, body) => postSignedNode(options, `v1/node/vault/${operation}`, body);
4417
- return createSecretCache({
4418
+ let cache;
4419
+ const post = async (operation, body) => {
4420
+ try {
4421
+ return await postSignedNode(options, `v1/node/vault/${operation}`, body);
4422
+ } catch (cause) {
4423
+ if (cause instanceof SignedNodeHttpError && cause.code === "VAULT_API_ONLY")
4424
+ cache?.clear();
4425
+ throw cause;
4426
+ }
4427
+ };
4428
+ cache = createSecretCache({
4418
4429
  ttlMs: options.ttlMs,
4419
4430
  maxStaleMs: options.maxStaleMs,
4420
4431
  list: async () => {
@@ -4443,6 +4454,7 @@ function createNodeVaultCache(options) {
4443
4454
  };
4444
4455
  }
4445
4456
  });
4457
+ return cache;
4446
4458
  }
4447
4459
  function startNodeVaultSync(cache, options = {}) {
4448
4460
  const interval = Math.max(1000, options.intervalMs ?? 30000);
@@ -9331,7 +9343,7 @@ async function writeAndCloseProcessInput(input, value) {
9331
9343
  }
9332
9344
 
9333
9345
  // src/version.ts
9334
- var VERSION2 = "0.1.85";
9346
+ var VERSION2 = "0.1.86";
9335
9347
 
9336
9348
  // src/ssh-bootstrap.ts
9337
9349
  class SshBootstrapError extends Error {
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) {
@@ -4837,7 +4838,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4837
4838
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4838
4839
 
4839
4840
  // src/version.ts
4840
- var VERSION2 = "0.1.85";
4841
+ var VERSION2 = "0.1.86";
4841
4842
 
4842
4843
  // src/software.ts
4843
4844
  var PINNED_BUN_VERSION = "1.3.14";
@@ -12139,12 +12140,14 @@ var httpsOrigin = (name, raw) => {
12139
12140
  return value.origin;
12140
12141
  };
12141
12142
  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)) {
12143
+ 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
12144
  throw new Error("initial platform inventory coordinates are invalid");
12144
12145
  }
12145
12146
  safeAtom("initialInventory.region.label", value.region.label);
12146
12147
  if (value.region.city !== undefined)
12147
12148
  safeAtom("initialInventory.region.city", value.region.city);
12149
+ 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)))
12150
+ throw new Error("initial platform deployment evidence is invalid");
12148
12151
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
12149
12152
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
12150
12153
  "name",
@@ -12179,7 +12182,8 @@ function validatePlatformInitialInventory(value) {
12179
12182
  databaseRole: compute.databaseRole,
12180
12183
  databaseAgency: compute.databaseAgency,
12181
12184
  confidential: true
12182
- }))
12185
+ })),
12186
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
12183
12187
  };
12184
12188
  }
12185
12189
  var systemdValue = (name, raw) => {
@@ -15143,8 +15147,10 @@ function strictBootstrapDocument(value) {
15143
15147
  ], "runtime environment");
15144
15148
  const environment = runtime.environment;
15145
15149
  if (environment.initialInventory !== undefined) {
15146
- const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
15150
+ const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
15147
15151
  exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
15152
+ if (inventory.deployment !== undefined)
15153
+ exactKeys2(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
15148
15154
  if (!Array.isArray(inventory.computes))
15149
15155
  throw new Error("initial inventory computes must be an array");
15150
15156
  for (const compute of inventory.computes)
@@ -17430,7 +17436,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
17430
17436
  }
17431
17437
 
17432
17438
  // src/platform-genesis-config.ts
17433
- function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region) {
17439
+ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment) {
17434
17440
  const fleet = validatePlatformGenesisGuests(guests);
17435
17441
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
17436
17442
  throw new Error("platform genesis requires the reviewed metal hostname");
@@ -17458,7 +17464,8 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17458
17464
  confidential: guest.confidential,
17459
17465
  databaseRole: index === 0 ? "master" : "joiner",
17460
17466
  databaseAgency: "member"
17461
- }))
17467
+ })),
17468
+ ...deployment ? { deployment } : {}
17462
17469
  };
17463
17470
  return fleet.map((guest, index) => {
17464
17471
  const node = nodes[index];
@@ -17499,6 +17506,122 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
17499
17506
  });
17500
17507
  }
17501
17508
 
17509
+ // src/platform-launch-profile.ts
17510
+ var PROFILES = {
17511
+ development: {
17512
+ environment: "development",
17513
+ branch: "dev",
17514
+ appOrigin: "https://dev.forgezero.net",
17515
+ apiOrigin: "https://dev-api.forgezero.net",
17516
+ zoneName: "forgezero.net",
17517
+ kvNamespaceTitle: "forgezero-dev-nodes",
17518
+ workerScriptName: "forgezero-api-edge-development",
17519
+ region: { key: "in-south", label: "India South", country: "IN" },
17520
+ agentOtlpEndpoint: "https://otel.forgezero.net",
17521
+ privateCidrs: ["10.42.0.0/24"]
17522
+ },
17523
+ production: {
17524
+ environment: "production",
17525
+ branch: "main",
17526
+ appOrigin: "https://www.forgezero.net",
17527
+ apiOrigin: "https://api.forgezero.net",
17528
+ zoneName: "forgezero.net",
17529
+ kvNamespaceTitle: "forgezero-nodes",
17530
+ workerScriptName: "forgezero-api-edge-production",
17531
+ region: { key: "in-south", label: "India South", country: "IN" },
17532
+ agentOtlpEndpoint: "https://otel.forgezero.net",
17533
+ privateCidrs: ["10.42.0.0/24"]
17534
+ }
17535
+ };
17536
+ function forgeZeroLaunchProfile(environment) {
17537
+ const profile = PROFILES[environment];
17538
+ return {
17539
+ ...profile,
17540
+ nodeHostname: (index) => environment === "development" ? `dev-api-n${index + 1}.forgezero.net` : `api-n${index + 1}.forgezero.net`
17541
+ };
17542
+ }
17543
+ var EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
17544
+ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
17545
+ const fleet = validatePlatformGenesisGuests(guests);
17546
+ if (!EMAIL.test(owner.custodianEmail) || !EMAIL.test(owner.email.from)) {
17547
+ throw new Error("bootstrap custodian and From addresses must be valid email addresses");
17548
+ }
17549
+ if (owner.email.provider === "smtp" && (!owner.email.host || !Number.isInteger(owner.email.port) || owner.email.port < 1 || owner.email.port > 65535 || !owner.email.user)) {
17550
+ throw new Error("SMTP host, port and user are required");
17551
+ }
17552
+ if (bundle.manifest.branch !== profile.branch)
17553
+ throw new Error("bootstrap bundle branch does not match the launch profile");
17554
+ const coordinators = fleet.map(({ address }) => `http://${address}:8529`);
17555
+ const nodeHostnames = fleet.map((_, index) => profile.nodeHostname(index));
17556
+ const first = fleet[0];
17557
+ return validateBootstrapConfig({
17558
+ kind: "platform",
17559
+ environment: profile.environment,
17560
+ profile: "platform-db-api",
17561
+ computeReference: first.name,
17562
+ nodeHostname: nodeHostnames[0],
17563
+ apiUrl: profile.apiOrigin,
17564
+ bootstrapBundle: {
17565
+ bundleFile: bundle.bundlePath,
17566
+ manifestFile: bundle.manifestPath,
17567
+ branch: profile.branch
17568
+ },
17569
+ telemetryEndpoint: profile.agentOtlpEndpoint,
17570
+ database: {
17571
+ role: "master",
17572
+ agency: "member",
17573
+ readPreferred: false,
17574
+ serverMode: "default",
17575
+ address: first.address,
17576
+ coordinators
17577
+ },
17578
+ enrolment: { source: "genesis-derived" },
17579
+ runtime: {
17580
+ environment: {
17581
+ softwareProfile: "platform-db-api",
17582
+ databaseRole: "master",
17583
+ databaseCoordinators: coordinators,
17584
+ databaseReadPreferredCoordinators: [],
17585
+ databaseAddress: first.address,
17586
+ databaseNetworkMode: "private-lan",
17587
+ databaseReplicationFactor: 2,
17588
+ databaseWriteConcern: 2,
17589
+ databaseUser: "forgezero-api",
17590
+ nodeHostname: nodeHostnames[0],
17591
+ nodeRegion: profile.region.key,
17592
+ nodeRole: "guest",
17593
+ appOrigin: profile.appOrigin,
17594
+ apiOrigin: profile.apiOrigin,
17595
+ publicApiPort: 3000,
17596
+ sharedDirectory: "/opt/forgezero/shared",
17597
+ seedSyncPeers: fleet.slice(1).flatMap(({ address }) => [3001, 3002].map((port) => `ws://${address}:${port}/_internal/forgezero/seed-mesh/v2`)),
17598
+ seedSyncMembers: nodeHostnames,
17599
+ seedSyncEpoch: `${profile.environment}-${bundle.manifest.revision.slice(0, 16)}`,
17600
+ concurrencyLimit: 128,
17601
+ drainDeadlineMs: 30000,
17602
+ otlpEndpoint: "http://127.0.0.1:4318",
17603
+ otlpCollectorUnit: FORGEZERO_OTEL_COLLECTOR_UNIT,
17604
+ agentOtlpEndpoint: profile.agentOtlpEndpoint,
17605
+ otlpFlushIntervalMs: 1e4,
17606
+ otlpTraceSampleRatio: 0.1,
17607
+ custodianEmail: owner.custodianEmail,
17608
+ email: owner.email,
17609
+ deployProfile: profile.environment
17610
+ },
17611
+ serviceUser: "forgezero-api",
17612
+ slotsDirectory: "/opt/forgezero/slots",
17613
+ bluePort: 3001,
17614
+ greenPort: 3002,
17615
+ healthPath: "/api/health",
17616
+ keepReleases: 5
17617
+ },
17618
+ firewall: { enabled: true, sshPort: 22, privateCidrs: [...profile.privateCidrs] },
17619
+ installCloudflared: true,
17620
+ installWarp: false,
17621
+ cloudflareHandoff: { handoffFile: "/run/forgezero/cloudflare-handoff.json", nodeName: first.name }
17622
+ });
17623
+ }
17624
+
17502
17625
  // src/host-maintenance.ts
17503
17626
  import { lstatSync as lstatSync8, readFileSync as readFileSync13 } from "fs";
17504
17627
  var ROOT = "/opt/forgezero";
@@ -18052,6 +18175,8 @@ function parseOptions(argv2) {
18052
18175
  options.dataFile = argv2[++index];
18053
18176
  else if (token === "--bootstrap-config")
18054
18177
  options.bootstrapConfigPath = argv2[++index];
18178
+ else if (token === "--secrets-env-file")
18179
+ options.bootstrapSecretsEnvFile = argv2[++index];
18055
18180
  else if (token === "--output")
18056
18181
  options.outputPath = argv2[++index];
18057
18182
  else if (token === "--query")
@@ -18648,9 +18773,100 @@ async function bootstrapSecret(question) {
18648
18773
  throw new Error(`${question} was not provided`);
18649
18774
  return value;
18650
18775
  }
18651
- async function promptPlatformBootstrapSecrets(config) {
18776
+ var PLATFORM_CLUSTER_CREDENTIAL = "forgezero-platform-cluster-bootstrap-code";
18777
+ function readPlatformLaunchEnvironment(path) {
18778
+ const absolute2 = resolve12(path);
18779
+ const stat2 = lstatSync10(absolute2);
18780
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
18781
+ throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
18782
+ }
18783
+ const values = new Map;
18784
+ for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
18785
+ const line = raw.trim();
18786
+ if (!line || line.startsWith("#"))
18787
+ continue;
18788
+ const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
18789
+ if (!match)
18790
+ throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
18791
+ if (values.has(match[1]))
18792
+ throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
18793
+ let value = match[2].trim();
18794
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
18795
+ value = value.slice(1, -1);
18796
+ }
18797
+ values.set(match[1], value);
18798
+ }
18799
+ const requiredValue = (name) => {
18800
+ const value = values.get(name)?.trim();
18801
+ if (!value)
18802
+ throw new Error(`bootstrap secret environment is missing ${name}`);
18803
+ return value;
18804
+ };
18805
+ const provider = requiredValue("FZ_EMAIL_PROVIDER");
18806
+ if (provider !== "smtp" && provider !== "jetemail")
18807
+ throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
18808
+ const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
18809
+ if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
18810
+ throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
18811
+ }
18812
+ const owner = provider === "jetemail" ? {
18813
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18814
+ email: {
18815
+ provider,
18816
+ from: requiredValue("FZ_EMAIL_FROM"),
18817
+ eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
18818
+ }
18819
+ } : {
18820
+ custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
18821
+ email: {
18822
+ provider,
18823
+ from: requiredValue("FZ_EMAIL_FROM"),
18824
+ host: requiredValue("FZ_SMTP_HOST"),
18825
+ port: smtpPort,
18826
+ user: requiredValue("FZ_SMTP_USER")
18827
+ }
18828
+ };
18829
+ return {
18830
+ owner,
18831
+ emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
18832
+ cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
18833
+ cloudflareApiToken: requiredValue("CF_API_TOKEN")
18834
+ };
18835
+ }
18836
+ async function platformLaunchClusterBootstrapCode(outputDirectory) {
18837
+ const path = join13(dirname14(outputDirectory), `${PLATFORM_CLUSTER_CREDENTIAL}.cred`);
18838
+ if (existsSync12(path)) {
18839
+ const stat2 = lstatSync10(path);
18840
+ if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
18841
+ throw new Error(`platform cluster credential must be one owner-only regular file: ${path}`);
18842
+ }
18843
+ const child2 = Bun.spawn(["systemd-creds", "decrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, path, "-"], { stdin: "ignore", stdout: "pipe", stderr: "pipe" });
18844
+ const [stdout, stderr2, exitCode2] = await Promise.all([
18845
+ new Response(child2.stdout).text(),
18846
+ new Response(child2.stderr).text(),
18847
+ child2.exited
18848
+ ]);
18849
+ if (exitCode2 !== 0)
18850
+ throw new Error(`cannot open the retained platform cluster credential: ${stderr2.trim()}`);
18851
+ const value2 = stdout.trim();
18852
+ if (!/^[a-f0-9]{64}$/i.test(value2))
18853
+ throw new Error("retained platform cluster credential is invalid");
18854
+ return value2;
18855
+ }
18856
+ mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
18857
+ const value = randomBytes10(32).toString("hex");
18858
+ const child = Bun.spawn(["systemd-creds", "encrypt", `--name=${PLATFORM_CLUSTER_CREDENTIAL}`, "-", path], { stdin: "pipe", stdout: "pipe", stderr: "pipe" });
18859
+ await writeAndCloseProcessInput(child.stdin, `${value}
18860
+ `);
18861
+ const [stderr, exitCode] = await Promise.all([new Response(child.stderr).text(), child.exited]);
18862
+ if (exitCode !== 0)
18863
+ throw new Error(`cannot retain the platform cluster credential: ${stderr.trim()}`);
18864
+ chmodSync7(path, 384);
18865
+ return value;
18866
+ }
18867
+ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
18652
18868
  return validatePlatformBootstrapSecrets(config, {
18653
- clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
18869
+ clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
18654
18870
  emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
18655
18871
  ...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
18656
18872
  ...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
@@ -18843,51 +19059,53 @@ function writeMetalOperatorFiles(directory) {
18843
19059
  });
18844
19060
  return { metalConfig, remoteRequest };
18845
19061
  }
18846
- function writePlatformGenesisFleet(directory) {
19062
+ function writePlatformGenesisFleet(directory, preset) {
18847
19063
  const output = genesisOutputDirectory(directory);
18848
- const metalRequestFile = bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
19064
+ const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
18849
19065
  const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
18850
- const guestHostKeysFile = bootstrapAnswer("Owner-only guest host-key evidence file");
19066
+ const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
18851
19067
  const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
18852
19068
  const fleet = metalRequest.genesis.nodes;
18853
19069
  const metalHostname = readMetalBootstrapConfig(metalRequest.metalConfigFile, {
18854
19070
  allowHistoricalRelease: true
18855
19071
  }).metalHostname;
18856
19072
  const checkpointPath = join13(output, "cloudflare-handoff.json");
18857
- const template = interactiveBootstrap("platform", fleet, checkpointPath);
18858
- const region = {
19073
+ const template = preset ? forgeZeroLaunchTemplate(preset.profile, fleet, preset.bundle, preset.owner) : interactiveBootstrap("platform", fleet, checkpointPath);
19074
+ const region = preset ? preset.profile.region : {
18859
19075
  label: bootstrapAnswer(`Region ${template.runtime.environment.nodeRegion} display label`),
18860
19076
  country: bootstrapAnswer("Region ISO 3166-1 alpha-2 country code").toUpperCase(),
18861
19077
  city: bootstrapAnswer("Region city")
18862
19078
  };
18863
- const nodes = fleet.map((guest, index) => index === 0 ? {
18864
- nodeHostname: template.nodeHostname,
18865
- cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18866
- } : {
18867
- nodeHostname: bootstrapAnswer(`${guest.name} public node hostname`),
19079
+ const nodes = fleet.map((guest, index) => ({
19080
+ nodeHostname: preset ? preset.profile.nodeHostname(index) : index === 0 ? template.nodeHostname : bootstrapAnswer(`${guest.name} public node hostname`),
18868
19081
  cloudflareHandoffFile: cloudflareHostHandoffPath(checkpointPath, guest.name)
18869
- });
18870
- const realtimeEnabled = bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
19082
+ }));
19083
+ const realtimeEnabled = preset ? true : bootstrapBoolean("Configure existing Worker realtime fan-out?", "yes");
18871
19084
  const cloudflareConfig = writeBootstrapConfig(join13(output, "cloudflare.json"), createCloudflareBootstrapDiscoveryCommandConfig({
18872
19085
  checkpointPath,
18873
19086
  discovery: {
18874
- zoneName: bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
18875
- kvNamespaceTitle: bootstrapAnswer("Existing Worker-bound KV namespace title"),
19087
+ zoneName: preset?.profile.zoneName ?? bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net"),
19088
+ kvNamespaceTitle: preset?.profile.kvNamespaceTitle ?? bootstrapAnswer("Existing Worker-bound KV namespace title"),
18876
19089
  ...realtimeEnabled ? {
18877
- workerScriptName: bootstrapAnswer("Existing Worker script name"),
18878
- endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
18879
- producer: bootstrapAnswer("Realtime producer identity", "platform-api")
19090
+ workerScriptName: preset?.profile.workerScriptName ?? bootstrapAnswer("Existing Worker script name"),
19091
+ endpoint: preset?.profile.apiOrigin ?? bootstrapAnswer("Stable public Worker HTTPS endpoint"),
19092
+ producer: "platform-api"
18880
19093
  } : {}
18881
19094
  },
18882
19095
  nodes: fleet.map((guest, index) => ({
18883
19096
  nodeName: guest.name,
18884
19097
  hostname: nodes[index].nodeHostname,
18885
19098
  service: "http://127.0.0.1:3000",
18886
- tunnelName: bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
19099
+ tunnelName: preset ? guest.name : bootstrapAnswer(`${guest.name} Tunnel name`, guest.name)
18887
19100
  }))
18888
19101
  }));
18889
19102
  const cloudflareAcceptance = join13(output, "cloudflare-acceptance.json");
18890
- const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region).map((config) => {
19103
+ const configs = platformGenesisBootstrapConfigs(template, fleet, nodes, metalHostname, region, preset ? {
19104
+ source: "bootstrap-bundle",
19105
+ branch: preset.profile.branch,
19106
+ revision: preset.bundle.manifest.revision,
19107
+ bundleSha256: preset.bundle.manifest.sha256
19108
+ } : undefined).map((config) => {
18891
19109
  const path = `${output}/${config.computeReference}-platform.json`;
18892
19110
  writeBootstrapConfig(path, config);
18893
19111
  return path;
@@ -18919,6 +19137,66 @@ function writePlatformGenesisFleet(directory) {
18919
19137
  const fleetRequest = writeOperatorPlatformBootstrapFleetRequest(join13(output, "platform-fleet-remote.json"), requests, { configFile: cloudflareConfig, acceptanceFile: cloudflareAcceptance });
18920
19138
  return { configs, requests, fleetRequest, cloudflareConfig, cloudflareAcceptance };
18921
19139
  }
19140
+ function selectedForgeZeroLaunchEnvironment(value) {
19141
+ if (value === "app")
19142
+ return "development";
19143
+ if (value !== "development" && value !== "production") {
19144
+ throw new Error("platform launch --profile must be development or production");
19145
+ }
19146
+ return value;
19147
+ }
19148
+ function forgeZeroLaunchOwnerInput() {
19149
+ const custodianEmail = bootstrapAnswer("First custodian email");
19150
+ const provider = bootstrapAnswer("Bootstrap email provider (smtp/jetemail)", "jetemail");
19151
+ if (provider !== "smtp" && provider !== "jetemail") {
19152
+ throw new Error("Bootstrap email provider must be smtp or jetemail");
19153
+ }
19154
+ const from = bootstrapAnswer("Bootstrap email From address");
19155
+ if (provider === "jetemail") {
19156
+ return {
19157
+ custodianEmail,
19158
+ email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
19159
+ };
19160
+ }
19161
+ return {
19162
+ custodianEmail,
19163
+ email: {
19164
+ provider,
19165
+ from,
19166
+ host: bootstrapAnswer("SMTP host"),
19167
+ port: bootstrapNumber("SMTP port", "587"),
19168
+ user: bootstrapAnswer("SMTP user")
19169
+ }
19170
+ };
19171
+ }
19172
+ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
19173
+ const profile = forgeZeroLaunchProfile(environment);
19174
+ const base = dirname14(outputDirectory);
19175
+ const metalRequestFile = join13(base, "metal", "metal-remote.json");
19176
+ const guestHostKeysFile = join13(base, "genesis-guest-host-keys.json");
19177
+ for (const [path, label] of [
19178
+ [metalRequestFile, "reviewed Metal request"],
19179
+ [guestHostKeysFile, "pinned genesis guest host-key evidence"]
19180
+ ]) {
19181
+ if (!existsSync12(path))
19182
+ throw new Error(`${label} is missing at ${path}`);
19183
+ }
19184
+ const root = resolve12(projectRoot);
19185
+ const nestedApi = join13(root, "api");
19186
+ const repositoryRoot = existsSync12(join13(nestedApi, "package.json")) ? nestedApi : root;
19187
+ const bundlePath = join13(base, "api.bundle");
19188
+ const bundle = existsSync12(bundlePath) || existsSync12(`${bundlePath}.json`) ? await readBootstrapBundle(bundlePath) : await buildBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
19189
+ if (bundle.manifest.branch !== profile.branch) {
19190
+ throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
19191
+ }
19192
+ return writePlatformGenesisFleet(outputDirectory, {
19193
+ profile,
19194
+ metalRequestFile,
19195
+ guestHostKeysFile,
19196
+ bundle,
19197
+ owner: owner ?? forgeZeroLaunchOwnerInput()
19198
+ });
19199
+ }
18922
19200
  function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint) {
18923
19201
  if (!process.stdin.isTTY)
18924
19202
  throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
@@ -19038,7 +19316,7 @@ function interactiveBootstrap(kind, genesisGuests, genesisCloudflareCheckpoint)
19038
19316
  } : {}
19039
19317
  };
19040
19318
  }
19041
- async function runAttendedPlatformFleet(fleet, apply) {
19319
+ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapCode, environmentInput) {
19042
19320
  const plan = planOperatorPlatformFleetBootstrap(fleet, "apply");
19043
19321
  const cloudflarePlan = readCloudflareBootstrapCommandPlan(fleet.cloudflareConfigFile);
19044
19322
  const coordinates = operatorPlatformFleetCoordinates(fleet);
@@ -19069,7 +19347,12 @@ async function runAttendedPlatformFleet(fleet, apply) {
19069
19347
  const firstConfig = readBootstrapConfig(first.platformConfigFile);
19070
19348
  if (firstConfig.kind !== "platform")
19071
19349
  throw new Error("remote platform fleet requires platform configs");
19072
- const secrets = await promptPlatformBootstrapSecrets(firstConfig);
19350
+ const secrets = environmentInput ? validatePlatformBootstrapSecrets(firstConfig, {
19351
+ clusterBootstrapCode: generatedClusterBootstrapCode,
19352
+ emailSecret: environmentInput.emailSecret,
19353
+ cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
19354
+ cloudflareApiToken: environmentInput.cloudflareApiToken
19355
+ }) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
19073
19356
  const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
19074
19357
  tunnelToken: secrets.cloudflareTunnelToken,
19075
19358
  apiToken: secrets.cloudflareApiToken
@@ -19104,15 +19387,21 @@ async function cmdBootstrap(options, args) {
19104
19387
  return 0;
19105
19388
  }
19106
19389
  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]");
19390
+ if (args[2] !== undefined || options.bootstrapConfigPath) {
19391
+ throw new Error("Usage: fz bootstrap platform launch [--profile development|production] [--output <absolute-owner-only-directory>] [--secrets-env-file <owner-only-env>] [--apply]");
19109
19392
  }
19110
- const generated = writePlatformGenesisFleet(options.outputPath);
19393
+ const environment = selectedForgeZeroLaunchEnvironment(options.deployProfile);
19394
+ if (environment === "production" && options.bootstrapSecretsEnvFile) {
19395
+ throw new Error("production platform launch requires interactive hidden secret prompts");
19396
+ }
19397
+ const outputDirectory = options.outputPath ?? `/secure/forgezero/${environment}/genesis`;
19398
+ const environmentInput = options.bootstrapSecretsEnvFile ? readPlatformLaunchEnvironment(options.bootstrapSecretsEnvFile) : undefined;
19399
+ const generated = await prepareForgeZeroPlatformLaunch(environment, outputDirectory, options.projectRoot, environmentInput?.owner);
19111
19400
  const fleet = readOperatorPlatformBootstrapFleetRequest(generated.fleetRequest);
19112
- if (options.apply && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
19401
+ if (options.apply && !environmentInput && bootstrapAnswer("Type APPLY to run the reviewed Cloudflare and three-compute plan") !== "APPLY") {
19113
19402
  throw new Error("attended platform bootstrap was not confirmed");
19114
19403
  }
19115
- return await runAttendedPlatformFleet(fleet, options.apply);
19404
+ return await runAttendedPlatformFleet(fleet, options.apply, options.apply ? await platformLaunchClusterBootstrapCode(outputDirectory) : undefined, environmentInput);
19116
19405
  }
19117
19406
  if (operation === "platform" && args[1] === "fleet") {
19118
19407
  const mode = args[2];
@@ -19922,8 +20211,9 @@ function usage() {
19922
20211
  fz bootstrap platform bundle
19923
20212
  Build one clean exact-revision API Git bundle for genesis
19924
20213
  fz bootstrap platform launch
19925
- Prompt, review and run the complete attended platform
19926
- bootstrap; --output is the owner-only working directory
20214
+ One profile-driven attended launch. Public topology,
20215
+ bundle and edge names are derived; the owner supplies
20216
+ only email settings, hidden provider tokens and APPLY
19927
20217
  fz bootstrap platform remote <apply|status>
19928
20218
  Run typed bootstrap from the operator laptop through
19929
20219
  a pinned host; apply copies and verifies that bundle
@@ -20025,7 +20315,7 @@ function usage() {
20025
20315
  --root <path> Project root for project/deploy commands
20026
20316
  --name <name> Project or deploy name during init
20027
20317
  --purpose <text> Product outcome during project init
20028
- --profile <name> Initial deploy profile (default app)
20318
+ --profile <name> Platform launch environment or initial deploy profile
20029
20319
  --software <key@ver> Initial tested software coordinate; repeatable
20030
20320
  --channel <name> Catalog view: production or development (shows testing)
20031
20321
  --attestation Require hardware attestation for every deploy step
@@ -22,9 +22,11 @@ import {
22
22
 
23
23
  class SignedNodeHttpError extends Error {
24
24
  status;
25
- constructor(status, message) {
25
+ code;
26
+ constructor(status, message, code) {
26
27
  super(message);
27
28
  this.status = status;
29
+ this.code = code;
28
30
  this.name = "SignedNodeHttpError";
29
31
  }
30
32
  }
@@ -71,7 +73,7 @@ async function postSignedNode(options, path, body) {
71
73
  if (!response.ok) {
72
74
  const failure = payload;
73
75
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
74
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
76
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
75
77
  }
76
78
  try {
77
79
  return await openResponse(recipient.secretKey, signature, payload);
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
366
366
  }
367
367
 
368
368
  // src/version.ts
369
- var VERSION = "0.1.85";
369
+ var VERSION = "0.1.86";
370
370
 
371
371
  // src/otel-collector.ts
372
372
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -9,9 +9,11 @@ import {
9
9
 
10
10
  class SignedNodeHttpError extends Error {
11
11
  status;
12
- constructor(status, message) {
12
+ code;
13
+ constructor(status, message, code) {
13
14
  super(message);
14
15
  this.status = status;
16
+ this.code = code;
15
17
  this.name = "SignedNodeHttpError";
16
18
  }
17
19
  }
@@ -58,7 +60,7 @@ async function postSignedNode(options, path, body) {
58
60
  if (!response.ok) {
59
61
  const failure = payload;
60
62
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
61
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
63
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
62
64
  }
63
65
  try {
64
66
  return await openResponse(recipient.secretKey, signature, payload);
@@ -129,9 +129,11 @@ import {
129
129
 
130
130
  class SignedNodeHttpError extends Error {
131
131
  status;
132
- constructor(status, message) {
132
+ code;
133
+ constructor(status, message, code) {
133
134
  super(message);
134
135
  this.status = status;
136
+ this.code = code;
135
137
  this.name = "SignedNodeHttpError";
136
138
  }
137
139
  }
@@ -178,7 +180,7 @@ async function postSignedNode(options, path, body) {
178
180
  if (!response.ok) {
179
181
  const failure = payload;
180
182
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
181
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
183
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
182
184
  }
183
185
  try {
184
186
  return await openResponse(recipient.secretKey, signature, payload);
@@ -216,8 +218,17 @@ function tenantNodeApiUrl(apiUrl, tenantSlug) {
216
218
  return url.toString().replace(/\/$/, "");
217
219
  }
218
220
  function createNodeVaultCache(options) {
219
- const post = (operation, body) => postSignedNode(options, `v1/node/vault/${operation}`, body);
220
- return createSecretCache({
221
+ let cache;
222
+ const post = async (operation, body) => {
223
+ try {
224
+ return await postSignedNode(options, `v1/node/vault/${operation}`, body);
225
+ } catch (cause) {
226
+ if (cause instanceof SignedNodeHttpError && cause.code === "VAULT_API_ONLY")
227
+ cache?.clear();
228
+ throw cause;
229
+ }
230
+ };
231
+ cache = createSecretCache({
221
232
  ttlMs: options.ttlMs,
222
233
  maxStaleMs: options.maxStaleMs,
223
234
  list: async () => {
@@ -246,6 +257,7 @@ function createNodeVaultCache(options) {
246
257
  };
247
258
  }
248
259
  });
260
+ return cache;
249
261
  }
250
262
  function startNodeVaultSync(cache, options = {}) {
251
263
  const interval = Math.max(1000, options.intervalMs ?? 30000);
@@ -1416,7 +1416,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1416
1416
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1417
1417
 
1418
1418
  // src/version.ts
1419
- var VERSION = "0.1.85";
1419
+ var VERSION = "0.1.86";
1420
1420
 
1421
1421
  // src/software.ts
1422
1422
  var PINNED_BUN_VERSION = "1.3.14";
@@ -3002,12 +3002,14 @@ var httpsOrigin = (name, raw) => {
3002
3002
  return value.origin;
3003
3003
  };
3004
3004
  function validatePlatformInitialInventory(value) {
3005
- 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)) {
3005
+ 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)) {
3006
3006
  throw new Error("initial platform inventory coordinates are invalid");
3007
3007
  }
3008
3008
  safeAtom("initialInventory.region.label", value.region.label);
3009
3009
  if (value.region.city !== undefined)
3010
3010
  safeAtom("initialInventory.region.city", value.region.city);
3011
+ 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)))
3012
+ throw new Error("initial platform deployment evidence is invalid");
3011
3013
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
3012
3014
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
3013
3015
  "name",
@@ -3042,7 +3044,8 @@ function validatePlatformInitialInventory(value) {
3042
3044
  databaseRole: compute.databaseRole,
3043
3045
  databaseAgency: compute.databaseAgency,
3044
3046
  confidential: true
3045
- }))
3047
+ })),
3048
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
3046
3049
  };
3047
3050
  }
3048
3051
  var systemdValue = (name, raw) => {
@@ -4770,8 +4773,10 @@ function strictBootstrapDocument(value) {
4770
4773
  ], "runtime environment");
4771
4774
  const environment = runtime.environment;
4772
4775
  if (environment.initialInventory !== undefined) {
4773
- const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes"], "initial inventory");
4776
+ const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes", "deployment"], "initial inventory");
4774
4777
  exactKeys(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
4778
+ if (inventory.deployment !== undefined)
4779
+ exactKeys(inventory.deployment, ["source", "branch", "revision", "bundleSha256"], "initial deployment evidence");
4775
4780
  if (!Array.isArray(inventory.computes))
4776
4781
  throw new Error("initial inventory computes must be an array");
4777
4782
  for (const compute of inventory.computes)
@@ -31,6 +31,13 @@ export interface PlatformInitialInventory {
31
31
  PlatformInitialInventoryCompute,
32
32
  PlatformInitialInventoryCompute
33
33
  ];
34
+ /** Generation-one release evidence; external Git automation is connected after custody opens Vault. */
35
+ deployment?: {
36
+ source: 'bootstrap-bundle';
37
+ branch: 'dev' | 'main';
38
+ revision: string;
39
+ bundleSha256: string;
40
+ };
34
41
  }
35
42
  export type PlatformBootstrapEmail = {
36
43
  provider: 'smtp';
@@ -724,12 +724,14 @@ var httpsOrigin = (name, raw) => {
724
724
  return value.origin;
725
725
  };
726
726
  function validatePlatformInitialInventory(value) {
727
- 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)) {
727
+ 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)) {
728
728
  throw new Error("initial platform inventory coordinates are invalid");
729
729
  }
730
730
  safeAtom("initialInventory.region.label", value.region.label);
731
731
  if (value.region.city !== undefined)
732
732
  safeAtom("initialInventory.region.city", value.region.city);
733
+ 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)))
734
+ throw new Error("initial platform deployment evidence is invalid");
733
735
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
734
736
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
735
737
  "name",
@@ -764,7 +766,8 @@ function validatePlatformInitialInventory(value) {
764
766
  databaseRole: compute.databaseRole,
765
767
  databaseAgency: compute.databaseAgency,
766
768
  confidential: true
767
- }))
769
+ })),
770
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
768
771
  };
769
772
  }
770
773
  var systemdValue = (name, raw) => {
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2647
  }
2648
2648
 
2649
2649
  // src/version.ts
2650
- var VERSION3 = "0.1.85";
2650
+ var VERSION3 = "0.1.86";
2651
2651
 
2652
2652
  // src/egress-policy.ts
2653
2653
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -3820,12 +3820,14 @@ var httpsOrigin = (name, raw) => {
3820
3820
  return value.origin;
3821
3821
  };
3822
3822
  function validatePlatformInitialInventory(value) {
3823
- 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)) {
3823
+ 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)) {
3824
3824
  throw new Error("initial platform inventory coordinates are invalid");
3825
3825
  }
3826
3826
  safeAtom("initialInventory.region.label", value.region.label);
3827
3827
  if (value.region.city !== undefined)
3828
3828
  safeAtom("initialInventory.region.city", value.region.city);
3829
+ 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)))
3830
+ throw new Error("initial platform deployment evidence is invalid");
3829
3831
  const computes = validatePlatformGenesisGuests(value.computes.map((compute) => {
3830
3832
  if (!compute || typeof compute !== "object" || Array.isArray(compute) || Object.keys(compute).some((key) => ![
3831
3833
  "name",
@@ -3860,7 +3862,8 @@ function validatePlatformInitialInventory(value) {
3860
3862
  databaseRole: compute.databaseRole,
3861
3863
  databaseAgency: compute.databaseAgency,
3862
3864
  confidential: true
3863
- }))
3865
+ })),
3866
+ ...value.deployment ? { deployment: { ...value.deployment } } : {}
3864
3867
  };
3865
3868
  }
3866
3869
  var systemdValue = (name, raw) => {
@@ -10,4 +10,9 @@ export interface PlatformGenesisRegionInput {
10
10
  city?: string;
11
11
  }
12
12
  /** Expand one reviewed common template into the exact three genesis hosts. */
13
- export declare function platformGenesisBootstrapConfigs(template: PlatformBootstrapConfig, guests: readonly PlatformGenesisGuest[], nodes: readonly PlatformGenesisNodeConfigInput[], metalHostname: string, region: PlatformGenesisRegionInput): readonly PlatformBootstrapConfig[];
13
+ export declare function platformGenesisBootstrapConfigs(template: PlatformBootstrapConfig, guests: readonly PlatformGenesisGuest[], nodes: readonly PlatformGenesisNodeConfigInput[], metalHostname: string, region: PlatformGenesisRegionInput, deployment?: {
14
+ source: 'bootstrap-bundle';
15
+ branch: 'dev' | 'main';
16
+ revision: string;
17
+ bundleSha256: string;
18
+ }): readonly PlatformBootstrapConfig[];
@@ -0,0 +1,44 @@
1
+ import type { PlatformBootstrapConfig } from './bootstrap';
2
+ import type { BootstrapBundleBuildResult } from './bootstrap-bundle';
3
+ import type { PlatformGenesisGuest } from './platform-genesis';
4
+ export type ForgeZeroLaunchEnvironment = 'development' | 'production';
5
+ export interface ForgeZeroLaunchProfile {
6
+ environment: ForgeZeroLaunchEnvironment;
7
+ branch: 'dev' | 'main';
8
+ appOrigin: string;
9
+ apiOrigin: string;
10
+ zoneName: 'forgezero.net';
11
+ kvNamespaceTitle: string;
12
+ workerScriptName: string;
13
+ region: {
14
+ key: string;
15
+ label: string;
16
+ country: string;
17
+ city?: string;
18
+ };
19
+ agentOtlpEndpoint: string;
20
+ privateCidrs: readonly string[];
21
+ nodeHostname(nodeIndex: number): string;
22
+ }
23
+ export interface ForgeZeroLaunchOwnerInput {
24
+ custodianEmail: string;
25
+ email: {
26
+ provider: 'smtp';
27
+ host: string;
28
+ port: number;
29
+ user: string;
30
+ from: string;
31
+ } | {
32
+ provider: 'jetemail';
33
+ from: string;
34
+ eu: boolean;
35
+ };
36
+ }
37
+ /**
38
+ * ForgeZero's own genesis public coordinates. They are reviewed source data,
39
+ * not an operator questionnaire. Tenant and reusable Agent deployments remain
40
+ * driven by their own typed plans and never inherit these product coordinates.
41
+ */
42
+ export declare function forgeZeroLaunchProfile(environment: ForgeZeroLaunchEnvironment): ForgeZeroLaunchProfile;
43
+ /** Build the one common config which is expanded into the three fixed nodes. */
44
+ export declare function forgeZeroLaunchTemplate(profile: ForgeZeroLaunchProfile, guests: readonly PlatformGenesisGuest[], bundle: BootstrapBundleBuildResult, owner: ForgeZeroLaunchOwnerInput): PlatformBootstrapConfig;
package/dist/provision.js CHANGED
@@ -2647,7 +2647,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2647
2647
  }
2648
2648
 
2649
2649
  // src/version.ts
2650
- var VERSION3 = "0.1.85";
2650
+ var VERSION3 = "0.1.86";
2651
2651
 
2652
2652
  // src/egress-policy.ts
2653
2653
  import { realpathSync as realpathSync3 } from "node:fs";
@@ -9,9 +9,11 @@ import {
9
9
 
10
10
  class SignedNodeHttpError extends Error {
11
11
  status;
12
- constructor(status, message) {
12
+ code;
13
+ constructor(status, message, code) {
13
14
  super(message);
14
15
  this.status = status;
16
+ this.code = code;
15
17
  this.name = "SignedNodeHttpError";
16
18
  }
17
19
  }
@@ -58,7 +60,7 @@ async function postSignedNode(options, path, body) {
58
60
  if (!response.ok) {
59
61
  const failure = payload;
60
62
  const reason = failure ? failure.error?.message ?? failure.message : undefined;
61
- throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`);
63
+ throw new SignedNodeHttpError(response.status, reason || `signed node request returned HTTP ${response.status}`, failure?.error?.code);
62
64
  }
63
65
  try {
64
66
  return await openResponse(recipient.secretKey, signature, payload);
@@ -8,7 +8,8 @@ export interface SignedNodeHttpOptions {
8
8
  }
9
9
  export declare class SignedNodeHttpError extends Error {
10
10
  readonly status: number;
11
- constructor(status: number, message: string);
11
+ readonly code?: string | undefined;
12
+ constructor(status: number, message: string, code?: string | undefined);
12
13
  }
13
14
  /**
14
15
  * Validate the one origin every outbound Agent client shares.
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.85";
2
+ export declare const VERSION = "0.1.86";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.85",
3
+ "version": "0.1.86",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",
@@ -11,13 +11,13 @@
11
11
  "typescript": "^5.6.0",
12
12
  "@types/bun": "latest",
13
13
  "@types/node": "^22.0.0",
14
- "@forgezero/access": "0.1.7",
14
+ "@forgezero/access": "0.1.8",
15
15
  "@noble/curves": "^2.2.0",
16
16
  "@noble/post-quantum": "^0.6.1"
17
17
  },
18
18
  "dependencies": {
19
- "@forgezero/runtime": "0.1.12",
20
- "@forgezero/vault": "0.1.17",
19
+ "@forgezero/runtime": "0.1.13",
20
+ "@forgezero/vault": "0.1.18",
21
21
  "@noble/curves": "2.2.0",
22
22
  "@noble/hashes": "2.2.0",
23
23
  "@noble/post-quantum": "0.6.1",