@forgezero/agent 0.1.57 → 0.1.59
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 +17 -15
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +22 -11
- package/dist/bootstrap.js +257 -159
- package/dist/cli/cloudflare-bootstrap.d.ts +14 -6
- package/dist/cloudflare-bootstrap.d.ts +10 -29
- package/dist/cloudflare-bootstrap.js +114 -81
- package/dist/cloudflare-edge.d.ts +28 -0
- package/dist/cloudflare-edge.js +78 -3
- 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 +392 -278
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +4 -1
- package/dist/operator-bootstrap.js +274 -179
- 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/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.59";
|
|
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";
|
|
@@ -11958,7 +11964,7 @@ function isPrivateDatabaseAddress(value) {
|
|
|
11958
11964
|
return false;
|
|
11959
11965
|
}
|
|
11960
11966
|
var endpoint = "https://api.cloudflare.com/client/v4";
|
|
11961
|
-
async function
|
|
11967
|
+
async function cfEnvelope(config, path, init = {}, fetcher = fetch) {
|
|
11962
11968
|
const response = await fetcher(`${endpoint}${path}`, {
|
|
11963
11969
|
...init,
|
|
11964
11970
|
headers: {
|
|
@@ -11971,7 +11977,68 @@ async function cf(config, path, init = {}, fetcher = fetch) {
|
|
|
11971
11977
|
if (!response.ok || body.success !== true) {
|
|
11972
11978
|
throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
|
|
11973
11979
|
}
|
|
11974
|
-
return body
|
|
11980
|
+
return body;
|
|
11981
|
+
}
|
|
11982
|
+
async function cf(config, path, init = {}, fetcher = fetch) {
|
|
11983
|
+
return (await cfEnvelope(config, path, init, fetcher)).result;
|
|
11984
|
+
}
|
|
11985
|
+
async function cfPages(config, path, perPage, fetcher) {
|
|
11986
|
+
const output = [];
|
|
11987
|
+
for (let page = 1;page <= 100; page += 1) {
|
|
11988
|
+
const separator = path.includes("?") ? "&" : "?";
|
|
11989
|
+
const envelope = await cfEnvelope(config, `${path}${separator}page=${page}&per_page=${perPage}`, {}, fetcher);
|
|
11990
|
+
const result = Array.isArray(envelope.result) ? envelope.result : [];
|
|
11991
|
+
output.push(...result);
|
|
11992
|
+
const totalPages = envelope.result_info?.total_pages;
|
|
11993
|
+
if (Number.isInteger(totalPages) ? page >= totalPages : result.length < perPage)
|
|
11994
|
+
return output;
|
|
11995
|
+
}
|
|
11996
|
+
throw new Error("Cloudflare pagination exceeded the reviewed 100-page bound");
|
|
11997
|
+
}
|
|
11998
|
+
var exactHexId = (value, label) => {
|
|
11999
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
12000
|
+
if (!/^[a-f0-9]{32}$/.test(normalized))
|
|
12001
|
+
throw new Error(`${label} is malformed`);
|
|
12002
|
+
return normalized;
|
|
12003
|
+
};
|
|
12004
|
+
async function discoverCloudflareBootstrapResources(config, fetcher = fetch) {
|
|
12005
|
+
const zoneName = config.zoneName.trim().toLowerCase().replace(/\.$/, "");
|
|
12006
|
+
const kvNamespaceTitle = config.kvNamespaceTitle.trim();
|
|
12007
|
+
if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(zoneName)) {
|
|
12008
|
+
throw new Error("Cloudflare zone name is invalid");
|
|
12009
|
+
}
|
|
12010
|
+
if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
|
|
12011
|
+
throw new Error("Cloudflare KV namespace title is invalid");
|
|
12012
|
+
}
|
|
12013
|
+
const [managementStatus, runtimeStatus] = await Promise.all([
|
|
12014
|
+
cf({ apiToken: config.tunnelToken }, "/user/tokens/verify", {}, fetcher),
|
|
12015
|
+
cf({ apiToken: config.apiToken }, "/user/tokens/verify", {}, fetcher)
|
|
12016
|
+
]);
|
|
12017
|
+
if (managementStatus.status !== "active")
|
|
12018
|
+
throw new Error("CF_TUNNEL_TOKEN is not active");
|
|
12019
|
+
if (runtimeStatus.status !== "active")
|
|
12020
|
+
throw new Error("CF_API_TOKEN is not active");
|
|
12021
|
+
const zones = await cfPages({ apiToken: config.tunnelToken }, `/zones?name=${encodeURIComponent(zoneName)}&match=all&status=active`, 50, fetcher);
|
|
12022
|
+
const matchingZones = zones.filter(({ name }) => name?.trim().toLowerCase() === zoneName);
|
|
12023
|
+
if (matchingZones.length !== 1) {
|
|
12024
|
+
throw new Error(`Cloudflare zone ${zoneName} must resolve to exactly one active zone`);
|
|
12025
|
+
}
|
|
12026
|
+
const zoneId = exactHexId(matchingZones[0].id, "Cloudflare zone id");
|
|
12027
|
+
const accountId = exactHexId(matchingZones[0].account?.id, "Cloudflare account id");
|
|
12028
|
+
const namespaces = await cfPages({ apiToken: config.apiToken }, `/accounts/${accountId}/storage/kv/namespaces?order=title&direction=asc`, 1000, fetcher);
|
|
12029
|
+
const matchingNamespaces = namespaces.filter(({ title }) => title === kvNamespaceTitle);
|
|
12030
|
+
if (matchingNamespaces.length !== 1) {
|
|
12031
|
+
throw new Error(`Cloudflare KV namespace ${kvNamespaceTitle} must resolve to exactly one namespace`);
|
|
12032
|
+
}
|
|
12033
|
+
const kvNamespaceId = exactHexId(matchingNamespaces[0].id, "Cloudflare KV namespace id");
|
|
12034
|
+
if (config.workerScriptName) {
|
|
12035
|
+
await verifyCloudflareWorkerDurableObjects({
|
|
12036
|
+
accountId,
|
|
12037
|
+
scriptName: config.workerScriptName,
|
|
12038
|
+
apiToken: config.apiToken
|
|
12039
|
+
}, fetcher);
|
|
12040
|
+
}
|
|
12041
|
+
return { accountId, zoneId, kvNamespaceId };
|
|
11975
12042
|
}
|
|
11976
12043
|
async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
|
|
11977
12044
|
const [address, prefixText, ...extra] = config.network.split("/");
|
|
@@ -12085,7 +12152,7 @@ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
|
|
|
12085
12152
|
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
|
|
12086
12153
|
throw new Error("Cloudflare Worker script name is invalid");
|
|
12087
12154
|
}
|
|
12088
|
-
const namespaces = await
|
|
12155
|
+
const namespaces = await cfPages(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces`, 1000, fetcher);
|
|
12089
12156
|
const owned = namespaces.filter(({ script }) => script === config.scriptName);
|
|
12090
12157
|
if (!owned.length)
|
|
12091
12158
|
throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
|
|
@@ -12157,13 +12224,23 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
|
12157
12224
|
}
|
|
12158
12225
|
return { tunnel, connectorToken, created };
|
|
12159
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
|
+
}
|
|
12160
12239
|
|
|
12161
12240
|
// src/cloudflare-bootstrap.ts
|
|
12162
12241
|
var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
|
|
12163
|
-
var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
12164
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;
|
|
12165
12243
|
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
12166
|
-
var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
|
|
12167
12244
|
var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
12168
12245
|
async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
|
|
12169
12246
|
const metadata = await handle.stat();
|
|
@@ -12197,26 +12274,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
|
|
|
12197
12274
|
await handle?.close();
|
|
12198
12275
|
}
|
|
12199
12276
|
}
|
|
12200
|
-
|
|
12201
|
-
|
|
12202
|
-
|
|
12203
|
-
|
|
12204
|
-
|
|
12205
|
-
}
|
|
12206
|
-
async function readCloudflareBootstrapTokens(files) {
|
|
12207
|
-
const unsupported = Object.keys(files).filter((key) => ![
|
|
12208
|
-
"tunnelTokenFile",
|
|
12209
|
-
"apiTokenFile"
|
|
12210
|
-
].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));
|
|
12211
12282
|
if (unsupported.length)
|
|
12212
|
-
throw new Error(`Cloudflare bootstrap
|
|
12213
|
-
|
|
12214
|
-
|
|
12215
|
-
|
|
12216
|
-
|
|
12217
|
-
readOwnerApiToken(files.tunnelTokenFile),
|
|
12218
|
-
readOwnerApiToken(files.apiTokenFile)
|
|
12219
|
-
]);
|
|
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");
|
|
12220
12288
|
if (tunnelToken === apiToken) {
|
|
12221
12289
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
|
|
12222
12290
|
}
|
|
@@ -12392,7 +12460,7 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
12392
12460
|
return {
|
|
12393
12461
|
format: 1,
|
|
12394
12462
|
kind: "forgezero-cloudflare-bootstrap-plan",
|
|
12395
|
-
mode: "attended-
|
|
12463
|
+
mode: "attended-hidden-input",
|
|
12396
12464
|
outputFile: resolve5(outputPath),
|
|
12397
12465
|
coordinates,
|
|
12398
12466
|
operations: [
|
|
@@ -12400,19 +12468,19 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
12400
12468
|
...coordinates.realtime ? [
|
|
12401
12469
|
"prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
|
|
12402
12470
|
] : [],
|
|
12403
|
-
"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",
|
|
12404
12472
|
...coordinates.nodes.some(({ mesh }) => mesh) ? [
|
|
12405
|
-
"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",
|
|
12406
12474
|
"reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
|
|
12407
12475
|
] : [],
|
|
12408
12476
|
"preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
|
|
12409
12477
|
"reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
|
|
12410
|
-
"write one
|
|
12478
|
+
"write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
|
|
12411
12479
|
],
|
|
12412
12480
|
secrets: [
|
|
12413
|
-
"API tokens are
|
|
12414
|
-
"
|
|
12415
|
-
"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"
|
|
12416
12484
|
]
|
|
12417
12485
|
};
|
|
12418
12486
|
}
|
|
@@ -12477,25 +12545,15 @@ async function readExistingOutput(path) {
|
|
|
12477
12545
|
const resources = output.resources;
|
|
12478
12546
|
const unsupportedResource = Object.keys(resources).filter((key) => ![
|
|
12479
12547
|
"kvNamespaceId",
|
|
12480
|
-
"apiToken",
|
|
12481
|
-
"realtime",
|
|
12482
12548
|
"nodes"
|
|
12483
12549
|
].includes(key));
|
|
12484
12550
|
if (unsupportedResource.length) {
|
|
12485
12551
|
throw new Error(`${resolve5(path)} resources contain unsupported field ${unsupportedResource[0]}`);
|
|
12486
12552
|
}
|
|
12487
12553
|
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
12488
|
-
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !
|
|
12554
|
+
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
|
|
12489
12555
|
throw new Error(`${resolve5(path)} has malformed Cloudflare bootstrap resources`);
|
|
12490
12556
|
}
|
|
12491
|
-
if (coordinates.realtime) {
|
|
12492
|
-
const realtime = resources.realtime;
|
|
12493
|
-
if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
|
|
12494
|
-
throw new Error(`${resolve5(path)} has malformed Cloudflare realtime resources`);
|
|
12495
|
-
}
|
|
12496
|
-
} else if (resources.realtime !== undefined) {
|
|
12497
|
-
throw new Error(`${resolve5(path)} contains undeclared Cloudflare realtime resources`);
|
|
12498
|
-
}
|
|
12499
12557
|
const seen = new Set;
|
|
12500
12558
|
for (const item of resources.nodes) {
|
|
12501
12559
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -12508,16 +12566,15 @@ async function readExistingOutput(path) {
|
|
|
12508
12566
|
"service",
|
|
12509
12567
|
"tunnelName",
|
|
12510
12568
|
"tunnelId",
|
|
12511
|
-
"connectorToken",
|
|
12512
12569
|
"mesh"
|
|
12513
12570
|
].includes(key));
|
|
12514
12571
|
const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
|
|
12515
|
-
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 ?? ""))) {
|
|
12516
12573
|
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare node resource`);
|
|
12517
12574
|
}
|
|
12518
12575
|
if (expected.mesh) {
|
|
12519
12576
|
const mesh = node.mesh;
|
|
12520
|
-
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 ?? ""))) {
|
|
12521
12578
|
throw new Error(`${resolve5(path)} has a malformed or unbound Cloudflare Mesh resource`);
|
|
12522
12579
|
}
|
|
12523
12580
|
} else if (node.mesh !== undefined) {
|
|
@@ -12561,11 +12618,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12561
12618
|
"hostname",
|
|
12562
12619
|
"service",
|
|
12563
12620
|
"tunnelId",
|
|
12564
|
-
"connectorToken",
|
|
12565
12621
|
"accountId",
|
|
12566
12622
|
"zoneId",
|
|
12567
12623
|
"kvNamespaceId",
|
|
12568
|
-
"apiToken",
|
|
12569
12624
|
"mesh",
|
|
12570
12625
|
"realtime"
|
|
12571
12626
|
].includes(key));
|
|
@@ -12576,11 +12631,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12576
12631
|
try {
|
|
12577
12632
|
service = normalizeService(output.service ?? "");
|
|
12578
12633
|
} catch {}
|
|
12579
|
-
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 ?? "")) {
|
|
12580
12635
|
throw new Error("Cloudflare host handoff is malformed or belongs to another node");
|
|
12581
12636
|
}
|
|
12582
12637
|
if (output.mesh !== undefined) {
|
|
12583
|
-
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) => {
|
|
12584
12639
|
try {
|
|
12585
12640
|
return privateMeshCidr(route) !== route;
|
|
12586
12641
|
} catch {
|
|
@@ -12609,7 +12664,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
12609
12664
|
} catch {
|
|
12610
12665
|
throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
|
|
12611
12666
|
}
|
|
12612
|
-
if (
|
|
12667
|
+
if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
|
|
12613
12668
|
throw new Error("Cloudflare host handoff contains malformed realtime credentials");
|
|
12614
12669
|
}
|
|
12615
12670
|
}
|
|
@@ -12674,18 +12729,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
12674
12729
|
hostname: node.hostname,
|
|
12675
12730
|
service: node.service,
|
|
12676
12731
|
tunnelId: node.tunnelId,
|
|
12677
|
-
connectorToken: node.connectorToken,
|
|
12678
12732
|
accountId: output.coordinates.accountId,
|
|
12679
12733
|
zoneId: output.coordinates.zoneId,
|
|
12680
12734
|
kvNamespaceId: output.resources.kvNamespaceId,
|
|
12681
|
-
|
|
12682
|
-
...output.coordinates.realtime && output.resources.realtime ? { realtime: {
|
|
12683
|
-
...output.coordinates.realtime,
|
|
12684
|
-
...output.resources.realtime
|
|
12685
|
-
} } : {},
|
|
12735
|
+
...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
|
|
12686
12736
|
...node.mesh ? { mesh: {
|
|
12687
12737
|
connectorId: node.mesh.connectorId,
|
|
12688
|
-
connectorToken: node.mesh.connectorToken,
|
|
12689
12738
|
routes: node.mesh.routes
|
|
12690
12739
|
} } : {}
|
|
12691
12740
|
};
|
|
@@ -12693,7 +12742,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
12693
12742
|
}
|
|
12694
12743
|
}
|
|
12695
12744
|
var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
12696
|
-
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
|
+
});
|
|
12697
12749
|
async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
|
|
12698
12750
|
const coordinates = validateCloudflareBootstrapCoordinates(input);
|
|
12699
12751
|
const absoluteOutput = resolve5(outputPath);
|
|
@@ -12711,10 +12763,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12711
12763
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
|
|
12712
12764
|
}
|
|
12713
12765
|
await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
|
|
12714
|
-
const realtime = coordinates.realtime ?
|
|
12715
|
-
publishSecret: newRealtimeSecret(),
|
|
12716
|
-
ticketSecret: newRealtimeSecret()
|
|
12717
|
-
} : undefined;
|
|
12766
|
+
const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
|
|
12718
12767
|
if (coordinates.realtime && realtime) {
|
|
12719
12768
|
await verifyCloudflareWorkerDurableObjects({
|
|
12720
12769
|
accountId: coordinates.accountId,
|
|
@@ -12729,8 +12778,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12729
12778
|
coordinates,
|
|
12730
12779
|
resources: {
|
|
12731
12780
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12732
|
-
apiToken,
|
|
12733
|
-
realtime,
|
|
12734
12781
|
nodes: existing?.resources.nodes ?? []
|
|
12735
12782
|
}
|
|
12736
12783
|
});
|
|
@@ -12749,7 +12796,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12749
12796
|
let resource;
|
|
12750
12797
|
let created = false;
|
|
12751
12798
|
if (checkpointed) {
|
|
12752
|
-
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)) {
|
|
12753
12800
|
throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
|
|
12754
12801
|
}
|
|
12755
12802
|
resource = checkpointed;
|
|
@@ -12770,15 +12817,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12770
12817
|
}, fetcher);
|
|
12771
12818
|
mesh = {
|
|
12772
12819
|
...node.mesh,
|
|
12773
|
-
connectorId: ensured.connector.id
|
|
12774
|
-
connectorToken: ensured.connectorToken
|
|
12820
|
+
connectorId: ensured.connector.id
|
|
12775
12821
|
};
|
|
12776
12822
|
}
|
|
12777
12823
|
const { mesh: _declaredMesh, ...publicNode } = node;
|
|
12778
12824
|
resource = {
|
|
12779
12825
|
...publicNode,
|
|
12780
12826
|
tunnelId: tunnel.tunnel.id,
|
|
12781
|
-
connectorToken: tunnel.connectorToken,
|
|
12782
12827
|
...mesh ? { mesh } : {}
|
|
12783
12828
|
};
|
|
12784
12829
|
}
|
|
@@ -12792,8 +12837,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12792
12837
|
coordinates,
|
|
12793
12838
|
resources: {
|
|
12794
12839
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12795
|
-
apiToken,
|
|
12796
|
-
...realtime ? { realtime } : {},
|
|
12797
12840
|
nodes: [...nodeResources]
|
|
12798
12841
|
}
|
|
12799
12842
|
});
|
|
@@ -12837,8 +12880,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
12837
12880
|
coordinates,
|
|
12838
12881
|
resources: {
|
|
12839
12882
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
12840
|
-
apiToken,
|
|
12841
|
-
...realtime ? { realtime } : {},
|
|
12842
12883
|
nodes: nodeResources
|
|
12843
12884
|
},
|
|
12844
12885
|
created: {
|
|
@@ -12860,10 +12901,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
|
|
|
12860
12901
|
nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
|
|
12861
12902
|
};
|
|
12862
12903
|
}
|
|
12863
|
-
if (!request.
|
|
12864
|
-
throw new Error("Cloudflare apply requires exactly two
|
|
12904
|
+
if (!request.tokens) {
|
|
12905
|
+
throw new Error("Cloudflare apply requires exactly two attended API tokens");
|
|
12865
12906
|
}
|
|
12866
|
-
const tokens =
|
|
12907
|
+
const tokens = validateCloudflareBootstrapTokens(request.tokens);
|
|
12867
12908
|
const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
|
|
12868
12909
|
return {
|
|
12869
12910
|
format: 1,
|
|
@@ -13121,6 +13162,78 @@ var PLATFORM_BOOTSTRAP_PROFILES = [
|
|
|
13121
13162
|
"platform-db-api",
|
|
13122
13163
|
"platform-api"
|
|
13123
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
|
+
}
|
|
13124
13237
|
var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
|
|
13125
13238
|
function resolveInstalledBootstrapKind(states) {
|
|
13126
13239
|
if (states.compute && states.metal) {
|
|
@@ -13136,6 +13249,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
|
|
|
13136
13249
|
var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
|
|
13137
13250
|
var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
|
|
13138
13251
|
var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
|
|
13252
|
+
var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
|
|
13139
13253
|
var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
|
|
13140
13254
|
var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
|
|
13141
13255
|
var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
|
|
@@ -13144,7 +13258,6 @@ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
|
|
|
13144
13258
|
var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
|
|
13145
13259
|
var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
|
|
13146
13260
|
var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
|
|
13147
|
-
var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
|
|
13148
13261
|
var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
|
|
13149
13262
|
var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
|
|
13150
13263
|
var CONTROL_SOCKET = "/run/forgezero/control.sock";
|
|
@@ -13222,8 +13335,6 @@ function validateBootstrapConfig(value) {
|
|
|
13222
13335
|
if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
|
|
13223
13336
|
throw new Error("tenant API must be public HTTPS or loopback HTTP");
|
|
13224
13337
|
}
|
|
13225
|
-
if (!value.enrolTokenFile)
|
|
13226
|
-
throw new Error("enrolled-compute activation requires an enrolment-token file");
|
|
13227
13338
|
if (value.bootstrapRunner) {
|
|
13228
13339
|
if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
|
|
13229
13340
|
throw new Error("bootstrap runner SSH private-key file must be absolute");
|
|
@@ -13267,8 +13378,6 @@ function validateBootstrapConfig(value) {
|
|
|
13267
13378
|
if (!/^[a-z0-9](?:[a-z0-9:_-]{0,126}[a-z0-9])?$/.test(value.computeReference)) {
|
|
13268
13379
|
throw new Error("platform compute reference is malformed");
|
|
13269
13380
|
}
|
|
13270
|
-
if (!value.database.bootstrapSecretFile)
|
|
13271
|
-
throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
|
|
13272
13381
|
const write = value.database.coordinators.map(privateOrigin);
|
|
13273
13382
|
if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
|
|
13274
13383
|
throw new Error("database coordinators must contain 1-16 unique private origins");
|
|
@@ -13283,12 +13392,6 @@ function validateBootstrapConfig(value) {
|
|
|
13283
13392
|
}
|
|
13284
13393
|
if (runtime.deployProfile !== value.environment)
|
|
13285
13394
|
throw new Error("runtime deployment profile disagrees with bootstrap environment");
|
|
13286
|
-
if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
|
|
13287
|
-
throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
|
|
13288
|
-
}
|
|
13289
|
-
if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
|
|
13290
|
-
throw new Error("Bootstrap email credential file must be an absolute single-line path");
|
|
13291
|
-
}
|
|
13292
13395
|
if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
|
|
13293
13396
|
throw new Error("runtime database topology disagrees with bootstrap topology");
|
|
13294
13397
|
}
|
|
@@ -13299,9 +13402,8 @@ function validateBootstrapConfig(value) {
|
|
|
13299
13402
|
if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
|
|
13300
13403
|
throw new Error("enabled firewall requires unique private cluster CIDRs");
|
|
13301
13404
|
}
|
|
13302
|
-
if (!["genesis-derived", "api-token"].includes(value.enrolment.source)
|
|
13303
|
-
throw new Error("platform enrolment source
|
|
13304
|
-
}
|
|
13405
|
+
if (!["genesis-derived", "api-token"].includes(value.enrolment.source))
|
|
13406
|
+
throw new Error("platform enrolment source is invalid");
|
|
13305
13407
|
value.runtime.environment = runtime;
|
|
13306
13408
|
return value;
|
|
13307
13409
|
}
|
|
@@ -13515,7 +13617,7 @@ async function waitForCloudflaredTunnel(host, expectedTunnelId) {
|
|
|
13515
13617
|
var derive = (root, label) => {
|
|
13516
13618
|
if (!/^[a-f0-9]{64}$/i.test(root))
|
|
13517
13619
|
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
13518
|
-
return
|
|
13620
|
+
return createHmac2("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
|
|
13519
13621
|
};
|
|
13520
13622
|
async function seal(host, name, destination2, value) {
|
|
13521
13623
|
if (host.exists(destination2))
|
|
@@ -13805,15 +13907,10 @@ async function bootstrapStatus(host = localBootstrapHost()) {
|
|
|
13805
13907
|
problems.push("durable Agent enrolment state is missing");
|
|
13806
13908
|
return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
|
|
13807
13909
|
}
|
|
13808
|
-
async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
13910
|
+
async function applyBootstrap(input, host = localBootstrapHost(), secrets, dependencies = {}) {
|
|
13809
13911
|
const config = validateBootstrapConfig(structuredClone(input));
|
|
13810
13912
|
if (host.uid() !== 0)
|
|
13811
13913
|
throw new Error("fz bootstrap --apply must run as root");
|
|
13812
|
-
if (config.kind === "enrolled-compute") {
|
|
13813
|
-
const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
|
|
13814
|
-
if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
|
|
13815
|
-
throw new Error("tenant enrolment token is malformed");
|
|
13816
|
-
}
|
|
13817
13914
|
let installed;
|
|
13818
13915
|
if (host.exists(STATE_PATH))
|
|
13819
13916
|
installed = parseStoredState(host.read(STATE_PATH));
|
|
@@ -13871,11 +13968,21 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13871
13968
|
throw new Error("bootstrap repair coordinates do not match the installed host identity");
|
|
13872
13969
|
}
|
|
13873
13970
|
}
|
|
13874
|
-
const platformPrivate = config.kind === "platform" ? {
|
|
13875
|
-
|
|
13876
|
-
|
|
13877
|
-
|
|
13878
|
-
|
|
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;
|
|
13879
13986
|
if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
|
|
13880
13987
|
privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
|
|
13881
13988
|
}
|
|
@@ -13884,17 +13991,31 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13884
13991
|
const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
|
|
13885
13992
|
host.mkdir(CREDS, 448);
|
|
13886
13993
|
host.mkdir("/var/lib/forgezero", 448);
|
|
13887
|
-
if (
|
|
13888
|
-
|
|
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);
|
|
13997
|
+
}
|
|
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);
|
|
14006
|
+
}
|
|
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);
|
|
13889
14009
|
}
|
|
13890
|
-
|
|
13891
|
-
|
|
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);
|
|
13892
14013
|
}
|
|
13893
|
-
if (
|
|
13894
|
-
await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL,
|
|
14014
|
+
if (realtimeSecrets && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
|
|
14015
|
+
await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, realtimeSecrets.ticketSecret);
|
|
13895
14016
|
}
|
|
13896
|
-
if (
|
|
13897
|
-
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL,
|
|
14017
|
+
if (connectorCapabilities?.meshConnectorToken && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
|
|
14018
|
+
await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, connectorCapabilities.meshConnectorToken);
|
|
13898
14019
|
}
|
|
13899
14020
|
await host.installAgent(config);
|
|
13900
14021
|
if (config.kind === "platform" && config.firewall.enabled) {
|
|
@@ -13921,22 +14042,15 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
13921
14042
|
}
|
|
13922
14043
|
await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
|
|
13923
14044
|
await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
|
|
13924
|
-
const
|
|
13925
|
-
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
|
|
13926
|
-
if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
|
|
13927
|
-
throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
|
|
13928
|
-
}
|
|
14045
|
+
const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "fz_smtp.password" : config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : undefined;
|
|
13929
14046
|
for (const [name, source] of Object.entries({
|
|
13930
14047
|
...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
|
|
13931
|
-
"backup
|
|
14048
|
+
"backup.s3.secretAccessKey": platformPrivate.backup
|
|
13932
14049
|
})) {
|
|
13933
14050
|
if (source) {
|
|
13934
14051
|
const destination2 = `${CREDS}/${name}.cred`;
|
|
13935
14052
|
if (!host.exists(destination2)) {
|
|
13936
14053
|
await seal(host, name, destination2, source);
|
|
13937
|
-
const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
|
|
13938
|
-
if (sourcePath)
|
|
13939
|
-
host.remove(sourcePath);
|
|
13940
14054
|
}
|
|
13941
14055
|
}
|
|
13942
14056
|
}
|
|
@@ -14006,7 +14120,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14006
14120
|
if (config.database.role === "master") {
|
|
14007
14121
|
const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
|
|
14008
14122
|
if (!host.exists(invite)) {
|
|
14009
|
-
host.write(invite, `plt_${
|
|
14123
|
+
host.write(invite, `plt_${randomBytes6(24).toString("hex")}
|
|
14010
14124
|
`, 384);
|
|
14011
14125
|
await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
|
|
14012
14126
|
}
|
|
@@ -14026,13 +14140,6 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14026
14140
|
verifiedAt: new Date().toISOString()
|
|
14027
14141
|
};
|
|
14028
14142
|
host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
|
|
14029
|
-
`, 384);
|
|
14030
|
-
}
|
|
14031
|
-
if (!alreadyEnrolled) {
|
|
14032
|
-
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}`)}`;
|
|
14033
|
-
if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
|
|
14034
|
-
throw new Error("platform enrolment token is malformed");
|
|
14035
|
-
host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
|
|
14036
14143
|
`, 384);
|
|
14037
14144
|
}
|
|
14038
14145
|
await checked2(host, [
|
|
@@ -14044,15 +14151,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
|
|
|
14044
14151
|
"deploy",
|
|
14045
14152
|
...config.database.role === "master" ? ["--release-executor"] : []
|
|
14046
14153
|
], "initial Agent deployment");
|
|
14047
|
-
if (!alreadyEnrolled) {
|
|
14048
|
-
await host.installAgent(config, PLATFORM_ENROL_SOURCE);
|
|
14049
|
-
if (config.enrolment.source === "api-token")
|
|
14050
|
-
host.remove(config.enrolment.tokenFile);
|
|
14051
|
-
}
|
|
14052
14154
|
}
|
|
14053
14155
|
if (config.cloudflareHandoff) {
|
|
14054
|
-
if (!host.exists(TUNNEL_CREDENTIAL) &&
|
|
14055
|
-
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);
|
|
14056
14158
|
}
|
|
14057
14159
|
if (!host.exists(TUNNEL_CREDENTIAL))
|
|
14058
14160
|
throw new Error("sealed cloudflared connector credential is missing");
|
|
@@ -14126,7 +14228,6 @@ function strictBootstrapDocument(value) {
|
|
|
14126
14228
|
"installWarp",
|
|
14127
14229
|
"cloudflareHandoff",
|
|
14128
14230
|
"realm",
|
|
14129
|
-
"enrolTokenFile",
|
|
14130
14231
|
"software",
|
|
14131
14232
|
"deploymentCredentials",
|
|
14132
14233
|
"bootstrapRunner"
|
|
@@ -14135,8 +14236,8 @@ function strictBootstrapDocument(value) {
|
|
|
14135
14236
|
exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
|
|
14136
14237
|
if (root.cloudflareHandoff !== undefined)
|
|
14137
14238
|
exactKeys2(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
|
|
14138
|
-
exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"
|
|
14139
|
-
exactKeys2(root.enrolment, ["source"
|
|
14239
|
+
exactKeys2(root.database, ["role", "agency", "serverMode", "address", "master", "coordinators"], "database config");
|
|
14240
|
+
exactKeys2(root.enrolment, ["source"], "platform enrolment config");
|
|
14140
14241
|
const runtime = exactKeys2(root.runtime, [
|
|
14141
14242
|
"environment",
|
|
14142
14243
|
"serviceUser",
|
|
@@ -14144,8 +14245,7 @@ function strictBootstrapDocument(value) {
|
|
|
14144
14245
|
"bluePort",
|
|
14145
14246
|
"greenPort",
|
|
14146
14247
|
"healthPath",
|
|
14147
|
-
"keepReleases"
|
|
14148
|
-
"credentialFiles"
|
|
14248
|
+
"keepReleases"
|
|
14149
14249
|
], "runtime config");
|
|
14150
14250
|
exactKeys2(runtime.environment, [
|
|
14151
14251
|
"softwareProfile",
|
|
@@ -14183,8 +14283,6 @@ function strictBootstrapDocument(value) {
|
|
|
14183
14283
|
"cloudflare",
|
|
14184
14284
|
"realtime"
|
|
14185
14285
|
], "runtime environment");
|
|
14186
|
-
if (runtime.credentialFiles !== undefined)
|
|
14187
|
-
exactKeys2(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
|
|
14188
14286
|
const environment = runtime.environment;
|
|
14189
14287
|
if (environment.email !== undefined) {
|
|
14190
14288
|
const email = exactKeys2(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
|
|
@@ -14268,10 +14366,10 @@ function localBootstrapHost() {
|
|
|
14268
14366
|
throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
|
|
14269
14367
|
return result;
|
|
14270
14368
|
},
|
|
14271
|
-
async installAgent(config
|
|
14369
|
+
async installAgent(config) {
|
|
14272
14370
|
const capabilities = await readCapabilities(localRunner);
|
|
14273
14371
|
const deployRoot = config.deployRoot ?? "/opt/forgezero";
|
|
14274
|
-
const hasBinding = config.kind === "enrolled-compute" ||
|
|
14372
|
+
const hasBinding = config.kind === "enrolled-compute" || existsSync8(ENROL_CREDENTIAL) || existsSync8("/var/lib/forgezero/enrolment.json");
|
|
14275
14373
|
if (config.kind === "platform") {
|
|
14276
14374
|
const lifecycle = config.database.role === "none" ? {
|
|
14277
14375
|
apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
|
|
@@ -14314,8 +14412,7 @@ function localBootstrapHost() {
|
|
|
14314
14412
|
telemetryEndpoint: config.telemetryEndpoint,
|
|
14315
14413
|
binPath: "/usr/local/lib/forgezero/agent/fz-agent",
|
|
14316
14414
|
sourceBinPath: PACKAGED_AGENT_BIN,
|
|
14317
|
-
...
|
|
14318
|
-
enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
|
|
14415
|
+
...hasBinding ? {
|
|
14319
14416
|
enrolTokenCredentialPath: ENROL_CREDENTIAL,
|
|
14320
14417
|
enrolStatePath: "/var/lib/forgezero/enrolment.json",
|
|
14321
14418
|
apiUrl: config.apiUrl,
|
|
@@ -14337,6 +14434,14 @@ function localBootstrapHost() {
|
|
|
14337
14434
|
// src/cli/cloudflare-bootstrap.ts
|
|
14338
14435
|
import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
|
|
14339
14436
|
import { dirname as dirname9, resolve as resolve6 } from "path";
|
|
14437
|
+
async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
|
|
14438
|
+
return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
|
|
14439
|
+
zoneName: input.zoneName,
|
|
14440
|
+
kvNamespaceTitle: input.kvNamespaceTitle,
|
|
14441
|
+
...input.workerScriptName ? { workerScriptName: input.workerScriptName } : {},
|
|
14442
|
+
...input.tokens
|
|
14443
|
+
}, dependencies.fetcher);
|
|
14444
|
+
}
|
|
14340
14445
|
var exactKeys3 = (value, allowed, label) => {
|
|
14341
14446
|
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
|
14342
14447
|
if (unknown.length)
|
|
@@ -14351,20 +14456,11 @@ function createCloudflareBootstrapCommandConfig(input) {
|
|
|
14351
14456
|
if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
|
|
14352
14457
|
throw new Error("Cloudflare bootstrap checkpointPath is required");
|
|
14353
14458
|
}
|
|
14354
|
-
for (const key of ["tunnelTokenFile", "apiTokenFile"]) {
|
|
14355
|
-
if (typeof input.tokenFiles?.[key] !== "string" || !input.tokenFiles[key].trim()) {
|
|
14356
|
-
throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
|
|
14357
|
-
}
|
|
14358
|
-
}
|
|
14359
14459
|
return {
|
|
14360
14460
|
format: 1,
|
|
14361
14461
|
kind: "forgezero-cloudflare-bootstrap-request",
|
|
14362
14462
|
checkpointPath: input.checkpointPath.trim(),
|
|
14363
|
-
coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
|
|
14364
|
-
tokenFiles: {
|
|
14365
|
-
tunnelTokenFile: input.tokenFiles.tunnelTokenFile.trim(),
|
|
14366
|
-
apiTokenFile: input.tokenFiles.apiTokenFile.trim()
|
|
14367
|
-
}
|
|
14463
|
+
coordinates: validateCloudflareBootstrapCoordinates(input.coordinates)
|
|
14368
14464
|
};
|
|
14369
14465
|
}
|
|
14370
14466
|
function readOwnerConfig(path) {
|
|
@@ -14392,7 +14488,7 @@ function readOwnerConfig(path) {
|
|
|
14392
14488
|
function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
14393
14489
|
const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
|
|
14394
14490
|
const baseDirectory = dirname9(resolve6(path));
|
|
14395
|
-
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"
|
|
14491
|
+
exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates"], "Cloudflare bootstrap config");
|
|
14396
14492
|
if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
|
|
14397
14493
|
throw new Error("Cloudflare bootstrap config format/kind is invalid");
|
|
14398
14494
|
}
|
|
@@ -14428,33 +14524,16 @@ function readCloudflareBootstrapCommandConfig(path, mode) {
|
|
|
14428
14524
|
}
|
|
14429
14525
|
}
|
|
14430
14526
|
const coordinates = validateCloudflareBootstrapCoordinates(coordinateSource);
|
|
14431
|
-
let tokenFiles;
|
|
14432
|
-
if (input.tokenFiles !== undefined) {
|
|
14433
|
-
const source = record2(input.tokenFiles, "Cloudflare bootstrap tokenFiles");
|
|
14434
|
-
const keys = ["tunnelTokenFile", "apiTokenFile"];
|
|
14435
|
-
exactKeys3(source, keys, "Cloudflare bootstrap tokenFiles");
|
|
14436
|
-
for (const key of keys) {
|
|
14437
|
-
if (typeof source[key] !== "string" || !source[key].trim()) {
|
|
14438
|
-
throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
|
|
14439
|
-
}
|
|
14440
|
-
}
|
|
14441
|
-
tokenFiles = {
|
|
14442
|
-
tunnelTokenFile: resolve6(baseDirectory, source.tunnelTokenFile),
|
|
14443
|
-
apiTokenFile: resolve6(baseDirectory, source.apiTokenFile)
|
|
14444
|
-
};
|
|
14445
|
-
}
|
|
14446
|
-
if (mode === "apply" && !tokenFiles) {
|
|
14447
|
-
throw new Error("Cloudflare apply config requires exactly two owner-only tokenFiles paths");
|
|
14448
|
-
}
|
|
14449
14527
|
return {
|
|
14450
14528
|
mode,
|
|
14451
14529
|
coordinates,
|
|
14452
|
-
checkpointPath: resolve6(baseDirectory, input.checkpointPath)
|
|
14453
|
-
...tokenFiles ? { tokenFiles } : {}
|
|
14530
|
+
checkpointPath: resolve6(baseDirectory, input.checkpointPath)
|
|
14454
14531
|
};
|
|
14455
14532
|
}
|
|
14456
|
-
async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {}) {
|
|
14533
|
+
async function runCloudflareBootstrapCommand(configPath, apply, tokens, dependencies = {}) {
|
|
14457
14534
|
const request = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
|
|
14535
|
+
if (apply)
|
|
14536
|
+
request.tokens = tokens;
|
|
14458
14537
|
const evidence = await (dependencies.run ?? runAttendedCloudflareBootstrap)(request);
|
|
14459
14538
|
(dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
|
|
14460
14539
|
`);
|
|
@@ -14492,7 +14571,7 @@ async function runCloudflareBootstrapFinalizeCommand(configPath, dependencies =
|
|
|
14492
14571
|
}
|
|
14493
14572
|
|
|
14494
14573
|
// src/metal-bootstrap.ts
|
|
14495
|
-
import { createHash as createHash4, randomBytes as
|
|
14574
|
+
import { createHash as createHash4, randomBytes as randomBytes7 } from "crypto";
|
|
14496
14575
|
import {
|
|
14497
14576
|
chmodSync as chmodSync4,
|
|
14498
14577
|
chownSync,
|
|
@@ -15228,7 +15307,7 @@ async function applyMetalBootstrap(config, options) {
|
|
|
15228
15307
|
atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
|
|
15229
15308
|
`, 384);
|
|
15230
15309
|
if (!existsSync9(SEED_CREDENTIAL_PATH)) {
|
|
15231
|
-
const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() :
|
|
15310
|
+
const seed = config.agentSeedFile ? readFileSync10(config.agentSeedFile, "utf8").trim() : randomBytes7(32).toString("base64url");
|
|
15232
15311
|
if (seed.length < 32 || /[\0\r\n]/.test(seed))
|
|
15233
15312
|
throw new MetalBootstrapError("metal Agent seed is invalid");
|
|
15234
15313
|
await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
|
|
@@ -15394,7 +15473,7 @@ async function metalBootstrapStatus(exec = defaultExec2) {
|
|
|
15394
15473
|
}
|
|
15395
15474
|
|
|
15396
15475
|
// src/operator-bootstrap.ts
|
|
15397
|
-
import { createHash as createHash5, randomBytes as
|
|
15476
|
+
import { createHash as createHash5, randomBytes as randomBytes8 } from "crypto";
|
|
15398
15477
|
import { lstatSync as lstatSync6, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync11 } from "fs";
|
|
15399
15478
|
import { isIP as isIP6 } from "net";
|
|
15400
15479
|
import { tmpdir } from "os";
|
|
@@ -15682,12 +15761,15 @@ function planOperatorMetalBootstrap(request, mode) {
|
|
|
15682
15761
|
};
|
|
15683
15762
|
}
|
|
15684
15763
|
var secretSources = (config) => [
|
|
15685
|
-
["cluster-code", config.database.bootstrapSecretFile],
|
|
15686
|
-
...config.enrolment.tokenFile ? [["enrol-token", config.enrolment.tokenFile]] : [],
|
|
15687
|
-
...config.runtime.credentialFiles?.emailSecret ? [["email-secret", config.runtime.credentialFiles.emailSecret]] : [],
|
|
15688
|
-
...config.runtime.credentialFiles?.backupS3Secret ? [["backup-s3-secret", config.runtime.credentialFiles.backupS3Secret]] : [],
|
|
15689
15764
|
...config.cloudflareHandoff ? [["cloudflare-handoff", config.cloudflareHandoff.handoffFile]] : []
|
|
15690
15765
|
];
|
|
15766
|
+
var attendedSecretNames = (config) => [
|
|
15767
|
+
"cluster-bootstrap-code",
|
|
15768
|
+
config.runtime.environment.email?.provider === "jetemail" ? "fz_jetemail.apiKey" : "fz_smtp.password",
|
|
15769
|
+
...config.enrolment.source === "api-token" ? ["enrol-token"] : [],
|
|
15770
|
+
...config.runtime.environment.backup ? ["backup.s3.secretAccessKey"] : [],
|
|
15771
|
+
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? ["CF_TUNNEL_TOKEN", "CF_API_TOKEN"] : []
|
|
15772
|
+
];
|
|
15691
15773
|
function planOperatorPlatformBootstrap(request, mode) {
|
|
15692
15774
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
15693
15775
|
if (config.kind !== "platform")
|
|
@@ -15709,7 +15791,7 @@ function planOperatorPlatformBootstrap(request, mode) {
|
|
|
15709
15791
|
`run typed fz bootstrap platform${mode === "prepare" ? " prepare" : ""} --apply`,
|
|
15710
15792
|
"remove transient local and remote staging data"
|
|
15711
15793
|
],
|
|
15712
|
-
secretInputs: secretSources(config).map(([name]) => name)
|
|
15794
|
+
secretInputs: mode === "apply" ? [...attendedSecretNames(config), ...secretSources(config).map(([name]) => name)] : []
|
|
15713
15795
|
};
|
|
15714
15796
|
}
|
|
15715
15797
|
var defaultExec3 = async (argv2, options = {}) => {
|
|
@@ -15826,15 +15908,7 @@ function stageConfig(config, directory) {
|
|
|
15826
15908
|
writeFileSync11(local, bytes, { mode: 384, flag: "wx" });
|
|
15827
15909
|
staged.push(name);
|
|
15828
15910
|
const remotePath = `${REMOTE_STAGE}/${name}`;
|
|
15829
|
-
if (name === "
|
|
15830
|
-
rewritten.database.bootstrapSecretFile = remotePath;
|
|
15831
|
-
else if (name === "enrol-token")
|
|
15832
|
-
rewritten.enrolment.tokenFile = remotePath;
|
|
15833
|
-
else if (name === "email-secret")
|
|
15834
|
-
rewritten.runtime.credentialFiles.emailSecret = remotePath;
|
|
15835
|
-
else if (name === "backup-s3-secret")
|
|
15836
|
-
rewritten.runtime.credentialFiles.backupS3Secret = remotePath;
|
|
15837
|
-
else if (name === "cloudflare-handoff")
|
|
15911
|
+
if (name === "cloudflare-handoff")
|
|
15838
15912
|
rewritten.cloudflareHandoff.handoffFile = remotePath;
|
|
15839
15913
|
}
|
|
15840
15914
|
const path = join10(directory, "platform-config.json");
|
|
@@ -15918,7 +15992,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
15918
15992
|
socketPath(request.target.agentSocket);
|
|
15919
15993
|
const exec = options.exec ?? defaultExec3;
|
|
15920
15994
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-bootstrap-"));
|
|
15921
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
15995
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
|
|
15922
15996
|
let knownHosts = "";
|
|
15923
15997
|
try {
|
|
15924
15998
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -15929,6 +16003,7 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
15929
16003
|
const config = readBootstrapConfig(request.platformConfigFile);
|
|
15930
16004
|
if (config.kind !== "platform")
|
|
15931
16005
|
throw new Error("operator bootstrap requires a platform config");
|
|
16006
|
+
const secrets = mode === "apply" ? validatePlatformBootstrapSecrets(config, options.secrets) : undefined;
|
|
15932
16007
|
const staged = stageConfig(config, directory);
|
|
15933
16008
|
await installPackagedAgent(request, knownHosts, exec, directory, remoteTemp, options);
|
|
15934
16009
|
await copy(exec, request, knownHosts, staged.path, `${remoteTemp}/platform-config.json`, true);
|
|
@@ -15939,8 +16014,11 @@ async function applyOperatorPlatformBootstrap(request, mode, options = {}) {
|
|
|
15939
16014
|
const command = ["/usr/bin/sudo", "-n", "/usr/local/bin/fz", "bootstrap", "platform"];
|
|
15940
16015
|
if (mode === "prepare")
|
|
15941
16016
|
command.push("prepare");
|
|
16017
|
+
else
|
|
16018
|
+
command.push("credentials-stdin");
|
|
15942
16019
|
command.push("--bootstrap-config", `${REMOTE_STAGE}/platform-config.json`, "--apply");
|
|
15943
|
-
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap")
|
|
16020
|
+
const output = await remote(exec, request, knownHosts, command, "remote typed bootstrap", mode === "apply", secrets ? `${JSON.stringify(secrets)}
|
|
16021
|
+
` : undefined);
|
|
15944
16022
|
return { plan, output };
|
|
15945
16023
|
} finally {
|
|
15946
16024
|
if (knownHosts) {
|
|
@@ -15957,7 +16035,7 @@ async function applyOperatorMetalBootstrap(request, mode, options = {}) {
|
|
|
15957
16035
|
socketPath(request.target.agentSocket);
|
|
15958
16036
|
const exec = options.exec ?? defaultExec3;
|
|
15959
16037
|
const directory = mkdtempSync(join10(tmpdir(), "forgezero-operator-metal-"));
|
|
15960
|
-
const remoteTemp = `/tmp/forgezero-operator-${
|
|
16038
|
+
const remoteTemp = `/tmp/forgezero-operator-${randomBytes8(12).toString("hex")}`;
|
|
15961
16039
|
let knownHosts = "";
|
|
15962
16040
|
try {
|
|
15963
16041
|
knownHosts = writeKnownHosts(request, directory);
|
|
@@ -16032,8 +16110,7 @@ function platformGenesisBootstrapConfigs(template, nodes) {
|
|
|
16032
16110
|
serverMode: "default",
|
|
16033
16111
|
address: guest.address,
|
|
16034
16112
|
...role === "joiner" ? { master } : {},
|
|
16035
|
-
coordinators
|
|
16036
|
-
bootstrapSecretFile: template.database.bootstrapSecretFile
|
|
16113
|
+
coordinators
|
|
16037
16114
|
};
|
|
16038
16115
|
config.enrolment = { source: "genesis-derived" };
|
|
16039
16116
|
config.installCloudflared = true;
|
|
@@ -16915,8 +16992,10 @@ async function cmdStatus(options) {
|
|
|
16915
16992
|
async function cmdAgent(options, args) {
|
|
16916
16993
|
if (args[0] === "activate") {
|
|
16917
16994
|
try {
|
|
16918
|
-
|
|
16919
|
-
|
|
16995
|
+
const credentialStdin = args[1] === "credentials-stdin";
|
|
16996
|
+
if (args[1] !== undefined && !credentialStdin || args[2] !== undefined) {
|
|
16997
|
+
throw new Error("internal Agent activation accepts only credentials-stdin");
|
|
16998
|
+
}
|
|
16920
16999
|
if (!options.bootstrapConfigPath)
|
|
16921
17000
|
throw new Error("internal Agent activation requires --bootstrap-config");
|
|
16922
17001
|
const config = readBootstrapConfig(options.bootstrapConfigPath);
|
|
@@ -16927,7 +17006,19 @@ async function cmdAgent(options, args) {
|
|
|
16927
17006
|
out.line(JSON.stringify(plan2, null, 2));
|
|
16928
17007
|
return 0;
|
|
16929
17008
|
}
|
|
16930
|
-
|
|
17009
|
+
if (!credentialStdin)
|
|
17010
|
+
throw new Error("internal Agent activation requires credentials-stdin");
|
|
17011
|
+
const body = await Bun.stdin.text();
|
|
17012
|
+
if (body.length < 2 || body.length > 65536)
|
|
17013
|
+
throw new Error("activation credential stdin has an invalid size");
|
|
17014
|
+
let parsed;
|
|
17015
|
+
try {
|
|
17016
|
+
parsed = JSON.parse(body);
|
|
17017
|
+
} catch {
|
|
17018
|
+
throw new Error("activation credential stdin is not valid JSON");
|
|
17019
|
+
}
|
|
17020
|
+
const secrets = validateEnrolledComputeBootstrapSecrets(config, parsed);
|
|
17021
|
+
out.line(JSON.stringify(await applyBootstrap(config, undefined, secrets), null, 2));
|
|
16931
17022
|
return 0;
|
|
16932
17023
|
} catch (cause) {
|
|
16933
17024
|
out.fail(cause instanceof Error ? cause.message : String(cause));
|
|
@@ -16938,7 +17029,6 @@ async function cmdAgent(options, args) {
|
|
|
16938
17029
|
out.fail("Usage: fz agent install [--apply]");
|
|
16939
17030
|
return 1;
|
|
16940
17031
|
}
|
|
16941
|
-
const enrolTokenSourcePath = "/run/forgezero-enrol-token";
|
|
16942
17032
|
const enrolTokenCredentialPath = "/etc/forgezero/creds/enrol-token.cred";
|
|
16943
17033
|
const enrolStatePath = "/var/lib/forgezero/enrolment.json";
|
|
16944
17034
|
const gitCredentialPath = process.env.FZ_GIT_CREDENTIAL_PATH ?? "/etc/forgezero/creds/git-deploy-key.cred";
|
|
@@ -16978,7 +17068,6 @@ async function cmdAgent(options, args) {
|
|
|
16978
17068
|
telemetryEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
|
|
16979
17069
|
...options.enrol ? {
|
|
16980
17070
|
pullDeployments: true,
|
|
16981
|
-
enrolTokenSourcePath,
|
|
16982
17071
|
enrolTokenCredentialPath,
|
|
16983
17072
|
enrolStatePath,
|
|
16984
17073
|
deployRoot: process.env.FZ_DEPLOY_ROOT ?? "/opt/forgezero"
|
|
@@ -17016,23 +17105,20 @@ async function cmdAgent(options, args) {
|
|
|
17016
17105
|
out.ok(`Wrote ${auxiliary.path}`);
|
|
17017
17106
|
}
|
|
17018
17107
|
if (options.enrol) {
|
|
17019
|
-
if (existsSync11(
|
|
17020
|
-
const
|
|
17021
|
-
|
|
17022
|
-
if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
|
|
17023
|
-
throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
|
|
17024
|
-
}
|
|
17025
|
-
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
17026
|
-
throw new Error("The preloaded enrolment token is malformed.");
|
|
17027
|
-
}
|
|
17028
|
-
} else {
|
|
17029
|
-
const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", "ForgeZero one-time enrolment token:"], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
|
|
17030
|
-
const token = (await new Response(prompt.stdout).text()).trim();
|
|
17031
|
-
if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
|
|
17108
|
+
if (!existsSync11(enrolStatePath) && !existsSync11(enrolTokenCredentialPath)) {
|
|
17109
|
+
const token = await bootstrapSecret("ForgeZero one-time enrolment token");
|
|
17110
|
+
if (!/^fze_[A-Za-z0-9_-]{40,100}$/.test(token))
|
|
17032
17111
|
throw new Error("A valid fze_ enrolment token was not provided.");
|
|
17112
|
+
mkdirSync11(dirname12(enrolTokenCredentialPath), { recursive: true, mode: 448 });
|
|
17113
|
+
const sealing = Bun.spawn(["systemd-creds", "encrypt", "--name=enrol-token", "-", enrolTokenCredentialPath], { stdin: "pipe", stdout: "ignore", stderr: "pipe" });
|
|
17114
|
+
if (sealing.stdin && typeof sealing.stdin !== "number") {
|
|
17115
|
+
sealing.stdin.write(`${token}
|
|
17116
|
+
`);
|
|
17117
|
+
sealing.stdin.end();
|
|
17033
17118
|
}
|
|
17034
|
-
|
|
17035
|
-
|
|
17119
|
+
const error = await new Response(sealing.stderr).text();
|
|
17120
|
+
if (await sealing.exited !== 0)
|
|
17121
|
+
throw new Error(`could not seal enrolment credential: ${error.trim()}`);
|
|
17036
17122
|
}
|
|
17037
17123
|
}
|
|
17038
17124
|
const transcript = await applyPlan(plan, localRunner);
|
|
@@ -17046,11 +17132,6 @@ async function cmdAgent(options, args) {
|
|
|
17046
17132
|
out.line();
|
|
17047
17133
|
return 0;
|
|
17048
17134
|
} catch (cause) {
|
|
17049
|
-
if (options.enrol) {
|
|
17050
|
-
try {
|
|
17051
|
-
unlinkSync3(enrolTokenSourcePath);
|
|
17052
|
-
} catch {}
|
|
17053
|
-
}
|
|
17054
17135
|
out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
|
|
17055
17136
|
out.step("Run this as root, or use --json and hand the unit to your provisioner.");
|
|
17056
17137
|
return 1;
|
|
@@ -17071,6 +17152,27 @@ var bootstrapNumber = (question, fallback) => {
|
|
|
17071
17152
|
throw new Error(`${question} must be an integer`);
|
|
17072
17153
|
return value;
|
|
17073
17154
|
};
|
|
17155
|
+
async function bootstrapSecret(question) {
|
|
17156
|
+
if (!process.stdin.isTTY)
|
|
17157
|
+
throw new Error(`${question} requires an interactive terminal`);
|
|
17158
|
+
const prompt = Bun.spawn(["systemd-ask-password", "--timeout=0", "--echo=no", `${question}:`], { stdin: "inherit", stdout: "pipe", stderr: "inherit" });
|
|
17159
|
+
const value = (await new Response(prompt.stdout).text()).trim();
|
|
17160
|
+
if (await prompt.exited !== 0 || !value)
|
|
17161
|
+
throw new Error(`${question} was not provided`);
|
|
17162
|
+
return value;
|
|
17163
|
+
}
|
|
17164
|
+
async function promptPlatformBootstrapSecrets(config) {
|
|
17165
|
+
return validatePlatformBootstrapSecrets(config, {
|
|
17166
|
+
clusterBootstrapCode: await bootstrapSecret("Shared 64-hex cluster bootstrap code"),
|
|
17167
|
+
emailSecret: await bootstrapSecret(config.runtime.environment.email?.provider === "jetemail" ? "JetEmail API key" : "SMTP password"),
|
|
17168
|
+
...config.enrolment.source === "api-token" ? { enrolmentToken: await bootstrapSecret("API-issued one-time platform enrolment token") } : {},
|
|
17169
|
+
...config.runtime.environment.backup ? { backupS3Secret: await bootstrapSecret("Backup S3 secret key") } : {},
|
|
17170
|
+
...config.cloudflareHandoff || config.runtime.environment.cloudflare ? {
|
|
17171
|
+
cloudflareTunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17172
|
+
cloudflareApiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17173
|
+
} : {}
|
|
17174
|
+
});
|
|
17175
|
+
}
|
|
17074
17176
|
var bootstrapList = (question, fallback) => bootstrapAnswer(question, fallback).split(",").map((value) => value.trim()).filter(Boolean);
|
|
17075
17177
|
var parseCpuPools = (value) => value.split(";").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
|
|
17076
17178
|
const [key, cpus, physicalCores, memoryNodes] = entry.split("|").map((field) => field?.trim());
|
|
@@ -17138,13 +17240,26 @@ function writeBootstrapConfig(path, config) {
|
|
|
17138
17240
|
`, { mode: 384, flag: "wx" });
|
|
17139
17241
|
return path;
|
|
17140
17242
|
}
|
|
17141
|
-
function interactiveCloudflareBootstrap() {
|
|
17243
|
+
async function interactiveCloudflareBootstrap() {
|
|
17142
17244
|
if (!process.stdin.isTTY)
|
|
17143
17245
|
throw new Error("interactive Cloudflare config generation requires a terminal");
|
|
17246
|
+
const tokens = {
|
|
17247
|
+
tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17248
|
+
apiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17249
|
+
};
|
|
17250
|
+
const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
|
|
17251
|
+
const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
|
|
17252
|
+
const realtimeEnabled = bootstrapAnswer("Configure existing Worker realtime fan-out? (yes/no)", "yes") === "yes";
|
|
17253
|
+
const workerScriptName = realtimeEnabled ? bootstrapAnswer("Existing Worker script name") : undefined;
|
|
17254
|
+
const discovered = await discoverCloudflareBootstrapCommandResources({
|
|
17255
|
+
zoneName,
|
|
17256
|
+
kvNamespaceTitle,
|
|
17257
|
+
...workerScriptName ? { workerScriptName } : {},
|
|
17258
|
+
tokens
|
|
17259
|
+
});
|
|
17144
17260
|
const nodeCount = bootstrapNumber("Number of public API nodes", "3");
|
|
17145
17261
|
if (nodeCount < 1 || nodeCount > 32)
|
|
17146
17262
|
throw new Error("Number of public API nodes must be between 1 and 32");
|
|
17147
|
-
const realtimeEnabled = bootstrapAnswer("Configure existing Worker realtime fan-out? (yes/no)", "yes") === "yes";
|
|
17148
17263
|
const meshEnabled = bootstrapAnswer("Configure private Mesh/WARP routes? (yes/no)", "no") === "yes";
|
|
17149
17264
|
const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
|
|
17150
17265
|
const nodes = Array.from({ length: nodeCount }, (_, index) => {
|
|
@@ -17168,22 +17283,16 @@ function interactiveCloudflareBootstrap() {
|
|
|
17168
17283
|
return createCloudflareBootstrapCommandConfig({
|
|
17169
17284
|
checkpointPath: bootstrapAnswer("Owner-only resumable checkpoint path", "./cloudflare-handoff.json"),
|
|
17170
17285
|
coordinates: {
|
|
17171
|
-
|
|
17172
|
-
zoneId: bootstrapAnswer("Cloudflare zone id"),
|
|
17173
|
-
kvNamespaceId: bootstrapAnswer("Existing Worker-bound KV namespace id"),
|
|
17286
|
+
...discovered,
|
|
17174
17287
|
...meshDevicePolicyId ? { meshDevicePolicyId } : {},
|
|
17175
17288
|
...realtimeEnabled ? {
|
|
17176
17289
|
realtime: {
|
|
17177
|
-
workerScriptName
|
|
17290
|
+
workerScriptName,
|
|
17178
17291
|
endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
|
|
17179
17292
|
producer: bootstrapAnswer("Realtime producer identity", "platform-api")
|
|
17180
17293
|
}
|
|
17181
17294
|
} : {},
|
|
17182
17295
|
nodes
|
|
17183
|
-
},
|
|
17184
|
-
tokenFiles: {
|
|
17185
|
-
tunnelTokenFile: bootstrapAnswer("Owner-only CF_TUNNEL_TOKEN file", "./CF_TUNNEL_TOKEN"),
|
|
17186
|
-
apiTokenFile: bootstrapAnswer("Owner-only CF_API_TOKEN file", "./CF_API_TOKEN")
|
|
17187
17296
|
}
|
|
17188
17297
|
});
|
|
17189
17298
|
}
|
|
@@ -17199,20 +17308,9 @@ function genesisOutputDirectory(path) {
|
|
|
17199
17308
|
}
|
|
17200
17309
|
return path;
|
|
17201
17310
|
}
|
|
17202
|
-
function ensureGenesisClusterSecret(directory) {
|
|
17203
|
-
const path = `${directory}/cluster-bootstrap.code`;
|
|
17204
|
-
if (!existsSync11(path))
|
|
17205
|
-
writeFileSync12(path, `${randomBytes10(32).toString("hex")}
|
|
17206
|
-
`, { mode: 384, flag: "wx" });
|
|
17207
|
-
if (!/^[a-f0-9]{64}$/i.test(readOwnerOnlySecret(path, "cluster bootstrap code"))) {
|
|
17208
|
-
throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
|
|
17209
|
-
}
|
|
17210
|
-
return path;
|
|
17211
|
-
}
|
|
17212
17311
|
function writePlatformGenesisFleet(directory) {
|
|
17213
17312
|
const output = genesisOutputDirectory(directory);
|
|
17214
|
-
const
|
|
17215
|
-
const template = interactiveBootstrap("platform", { bootstrapSecretFile: secret });
|
|
17313
|
+
const template = interactiveBootstrap("platform", true);
|
|
17216
17314
|
const fleet = PLATFORM_GENESIS_FLEETS[template.environment];
|
|
17217
17315
|
const nodes = fleet.map((guest, index) => index === 0 ? {
|
|
17218
17316
|
nodeHostname: template.nodeHostname,
|
|
@@ -17226,9 +17324,9 @@ function writePlatformGenesisFleet(directory) {
|
|
|
17226
17324
|
writeBootstrapConfig(path, config);
|
|
17227
17325
|
return path;
|
|
17228
17326
|
});
|
|
17229
|
-
return {
|
|
17327
|
+
return { configs };
|
|
17230
17328
|
}
|
|
17231
|
-
function interactiveBootstrap(kind, genesis) {
|
|
17329
|
+
function interactiveBootstrap(kind, genesis = false) {
|
|
17232
17330
|
if (!process.stdin.isTTY)
|
|
17233
17331
|
throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
|
|
17234
17332
|
const environment = bootstrapAnswer("Environment (production/development)", "production");
|
|
@@ -17262,18 +17360,14 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17262
17360
|
const smtpPort = emailProvider === "smtp" ? Number(bootstrapAnswer("SMTP port", "587")) : undefined;
|
|
17263
17361
|
const smtpUser = emailProvider === "smtp" ? bootstrapAnswer("SMTP user") : undefined;
|
|
17264
17362
|
const jetemailEu = emailProvider === "jetemail" ? bootstrapAnswer("Use JetEmail EU processing? (yes/no)", "no") === "yes" : undefined;
|
|
17265
|
-
const emailSecret = bootstrapAnswer(emailProvider === "smtp" ? "Root-only SMTP password file" : "Root-only JetEmail API-key file");
|
|
17266
17363
|
const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
|
|
17267
17364
|
const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
|
|
17268
17365
|
const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
|
|
17269
17366
|
const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
|
|
17270
|
-
const backupS3Secret = backupEndpoint ? bootstrapAnswer("Root-only backup S3 secret file") : undefined;
|
|
17271
17367
|
const cloudflareHandoffFile = bootstrapAnswer(genesis ? `${computeReference} node-specific Cloudflare host handoff` : "Node-specific Cloudflare host handoff (blank for no public edge)", genesis ? undefined : "");
|
|
17272
17368
|
const cloudflareNodeName = cloudflareHandoffFile ? bootstrapAnswer("Cloudflare handoff node name", computeReference) : undefined;
|
|
17273
17369
|
const databaseNetworkMode = "private-lan";
|
|
17274
|
-
const bootstrapSecretFile = genesis?.bootstrapSecretFile ?? bootstrapAnswer("Root-only shared cluster bootstrap-code file");
|
|
17275
17370
|
const enrolmentSource = genesis ? "genesis-derived" : bootstrapAnswer("Enrolment source (genesis-derived/api-token)", "genesis-derived");
|
|
17276
|
-
const platformEnrolTokenFile = enrolmentSource === "api-token" ? bootstrapAnswer("Root-only API-issued platform enrolment-token file") : undefined;
|
|
17277
17371
|
return {
|
|
17278
17372
|
kind,
|
|
17279
17373
|
environment,
|
|
@@ -17290,10 +17384,9 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17290
17384
|
serverMode,
|
|
17291
17385
|
address,
|
|
17292
17386
|
master,
|
|
17293
|
-
coordinators
|
|
17294
|
-
bootstrapSecretFile
|
|
17387
|
+
coordinators
|
|
17295
17388
|
},
|
|
17296
|
-
enrolment: { source: enrolmentSource
|
|
17389
|
+
enrolment: { source: enrolmentSource },
|
|
17297
17390
|
runtime: {
|
|
17298
17391
|
environment: {
|
|
17299
17392
|
softwareProfile: profile,
|
|
@@ -17336,11 +17429,7 @@ function interactiveBootstrap(kind, genesis) {
|
|
|
17336
17429
|
bluePort: 3001,
|
|
17337
17430
|
greenPort: 3002,
|
|
17338
17431
|
healthPath: "/api/health",
|
|
17339
|
-
keepReleases: 5
|
|
17340
|
-
credentialFiles: {
|
|
17341
|
-
emailSecret,
|
|
17342
|
-
backupS3Secret
|
|
17343
|
-
}
|
|
17432
|
+
keepReleases: 5
|
|
17344
17433
|
},
|
|
17345
17434
|
firewall: {
|
|
17346
17435
|
enabled: bootstrapAnswer("Enable host firewall? (yes/no)", "yes") === "yes",
|
|
@@ -17367,7 +17456,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17367
17456
|
return 0;
|
|
17368
17457
|
}
|
|
17369
17458
|
if (kind === "cloudflare") {
|
|
17370
|
-
const path = writeBootstrapConfig(options.outputPath, interactiveCloudflareBootstrap());
|
|
17459
|
+
const path = writeBootstrapConfig(options.outputPath, await interactiveCloudflareBootstrap());
|
|
17371
17460
|
readCloudflareBootstrapCommandConfig(path, "plan");
|
|
17372
17461
|
out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
|
|
17373
17462
|
return 0;
|
|
@@ -17390,7 +17479,11 @@ async function cmdBootstrap(options, args) {
|
|
|
17390
17479
|
out.step("Review the pinned target and secret names, then repeat with --apply to contact the host.");
|
|
17391
17480
|
return 0;
|
|
17392
17481
|
}
|
|
17393
|
-
|
|
17482
|
+
const platformConfig = readBootstrapConfig(request.platformConfigFile);
|
|
17483
|
+
if (platformConfig.kind !== "platform")
|
|
17484
|
+
throw new Error("remote platform bootstrap requires a platform config");
|
|
17485
|
+
const secrets2 = mode === "apply" ? await promptPlatformBootstrapSecrets(platformConfig) : undefined;
|
|
17486
|
+
out.line(JSON.stringify(await applyOperatorPlatformBootstrap(request, mode, { secrets: secrets2 }), null, 2));
|
|
17394
17487
|
return 0;
|
|
17395
17488
|
}
|
|
17396
17489
|
if (operation === "metal" && args[1] === "remote") {
|
|
@@ -17431,7 +17524,7 @@ async function cmdBootstrap(options, args) {
|
|
|
17431
17524
|
steps: [
|
|
17432
17525
|
"probe every declared public node",
|
|
17433
17526
|
"write secret-free acceptance evidence",
|
|
17434
|
-
"remove the
|
|
17527
|
+
"remove the completed non-secret checkpoint and node handoffs"
|
|
17435
17528
|
]
|
|
17436
17529
|
}, null, 2));
|
|
17437
17530
|
out.step("Review the evidence destination, then repeat with --apply to finalize and remove bootstrap capabilities.");
|
|
@@ -17442,7 +17535,11 @@ async function cmdBootstrap(options, args) {
|
|
|
17442
17535
|
}
|
|
17443
17536
|
if (args[2] !== undefined)
|
|
17444
17537
|
throw new Error("Cloudflare bootstrap operation must be plan/apply, verify or finalize");
|
|
17445
|
-
|
|
17538
|
+
const tokens = options.apply ? {
|
|
17539
|
+
tunnelToken: await bootstrapSecret("CF_TUNNEL_TOKEN"),
|
|
17540
|
+
apiToken: await bootstrapSecret("CF_API_TOKEN")
|
|
17541
|
+
} : undefined;
|
|
17542
|
+
await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply, tokens);
|
|
17446
17543
|
return 0;
|
|
17447
17544
|
}
|
|
17448
17545
|
if (operation === "platform" && args[1] === "prepare") {
|
|
@@ -17464,6 +17561,9 @@ async function cmdBootstrap(options, args) {
|
|
|
17464
17561
|
out.line(JSON.stringify(await preparePlatformBootstrap(config2), null, 2));
|
|
17465
17562
|
return 0;
|
|
17466
17563
|
}
|
|
17564
|
+
const credentialStdin = operation === "platform" && args[1] === "credentials-stdin";
|
|
17565
|
+
if (credentialStdin && args[2] !== undefined)
|
|
17566
|
+
throw new Error("platform credential stdin accepts no additional positional arguments");
|
|
17467
17567
|
const installedBootstrapKind = () => resolveInstalledBootstrapKind({
|
|
17468
17568
|
metal: existsSync11(METAL_BOOTSTRAP_STATE_PATH),
|
|
17469
17569
|
compute: existsSync11(BOOTSTRAP_STATE_PATH)
|
|
@@ -17524,7 +17624,21 @@ async function cmdBootstrap(options, args) {
|
|
|
17524
17624
|
out.step("Review the plan, then repeat with --apply as root.");
|
|
17525
17625
|
return 0;
|
|
17526
17626
|
}
|
|
17527
|
-
const
|
|
17627
|
+
const secrets = config.kind === "platform" && !credentialStdin ? await promptPlatformBootstrapSecrets(config) : undefined;
|
|
17628
|
+
let stdinSecrets = secrets;
|
|
17629
|
+
if (config.kind === "platform" && credentialStdin) {
|
|
17630
|
+
const body = await Bun.stdin.text();
|
|
17631
|
+
if (body.length < 2 || body.length > 65536)
|
|
17632
|
+
throw new Error("bootstrap credential stdin has an invalid size");
|
|
17633
|
+
let parsed;
|
|
17634
|
+
try {
|
|
17635
|
+
parsed = JSON.parse(body);
|
|
17636
|
+
} catch {
|
|
17637
|
+
throw new Error("bootstrap credential stdin is not valid JSON");
|
|
17638
|
+
}
|
|
17639
|
+
stdinSecrets = validatePlatformBootstrapSecrets(config, parsed);
|
|
17640
|
+
}
|
|
17641
|
+
const result = await applyBootstrap(config, undefined, stdinSecrets);
|
|
17528
17642
|
out.line(JSON.stringify(result, null, 2));
|
|
17529
17643
|
return 0;
|
|
17530
17644
|
} catch (cause) {
|
|
@@ -18077,13 +18191,13 @@ function usage() {
|
|
|
18077
18191
|
strict three-node platform genesis directory; never apply locally
|
|
18078
18192
|
fz bootstrap platform cloudflare
|
|
18079
18193
|
Plan/apply attended Tunnel and DNS reconciliation
|
|
18080
|
-
using
|
|
18194
|
+
using hidden management and KV/Worker runtime token prompts
|
|
18081
18195
|
fz bootstrap platform cloudflare verify
|
|
18082
18196
|
From the operator laptop, prove every ordinary public
|
|
18083
18197
|
node /api/health using the 0600 checkpoint
|
|
18084
18198
|
fz bootstrap platform cloudflare finalize
|
|
18085
18199
|
Persist secret-free public-node acceptance, then remove
|
|
18086
|
-
the
|
|
18200
|
+
the completed non-secret checkpoint and node handoffs
|
|
18087
18201
|
fz bootstrap metal Plan/install an identity-only physical provisioner
|
|
18088
18202
|
fz bootstrap metal remote <apply|genesis|rehearsal|status>
|
|
18089
18203
|
Install/bootstrap one pinned blank metal target, then
|