@forgezero/agent 0.1.102 → 0.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap-bundle.d.ts +6 -0
- package/dist/bootstrap-bundle.js +41 -0
- package/dist/bootstrap.d.ts +4 -0
- package/dist/bootstrap.js +126 -9
- package/dist/database-auth-verify.d.ts +6 -0
- package/dist/deployment-connectivity.d.ts +21 -0
- package/dist/deployment-connectivity.js +117 -21
- package/dist/deployment-pull.d.ts +1 -1
- package/dist/deployment-topology.d.ts +2 -0
- package/dist/deployment-topology.js +2 -0
- package/dist/deployment.d.ts +18 -1
- package/dist/fz-agent.js +251 -34
- package/dist/fz.js +313 -97
- package/dist/metal-bootstrap.d.ts +8 -0
- package/dist/metal-bootstrap.js +10 -3
- package/dist/operator-bootstrap.d.ts +10 -0
- package/dist/operator-bootstrap.js +181 -12
- package/dist/platform-bootstrap-runtime.d.ts +21 -1
- package/dist/platform-bootstrap-runtime.js +20 -1
- package/dist/platform-fleet-verification.js +149 -28
- package/dist/platform-genesis-config.d.ts +1 -1
- package/dist/platform-launch-env.d.ts +19 -0
- package/dist/platform-launch-profile.d.ts +5 -0
- package/dist/provision.js +120 -24
- package/dist/software-helper.js +117 -21
- package/dist/version.d.ts +1 -1
- package/package.json +2 -2
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.20";
|
|
4638
4638
|
var init_dist = __esm(() => {
|
|
4639
4639
|
init_identity();
|
|
4640
4640
|
VaultError = class VaultError extends Error {
|
|
@@ -4810,9 +4810,9 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4810
4810
|
}
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
|
-
import { existsSync as existsSync13, lstatSync as
|
|
4813
|
+
import { existsSync as existsSync13, lstatSync as lstatSync11, mkdirSync as mkdirSync13, readFileSync as readFileSync17, renameSync as renameSync10, rmSync as rmSync8, writeFileSync as writeFileSync13 } from "fs";
|
|
4814
4814
|
import { randomBytes as randomBytes10 } from "crypto";
|
|
4815
|
-
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as
|
|
4815
|
+
import { basename as basename3, dirname as dirname14, isAbsolute as isAbsolute7, join as join13, resolve as resolve13 } from "path";
|
|
4816
4816
|
|
|
4817
4817
|
// src/process-input.ts
|
|
4818
4818
|
async function writeAndCloseProcessInput(input, value) {
|
|
@@ -4841,7 +4841,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4841
4841
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4842
4842
|
|
|
4843
4843
|
// src/version.ts
|
|
4844
|
-
var VERSION2 = "0.1.
|
|
4844
|
+
var VERSION2 = "0.1.107";
|
|
4845
4845
|
|
|
4846
4846
|
// src/software.ts
|
|
4847
4847
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -12058,7 +12058,7 @@ function removeSession(api, realm, path = defaultSessionPath()) {
|
|
|
12058
12058
|
}
|
|
12059
12059
|
|
|
12060
12060
|
// src/bootstrap.ts
|
|
12061
|
-
import { createHash as createHash4, createHmac as createHmac2, randomBytes as randomBytes7 } from "crypto";
|
|
12061
|
+
import { createHash as createHash4, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes7 } from "crypto";
|
|
12062
12062
|
import {
|
|
12063
12063
|
chmodSync as chmodSync4,
|
|
12064
12064
|
existsSync as existsSync9,
|
|
@@ -12239,10 +12239,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
12239
12239
|
};
|
|
12240
12240
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
12241
12241
|
function validatePlatformInitialInventory(value) {
|
|
12242
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "region", "computes", "attestation", "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)) {
|
|
12242
|
+
if (!value || typeof value !== "object" || Array.isArray(value) || Object.keys(value).some((key) => !["metalHostname", "metalIdentity", "region", "computes", "attestation", "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)) {
|
|
12243
12243
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
12244
12244
|
}
|
|
12245
12245
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
12246
|
+
if (value.metalIdentity !== undefined) {
|
|
12247
|
+
const identity = value.metalIdentity;
|
|
12248
|
+
if (!identity || typeof identity !== "object" || Array.isArray(identity) || Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !identity.publicKeys || identity.publicKeys.ed25519 !== identity.nodeKey || !/^[A-Za-z0-9_-]{1,8192}$/.test(identity.publicKeys.mlDsa) || Object.keys(identity.publicKeys).some((key) => !["ed25519", "mlDsa"].includes(key))) {
|
|
12249
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
12250
|
+
}
|
|
12251
|
+
}
|
|
12246
12252
|
if (value.region.city !== undefined)
|
|
12247
12253
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
12248
12254
|
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)))
|
|
@@ -12277,6 +12283,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
12277
12283
|
}
|
|
12278
12284
|
return {
|
|
12279
12285
|
metalHostname: value.metalHostname,
|
|
12286
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
12280
12287
|
region: { ...value.region },
|
|
12281
12288
|
computes: value.computes.map((compute, index) => ({
|
|
12282
12289
|
...computes[index],
|
|
@@ -12330,6 +12337,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
12330
12337
|
} else if (input.email !== undefined) {
|
|
12331
12338
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
12332
12339
|
}
|
|
12340
|
+
if (input.githubApp) {
|
|
12341
|
+
if (!/^(?:Iv1\.[A-Fa-f0-9]{16}|Ov23li[A-Za-z0-9]{14,})$/.test(input.githubApp.clientId) || !/^[1-9][0-9]{0,19}$/.test(input.githubApp.appId) || !/^[a-z0-9][a-z0-9-]{0,99}$/.test(input.githubApp.slug)) {
|
|
12342
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
12343
|
+
}
|
|
12344
|
+
}
|
|
12333
12345
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
12334
12346
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
12335
12347
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -12460,6 +12472,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
12460
12472
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
12461
12473
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
12462
12474
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
12475
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
12476
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
12477
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
12463
12478
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
12464
12479
|
};
|
|
12465
12480
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -12471,6 +12486,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
12471
12486
|
const optional = [
|
|
12472
12487
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
12473
12488
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
12489
|
+
["fz_github.clientSecret", options.githubApp],
|
|
12490
|
+
["fz_github.privateKey", options.githubApp],
|
|
12491
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
12474
12492
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
12475
12493
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
12476
12494
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -12478,6 +12496,7 @@ function platformApiCredentialSpecs(options) {
|
|
|
12478
12496
|
];
|
|
12479
12497
|
return [
|
|
12480
12498
|
{ name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
|
|
12499
|
+
{ name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
|
|
12481
12500
|
{ name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
|
|
12482
12501
|
...optional.filter(([, present]) => present).map(([name]) => ({
|
|
12483
12502
|
name,
|
|
@@ -14061,6 +14080,46 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
14061
14080
|
throw cause;
|
|
14062
14081
|
}
|
|
14063
14082
|
}
|
|
14083
|
+
async function refreshBootstrapBundle(input, exec = run) {
|
|
14084
|
+
const outputPath = resolve6(input.outputPath);
|
|
14085
|
+
const manifestPath = `${outputPath}.json`;
|
|
14086
|
+
if (existsSync8(outputPath) !== existsSync8(manifestPath)) {
|
|
14087
|
+
throw new Error("bootstrap bundle and manifest must either both exist or both be absent");
|
|
14088
|
+
}
|
|
14089
|
+
const candidatePath = `${outputPath}.candidate.${process.pid}.${randomBytes6(6).toString("hex")}`;
|
|
14090
|
+
const candidate = await buildBootstrapBundle({ ...input, outputPath: candidatePath }, exec);
|
|
14091
|
+
try {
|
|
14092
|
+
if (!existsSync8(outputPath)) {
|
|
14093
|
+
renameSync7(candidate.bundlePath, outputPath);
|
|
14094
|
+
renameSync7(candidate.manifestPath, manifestPath);
|
|
14095
|
+
return readBootstrapBundle(outputPath);
|
|
14096
|
+
}
|
|
14097
|
+
const current2 = await readBootstrapBundle(outputPath);
|
|
14098
|
+
if (current2.manifest.branch === candidate.manifest.branch && current2.manifest.revision === candidate.manifest.revision) {
|
|
14099
|
+
rmSync5(candidate.bundlePath, { force: true });
|
|
14100
|
+
rmSync5(candidate.manifestPath, { force: true });
|
|
14101
|
+
return current2;
|
|
14102
|
+
}
|
|
14103
|
+
const archivePath = `${outputPath}.before-${current2.manifest.revision.slice(0, 7)}-${Date.now()}`;
|
|
14104
|
+
const archiveManifestPath = `${archivePath}.json`;
|
|
14105
|
+
renameSync7(outputPath, archivePath);
|
|
14106
|
+
renameSync7(manifestPath, archiveManifestPath);
|
|
14107
|
+
try {
|
|
14108
|
+
renameSync7(candidate.bundlePath, outputPath);
|
|
14109
|
+
renameSync7(candidate.manifestPath, manifestPath);
|
|
14110
|
+
return readBootstrapBundle(outputPath);
|
|
14111
|
+
} catch (cause) {
|
|
14112
|
+
rmSync5(outputPath, { force: true });
|
|
14113
|
+
rmSync5(manifestPath, { force: true });
|
|
14114
|
+
renameSync7(archivePath, outputPath);
|
|
14115
|
+
renameSync7(archiveManifestPath, manifestPath);
|
|
14116
|
+
throw cause;
|
|
14117
|
+
}
|
|
14118
|
+
} finally {
|
|
14119
|
+
rmSync5(candidate.bundlePath, { force: true });
|
|
14120
|
+
rmSync5(candidate.manifestPath, { force: true });
|
|
14121
|
+
}
|
|
14122
|
+
}
|
|
14064
14123
|
|
|
14065
14124
|
// src/bootstrap.ts
|
|
14066
14125
|
var PLATFORM_BOOTSTRAP_PROFILES = [
|
|
@@ -14102,7 +14161,17 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14102
14161
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
14103
14162
|
throw new Error("bootstrap credential input must be an object");
|
|
14104
14163
|
const source = input;
|
|
14105
|
-
const allowed = [
|
|
14164
|
+
const allowed = [
|
|
14165
|
+
"clusterBootstrapCode",
|
|
14166
|
+
"emailSecret",
|
|
14167
|
+
"enrolmentToken",
|
|
14168
|
+
"backupS3Secret",
|
|
14169
|
+
"cloudflareTunnelToken",
|
|
14170
|
+
"cloudflareApiToken",
|
|
14171
|
+
"githubClientSecret",
|
|
14172
|
+
"githubPrivateKeyBase64",
|
|
14173
|
+
"githubWebhookSecret"
|
|
14174
|
+
];
|
|
14106
14175
|
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
14107
14176
|
if (unknown.length)
|
|
14108
14177
|
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
@@ -14112,6 +14181,9 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14112
14181
|
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
14113
14182
|
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
14114
14183
|
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
14184
|
+
const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
|
|
14185
|
+
const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
|
|
14186
|
+
const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
|
|
14115
14187
|
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
14116
14188
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
14117
14189
|
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
@@ -14131,12 +14203,29 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14131
14203
|
}
|
|
14132
14204
|
if (cloudflareTunnelToken)
|
|
14133
14205
|
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
14206
|
+
const githubConfigured = Boolean(config.runtime.environment.githubApp);
|
|
14207
|
+
if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
|
|
14208
|
+
throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
|
|
14209
|
+
}
|
|
14210
|
+
if (githubConfigured) {
|
|
14211
|
+
if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
|
|
14212
|
+
throw new Error("GitHub App client or webhook secret is malformed");
|
|
14213
|
+
}
|
|
14214
|
+
try {
|
|
14215
|
+
const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
|
|
14216
|
+
if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
|
|
14217
|
+
throw new Error("not RSA");
|
|
14218
|
+
} catch {
|
|
14219
|
+
throw new Error("GitHub App private key must be a base64-encoded RSA private key");
|
|
14220
|
+
}
|
|
14221
|
+
}
|
|
14134
14222
|
return {
|
|
14135
14223
|
clusterBootstrapCode,
|
|
14136
14224
|
emailSecret,
|
|
14137
14225
|
...enrolmentToken ? { enrolmentToken } : {},
|
|
14138
14226
|
...backupS3Secret ? { backupS3Secret } : {},
|
|
14139
|
-
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
14227
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
|
|
14228
|
+
...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
|
|
14140
14229
|
};
|
|
14141
14230
|
}
|
|
14142
14231
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
@@ -14151,6 +14240,7 @@ var STATE_PATH = BOOTSTRAP_STATE_PATH;
|
|
|
14151
14240
|
var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
|
|
14152
14241
|
var CREDS = "/etc/forgezero/creds";
|
|
14153
14242
|
var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
14243
|
+
var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
|
|
14154
14244
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
14155
14245
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
14156
14246
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
@@ -14436,7 +14526,7 @@ WantedBy=multi-user.target
|
|
|
14436
14526
|
function databaseVerifyUnit(config) {
|
|
14437
14527
|
const { address } = config.database;
|
|
14438
14528
|
return `[Unit]
|
|
14439
|
-
Description=
|
|
14529
|
+
Description=Secure and verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
|
|
14440
14530
|
Requires=forgezero-db.service
|
|
14441
14531
|
After=forgezero-db.service
|
|
14442
14532
|
PartOf=forgezero-db.service
|
|
@@ -14446,7 +14536,9 @@ Type=oneshot
|
|
|
14446
14536
|
User=arangodb
|
|
14447
14537
|
Group=arangodb
|
|
14448
14538
|
LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
|
|
14449
|
-
|
|
14539
|
+
LoadCredentialEncrypted=arangodb-root-password:${ARANGO_ROOT_CREDENTIAL}
|
|
14540
|
+
ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;let ok=false;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default"){ok=true;break;}last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}if(!ok)throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));const password=require("fs").read("%d/arangodb-root-password").trim();if(password.length<48)throw new Error("ArangoDB root credential is malformed");require("@arangodb/users").update("root",password,true);'
|
|
14541
|
+
ExecStart=/usr/local/bin/fz-agent database-auth-verify --endpoint=http://${unitEscape(address)}:8529
|
|
14450
14542
|
RemainAfterExit=yes
|
|
14451
14543
|
TimeoutStartSec=200
|
|
14452
14544
|
NoNewPrivileges=true
|
|
@@ -14688,6 +14780,12 @@ function approvedPlatformRepairIdentityDigests(config) {
|
|
|
14688
14780
|
delete beforeAttestation.runtime.environment.initialInventory.attestation;
|
|
14689
14781
|
prior.push(beforeAttestation);
|
|
14690
14782
|
}
|
|
14783
|
+
const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
|
|
14784
|
+
if (metalIdentity && config.enrolment.source === "genesis-derived") {
|
|
14785
|
+
const beforeMetalIdentity = structuredClone(config);
|
|
14786
|
+
delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
|
|
14787
|
+
prior.push(beforeMetalIdentity);
|
|
14788
|
+
}
|
|
14691
14789
|
if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
|
|
14692
14790
|
const beforeReplicaCustodianEmail = structuredClone(config);
|
|
14693
14791
|
delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
|
|
@@ -14907,6 +15005,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
14907
15005
|
if (nginx.exitCode !== 0)
|
|
14908
15006
|
problems.push("nginx configuration is invalid");
|
|
14909
15007
|
if (state.databaseRole !== "none") {
|
|
15008
|
+
for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
|
|
15009
|
+
services[credential] = host.exists(credential);
|
|
15010
|
+
if (!services[credential])
|
|
15011
|
+
problems.push(`${credential} is missing`);
|
|
15012
|
+
}
|
|
14910
15013
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
14911
15014
|
const expectsNoAgency = state.databaseAgency === "none";
|
|
14912
15015
|
const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
|
|
@@ -15045,7 +15148,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15045
15148
|
enrolmentToken: checked4.enrolmentToken,
|
|
15046
15149
|
backup: checked4.backupS3Secret,
|
|
15047
15150
|
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
15048
|
-
cloudflareApiToken: checked4.cloudflareApiToken
|
|
15151
|
+
cloudflareApiToken: checked4.cloudflareApiToken,
|
|
15152
|
+
githubClientSecret: checked4.githubClientSecret,
|
|
15153
|
+
githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
|
|
15154
|
+
githubWebhookSecret: checked4.githubWebhookSecret
|
|
15049
15155
|
};
|
|
15050
15156
|
})() : undefined;
|
|
15051
15157
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -15138,11 +15244,17 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15138
15244
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
15139
15245
|
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
15140
15246
|
}
|
|
15247
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
15248
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
15249
|
+
}
|
|
15141
15250
|
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
15142
15251
|
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
15143
15252
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
15144
15253
|
for (const [name, source] of Object.entries({
|
|
15145
15254
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
15255
|
+
"fz_github.clientSecret": platformPrivate.githubClientSecret,
|
|
15256
|
+
"fz_github.privateKey": platformPrivate.githubPrivateKey,
|
|
15257
|
+
"fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
|
|
15146
15258
|
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
15147
15259
|
})) {
|
|
15148
15260
|
if (source) {
|
|
@@ -15159,6 +15271,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15159
15271
|
const credentials = platformApiCredentialSpecs({
|
|
15160
15272
|
emailProvider: runtime.environment.email?.provider,
|
|
15161
15273
|
cloudflareKv: cloudflareConfigured,
|
|
15274
|
+
githubApp: Boolean(runtime.environment.githubApp),
|
|
15162
15275
|
realtime: Boolean(runtime.environment.realtime)
|
|
15163
15276
|
});
|
|
15164
15277
|
const units = renderPlatformApiUnits({
|
|
@@ -15417,7 +15530,11 @@ function strictBootstrapDocument(value) {
|
|
|
15417
15530
|
], "runtime environment");
|
|
15418
15531
|
const environment = runtime.environment;
|
|
15419
15532
|
if (environment.initialInventory !== undefined) {
|
|
15420
|
-
const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "region", "computes", "attestation", "deployment"], "initial inventory");
|
|
15533
|
+
const inventory = exactKeys2(environment.initialInventory, ["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"], "initial inventory");
|
|
15534
|
+
if (inventory.metalIdentity !== undefined) {
|
|
15535
|
+
const identity = exactKeys2(inventory.metalIdentity, ["nodeKey", "publicKeys"], "initial Metal identity");
|
|
15536
|
+
exactKeys2(identity.publicKeys, ["ed25519", "mlDsa"], "initial Metal public keys");
|
|
15537
|
+
}
|
|
15421
15538
|
exactKeys2(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
|
|
15422
15539
|
if (inventory.attestation !== undefined) {
|
|
15423
15540
|
const attestation = exactKeys2(inventory.attestation, ["measurement", "tcbFloor"], "initial attestation evidence");
|
|
@@ -16156,7 +16273,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
|
|
|
16156
16273
|
|
|
16157
16274
|
class MetalBootstrapError extends Error {
|
|
16158
16275
|
}
|
|
16159
|
-
function
|
|
16276
|
+
function parseMetalPublicIdentity(value) {
|
|
16160
16277
|
let parsed;
|
|
16161
16278
|
try {
|
|
16162
16279
|
parsed = JSON.parse(value);
|
|
@@ -16171,7 +16288,13 @@ function normalizeMetalPublicIdentity(value) {
|
|
|
16171
16288
|
if (Object.keys(identity).some((key) => !["nodeKey", "publicKeys"].includes(key)) || typeof identity.nodeKey !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(identity.nodeKey) || !keys || Object.keys(keys).some((key) => !["ed25519", "mlDsa"].includes(key)) || keys.ed25519 !== identity.nodeKey || typeof keys.mlDsa !== "string" || !/^[A-Za-z0-9_-]{1,8192}$/.test(keys.mlDsa)) {
|
|
16172
16289
|
throw new MetalBootstrapError("metal public identity proof was invalid");
|
|
16173
16290
|
}
|
|
16174
|
-
return
|
|
16291
|
+
return {
|
|
16292
|
+
nodeKey: identity.nodeKey,
|
|
16293
|
+
publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
|
|
16294
|
+
};
|
|
16295
|
+
}
|
|
16296
|
+
function normalizeMetalPublicIdentity(value) {
|
|
16297
|
+
return JSON.stringify(parseMetalPublicIdentity(value));
|
|
16175
16298
|
}
|
|
16176
16299
|
var defaultExec2 = async (argv2, stdin) => {
|
|
16177
16300
|
const child = Bun.spawn([...argv2], {
|
|
@@ -17061,7 +17184,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
|
|
|
17061
17184
|
return writeOwnerJson2(path, request, "operator fleet request");
|
|
17062
17185
|
}
|
|
17063
17186
|
var validateMetalRequestValue = (value, options = {}) => {
|
|
17064
|
-
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
17187
|
+
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
|
|
17065
17188
|
if (root.kind !== "metal-remote")
|
|
17066
17189
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
17067
17190
|
const targetValue = exactKeys5(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
|
|
@@ -17080,6 +17203,12 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17080
17203
|
if (typeof root.metalConfigFile !== "string")
|
|
17081
17204
|
throw new Error("metalConfigFile must be an absolute path");
|
|
17082
17205
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
17206
|
+
if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
|
|
17207
|
+
throw new Error("metalIdentityEvidenceFile must be an absolute path");
|
|
17208
|
+
}
|
|
17209
|
+
if (typeof root.metalIdentityEvidenceFile === "string") {
|
|
17210
|
+
canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
|
|
17211
|
+
}
|
|
17083
17212
|
if (options.validateMetalConfig !== false)
|
|
17084
17213
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
17085
17214
|
const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
|
|
@@ -17128,6 +17257,7 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17128
17257
|
return {
|
|
17129
17258
|
kind: "metal-remote",
|
|
17130
17259
|
metalConfigFile: root.metalConfigFile,
|
|
17260
|
+
...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
|
|
17131
17261
|
target: {
|
|
17132
17262
|
...target,
|
|
17133
17263
|
identityPublicKeyFile: targetValue.identityPublicKeyFile,
|
|
@@ -17148,6 +17278,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
17148
17278
|
validateMetalRequestValue(request);
|
|
17149
17279
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
17150
17280
|
}
|
|
17281
|
+
function readOperatorMetalIdentityEvidence(path, request) {
|
|
17282
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
|
|
17283
|
+
const root = exactKeys5(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
|
|
17284
|
+
if (root.format !== 1 || root.kind !== "forgezero-operator-metal-identity" || typeof root.metalHostname !== "string" || typeof root.metalHostKeySha256 !== "string" || typeof root.nodeKey !== "string" || !root.publicKeys || typeof root.publicKeys !== "object") {
|
|
17285
|
+
throw new Error("operator metal identity evidence is malformed");
|
|
17286
|
+
}
|
|
17287
|
+
const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
|
|
17288
|
+
const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
|
|
17289
|
+
if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
|
|
17290
|
+
throw new Error("operator metal identity evidence does not match the reviewed Metal target");
|
|
17291
|
+
}
|
|
17292
|
+
return {
|
|
17293
|
+
format: 1,
|
|
17294
|
+
kind: "forgezero-operator-metal-identity",
|
|
17295
|
+
metalHostname: root.metalHostname,
|
|
17296
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
17297
|
+
...identity
|
|
17298
|
+
};
|
|
17299
|
+
}
|
|
17151
17300
|
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
17152
17301
|
const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
17153
17302
|
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
@@ -17833,6 +17982,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
17833
17982
|
`${REMOTE_STAGE}/metal-config.json`,
|
|
17834
17983
|
"--apply"
|
|
17835
17984
|
], "remote typed metal bootstrap");
|
|
17985
|
+
if (request.metalIdentityEvidenceFile) {
|
|
17986
|
+
let applied;
|
|
17987
|
+
try {
|
|
17988
|
+
applied = JSON.parse(output);
|
|
17989
|
+
} catch {
|
|
17990
|
+
throw new Error("remote Metal bootstrap did not return identity evidence");
|
|
17991
|
+
}
|
|
17992
|
+
const publicIdentity2 = applied?.publicIdentity;
|
|
17993
|
+
if (typeof publicIdentity2 !== "string")
|
|
17994
|
+
throw new Error("remote Metal bootstrap omitted its public identity");
|
|
17995
|
+
const identity = parseMetalPublicIdentity(publicIdentity2);
|
|
17996
|
+
writeOwnerJson2(request.metalIdentityEvidenceFile, {
|
|
17997
|
+
format: 1,
|
|
17998
|
+
kind: "forgezero-operator-metal-identity",
|
|
17999
|
+
metalHostname: config.metalHostname,
|
|
18000
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
18001
|
+
...identity
|
|
18002
|
+
}, "operator metal identity evidence");
|
|
18003
|
+
}
|
|
17836
18004
|
return { plan, output };
|
|
17837
18005
|
} finally {
|
|
17838
18006
|
if (knownHosts) {
|
|
@@ -17886,7 +18054,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
|
|
|
17886
18054
|
}
|
|
17887
18055
|
|
|
17888
18056
|
// src/platform-genesis-config.ts
|
|
17889
|
-
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation) {
|
|
18057
|
+
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation, metalIdentity) {
|
|
17890
18058
|
const fleet = validatePlatformGenesisGuests(guests);
|
|
17891
18059
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
|
|
17892
18060
|
throw new Error("platform genesis requires the reviewed metal hostname");
|
|
@@ -17902,6 +18070,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
|
|
|
17902
18070
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
17903
18071
|
const initialInventory = {
|
|
17904
18072
|
metalHostname,
|
|
18073
|
+
...metalIdentity ? { metalIdentity: structuredClone(metalIdentity) } : {},
|
|
17905
18074
|
region: {
|
|
17906
18075
|
key: template.runtime.environment.nodeRegion,
|
|
17907
18076
|
label: region.label,
|
|
@@ -18065,6 +18234,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
|
|
|
18065
18234
|
otlpTraceSampleRatio: 0.1,
|
|
18066
18235
|
custodianEmail: owner.custodianEmail,
|
|
18067
18236
|
email: owner.email,
|
|
18237
|
+
...owner.githubApp ? { githubApp: owner.githubApp } : {},
|
|
18068
18238
|
deployProfile: profile.environment
|
|
18069
18239
|
},
|
|
18070
18240
|
serviceUser: "forgezero-api",
|
|
@@ -18421,6 +18591,92 @@ function planAppBuild(input) {
|
|
|
18421
18591
|
};
|
|
18422
18592
|
}
|
|
18423
18593
|
|
|
18594
|
+
// src/platform-launch-env.ts
|
|
18595
|
+
import { lstatSync as lstatSync10, readFileSync as readFileSync16 } from "fs";
|
|
18596
|
+
import { resolve as resolve12 } from "path";
|
|
18597
|
+
var booleanValue = (values, name, fallback) => {
|
|
18598
|
+
const raw = values.get(name)?.trim().toLowerCase();
|
|
18599
|
+
if (raw === undefined || raw === "")
|
|
18600
|
+
return fallback;
|
|
18601
|
+
if (raw === "true")
|
|
18602
|
+
return true;
|
|
18603
|
+
if (raw === "false")
|
|
18604
|
+
return false;
|
|
18605
|
+
throw new Error(`${name} must be true or false`);
|
|
18606
|
+
};
|
|
18607
|
+
function readPlatformLaunchEnvironment(path) {
|
|
18608
|
+
const absolute2 = resolve12(path);
|
|
18609
|
+
const stat2 = lstatSync10(absolute2);
|
|
18610
|
+
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
|
|
18611
|
+
throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
|
|
18612
|
+
}
|
|
18613
|
+
const values = new Map;
|
|
18614
|
+
for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
|
|
18615
|
+
const line = raw.trim();
|
|
18616
|
+
if (!line || line.startsWith("#"))
|
|
18617
|
+
continue;
|
|
18618
|
+
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
18619
|
+
if (!match)
|
|
18620
|
+
throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
|
|
18621
|
+
if (values.has(match[1]))
|
|
18622
|
+
throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
|
|
18623
|
+
let value = match[2].trim();
|
|
18624
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
18625
|
+
value = value.slice(1, -1);
|
|
18626
|
+
}
|
|
18627
|
+
values.set(match[1], value);
|
|
18628
|
+
}
|
|
18629
|
+
const requiredValue = (name) => {
|
|
18630
|
+
const value = values.get(name)?.trim();
|
|
18631
|
+
if (!value)
|
|
18632
|
+
throw new Error(`bootstrap secret environment is missing ${name}`);
|
|
18633
|
+
return value;
|
|
18634
|
+
};
|
|
18635
|
+
const provider = requiredValue("FZ_EMAIL_PROVIDER");
|
|
18636
|
+
if (provider !== "smtp" && provider !== "jetemail")
|
|
18637
|
+
throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
|
|
18638
|
+
const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
|
|
18639
|
+
if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
|
|
18640
|
+
throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
|
|
18641
|
+
}
|
|
18642
|
+
const githubEnabled = booleanValue(values, "FZ_GITHUB_ENABLED", false);
|
|
18643
|
+
const githubApp = githubEnabled ? {
|
|
18644
|
+
clientId: requiredValue("FZ_GITHUB_CLIENT_ID"),
|
|
18645
|
+
appId: requiredValue("FZ_GITHUB_APP_ID"),
|
|
18646
|
+
slug: requiredValue("FZ_GITHUB_APP_SLUG")
|
|
18647
|
+
} : undefined;
|
|
18648
|
+
const owner = provider === "jetemail" ? {
|
|
18649
|
+
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
18650
|
+
email: {
|
|
18651
|
+
provider,
|
|
18652
|
+
from: requiredValue("FZ_EMAIL_FROM"),
|
|
18653
|
+
eu: booleanValue(values, "FZ_JETEMAIL_EU", false)
|
|
18654
|
+
},
|
|
18655
|
+
...githubApp ? { githubApp } : {}
|
|
18656
|
+
} : {
|
|
18657
|
+
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
18658
|
+
email: {
|
|
18659
|
+
provider,
|
|
18660
|
+
from: requiredValue("FZ_EMAIL_FROM"),
|
|
18661
|
+
host: requiredValue("FZ_SMTP_HOST"),
|
|
18662
|
+
port: smtpPort,
|
|
18663
|
+
user: requiredValue("FZ_SMTP_USER")
|
|
18664
|
+
},
|
|
18665
|
+
...githubApp ? { githubApp } : {}
|
|
18666
|
+
};
|
|
18667
|
+
return {
|
|
18668
|
+
owner,
|
|
18669
|
+
emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
|
|
18670
|
+
cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
|
|
18671
|
+
cloudflareApiToken: requiredValue("CF_API_TOKEN"),
|
|
18672
|
+
...githubEnabled ? {
|
|
18673
|
+
githubClientSecret: requiredValue("FZ_GITHUB_CLIENT_SECRET"),
|
|
18674
|
+
githubPrivateKeyBase64: requiredValue("FZ_GITHUB_PRIVATE_KEY_BASE64"),
|
|
18675
|
+
githubWebhookSecret: requiredValue("FZ_GITHUB_WEBHOOK_SECRET")
|
|
18676
|
+
} : {}
|
|
18677
|
+
};
|
|
18678
|
+
}
|
|
18679
|
+
|
|
18424
18680
|
// src/cli/index.ts
|
|
18425
18681
|
var DEFAULT_MODE = (THRESHOLD_MODES.find((mode) => mode.threshold === 1 && mode.total === 1) ?? THRESHOLD_MODES[0]).id;
|
|
18426
18682
|
var PACKAGED_AGENT_BIN2 = operatorPackagedAgentPath();
|
|
@@ -18733,7 +18989,7 @@ async function api(options, path, init) {
|
|
|
18733
18989
|
return { status: response.status, body };
|
|
18734
18990
|
}
|
|
18735
18991
|
function sleep(ms) {
|
|
18736
|
-
return new Promise((
|
|
18992
|
+
return new Promise((resolve14) => setTimeout(resolve14, ms));
|
|
18737
18993
|
}
|
|
18738
18994
|
function browserCommand(url) {
|
|
18739
18995
|
if (process.platform === "darwin")
|
|
@@ -18818,7 +19074,7 @@ function requireSession() {
|
|
|
18818
19074
|
function readOwnerOnlySecret(path, label) {
|
|
18819
19075
|
let metadata;
|
|
18820
19076
|
try {
|
|
18821
|
-
metadata =
|
|
19077
|
+
metadata = lstatSync11(path);
|
|
18822
19078
|
} catch {
|
|
18823
19079
|
throw new Error(`The ${label} file ${path} is unreadable.`);
|
|
18824
19080
|
}
|
|
@@ -18832,7 +19088,7 @@ function readOwnerOnlySecret(path, label) {
|
|
|
18832
19088
|
if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
18833
19089
|
throw new Error(`The ${label} file ${path} is not owned by the current user.`);
|
|
18834
19090
|
}
|
|
18835
|
-
const value =
|
|
19091
|
+
const value = readFileSync17(path, "utf8").trim();
|
|
18836
19092
|
if (!value)
|
|
18837
19093
|
throw new Error(`The ${label} file ${path} is empty.`);
|
|
18838
19094
|
return value;
|
|
@@ -19013,7 +19269,7 @@ async function cmdApi(options, args) {
|
|
|
19013
19269
|
let body = undefined;
|
|
19014
19270
|
if (options.data !== undefined && options.dataFile)
|
|
19015
19271
|
throw new Error("Use only one of --data or --data-file.");
|
|
19016
|
-
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ?
|
|
19272
|
+
const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync17(options.dataFile, "utf8") : options.data;
|
|
19017
19273
|
if (encoded !== undefined)
|
|
19018
19274
|
body = JSON.parse(encoded);
|
|
19019
19275
|
const result = await api(options, `${url.pathname}${url.search}`, { method, body });
|
|
@@ -19192,7 +19448,7 @@ async function cmdAgent(options, args) {
|
|
|
19192
19448
|
out.line();
|
|
19193
19449
|
out.line(" Compatibility SSH deploy public key:");
|
|
19194
19450
|
out.line();
|
|
19195
|
-
out.line(` ${
|
|
19451
|
+
out.line(` ${readFileSync17(gitIdentity.gitPublicKeyPath, "utf8").trim()}`);
|
|
19196
19452
|
out.line();
|
|
19197
19453
|
}
|
|
19198
19454
|
return 0;
|
|
@@ -19232,65 +19488,6 @@ async function bootstrapSecret(question) {
|
|
|
19232
19488
|
throw new Error(`${question} was not provided`);
|
|
19233
19489
|
return value;
|
|
19234
19490
|
}
|
|
19235
|
-
function readPlatformLaunchEnvironment(path) {
|
|
19236
|
-
const absolute2 = resolve12(path);
|
|
19237
|
-
const stat2 = lstatSync10(absolute2);
|
|
19238
|
-
if (!stat2.isFile() || stat2.isSymbolicLink() || stat2.nlink !== 1 || typeof process.getuid === "function" && stat2.uid !== process.getuid() || (stat2.mode & 63) !== 0) {
|
|
19239
|
-
throw new Error(`bootstrap secret environment must be one owner-only regular file: ${absolute2}`);
|
|
19240
|
-
}
|
|
19241
|
-
const values = new Map;
|
|
19242
|
-
for (const [index, raw] of readFileSync16(absolute2, "utf8").split(/\r?\n/).entries()) {
|
|
19243
|
-
const line = raw.trim();
|
|
19244
|
-
if (!line || line.startsWith("#"))
|
|
19245
|
-
continue;
|
|
19246
|
-
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
19247
|
-
if (!match)
|
|
19248
|
-
throw new Error(`invalid environment assignment at ${absolute2}:${index + 1}`);
|
|
19249
|
-
if (values.has(match[1]))
|
|
19250
|
-
throw new Error(`duplicate environment assignment ${match[1]} at ${absolute2}:${index + 1}`);
|
|
19251
|
-
let value = match[2].trim();
|
|
19252
|
-
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
19253
|
-
value = value.slice(1, -1);
|
|
19254
|
-
}
|
|
19255
|
-
values.set(match[1], value);
|
|
19256
|
-
}
|
|
19257
|
-
const requiredValue = (name) => {
|
|
19258
|
-
const value = values.get(name)?.trim();
|
|
19259
|
-
if (!value)
|
|
19260
|
-
throw new Error(`bootstrap secret environment is missing ${name}`);
|
|
19261
|
-
return value;
|
|
19262
|
-
};
|
|
19263
|
-
const provider = requiredValue("FZ_EMAIL_PROVIDER");
|
|
19264
|
-
if (provider !== "smtp" && provider !== "jetemail")
|
|
19265
|
-
throw new Error("FZ_EMAIL_PROVIDER must be smtp or jetemail");
|
|
19266
|
-
const smtpPort = provider === "smtp" ? Number(requiredValue("FZ_SMTP_PORT")) : undefined;
|
|
19267
|
-
if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
|
|
19268
|
-
throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
|
|
19269
|
-
}
|
|
19270
|
-
const owner = provider === "jetemail" ? {
|
|
19271
|
-
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
19272
|
-
email: {
|
|
19273
|
-
provider,
|
|
19274
|
-
from: requiredValue("FZ_EMAIL_FROM"),
|
|
19275
|
-
eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
|
|
19276
|
-
}
|
|
19277
|
-
} : {
|
|
19278
|
-
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
19279
|
-
email: {
|
|
19280
|
-
provider,
|
|
19281
|
-
from: requiredValue("FZ_EMAIL_FROM"),
|
|
19282
|
-
host: requiredValue("FZ_SMTP_HOST"),
|
|
19283
|
-
port: smtpPort,
|
|
19284
|
-
user: requiredValue("FZ_SMTP_USER")
|
|
19285
|
-
}
|
|
19286
|
-
};
|
|
19287
|
-
return {
|
|
19288
|
-
owner,
|
|
19289
|
-
emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
|
|
19290
|
-
cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
|
|
19291
|
-
cloudflareApiToken: requiredValue("CF_API_TOKEN")
|
|
19292
|
-
};
|
|
19293
|
-
}
|
|
19294
19491
|
async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
|
|
19295
19492
|
return validatePlatformBootstrapSecrets(config, {
|
|
19296
19493
|
clusterBootstrapCode: generatedClusterBootstrapCode ?? await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
@@ -19300,6 +19497,11 @@ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapC
|
|
|
19300
19497
|
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
|
|
19301
19498
|
cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
19302
19499
|
cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
19500
|
+
} : {},
|
|
19501
|
+
...config.runtime.environment.githubApp ? {
|
|
19502
|
+
githubClientSecret: await bootstrapSecret("GitHub App client secret"),
|
|
19503
|
+
githubPrivateKeyBase64: await bootstrapSecret("GitHub App RSA private key (base64)"),
|
|
19504
|
+
githubWebhookSecret: await bootstrapSecret("GitHub App webhook secret")
|
|
19303
19505
|
} : {}
|
|
19304
19506
|
});
|
|
19305
19507
|
}
|
|
@@ -19361,7 +19563,7 @@ function interactiveMetalBootstrap() {
|
|
|
19361
19563
|
});
|
|
19362
19564
|
}
|
|
19363
19565
|
function writeBootstrapConfig(path, config) {
|
|
19364
|
-
if (!isAbsolute7(path) ||
|
|
19566
|
+
if (!isAbsolute7(path) || resolve13(path) !== path)
|
|
19365
19567
|
throw new Error("--output must be a canonical absolute path");
|
|
19366
19568
|
mkdirSync13(dirname14(path), { recursive: true, mode: 448 });
|
|
19367
19569
|
writeFileSync13(path, `${JSON.stringify(config, null, 2)}
|
|
@@ -19425,11 +19627,11 @@ async function interactiveCloudflareBootstrap() {
|
|
|
19425
19627
|
});
|
|
19426
19628
|
}
|
|
19427
19629
|
function genesisOutputDirectory(path) {
|
|
19428
|
-
if (!isAbsolute7(path) ||
|
|
19630
|
+
if (!isAbsolute7(path) || resolve13(path) !== path)
|
|
19429
19631
|
throw new Error("--output must be a canonical absolute directory");
|
|
19430
19632
|
if (!existsSync13(path))
|
|
19431
19633
|
mkdirSync13(path, { recursive: true, mode: 448 });
|
|
19432
|
-
const metadata =
|
|
19634
|
+
const metadata = lstatSync11(path);
|
|
19433
19635
|
const uid = process.getuid?.();
|
|
19434
19636
|
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 63) !== 0 || uid !== undefined && uid !== 0 && metadata.uid !== uid) {
|
|
19435
19637
|
throw new Error("platform genesis output must be an owner-only directory owned by the current user");
|
|
@@ -19481,6 +19683,7 @@ function writeMetalOperatorFiles(directory) {
|
|
|
19481
19683
|
const remoteRequest = writeOperatorMetalBootstrapRequest(join13(output, "metal-remote.json"), {
|
|
19482
19684
|
kind: "metal-remote",
|
|
19483
19685
|
metalConfigFile: metalConfig,
|
|
19686
|
+
metalIdentityEvidenceFile: join13(output, "metal-identity.json"),
|
|
19484
19687
|
target: { ...target, ...identity },
|
|
19485
19688
|
genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
|
|
19486
19689
|
});
|
|
@@ -19501,6 +19704,10 @@ function writePlatformGenesisFleet(directory, preset) {
|
|
|
19501
19704
|
const output = genesisOutputDirectory(directory);
|
|
19502
19705
|
const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
19503
19706
|
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
|
|
19707
|
+
if (!metalRequest.metalIdentityEvidenceFile) {
|
|
19708
|
+
throw new Error("the reviewed Metal request predates automatic identity seeding; rerun Metal apply with a generated request");
|
|
19709
|
+
}
|
|
19710
|
+
const metalIdentity = readOperatorMetalIdentityEvidence(metalRequest.metalIdentityEvidenceFile, metalRequest);
|
|
19504
19711
|
const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
19505
19712
|
const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
19506
19713
|
const fleet = metalRequest.genesis.nodes;
|
|
@@ -19545,7 +19752,7 @@ function writePlatformGenesisFleet(directory, preset) {
|
|
|
19545
19752
|
branch: preset.profile.branch,
|
|
19546
19753
|
revision: preset.bundle.manifest.revision,
|
|
19547
19754
|
bundleSha256: preset.bundle.manifest.sha256
|
|
19548
|
-
} : undefined, preset?.profile.attestation).map((config) => {
|
|
19755
|
+
} : undefined, preset?.profile.attestation, { nodeKey: metalIdentity.nodeKey, publicKeys: { ...metalIdentity.publicKeys } }).map((config) => {
|
|
19549
19756
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
19550
19757
|
if (preset)
|
|
19551
19758
|
writeDerivedLaunchFile(path, (temporary) => writeBootstrapConfig(temporary, config));
|
|
@@ -19599,10 +19806,16 @@ function forgeZeroLaunchOwnerInput() {
|
|
|
19599
19806
|
throw new Error("Bootstrap email provider must be smtp or jetemail");
|
|
19600
19807
|
}
|
|
19601
19808
|
const from = bootstrapAnswer("Bootstrap email From address");
|
|
19809
|
+
const githubApp = bootstrapBoolean("Configure the platform GitHub App now?", "no") ? {
|
|
19810
|
+
clientId: bootstrapAnswer("GitHub App client ID"),
|
|
19811
|
+
appId: bootstrapAnswer("GitHub App ID"),
|
|
19812
|
+
slug: bootstrapAnswer("GitHub App slug")
|
|
19813
|
+
} : undefined;
|
|
19602
19814
|
if (provider === "jetemail") {
|
|
19603
19815
|
return {
|
|
19604
19816
|
custodianEmail,
|
|
19605
|
-
email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
|
|
19817
|
+
email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") },
|
|
19818
|
+
...githubApp ? { githubApp } : {}
|
|
19606
19819
|
};
|
|
19607
19820
|
}
|
|
19608
19821
|
return {
|
|
@@ -19613,7 +19826,8 @@ function forgeZeroLaunchOwnerInput() {
|
|
|
19613
19826
|
host: bootstrapAnswer("SMTP host"),
|
|
19614
19827
|
port: bootstrapNumber("SMTP port", "587"),
|
|
19615
19828
|
user: bootstrapAnswer("SMTP user")
|
|
19616
|
-
}
|
|
19829
|
+
},
|
|
19830
|
+
...githubApp ? { githubApp } : {}
|
|
19617
19831
|
};
|
|
19618
19832
|
}
|
|
19619
19833
|
async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
|
|
@@ -19628,14 +19842,11 @@ async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, proj
|
|
|
19628
19842
|
if (!existsSync13(path))
|
|
19629
19843
|
throw new Error(`${label} is missing at ${path}`);
|
|
19630
19844
|
}
|
|
19631
|
-
const root =
|
|
19845
|
+
const root = resolve13(projectRoot);
|
|
19632
19846
|
const nestedApi = join13(root, "api");
|
|
19633
19847
|
const repositoryRoot = existsSync13(join13(nestedApi, "package.json")) ? nestedApi : root;
|
|
19634
19848
|
const bundlePath = join13(base, "api.bundle");
|
|
19635
|
-
const bundle =
|
|
19636
|
-
if (bundle.manifest.branch !== profile.branch) {
|
|
19637
|
-
throw new Error(`existing bootstrap bundle is for ${bundle.manifest.branch}, expected ${profile.branch}`);
|
|
19638
|
-
}
|
|
19849
|
+
const bundle = await refreshBootstrapBundle({ repositoryRoot, outputPath: bundlePath, branch: profile.branch });
|
|
19639
19850
|
return writePlatformGenesisFleet(outputDirectory, {
|
|
19640
19851
|
profile,
|
|
19641
19852
|
metalRequestFile,
|
|
@@ -19798,7 +20009,12 @@ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapC
|
|
|
19798
20009
|
clusterBootstrapCode: generatedClusterBootstrapCode,
|
|
19799
20010
|
emailSecret: environmentInput.emailSecret,
|
|
19800
20011
|
cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
|
|
19801
|
-
cloudflareApiToken: environmentInput.cloudflareApiToken
|
|
20012
|
+
cloudflareApiToken: environmentInput.cloudflareApiToken,
|
|
20013
|
+
...environmentInput.githubClientSecret ? {
|
|
20014
|
+
githubClientSecret: environmentInput.githubClientSecret,
|
|
20015
|
+
githubPrivateKeyBase64: environmentInput.githubPrivateKeyBase64,
|
|
20016
|
+
githubWebhookSecret: environmentInput.githubWebhookSecret
|
|
20017
|
+
} : {}
|
|
19802
20018
|
}) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
|
|
19803
20019
|
const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
|
|
19804
20020
|
tunnelToken: secrets.cloudflareTunnelToken,
|
|
@@ -20112,7 +20328,7 @@ async function cmdUnlock(options) {
|
|
|
20112
20328
|
if (options.key || options.userExplicit) {
|
|
20113
20329
|
throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
|
|
20114
20330
|
}
|
|
20115
|
-
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ?
|
|
20331
|
+
const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync17(0, "utf8").trim() : "";
|
|
20116
20332
|
const phrase = phraseText ? phraseText.split(/\s+/) : [];
|
|
20117
20333
|
if (phrase.length !== 24) {
|
|
20118
20334
|
throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
|
|
@@ -20345,7 +20561,7 @@ function projectFromCheckout(options) {
|
|
|
20345
20561
|
try {
|
|
20346
20562
|
return loadConfig({
|
|
20347
20563
|
cwd: options.projectRoot,
|
|
20348
|
-
readFile: (path) => existsSync13(path) ?
|
|
20564
|
+
readFile: (path) => existsSync13(path) ? readFileSync17(path, "utf8") : undefined
|
|
20349
20565
|
}).config.project;
|
|
20350
20566
|
} catch (cause) {
|
|
20351
20567
|
throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
@@ -20412,7 +20628,7 @@ async function cmdDeploy(options, args) {
|
|
|
20412
20628
|
throw new Error("typed runtime initialization selects its tested software and attestation from --runtime/--isolation; do not combine --software or --attestation");
|
|
20413
20629
|
}
|
|
20414
20630
|
initializeTypeScriptDeployment(options.projectRoot, {
|
|
20415
|
-
name: options.projectName ?? basename3(
|
|
20631
|
+
name: options.projectName ?? basename3(resolve13(options.projectRoot)),
|
|
20416
20632
|
force: options.force,
|
|
20417
20633
|
runtime: options.deployRuntime,
|
|
20418
20634
|
operatingSystem: options.deployOperatingSystem,
|
|
@@ -20467,7 +20683,7 @@ async function cmdDeploy(options, args) {
|
|
|
20467
20683
|
return problems.length === 0 ? 0 : 1;
|
|
20468
20684
|
}
|
|
20469
20685
|
if (operation === "check" || operation === "sync") {
|
|
20470
|
-
if (existsSync13(
|
|
20686
|
+
if (existsSync13(resolve13(options.projectRoot, DEPLOY_SOURCE_FILE))) {
|
|
20471
20687
|
const expected = await compileDeploymentProject(options.projectRoot, { write: false });
|
|
20472
20688
|
const actual = inspectCompiledDeployment(options.projectRoot);
|
|
20473
20689
|
const current2 = canonicalJson(expected.plan) === canonicalJson(actual.plan);
|
|
@@ -20543,7 +20759,7 @@ async function cmdDeploy(options, args) {
|
|
|
20543
20759
|
branch: options.branch,
|
|
20544
20760
|
cloneUrl: required(options.cloneUrl, "--clone-url"),
|
|
20545
20761
|
sourceAuth,
|
|
20546
|
-
...options.knownHostsFile ? { knownHosts:
|
|
20762
|
+
...options.knownHostsFile ? { knownHosts: readFileSync17(options.knownHostsFile, "utf8") } : {},
|
|
20547
20763
|
projectKey
|
|
20548
20764
|
} });
|
|
20549
20765
|
const pipelineKey2 = String(created.pipelineKey);
|