@forgezero/agent 0.1.58 → 0.1.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -12
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +22 -11
- package/dist/bootstrap.js +193 -156
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -7
- package/dist/cloudflare-bootstrap.d.ts +10 -29
- package/dist/cloudflare-bootstrap.js +50 -78
- package/dist/cloudflare-edge.d.ts +10 -0
- package/dist/cloudflare-edge.js +13 -0
- package/dist/credential-schema.d.ts +9 -9
- package/dist/credential-schema.js +7 -7
- package/dist/fz-agent.js +27 -22
- package/dist/fz.js +330 -282
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +5 -2
- package/dist/operator-bootstrap.js +226 -181
- package/dist/platform-bootstrap-runtime.d.ts +1 -1
- package/dist/platform-bootstrap-runtime.js +3 -2
- package/dist/platform-fleet-verification.js +21 -14
- package/dist/platform-genesis.d.ts +2 -0
- package/dist/provision.js +12 -6
- package/dist/recovery-host.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/fz.js
CHANGED
|
@@ -4811,8 +4811,7 @@ async function spawnWith(command, env, report = () => {}, options = {}) {
|
|
|
4811
4811
|
|
|
4812
4812
|
// src/cli/index.ts
|
|
4813
4813
|
init_dist();
|
|
4814
|
-
import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14,
|
|
4815
|
-
import { randomBytes as randomBytes10 } from "crypto";
|
|
4814
|
+
import { existsSync as existsSync11, lstatSync as lstatSync9, mkdirSync as mkdirSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync12 } from "fs";
|
|
4816
4815
|
import { basename as basename3, dirname as dirname12, isAbsolute as isAbsolute5, resolve as resolve9 } from "path";
|
|
4817
4816
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
4818
4817
|
import { hostname } from "os";
|
|
@@ -4830,7 +4829,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
|
|
|
4830
4829
|
var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
|
|
4831
4830
|
|
|
4832
4831
|
// src/version.ts
|
|
4833
|
-
var VERSION2 = "0.1.
|
|
4832
|
+
var VERSION2 = "0.1.60";
|
|
4834
4833
|
|
|
4835
4834
|
// src/software.ts
|
|
4836
4835
|
var PINNED_BUN_VERSION = "1.3.14";
|
|
@@ -6099,10 +6098,11 @@ function planProvision(options) {
|
|
|
6099
6098
|
const warpEnabled = warpValues.every(Boolean);
|
|
6100
6099
|
if (warpValues.some(Boolean) && !warpEnabled)
|
|
6101
6100
|
throw new Error("WARP configuration must be supplied together");
|
|
6102
|
-
const enrolmentEnabled = Boolean(options.
|
|
6103
|
-
if (Boolean(options.
|
|
6104
|
-
throw new Error("direct enrolment paths must be supplied together");
|
|
6105
|
-
|
|
6101
|
+
const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
|
|
6102
|
+
if (Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath) || options.enrolTokenSourcePath && !enrolmentEnabled) {
|
|
6103
|
+
throw new Error("direct enrolment credential and state paths must be supplied together");
|
|
6104
|
+
}
|
|
6105
|
+
const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
|
|
6106
6106
|
const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
|
|
6107
6107
|
const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
|
|
6108
6108
|
const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
|
|
@@ -6230,7 +6230,12 @@ function planProvision(options) {
|
|
|
6230
6230
|
] : [],
|
|
6231
6231
|
...enrolmentEnabled ? [
|
|
6232
6232
|
step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
|
|
6233
|
-
step("encrypted one-time enrolment capability", {
|
|
6233
|
+
...enrolTokenSourcePath ? [step("encrypted one-time enrolment capability", {
|
|
6234
|
+
kind: "ensure-enrolment",
|
|
6235
|
+
state: enrolStatePath,
|
|
6236
|
+
source: enrolTokenSourcePath,
|
|
6237
|
+
credential: enrolTokenCredentialPath
|
|
6238
|
+
})] : []
|
|
6234
6239
|
] : [],
|
|
6235
6240
|
...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
|
|
6236
6241
|
{ path: deployRoot, mode: 493, owner: "root", group: "root" },
|
|
@@ -11550,7 +11555,7 @@ function removeSession(api, realm, path = defaultSessionPath()) {
|
|
|
11550
11555
|
}
|
|
11551
11556
|
|
|
11552
11557
|
// src/bootstrap.ts
|
|
11553
|
-
import { createHash as createHash3, createHmac, randomBytes as
|
|
11558
|
+
import { createHash as createHash3, createHmac as createHmac2, randomBytes as randomBytes6 } from "crypto";
|
|
11554
11559
|
import {
|
|
11555
11560
|
chmodSync as chmodSync3,
|
|
11556
11561
|
existsSync as existsSync8,
|
|
@@ -11761,9 +11766,10 @@ function renderPlatformSharedEnvironment(input) {
|
|
|
11761
11766
|
}
|
|
11762
11767
|
function platformApiCredentialSpecs(options) {
|
|
11763
11768
|
const optional = [
|
|
11764
|
-
["
|
|
11765
|
-
["
|
|
11769
|
+
["fz_smtp.password", options.emailProvider === "smtp"],
|
|
11770
|
+
["fz_jetemail.apiKey", options.emailProvider === "jetemail"],
|
|
11766
11771
|
["CF_API_TOKEN", options.cloudflareKv],
|
|
11772
|
+
["CF_TUNNEL_TOKEN", options.cloudflareKv],
|
|
11767
11773
|
["REALTIME_PUBLISH_SECRET", options.realtime],
|
|
11768
11774
|
["REALTIME_TICKET_SECRET", options.realtime]
|
|
11769
11775
|
];
|
|
@@ -11937,7 +11943,7 @@ function planLocalOtlpProof(endpoint, collectorUnit) {
|
|
|
11937
11943
|
|
|
11938
11944
|
// src/cloudflare-bootstrap.ts
|
|
11939
11945
|
import { constants } from "fs";
|
|
11940
|
-
import {
|
|
11946
|
+
import { createHmac, randomUUID } from "crypto";
|
|
11941
11947
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
|
|
11942
11948
|
import { dirname as dirname7, join as join6, resolve as resolve5 } from "path";
|
|
11943
11949
|
import { isIP as isIP3 } from "net";
|
|
@@ -12218,13 +12224,23 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
|
12218
12224
|
}
|
|
12219
12225
|
return { tunnel, connectorToken, created };
|
|
12220
12226
|
}
|
|
12227
|
+
async function retrieveCloudflareConnectorTokens(config, fetcher = fetch) {
|
|
12228
|
+
if (!/^[a-f0-9]{32}$/i.test(config.accountId) || !/^[0-9a-f-]{36}$/i.test(config.tunnelId) || config.meshConnectorId && !/^[0-9a-f-]{36}$/i.test(config.meshConnectorId)) {
|
|
12229
|
+
throw new Error("Cloudflare connector retrieval coordinates are invalid");
|
|
12230
|
+
}
|
|
12231
|
+
const connectorToken = await cf(config, `/accounts/${config.accountId}/cfd_tunnel/${encodeURIComponent(config.tunnelId)}/token`, {}, fetcher);
|
|
12232
|
+
const meshConnectorToken = config.meshConnectorId ? await cf(config, `/accounts/${config.accountId}/warp_connector/${encodeURIComponent(config.meshConnectorId)}/token`, {}, fetcher) : undefined;
|
|
12233
|
+
for (const value of [connectorToken, meshConnectorToken]) {
|
|
12234
|
+
if (value !== undefined && (!value || value.length > 16384))
|
|
12235
|
+
throw new Error("Cloudflare returned an invalid connector token");
|
|
12236
|
+
}
|
|
12237
|
+
return { connectorToken, ...meshConnectorToken ? { meshConnectorToken } : {} };
|
|
12238
|
+
}
|
|
12221
12239
|
|
|
12222
12240
|
// src/cloudflare-bootstrap.ts
|
|
12223
12241
|
var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
|
|
12224
|
-
var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12225
12242
|
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12226
12243
|
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
12227
|
-
var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
|
|
12228
12244
|
var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
12229
12245
|
async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
|
|
12230
12246
|
const metadata = await handle.stat();
|
|
@@ -12258,26 +12274,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
|
|
|
12258
12274
|
await handle?.close();
|
|
12259
12275
|
}
|
|
12260
12276
|
}
|
|
12261
|
-
|
|
12262
|
-
|
|
12263
|
-
|
|
12264
|
-
|
|
12265
|
-
|
|
12266
|
-
}
|
|
12267
|
-
async function readCloudflareBootstrapTokens(files) {
|
|
12268
|
-
const unsupported = Object.keys(files).filter((key) => ![
|
|
12269
|
-
"tunnelTokenFile",
|
|
12270
|
-
"apiTokenFile"
|
|
12271
|
-
].includes(key));
|
|
12277
|
+
function validateCloudflareBootstrapTokens(input) {
|
|
12278
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
12279
|
+
throw new Error("Cloudflare bootstrap tokens must be an object");
|
|
12280
|
+
const source = input;
|
|
12281
|
+
const unsupported = Object.keys(source).filter((key) => !["tunnelToken", "apiToken"].includes(key));
|
|
12272
12282
|
if (unsupported.length)
|
|
12273
|
-
throw new Error(`Cloudflare bootstrap
|
|
12274
|
-
|
|
12275
|
-
|
|
12276
|
-
|
|
12277
|
-
|
|
12278
|
-
readOwnerApiToken(files.tunnelTokenFile),
|
|
12279
|
-
readOwnerApiToken(files.apiTokenFile)
|
|
12280
|
-
]);
|
|
12283
|
+
throw new Error(`Cloudflare bootstrap tokens contain unsupported field ${unsupported[0]}`);
|
|
12284
|
+
const tunnelToken = typeof source.tunnelToken === "string" ? source.tunnelToken.trim() : "";
|
|
12285
|
+
const apiToken = typeof source.apiToken === "string" ? source.apiToken.trim() : "";
|
|
12286
|
+
if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken))
|
|
12287
|
+
throw new Error("Cloudflare bootstrap requires two valid API tokens");
|
|
12281
12288
|
if (tunnelToken === apiToken) {
|
|
12282
12289
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
|
|
12283
12290
|
}
|
|
@@ -12453,7 +12460,7 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
12453
12460
|
return {
|
|
12454
12461
|
format: 1,
|
|
12455
12462
|
kind: "forgezero-cloudflare-bootstrap-plan",
|
|
12456
|
-
mode: "attended-
|
|
12463
|
+
mode: "attended-hidden-input",
|
|
12457
12464
|
outputFile: resolve5(outputPath),
|
|
12458
12465
|
coordinates,
|
|
12459
12466
|
operations: [
|
|
@@ -12461,19 +12468,19 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
12461
12468
|
...coordinates.realtime ? [
|
|
12462
12469
|
"prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
|
|
12463
12470
|
] : [],
|
|
12464
|
-
"create or reuse one remotely-managed Tunnel per node and checkpoint
|
|
12471
|
+
"create or reuse one remotely-managed Tunnel per node and checkpoint only its non-secret id",
|
|
12465
12472
|
...coordinates.nodes.some(({ mesh }) => mesh) ? [
|
|
12466
|
-
"create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its
|
|
12473
|
+
"create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint only its non-secret id",
|
|
12467
12474
|
"reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
|
|
12468
12475
|
] : [],
|
|
12469
12476
|
"preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
|
|
12470
12477
|
"reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
|
|
12471
|
-
"write one
|
|
12478
|
+
"write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
|
|
12472
12479
|
],
|
|
12473
12480
|
secrets: [
|
|
12474
|
-
"API tokens are
|
|
12475
|
-
"
|
|
12476
|
-
"the
|
|
12481
|
+
"API tokens are accepted only from a hidden prompt and are never placed in JSON, files, argv or stdout",
|
|
12482
|
+
"checkpoint and host handoffs contain no connector, management, runtime or realtime secret",
|
|
12483
|
+
"the target Agent retrieves its connector tokens and seals both Vault fallback tokens directly"
|
|
12477
12484
|
]
|
|
12478
12485
|
};
|
|
12479
12486
|
}
|
|
@@ -12538,25 +12545,15 @@ async function readExistingOutput(path) {
|
|
|
12538
12545
|
const resources = output.resources;
|
|
12539
12546
|
const unsupportedResource = Object.keys(resources).filter((key) => ![
|
|
12540
12547
|
"kvNamespaceId",
|
|
12541
|
-
"apiToken",
|
|
12542
|
-
"realtime",
|
|
12543
12548
|
"nodes"
|
|
12544
12549
|
].includes(key));
|
|
12545
12550
|
if (unsupportedResource.length) {
|
|
12546
12551
|
throw new Error(`${resolve5(path)} resources contain unsupported field ${unsupportedResource[0]}`);
|
|
12547
12552
|
}
|
|
12548
12553
|
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
12549
|
-
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !
|
|
12554
|
+
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
|
|
12550
12555
|
throw new Error(`${resolve5(path)} has malformed Cloudflare bootstrap resources`);
|
|
12551
12556
|
}
|
|
12552
|
-
if (coordinates.realtime) {
|
|
12553
|
-
const realtime = resources.realtime;
|
|
12554
|
-
if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
|
|
12555
|
-
throw new Error(`${resolve5(path)} has malformed Cloudflare realtime resources`);
|
|
12556
|
-
}
|
|
12557
|
-
} else if (resources.realtime !== undefined) {
|
|
12558
|
-
throw new Error(`${resolve5(path)} contains undeclared Cloudflare realtime resources`);
|
|
12559
|
-
}
|
|
12560
12557
|
const seen = new Set;
|
|
12561
12558
|
for (const item of resources.nodes) {
|
|
12562
12559
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -12569,16 +12566,15 @@ async function readExistingOutput(path) {
|
|
|
12569
12566
|
"service",
|
|
12570
12567
|
"tunnelName",
|
|
12571
12568
|
"tunnelId",
|
|
12572
|
-
"connectorToken",
|
|
12573
12569
|
"mesh"
|
|
12574
12570
|
].includes(key));
|
|
12575
12571
|
const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
|
|
12576
|
-
if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? ""))
|
|
12572
|
+
if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? ""))) {
|
|
12577
12573
|
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare node resource`);
|
|
12578
12574
|
}
|
|
12579
12575
|
if (expected.mesh) {
|
|
12580
12576
|
const mesh = node.mesh;
|
|
12581
|
-
if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? ""))
|
|
12577
|
+
if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? ""))) {
|
|
12582
12578
|
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare Mesh resource`);
|
|
12583
12579
|
}
|
|
12584
12580
|
} else if (node.mesh !== undefined) {
|
|
@@ -12622,11 +12618,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12622
12618
|
"hostname",
|
|
12623
12619
|
"service",
|
|
12624
12620
|
"tunnelId",
|
|
12625
|
-
"connectorToken",
|
|
12626
12621
|
"accountId",
|
|
12627
12622
|
"zoneId",
|
|
12628
12623
|
"kvNamespaceId",
|
|
12629
|
-
"apiToken",
|
|
12630
12624
|
"mesh",
|
|
12631
12625
|
"realtime"
|
|
12632
12626
|
].includes(key));
|
|
@@ -12637,11 +12631,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12637
12631
|
try {
|
|
12638
12632
|
service = normalizeService(output.service ?? "");
|
|
12639
12633
|
} catch {}
|
|
12640
|
-
if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") ||
|
|
12634
|
+
if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
|
|
12641
12635
|
throw new Error("Cloudflare host handoff is malformed or belongs to another node");
|
|
12642
12636
|
}
|
|
12643
12637
|
if (output.mesh !== undefined) {
|
|
12644
|
-
if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !
|
|
12638
|
+
if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
|
|
12645
12639
|
try {
|
|
12646
12640
|
return privateMeshCidr(route) !== route;
|
|
12647
12641
|
} catch {
|
|
@@ -12670,7 +12664,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12670
12664
|
} catch {
|
|
12671
12665
|
throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
|
|
12672
12666
|
}
|
|
12673
|
-
if (
|
|
12667
|
+
if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
|
|
12674
12668
|
throw new Error("Cloudflare host handoff contains malformed realtime credentials");
|
|
12675
12669
|
}
|
|
12676
12670
|
}
|
|
@@ -12735,18 +12729,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
12735
12729
|
hostname: node.hostname,
|
|
12736
12730
|
service: node.service,
|
|
12737
12731
|
tunnelId: node.tunnelId,
|
|
12738
|
-
connectorToken: node.connectorToken,
|
|
12739
12732
|
accountId: output.coordinates.accountId,
|
|
12740
12733
|
zoneId: output.coordinates.zoneId,
|
|
12741
12734
|
kvNamespaceId: output.resources.kvNamespaceId,
|
|
12742
|
-
|
|
12743
|
-
...output.coordinates.realtime && output.resources.realtime ? { realtime: {
|
|
12744
|
-
...output.coordinates.realtime,
|
|
12745
|
-
...output.resources.realtime
|
|
12746
|
-
} } : {},
|
|
12735
|
+
...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
|
|
12747
12736
|
...node.mesh ? { mesh: {
|
|
12748
12737
|
connectorId: node.mesh.connectorId,
|
|
12749
|
-
connectorToken: node.mesh.connectorToken,
|
|
12750
12738
|
routes: node.mesh.routes
|
|
12751
12739
|
} } : {}
|
|
12752
12740
|
};
|
|
@@ -12754,7 +12742,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
12754
12742
|
}
|
|
12755
12743
|
}
|
|
12756
12744
|
var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
12757
|
-
var
|
|
12745
|
+
var deriveCloudflareRealtimeSecrets = (apiToken) => ({
|
|
12746
|
+
publishSecret: createHmac("sha512", apiToken).update("forgezero/realtime/publish/v1").digest("base64url"),
|
|
12747
|
+
ticketSecret: createHmac("sha512", apiToken).update("forgezero/realtime/ticket/v1").digest("base64url")
|
|
12748
|
+
});
|
|
12758
12749
|
async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
|
|
12759
12750
|
const coordinates = validateCloudflareBootstrapCoordinates(input);
|
|
12760
12751
|
const absoluteOutput = resolve5(outputPath);
|
|
@@ -12772,10 +12763,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12772
12763
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
|
|
12773
12764
|
}
|
|
12774
12765
|
await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
|
|
12775
|
-
const realtime = coordinates.realtime ?
|
|
12776
|
-
publishSecret: newRealtimeSecret(),
|
|
12777
|
-
ticketSecret: newRealtimeSecret()
|
|
12778
|
-
} : undefined;
|
|
12766
|
+
const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
|
|
12779
12767
|
if (coordinates.realtime && realtime) {
|
|
12780
12768
|
await verifyCloudflareWorkerDurableObjects({
|
|
12781
12769
|
accountId: coordinates.accountId,
|
|
@@ -12790,8 +12778,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12790
12778
|
coordinates,
|
|
12791
12779
|
resources: {
|
|
12792
12780
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12793
|
-
apiToken,
|
|
12794
|
-
realtime,
|
|
12795
12781
|
nodes: existing?.resources.nodes ?? []
|
|
12796
12782
|
}
|
|
12797
12783
|
});
|
|
@@ -12810,7 +12796,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12810
12796
|
let resource;
|
|
12811
12797
|
let created = false;
|
|
12812
12798
|
if (checkpointed) {
|
|
12813
|
-
if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)
|
|
12799
|
+
if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)) {
|
|
12814
12800
|
throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
|
|
12815
12801
|
}
|
|
12816
12802
|
resource = checkpointed;
|
|
@@ -12831,15 +12817,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12831
12817
|
}, fetcher);
|
|
12832
12818
|
mesh = {
|
|
12833
12819
|
...node.mesh,
|
|
12834
|
-
connectorId: ensured.connector.id
|
|
12835
|
-
connectorToken: ensured.connectorToken
|
|
12820
|
+
connectorId: ensured.connector.id
|
|
12836
12821
|
};
|
|
12837
12822
|
}
|
|
12838
12823
|
const { mesh: _declaredMesh, ...publicNode } = node;
|
|
12839
12824
|
resource = {
|
|
12840
12825
|
...publicNode,
|
|
12841
12826
|
tunnelId: tunnel.tunnel.id,
|
|
12842
|
-
connectorToken: tunnel.connectorToken,
|
|
12843
12827
|
...mesh ? { mesh } : {}
|
|
12844
12828
|
};
|
|
12845
12829
|
}
|
|
@@ -12853,8 +12837,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12853
12837
|
coordinates,
|
|
12854
12838
|
resources: {
|
|
12855
12839
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12856
|
-
apiToken,
|
|
12857
|
-
...realtime ? { realtime } : {},
|
|
12858
12840
|
nodes: [...nodeResources]
|
|
12859
12841
|
}
|
|
12860
12842
|
});
|
|
@@ -12898,8 +12880,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12898
12880
|
coordinates,
|
|
12899
12881
|
resources: {
|
|
12900
12882
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12901
|
-
apiToken,
|
|
12902
|
-
...realtime ? { realtime } : {},
|
|
12903
12883
|
nodes: nodeResources
|
|
12904
12884
|
},
|
|
12905
12885
|
created: {
|
|
@@ -12921,10 +12901,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
|
|
|
12921
12901
|
nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
|
|
12922
12902
|
};
|
|
12923
12903
|
}
|
|
12924
|
-
if (!request.
|
|
12925
|
-
throw new Error("Cloudflare apply requires exactly two
|
|
12904
|
+
if (!request.tokens) {
|
|
12905
|
+
throw new Error("Cloudflare apply requires exactly two attended API tokens");
|
|
12926
12906
|
}
|
|
12927
|
-
const tokens =
|
|
12907
|
+
const tokens = validateCloudflareBootstrapTokens(request.tokens);
|
|
12928
12908
|
const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
|
|
12929
12909
|
return {
|
|
12930
12910
|
format: 1,
|
|
@@ -13182,6 +13162,78 @@ var PLATFORM_BOOTSTRAP_PROFILES = [
|
|
|
13182
13162
|
"platform-db-api",
|
|
13183
13163
|
"platform-api"
|
|
13184
13164
|
];
|
|
13165
|
+
function validateCloudflareBootstrapSecretPair(input) {
|
|
13166
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
13167
|
+
throw new Error("Cloudflare credential input must be an object");
|
|
13168
|
+
const source = input;
|
|
13169
|
+
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : "";
|
|
13170
|
+
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : "";
|
|
13171
|
+
if (!/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareTunnelToken) || !/^[A-Za-z0-9._-]{40,80}$/.test(cloudflareApiToken) || cloudflareTunnelToken === cloudflareApiToken) {
|
|
13172
|
+
throw new Error("Cloudflare bootstrap credentials must be distinct valid API tokens");
|
|
13173
|
+
}
|
|
13174
|
+
return { cloudflareTunnelToken, cloudflareApiToken };
|
|
13175
|
+
}
|
|
13176
|
+
function validateEnrolledComputeBootstrapSecrets(config, input) {
|
|
13177
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
13178
|
+
throw new Error("compute activation credential input must be an object");
|
|
13179
|
+
const source = input;
|
|
13180
|
+
const allowed = ["enrolmentToken", "cloudflareTunnelToken", "cloudflareApiToken"];
|
|
13181
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
13182
|
+
if (unknown.length)
|
|
13183
|
+
throw new Error(`compute activation credential input contains unsupported field ${unknown[0]}`);
|
|
13184
|
+
const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : "";
|
|
13185
|
+
if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken))
|
|
13186
|
+
throw new Error("compute enrolment token is malformed");
|
|
13187
|
+
const cloudflareConfigured = Boolean(config.cloudflareHandoff);
|
|
13188
|
+
if (cloudflareConfigured !== Boolean(source.cloudflareTunnelToken && source.cloudflareApiToken)) {
|
|
13189
|
+
throw new Error("Cloudflare compute activation requires CF_TUNNEL_TOKEN and CF_API_TOKEN together");
|
|
13190
|
+
}
|
|
13191
|
+
return {
|
|
13192
|
+
enrolmentToken,
|
|
13193
|
+
...cloudflareConfigured ? validateCloudflareBootstrapSecretPair(source) : {}
|
|
13194
|
+
};
|
|
13195
|
+
}
|
|
13196
|
+
function validatePlatformBootstrapSecrets(config, input) {
|
|
13197
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
13198
|
+
throw new Error("bootstrap credential input must be an object");
|
|
13199
|
+
const source = input;
|
|
13200
|
+
const allowed = ["clusterBootstrapCode", "emailSecret", "enrolmentToken", "backupS3Secret", "cloudflareTunnelToken", "cloudflareApiToken"];
|
|
13201
|
+
const unknown = Object.keys(source).filter((key) => !allowed.includes(key));
|
|
13202
|
+
if (unknown.length)
|
|
13203
|
+
throw new Error(`bootstrap credential input contains unsupported field ${unknown[0]}`);
|
|
13204
|
+
const clusterBootstrapCode = typeof source.clusterBootstrapCode === "string" ? source.clusterBootstrapCode.trim() : "";
|
|
13205
|
+
const emailSecret = typeof source.emailSecret === "string" ? source.emailSecret.trim() : "";
|
|
13206
|
+
const enrolmentToken = typeof source.enrolmentToken === "string" ? source.enrolmentToken.trim() : undefined;
|
|
13207
|
+
const backupS3Secret = typeof source.backupS3Secret === "string" ? source.backupS3Secret.trim() : undefined;
|
|
13208
|
+
const cloudflareTunnelToken = typeof source.cloudflareTunnelToken === "string" ? source.cloudflareTunnelToken.trim() : undefined;
|
|
13209
|
+
const cloudflareApiToken = typeof source.cloudflareApiToken === "string" ? source.cloudflareApiToken.trim() : undefined;
|
|
13210
|
+
if (!/^[a-f0-9]{64}$/i.test(clusterBootstrapCode))
|
|
13211
|
+
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
13212
|
+
if (!emailSecret || emailSecret.length > 16384 || /[\r\n\0]/.test(emailSecret))
|
|
13213
|
+
throw new Error("bootstrap email credential is malformed");
|
|
13214
|
+
if (config.enrolment.source === "api-token" !== Boolean(enrolmentToken) || enrolmentToken && !/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolmentToken)) {
|
|
13215
|
+
throw new Error("platform enrolment source and attended token disagree");
|
|
13216
|
+
}
|
|
13217
|
+
if (Boolean(config.runtime.environment.backup) !== Boolean(backupS3Secret)) {
|
|
13218
|
+
throw new Error("backup configuration and its attended credential must be supplied together");
|
|
13219
|
+
}
|
|
13220
|
+
if (backupS3Secret && (backupS3Secret.length > 16384 || /[\r\n\0]/.test(backupS3Secret))) {
|
|
13221
|
+
throw new Error("backup credential is malformed");
|
|
13222
|
+
}
|
|
13223
|
+
const cloudflareConfigured = Boolean(config.cloudflareHandoff || config.runtime.environment.cloudflare);
|
|
13224
|
+
if (cloudflareConfigured !== Boolean(cloudflareTunnelToken && cloudflareApiToken)) {
|
|
13225
|
+
throw new Error("Cloudflare configuration requires attended CF_TUNNEL_TOKEN and CF_API_TOKEN together");
|
|
13226
|
+
}
|
|
13227
|
+
if (cloudflareTunnelToken)
|
|
13228
|
+
validateCloudflareBootstrapSecretPair({ cloudflareTunnelToken, cloudflareApiToken });
|
|
13229
|
+
return {
|
|
13230
|
+
clusterBootstrapCode,
|
|
13231
|
+
emailSecret,
|
|
13232
|
+
...enrolmentToken ? { enrolmentToken } : {},
|
|
13233
|
+
...backupS3Secret ? { backupS3Secret } : {},
|
|
13234
|
+
...cloudflareTunnelToken ? { cloudflareTunnelToken, cloudflareApiToken } : {}
|
|
13235
|
+
};
|
|
13236
|
+
}
|
|
13185
13237
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
13186
13238
|
function resolveInstalledBootstrapKind(states) {
|
|
13187
13239
|
if (states.compute && states.metal) {
|
|
@@ -13197,6 +13249,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
|
13197
13249
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
13198
13250
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
13199
13251
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
13252
|
+
var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
|
|
13200
13253
|
var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
|
|
13201
13254
|
var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
|
|
13202
13255
|
var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
|
|
@@ -13205,7 +13258,6 @@ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
|
13205
13258
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
13206
13259
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
13207
13260
|
var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
|
|
13208
|
-
var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
|
|
13209
13261
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
13210
13262
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
13211
13263
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -13283,8 +13335,6 @@ function validateBootstrapConfig(value) {
|
|
|
13283
13335
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
13284
13336
|
throw new Error("tenant API must be public HTTPS or loopback HTTP");
|
|
13285
13337
|
}
|
|
13286
|
-
if (!value.enrolTokenFile)
|
|
13287
|
-
throw new Error("enrolled-compute activation requires an enrolment-token file");
|
|
13288
13338
|
if (value.bootstrapRunner) {
|
|
13289
13339
|
if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
|
|
13290
13340
|
throw new Error("bootstrap runner SSH private-key file must be absolute");
|
|
@@ -13328,8 +13378,6 @@ function validateBootstrapConfig(value) {
|
|
|
13328
13378
|
if (!/^[a-z0-9](?:[a-z0-9:_-]{0,126}[a-z0-9])?$/.test(value.computeReference)) {
|
|
13329
13379
|
throw new Error("platform compute reference is malformed");
|
|
13330
13380
|
}
|
|
13331
|
-
if (!value.database.bootstrapSecretFile)
|
|
13332
|
-
throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
|
|
13333
13381
|
const write = value.database.coordinators.map(privateOrigin);
|
|
13334
13382
|
if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
|
|
13335
13383
|
throw new Error("database coordinators must contain 1-16 unique private origins");
|
|
@@ -13344,12 +13392,6 @@ function validateBootstrapConfig(value) {
|
|
|
13344
13392
|
}
|
|
13345
13393
|
if (runtime.deployProfile !== value.environment)
|
|
13346
13394
|
throw new Error("runtime deployment profile disagrees with bootstrap environment");
|
|
13347
|
-
if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
|
|
13348
|
-
throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
|
|
13349
|
-
}
|
|
13350
|
-
if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
|
|
13351
|
-
throw new Error("Bootstrap email credential file must be an absolute single-line path");
|
|
13352
|
-
}
|
|
13353
13395
|
if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
|
|
13354
13396
|
throw new Error("runtime database topology disagrees with bootstrap topology");
|
|
13355
13397
|
}
|
|
@@ -13360,9 +13402,8 @@ function validateBootstrapConfig(value) {
|
|
|
13360
13402
|
if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
|
|
13361
13403
|
throw new Error("enabled firewall requires unique private cluster CIDRs");
|
|
13362
13404
|
}
|
|
13363
|
-
if (!["genesis-derived", "api-token"].includes(value.enrolment.source)
|
|
13364
|
-
throw new Error("platform enrolment source
|
|
13365
|
-
}
|
|
13405
|
+
if (!["genesis-derived", "api-token"].includes(value.enrolment.source))
|
|
13406
|
+
throw new Error("platform enrolment source is invalid");
|
|
13366
13407
|
value.runtime.environment = runtime;
|
|
13367
13408
|
return value;
|
|
13368
13409
|
}
|
|
@@ -13576,7 +13617,7 @@ async function waitForCloudflaredTunnel(host, expectedTunnelId) {
|
|
|
13576
13617
|
var derive = (root, label) => {
|
|
13577
13618
|
if (!/^[a-f0-9]{64}$/i.test(root))
|
|
13578
13619
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
13579
|
-
return
|
|
13620
|
+
return createHmac2("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
|
|
13580
13621
|
};
|
|
13581
13622
|
async function seal(host, name, destination2, value) {
|
|
13582
13623
|
if (host.exists(destination2))
|
|
@@ -13866,15 +13907,10 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
13866
13907
|
problems.push("durable Agent enrolment state is missing");
|
|
13867
13908
|
return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
|
|
13868
13909
|
}
|
|
13869
|
-
async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
13910
|
+
async function applyBootstrap(input, host = localBootstrapHost(), secrets, dependencies = {}) {
|
|
13870
13911
|
const config = validateBootstrapConfig(structuredClone(input));
|
|
13871
13912
|
if (host.uid() !== 0)
|
|
13872
13913
|
throw new Error("fz bootstrap --apply must run as root");
|
|
13873
|
-
if (config.kind === "enrolled-compute") {
|
|
13874
|
-
const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
|
|
13875
|
-
if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
|
|
13876
|
-
throw new Error("tenant enrolment token is malformed");
|
|
13877
|
-
}
|
|
13878
13914
|
let installed;
|
|
13879
13915
|
if (host.exists(STATE_PATH))
|
|
13880
13916
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
@@ -13932,11 +13968,21 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13932
13968
|
throw new Error("bootstrap repair coordinates do not match the installed host identity");
|
|
13933
13969
|
}
|
|
13934
13970
|
}
|
|
13935
|
-
const platformPrivate = config.kind === "platform" ? {
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13971
|
+
const platformPrivate = config.kind === "platform" ? (() => {
|
|
13972
|
+
if (!secrets)
|
|
13973
|
+
throw new Error("platform apply requires attended credentials on stdin");
|
|
13974
|
+
const checked3 = validatePlatformBootstrapSecrets(config, secrets);
|
|
13975
|
+
return {
|
|
13976
|
+
root: checked3.clusterBootstrapCode,
|
|
13977
|
+
email: checked3.emailSecret,
|
|
13978
|
+
enrolmentToken: checked3.enrolmentToken,
|
|
13979
|
+
backup: checked3.backupS3Secret,
|
|
13980
|
+
cloudflareTunnelToken: checked3.cloudflareTunnelToken,
|
|
13981
|
+
cloudflareApiToken: checked3.cloudflareApiToken
|
|
13982
|
+
};
|
|
13983
|
+
})() : undefined;
|
|
13984
|
+
const enrolledPrivate = config.kind === "enrolled-compute" ? validateEnrolledComputeBootstrapSecrets(config, secrets) : undefined;
|
|
13985
|
+
const cloudflarePrivate = cloudflare ? config.kind === "platform" ? { cloudflareTunnelToken: platformPrivate.cloudflareTunnelToken, cloudflareApiToken: platformPrivate.cloudflareApiToken } : validateCloudflareBootstrapSecretPair(enrolledPrivate) : undefined;
|
|
13940
13986
|
if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
|
|
13941
13987
|
privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
|
|
13942
13988
|
}
|
|
@@ -13945,17 +13991,31 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13945
13991
|
const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
13946
13992
|
host.mkdir(CREDS, 448);
|
|
13947
13993
|
host.mkdir("/var/lib/forgezero", 448);
|
|
13948
|
-
if (
|
|
13949
|
-
|
|
13994
|
+
if (!alreadyEnrolled && !host.exists(ENROL_CREDENTIAL)) {
|
|
13995
|
+
const enrolmentToken = config.kind === "platform" ? config.enrolment.source === "api-token" ? platformPrivate.enrolmentToken : `fze_${derive(derive(platformPrivate.root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}` : enrolledPrivate.enrolmentToken;
|
|
13996
|
+
await seal(host, "enrol-token", ENROL_CREDENTIAL, enrolmentToken);
|
|
13950
13997
|
}
|
|
13951
|
-
|
|
13952
|
-
|
|
13998
|
+
const connectorCapabilities = cloudflare ? await retrieveCloudflareConnectorTokens({
|
|
13999
|
+
accountId: cloudflare.accountId,
|
|
14000
|
+
tunnelId: cloudflare.tunnelId,
|
|
14001
|
+
...cloudflare.mesh ? { meshConnectorId: cloudflare.mesh.connectorId } : {},
|
|
14002
|
+
apiToken: cloudflarePrivate.cloudflareTunnelToken
|
|
14003
|
+
}, dependencies.fetcher ?? fetch) : undefined;
|
|
14004
|
+
if (cloudflarePrivate && !host.exists(CF_API_CREDENTIAL)) {
|
|
14005
|
+
await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflarePrivate.cloudflareApiToken);
|
|
13953
14006
|
}
|
|
13954
|
-
if (
|
|
13955
|
-
await seal(host, "
|
|
14007
|
+
if (config.kind === "platform" && platformPrivate.cloudflareTunnelToken && !host.exists(CF_TUNNEL_API_CREDENTIAL)) {
|
|
14008
|
+
await seal(host, "CF_TUNNEL_TOKEN", CF_TUNNEL_API_CREDENTIAL, platformPrivate.cloudflareTunnelToken);
|
|
13956
14009
|
}
|
|
13957
|
-
|
|
13958
|
-
|
|
14010
|
+
const realtimeSecrets = cloudflare?.realtime && cloudflarePrivate ? deriveCloudflareRealtimeSecrets(cloudflarePrivate.cloudflareApiToken) : undefined;
|
|
14011
|
+
if (realtimeSecrets && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
|
|
14012
|
+
await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, realtimeSecrets.publishSecret);
|
|
14013
|
+
}
|
|
14014
|
+
if (realtimeSecrets && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
|
|
14015
|
+
await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, realtimeSecrets.ticketSecret);
|
|
14016
|
+
}
|
|
14017
|
+
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14018
|
+
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
13959
14019
|
}
|
|
13960
14020
|
await host.installAgent(config);
|
|
13961
14021
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
@@ -13982,22 +14042,15 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13982
14042
|
}
|
|
13983
14043
|
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
13984
14044
|
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
13985
|
-
const
|
|
13986
|
-
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
|
|
13987
|
-
if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
|
|
13988
|
-
throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
|
|
13989
|
-
}
|
|
14045
|
+
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
13990
14046
|
for (const [name, source] of Object.entries({
|
|
13991
14047
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
13992
|
-
"backup
|
|
14048
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
13993
14049
|
})) {
|
|
13994
14050
|
if (source) {
|
|
13995
14051
|
const destination2 = `${CREDS}/${name}.cred`;
|
|
13996
14052
|
if (!host.exists(destination2)) {
|
|
13997
14053
|
await seal(host, name, destination2, source);
|
|
13998
|
-
const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
|
|
13999
|
-
if (sourcePath)
|
|
14000
|
-
host.remove(sourcePath);
|
|
14001
14054
|
}
|
|
14002
14055
|
}
|
|
14003
14056
|
}
|
|
@@ -14067,7 +14120,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14067
14120
|
if (config.database.role === "master") {
|
|
14068
14121
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
14069
14122
|
if (!host.exists(invite)) {
|
|
14070
|
-
host.write(invite, `plt_${
|
|
14123
|
+
host.write(invite, `plt_${randomBytes6(24).toString("hex")}
|
|
14071
14124
|
`, 384);
|
|
14072
14125
|
await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
14073
14126
|
}
|
|
@@ -14087,13 +14140,6 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14087
14140
|
verifiedAt: new Date().toISOString()
|
|
14088
14141
|
};
|
|
14089
14142
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
14090
|
-
`, 384);
|
|
14091
|
-
}
|
|
14092
|
-
if (!alreadyEnrolled) {
|
|
14093
|
-
const enrolToken = config.enrolment.source === "api-token" ? privateFile(host, config.enrolment.tokenFile, "platform enrolment token") : `fze_${derive(derive(root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}`;
|
|
14094
|
-
if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
|
|
14095
|
-
throw new Error("platform enrolment token is malformed");
|
|
14096
|
-
host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
|
|
14097
14143
|
`, 384);
|
|
14098
14144
|
}
|
|
14099
14145
|
await checked2(host, [
|
|
@@ -14105,15 +14151,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14105
14151
|
"deploy",
|
|
14106
14152
|
...config.database.role === "master" ? ["--release-executor"] : []
|
|
14107
14153
|
], "initial Agent deployment");
|
|
14108
|
-
if (!alreadyEnrolled) {
|
|
14109
|
-
await host.installAgent(config, PLATFORM_ENROL_SOURCE);
|
|
14110
|
-
if (config.enrolment.source === "api-token")
|
|
14111
|
-
host.remove(config.enrolment.tokenFile);
|
|
14112
|
-
}
|
|
14113
14154
|
}
|
|
14114
14155
|
if (config.cloudflareHandoff) {
|
|
14115
|
-
if (!host.exists(TUNNEL_CREDENTIAL) &&
|
|
14116
|
-
await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL,
|
|
14156
|
+
if (!host.exists(TUNNEL_CREDENTIAL) && connectorCapabilities) {
|
|
14157
|
+
await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, connectorCapabilities.connectorToken);
|
|
14117
14158
|
}
|
|
14118
14159
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
14119
14160
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
@@ -14187,7 +14228,6 @@ function strictBootstrapDocument(value) {
|
|
|
14187
14228
|
"installWarp",
|
|
14188
14229
|
"cloudflareHandoff",
|
|
14189
14230
|
"realm",
|
|
14190
|
-
"enrolTokenFile",
|
|
14191
14231
|
"software",
|
|
14192
14232
|
"deploymentCredentials",
|
|
14193
14233
|
"bootstrapRunner"
|
|
@@ -14196,8 +14236,8 @@ function strictBootstrapDocument(value) {
|
|
|
14196
14236
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14197
14237
|
if (root.cloudflareHandoff !== undefined)
|
|
14198
14238
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
14199
|
-
exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"
|
|
14200
|
-
exactKeys2(root.enrolment, ["source"
|
|
14239
|
+
exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
|
|
14240
|
+
exactKeys2(root.enrolment, ["source"], "platform enrolment config");
|
|
14201
14241
|
const runtime = exactKeys2(root.runtime, [
|
|
14202
14242
|
"environment",
|
|
14203
14243
|
"serviceUser",
|
|
@@ -14205,8 +14245,7 @@ function strictBootstrapDocument(value) {
|
|
|
14205
14245
|
"bluePort",
|
|
14206
14246
|
"greenPort",
|
|
14207
14247
|
"healthPath",
|
|
14208
|
-
"keepReleases"
|
|
14209
|
-
"credentialFiles"
|
|
14248
|
+
"keepReleases"
|
|
14210
14249
|
], "runtime config");
|
|
14211
14250
|
exactKeys2(runtime.environment, [
|
|
14212
14251
|
"softwareProfile",
|
|
@@ -14244,8 +14283,6 @@ function strictBootstrapDocument(value) {
|
|
|
14244
14283
|
"cloudflare",
|
|
14245
14284
|
"realtime"
|
|
14246
14285
|
], "runtime environment");
|
|
14247
|
-
if (runtime.credentialFiles !== undefined)
|
|
14248
|
-
exactKeys2(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
|
|
14249
14286
|
const environment = runtime.environment;
|
|
14250
14287
|
if (environment.email !== undefined) {
|
|
14251
14288
|
const email = exactKeys2(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
|
|
@@ -14329,10 +14366,10 @@ function localBootstrapHost() {
|
|
|
14329
14366
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
14330
14367
|
return result;
|
|
14331
14368
|
},
|
|
14332
|
-
async installAgent(config
|
|
14369
|
+
async installAgent(config) {
|
|
14333
14370
|
const capabilities = await readCapabilities(localRunner);
|
|
14334
14371
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
14335
|
-
const hasBinding = config.kind === "enrolled-compute" ||
|
|
14372
|
+
const hasBinding = config.kind === "enrolled-compute" || existsSync8(ENROL_CREDENTIAL) || existsSync8("/var/lib/forgezero/enrolment.json");
|
|
14336
14373
|
if (config.kind === "platform") {
|
|
14337
14374
|
const lifecycle = config.database.role === "none" ? {
|
|
14338
14375
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -14375,8 +14412,7 @@ function localBootstrapHost() {
|
|
|
14375
14412
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
14376
14413
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14377
14414
|
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14378
|
-
...
|
|
14379
|
-
enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
|
|
14415
|
+
...hasBinding ? {
|
|
14380
14416
|
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14381
14417
|
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
14382
14418
|
apiUrl: config.apiUrl,
|
|
@@ -14399,12 +14435,11 @@ function localBootstrapHost() {
|
|
|
14399
14435
|
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
|
|
14400
14436
|
import { dirname as dirname9, resolve as resolve6 } from "path";
|
|
14401
14437
|
async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
|
|
14402
|
-
const tokens = await readCloudflareBootstrapTokens(input.tokenFiles);
|
|
14403
14438
|
return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
|
|
14404
14439
|
zoneName: input.zoneName,
|
|
14405
14440
|
kvNamespaceTitle: input.kvNamespaceTitle,
|
|
14406
14441
|
...input.workerScriptName ? { workerScriptName: input.workerScriptName } : {},
|
|
14407
|
-
...tokens
|
|
14442
|
+
...input.tokens
|
|
14408
14443
|
}, dependencies.fetcher);
|
|
14409
14444
|
}
|
|
14410
14445
|
var exactKeys3 = (value, allowed, label) => {
|
|
@@ -14421,20 +14456,11 @@ function createCloudflareBootstrapCommandConfig(input) {
|
|
|
14421
14456
|
if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
|
|
14422
14457
|
throw new Error("Cloudflare bootstrap checkpointPath is required");
|
|
14423
14458
|
}
|
|
14424
|
-
for (const key of ["tunnelTokenFile", "apiTokenFile"]) {
|
|
14425
|
-
if (typeof input.tokenFiles?.[key] !== "string" || !input.tokenFiles[key].trim()) {
|
|
14426
|
-
throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
|
|
14427
|
-
}
|
|
14428
|
-
}
|
|
14429
14459
|
return {
|
|
14430
14460
|
format: 1,
|
|
14431
14461
|
kind: "forgezero-cloudflare-bootstrap-request",
|
|
14432
14462
|
checkpointPath: input.checkpointPath.trim(),
|
|
14433
|
-
coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
|
|
14434
|
-
tokenFiles: {
|
|
14435
|
-
tunnelTokenFile: input.tokenFiles.tunnelTokenFile.trim(),
|
|
14436
|
-
apiTokenFile: input.tokenFiles.apiTokenFile.trim()
|
|
14437
|
-
}
|
|
14463
|
+
coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
|
|
14438
14464
|
};
|
|
14439
14465
|
}
|
|
14440
14466
|
function readOwnerConfig(path) {
|
|
@@ -14462,7 +14488,7 @@ function readOwnerConfig(path) {
|
|
|
14462
14488
|
function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
14463
14489
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
|
|
14464
14490
|
const baseDirectory = dirname9(resolve6(path));
|
|
14465
|
-
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"
|
|
14491
|
+
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"], "Cloudflare bootstrap config");
|
|
14466
14492
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
|
|
14467
14493
|
throw new Error("Cloudflare bootstrap config format/kind is invalid");
|
|
14468
14494
|
}
|
|
@@ -14498,33 +14524,16 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
14498
14524
|
}
|
|
14499
14525
|
}
|
|
14500
14526
|
const coordinates = validateCloudflareBootstrapCoordinates(coordinateSource);
|
|
14501
|
-
let tokenFiles;
|
|
14502
|
-
if (input.tokenFiles !== undefined) {
|
|
14503
|
-
const source = record2(input.tokenFiles, "Cloudflare bootstrap tokenFiles");
|
|
14504
|
-
const keys = ["tunnelTokenFile", "apiTokenFile"];
|
|
14505
|
-
exactKeys3(source, keys, "Cloudflare bootstrap tokenFiles");
|
|
14506
|
-
for (const key of keys) {
|
|
14507
|
-
if (typeof source[key] !== "string" || !source[key].trim()) {
|
|
14508
|
-
throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
|
|
14509
|
-
}
|
|
14510
|
-
}
|
|
14511
|
-
tokenFiles = {
|
|
14512
|
-
tunnelTokenFile: resolve6(baseDirectory, source.tunnelTokenFile),
|
|
14513
|
-
apiTokenFile: resolve6(baseDirectory, source.apiTokenFile)
|
|
14514
|
-
};
|
|
14515
|
-
}
|
|
14516
|
-
if (mode === "apply" && !tokenFiles) {
|
|
14517
|
-
throw new Error("Cloudflare apply config requires exactly two owner-only tokenFiles paths");
|
|
14518
|
-
}
|
|
14519
14527
|
return {
|
|
14520
14528
|
mode,
|
|
14521
14529
|
coordinates,
|
|
14522
|
-
checkpointPath: resolve6(baseDirectory, input.checkpointPath)
|
|
14523
|
-
...tokenFiles ? { tokenFiles } : {}
|
|
14530
|
+
checkpointPath: resolve6(baseDirectory, input.checkpointPath)
|
|
14524
14531
|
};
|
|
14525
14532
|
}
|
|
14526
|
-
async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {}) {
|
|
14533
|
+
async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
|
|
14527
14534
|
const request = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
|
|
14535
|
+
if (apply)
|
|
14536
|
+
request.tokens = tokens;
|
|
14528
14537
|
const evidence = await (dependencies.run ?? runAttendedCloudflareBootstrap)(request);
|
|
14529
14538
|
(dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
|
|
14530
14539
|
`);
|
|
@@ -14562,7 +14571,7 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
14562
14571
|
}
|
|
14563
14572
|
|
|
14564
14573
|
// src/metal-bootstrap.ts
|
|
14565
|
-
import { createHash as createHash4, randomBytes as
|
|
14574
|
+
import { createHash as createHash4, randomBytes as randomBytes7 } from "crypto";
|
|
14566
14575
|
import {
|
|
14567
14576
|
chmodSync as chmodSync4,
|
|
14568
14577
|
chownSync,
|
|
@@ -15298,7 +15307,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15298
15307
|
atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
15299
15308
|
`, 384);
|
|
15300
15309
|
if (!existsSync9(SEED_CREDENTIAL_PATH)) {
|
|
15301
|
-
const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() :
|
|
15310
|
+
const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes7(32).toString("base64url");
|
|
15302
15311
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
15303
15312
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
15304
15313
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
@@ -15464,7 +15473,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15464
15473
|
}
|
|
15465
15474
|
|
|
15466
15475
|
// src/operator-bootstrap.ts
|
|
15467
|
-
import { createHash as createHash5, randomBytes as
|
|
15476
|
+
import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
|
|
15468
15477
|
import { lstatSync as lstatSync6, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
15469
15478
|
import { isIP as isIP6 } from "net";
|
|
15470
15479
|
import { tmpdir } from "os";
|
|
@@ -15577,6 +15586,17 @@ function platformCommunityRehearsalClaims(sshPublicKeys) {
|
|
|
15577
15586
|
}
|
|
15578
15587
|
}];
|
|
15579
15588
|
}
|
|
15589
|
+
function platformCommunityRehearsalDeletionClaims(sshPublicKeys) {
|
|
15590
|
+
return platformCommunityRehearsalClaims(sshPublicKeys).map((claim) => ({
|
|
15591
|
+
computeKey: claim.computeKey,
|
|
15592
|
+
claimToken: claim.claimToken,
|
|
15593
|
+
claimExpiresAtTs: claim.claimExpiresAtTs,
|
|
15594
|
+
attempt: claim.attempt,
|
|
15595
|
+
action: "delete",
|
|
15596
|
+
bootstrap: { kind: "platform-genesis" },
|
|
15597
|
+
spec: claim.spec
|
|
15598
|
+
}));
|
|
15599
|
+
}
|
|
15580
15600
|
|
|
15581
15601
|
// src/operator-bootstrap.ts
|
|
15582
15602
|
var REQUEST_LIMIT = 64 * 1024;
|
|
@@ -15730,17 +15750,17 @@ function readOperatorMetalBootstrapRequest(path) {
|
|
|
15730
15750
|
}
|
|
15731
15751
|
function planOperatorMetalBootstrap(request, mode) {
|
|
15732
15752
|
const config = readMetalBootstrapConfig(request.metalConfigFile);
|
|
15733
|
-
if (mode === "rehearsal" && request.genesis.environment !== "development") {
|
|
15753
|
+
if ((mode === "rehearsal" || mode === "rehearsal-cleanup") && request.genesis.environment !== "development") {
|
|
15734
15754
|
throw new Error("the disposable four-node Community rehearsal is development-only");
|
|
15735
15755
|
}
|
|
15736
15756
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
15737
|
-
const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
|
|
15757
|
+
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(keys) : platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
|
|
15738
15758
|
return {
|
|
15739
15759
|
kind: "metal-remote",
|
|
15740
15760
|
mode,
|
|
15741
15761
|
mutation: mode !== "status",
|
|
15742
15762
|
target: { address: request.target.address, port: request.target.port, user: request.target.user },
|
|
15743
|
-
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the fixed three-node seed plus ordinary n4 Community rehearsal claim to the constrained Metal helper" : "send three fixed platform-genesis claims to the constrained Metal helper"] : [
|
|
15763
|
+
steps: mode === "status" ? ["verify pinned SSH transport", "run typed fz bootstrap status"] : mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup" ? ["verify pinned SSH transport", mode === "rehearsal" ? "send the fixed three-node seed plus ordinary n4 Community rehearsal claim to the constrained Metal helper" : mode === "rehearsal-cleanup" ? "delete the exact disposable dev-fz-n1..n4 guests and their owned disks, units, seed data and inventory" : "send three fixed platform-genesis claims to the constrained Metal helper"] : [
|
|
15744
15764
|
"verify pinned SSH transport and caller-approved SSH agent",
|
|
15745
15765
|
"copy pinned Bun and packaged fz artifacts to private staging",
|
|
15746
15766
|
"stage the strict owner-only metal config and optional seed handoff",
|
|
@@ -15752,12 +15772,15 @@ function planOperatorMetalBootstrap(request, mode) {
|
|
|
15752
15772
|
};
|
|
15753
15773
|
}
|
|
15754
15774
|
var secretSources = (config) => [
|
|
15755
|
-
["cluster-code", config.database.bootstrapSecretFile],
|
|
15756
|
-
...config.enrolment.tokenFile ? [["enrol-token", config.enrolment.tokenFile]] : [],
|
|
15757
|
-
...config.runtime.credentialFiles?.emailSecret ? [["email-secret", config.runtime.credentialFiles.emailSecret]] : [],
|
|
15758
|
-
...config.runtime.credentialFiles?.backupS3Secret ? [["backup-s3-secret", config.runtime.credentialFiles.backupS3Secret]] : [],
|
|
15759
15775
|
...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
|
|
15760
15776
|
];
|
|
15777
|
+
var attendedSecretNames = (config) => [
|
|
15778
|
+
"cluster-bootstrap-code",
|
|
15779
|
+
config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
|
|
15780
|
+
...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
|
|
15781
|
+
...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
|
|
15782
|
+
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
|
|
15783
|
+
];
|
|
15761
15784
|
function planOperatorPlatformBootstrap(request, mode) {
|
|
15762
15785
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
15763
15786
|
if (config.kind !== "platform")
|
|
@@ -15779,7 +15802,7 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
15779
15802
|
`run typed fz bootstrap platform${mode === "prepare" ? " prepare" : ""} --apply`,
|
|
15780
15803
|
"remove transient local and remote staging data"
|
|
15781
15804
|
],
|
|
15782
|
-
secretInputs: secretSources(config).map(([name]) => name)
|
|
15805
|
+
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
15783
15806
|
};
|
|
15784
15807
|
}
|
|
15785
15808
|
var defaultExec3 = async (argv2, options = {}) => {
|
|
@@ -15896,15 +15919,7 @@ function stageConfig(config, directory) {
|
|
|
15896
15919
|
writeFileSync11(local, bytes, { mode: 384, flag: "wx" });
|
|
15897
15920
|
staged.push(name);
|
|
15898
15921
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
15899
|
-
if (name === "
|
|
15900
|
-
rewritten.database.bootstrapSecretFile = remotePath;
|
|
15901
|
-
else if (name === "enrol-token")
|
|
15902
|
-
rewritten.enrolment.tokenFile = remotePath;
|
|
15903
|
-
else if (name === "email-secret")
|
|
15904
|
-
rewritten.runtime.credentialFiles.emailSecret = remotePath;
|
|
15905
|
-
else if (name === "backup-s3-secret")
|
|
15906
|
-
rewritten.runtime.credentialFiles.backupS3Secret = remotePath;
|
|
15907
|
-
else if (name === "cloudflare-handoff")
|
|
15922
|
+
if (name === "cloudflare-handoff")
|
|
15908
15923
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
15909
15924
|
}
|
|
15910
15925
|
const path = join10(directory, "platform-config.json");
|
|
@@ -15988,7 +16003,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
15988
16003
|
socketPath(request.target.agentSocket);
|
|
15989
16004
|
const exec = options.exec ?? defaultExec3;
|
|
15990
16005
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
15991
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16006
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
|
|
15992
16007
|
let knownHosts = "";
|
|
15993
16008
|
try {
|
|
15994
16009
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -15999,6 +16014,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
15999
16014
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
16000
16015
|
if (config.kind !== "platform")
|
|
16001
16016
|
throw new Error("operator bootstrap requires a platform config");
|
|
16017
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
16002
16018
|
const staged = stageConfig(config, directory);
|
|
16003
16019
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
16004
16020
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -16009,8 +16025,11 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
16009
16025
|
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform"];
|
|
16010
16026
|
if (mode === "prepare")
|
|
16011
16027
|
command.push("prepare");
|
|
16028
|
+
else
|
|
16029
|
+
command.push("credentials-stdin");
|
|
16012
16030
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
16013
|
-
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap")
|
|
16031
|
+
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
16032
|
+
` : undefined);
|
|
16014
16033
|
return { plan, output };
|
|
16015
16034
|
} finally {
|
|
16016
16035
|
if (knownHosts) {
|
|
@@ -16027,7 +16046,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16027
16046
|
socketPath(request.target.agentSocket);
|
|
16028
16047
|
const exec = options.exec ?? defaultExec3;
|
|
16029
16048
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-metal-"));
|
|
16030
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16049
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
|
|
16031
16050
|
let knownHosts = "";
|
|
16032
16051
|
try {
|
|
16033
16052
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16035,10 +16054,10 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
16035
16054
|
const output2 = await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "status"], "remote metal status");
|
|
16036
16055
|
return { plan, output: output2 };
|
|
16037
16056
|
}
|
|
16038
|
-
if (mode === "genesis" || mode === "rehearsal") {
|
|
16057
|
+
if (mode === "genesis" || mode === "rehearsal" || mode === "rehearsal-cleanup") {
|
|
16039
16058
|
const keys = request.genesis.sshPublicKeyFiles.map((path) => publicIdentity(path));
|
|
16040
16059
|
const outputs = [];
|
|
16041
|
-
const claims = mode === "rehearsal" ? platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
|
|
16060
|
+
const claims = mode === "rehearsal" || mode === "rehearsal-cleanup" ? mode === "rehearsal-cleanup" ? platformCommunityRehearsalDeletionClaims(keys) : platformCommunityRehearsalClaims(keys) : platformGenesisClaims(request.genesis.environment, keys);
|
|
16042
16061
|
for (const claim of claims) {
|
|
16043
16062
|
outputs.push(await remote(exec, request, knownHosts, ["/usr/bin/sudo", "-n", "/usr/local/bin/fz-agent", "metal-apply", "--claim=-"], `provision ${claim.spec.guestName}`, false, `${JSON.stringify(claim)}
|
|
16044
16063
|
`));
|
|
@@ -16102,8 +16121,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16102
16121
|
serverMode: "default",
|
|
16103
16122
|
address: guest.address,
|
|
16104
16123
|
...role === "joiner" ? { master } : {},
|
|
16105
|
-
coordinators
|
|
16106
|
-
bootstrapSecretFile: template.database.bootstrapSecretFile
|
|
16124
|
+
coordinators
|
|
16107
16125
|
};
|
|
16108
16126
|
config.enrolment = { source: "genesis-derived" };
|
|
16109
16127
|
config.installCloudflared = true;
|
|
@@ -16985,8 +17003,10 @@ async function cmdStatus(options) {
|
|
|
16985
17003
|
async function cmdAgent(options, args) {
|
|
16986
17004
|
if (args[0] === "activate") {
|
|
16987
17005
|
try {
|
|
16988
|
-
|
|
16989
|
-
|
|
17006
|
+
const credentialStdin = args[1] === "credentials-stdin";
|
|
17007
|
+
if (args[1] !== undefined && !credentialStdin || args[2] !== undefined) {
|
|
17008
|
+
throw new Error("internal Agent activation accepts only credentials-stdin");
|
|
17009
|
+
}
|
|
16990
17010
|
if (!options.bootstrapConfigPath)
|
|
16991
17011
|
throw new Error("internal Agent activation requires --bootstrap-config");
|
|
16992
17012
|
const config = readBootstrapConfig(options.bootstrapConfigPath);
|
|
@@ -16997,7 +17017,19 @@ async function cmdAgent(options, args) {
|
|
|
16997
17017
|
out.line(JSON.stringify(plan2, null, 2));
|
|
16998
17018
|
return 0;
|
|
16999
17019
|
}
|
|
17000
|
-
|
|
17020
|
+
if (!credentialStdin)
|
|
17021
|
+
throw new Error("internal Agent activation requires credentials-stdin");
|
|
17022
|
+
const body = await Bun.stdin.text();
|
|
17023
|
+
if (body.length < 2 || body.length > 65536)
|
|
17024
|
+
throw new Error("activation credential stdin has an invalid size");
|
|
17025
|
+
let parsed;
|
|
17026
|
+
try {
|
|
17027
|
+
parsed = JSON.parse(body);
|
|
17028
|
+
} catch {
|
|
17029
|
+
throw new Error("activation credential stdin is not valid JSON");
|
|
17030
|
+
}
|
|
17031
|
+
const secrets = validateEnrolledComputeBootstrapSecrets(config, parsed);
|
|
17032
|
+
out.line(JSON.stringify(await applyBootstrap(config, undefined, secrets), null, 2));
|
|
17001
17033
|
return 0;
|
|
17002
17034
|
} catch (cause) {
|
|
17003
17035
|
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -17008,7 +17040,6 @@ async function cmdAgent(options, args) {
|
|
|
17008
17040
|
out.fail("Usage: fz agent install [--apply]");
|
|
17009
17041
|
return 1;
|
|
17010
17042
|
}
|
|
17011
|
-
const enrolTokenSourcePath = "/run/forgezero-enrol-token";
|
|
17012
17043
|
const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
|
|
17013
17044
|
const enrolStatePath = "/var/lib/forgezero/enrolment.json";
|
|
17014
17045
|
const gitCredentialPath = process.env.FZ_GIT_CREDENTIAL_PATH ?? "/etc/forgezero/creds/git-deploy-key.cred";
|
|
@@ -17048,7 +17079,6 @@ async function cmdAgent(options, args) {
|
|
|
17048
17079
|
telemetryEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
17049
17080
|
...options.enrol ? {
|
|
17050
17081
|
pullDeployments: true,
|
|
17051
|
-
enrolTokenSourcePath,
|
|
17052
17082
|
enrolTokenCredentialPath,
|
|
17053
17083
|
enrolStatePath,
|
|
17054
17084
|
deployRoot: process.env.FZ_DEPLOY_ROOT ?? "/opt/forgezero"
|
|
@@ -17086,23 +17116,20 @@ async function cmdAgent(options, args) {
|
|
|
17086
17116
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17087
17117
|
}
|
|
17088
17118
|
if (options.enrol) {
|
|
17089
|
-
if (existsSync11(
|
|
17090
|
-
const
|
|
17091
|
-
|
|
17092
|
-
if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
|
|
17093
|
-
throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
|
|
17094
|
-
}
|
|
17095
|
-
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
17096
|
-
throw new Error("The preloaded enrolment token is malformed.");
|
|
17097
|
-
}
|
|
17098
|
-
} else {
|
|
17099
|
-
const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", "ForgeZero one-time enrolment token:"], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
|
|
17100
|
-
const token = (await new Response(prompt.stdout).text()).trim();
|
|
17101
|
-
if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
17119
|
+
if (!existsSync11(enrolStatePath) && !existsSync11(enrolTokenCredentialPath)) {
|
|
17120
|
+
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17121
|
+
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17102
17122
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17123
|
+
mkdirSync11(dirname12(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17124
|
+
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17125
|
+
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17126
|
+
sealing.stdin.write(`${token}
|
|
17127
|
+
`);
|
|
17128
|
+
sealing.stdin.end();
|
|
17103
17129
|
}
|
|
17104
|
-
|
|
17105
|
-
|
|
17130
|
+
const error = await new Response(sealing.stderr).text();
|
|
17131
|
+
if (await sealing.exited !== 0)
|
|
17132
|
+
throw new Error(`could not seal enrolment credential: ${error.trim()}`);
|
|
17106
17133
|
}
|
|
17107
17134
|
}
|
|
17108
17135
|
const transcript = await applyPlan(plan, localRunner);
|
|
@@ -17116,11 +17143,6 @@ async function cmdAgent(options, args) {
|
|
|
17116
17143
|
out.line();
|
|
17117
17144
|
return 0;
|
|
17118
17145
|
} catch (cause) {
|
|
17119
|
-
if (options.enrol) {
|
|
17120
|
-
try {
|
|
17121
|
-
unlinkSync3(enrolTokenSourcePath);
|
|
17122
|
-
} catch {}
|
|
17123
|
-
}
|
|
17124
17146
|
out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
|
|
17125
17147
|
out.step("Run this as root, or use --json and hand the unit to your provisioner.");
|
|
17126
17148
|
return 1;
|
|
@@ -17141,6 +17163,27 @@ var bootstrapNumber = (question, fallback) => {
|
|
|
17141
17163
|
throw new Error(`${question} must be an integer`);
|
|
17142
17164
|
return value;
|
|
17143
17165
|
};
|
|
17166
|
+
async function bootstrapSecret(question) {
|
|
17167
|
+
if (!process.stdin.isTTY)
|
|
17168
|
+
throw new Error(`${question} requires an interactive terminal`);
|
|
17169
|
+
const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", `${question}:`], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
|
|
17170
|
+
const value = (await new Response(prompt.stdout).text()).trim();
|
|
17171
|
+
if (await prompt.exited !== 0 || !value)
|
|
17172
|
+
throw new Error(`${question} was not provided`);
|
|
17173
|
+
return value;
|
|
17174
|
+
}
|
|
17175
|
+
async function promptPlatformBootstrapSecrets(config) {
|
|
17176
|
+
return validatePlatformBootstrapSecrets(config, {
|
|
17177
|
+
clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
17178
|
+
emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
|
|
17179
|
+
...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
|
|
17180
|
+
...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
|
|
17181
|
+
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
|
|
17182
|
+
cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17183
|
+
cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17184
|
+
} : {}
|
|
17185
|
+
});
|
|
17186
|
+
}
|
|
17144
17187
|
var bootstrapList = (question, fallback) => bootstrapAnswer(question, fallback).split(",").map((value) => value.trim()).filter(Boolean);
|
|
17145
17188
|
var parseCpuPools = (value) => value.split(";").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
17146
17189
|
const [key, cpus, physicalCores, memoryNodes] = entry.split("|").map((field) => field?.trim());
|
|
@@ -17211,9 +17254,9 @@ function writeBootstrapConfig(path, config) {
|
|
|
17211
17254
|
async function interactiveCloudflareBootstrap() {
|
|
17212
17255
|
if (!process.stdin.isTTY)
|
|
17213
17256
|
throw new Error("interactive Cloudflare config generation requires a terminal");
|
|
17214
|
-
const
|
|
17215
|
-
|
|
17216
|
-
|
|
17257
|
+
const tokens = {
|
|
17258
|
+
tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17259
|
+
apiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17217
17260
|
};
|
|
17218
17261
|
const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
|
|
17219
17262
|
const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
|
|
@@ -17223,7 +17266,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17223
17266
|
zoneName,
|
|
17224
17267
|
kvNamespaceTitle,
|
|
17225
17268
|
...workerScriptName ? { workerScriptName } : {},
|
|
17226
|
-
|
|
17269
|
+
tokens
|
|
17227
17270
|
});
|
|
17228
17271
|
const nodeCount = bootstrapNumber("Number of public API nodes", "3");
|
|
17229
17272
|
if (nodeCount < 1 || nodeCount > 32)
|
|
@@ -17261,8 +17304,7 @@ async function interactiveCloudflareBootstrap() {
|
|
|
17261
17304
|
}
|
|
17262
17305
|
} : {},
|
|
17263
17306
|
nodes
|
|
17264
|
-
}
|
|
17265
|
-
tokenFiles
|
|
17307
|
+
}
|
|
17266
17308
|
});
|
|
17267
17309
|
}
|
|
17268
17310
|
function genesisOutputDirectory(path) {
|
|
@@ -17277,20 +17319,9 @@ function genesisOutputDirectory(path) {
|
|
|
17277
17319
|
}
|
|
17278
17320
|
return path;
|
|
17279
17321
|
}
|
|
17280
|
-
function ensureGenesisClusterSecret(directory) {
|
|
17281
|
-
const path = `${directory}/cluster-bootstrap.code`;
|
|
17282
|
-
if (!existsSync11(path))
|
|
17283
|
-
writeFileSync12(path, `${randomBytes10(32).toString("hex")}
|
|
17284
|
-
`, { mode: 384, flag: "wx" });
|
|
17285
|
-
if (!/^[a-f0-9]{64}$/i.test(readOwnerOnlySecret(path, "cluster bootstrap code"))) {
|
|
17286
|
-
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
17287
|
-
}
|
|
17288
|
-
return path;
|
|
17289
|
-
}
|
|
17290
17322
|
function writePlatformGenesisFleet(directory) {
|
|
17291
17323
|
const output = genesisOutputDirectory(directory);
|
|
17292
|
-
const
|
|
17293
|
-
const template = interactiveBootstrap("platform", { bootstrapSecretFile: secret });
|
|
17324
|
+
const template = interactiveBootstrap("platform", true);
|
|
17294
17325
|
const fleet = PLATFORM_GENESIS_FLEETS[template.environment];
|
|
17295
17326
|
const nodes = fleet.map((guest, index) => index === 0 ? {
|
|
17296
17327
|
nodeHostname: template.nodeHostname,
|
|
@@ -17304,9 +17335,9 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17304
17335
|
writeBootstrapConfig(path, config);
|
|
17305
17336
|
return path;
|
|
17306
17337
|
});
|
|
17307
|
-
return {
|
|
17338
|
+
return { configs };
|
|
17308
17339
|
}
|
|
17309
|
-
function interactiveBootstrap(kind, genesis) {
|
|
17340
|
+
function interactiveBootstrap(kind, genesis = false) {
|
|
17310
17341
|
if (!process.stdin.isTTY)
|
|
17311
17342
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
17312
17343
|
const environment = bootstrapAnswer("Environment (production/development)", "production");
|
|
@@ -17340,18 +17371,14 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17340
17371
|
const smtpPort = emailProvider === "smtp" ? Number(bootstrapAnswer("SMTP port", "587")) : undefined;
|
|
17341
17372
|
const smtpUser = emailProvider === "smtp" ? bootstrapAnswer("SMTP user") : undefined;
|
|
17342
17373
|
const jetemailEu = emailProvider === "jetemail" ? bootstrapAnswer("Use JetEmail EU processing? (yes/no)", "no") === "yes" : undefined;
|
|
17343
|
-
const emailSecret = bootstrapAnswer(emailProvider === "smtp" ? "Root-only SMTP password file" : "Root-only JetEmail API-key file");
|
|
17344
17374
|
const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
|
|
17345
17375
|
const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
|
|
17346
17376
|
const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
|
|
17347
17377
|
const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
|
|
17348
|
-
const backupS3Secret = backupEndpoint ? bootstrapAnswer("Root-only backup S3 secret file") : undefined;
|
|
17349
17378
|
const cloudflareHandoffFile = bootstrapAnswer(genesis ? `${computeReference} node-specific Cloudflare host handoff` : "Node-specific Cloudflare host handoff (blank for no public edge)", genesis ? undefined : "");
|
|
17350
17379
|
const cloudflareNodeName = cloudflareHandoffFile ? bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
|
|
17351
17380
|
const databaseNetworkMode = "private-lan";
|
|
17352
|
-
const bootstrapSecretFile = genesis?.bootstrapSecretFile ?? bootstrapAnswer("Root-only shared cluster bootstrap-code file");
|
|
17353
17381
|
const enrolmentSource = genesis ? "genesis-derived" : bootstrapAnswer("Enrolment source (genesis-derived/api-token)", "genesis-derived");
|
|
17354
|
-
const platformEnrolTokenFile = enrolmentSource === "api-token" ? bootstrapAnswer("Root-only API-issued platform enrolment-token file") : undefined;
|
|
17355
17382
|
return {
|
|
17356
17383
|
kind,
|
|
17357
17384
|
environment,
|
|
@@ -17368,10 +17395,9 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17368
17395
|
serverMode,
|
|
17369
17396
|
address,
|
|
17370
17397
|
master,
|
|
17371
|
-
coordinators
|
|
17372
|
-
bootstrapSecretFile
|
|
17398
|
+
coordinators
|
|
17373
17399
|
},
|
|
17374
|
-
enrolment: { source: enrolmentSource
|
|
17400
|
+
enrolment: { source: enrolmentSource },
|
|
17375
17401
|
runtime: {
|
|
17376
17402
|
environment: {
|
|
17377
17403
|
softwareProfile: profile,
|
|
@@ -17414,11 +17440,7 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17414
17440
|
bluePort: 3001,
|
|
17415
17441
|
greenPort: 3002,
|
|
17416
17442
|
healthPath: "/api/health",
|
|
17417
|
-
keepReleases: 5
|
|
17418
|
-
credentialFiles: {
|
|
17419
|
-
emailSecret,
|
|
17420
|
-
backupS3Secret
|
|
17421
|
-
}
|
|
17443
|
+
keepReleases: 5
|
|
17422
17444
|
},
|
|
17423
17445
|
firewall: {
|
|
17424
17446
|
enabled: bootstrapAnswer("Enable host firewall? (yes/no)", "yes") === "yes",
|
|
@@ -17468,13 +17490,17 @@ async function cmdBootstrap(options, args) {
|
|
|
17468
17490
|
out.step("Review the pinned target and secret names, then repeat with --apply to contact the host.");
|
|
17469
17491
|
return 0;
|
|
17470
17492
|
}
|
|
17471
|
-
|
|
17493
|
+
const platformConfig = readBootstrapConfig(request.platformConfigFile);
|
|
17494
|
+
if (platformConfig.kind !== "platform")
|
|
17495
|
+
throw new Error("remote platform bootstrap requires a platform config");
|
|
17496
|
+
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(platformConfig) : undefined;
|
|
17497
|
+
out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode, { secrets: secrets2 }), null, 2));
|
|
17472
17498
|
return 0;
|
|
17473
17499
|
}
|
|
17474
17500
|
if (operation === "metal" && args[1] === "remote") {
|
|
17475
17501
|
const mode = args[2];
|
|
17476
|
-
if (!mode || !["apply", "genesis", "rehearsal", "status"].includes(mode) || args[3] !== undefined) {
|
|
17477
|
-
throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
17502
|
+
if (!mode || !["apply", "genesis", "rehearsal", "rehearsal-cleanup", "status"].includes(mode) || args[3] !== undefined) {
|
|
17503
|
+
throw new Error("Usage: fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status> --bootstrap-config <owner-only-request.json> [--apply]");
|
|
17478
17504
|
}
|
|
17479
17505
|
if (!options.bootstrapConfigPath)
|
|
17480
17506
|
throw new Error("remote metal bootstrap requires --bootstrap-config");
|
|
@@ -17509,7 +17535,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17509
17535
|
steps: [
|
|
17510
17536
|
"probe every declared public node",
|
|
17511
17537
|
"write secret-free acceptance evidence",
|
|
17512
|
-
"remove the
|
|
17538
|
+
"remove the completed non-secret checkpoint and node handoffs"
|
|
17513
17539
|
]
|
|
17514
17540
|
}, null, 2));
|
|
17515
17541
|
out.step("Review the evidence destination, then repeat with --apply to finalize and remove bootstrap capabilities.");
|
|
@@ -17520,7 +17546,11 @@ async function cmdBootstrap(options, args) {
|
|
|
17520
17546
|
}
|
|
17521
17547
|
if (args[2] !== undefined)
|
|
17522
17548
|
throw new Error("Cloudflare bootstrap operation must be plan/apply, verify or finalize");
|
|
17523
|
-
|
|
17549
|
+
const tokens = options.apply ? {
|
|
17550
|
+
tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17551
|
+
apiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17552
|
+
} : undefined;
|
|
17553
|
+
await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply, tokens);
|
|
17524
17554
|
return 0;
|
|
17525
17555
|
}
|
|
17526
17556
|
if (operation === "platform" && args[1] === "prepare") {
|
|
@@ -17542,6 +17572,9 @@ async function cmdBootstrap(options, args) {
|
|
|
17542
17572
|
out.line(JSON.stringify(await preparePlatformBootstrap(config2), null, 2));
|
|
17543
17573
|
return 0;
|
|
17544
17574
|
}
|
|
17575
|
+
const credentialStdin = operation === "platform" && args[1] === "credentials-stdin";
|
|
17576
|
+
if (credentialStdin && args[2] !== undefined)
|
|
17577
|
+
throw new Error("platform credential stdin accepts no additional positional arguments");
|
|
17545
17578
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
17546
17579
|
metal: existsSync11(METAL_BOOTSTRAP_STATE_PATH),
|
|
17547
17580
|
compute: existsSync11(BOOTSTRAP_STATE_PATH)
|
|
@@ -17585,7 +17618,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17585
17618
|
return 0;
|
|
17586
17619
|
}
|
|
17587
17620
|
if (!["platform", "repair"].includes(operation)) {
|
|
17588
|
-
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
17621
|
+
throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|rehearsal-cleanup|status>]|status|repair [--bootstrap-config <path>] [--apply]");
|
|
17589
17622
|
}
|
|
17590
17623
|
const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
|
|
17591
17624
|
throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
|
|
@@ -17602,7 +17635,21 @@ async function cmdBootstrap(options, args) {
|
|
|
17602
17635
|
out.step("Review the plan, then repeat with --apply as root.");
|
|
17603
17636
|
return 0;
|
|
17604
17637
|
}
|
|
17605
|
-
const
|
|
17638
|
+
const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config) : undefined;
|
|
17639
|
+
let stdinSecrets = secrets;
|
|
17640
|
+
if (config.kind === "platform" && credentialStdin) {
|
|
17641
|
+
const body = await Bun.stdin.text();
|
|
17642
|
+
if (body.length < 2 || body.length > 65536)
|
|
17643
|
+
throw new Error("bootstrap credential stdin has an invalid size");
|
|
17644
|
+
let parsed;
|
|
17645
|
+
try {
|
|
17646
|
+
parsed = JSON.parse(body);
|
|
17647
|
+
} catch {
|
|
17648
|
+
throw new Error("bootstrap credential stdin is not valid JSON");
|
|
17649
|
+
}
|
|
17650
|
+
stdinSecrets = validatePlatformBootstrapSecrets(config, parsed);
|
|
17651
|
+
}
|
|
17652
|
+
const result = await applyBootstrap(config, undefined, stdinSecrets);
|
|
17606
17653
|
out.line(JSON.stringify(result, null, 2));
|
|
17607
17654
|
return 0;
|
|
17608
17655
|
} catch (cause) {
|
|
@@ -18155,15 +18202,15 @@ function usage() {
|
|
|
18155
18202
|
strict three-node platform genesis directory; never apply locally
|
|
18156
18203
|
fz bootstrap platform cloudflare
|
|
18157
18204
|
Plan/apply attended Tunnel and DNS reconciliation
|
|
18158
|
-
using
|
|
18205
|
+
using hidden management and KV/Worker runtime token prompts
|
|
18159
18206
|
fz bootstrap platform cloudflare verify
|
|
18160
18207
|
From the operator laptop, prove every ordinary public
|
|
18161
18208
|
node /api/health using the 0600 checkpoint
|
|
18162
18209
|
fz bootstrap platform cloudflare finalize
|
|
18163
18210
|
Persist secret-free public-node acceptance, then remove
|
|
18164
|
-
the
|
|
18211
|
+
the completed non-secret checkpoint and node handoffs
|
|
18165
18212
|
fz bootstrap metal Plan/install an identity-only physical provisioner
|
|
18166
|
-
fz bootstrap metal remote <apply|genesis|rehearsal|status>
|
|
18213
|
+
fz bootstrap metal remote <apply|genesis|rehearsal|rehearsal-cleanup|status>
|
|
18167
18214
|
Install/bootstrap one pinned blank metal target, then
|
|
18168
18215
|
provision the exact selected three-node genesis seed
|
|
18169
18216
|
fz bootstrap status Verify persisted profile and supervised units
|
|
@@ -18228,7 +18275,8 @@ function usage() {
|
|
|
18228
18275
|
--json Machine-readable output
|
|
18229
18276
|
--apply Apply the reviewed operation instead of planning it
|
|
18230
18277
|
--bootstrap-config <p> Private JSON coordinates for unattended bootstrap;
|
|
18231
|
-
|
|
18278
|
+
secrets come from hidden prompts or authenticated stdin
|
|
18279
|
+
and are sealed immediately as systemd credentials
|
|
18232
18280
|
--output <path> Canonical absolute destination for generated bootstrap config
|
|
18233
18281
|
--root <path> Project root for project/deploy commands
|
|
18234
18282
|
--name <name> Project or deploy name during init
|