@forgezero/agent 0.1.102 → 0.1.103
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.d.ts +4 -0
- package/dist/bootstrap.js +67 -6
- package/dist/fz-agent.js +1 -1
- package/dist/fz.js +166 -17
- 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 +122 -9
- package/dist/platform-bootstrap-runtime.d.ts +21 -1
- package/dist/platform-bootstrap-runtime.js +19 -1
- package/dist/platform-fleet-verification.js +21 -3
- package/dist/platform-genesis-config.d.ts +1 -1
- package/dist/platform-launch-profile.d.ts +5 -0
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/agent-heartbeat.js
CHANGED
package/dist/bootstrap.d.ts
CHANGED
|
@@ -115,6 +115,10 @@ export interface PlatformBootstrapSecrets {
|
|
|
115
115
|
backupS3Secret?: string;
|
|
116
116
|
cloudflareTunnelToken?: string;
|
|
117
117
|
cloudflareApiToken?: string;
|
|
118
|
+
githubClientSecret?: string;
|
|
119
|
+
/** One-line base64 form accepted by prompts/env; decoded before systemd sealing. */
|
|
120
|
+
githubPrivateKeyBase64?: string;
|
|
121
|
+
githubWebhookSecret?: string;
|
|
118
122
|
}
|
|
119
123
|
export interface EnrolledComputeBootstrapSecrets {
|
|
120
124
|
enrolmentToken: string;
|
package/dist/bootstrap.js
CHANGED
|
@@ -1386,7 +1386,7 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
1388
|
// src/bootstrap.ts
|
|
1389
|
-
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
1389
|
+
import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
|
|
1390
1390
|
import {
|
|
1391
1391
|
chmodSync as chmodSync3,
|
|
1392
1392
|
existsSync as existsSync5,
|
|
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1420
1420
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1421
1421
|
|
|
1422
1422
|
// src/version.ts
|
|
1423
|
-
var VERSION = "0.1.
|
|
1423
|
+
var VERSION = "0.1.103";
|
|
1424
1424
|
|
|
1425
1425
|
// src/software.ts
|
|
1426
1426
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3090,10 +3090,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
3090
3090
|
};
|
|
3091
3091
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
3092
3092
|
function validatePlatformInitialInventory(value) {
|
|
3093
|
-
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)) {
|
|
3093
|
+
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)) {
|
|
3094
3094
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
3095
3095
|
}
|
|
3096
3096
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
3097
|
+
if (value.metalIdentity !== undefined) {
|
|
3098
|
+
const identity = value.metalIdentity;
|
|
3099
|
+
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))) {
|
|
3100
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3097
3103
|
if (value.region.city !== undefined)
|
|
3098
3104
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
3099
3105
|
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)))
|
|
@@ -3128,6 +3134,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
3128
3134
|
}
|
|
3129
3135
|
return {
|
|
3130
3136
|
metalHostname: value.metalHostname,
|
|
3137
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
3131
3138
|
region: { ...value.region },
|
|
3132
3139
|
computes: value.computes.map((compute, index) => ({
|
|
3133
3140
|
...computes[index],
|
|
@@ -3181,6 +3188,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3181
3188
|
} else if (input.email !== undefined) {
|
|
3182
3189
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3183
3190
|
}
|
|
3191
|
+
if (input.githubApp) {
|
|
3192
|
+
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)) {
|
|
3193
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3184
3196
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3185
3197
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3186
3198
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -3311,6 +3323,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3311
3323
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
3312
3324
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
3313
3325
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
3326
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
3327
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
3328
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
3314
3329
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
3315
3330
|
};
|
|
3316
3331
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -3322,6 +3337,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
3322
3337
|
const optional = [
|
|
3323
3338
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
3324
3339
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
3340
|
+
["fz_github.clientSecret", options.githubApp],
|
|
3341
|
+
["fz_github.privateKey", options.githubApp],
|
|
3342
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
3325
3343
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
3326
3344
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
3327
3345
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -3717,7 +3735,17 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3717
3735
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3718
3736
|
throw new Error("bootstrap credential input must be an object");
|
|
3719
3737
|
const source = input;
|
|
3720
|
-
const allowed = [
|
|
3738
|
+
const allowed = [
|
|
3739
|
+
"clusterBootstrapCode",
|
|
3740
|
+
"emailSecret",
|
|
3741
|
+
"enrolmentToken",
|
|
3742
|
+
"backupS3Secret",
|
|
3743
|
+
"cloudflareTunnelToken",
|
|
3744
|
+
"cloudflareApiToken",
|
|
3745
|
+
"githubClientSecret",
|
|
3746
|
+
"githubPrivateKeyBase64",
|
|
3747
|
+
"githubWebhookSecret"
|
|
3748
|
+
];
|
|
3721
3749
|
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
3722
3750
|
if (unknown.length)
|
|
3723
3751
|
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
@@ -3727,6 +3755,9 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3727
3755
|
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
3728
3756
|
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
3729
3757
|
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
3758
|
+
const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
|
|
3759
|
+
const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
|
|
3760
|
+
const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
|
|
3730
3761
|
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
3731
3762
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
3732
3763
|
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
@@ -3746,12 +3777,29 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3746
3777
|
}
|
|
3747
3778
|
if (cloudflareTunnelToken)
|
|
3748
3779
|
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
3780
|
+
const githubConfigured = Boolean(config.runtime.environment.githubApp);
|
|
3781
|
+
if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
|
|
3782
|
+
throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
|
|
3783
|
+
}
|
|
3784
|
+
if (githubConfigured) {
|
|
3785
|
+
if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
|
|
3786
|
+
throw new Error("GitHub App client or webhook secret is malformed");
|
|
3787
|
+
}
|
|
3788
|
+
try {
|
|
3789
|
+
const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
|
|
3790
|
+
if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
|
|
3791
|
+
throw new Error("not RSA");
|
|
3792
|
+
} catch {
|
|
3793
|
+
throw new Error("GitHub App private key must be a base64-encoded RSA private key");
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3749
3796
|
return {
|
|
3750
3797
|
clusterBootstrapCode,
|
|
3751
3798
|
emailSecret,
|
|
3752
3799
|
...enrolmentToken ? { enrolmentToken } : {},
|
|
3753
3800
|
...backupS3Secret ? { backupS3Secret } : {},
|
|
3754
|
-
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
3801
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
|
|
3802
|
+
...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
|
|
3755
3803
|
};
|
|
3756
3804
|
}
|
|
3757
3805
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
@@ -4303,6 +4351,12 @@ function approvedPlatformRepairIdentityDigests(config) {
|
|
|
4303
4351
|
delete beforeAttestation.runtime.environment.initialInventory.attestation;
|
|
4304
4352
|
prior.push(beforeAttestation);
|
|
4305
4353
|
}
|
|
4354
|
+
const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
|
|
4355
|
+
if (metalIdentity && config.enrolment.source === "genesis-derived") {
|
|
4356
|
+
const beforeMetalIdentity = structuredClone(config);
|
|
4357
|
+
delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
|
|
4358
|
+
prior.push(beforeMetalIdentity);
|
|
4359
|
+
}
|
|
4306
4360
|
if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
|
|
4307
4361
|
const beforeReplicaCustodianEmail = structuredClone(config);
|
|
4308
4362
|
delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
|
|
@@ -4660,7 +4714,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4660
4714
|
enrolmentToken: checked4.enrolmentToken,
|
|
4661
4715
|
backup: checked4.backupS3Secret,
|
|
4662
4716
|
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4663
|
-
cloudflareApiToken: checked4.cloudflareApiToken
|
|
4717
|
+
cloudflareApiToken: checked4.cloudflareApiToken,
|
|
4718
|
+
githubClientSecret: checked4.githubClientSecret,
|
|
4719
|
+
githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
|
|
4720
|
+
githubWebhookSecret: checked4.githubWebhookSecret
|
|
4664
4721
|
};
|
|
4665
4722
|
})() : undefined;
|
|
4666
4723
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4758,6 +4815,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4758
4815
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4759
4816
|
for (const [name, source] of Object.entries({
|
|
4760
4817
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4818
|
+
"fz_github.clientSecret": platformPrivate.githubClientSecret,
|
|
4819
|
+
"fz_github.privateKey": platformPrivate.githubPrivateKey,
|
|
4820
|
+
"fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
|
|
4761
4821
|
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4762
4822
|
})) {
|
|
4763
4823
|
if (source) {
|
|
@@ -4774,6 +4834,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4774
4834
|
const credentials = platformApiCredentialSpecs({
|
|
4775
4835
|
emailProvider: runtime.environment.email?.provider,
|
|
4776
4836
|
cloudflareKv: cloudflareConfigured,
|
|
4837
|
+
githubApp: Boolean(runtime.environment.githubApp),
|
|
4777
4838
|
realtime: Boolean(runtime.environment.realtime)
|
|
4778
4839
|
});
|
|
4779
4840
|
const units = renderPlatformApiUnits({
|
package/dist/fz-agent.js
CHANGED
package/dist/fz.js
CHANGED
|
@@ -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.103";
|
|
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],
|
|
@@ -14102,7 +14120,17 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14102
14120
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
14103
14121
|
throw new Error("bootstrap credential input must be an object");
|
|
14104
14122
|
const source = input;
|
|
14105
|
-
const allowed = [
|
|
14123
|
+
const allowed = [
|
|
14124
|
+
"clusterBootstrapCode",
|
|
14125
|
+
"emailSecret",
|
|
14126
|
+
"enrolmentToken",
|
|
14127
|
+
"backupS3Secret",
|
|
14128
|
+
"cloudflareTunnelToken",
|
|
14129
|
+
"cloudflareApiToken",
|
|
14130
|
+
"githubClientSecret",
|
|
14131
|
+
"githubPrivateKeyBase64",
|
|
14132
|
+
"githubWebhookSecret"
|
|
14133
|
+
];
|
|
14106
14134
|
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
14107
14135
|
if (unknown.length)
|
|
14108
14136
|
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
@@ -14112,6 +14140,9 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14112
14140
|
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
14113
14141
|
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
14114
14142
|
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
14143
|
+
const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
|
|
14144
|
+
const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
|
|
14145
|
+
const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
|
|
14115
14146
|
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
14116
14147
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
14117
14148
|
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
@@ -14131,12 +14162,29 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
14131
14162
|
}
|
|
14132
14163
|
if (cloudflareTunnelToken)
|
|
14133
14164
|
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
14165
|
+
const githubConfigured = Boolean(config.runtime.environment.githubApp);
|
|
14166
|
+
if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
|
|
14167
|
+
throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
|
|
14168
|
+
}
|
|
14169
|
+
if (githubConfigured) {
|
|
14170
|
+
if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
|
|
14171
|
+
throw new Error("GitHub App client or webhook secret is malformed");
|
|
14172
|
+
}
|
|
14173
|
+
try {
|
|
14174
|
+
const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
|
|
14175
|
+
if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
|
|
14176
|
+
throw new Error("not RSA");
|
|
14177
|
+
} catch {
|
|
14178
|
+
throw new Error("GitHub App private key must be a base64-encoded RSA private key");
|
|
14179
|
+
}
|
|
14180
|
+
}
|
|
14134
14181
|
return {
|
|
14135
14182
|
clusterBootstrapCode,
|
|
14136
14183
|
emailSecret,
|
|
14137
14184
|
...enrolmentToken ? { enrolmentToken } : {},
|
|
14138
14185
|
...backupS3Secret ? { backupS3Secret } : {},
|
|
14139
|
-
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
14186
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
|
|
14187
|
+
...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
|
|
14140
14188
|
};
|
|
14141
14189
|
}
|
|
14142
14190
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
@@ -14688,6 +14736,12 @@ function approvedPlatformRepairIdentityDigests(config) {
|
|
|
14688
14736
|
delete beforeAttestation.runtime.environment.initialInventory.attestation;
|
|
14689
14737
|
prior.push(beforeAttestation);
|
|
14690
14738
|
}
|
|
14739
|
+
const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
|
|
14740
|
+
if (metalIdentity && config.enrolment.source === "genesis-derived") {
|
|
14741
|
+
const beforeMetalIdentity = structuredClone(config);
|
|
14742
|
+
delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
|
|
14743
|
+
prior.push(beforeMetalIdentity);
|
|
14744
|
+
}
|
|
14691
14745
|
if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
|
|
14692
14746
|
const beforeReplicaCustodianEmail = structuredClone(config);
|
|
14693
14747
|
delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
|
|
@@ -15045,7 +15099,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15045
15099
|
enrolmentToken: checked4.enrolmentToken,
|
|
15046
15100
|
backup: checked4.backupS3Secret,
|
|
15047
15101
|
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
15048
|
-
cloudflareApiToken: checked4.cloudflareApiToken
|
|
15102
|
+
cloudflareApiToken: checked4.cloudflareApiToken,
|
|
15103
|
+
githubClientSecret: checked4.githubClientSecret,
|
|
15104
|
+
githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
|
|
15105
|
+
githubWebhookSecret: checked4.githubWebhookSecret
|
|
15049
15106
|
};
|
|
15050
15107
|
})() : undefined;
|
|
15051
15108
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -15143,6 +15200,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15143
15200
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
15144
15201
|
for (const [name, source] of Object.entries({
|
|
15145
15202
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
15203
|
+
"fz_github.clientSecret": platformPrivate.githubClientSecret,
|
|
15204
|
+
"fz_github.privateKey": platformPrivate.githubPrivateKey,
|
|
15205
|
+
"fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
|
|
15146
15206
|
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
15147
15207
|
})) {
|
|
15148
15208
|
if (source) {
|
|
@@ -15159,6 +15219,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
15159
15219
|
const credentials = platformApiCredentialSpecs({
|
|
15160
15220
|
emailProvider: runtime.environment.email?.provider,
|
|
15161
15221
|
cloudflareKv: cloudflareConfigured,
|
|
15222
|
+
githubApp: Boolean(runtime.environment.githubApp),
|
|
15162
15223
|
realtime: Boolean(runtime.environment.realtime)
|
|
15163
15224
|
});
|
|
15164
15225
|
const units = renderPlatformApiUnits({
|
|
@@ -16156,7 +16217,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
|
|
|
16156
16217
|
|
|
16157
16218
|
class MetalBootstrapError extends Error {
|
|
16158
16219
|
}
|
|
16159
|
-
function
|
|
16220
|
+
function parseMetalPublicIdentity(value) {
|
|
16160
16221
|
let parsed;
|
|
16161
16222
|
try {
|
|
16162
16223
|
parsed = JSON.parse(value);
|
|
@@ -16171,7 +16232,13 @@ function normalizeMetalPublicIdentity(value) {
|
|
|
16171
16232
|
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
16233
|
throw new MetalBootstrapError("metal public identity proof was invalid");
|
|
16173
16234
|
}
|
|
16174
|
-
return
|
|
16235
|
+
return {
|
|
16236
|
+
nodeKey: identity.nodeKey,
|
|
16237
|
+
publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
|
|
16238
|
+
};
|
|
16239
|
+
}
|
|
16240
|
+
function normalizeMetalPublicIdentity(value) {
|
|
16241
|
+
return JSON.stringify(parseMetalPublicIdentity(value));
|
|
16175
16242
|
}
|
|
16176
16243
|
var defaultExec2 = async (argv2, stdin) => {
|
|
16177
16244
|
const child = Bun.spawn([...argv2], {
|
|
@@ -17061,7 +17128,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
|
|
|
17061
17128
|
return writeOwnerJson2(path, request, "operator fleet request");
|
|
17062
17129
|
}
|
|
17063
17130
|
var validateMetalRequestValue = (value, options = {}) => {
|
|
17064
|
-
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
17131
|
+
const root = exactKeys5(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
|
|
17065
17132
|
if (root.kind !== "metal-remote")
|
|
17066
17133
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
17067
17134
|
const targetValue = exactKeys5(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
|
|
@@ -17080,6 +17147,12 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17080
17147
|
if (typeof root.metalConfigFile !== "string")
|
|
17081
17148
|
throw new Error("metalConfigFile must be an absolute path");
|
|
17082
17149
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
17150
|
+
if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
|
|
17151
|
+
throw new Error("metalIdentityEvidenceFile must be an absolute path");
|
|
17152
|
+
}
|
|
17153
|
+
if (typeof root.metalIdentityEvidenceFile === "string") {
|
|
17154
|
+
canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
|
|
17155
|
+
}
|
|
17083
17156
|
if (options.validateMetalConfig !== false)
|
|
17084
17157
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
17085
17158
|
const genesis = exactKeys5(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
|
|
@@ -17128,6 +17201,7 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
17128
17201
|
return {
|
|
17129
17202
|
kind: "metal-remote",
|
|
17130
17203
|
metalConfigFile: root.metalConfigFile,
|
|
17204
|
+
...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
|
|
17131
17205
|
target: {
|
|
17132
17206
|
...target,
|
|
17133
17207
|
identityPublicKeyFile: targetValue.identityPublicKeyFile,
|
|
@@ -17148,6 +17222,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
17148
17222
|
validateMetalRequestValue(request);
|
|
17149
17223
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
17150
17224
|
}
|
|
17225
|
+
function readOperatorMetalIdentityEvidence(path, request) {
|
|
17226
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
|
|
17227
|
+
const root = exactKeys5(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
|
|
17228
|
+
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") {
|
|
17229
|
+
throw new Error("operator metal identity evidence is malformed");
|
|
17230
|
+
}
|
|
17231
|
+
const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
|
|
17232
|
+
const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
|
|
17233
|
+
if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
|
|
17234
|
+
throw new Error("operator metal identity evidence does not match the reviewed Metal target");
|
|
17235
|
+
}
|
|
17236
|
+
return {
|
|
17237
|
+
format: 1,
|
|
17238
|
+
kind: "forgezero-operator-metal-identity",
|
|
17239
|
+
metalHostname: root.metalHostname,
|
|
17240
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
17241
|
+
...identity
|
|
17242
|
+
};
|
|
17243
|
+
}
|
|
17151
17244
|
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
17152
17245
|
const root = exactKeys5(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
17153
17246
|
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
@@ -17833,6 +17926,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
17833
17926
|
`${REMOTE_STAGE}/metal-config.json`,
|
|
17834
17927
|
"--apply"
|
|
17835
17928
|
], "remote typed metal bootstrap");
|
|
17929
|
+
if (request.metalIdentityEvidenceFile) {
|
|
17930
|
+
let applied;
|
|
17931
|
+
try {
|
|
17932
|
+
applied = JSON.parse(output);
|
|
17933
|
+
} catch {
|
|
17934
|
+
throw new Error("remote Metal bootstrap did not return identity evidence");
|
|
17935
|
+
}
|
|
17936
|
+
const publicIdentity2 = applied?.publicIdentity;
|
|
17937
|
+
if (typeof publicIdentity2 !== "string")
|
|
17938
|
+
throw new Error("remote Metal bootstrap omitted its public identity");
|
|
17939
|
+
const identity = parseMetalPublicIdentity(publicIdentity2);
|
|
17940
|
+
writeOwnerJson2(request.metalIdentityEvidenceFile, {
|
|
17941
|
+
format: 1,
|
|
17942
|
+
kind: "forgezero-operator-metal-identity",
|
|
17943
|
+
metalHostname: config.metalHostname,
|
|
17944
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
17945
|
+
...identity
|
|
17946
|
+
}, "operator metal identity evidence");
|
|
17947
|
+
}
|
|
17836
17948
|
return { plan, output };
|
|
17837
17949
|
} finally {
|
|
17838
17950
|
if (knownHosts) {
|
|
@@ -17886,7 +17998,7 @@ async function applyAttendedPlatformBootstrap(fleetRequest, cloudflareRequest, a
|
|
|
17886
17998
|
}
|
|
17887
17999
|
|
|
17888
18000
|
// src/platform-genesis-config.ts
|
|
17889
|
-
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation) {
|
|
18001
|
+
function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname, region, deployment, attestation, metalIdentity) {
|
|
17890
18002
|
const fleet = validatePlatformGenesisGuests(guests);
|
|
17891
18003
|
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(metalHostname)) {
|
|
17892
18004
|
throw new Error("platform genesis requires the reviewed metal hostname");
|
|
@@ -17902,6 +18014,7 @@ function platformGenesisBootstrapConfigs(template, guests, nodes, metalHostname,
|
|
|
17902
18014
|
const coordinators = fleet.map((guest) => `http://${guest.address}:8529`);
|
|
17903
18015
|
const initialInventory = {
|
|
17904
18016
|
metalHostname,
|
|
18017
|
+
...metalIdentity ? { metalIdentity: structuredClone(metalIdentity) } : {},
|
|
17905
18018
|
region: {
|
|
17906
18019
|
key: template.runtime.environment.nodeRegion,
|
|
17907
18020
|
label: region.label,
|
|
@@ -18065,6 +18178,7 @@ function forgeZeroLaunchTemplate(profile, guests, bundle, owner) {
|
|
|
18065
18178
|
otlpTraceSampleRatio: 0.1,
|
|
18066
18179
|
custodianEmail: owner.custodianEmail,
|
|
18067
18180
|
email: owner.email,
|
|
18181
|
+
...owner.githubApp ? { githubApp: owner.githubApp } : {},
|
|
18068
18182
|
deployProfile: profile.environment
|
|
18069
18183
|
},
|
|
18070
18184
|
serviceUser: "forgezero-api",
|
|
@@ -19267,13 +19381,20 @@ function readPlatformLaunchEnvironment(path) {
|
|
|
19267
19381
|
if (smtpPort !== undefined && (!Number.isInteger(smtpPort) || smtpPort < 1 || smtpPort > 65535)) {
|
|
19268
19382
|
throw new Error("FZ_SMTP_PORT must be an integer from 1 through 65535");
|
|
19269
19383
|
}
|
|
19384
|
+
const githubEnabled = (values.get("FZ_GITHUB_ENABLED") ?? "false").trim().toLowerCase() === "true";
|
|
19385
|
+
const githubApp = githubEnabled ? {
|
|
19386
|
+
clientId: requiredValue("FZ_GITHUB_CLIENT_ID"),
|
|
19387
|
+
appId: requiredValue("FZ_GITHUB_APP_ID"),
|
|
19388
|
+
slug: requiredValue("FZ_GITHUB_APP_SLUG")
|
|
19389
|
+
} : undefined;
|
|
19270
19390
|
const owner = provider === "jetemail" ? {
|
|
19271
19391
|
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
19272
19392
|
email: {
|
|
19273
19393
|
provider,
|
|
19274
19394
|
from: requiredValue("FZ_EMAIL_FROM"),
|
|
19275
19395
|
eu: (values.get("FZ_JETEMAIL_EU") ?? "false").trim().toLowerCase() === "true"
|
|
19276
|
-
}
|
|
19396
|
+
},
|
|
19397
|
+
...githubApp ? { githubApp } : {}
|
|
19277
19398
|
} : {
|
|
19278
19399
|
custodianEmail: requiredValue("FZ_BOOTSTRAP_CUSTODIAN_EMAIL"),
|
|
19279
19400
|
email: {
|
|
@@ -19282,13 +19403,19 @@ function readPlatformLaunchEnvironment(path) {
|
|
|
19282
19403
|
host: requiredValue("FZ_SMTP_HOST"),
|
|
19283
19404
|
port: smtpPort,
|
|
19284
19405
|
user: requiredValue("FZ_SMTP_USER")
|
|
19285
|
-
}
|
|
19406
|
+
},
|
|
19407
|
+
...githubApp ? { githubApp } : {}
|
|
19286
19408
|
};
|
|
19287
19409
|
return {
|
|
19288
19410
|
owner,
|
|
19289
19411
|
emailSecret: requiredValue(provider === "jetemail" ? "FZ_JETEMAIL_API_KEY" : "FZ_SMTP_PASSWORD"),
|
|
19290
19412
|
cloudflareTunnelToken: requiredValue("CF_TUNNEL_TOKEN"),
|
|
19291
|
-
cloudflareApiToken: requiredValue("CF_API_TOKEN")
|
|
19413
|
+
cloudflareApiToken: requiredValue("CF_API_TOKEN"),
|
|
19414
|
+
...githubEnabled ? {
|
|
19415
|
+
githubClientSecret: requiredValue("FZ_GITHUB_CLIENT_SECRET"),
|
|
19416
|
+
githubPrivateKeyBase64: requiredValue("FZ_GITHUB_PRIVATE_KEY_BASE64"),
|
|
19417
|
+
githubWebhookSecret: requiredValue("FZ_GITHUB_WEBHOOK_SECRET")
|
|
19418
|
+
} : {}
|
|
19292
19419
|
};
|
|
19293
19420
|
}
|
|
19294
19421
|
async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapCode) {
|
|
@@ -19300,6 +19427,11 @@ async function promptPlatformBootstrapSecrets(config, generatedClusterBootstrapC
|
|
|
19300
19427
|
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
|
|
19301
19428
|
cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
19302
19429
|
cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
19430
|
+
} : {},
|
|
19431
|
+
...config.runtime.environment.githubApp ? {
|
|
19432
|
+
githubClientSecret: await bootstrapSecret("GitHub App client secret"),
|
|
19433
|
+
githubPrivateKeyBase64: await bootstrapSecret("GitHub App RSA private key (base64)"),
|
|
19434
|
+
githubWebhookSecret: await bootstrapSecret("GitHub App webhook secret")
|
|
19303
19435
|
} : {}
|
|
19304
19436
|
});
|
|
19305
19437
|
}
|
|
@@ -19481,6 +19613,7 @@ function writeMetalOperatorFiles(directory) {
|
|
|
19481
19613
|
const remoteRequest = writeOperatorMetalBootstrapRequest(join13(output, "metal-remote.json"), {
|
|
19482
19614
|
kind: "metal-remote",
|
|
19483
19615
|
metalConfigFile: metalConfig,
|
|
19616
|
+
metalIdentityEvidenceFile: join13(output, "metal-identity.json"),
|
|
19484
19617
|
target: { ...target, ...identity },
|
|
19485
19618
|
genesis: { ...interactiveGenesisGuests(), sshPublicKeyFiles: [identity.identityPublicKeyFile] }
|
|
19486
19619
|
});
|
|
@@ -19501,6 +19634,10 @@ function writePlatformGenesisFleet(directory, preset) {
|
|
|
19501
19634
|
const output = genesisOutputDirectory(directory);
|
|
19502
19635
|
const metalRequestFile = preset?.metalRequestFile ?? bootstrapAnswer("Owner-only metal remote request containing the reviewed genesis guests");
|
|
19503
19636
|
const metalRequest = readOperatorMetalBootstrapRequest(metalRequestFile, { validateMetalConfig: false });
|
|
19637
|
+
if (!metalRequest.metalIdentityEvidenceFile) {
|
|
19638
|
+
throw new Error("the reviewed Metal request predates automatic identity seeding; rerun Metal apply with a generated request");
|
|
19639
|
+
}
|
|
19640
|
+
const metalIdentity = readOperatorMetalIdentityEvidence(metalRequest.metalIdentityEvidenceFile, metalRequest);
|
|
19504
19641
|
const guestHostKeysFile = preset?.guestHostKeysFile ?? bootstrapAnswer("Owner-only guest host-key evidence file");
|
|
19505
19642
|
const guestHostKeys = readOperatorGuestHostKeyEvidence(guestHostKeysFile, metalRequest);
|
|
19506
19643
|
const fleet = metalRequest.genesis.nodes;
|
|
@@ -19545,7 +19682,7 @@ function writePlatformGenesisFleet(directory, preset) {
|
|
|
19545
19682
|
branch: preset.profile.branch,
|
|
19546
19683
|
revision: preset.bundle.manifest.revision,
|
|
19547
19684
|
bundleSha256: preset.bundle.manifest.sha256
|
|
19548
|
-
} : undefined, preset?.profile.attestation).map((config) => {
|
|
19685
|
+
} : undefined, preset?.profile.attestation, { nodeKey: metalIdentity.nodeKey, publicKeys: { ...metalIdentity.publicKeys } }).map((config) => {
|
|
19549
19686
|
const path = `${output}/${config.computeReference}-platform.json`;
|
|
19550
19687
|
if (preset)
|
|
19551
19688
|
writeDerivedLaunchFile(path, (temporary) => writeBootstrapConfig(temporary, config));
|
|
@@ -19599,10 +19736,16 @@ function forgeZeroLaunchOwnerInput() {
|
|
|
19599
19736
|
throw new Error("Bootstrap email provider must be smtp or jetemail");
|
|
19600
19737
|
}
|
|
19601
19738
|
const from = bootstrapAnswer("Bootstrap email From address");
|
|
19739
|
+
const githubApp = bootstrapBoolean("Configure the platform GitHub App now?", "no") ? {
|
|
19740
|
+
clientId: bootstrapAnswer("GitHub App client ID"),
|
|
19741
|
+
appId: bootstrapAnswer("GitHub App ID"),
|
|
19742
|
+
slug: bootstrapAnswer("GitHub App slug")
|
|
19743
|
+
} : undefined;
|
|
19602
19744
|
if (provider === "jetemail") {
|
|
19603
19745
|
return {
|
|
19604
19746
|
custodianEmail,
|
|
19605
|
-
email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") }
|
|
19747
|
+
email: { provider, from, eu: bootstrapBoolean("Use JetEmail EU processing?", "no") },
|
|
19748
|
+
...githubApp ? { githubApp } : {}
|
|
19606
19749
|
};
|
|
19607
19750
|
}
|
|
19608
19751
|
return {
|
|
@@ -19613,7 +19756,8 @@ function forgeZeroLaunchOwnerInput() {
|
|
|
19613
19756
|
host: bootstrapAnswer("SMTP host"),
|
|
19614
19757
|
port: bootstrapNumber("SMTP port", "587"),
|
|
19615
19758
|
user: bootstrapAnswer("SMTP user")
|
|
19616
|
-
}
|
|
19759
|
+
},
|
|
19760
|
+
...githubApp ? { githubApp } : {}
|
|
19617
19761
|
};
|
|
19618
19762
|
}
|
|
19619
19763
|
async function prepareForgeZeroPlatformLaunch(environment, outputDirectory, projectRoot, owner) {
|
|
@@ -19798,7 +19942,12 @@ async function runAttendedPlatformFleet(fleet, apply, generatedClusterBootstrapC
|
|
|
19798
19942
|
clusterBootstrapCode: generatedClusterBootstrapCode,
|
|
19799
19943
|
emailSecret: environmentInput.emailSecret,
|
|
19800
19944
|
cloudflareTunnelToken: environmentInput.cloudflareTunnelToken,
|
|
19801
|
-
cloudflareApiToken: environmentInput.cloudflareApiToken
|
|
19945
|
+
cloudflareApiToken: environmentInput.cloudflareApiToken,
|
|
19946
|
+
...environmentInput.githubClientSecret ? {
|
|
19947
|
+
githubClientSecret: environmentInput.githubClientSecret,
|
|
19948
|
+
githubPrivateKeyBase64: environmentInput.githubPrivateKeyBase64,
|
|
19949
|
+
githubWebhookSecret: environmentInput.githubWebhookSecret
|
|
19950
|
+
} : {}
|
|
19802
19951
|
}) : await promptPlatformBootstrapSecrets(firstConfig, generatedClusterBootstrapCode);
|
|
19803
19952
|
const cloudflare = await resolveCloudflareBootstrapCommandConfig(fleet.cloudflareConfigFile, {
|
|
19804
19953
|
tunnelToken: secrets.cloudflareTunnelToken,
|
|
@@ -43,6 +43,14 @@ export interface MetalBootstrapApplyOptions {
|
|
|
43
43
|
}
|
|
44
44
|
export declare class MetalBootstrapError extends Error {
|
|
45
45
|
}
|
|
46
|
+
export interface MetalPublicIdentity {
|
|
47
|
+
nodeKey: string;
|
|
48
|
+
publicKeys: {
|
|
49
|
+
ed25519: string;
|
|
50
|
+
mlDsa: string;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
export declare function parseMetalPublicIdentity(value: string): MetalPublicIdentity;
|
|
46
54
|
export declare function normalizeMetalPublicIdentity(value: string): string;
|
|
47
55
|
export interface MetalBootstrapValidationOptions {
|
|
48
56
|
/**
|
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.103";
|
|
370
370
|
|
|
371
371
|
// src/otel-collector.ts
|
|
372
372
|
var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
|
|
@@ -543,7 +543,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
|
|
|
543
543
|
|
|
544
544
|
class MetalBootstrapError extends Error {
|
|
545
545
|
}
|
|
546
|
-
function
|
|
546
|
+
function parseMetalPublicIdentity(value) {
|
|
547
547
|
let parsed;
|
|
548
548
|
try {
|
|
549
549
|
parsed = JSON.parse(value);
|
|
@@ -558,7 +558,13 @@ function normalizeMetalPublicIdentity(value) {
|
|
|
558
558
|
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)) {
|
|
559
559
|
throw new MetalBootstrapError("metal public identity proof was invalid");
|
|
560
560
|
}
|
|
561
|
-
return
|
|
561
|
+
return {
|
|
562
|
+
nodeKey: identity.nodeKey,
|
|
563
|
+
publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
function normalizeMetalPublicIdentity(value) {
|
|
567
|
+
return JSON.stringify(parseMetalPublicIdentity(value));
|
|
562
568
|
}
|
|
563
569
|
var defaultExec2 = async (argv, stdin) => {
|
|
564
570
|
const child = Bun.spawn([...argv], {
|
|
@@ -1258,6 +1264,7 @@ export {
|
|
|
1258
1264
|
renderMetalUnits,
|
|
1259
1265
|
readMetalBootstrapConfig,
|
|
1260
1266
|
planMetalBootstrap,
|
|
1267
|
+
parseMetalPublicIdentity,
|
|
1261
1268
|
normalizeMetalPublicIdentity,
|
|
1262
1269
|
metalBootstrapStatus,
|
|
1263
1270
|
applyMetalBootstrap,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type PlatformBootstrapConfig, type PlatformBootstrapSecrets } from './bootstrap';
|
|
2
|
+
import { type MetalPublicIdentity } from './metal-bootstrap';
|
|
2
3
|
import { type PlatformGenesisGuest } from './platform-genesis';
|
|
3
4
|
export interface OperatorSshHop {
|
|
4
5
|
address: string;
|
|
@@ -20,12 +21,20 @@ export interface OperatorMetalBootstrapRequest {
|
|
|
20
21
|
kind: 'metal-remote';
|
|
21
22
|
target: OperatorPlatformBootstrapRequest['target'];
|
|
22
23
|
metalConfigFile: string;
|
|
24
|
+
/** Secret-free, pinned-host identity evidence written automatically after apply. */
|
|
25
|
+
metalIdentityEvidenceFile?: string;
|
|
23
26
|
genesis: {
|
|
24
27
|
nodes: readonly [PlatformGenesisGuest, PlatformGenesisGuest, PlatformGenesisGuest];
|
|
25
28
|
rehearsalNode?: PlatformGenesisGuest;
|
|
26
29
|
sshPublicKeyFiles: string[];
|
|
27
30
|
};
|
|
28
31
|
}
|
|
32
|
+
export interface OperatorMetalIdentityEvidence extends MetalPublicIdentity {
|
|
33
|
+
format: 1;
|
|
34
|
+
kind: 'forgezero-operator-metal-identity';
|
|
35
|
+
metalHostname: string;
|
|
36
|
+
metalHostKeySha256: string;
|
|
37
|
+
}
|
|
29
38
|
export interface OperatorGuestHostKeyEvidence {
|
|
30
39
|
format: 1;
|
|
31
40
|
kind: 'forgezero-operator-guest-host-keys';
|
|
@@ -139,6 +148,7 @@ export declare function readOperatorMetalBootstrapRequest(path: string, options?
|
|
|
139
148
|
validateMetalConfig?: boolean;
|
|
140
149
|
}): OperatorMetalBootstrapRequest;
|
|
141
150
|
export declare function writeOperatorMetalBootstrapRequest(path: string, request: OperatorMetalBootstrapRequest): string;
|
|
151
|
+
export declare function readOperatorMetalIdentityEvidence(path: string, request: OperatorMetalBootstrapRequest): OperatorMetalIdentityEvidence;
|
|
142
152
|
export declare function readOperatorGuestHostKeyEvidence(path: string, request?: OperatorMetalBootstrapRequest): OperatorGuestHostKeyEvidence;
|
|
143
153
|
export declare function writeOperatorGuestHostKeyEvidence(path: string, evidence: OperatorGuestHostKeyEvidence, request?: OperatorMetalBootstrapRequest): string;
|
|
144
154
|
export declare function planOperatorMetalBootstrap(request: OperatorMetalBootstrapRequest, mode: OperatorMetalBootstrapMode): OperatorMetalBootstrapPlan;
|
|
@@ -1386,7 +1386,7 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
1386
1386
|
}
|
|
1387
1387
|
|
|
1388
1388
|
// src/bootstrap.ts
|
|
1389
|
-
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
1389
|
+
import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
|
|
1390
1390
|
import {
|
|
1391
1391
|
chmodSync as chmodSync3,
|
|
1392
1392
|
existsSync as existsSync5,
|
|
@@ -1420,7 +1420,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1420
1420
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1421
1421
|
|
|
1422
1422
|
// src/version.ts
|
|
1423
|
-
var VERSION = "0.1.
|
|
1423
|
+
var VERSION = "0.1.103";
|
|
1424
1424
|
|
|
1425
1425
|
// src/software.ts
|
|
1426
1426
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3090,10 +3090,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
3090
3090
|
};
|
|
3091
3091
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
3092
3092
|
function validatePlatformInitialInventory(value) {
|
|
3093
|
-
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)) {
|
|
3093
|
+
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)) {
|
|
3094
3094
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
3095
3095
|
}
|
|
3096
3096
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
3097
|
+
if (value.metalIdentity !== undefined) {
|
|
3098
|
+
const identity = value.metalIdentity;
|
|
3099
|
+
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))) {
|
|
3100
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3097
3103
|
if (value.region.city !== undefined)
|
|
3098
3104
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
3099
3105
|
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)))
|
|
@@ -3128,6 +3134,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
3128
3134
|
}
|
|
3129
3135
|
return {
|
|
3130
3136
|
metalHostname: value.metalHostname,
|
|
3137
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
3131
3138
|
region: { ...value.region },
|
|
3132
3139
|
computes: value.computes.map((compute, index) => ({
|
|
3133
3140
|
...computes[index],
|
|
@@ -3181,6 +3188,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3181
3188
|
} else if (input.email !== undefined) {
|
|
3182
3189
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3183
3190
|
}
|
|
3191
|
+
if (input.githubApp) {
|
|
3192
|
+
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)) {
|
|
3193
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3184
3196
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3185
3197
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3186
3198
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -3311,6 +3323,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3311
3323
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
3312
3324
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
3313
3325
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
3326
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
3327
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
3328
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
3314
3329
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
3315
3330
|
};
|
|
3316
3331
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -3322,6 +3337,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
3322
3337
|
const optional = [
|
|
3323
3338
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
3324
3339
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
3340
|
+
["fz_github.clientSecret", options.githubApp],
|
|
3341
|
+
["fz_github.privateKey", options.githubApp],
|
|
3342
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
3325
3343
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
3326
3344
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
3327
3345
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -3717,7 +3735,17 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3717
3735
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3718
3736
|
throw new Error("bootstrap credential input must be an object");
|
|
3719
3737
|
const source = input;
|
|
3720
|
-
const allowed = [
|
|
3738
|
+
const allowed = [
|
|
3739
|
+
"clusterBootstrapCode",
|
|
3740
|
+
"emailSecret",
|
|
3741
|
+
"enrolmentToken",
|
|
3742
|
+
"backupS3Secret",
|
|
3743
|
+
"cloudflareTunnelToken",
|
|
3744
|
+
"cloudflareApiToken",
|
|
3745
|
+
"githubClientSecret",
|
|
3746
|
+
"githubPrivateKeyBase64",
|
|
3747
|
+
"githubWebhookSecret"
|
|
3748
|
+
];
|
|
3721
3749
|
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
3722
3750
|
if (unknown.length)
|
|
3723
3751
|
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
@@ -3727,6 +3755,9 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3727
3755
|
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
3728
3756
|
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
3729
3757
|
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
3758
|
+
const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
|
|
3759
|
+
const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
|
|
3760
|
+
const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
|
|
3730
3761
|
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
3731
3762
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
3732
3763
|
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
@@ -3746,12 +3777,29 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3746
3777
|
}
|
|
3747
3778
|
if (cloudflareTunnelToken)
|
|
3748
3779
|
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
3780
|
+
const githubConfigured = Boolean(config.runtime.environment.githubApp);
|
|
3781
|
+
if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
|
|
3782
|
+
throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
|
|
3783
|
+
}
|
|
3784
|
+
if (githubConfigured) {
|
|
3785
|
+
if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
|
|
3786
|
+
throw new Error("GitHub App client or webhook secret is malformed");
|
|
3787
|
+
}
|
|
3788
|
+
try {
|
|
3789
|
+
const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
|
|
3790
|
+
if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
|
|
3791
|
+
throw new Error("not RSA");
|
|
3792
|
+
} catch {
|
|
3793
|
+
throw new Error("GitHub App private key must be a base64-encoded RSA private key");
|
|
3794
|
+
}
|
|
3795
|
+
}
|
|
3749
3796
|
return {
|
|
3750
3797
|
clusterBootstrapCode,
|
|
3751
3798
|
emailSecret,
|
|
3752
3799
|
...enrolmentToken ? { enrolmentToken } : {},
|
|
3753
3800
|
...backupS3Secret ? { backupS3Secret } : {},
|
|
3754
|
-
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
3801
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
|
|
3802
|
+
...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
|
|
3755
3803
|
};
|
|
3756
3804
|
}
|
|
3757
3805
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
@@ -4303,6 +4351,12 @@ function approvedPlatformRepairIdentityDigests(config) {
|
|
|
4303
4351
|
delete beforeAttestation.runtime.environment.initialInventory.attestation;
|
|
4304
4352
|
prior.push(beforeAttestation);
|
|
4305
4353
|
}
|
|
4354
|
+
const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
|
|
4355
|
+
if (metalIdentity && config.enrolment.source === "genesis-derived") {
|
|
4356
|
+
const beforeMetalIdentity = structuredClone(config);
|
|
4357
|
+
delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
|
|
4358
|
+
prior.push(beforeMetalIdentity);
|
|
4359
|
+
}
|
|
4306
4360
|
if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
|
|
4307
4361
|
const beforeReplicaCustodianEmail = structuredClone(config);
|
|
4308
4362
|
delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
|
|
@@ -4660,7 +4714,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4660
4714
|
enrolmentToken: checked4.enrolmentToken,
|
|
4661
4715
|
backup: checked4.backupS3Secret,
|
|
4662
4716
|
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4663
|
-
cloudflareApiToken: checked4.cloudflareApiToken
|
|
4717
|
+
cloudflareApiToken: checked4.cloudflareApiToken,
|
|
4718
|
+
githubClientSecret: checked4.githubClientSecret,
|
|
4719
|
+
githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
|
|
4720
|
+
githubWebhookSecret: checked4.githubWebhookSecret
|
|
4664
4721
|
};
|
|
4665
4722
|
})() : undefined;
|
|
4666
4723
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4758,6 +4815,9 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4758
4815
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4759
4816
|
for (const [name, source] of Object.entries({
|
|
4760
4817
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4818
|
+
"fz_github.clientSecret": platformPrivate.githubClientSecret,
|
|
4819
|
+
"fz_github.privateKey": platformPrivate.githubPrivateKey,
|
|
4820
|
+
"fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
|
|
4761
4821
|
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4762
4822
|
})) {
|
|
4763
4823
|
if (source) {
|
|
@@ -4774,6 +4834,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4774
4834
|
const credentials = platformApiCredentialSpecs({
|
|
4775
4835
|
emailProvider: runtime.environment.email?.provider,
|
|
4776
4836
|
cloudflareKv: cloudflareConfigured,
|
|
4837
|
+
githubApp: Boolean(runtime.environment.githubApp),
|
|
4777
4838
|
realtime: Boolean(runtime.environment.realtime)
|
|
4778
4839
|
});
|
|
4779
4840
|
const units = renderPlatformApiUnits({
|
|
@@ -5501,7 +5562,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
|
|
|
5501
5562
|
|
|
5502
5563
|
class MetalBootstrapError extends Error {
|
|
5503
5564
|
}
|
|
5504
|
-
function
|
|
5565
|
+
function parseMetalPublicIdentity(value) {
|
|
5505
5566
|
let parsed;
|
|
5506
5567
|
try {
|
|
5507
5568
|
parsed = JSON.parse(value);
|
|
@@ -5516,7 +5577,13 @@ function normalizeMetalPublicIdentity(value) {
|
|
|
5516
5577
|
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)) {
|
|
5517
5578
|
throw new MetalBootstrapError("metal public identity proof was invalid");
|
|
5518
5579
|
}
|
|
5519
|
-
return
|
|
5580
|
+
return {
|
|
5581
|
+
nodeKey: identity.nodeKey,
|
|
5582
|
+
publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
|
|
5583
|
+
};
|
|
5584
|
+
}
|
|
5585
|
+
function normalizeMetalPublicIdentity(value) {
|
|
5586
|
+
return JSON.stringify(parseMetalPublicIdentity(value));
|
|
5520
5587
|
}
|
|
5521
5588
|
var defaultExec2 = async (argv, stdin) => {
|
|
5522
5589
|
const child = Bun.spawn([...argv], {
|
|
@@ -6412,7 +6479,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
|
|
|
6412
6479
|
return writeOwnerJson2(path, request, "operator fleet request");
|
|
6413
6480
|
}
|
|
6414
6481
|
var validateMetalRequestValue = (value, options = {}) => {
|
|
6415
|
-
const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
6482
|
+
const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
|
|
6416
6483
|
if (root.kind !== "metal-remote")
|
|
6417
6484
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
6418
6485
|
const targetValue = exactKeys3(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
|
|
@@ -6431,6 +6498,12 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
6431
6498
|
if (typeof root.metalConfigFile !== "string")
|
|
6432
6499
|
throw new Error("metalConfigFile must be an absolute path");
|
|
6433
6500
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
6501
|
+
if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
|
|
6502
|
+
throw new Error("metalIdentityEvidenceFile must be an absolute path");
|
|
6503
|
+
}
|
|
6504
|
+
if (typeof root.metalIdentityEvidenceFile === "string") {
|
|
6505
|
+
canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
|
|
6506
|
+
}
|
|
6434
6507
|
if (options.validateMetalConfig !== false)
|
|
6435
6508
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
6436
6509
|
const genesis = exactKeys3(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
|
|
@@ -6479,6 +6552,7 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
6479
6552
|
return {
|
|
6480
6553
|
kind: "metal-remote",
|
|
6481
6554
|
metalConfigFile: root.metalConfigFile,
|
|
6555
|
+
...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
|
|
6482
6556
|
target: {
|
|
6483
6557
|
...target,
|
|
6484
6558
|
identityPublicKeyFile: targetValue.identityPublicKeyFile,
|
|
@@ -6499,6 +6573,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
6499
6573
|
validateMetalRequestValue(request);
|
|
6500
6574
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
6501
6575
|
}
|
|
6576
|
+
function readOperatorMetalIdentityEvidence(path, request) {
|
|
6577
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
|
|
6578
|
+
const root = exactKeys3(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
|
|
6579
|
+
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") {
|
|
6580
|
+
throw new Error("operator metal identity evidence is malformed");
|
|
6581
|
+
}
|
|
6582
|
+
const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
|
|
6583
|
+
const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
|
|
6584
|
+
if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
|
|
6585
|
+
throw new Error("operator metal identity evidence does not match the reviewed Metal target");
|
|
6586
|
+
}
|
|
6587
|
+
return {
|
|
6588
|
+
format: 1,
|
|
6589
|
+
kind: "forgezero-operator-metal-identity",
|
|
6590
|
+
metalHostname: root.metalHostname,
|
|
6591
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
6592
|
+
...identity
|
|
6593
|
+
};
|
|
6594
|
+
}
|
|
6502
6595
|
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
6503
6596
|
const root = exactKeys3(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
6504
6597
|
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
@@ -7279,6 +7372,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
7279
7372
|
`${REMOTE_STAGE}/metal-config.json`,
|
|
7280
7373
|
"--apply"
|
|
7281
7374
|
], "remote typed metal bootstrap");
|
|
7375
|
+
if (request.metalIdentityEvidenceFile) {
|
|
7376
|
+
let applied;
|
|
7377
|
+
try {
|
|
7378
|
+
applied = JSON.parse(output);
|
|
7379
|
+
} catch {
|
|
7380
|
+
throw new Error("remote Metal bootstrap did not return identity evidence");
|
|
7381
|
+
}
|
|
7382
|
+
const publicIdentity2 = applied?.publicIdentity;
|
|
7383
|
+
if (typeof publicIdentity2 !== "string")
|
|
7384
|
+
throw new Error("remote Metal bootstrap omitted its public identity");
|
|
7385
|
+
const identity = parseMetalPublicIdentity(publicIdentity2);
|
|
7386
|
+
writeOwnerJson2(request.metalIdentityEvidenceFile, {
|
|
7387
|
+
format: 1,
|
|
7388
|
+
kind: "forgezero-operator-metal-identity",
|
|
7389
|
+
metalHostname: config.metalHostname,
|
|
7390
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
7391
|
+
...identity
|
|
7392
|
+
}, "operator metal identity evidence");
|
|
7393
|
+
}
|
|
7282
7394
|
return { plan, output };
|
|
7283
7395
|
} finally {
|
|
7284
7396
|
if (knownHosts) {
|
|
@@ -7299,6 +7411,7 @@ export {
|
|
|
7299
7411
|
redactOperatorSecretDiagnostic,
|
|
7300
7412
|
readOperatorPlatformBootstrapRequest,
|
|
7301
7413
|
readOperatorPlatformBootstrapFleetRequest,
|
|
7414
|
+
readOperatorMetalIdentityEvidence,
|
|
7302
7415
|
readOperatorMetalBootstrapRequest,
|
|
7303
7416
|
readOperatorGuestHostKeyEvidence,
|
|
7304
7417
|
planOperatorPlatformFleetBootstrap,
|
|
@@ -19,6 +19,14 @@ export interface PlatformInitialInventoryCompute {
|
|
|
19
19
|
}
|
|
20
20
|
export interface PlatformInitialInventory {
|
|
21
21
|
metalHostname: string;
|
|
22
|
+
/** Hybrid public identity proved by the pinned Metal bootstrap transport. */
|
|
23
|
+
metalIdentity?: {
|
|
24
|
+
nodeKey: string;
|
|
25
|
+
publicKeys: {
|
|
26
|
+
ed25519: string;
|
|
27
|
+
mlDsa: string;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
22
30
|
region: {
|
|
23
31
|
key: string;
|
|
24
32
|
label: string;
|
|
@@ -60,6 +68,16 @@ export type PlatformBootstrapEmail = {
|
|
|
60
68
|
from: string;
|
|
61
69
|
eu: boolean;
|
|
62
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Public coordinates for one GitHub App. Its client secret, RSA private key and
|
|
73
|
+
* webhook secret are attended inputs sealed as systemd credentials and later
|
|
74
|
+
* imported into the unlocked platform Vault.
|
|
75
|
+
*/
|
|
76
|
+
export interface PlatformBootstrapGitHubApp {
|
|
77
|
+
clientId: string;
|
|
78
|
+
appId: string;
|
|
79
|
+
slug: string;
|
|
80
|
+
}
|
|
63
81
|
export interface PlatformSharedEnvironment {
|
|
64
82
|
softwareProfile: PlatformSoftwareProfile;
|
|
65
83
|
databaseRole: PlatformDatabaseRole;
|
|
@@ -91,6 +109,7 @@ export interface PlatformSharedEnvironment {
|
|
|
91
109
|
otlpTraceSampleRatio: number;
|
|
92
110
|
custodianEmail?: string;
|
|
93
111
|
email?: PlatformBootstrapEmail;
|
|
112
|
+
githubApp?: PlatformBootstrapGitHubApp;
|
|
94
113
|
backup?: {
|
|
95
114
|
endpoint: string;
|
|
96
115
|
region: string;
|
|
@@ -118,12 +137,13 @@ export declare function validatePlatformSharedEnvironment(input: PlatformSharedE
|
|
|
118
137
|
/** Render only non-secret runtime coordinates. Passwords/tokens have no field in this contract. */
|
|
119
138
|
export declare function renderPlatformSharedEnvironment(input: PlatformSharedEnvironment): string;
|
|
120
139
|
export interface SystemdCredentialSpec {
|
|
121
|
-
name: 'arangodb-jwt' | 'seed-sync-root' | 'fz_smtp.password' | 'fz_jetemail.apiKey' | 'CF_API_TOKEN' | 'CF_TUNNEL_TOKEN' | 'REALTIME_PUBLISH_SECRET' | 'REALTIME_TICKET_SECRET';
|
|
140
|
+
name: 'arangodb-jwt' | 'seed-sync-root' | 'fz_smtp.password' | 'fz_jetemail.apiKey' | 'fz_github.clientSecret' | 'fz_github.privateKey' | 'fz_github.webhookSecret' | 'CF_API_TOKEN' | 'CF_TUNNEL_TOKEN' | 'REALTIME_PUBLISH_SECRET' | 'REALTIME_TICKET_SECRET';
|
|
122
141
|
encryptedPath: string;
|
|
123
142
|
required: boolean;
|
|
124
143
|
}
|
|
125
144
|
export declare function platformApiCredentialSpecs(options: {
|
|
126
145
|
emailProvider?: PlatformBootstrapEmail['provider'];
|
|
146
|
+
githubApp: boolean;
|
|
127
147
|
cloudflareKv: boolean;
|
|
128
148
|
realtime: boolean;
|
|
129
149
|
}): SystemdCredentialSpec[];
|
|
@@ -730,10 +730,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
730
730
|
};
|
|
731
731
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
732
732
|
function validatePlatformInitialInventory(value) {
|
|
733
|
-
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)) {
|
|
733
|
+
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)) {
|
|
734
734
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
735
735
|
}
|
|
736
736
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
737
|
+
if (value.metalIdentity !== undefined) {
|
|
738
|
+
const identity = value.metalIdentity;
|
|
739
|
+
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))) {
|
|
740
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
737
743
|
if (value.region.city !== undefined)
|
|
738
744
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
739
745
|
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)))
|
|
@@ -768,6 +774,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
768
774
|
}
|
|
769
775
|
return {
|
|
770
776
|
metalHostname: value.metalHostname,
|
|
777
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
771
778
|
region: { ...value.region },
|
|
772
779
|
computes: value.computes.map((compute, index) => ({
|
|
773
780
|
...computes[index],
|
|
@@ -821,6 +828,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
821
828
|
} else if (input.email !== undefined) {
|
|
822
829
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
823
830
|
}
|
|
831
|
+
if (input.githubApp) {
|
|
832
|
+
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)) {
|
|
833
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
834
|
+
}
|
|
835
|
+
}
|
|
824
836
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
825
837
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
826
838
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -951,6 +963,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
951
963
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
952
964
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
953
965
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
966
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
967
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
968
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
954
969
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
955
970
|
};
|
|
956
971
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -962,6 +977,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
962
977
|
const optional = [
|
|
963
978
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
964
979
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
980
|
+
["fz_github.clientSecret", options.githubApp],
|
|
981
|
+
["fz_github.privateKey", options.githubApp],
|
|
982
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
965
983
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
966
984
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
967
985
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -2908,7 +2908,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2908
2908
|
}
|
|
2909
2909
|
|
|
2910
2910
|
// src/version.ts
|
|
2911
|
-
var VERSION3 = "0.1.
|
|
2911
|
+
var VERSION3 = "0.1.103";
|
|
2912
2912
|
|
|
2913
2913
|
// src/egress-policy.ts
|
|
2914
2914
|
import { realpathSync as realpathSync3 } from "node:fs";
|
|
@@ -4142,10 +4142,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
4142
4142
|
};
|
|
4143
4143
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
4144
4144
|
function validatePlatformInitialInventory(value) {
|
|
4145
|
-
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)) {
|
|
4145
|
+
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)) {
|
|
4146
4146
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
4147
4147
|
}
|
|
4148
4148
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
4149
|
+
if (value.metalIdentity !== undefined) {
|
|
4150
|
+
const identity = value.metalIdentity;
|
|
4151
|
+
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))) {
|
|
4152
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4149
4155
|
if (value.region.city !== undefined)
|
|
4150
4156
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
4151
4157
|
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)))
|
|
@@ -4180,6 +4186,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
4180
4186
|
}
|
|
4181
4187
|
return {
|
|
4182
4188
|
metalHostname: value.metalHostname,
|
|
4189
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
4183
4190
|
region: { ...value.region },
|
|
4184
4191
|
computes: value.computes.map((compute, index) => ({
|
|
4185
4192
|
...computes[index],
|
|
@@ -4233,6 +4240,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
4233
4240
|
} else if (input.email !== undefined) {
|
|
4234
4241
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
4235
4242
|
}
|
|
4243
|
+
if (input.githubApp) {
|
|
4244
|
+
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)) {
|
|
4245
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4236
4248
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
4237
4249
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
4238
4250
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -4363,6 +4375,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
4363
4375
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
4364
4376
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
4365
4377
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
4378
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
4379
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
4380
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
4366
4381
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
4367
4382
|
};
|
|
4368
4383
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -4374,6 +4389,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
4374
4389
|
const optional = [
|
|
4375
4390
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
4376
4391
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
4392
|
+
["fz_github.clientSecret", options.githubApp],
|
|
4393
|
+
["fz_github.privateKey", options.githubApp],
|
|
4394
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
4377
4395
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
4378
4396
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
4379
4397
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -4727,7 +4745,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
4727
4745
|
import { lstatSync as lstatSync5 } from "node:fs";
|
|
4728
4746
|
|
|
4729
4747
|
// src/bootstrap.ts
|
|
4730
|
-
import { createHash as createHash7, createHmac as createHmac2, randomBytes as randomBytes3 } from "node:crypto";
|
|
4748
|
+
import { createHash as createHash7, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "node:crypto";
|
|
4731
4749
|
import {
|
|
4732
4750
|
chmodSync as chmodSync9,
|
|
4733
4751
|
existsSync as existsSync11,
|
|
@@ -15,4 +15,4 @@ export declare function platformGenesisBootstrapConfigs(template: PlatformBootst
|
|
|
15
15
|
branch: 'dev' | 'main';
|
|
16
16
|
revision: string;
|
|
17
17
|
bundleSha256: string;
|
|
18
|
-
}, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation']): readonly PlatformBootstrapConfig[];
|
|
18
|
+
}, attestation?: import('./platform-bootstrap-runtime').PlatformInitialInventory['attestation'], metalIdentity?: NonNullable<import('./platform-bootstrap-runtime').PlatformInitialInventory['metalIdentity']>): readonly PlatformBootstrapConfig[];
|
|
@@ -45,6 +45,11 @@ export interface ForgeZeroLaunchOwnerInput {
|
|
|
45
45
|
from: string;
|
|
46
46
|
eu: boolean;
|
|
47
47
|
};
|
|
48
|
+
githubApp?: {
|
|
49
|
+
clientId: string;
|
|
50
|
+
appId: string;
|
|
51
|
+
slug: string;
|
|
52
|
+
};
|
|
48
53
|
}
|
|
49
54
|
/**
|
|
50
55
|
* ForgeZero's own genesis public coordinates. They are reviewed source data,
|
package/dist/provision.js
CHANGED
|
@@ -2908,7 +2908,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
|
|
|
2908
2908
|
}
|
|
2909
2909
|
|
|
2910
2910
|
// src/version.ts
|
|
2911
|
-
var VERSION3 = "0.1.
|
|
2911
|
+
var VERSION3 = "0.1.103";
|
|
2912
2912
|
|
|
2913
2913
|
// src/egress-policy.ts
|
|
2914
2914
|
import { realpathSync as realpathSync3 } from "node:fs";
|
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.103";
|