@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
|
@@ -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.107";
|
|
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;
|
|
@@ -1384,9 +1384,49 @@ async function buildBootstrapBundle(input, exec = run) {
|
|
|
1384
1384
|
throw cause;
|
|
1385
1385
|
}
|
|
1386
1386
|
}
|
|
1387
|
+
async function refreshBootstrapBundle(input, exec = run) {
|
|
1388
|
+
const outputPath = resolve2(input.outputPath);
|
|
1389
|
+
const manifestPath = `${outputPath}.json`;
|
|
1390
|
+
if (existsSync(outputPath) !== existsSync(manifestPath)) {
|
|
1391
|
+
throw new Error("bootstrap bundle and manifest must either both exist or both be absent");
|
|
1392
|
+
}
|
|
1393
|
+
const candidatePath = `${outputPath}.candidate.${process.pid}.${randomBytes(6).toString("hex")}`;
|
|
1394
|
+
const candidate = await buildBootstrapBundle({ ...input, outputPath: candidatePath }, exec);
|
|
1395
|
+
try {
|
|
1396
|
+
if (!existsSync(outputPath)) {
|
|
1397
|
+
renameSync(candidate.bundlePath, outputPath);
|
|
1398
|
+
renameSync(candidate.manifestPath, manifestPath);
|
|
1399
|
+
return readBootstrapBundle(outputPath);
|
|
1400
|
+
}
|
|
1401
|
+
const current = await readBootstrapBundle(outputPath);
|
|
1402
|
+
if (current.manifest.branch === candidate.manifest.branch && current.manifest.revision === candidate.manifest.revision) {
|
|
1403
|
+
rmSync(candidate.bundlePath, { force: true });
|
|
1404
|
+
rmSync(candidate.manifestPath, { force: true });
|
|
1405
|
+
return current;
|
|
1406
|
+
}
|
|
1407
|
+
const archivePath = `${outputPath}.before-${current.manifest.revision.slice(0, 7)}-${Date.now()}`;
|
|
1408
|
+
const archiveManifestPath = `${archivePath}.json`;
|
|
1409
|
+
renameSync(outputPath, archivePath);
|
|
1410
|
+
renameSync(manifestPath, archiveManifestPath);
|
|
1411
|
+
try {
|
|
1412
|
+
renameSync(candidate.bundlePath, outputPath);
|
|
1413
|
+
renameSync(candidate.manifestPath, manifestPath);
|
|
1414
|
+
return readBootstrapBundle(outputPath);
|
|
1415
|
+
} catch (cause) {
|
|
1416
|
+
rmSync(outputPath, { force: true });
|
|
1417
|
+
rmSync(manifestPath, { force: true });
|
|
1418
|
+
renameSync(archivePath, outputPath);
|
|
1419
|
+
renameSync(archiveManifestPath, manifestPath);
|
|
1420
|
+
throw cause;
|
|
1421
|
+
}
|
|
1422
|
+
} finally {
|
|
1423
|
+
rmSync(candidate.bundlePath, { force: true });
|
|
1424
|
+
rmSync(candidate.manifestPath, { force: true });
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1387
1427
|
|
|
1388
1428
|
// src/bootstrap.ts
|
|
1389
|
-
import { createHash as createHash2, createHmac as createHmac2, randomBytes as randomBytes3 } from "crypto";
|
|
1429
|
+
import { createHash as createHash2, createHmac as createHmac2, createPrivateKey, randomBytes as randomBytes3 } from "crypto";
|
|
1390
1430
|
import {
|
|
1391
1431
|
chmodSync as chmodSync3,
|
|
1392
1432
|
existsSync as existsSync5,
|
|
@@ -1420,7 +1460,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
1420
1460
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
1421
1461
|
|
|
1422
1462
|
// src/version.ts
|
|
1423
|
-
var VERSION = "0.1.
|
|
1463
|
+
var VERSION = "0.1.107";
|
|
1424
1464
|
|
|
1425
1465
|
// src/software.ts
|
|
1426
1466
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -3090,10 +3130,16 @@ var httpsOrigin = (name, raw) => {
|
|
|
3090
3130
|
};
|
|
3091
3131
|
var agentTelemetryOrigin = (raw) => raw === "http://127.0.0.1:4318" ? raw : httpsOrigin("agentOtlpEndpoint", raw);
|
|
3092
3132
|
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)) {
|
|
3133
|
+
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
3134
|
throw new Error("initial platform inventory coordinates are invalid");
|
|
3095
3135
|
}
|
|
3096
3136
|
safeAtom("initialInventory.region.label", value.region.label);
|
|
3137
|
+
if (value.metalIdentity !== undefined) {
|
|
3138
|
+
const identity = value.metalIdentity;
|
|
3139
|
+
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))) {
|
|
3140
|
+
throw new Error("initial platform Metal identity is invalid");
|
|
3141
|
+
}
|
|
3142
|
+
}
|
|
3097
3143
|
if (value.region.city !== undefined)
|
|
3098
3144
|
safeAtom("initialInventory.region.city", value.region.city);
|
|
3099
3145
|
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 +3174,7 @@ function validatePlatformInitialInventory(value) {
|
|
|
3128
3174
|
}
|
|
3129
3175
|
return {
|
|
3130
3176
|
metalHostname: value.metalHostname,
|
|
3177
|
+
...value.metalIdentity ? { metalIdentity: structuredClone(value.metalIdentity) } : {},
|
|
3131
3178
|
region: { ...value.region },
|
|
3132
3179
|
computes: value.computes.map((compute, index) => ({
|
|
3133
3180
|
...computes[index],
|
|
@@ -3181,6 +3228,11 @@ function validatePlatformSharedEnvironment(input) {
|
|
|
3181
3228
|
} else if (input.email !== undefined) {
|
|
3182
3229
|
throw new Error("Bootstrap email provider must be smtp or jetemail.");
|
|
3183
3230
|
}
|
|
3231
|
+
if (input.githubApp) {
|
|
3232
|
+
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)) {
|
|
3233
|
+
throw new Error("GitHub App client id, app id or slug is malformed.");
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3184
3236
|
boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
|
|
3185
3237
|
if (!Array.isArray(input.seedSyncMembers) || input.seedSyncMembers.length < 3 || input.seedSyncMembers.length > 64 || new Set(input.seedSyncMembers).size !== input.seedSyncMembers.length) {
|
|
3186
3238
|
throw new Error("seedSyncMembers must contain 3 to 64 unique physical host identities.");
|
|
@@ -3311,6 +3363,9 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
3311
3363
|
FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
|
|
3312
3364
|
FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
|
|
3313
3365
|
FZ_REALTIME_PRODUCER: value.realtime?.producer ?? "",
|
|
3366
|
+
FZ_GITHUB_CLIENT_ID: value.githubApp?.clientId ?? "",
|
|
3367
|
+
FZ_GITHUB_APP_ID: value.githubApp?.appId ?? "",
|
|
3368
|
+
FZ_GITHUB_APP_SLUG: value.githubApp?.slug ?? "",
|
|
3314
3369
|
FZ_PLATFORM_INITIAL_INVENTORY: value.initialInventory ? JSON.stringify(value.initialInventory) : ""
|
|
3315
3370
|
};
|
|
3316
3371
|
return `# Generated by fz bootstrap platform. Non-secret coordinates only.
|
|
@@ -3322,6 +3377,9 @@ function platformApiCredentialSpecs(options) {
|
|
|
3322
3377
|
const optional = [
|
|
3323
3378
|
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
3324
3379
|
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
3380
|
+
["fz_github.clientSecret", options.githubApp],
|
|
3381
|
+
["fz_github.privateKey", options.githubApp],
|
|
3382
|
+
["fz_github.webhookSecret", options.githubApp],
|
|
3325
3383
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
3326
3384
|
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
3327
3385
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
@@ -3329,6 +3387,7 @@ function platformApiCredentialSpecs(options) {
|
|
|
3329
3387
|
];
|
|
3330
3388
|
return [
|
|
3331
3389
|
{ name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
|
|
3390
|
+
{ name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
|
|
3332
3391
|
{ name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
|
|
3333
3392
|
...optional.filter(([, present]) => present).map(([name]) => ({
|
|
3334
3393
|
name,
|
|
@@ -3717,7 +3776,17 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3717
3776
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
3718
3777
|
throw new Error("bootstrap credential input must be an object");
|
|
3719
3778
|
const source = input;
|
|
3720
|
-
const allowed = [
|
|
3779
|
+
const allowed = [
|
|
3780
|
+
"clusterBootstrapCode",
|
|
3781
|
+
"emailSecret",
|
|
3782
|
+
"enrolmentToken",
|
|
3783
|
+
"backupS3Secret",
|
|
3784
|
+
"cloudflareTunnelToken",
|
|
3785
|
+
"cloudflareApiToken",
|
|
3786
|
+
"githubClientSecret",
|
|
3787
|
+
"githubPrivateKeyBase64",
|
|
3788
|
+
"githubWebhookSecret"
|
|
3789
|
+
];
|
|
3721
3790
|
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
3722
3791
|
if (unknown.length)
|
|
3723
3792
|
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
@@ -3727,6 +3796,9 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3727
3796
|
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
3728
3797
|
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
3729
3798
|
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
3799
|
+
const githubClientSecret = typeof source.githubClientSecret === "string" ? source.githubClientSecret.trim() : undefined;
|
|
3800
|
+
const githubPrivateKeyBase64 = typeof source.githubPrivateKeyBase64 === "string" ? source.githubPrivateKeyBase64.trim() : undefined;
|
|
3801
|
+
const githubWebhookSecret = typeof source.githubWebhookSecret === "string" ? source.githubWebhookSecret.trim() : undefined;
|
|
3730
3802
|
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
3731
3803
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
3732
3804
|
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
@@ -3746,12 +3818,29 @@ function validatePlatformBootstrapSecrets(config, input) {
|
|
|
3746
3818
|
}
|
|
3747
3819
|
if (cloudflareTunnelToken)
|
|
3748
3820
|
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
3821
|
+
const githubConfigured = Boolean(config.runtime.environment.githubApp);
|
|
3822
|
+
if (githubConfigured !== Boolean(githubClientSecret && githubPrivateKeyBase64 && githubWebhookSecret)) {
|
|
3823
|
+
throw new Error("GitHub App coordinates require client secret, private key and webhook secret together");
|
|
3824
|
+
}
|
|
3825
|
+
if (githubConfigured) {
|
|
3826
|
+
if (githubClientSecret.length < 20 || githubClientSecret.length > 512 || /[\r\n\0]/.test(githubClientSecret) || githubWebhookSecret.length < 32 || githubWebhookSecret.length > 512 || /[\r\n\0]/.test(githubWebhookSecret)) {
|
|
3827
|
+
throw new Error("GitHub App client or webhook secret is malformed");
|
|
3828
|
+
}
|
|
3829
|
+
try {
|
|
3830
|
+
const pem = Buffer.from(githubPrivateKeyBase64, "base64").toString("utf8");
|
|
3831
|
+
if (createPrivateKey(pem).asymmetricKeyType !== "rsa")
|
|
3832
|
+
throw new Error("not RSA");
|
|
3833
|
+
} catch {
|
|
3834
|
+
throw new Error("GitHub App private key must be a base64-encoded RSA private key");
|
|
3835
|
+
}
|
|
3836
|
+
}
|
|
3749
3837
|
return {
|
|
3750
3838
|
clusterBootstrapCode,
|
|
3751
3839
|
emailSecret,
|
|
3752
3840
|
...enrolmentToken ? { enrolmentToken } : {},
|
|
3753
3841
|
...backupS3Secret ? { backupS3Secret } : {},
|
|
3754
|
-
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
3842
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {},
|
|
3843
|
+
...githubConfigured ? { githubClientSecret, githubPrivateKeyBase64, githubWebhookSecret } : {}
|
|
3755
3844
|
};
|
|
3756
3845
|
}
|
|
3757
3846
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
@@ -3766,6 +3855,7 @@ var STATE_PATH = BOOTSTRAP_STATE_PATH;
|
|
|
3766
3855
|
var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
|
|
3767
3856
|
var CREDS = "/etc/forgezero/creds";
|
|
3768
3857
|
var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
3858
|
+
var ARANGO_ROOT_CREDENTIAL = `${CREDS}/arangodb-root-password.cred`;
|
|
3769
3859
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
3770
3860
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
3771
3861
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
@@ -4051,7 +4141,7 @@ WantedBy=multi-user.target
|
|
|
4051
4141
|
function databaseVerifyUnit(config) {
|
|
4052
4142
|
const { address } = config.database;
|
|
4053
4143
|
return `[Unit]
|
|
4054
|
-
Description=
|
|
4144
|
+
Description=Secure and verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
|
|
4055
4145
|
Requires=forgezero-db.service
|
|
4056
4146
|
After=forgezero-db.service
|
|
4057
4147
|
PartOf=forgezero-db.service
|
|
@@ -4061,7 +4151,9 @@ Type=oneshot
|
|
|
4061
4151
|
User=arangodb
|
|
4062
4152
|
Group=arangodb
|
|
4063
4153
|
LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
|
|
4064
|
-
|
|
4154
|
+
LoadCredentialEncrypted=arangodb-root-password:${ARANGO_ROOT_CREDENTIAL}
|
|
4155
|
+
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);'
|
|
4156
|
+
ExecStart=/usr/local/bin/fz-agent database-auth-verify --endpoint=http://${unitEscape(address)}:8529
|
|
4065
4157
|
RemainAfterExit=yes
|
|
4066
4158
|
TimeoutStartSec=200
|
|
4067
4159
|
NoNewPrivileges=true
|
|
@@ -4303,6 +4395,12 @@ function approvedPlatformRepairIdentityDigests(config) {
|
|
|
4303
4395
|
delete beforeAttestation.runtime.environment.initialInventory.attestation;
|
|
4304
4396
|
prior.push(beforeAttestation);
|
|
4305
4397
|
}
|
|
4398
|
+
const metalIdentity = config.runtime.environment.initialInventory?.metalIdentity;
|
|
4399
|
+
if (metalIdentity && config.enrolment.source === "genesis-derived") {
|
|
4400
|
+
const beforeMetalIdentity = structuredClone(config);
|
|
4401
|
+
delete beforeMetalIdentity.runtime.environment.initialInventory.metalIdentity;
|
|
4402
|
+
prior.push(beforeMetalIdentity);
|
|
4403
|
+
}
|
|
4306
4404
|
if (config.database.role === "joiner" && config.enrolment.source === "genesis-derived" && config.runtime.environment.custodianEmail) {
|
|
4307
4405
|
const beforeReplicaCustodianEmail = structuredClone(config);
|
|
4308
4406
|
delete beforeReplicaCustodianEmail.runtime.environment.custodianEmail;
|
|
@@ -4522,6 +4620,11 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
4522
4620
|
if (nginx.exitCode !== 0)
|
|
4523
4621
|
problems.push("nginx configuration is invalid");
|
|
4524
4622
|
if (state.databaseRole !== "none") {
|
|
4623
|
+
for (const credential of [JWT_CREDENTIAL, ARANGO_ROOT_CREDENTIAL]) {
|
|
4624
|
+
services[credential] = host.exists(credential);
|
|
4625
|
+
if (!services[credential])
|
|
4626
|
+
problems.push(`${credential} is missing`);
|
|
4627
|
+
}
|
|
4525
4628
|
const unitPath = "/etc/systemd/system/forgezero-db.service";
|
|
4526
4629
|
const expectsNoAgency = state.databaseAgency === "none";
|
|
4527
4630
|
const unitHasNoAgency = host.exists(unitPath) && host.read(unitPath).includes("--cluster.start-agent=false");
|
|
@@ -4660,7 +4763,10 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4660
4763
|
enrolmentToken: checked4.enrolmentToken,
|
|
4661
4764
|
backup: checked4.backupS3Secret,
|
|
4662
4765
|
cloudflareTunnelToken: checked4.cloudflareTunnelToken,
|
|
4663
|
-
cloudflareApiToken: checked4.cloudflareApiToken
|
|
4766
|
+
cloudflareApiToken: checked4.cloudflareApiToken,
|
|
4767
|
+
githubClientSecret: checked4.githubClientSecret,
|
|
4768
|
+
githubPrivateKey: checked4.githubPrivateKeyBase64 ? Buffer.from(checked4.githubPrivateKeyBase64, "base64").toString("utf8") : undefined,
|
|
4769
|
+
githubWebhookSecret: checked4.githubWebhookSecret
|
|
4664
4770
|
};
|
|
4665
4771
|
})() : undefined;
|
|
4666
4772
|
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
@@ -4753,11 +4859,17 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4753
4859
|
if (!host.exists(JWT_CREDENTIAL)) {
|
|
4754
4860
|
await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
|
|
4755
4861
|
}
|
|
4862
|
+
if (!host.exists(ARANGO_ROOT_CREDENTIAL)) {
|
|
4863
|
+
await seal(host, "arangodb-root-password", ARANGO_ROOT_CREDENTIAL, `fzr_${derive(root, "forgezero/cluster/arangodb-root-password/v1")}`);
|
|
4864
|
+
}
|
|
4756
4865
|
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
4757
4866
|
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
4758
4867
|
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
4759
4868
|
for (const [name, source] of Object.entries({
|
|
4760
4869
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
4870
|
+
"fz_github.clientSecret": platformPrivate.githubClientSecret,
|
|
4871
|
+
"fz_github.privateKey": platformPrivate.githubPrivateKey,
|
|
4872
|
+
"fz_github.webhookSecret": platformPrivate.githubWebhookSecret,
|
|
4761
4873
|
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
4762
4874
|
})) {
|
|
4763
4875
|
if (source) {
|
|
@@ -4774,6 +4886,7 @@ async function applyBootstrap(input, host = localBootstrapHost(), secrets, depen
|
|
|
4774
4886
|
const credentials = platformApiCredentialSpecs({
|
|
4775
4887
|
emailProvider: runtime.environment.email?.provider,
|
|
4776
4888
|
cloudflareKv: cloudflareConfigured,
|
|
4889
|
+
githubApp: Boolean(runtime.environment.githubApp),
|
|
4777
4890
|
realtime: Boolean(runtime.environment.realtime)
|
|
4778
4891
|
});
|
|
4779
4892
|
const units = renderPlatformApiUnits({
|
|
@@ -5032,7 +5145,11 @@ function strictBootstrapDocument(value) {
|
|
|
5032
5145
|
], "runtime environment");
|
|
5033
5146
|
const environment = runtime.environment;
|
|
5034
5147
|
if (environment.initialInventory !== undefined) {
|
|
5035
|
-
const inventory = exactKeys(environment.initialInventory, ["metalHostname", "region", "computes", "attestation", "deployment"], "initial inventory");
|
|
5148
|
+
const inventory = exactKeys(environment.initialInventory, ["metalHostname", "metalIdentity", "region", "computes", "attestation", "deployment"], "initial inventory");
|
|
5149
|
+
if (inventory.metalIdentity !== undefined) {
|
|
5150
|
+
const identity = exactKeys(inventory.metalIdentity, ["nodeKey", "publicKeys"], "initial Metal identity");
|
|
5151
|
+
exactKeys(identity.publicKeys, ["ed25519", "mlDsa"], "initial Metal public keys");
|
|
5152
|
+
}
|
|
5036
5153
|
exactKeys(inventory.region, ["key", "label", "country", "city", "confidentialCapable"], "initial inventory region");
|
|
5037
5154
|
if (inventory.attestation !== undefined) {
|
|
5038
5155
|
const attestation = exactKeys(inventory.attestation, ["measurement", "tcbFloor"], "initial attestation evidence");
|
|
@@ -5501,7 +5618,7 @@ var SUPPORTED_BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dc
|
|
|
5501
5618
|
|
|
5502
5619
|
class MetalBootstrapError extends Error {
|
|
5503
5620
|
}
|
|
5504
|
-
function
|
|
5621
|
+
function parseMetalPublicIdentity(value) {
|
|
5505
5622
|
let parsed;
|
|
5506
5623
|
try {
|
|
5507
5624
|
parsed = JSON.parse(value);
|
|
@@ -5516,7 +5633,13 @@ function normalizeMetalPublicIdentity(value) {
|
|
|
5516
5633
|
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
5634
|
throw new MetalBootstrapError("metal public identity proof was invalid");
|
|
5518
5635
|
}
|
|
5519
|
-
return
|
|
5636
|
+
return {
|
|
5637
|
+
nodeKey: identity.nodeKey,
|
|
5638
|
+
publicKeys: { ed25519: keys.ed25519, mlDsa: keys.mlDsa }
|
|
5639
|
+
};
|
|
5640
|
+
}
|
|
5641
|
+
function normalizeMetalPublicIdentity(value) {
|
|
5642
|
+
return JSON.stringify(parseMetalPublicIdentity(value));
|
|
5520
5643
|
}
|
|
5521
5644
|
var defaultExec2 = async (argv, stdin) => {
|
|
5522
5645
|
const child = Bun.spawn([...argv], {
|
|
@@ -6412,7 +6535,7 @@ function writeOperatorPlatformBootstrapFleetRequest(path, requestFiles, cloudfla
|
|
|
6412
6535
|
return writeOwnerJson2(path, request, "operator fleet request");
|
|
6413
6536
|
}
|
|
6414
6537
|
var validateMetalRequestValue = (value, options = {}) => {
|
|
6415
|
-
const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "genesis"], "operator metal request");
|
|
6538
|
+
const root = exactKeys3(value, ["kind", "target", "metalConfigFile", "metalIdentityEvidenceFile", "genesis"], "operator metal request");
|
|
6416
6539
|
if (root.kind !== "metal-remote")
|
|
6417
6540
|
throw new Error("operator metal bootstrap kind must be metal-remote");
|
|
6418
6541
|
const targetValue = exactKeys3(root.target, ["address", "port", "user", "hostKey", "hostKeySha256", "identityPublicKeyFile", "agentSocket"], "operator metal target");
|
|
@@ -6431,6 +6554,12 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
6431
6554
|
if (typeof root.metalConfigFile !== "string")
|
|
6432
6555
|
throw new Error("metalConfigFile must be an absolute path");
|
|
6433
6556
|
canonicalOutputPath(root.metalConfigFile, "metalConfigFile");
|
|
6557
|
+
if (root.metalIdentityEvidenceFile !== undefined && typeof root.metalIdentityEvidenceFile !== "string") {
|
|
6558
|
+
throw new Error("metalIdentityEvidenceFile must be an absolute path");
|
|
6559
|
+
}
|
|
6560
|
+
if (typeof root.metalIdentityEvidenceFile === "string") {
|
|
6561
|
+
canonicalOutputPath(root.metalIdentityEvidenceFile, "metalIdentityEvidenceFile");
|
|
6562
|
+
}
|
|
6434
6563
|
if (options.validateMetalConfig !== false)
|
|
6435
6564
|
readMetalBootstrapConfig(root.metalConfigFile);
|
|
6436
6565
|
const genesis = exactKeys3(root.genesis, ["nodes", "rehearsalNode", "sshPublicKeyFiles"], "operator metal genesis");
|
|
@@ -6479,6 +6608,7 @@ var validateMetalRequestValue = (value, options = {}) => {
|
|
|
6479
6608
|
return {
|
|
6480
6609
|
kind: "metal-remote",
|
|
6481
6610
|
metalConfigFile: root.metalConfigFile,
|
|
6611
|
+
...typeof root.metalIdentityEvidenceFile === "string" ? { metalIdentityEvidenceFile: root.metalIdentityEvidenceFile } : {},
|
|
6482
6612
|
target: {
|
|
6483
6613
|
...target,
|
|
6484
6614
|
identityPublicKeyFile: targetValue.identityPublicKeyFile,
|
|
@@ -6499,6 +6629,25 @@ function writeOperatorMetalBootstrapRequest(path, request) {
|
|
|
6499
6629
|
validateMetalRequestValue(request);
|
|
6500
6630
|
return writeOwnerJson2(path, request, "operator metal request");
|
|
6501
6631
|
}
|
|
6632
|
+
function readOperatorMetalIdentityEvidence(path, request) {
|
|
6633
|
+
const value = JSON.parse(new TextDecoder().decode(ownerFile(path, REQUEST_LIMIT, "operator metal identity evidence")));
|
|
6634
|
+
const root = exactKeys3(value, ["format", "kind", "metalHostname", "metalHostKeySha256", "nodeKey", "publicKeys"], "operator metal identity evidence");
|
|
6635
|
+
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") {
|
|
6636
|
+
throw new Error("operator metal identity evidence is malformed");
|
|
6637
|
+
}
|
|
6638
|
+
const identity = parseMetalPublicIdentity(JSON.stringify({ nodeKey: root.nodeKey, publicKeys: root.publicKeys }));
|
|
6639
|
+
const config = readMetalBootstrapConfig(request.metalConfigFile, { allowHistoricalRelease: true });
|
|
6640
|
+
if (root.metalHostname !== config.metalHostname || root.metalHostKeySha256 !== request.target.hostKeySha256) {
|
|
6641
|
+
throw new Error("operator metal identity evidence does not match the reviewed Metal target");
|
|
6642
|
+
}
|
|
6643
|
+
return {
|
|
6644
|
+
format: 1,
|
|
6645
|
+
kind: "forgezero-operator-metal-identity",
|
|
6646
|
+
metalHostname: root.metalHostname,
|
|
6647
|
+
metalHostKeySha256: root.metalHostKeySha256,
|
|
6648
|
+
...identity
|
|
6649
|
+
};
|
|
6650
|
+
}
|
|
6502
6651
|
var validateGuestHostKeyEvidenceValue = (value, request) => {
|
|
6503
6652
|
const root = exactKeys3(value, ["format", "kind", "metalHostKeySha256", "nodes"], "operator guest host-key evidence");
|
|
6504
6653
|
if (root.format !== 1 || root.kind !== "forgezero-operator-guest-host-keys" || typeof root.metalHostKeySha256 !== "string" || !Array.isArray(root.nodes)) {
|
|
@@ -7279,6 +7428,25 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
7279
7428
|
`${REMOTE_STAGE}/metal-config.json`,
|
|
7280
7429
|
"--apply"
|
|
7281
7430
|
], "remote typed metal bootstrap");
|
|
7431
|
+
if (request.metalIdentityEvidenceFile) {
|
|
7432
|
+
let applied;
|
|
7433
|
+
try {
|
|
7434
|
+
applied = JSON.parse(output);
|
|
7435
|
+
} catch {
|
|
7436
|
+
throw new Error("remote Metal bootstrap did not return identity evidence");
|
|
7437
|
+
}
|
|
7438
|
+
const publicIdentity2 = applied?.publicIdentity;
|
|
7439
|
+
if (typeof publicIdentity2 !== "string")
|
|
7440
|
+
throw new Error("remote Metal bootstrap omitted its public identity");
|
|
7441
|
+
const identity = parseMetalPublicIdentity(publicIdentity2);
|
|
7442
|
+
writeOwnerJson2(request.metalIdentityEvidenceFile, {
|
|
7443
|
+
format: 1,
|
|
7444
|
+
kind: "forgezero-operator-metal-identity",
|
|
7445
|
+
metalHostname: config.metalHostname,
|
|
7446
|
+
metalHostKeySha256: request.target.hostKeySha256,
|
|
7447
|
+
...identity
|
|
7448
|
+
}, "operator metal identity evidence");
|
|
7449
|
+
}
|
|
7282
7450
|
return { plan, output };
|
|
7283
7451
|
} finally {
|
|
7284
7452
|
if (knownHosts) {
|
|
@@ -7299,6 +7467,7 @@ export {
|
|
|
7299
7467
|
redactOperatorSecretDiagnostic,
|
|
7300
7468
|
readOperatorPlatformBootstrapRequest,
|
|
7301
7469
|
readOperatorPlatformBootstrapFleetRequest,
|
|
7470
|
+
readOperatorMetalIdentityEvidence,
|
|
7302
7471
|
readOperatorMetalBootstrapRequest,
|
|
7303
7472
|
readOperatorGuestHostKeyEvidence,
|
|
7304
7473
|
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' | 'arangodb-root-password' | '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],
|
|
@@ -969,6 +987,7 @@ function platformApiCredentialSpecs(options) {
|
|
|
969
987
|
];
|
|
970
988
|
return [
|
|
971
989
|
{ name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
|
|
990
|
+
{ name: "arangodb-root-password", encryptedPath: "/etc/forgezero/creds/arangodb-root-password.cred", required: true },
|
|
972
991
|
{ name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
|
|
973
992
|
...optional.filter(([, present]) => present).map(([name]) => ({
|
|
974
993
|
name,
|