@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 +13 -14
- package/dist/agent-heartbeat.js +5 -3
- package/dist/bootstrap.js +9 -4
- package/dist/credential-schema.js +16 -4
- package/dist/fz-agent.js +17 -5
- package/dist/fz.js +329 -39
- package/dist/guest-enrolment.js +4 -2
- package/dist/metal-bootstrap.js +1 -1
- package/dist/migration-pull.js +4 -2
- package/dist/node-vault.js +16 -4
- package/dist/operator-bootstrap.js +9 -4
- package/dist/platform-bootstrap-runtime.d.ts +7 -0
- package/dist/platform-bootstrap-runtime.js +5 -2
- package/dist/platform-fleet-verification.js +6 -3
- package/dist/platform-genesis-config.d.ts +6 -1
- package/dist/platform-launch-profile.d.ts +44 -0
- package/dist/provision.js +1 -1
- package/dist/provisioning-pull.js +4 -2
- package/dist/signed-node-http.d.ts +2 -1
- package/dist/version.d.ts +1 -1
- package/package.json +4 -4
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.
|
|
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
|
|
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
|
|
93
|
-
fz bootstrap platform
|
|
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
|
|
107
|
-
fz bootstrap platform
|
|
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
|
|
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 —
|
|
758
|
+
## @forgezero/agent/operator-bootstrap — Launch the platform from an operator laptop
|
|
759
759
|
|
|
760
|
-
The
|
|
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
|
-
|
|
764
|
-
|
|
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)
|
package/dist/agent-heartbeat.js
CHANGED
|
@@ -691,9 +691,11 @@ import {
|
|
|
691
691
|
|
|
692
692
|
class SignedNodeHttpError extends Error {
|
|
693
693
|
status;
|
|
694
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4417
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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
|
|
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 =
|
|
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 ||
|
|
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
|
|
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
|
-
|
|
19926
|
-
|
|
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>
|
|
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
|
package/dist/guest-enrolment.js
CHANGED
|
@@ -22,9 +22,11 @@ import {
|
|
|
22
22
|
|
|
23
23
|
class SignedNodeHttpError extends Error {
|
|
24
24
|
status;
|
|
25
|
-
|
|
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);
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -366,7 +366,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
366
366
|
}
|
|
367
367
|
|
|
368
368
|
// src/version.ts
|
|
369
|
-
var VERSION = "0.1.
|
|
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";
|
package/dist/migration-pull.js
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
|
|
10
10
|
class SignedNodeHttpError extends Error {
|
|
11
11
|
status;
|
|
12
|
-
|
|
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);
|
package/dist/node-vault.js
CHANGED
|
@@ -129,9 +129,11 @@ import {
|
|
|
129
129
|
|
|
130
130
|
class SignedNodeHttpError extends Error {
|
|
131
131
|
status;
|
|
132
|
-
|
|
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
|
-
|
|
220
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
20
|
-
"@forgezero/vault": "0.1.
|
|
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",
|