@forgezero/agent 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fz.js CHANGED
@@ -4857,9 +4857,9 @@ async function spawnWith(command, env, report = () => {}) {
4857
4857
 
4858
4858
  // src/cli/index.ts
4859
4859
  init_dist();
4860
- import { existsSync as existsSync4, lstatSync as lstatSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
4861
- import { dirname as dirname3 } from "path";
4862
- import { fileURLToPath } from "url";
4860
+ import { existsSync as existsSync6, lstatSync as lstatSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync7, statSync as statSync2, unlinkSync as unlinkSync3, writeFileSync as writeFileSync7 } from "fs";
4861
+ import { dirname as dirname8 } from "path";
4862
+ import { fileURLToPath as fileURLToPath2 } from "url";
4863
4863
  import { hostname } from "os";
4864
4864
 
4865
4865
  // src/agent-update.ts
@@ -4875,7 +4875,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4875
4875
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4876
4876
 
4877
4877
  // src/version.ts
4878
- var VERSION2 = "0.1.34";
4878
+ var VERSION2 = "0.1.35";
4879
4879
 
4880
4880
  // src/software.ts
4881
4881
  var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
@@ -5806,7 +5806,7 @@ function planProvision(options) {
5806
5806
  },
5807
5807
  {
5808
5808
  label: "encrypted one-time enrolment capability",
5809
- command: `test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
5809
+ command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
5810
5810
  }
5811
5811
  ] : [],
5812
5812
  ...deploymentEnabled ? [{
@@ -10090,256 +10090,3561 @@ function removeSession(api, realm, path = defaultSessionPath()) {
10090
10090
  persist(path, file);
10091
10091
  }
10092
10092
 
10093
- // src/cli/index.ts
10094
- var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
10095
- var DEFAULT_MODE = RECOMMENDED_MODE;
10096
- var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
10097
- function parseOptions(argv) {
10098
- const options = {
10099
- api: process.env.FZ_API ?? "http://localhost:8787",
10100
- apiExplicit: Boolean(process.env.FZ_API),
10101
- app: process.env.FZ_APP,
10102
- realm: process.env.FZ_REALM ?? "platform",
10103
- realmExplicit: Boolean(process.env.FZ_REALM),
10104
- json: false,
10105
- apply: false,
10106
- enrol: false,
10107
- socket: process.env.SSH_AUTH_SOCK,
10108
- mode: DEFAULT_MODE,
10109
- user: process.env.FZ_USER ?? "operator",
10110
- userExplicit: Boolean(process.env.FZ_USER),
10111
- phraseStdin: false,
10112
- preserveEnv: false,
10113
- projectRoot: process.cwd(),
10114
- deployProfile: "app",
10115
- deploySoftware: [],
10116
- deployChannel: "production",
10117
- requireAttestation: false,
10118
- force: false,
10119
- noBrowser: false,
10120
- provider: "github",
10121
- branch: "main",
10122
- authMode: "public",
10123
- targets: [],
10124
- wait: false,
10125
- timeoutSeconds: 900,
10126
- queries: []
10127
- };
10128
- const positional = [];
10129
- for (let index = 0;index < argv.length; index += 1) {
10130
- const token = argv[index];
10131
- if (token === "--api") {
10132
- options.api = argv[++index] ?? options.api;
10133
- options.apiExplicit = true;
10134
- } else if (token === "--app")
10135
- options.app = argv[++index];
10136
- else if (token === "--realm") {
10137
- options.realm = argv[++index] ?? options.realm;
10138
- options.realmExplicit = true;
10139
- } else if (token === "--socket")
10140
- options.socket = argv[++index];
10141
- else if (token === "--json")
10142
- options.json = true;
10143
- else if (token === "--apply")
10144
- options.apply = true;
10145
- else if (token === "--enrol")
10146
- options.enrol = true;
10147
- else if (token === "--preserve-env")
10148
- options.preserveEnv = true;
10149
- else if (token === "--root")
10150
- options.projectRoot = argv[++index] ?? options.projectRoot;
10151
- else if (token === "--name")
10152
- options.projectName = argv[++index];
10153
- else if (token === "--purpose")
10154
- options.projectPurpose = argv[++index];
10155
- else if (token === "--profile")
10156
- options.deployProfile = argv[++index] ?? options.deployProfile;
10157
- else if (token === "--software")
10158
- options.deploySoftware.push(argv[++index] ?? "");
10159
- else if (token === "--channel") {
10160
- const channel = argv[++index];
10161
- if (channel === "development" || channel === "production")
10162
- options.deployChannel = channel;
10163
- else
10164
- options.optionError = "--channel must be production or development.";
10165
- } else if (token === "--attestation")
10166
- options.requireAttestation = true;
10167
- else if (token === "--force")
10168
- options.force = true;
10169
- else if (token === "--no-browser")
10170
- options.noBrowser = true;
10171
- else if (token === "--project")
10172
- options.projectKey = argv[++index];
10173
- else if (token === "--pipeline")
10174
- options.pipelineKey = argv[++index];
10175
- else if (token === "--revision")
10176
- options.revision = argv[++index];
10177
- else if (token === "--label")
10178
- options.label = argv[++index];
10179
- else if (token === "--provider") {
10180
- const value = argv[++index];
10181
- if (value === "github" || value === "gitlab" || value === "generic")
10182
- options.provider = value;
10183
- else
10184
- options.optionError = "--provider must be github, gitlab, or generic.";
10185
- } else if (token === "--repository")
10186
- options.repository = argv[++index];
10187
- else if (token === "--branch")
10188
- options.branch = argv[++index] ?? options.branch;
10189
- else if (token === "--clone-url")
10190
- options.cloneUrl = argv[++index];
10191
- else if (token === "--auth") {
10192
- const value = argv[++index];
10193
- if (value === "public" || value === "node-ssh" || value === "vault-token")
10194
- options.authMode = value;
10195
- else
10196
- options.optionError = "--auth must be public, node-ssh, or vault-token.";
10197
- } else if (token === "--git-secret")
10198
- options.gitSecret = argv[++index];
10199
- else if (token === "--git-username")
10200
- options.gitUsername = argv[++index];
10201
- else if (token === "--known-hosts")
10202
- options.knownHostsFile = argv[++index];
10203
- else if (token === "--target")
10204
- options.targets.push(argv[++index] ?? "");
10205
- else if (token === "--target-key")
10206
- options.targetKey = argv[++index];
10207
- else if (token === "--wait")
10208
- options.wait = true;
10209
- else if (token === "--timeout") {
10210
- const value = Number(argv[++index] ?? "");
10211
- if (Number.isFinite(value) && value > 0 && value <= 86400)
10212
- options.timeoutSeconds = value;
10213
- else
10214
- options.optionError = "--timeout must be between 1 and 86400 seconds.";
10215
- } else if (token === "--data")
10216
- options.data = argv[++index];
10217
- else if (token === "--data-file")
10218
- options.dataFile = argv[++index];
10219
- else if (token === "--query")
10220
- options.queries.push(argv[++index] ?? "");
10221
- else if (token === "--key")
10222
- options.key = argv[++index];
10223
- else if (token === "--mode")
10224
- options.mode = argv[++index] ?? options.mode;
10225
- else if (token === "--user") {
10226
- options.user = argv[++index] ?? options.user;
10227
- options.userExplicit = true;
10228
- } else if (token === "--token-file")
10229
- options.tokenFile = argv[++index];
10230
- else if (token === "--phrase-file")
10231
- options.phraseFile = argv[++index];
10232
- else if (token === "--phrase-stdin")
10233
- options.phraseStdin = true;
10234
- else
10235
- positional.push(token);
10093
+ // src/bootstrap.ts
10094
+ import { createHmac, randomBytes as randomBytes5 } from "crypto";
10095
+ import {
10096
+ chmodSync as chmodSync2,
10097
+ existsSync as existsSync4,
10098
+ lstatSync as lstatSync2,
10099
+ mkdirSync as mkdirSync4,
10100
+ readFileSync as readFileSync4,
10101
+ renameSync as renameSync3,
10102
+ rmSync,
10103
+ writeFileSync as writeFileSync4
10104
+ } from "fs";
10105
+ import { dirname as dirname4 } from "path";
10106
+ import { fileURLToPath } from "url";
10107
+
10108
+ // src/platform-bootstrap-runtime.ts
10109
+ var safeAtom = (name, value) => {
10110
+ if (!value || /[\0\r\n]/.test(value))
10111
+ throw new Error(`${name} must be non-empty and single-line.`);
10112
+ return value;
10113
+ };
10114
+ var boundedInteger = (name, value, minimum, maximum) => {
10115
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
10116
+ throw new Error(`${name} must be an integer from ${minimum} through ${maximum}.`);
10236
10117
  }
10237
- return { command: positional[0] ?? "help", args: positional.slice(1), options };
10238
- }
10239
- var out = {
10240
- line: (text3 = "") => process.stdout.write(`${text3}
10241
- `),
10242
- step: (text3) => process.stdout.write(` ${text3}
10243
- `),
10244
- warn: (text3) => process.stderr.write(` ! ${text3}
10245
- `),
10246
- fail: (text3) => process.stderr.write(` \u2717 ${text3}
10247
- `),
10248
- ok: (text3) => process.stdout.write(` \u2713 ${text3}
10249
- `)
10118
+ return value;
10250
10119
  };
10251
- var sessionCookie = null;
10252
- var storedSession = null;
10253
- function captureSession(response) {
10254
- const setCookie = response.headers.get("set-cookie");
10255
- if (!setCookie)
10256
- return;
10257
- const cookie = setCookie.split(";")[0];
10258
- sessionCookie = cookie;
10259
- if (storedSession && (cookie !== storedSession.cookie || Date.now() - storedSession.lastUsedAtTs >= 60000)) {
10260
- storedSession = { ...storedSession, cookie, lastUsedAtTs: Date.now() };
10261
- saveSession(storedSession);
10120
+ var privateCoordinator = (raw) => {
10121
+ const url = new URL(raw);
10122
+ if (url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
10123
+ throw new Error("ArangoDB coordinator URLs must be credential-free private HTTP origins.");
10262
10124
  }
10263
- }
10264
- function requestHeaders(apiBase, cookie) {
10265
- return {
10266
- "content-type": "application/json",
10267
- origin: new URL(apiBase).origin,
10268
- ...cookie ? { cookie } : {}
10269
- };
10270
- }
10271
- async function api(options, path, init) {
10272
- const base = options.realm === "platform" ? "/api" : `/api/t/${encodeURIComponent(options.realm)}`;
10273
- const response = await fetch(`${options.api}${base}${path}`, {
10274
- method: init?.method ?? "GET",
10275
- headers: { ...requestHeaders(options.api, sessionCookie), "user-agent": `forgezero-cli/${VERSION2}` },
10276
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10277
- });
10278
- captureSession(response);
10279
- let body = null;
10280
- try {
10281
- body = await response.json();
10282
- } catch {
10283
- body = null;
10125
+ const host = url.hostname.replace(/^\[|\]$/g, "");
10126
+ const privateHost = host === "localhost" || host === "::1" || host.startsWith("fd") || host.startsWith("fc") || /^127\./.test(host) || /^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
10127
+ if (!privateHost || url.port && url.port !== "8529") {
10128
+ throw new Error("ArangoDB coordinators must use private addresses and port 8529.");
10284
10129
  }
10285
- const challenge = body?.security;
10286
- if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && custodyIdentity && !path.startsWith("/security/step-up/")) {
10287
- const proved = await proveWithAgent(options, challenge.requestKey);
10288
- if (proved) {
10289
- const replay = await fetch(`${options.api}${base}${path}`, {
10290
- method: init?.method ?? "GET",
10291
- headers: {
10292
- ...requestHeaders(options.api, sessionCookie),
10293
- "x-security-request-key": challenge.requestKey
10294
- },
10295
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10296
- });
10297
- captureSession(replay);
10298
- let replayed = null;
10299
- try {
10300
- replayed = await replay.json();
10301
- } catch {
10302
- replayed = null;
10303
- }
10304
- return { status: replay.status, body: replayed };
10305
- }
10130
+ return url.origin;
10131
+ };
10132
+ var httpsOrigin = (name, raw) => {
10133
+ const value = new URL(raw);
10134
+ if (value.protocol !== "https:" || value.username || value.password || value.search || value.hash || value.pathname !== "/") {
10135
+ throw new Error(`${name} must be a credential-free HTTPS origin.`);
10306
10136
  }
10307
- if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && storedSession && !path.startsWith("/security/step-up/")) {
10308
- const proved = await proveWithBrowser(options, challenge.requestKey, base);
10309
- if (proved) {
10310
- const replay = await fetch(`${options.api}${base}${path}`, {
10311
- method: init?.method ?? "GET",
10312
- headers: {
10313
- ...requestHeaders(options.api, sessionCookie),
10314
- "user-agent": `forgezero-cli/${VERSION2}`,
10315
- "x-security-request-key": challenge.requestKey
10316
- },
10317
- body: init?.body === undefined ? undefined : JSON.stringify(init.body)
10318
- });
10319
- captureSession(replay);
10320
- let replayed = null;
10321
- try {
10322
- replayed = await replay.json();
10323
- } catch {}
10324
- return { status: replay.status, body: replayed };
10137
+ return value.origin;
10138
+ };
10139
+ var systemdValue = (name, raw) => {
10140
+ if (/[\0\r\n]/.test(raw))
10141
+ throw new Error(`${name} must be single-line.`);
10142
+ const value = raw;
10143
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
10144
+ };
10145
+ function validatePlatformSharedEnvironment(input, options = {}) {
10146
+ if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
10147
+ throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
10148
+ }
10149
+ if (input.databaseCoordinators.length < 1 || input.databaseCoordinators.length > 16) {
10150
+ throw new Error("databaseCoordinators must contain 1 through 16 endpoints.");
10151
+ }
10152
+ const coordinators = input.databaseCoordinators.map(privateCoordinator);
10153
+ if (new Set(coordinators).size !== coordinators.length)
10154
+ throw new Error("databaseCoordinators must be unique.");
10155
+ if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
10156
+ throw new Error("Database networking must be private-lan or cloudflare-warp.");
10157
+ }
10158
+ boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
10159
+ boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
10160
+ if (input.databaseWriteConcern > input.databaseReplicationFactor) {
10161
+ throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
10162
+ }
10163
+ boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
10164
+ boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
10165
+ boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
10166
+ boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
10167
+ boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
10168
+ if (!Number.isFinite(input.otlpTraceSampleRatio) || input.otlpTraceSampleRatio < 0 || input.otlpTraceSampleRatio > 1) {
10169
+ throw new Error("otlpTraceSampleRatio must be from 0 through 1.");
10170
+ }
10171
+ if (input.otlpEndpoint !== "http://127.0.0.1:4318")
10172
+ throw new Error("OTLP must use the exact local collector endpoint.");
10173
+ validateCollectorUnit(input.otlpCollectorUnit);
10174
+ httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint);
10175
+ for (const [name, value] of Object.entries({
10176
+ nodeHostname: input.nodeHostname,
10177
+ nodeRegion: input.nodeRegion,
10178
+ databaseUser: input.databaseUser,
10179
+ sharedDirectory: input.sharedDirectory,
10180
+ seedSyncEpoch: input.seedSyncEpoch,
10181
+ repository: input.repository,
10182
+ branch: input.branch,
10183
+ deployProfile: input.deployProfile
10184
+ }))
10185
+ safeAtom(name, value);
10186
+ if (!input.sharedDirectory.startsWith("/"))
10187
+ throw new Error("sharedDirectory must be absolute.");
10188
+ for (const peer of input.seedSyncPeers) {
10189
+ const url = new URL(peer);
10190
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
10191
+ throw new Error("Seed peers must be WebSocket URLs.");
10192
+ if (url.username || url.password || url.hash)
10193
+ throw new Error("Seed peers cannot contain credentials or fragments.");
10194
+ }
10195
+ if (input.smtp) {
10196
+ safeAtom("smtp.host", input.smtp.host);
10197
+ boundedInteger("smtp.port", input.smtp.port, 1, 65535);
10198
+ safeAtom("smtp.from", input.smtp.from);
10199
+ if (input.smtp.user)
10200
+ safeAtom("smtp.user", input.smtp.user);
10201
+ }
10202
+ if (input.backup) {
10203
+ httpsOrigin("backup.endpoint", input.backup.endpoint);
10204
+ for (const [name, value] of Object.entries(input.backup))
10205
+ safeAtom(`backup.${name}`, value);
10206
+ }
10207
+ if (input.cloudflare) {
10208
+ if (![input.cloudflare.accountId, input.cloudflare.zoneId, input.cloudflare.kvNamespaceId].every((item) => /^[a-f0-9]{32}$/i.test(item)) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.cloudflare.tunnelId)) {
10209
+ throw new Error("Cloudflare account, zone, KV and Tunnel ids are malformed.");
10210
+ }
10211
+ const service = new URL(input.cloudflare.tunnelService);
10212
+ if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
10213
+ throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
10214
+ if (input.cloudflare.warp) {
10215
+ if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(input.cloudflare.warp.organization) || !/^[0-9a-f-]{36}$/i.test(input.cloudflare.warp.virtualNetworkId) || !/^[A-Za-z0-9_-]{1,128}$/.test(input.cloudflare.warp.deviceProfileId)) {
10216
+ throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
10217
+ }
10325
10218
  }
10326
10219
  }
10327
- if (response.status === 401 && storedSession) {
10328
- removeSession(storedSession.api, storedSession.realm);
10329
- storedSession = null;
10330
- sessionCookie = null;
10220
+ if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
10221
+ throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
10331
10222
  }
10332
- return { status: response.status, body };
10223
+ return {
10224
+ ...input,
10225
+ databaseCoordinators: coordinators,
10226
+ appOrigin: httpsOrigin("appOrigin", input.appOrigin),
10227
+ apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
10228
+ agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
10229
+ };
10333
10230
  }
10334
- function sleep(ms) {
10335
- return new Promise((resolve2) => setTimeout(resolve2, ms));
10231
+ function renderPlatformSharedEnvironment(input) {
10232
+ const value = validatePlatformSharedEnvironment(input);
10233
+ const appHost = new URL(value.appOrigin).hostname.split(".").slice(-2).join(".");
10234
+ const apiHost = new URL(value.apiOrigin).hostname.split(".").slice(-2).join(".");
10235
+ const entries = {
10236
+ ARANGO_URL: value.databaseCoordinators[0],
10237
+ ARANGO_URLS: value.databaseCoordinators.join(","),
10238
+ ARANGO_DB: "fz",
10239
+ FZ_DATABASE_MODE: "platform",
10240
+ ARANGO_USER: value.databaseUser,
10241
+ ARANGO_REPLICATION_FACTOR: String(value.databaseReplicationFactor),
10242
+ ARANGO_WRITE_CONCERN: String(value.databaseWriteConcern),
10243
+ FZ_DB_ROLE: value.databaseRole,
10244
+ FZ_SOFTWARE_PROFILE: value.softwareProfile,
10245
+ FZ_ROLE: value.nodeRole,
10246
+ FZ_DB_ADDRESS: value.databaseAddress ?? "",
10247
+ FZ_DB_MASTER: value.databaseMaster ?? "",
10248
+ FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
10249
+ FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
10250
+ FZ_SEED_SYNC_MEMBERS: String(value.seedSyncMembers),
10251
+ FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
10252
+ FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
10253
+ FZ_SHARED_DIR: value.sharedDirectory,
10254
+ FZ_PUBLIC_API_PORT: String(value.publicApiPort),
10255
+ ORIGIN: value.appOrigin,
10256
+ API_ORIGIN: value.apiOrigin,
10257
+ HOST: "127.0.0.1",
10258
+ APP_ORIGINS: value.appOrigin,
10259
+ TRUST_CLOUDFLARE_IP: "1",
10260
+ SESSION_COOKIE_SAMESITE: appHost === apiHost ? "lax" : "none",
10261
+ SESSION_COOKIE_DOMAIN: "",
10262
+ FZ_NODE_HOSTNAME: value.nodeHostname,
10263
+ FZ_NODE_REGION: value.nodeRegion,
10264
+ FZ_CONCURRENCY_LIMIT: String(value.concurrencyLimit),
10265
+ FZ_DRAIN_DEADLINE_MS: String(value.drainDeadlineMs),
10266
+ OTEL_EXPORTER_OTLP_ENDPOINT: value.otlpEndpoint,
10267
+ FZ_OTLP_COLLECTOR_UNIT: value.otlpCollectorUnit,
10268
+ OTEL_SERVICE_NAME: "forgezero-api",
10269
+ FZ_OTLP_FLUSH_INTERVAL_MS: String(value.otlpFlushIntervalMs),
10270
+ FZ_OTLP_TRACE_SAMPLE_RATIO: String(value.otlpTraceSampleRatio),
10271
+ FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
10272
+ FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
10273
+ FZ_PROFILE: value.deployProfile,
10274
+ FZ_REPO: value.repository,
10275
+ FZ_BRANCH: value.branch,
10276
+ FZ_SMTP_HOST: value.smtp?.host ?? "",
10277
+ FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
10278
+ FZ_SMTP_USER: value.smtp?.user ?? "",
10279
+ FZ_SMTP_FROM: value.smtp?.from ?? "",
10280
+ BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
10281
+ BACKUP_S3_REGION: value.backup?.region ?? "",
10282
+ BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
10283
+ BACKUP_S3_ACCESS_KEY_ID: value.backup?.accessKeyId ?? "",
10284
+ FZ_CF_ACCOUNT_ID: value.cloudflare?.accountId ?? "",
10285
+ FZ_CF_ZONE_ID: value.cloudflare?.zoneId ?? "",
10286
+ FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
10287
+ FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
10288
+ FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
10289
+ FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
10290
+ FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
10291
+ FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
10292
+ };
10293
+ return `# Generated by fz bootstrap platform. Non-secret coordinates only.
10294
+ ` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
10295
+ `) + `
10296
+ `;
10336
10297
  }
10337
- function browserCommand(url) {
10338
- if (process.platform === "darwin")
10339
- return ["open", url];
10340
- if (process.platform === "win32")
10341
- return null;
10342
- return ["xdg-open", url];
10298
+ function platformApiCredentialSpecs(options) {
10299
+ const optional = [
10300
+ ["bootstrap-smtp-password", options.smtp],
10301
+ ["cloudflare-kv-token", options.cloudflareKv],
10302
+ ["cloudflare-network-token", options.cloudflareNetwork]
10303
+ ];
10304
+ return [
10305
+ { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
10306
+ { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
10307
+ ...optional.filter(([, present]) => present).map(([name]) => ({
10308
+ name,
10309
+ encryptedPath: `/etc/forgezero/creds/${name}.cred`,
10310
+ required: false
10311
+ }))
10312
+ ];
10313
+ }
10314
+ function renderPlatformApiUnits(input) {
10315
+ for (const path of [input.sharedDirectory, input.sharedEnvironmentFile, input.slotsDirectory]) {
10316
+ if (!path.startsWith("/") || /[\r\n]/.test(path))
10317
+ throw new Error("Runtime paths must be absolute and single-line.");
10318
+ }
10319
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
10320
+ throw new Error("Invalid service user.");
10321
+ validateCollectorUnit(input.collectorUnit);
10322
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
10323
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
10324
+ if (input.bluePort === input.greenPort)
10325
+ throw new Error("Blue and green ports must differ.");
10326
+ const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
10327
+ `);
10328
+ const template = `[Unit]
10329
+ Description=ForgeZero (%i slot)
10330
+ After=network-online.target ${input.collectorUnit}
10331
+ Wants=network-online.target ${input.collectorUnit}
10332
+
10333
+ [Service]
10334
+ Type=simple
10335
+ User=${input.serviceUser}
10336
+ WorkingDirectory=${input.slotsDirectory}/%i
10337
+ Environment=NODE_ENV=production
10338
+ Environment=FZ_SLOT=%i
10339
+ EnvironmentFile=${input.sharedEnvironmentFile}
10340
+ ${credentials}
10341
+ ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
10342
+ Restart=always
10343
+ RestartSec=2
10344
+ TimeoutStopSec=35s
10345
+ LimitCORE=0
10346
+ UMask=0077
10347
+ NoNewPrivileges=yes
10348
+ PrivateTmp=yes
10349
+ PrivateDevices=yes
10350
+ ProtectSystem=strict
10351
+ ProtectHome=yes
10352
+ ReadOnlyPaths=${input.sharedDirectory}
10353
+ ProtectKernelTunables=yes
10354
+ ProtectKernelModules=yes
10355
+ ProtectControlGroups=yes
10356
+ RestrictSUIDSGID=yes
10357
+ RestrictRealtime=yes
10358
+ LockPersonality=yes
10359
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
10360
+
10361
+ [Install]
10362
+ WantedBy=multi-user.target
10363
+ `;
10364
+ return { template, dropIns: {
10365
+ blue: `[Service]
10366
+ Environment=PORT=${input.bluePort}
10367
+ `,
10368
+ green: `[Service]
10369
+ Environment=PORT=${input.greenPort}
10370
+ `
10371
+ } };
10372
+ }
10373
+ function renderPlatformNginx(input) {
10374
+ boundedInteger("publicPort", input.publicPort, 1024, 65535);
10375
+ boundedInteger("initialSlotPort", input.initialSlotPort, 1024, 65535);
10376
+ if (input.publicPort === input.initialSlotPort)
10377
+ throw new Error("Edge and slot ports must differ.");
10378
+ return {
10379
+ upstream: `upstream forgezero { server 127.0.0.1:${input.initialSlotPort}; }
10380
+ `,
10381
+ site: `server {
10382
+ listen 127.0.0.1:${input.publicPort};
10383
+ server_name _;
10384
+ location ^~ /api/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
10385
+ location ^~ /v1/ { proxy_pass http://forgezero; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
10386
+ location / { return 404; }
10387
+ }
10388
+ `
10389
+ };
10390
+ }
10391
+ function renderPlatformActivationFiles(input) {
10392
+ if (!input.root.startsWith("/") || /[\0\r\n]/.test(input.root))
10393
+ throw new Error("Activation root must be absolute and single-line.");
10394
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
10395
+ throw new Error("Invalid activation service user.");
10396
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
10397
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
10398
+ if (input.bluePort === input.greenPort)
10399
+ throw new Error("Activation slot ports must differ.");
10400
+ boundedInteger("keepReleases", input.keepReleases, 2, 100);
10401
+ if (!/^\/[A-Za-z0-9/_-]{1,128}$/.test(input.healthPath) || input.healthPath.includes("..")) {
10402
+ throw new Error("Activation health path is malformed.");
10403
+ }
10404
+ const environment = [
10405
+ `FZ_DIR=${input.root}`,
10406
+ `FZ_USER=${input.serviceUser}`,
10407
+ `FZ_BLUE_PORT=${input.bluePort}`,
10408
+ `FZ_GREEN_PORT=${input.greenPort}`,
10409
+ `FZ_HEALTH_PATH=${input.healthPath}`,
10410
+ `FZ_KEEP_RELEASES=${input.keepReleases}`
10411
+ ].join(`
10412
+ `) + `
10413
+ `;
10414
+ const helper = `#!/usr/bin/env bash
10415
+ set -Eeuo pipefail
10416
+ source /etc/forgezero/deploy.env
10417
+ [[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
10418
+ release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
10419
+ install -d -o root -g root -m 0755 "$slots"
10420
+ case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
10421
+ [[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
10422
+ slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
10423
+ if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
10424
+ target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
10425
+ chown -R root:"$FZ_USER" "$release"; chmod -R a-w "$release"; find "$release" -type d -exec chmod a+rx {} +; find "$release" -type f -exec chmod a+r {} +
10426
+ ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
10427
+ healthy=0; for _ in $(seq 1 30); do curl -fsS --max-time 2 "http://127.0.0.1:\${port}\${FZ_HEALTH_PATH}" >/dev/null 2>&1 && { healthy=1; break; }; sleep 1; done
10428
+ if (( ! healthy )); then systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; exit 1; fi
10429
+ upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
10430
+ printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
10431
+ if ! nginx -t || ! nginx -s reload; then [[ -s "$backup" ]] && cp "$backup" "$upstream" || rm -f "$upstream"; rm -f "$backup"; systemctl stop "forgezero@\${target}.service" || true; [[ -n "$previous_target_link" && -d "$previous_target_link" ]] && ln -sfn "$previous_target_link" "$target_link" || rm -f "$target_link"; nginx -t >/dev/null 2>&1 && nginx -s reload || true; exit 1; fi
10432
+ rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"; [[ -n "$previous_slot" && "$previous_slot" != "$target" ]] && systemctl stop "forgezero@\${previous_slot}.service" || true
10433
+ mapfile -t old < <(find "$releases" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\\n' | sort -rn | tail -n "+$((FZ_KEEP_RELEASES + 1))" | cut -d' ' -f2-)
10434
+ for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
10435
+ printf 'promoted %s on %s\\n' "$release" "$target"
10436
+ `;
10437
+ return {
10438
+ environment,
10439
+ helper,
10440
+ sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
10441
+ `
10442
+ };
10443
+ }
10444
+ function validateCollectorUnit(unit) {
10445
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit))
10446
+ throw new Error("Invalid OTLP collector service unit.");
10447
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-db)\.service$/.test(unit)) {
10448
+ throw new Error("OTLP collector must be independently supervised.");
10449
+ }
10450
+ }
10451
+ function planLocalOtlpProof(endpoint, collectorUnit) {
10452
+ if (endpoint !== "http://127.0.0.1:4318")
10453
+ throw new Error("OTLP proof requires exact loopback endpoint http://127.0.0.1:4318.");
10454
+ validateCollectorUnit(collectorUnit);
10455
+ return {
10456
+ unitCheck: { command: "systemctl", argv: ["is-active", "--quiet", collectorUnit] },
10457
+ receiverCheck: {
10458
+ command: "curl",
10459
+ acceptedStatus: "2xx",
10460
+ argv: [
10461
+ "--silent",
10462
+ "--show-error",
10463
+ "--max-time",
10464
+ "5",
10465
+ "--output",
10466
+ "/dev/null",
10467
+ "--write-out",
10468
+ "%{http_code}",
10469
+ "--request",
10470
+ "POST",
10471
+ "--header",
10472
+ "Content-Type: application/json",
10473
+ "--data-binary",
10474
+ "{}",
10475
+ `${endpoint}/v1/metrics`
10476
+ ]
10477
+ }
10478
+ };
10479
+ }
10480
+
10481
+ // src/cloudflare-bootstrap.ts
10482
+ import { constants } from "fs";
10483
+ import { chmod, lstat, mkdir, mkdtemp, open, rename, rm, stat, unlink } from "fs/promises";
10484
+ import { dirname as dirname3, join as join5, resolve as resolve2 } from "path";
10485
+ import { tmpdir } from "os";
10486
+ import { randomUUID } from "crypto";
10487
+ import { isIP as isIP3 } from "net";
10488
+
10489
+ // src/cloudflare-edge.ts
10490
+ import { isIP as isIP2 } from "net";
10491
+ function isPrivateDatabaseAddress(value) {
10492
+ const address = value.trim().toLowerCase();
10493
+ const family = isIP2(address);
10494
+ if (family === 4) {
10495
+ const [a, b] = address.split(".").map(Number);
10496
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
10497
+ }
10498
+ if (family === 6) {
10499
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
10500
+ return Number.isFinite(first) && (first & 65024) === 64512;
10501
+ }
10502
+ return false;
10503
+ }
10504
+ function privateDatabaseHostRoute(value) {
10505
+ const address = value.trim().toLowerCase();
10506
+ if (!isPrivateDatabaseAddress(address)) {
10507
+ throw new Error("database address must be an RFC 1918 IPv4 or unique-local IPv6 address");
10508
+ }
10509
+ return `${address}/${isIP2(address) === 4 ? 32 : 128}`;
10510
+ }
10511
+ var endpoint = "https://api.cloudflare.com/client/v4";
10512
+ async function cf(config, path, init = {}, fetcher = fetch) {
10513
+ const response = await fetcher(`${endpoint}${path}`, {
10514
+ ...init,
10515
+ headers: {
10516
+ authorization: `Bearer ${config.apiToken}`,
10517
+ "content-type": "application/json",
10518
+ ...init.headers
10519
+ }
10520
+ });
10521
+ const body = await response.json();
10522
+ if (!response.ok || body.success !== true) {
10523
+ throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
10524
+ }
10525
+ return body.result;
10526
+ }
10527
+ async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
10528
+ const [address, prefixText, ...extra] = config.network.split("/");
10529
+ const family = isIP2(address ?? "");
10530
+ const prefix = Number(prefixText);
10531
+ if (extra.length > 0 || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
10532
+ throw new Error("Cloudflare private route must be an explicit IPv4 or IPv6 CIDR");
10533
+ }
10534
+ const path = `/accounts/${config.accountId}/teamnet/routes`;
10535
+ const routes = await cf(config, path, {}, fetcher);
10536
+ const current = routes.find((route2) => !route2.deleted_at && route2.network === config.network && (route2.virtual_network_id ?? "") === (config.virtualNetworkId ?? ""));
10537
+ if (current) {
10538
+ if (current.tunnel_id !== config.tunnelId) {
10539
+ throw new Error(`private route ${config.network} already belongs to another Tunnel`);
10540
+ }
10541
+ return { route: current, created: false };
10542
+ }
10543
+ const route = await cf(config, path, {
10544
+ method: "POST",
10545
+ body: JSON.stringify({
10546
+ network: config.network,
10547
+ tunnel_id: config.tunnelId,
10548
+ comment: config.comment.slice(0, 100),
10549
+ ...config.virtualNetworkId ? { virtual_network_id: config.virtualNetworkId } : {}
10550
+ })
10551
+ }, fetcher);
10552
+ return { route, created: true };
10553
+ }
10554
+ async function ensureCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
10555
+ return ensureCloudflarePrivateRoute({
10556
+ ...config,
10557
+ network: privateDatabaseHostRoute(config.privateAddress)
10558
+ }, fetcher);
10559
+ }
10560
+ async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
10561
+ if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
10562
+ throw new Error("Cloudflare WARP policy id is invalid");
10563
+ const route = privateDatabaseHostRoute(config.privateAddress);
10564
+ const policy = config.policyId ? `/${config.policyId}` : "";
10565
+ const path = `/accounts/${config.accountId}/devices/policy${policy}/include`;
10566
+ const entries = await cf(config, path, {}, fetcher);
10567
+ if (entries.some((entry) => entry.address === route))
10568
+ return { entries, created: false };
10569
+ const next = [...entries, { address: route, description: config.description.slice(0, 100) }];
10570
+ const updated = await cf(config, path, {
10571
+ method: "PUT",
10572
+ body: JSON.stringify(next)
10573
+ }, fetcher);
10574
+ return { entries: updated, created: true };
10575
+ }
10576
+ async function configureCloudflareEdge(config, fetcher = fetch) {
10577
+ const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
10578
+ const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
10579
+ const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
10580
+ const current = await cf(tunnelAuth, tunnelPath, {}, fetcher);
10581
+ const existing = current.config?.ingress ?? [];
10582
+ const catchAll = existing.filter((rule) => !("hostname" in rule));
10583
+ const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
10584
+ await cf(tunnelAuth, tunnelPath, {
10585
+ method: "PUT",
10586
+ body: JSON.stringify({ config: { ingress: [
10587
+ { hostname: config.hostname, service: config.service },
10588
+ ...otherHosts,
10589
+ ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
10590
+ ] } })
10591
+ }, fetcher);
10592
+ const dnsPath = `/zones/${config.zoneId}/dns_records`;
10593
+ const records = await cf(dnsAuth, `${dnsPath}?type=CNAME&name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher);
10594
+ if (records.length > 1) {
10595
+ throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
10596
+ }
10597
+ const record2 = {
10598
+ type: "CNAME",
10599
+ name: config.hostname,
10600
+ content: `${config.tunnelId}.cfargotunnel.com`,
10601
+ proxied: true,
10602
+ ttl: 1
10603
+ };
10604
+ await cf(dnsAuth, records[0] ? `${dnsPath}/${records[0].id}` : dnsPath, {
10605
+ method: records[0] ? "PUT" : "POST",
10606
+ body: JSON.stringify(record2)
10607
+ }, fetcher);
10608
+ }
10609
+ var exactAccountPermissionGroup = async (config, name, fetcher) => {
10610
+ const groups = await cf(config, `/accounts/${config.accountId}/tokens/permission_groups?name=${encodeURIComponent(name)}` + "&scope=com.cloudflare.api.account", {}, fetcher);
10611
+ const matches = groups.filter((group) => group.name === name && group.scopes?.includes("com.cloudflare.api.account") && Boolean(group.id && /^[a-f0-9]{32}$/i.test(group.id)));
10612
+ if (matches.length !== 1) {
10613
+ throw new Error(`Cloudflare account token permission group ${name} is ${matches.length === 0 ? "missing" : "ambiguous"}`);
10614
+ }
10615
+ return { id: matches[0].id, name };
10616
+ };
10617
+ async function createCloudflareAccountRuntimeToken(config, fetcher = fetch) {
10618
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId))
10619
+ throw new Error("Cloudflare account id is invalid");
10620
+ const name = config.name.trim();
10621
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,119}$/.test(name)) {
10622
+ throw new Error("Cloudflare account runtime-token name is invalid");
10623
+ }
10624
+ const permissionNames = [...new Set(config.permissionNames)];
10625
+ if (permissionNames.length === 0)
10626
+ throw new Error("Cloudflare account runtime token needs a permission group");
10627
+ const permissionGroups = await Promise.all(permissionNames.map((permissionName) => exactAccountPermissionGroup(config, permissionName, fetcher)));
10628
+ const body = {
10629
+ name,
10630
+ policies: [{
10631
+ effect: "allow",
10632
+ permission_groups: permissionGroups.map(({ id: id2 }) => ({ id: id2 })),
10633
+ resources: { [`com.cloudflare.api.account.${config.accountId}`]: "*" }
10634
+ }]
10635
+ };
10636
+ const created = await cf(config, `/accounts/${config.accountId}/tokens`, { method: "POST", body: JSON.stringify(body) }, fetcher);
10637
+ if (!created.id || !/^[a-f0-9]{32}$/i.test(created.id) || !created.value || !/^[A-Za-z0-9._-]{40,80}$/.test(created.value)) {
10638
+ throw new Error("Cloudflare did not return the one-time account runtime-token id and value");
10639
+ }
10640
+ return { id: created.id, value: created.value, name, permissionNames };
10641
+ }
10642
+ async function ensureCloudflareAccessServiceToken(config, fetcher = fetch) {
10643
+ const name = config.name.trim();
10644
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
10645
+ throw new Error("Cloudflare Access service-token name is invalid");
10646
+ }
10647
+ const path = `/accounts/${config.accountId}/access/service_tokens`;
10648
+ const tokens = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10649
+ const matches = tokens.filter((token) => token.name === name);
10650
+ if (matches.length > 1)
10651
+ throw new Error(`Cloudflare Access service token ${name} is ambiguous`);
10652
+ if (matches[0]) {
10653
+ if (!config.existing || config.existing.tokenId !== matches[0].id || config.existing.clientId !== matches[0].client_id || !config.existing.clientSecret) {
10654
+ throw new Error(`Cloudflare Access service token ${name} exists but its one-time client secret was not supplied`);
10655
+ }
10656
+ return { credentials: config.existing, created: false };
10657
+ }
10658
+ const created = await cf(config, path, {
10659
+ method: "POST",
10660
+ body: JSON.stringify({ name, duration: config.duration ?? "8760h" })
10661
+ }, fetcher);
10662
+ if (!created.id || !created.client_id || !created.client_secret) {
10663
+ throw new Error("Cloudflare did not return the new Access service-token secret");
10664
+ }
10665
+ return {
10666
+ credentials: {
10667
+ tokenId: created.id,
10668
+ clientId: created.client_id,
10669
+ clientSecret: created.client_secret
10670
+ },
10671
+ created: true
10672
+ };
10673
+ }
10674
+ async function ensureCloudflareAccessPolicy(config, fetcher = fetch) {
10675
+ const path = `/accounts/${config.accountId}/access/policies`;
10676
+ const policies = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10677
+ const matches = policies.filter((policy2) => policy2.name === config.name);
10678
+ if (matches.length > 1)
10679
+ throw new Error(`Cloudflare Access policy ${config.name} is ambiguous`);
10680
+ const desired = {
10681
+ name: config.name,
10682
+ decision: "non_identity",
10683
+ include: [{ service_token: { token_id: config.serviceTokenId } }]
10684
+ };
10685
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, {
10686
+ method: matches[0] ? "PUT" : "POST",
10687
+ body: JSON.stringify(desired)
10688
+ }, fetcher);
10689
+ return { policy, created: !matches[0] };
10690
+ }
10691
+ async function ensureCloudflareAccessApplication(config, fetcher = fetch) {
10692
+ const path = `/accounts/${config.accountId}/access/apps`;
10693
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10694
+ const matches = applications.filter((application2) => application2.domain === config.hostname || application2.self_hosted_domains?.includes(config.hostname));
10695
+ if (matches.length > 1)
10696
+ throw new Error(`Cloudflare Access application for ${config.hostname} is ambiguous`);
10697
+ const desired = {
10698
+ name: config.name,
10699
+ type: "self_hosted",
10700
+ domain: config.hostname,
10701
+ session_duration: "24h",
10702
+ service_auth_401_redirect: true,
10703
+ policies: [{ id: config.policyId, precedence: 1 }]
10704
+ };
10705
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
10706
+ return { application, created: !matches[0] };
10707
+ }
10708
+ async function ensureCloudflareWarpEnrollmentApplication(config, fetcher = fetch) {
10709
+ const name = config.name.trim();
10710
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
10711
+ throw new Error("Cloudflare WARP enrollment application name is invalid");
10712
+ }
10713
+ const path = `/accounts/${config.accountId}/access/apps`;
10714
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10715
+ const matches = applications.filter((application2) => application2.type === "warp" || application2.name === name);
10716
+ if (matches.length > 1)
10717
+ throw new Error(`Cloudflare WARP enrollment application ${name} is ambiguous`);
10718
+ if (matches[0] && (matches[0].type !== "warp" || matches[0].name !== name)) {
10719
+ throw new Error(`Cloudflare Access application ${name} is not the owned WARP enrollment application`);
10720
+ }
10721
+ const desired = {
10722
+ name,
10723
+ type: "warp",
10724
+ policies: [{ id: config.policyId, precedence: 1 }]
10725
+ };
10726
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
10727
+ if (!application.id || application.type && application.type !== "warp") {
10728
+ throw new Error("Cloudflare did not return the WARP enrollment application");
10729
+ }
10730
+ return { application, created: !matches[0] };
10731
+ }
10732
+ async function ensureCloudflareVirtualNetwork(config, fetcher = fetch) {
10733
+ const name = config.name.trim();
10734
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
10735
+ throw new Error("Cloudflare VNET name is invalid");
10736
+ const path = `/accounts/${config.accountId}/teamnet/virtual_networks`;
10737
+ const networks = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10738
+ const matches = networks.filter((network) => !network.deleted_at && network.name === name);
10739
+ if (matches.length > 1)
10740
+ throw new Error(`Cloudflare VNET ${name} is ambiguous`);
10741
+ if (matches[0])
10742
+ return { virtualNetwork: matches[0], created: false };
10743
+ const virtualNetwork = await cf(config, path, {
10744
+ method: "POST",
10745
+ body: JSON.stringify({ name, comment: config.comment.slice(0, 256), is_default_network: false })
10746
+ }, fetcher);
10747
+ if (!virtualNetwork.id || !/^[0-9a-f-]{36}$/i.test(virtualNetwork.id)) {
10748
+ throw new Error("Cloudflare did not return the VNET id");
10749
+ }
10750
+ return { virtualNetwork, created: true };
10751
+ }
10752
+ async function ensureCloudflareWarpDevicePolicy(config, fetcher = fetch) {
10753
+ const name = config.name.trim();
10754
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
10755
+ throw new Error("Cloudflare WARP device profile name is invalid");
10756
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.serviceTokenId))
10757
+ throw new Error("Cloudflare service-token id is invalid");
10758
+ if (!/^[0-9a-f-]{36}$/i.test(config.virtualNetworkId))
10759
+ throw new Error("Cloudflare VNET id is invalid");
10760
+ const precedence = config.precedence ?? 100;
10761
+ if (!Number.isInteger(precedence) || precedence < 1 || precedence > 999999) {
10762
+ throw new Error("Cloudflare WARP device profile precedence is invalid");
10763
+ }
10764
+ const match = `identity.service_token_uuid == "${config.serviceTokenId}"`;
10765
+ const listPath = `/accounts/${config.accountId}/devices/policies`;
10766
+ const path = `/accounts/${config.accountId}/devices/policy`;
10767
+ const policies = await cf(config, `${listPath}?per_page=1000`, {}, fetcher);
10768
+ const matches = policies.filter((policy2) => policy2.name === name);
10769
+ if (matches.length > 1)
10770
+ throw new Error(`Cloudflare WARP device profile ${name} is ambiguous`);
10771
+ if (matches[0]?.match && matches[0].match !== match) {
10772
+ throw new Error(`Cloudflare WARP device profile ${name} belongs to another enrollment identity`);
10773
+ }
10774
+ const desired = {
10775
+ name,
10776
+ match,
10777
+ precedence,
10778
+ description: "ForgeZero non-interactive compute enrollment",
10779
+ enabled: true,
10780
+ allow_mode_switch: false,
10781
+ allowed_to_leave: false,
10782
+ auto_connect: 0,
10783
+ switch_locked: true,
10784
+ service_mode_v2: { mode: "warp" },
10785
+ virtual_networks: { allowed: [config.virtualNetworkId], default: config.virtualNetworkId }
10786
+ };
10787
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PATCH" : "POST", body: JSON.stringify(desired) }, fetcher);
10788
+ if (!policy.id)
10789
+ throw new Error("Cloudflare did not return the WARP device profile id");
10790
+ return { policy, created: !matches[0] };
10791
+ }
10792
+ async function configureCloudflareWorkerAccessSecrets(config, fetcher = fetch) {
10793
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(config.scriptName)) {
10794
+ throw new Error("Cloudflare Worker script name is invalid");
10795
+ }
10796
+ await cf(config, `/accounts/${config.accountId}/workers/scripts/${config.scriptName}/secrets-bulk`, {
10797
+ method: "PATCH",
10798
+ body: JSON.stringify({
10799
+ secrets: {
10800
+ CF_ACCESS_CLIENT_ID: {
10801
+ name: "CF_ACCESS_CLIENT_ID",
10802
+ type: "secret_text",
10803
+ text: config.credentials.clientId
10804
+ },
10805
+ CF_ACCESS_CLIENT_SECRET: {
10806
+ name: "CF_ACCESS_CLIENT_SECRET",
10807
+ type: "secret_text",
10808
+ text: config.credentials.clientSecret
10809
+ }
10810
+ }
10811
+ })
10812
+ }, fetcher);
10813
+ }
10814
+ async function ensureCloudflareKvNamespace(config, fetcher = fetch) {
10815
+ const title = config.title.trim();
10816
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(title)) {
10817
+ throw new Error("Cloudflare KV namespace title is invalid");
10818
+ }
10819
+ const path = `/accounts/${config.accountId}/storage/kv/namespaces`;
10820
+ const namespaces = await cf(config, `${path}?per_page=1000`, {}, fetcher);
10821
+ const matches = namespaces.filter((namespace2) => namespace2.title === title);
10822
+ if (matches.length > 1)
10823
+ throw new Error(`Cloudflare KV namespace ${title} is ambiguous`);
10824
+ if (matches[0])
10825
+ return { namespace: matches[0], created: false };
10826
+ const namespace = await cf(config, path, {
10827
+ method: "POST",
10828
+ body: JSON.stringify({ title })
10829
+ }, fetcher);
10830
+ return { namespace, created: true };
10831
+ }
10832
+ async function ensureCloudflareTunnel(config, fetcher = fetch) {
10833
+ const name = config.name.trim();
10834
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
10835
+ throw new Error("Cloudflare Tunnel name is invalid");
10836
+ }
10837
+ const path = `/accounts/${config.accountId}/cfd_tunnel`;
10838
+ const tunnels = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
10839
+ const matches = tunnels.filter((tunnel2) => tunnel2.name === name && !tunnel2.deleted_at);
10840
+ if (matches.length > 1)
10841
+ throw new Error(`Cloudflare Tunnel ${name} is ambiguous`);
10842
+ const created = !matches[0];
10843
+ const tunnel = matches[0] ?? await cf(config, path, {
10844
+ method: "POST",
10845
+ body: JSON.stringify({ name, config_src: "cloudflare" })
10846
+ }, fetcher);
10847
+ const connectorToken = await cf(config, `${path}/${encodeURIComponent(tunnel.id)}/token`, {}, fetcher);
10848
+ if (!connectorToken || connectorToken.length > 16384) {
10849
+ throw new Error("Cloudflare returned an invalid Tunnel connector token");
10850
+ }
10851
+ return { tunnel, connectorToken, created };
10852
+ }
10853
+
10854
+ // src/cloudflare-bootstrap.ts
10855
+ var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
10856
+ async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
10857
+ const metadata = await handle.stat();
10858
+ if (!metadata.isFile())
10859
+ throw new Error(`${path} must be a regular file`);
10860
+ if (metadata.nlink !== 1)
10861
+ throw new Error(`${path} must not have multiple hard links`);
10862
+ const uid = ownerUid();
10863
+ if (uid !== undefined && uid !== 0 && metadata.uid !== uid)
10864
+ throw new Error(`${path} must be owned by the current operator`);
10865
+ if ((metadata.mode & 63) !== 0)
10866
+ throw new Error(`${path} must not be accessible by group or other users`);
10867
+ if ((metadata.mode & 256) === 0)
10868
+ throw new Error(`${path} must be readable by its owner`);
10869
+ if (metadata.size < 1 || metadata.size > maximumBytes)
10870
+ throw new Error(`${path} has an invalid size`);
10871
+ }
10872
+ async function readOwnerOnlyFile(path, maximumBytes) {
10873
+ const absolute = resolve2(path);
10874
+ let handle;
10875
+ try {
10876
+ handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
10877
+ await assertOwnerOnlyHandle(absolute, handle, maximumBytes);
10878
+ return await handle.readFile({ encoding: "utf8" });
10879
+ } catch (cause) {
10880
+ if (cause instanceof Error && cause.message.startsWith(absolute))
10881
+ throw cause;
10882
+ throw new Error(`cannot securely read owner-only file ${absolute}`);
10883
+ } finally {
10884
+ await handle?.close();
10885
+ }
10886
+ }
10887
+ async function readOwnerApiToken(path) {
10888
+ const token = (await readOwnerOnlyFile(path, 4096)).trim();
10889
+ if (!/^[A-Za-z0-9._-]{40,80}$/.test(token)) {
10890
+ throw new Error(`${resolve2(path)} must contain exactly one Cloudflare API token`);
10891
+ }
10892
+ return token;
10893
+ }
10894
+ async function readCloudflareBootstrapTokens(files) {
10895
+ const entries = await Promise.all([
10896
+ ["apiToken", files.apiTokenFile],
10897
+ ["tunnelApiToken", files.tunnelApiTokenFile],
10898
+ ["dnsApiToken", files.dnsApiTokenFile],
10899
+ ["kvApiToken", files.kvApiTokenFile],
10900
+ ["accessApiToken", files.accessApiTokenFile],
10901
+ ["workerApiToken", files.workerApiTokenFile]
10902
+ ].map(async ([key, path]) => [key, path ? await readOwnerApiToken(path) : undefined]));
10903
+ const tokens = Object.fromEntries(entries.filter(([, value]) => value !== undefined));
10904
+ const unified = tokens.apiToken;
10905
+ for (const key of ["tunnelApiToken", "dnsApiToken", "kvApiToken", "accessApiToken", "workerApiToken"]) {
10906
+ if (!tokens[key] && !unified)
10907
+ throw new Error(`Cloudflare ${key} file is required when --token-file is omitted`);
10908
+ }
10909
+ return tokens;
10910
+ }
10911
+ var validateId = (value, label) => {
10912
+ const normalized = value.trim().toLowerCase();
10913
+ if (!/^[a-f0-9]{32}$/.test(normalized))
10914
+ throw new Error(`${label} must be a 32-character hexadecimal id`);
10915
+ return normalized;
10916
+ };
10917
+ var validateName = (value, label, maximum, allowSpaces = true) => {
10918
+ const normalized = value.trim();
10919
+ const pattern = allowSpaces ? /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/ : /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
10920
+ if (!normalized || normalized.length > maximum || !pattern.test(normalized)) {
10921
+ throw new Error(`${label} is invalid`);
10922
+ }
10923
+ return normalized;
10924
+ };
10925
+ var privateAddress = (value) => {
10926
+ const address = value.trim().toLowerCase();
10927
+ const family = isIP3(address);
10928
+ if (family === 4) {
10929
+ const [a, b] = address.split(".").map(Number);
10930
+ if (a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31)
10931
+ return address;
10932
+ }
10933
+ if (family === 6) {
10934
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
10935
+ if (Number.isFinite(first) && (first & 65024) === 64512)
10936
+ return address;
10937
+ }
10938
+ throw new Error("Cloudflare private database address must be RFC 1918 IPv4 or unique-local IPv6");
10939
+ };
10940
+ function validateCloudflareBootstrapCoordinates(input) {
10941
+ const nodeInputs = input.nodes?.length ? input.nodes : [{
10942
+ nodeName: input.tunnelName,
10943
+ hostname: input.hostname,
10944
+ service: input.service,
10945
+ tunnelName: input.tunnelName,
10946
+ applicationName: input.applicationName
10947
+ }];
10948
+ if (nodeInputs.length < 1 || nodeInputs.length > 32) {
10949
+ throw new Error("Cloudflare bootstrap requires between 1 and 32 explicit nodes");
10950
+ }
10951
+ const nodes = nodeInputs.map((node) => {
10952
+ const nodeName = node.nodeName.trim().toLowerCase();
10953
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName))
10954
+ throw new Error("Cloudflare node name is invalid");
10955
+ const hostname = node.hostname.trim().toLowerCase();
10956
+ if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(hostname)) {
10957
+ throw new Error("Cloudflare public node hostname is invalid");
10958
+ }
10959
+ const serviceUrl = new URL(node.service);
10960
+ if (serviceUrl.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(serviceUrl.hostname) || !serviceUrl.port || serviceUrl.pathname !== "/" || serviceUrl.search || serviceUrl.hash) {
10961
+ throw new Error("Cloudflare Tunnel service must be an explicit loopback HTTP port");
10962
+ }
10963
+ return {
10964
+ nodeName,
10965
+ hostname,
10966
+ service: serviceUrl.toString().replace(/\/$/, ""),
10967
+ tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name", 100, false),
10968
+ applicationName: validateName(node.applicationName, "Cloudflare Access application name", 100),
10969
+ ...node.privateAddress ? { privateAddress: privateAddress(node.privateAddress) } : {}
10970
+ };
10971
+ });
10972
+ for (const [label, values] of [
10973
+ ["node name", nodes.map(({ nodeName }) => nodeName)],
10974
+ ["hostname", nodes.map(({ hostname }) => hostname)],
10975
+ ["Tunnel name", nodes.map(({ tunnelName }) => tunnelName)],
10976
+ ["Access application name", nodes.map(({ applicationName }) => applicationName)]
10977
+ ]) {
10978
+ if (new Set(values).size !== values.length)
10979
+ throw new Error(`Cloudflare fleet ${label} must be unique`);
10980
+ }
10981
+ const first = nodes[0];
10982
+ const workerScriptName = input.workerScriptName.trim();
10983
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(workerScriptName))
10984
+ throw new Error("Cloudflare Worker script name is invalid");
10985
+ const workerCompatibilityDate = input.workerCompatibilityDate.trim();
10986
+ if (!/^20\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(workerCompatibilityDate)) {
10987
+ throw new Error("Cloudflare Worker compatibility date is invalid");
10988
+ }
10989
+ const publicDomains = [...new Set(input.publicDomains.map((domain) => domain.trim().toLowerCase()))];
10990
+ if (publicDomains.length < 1 || publicDomains.length > 10 || publicDomains.some((domain) => !/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(domain) || nodes.some(({ hostname }) => hostname === domain))) {
10991
+ throw new Error("Cloudflare Worker public domains are invalid or include the private origin hostname");
10992
+ }
10993
+ const workerDirectory = resolve2(input.workerDirectory);
10994
+ const workerMain = input.workerMain.trim();
10995
+ if (!workerMain || workerMain.startsWith("/") || workerMain.split(/[\\/]/).includes("..")) {
10996
+ throw new Error("Cloudflare Worker main must be a project-relative path");
10997
+ }
10998
+ const runtimeTokenNamePrefix = input.runtimeTokenNamePrefix.trim();
10999
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(runtimeTokenNamePrefix)) {
11000
+ throw new Error("Cloudflare runtime-token name prefix is invalid");
11001
+ }
11002
+ if (input.createPrivateNetworkRuntimeToken && !input.createRuntimeTokens) {
11003
+ throw new Error("private-network runtime token requires runtime-token creation");
11004
+ }
11005
+ const privateNetwork = input.privateNetwork ? {
11006
+ warpOrganization: validateName(input.privateNetwork.warpOrganization, "Cloudflare WARP organization", 63, false).toLowerCase(),
11007
+ virtualNetworkName: validateName(input.privateNetwork.virtualNetworkName, "Cloudflare VNET name", 100),
11008
+ deviceProfileName: validateName(input.privateNetwork.deviceProfileName, "Cloudflare WARP device profile name", 100),
11009
+ enrollmentApplicationName: validateName(input.privateNetwork.enrollmentApplicationName, "Cloudflare WARP enrollment application name", 100),
11010
+ ...input.privateNetwork.deviceProfilePrecedence !== undefined ? { deviceProfilePrecedence: input.privateNetwork.deviceProfilePrecedence } : {}
11011
+ } : undefined;
11012
+ if (privateNetwork && (!input.createPrivateNetworkRuntimeToken || nodes.every((node) => !node.privateAddress))) {
11013
+ throw new Error("Cloudflare private network requires its runtime token and at least one DB node private address");
11014
+ }
11015
+ if (!privateNetwork && nodes.some((node) => node.privateAddress)) {
11016
+ throw new Error("Cloudflare node private addresses require privateNetwork coordinates");
11017
+ }
11018
+ return {
11019
+ accountId: validateId(input.accountId, "Cloudflare account id"),
11020
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
11021
+ hostname: first.hostname,
11022
+ service: first.service,
11023
+ tunnelName: first.tunnelName,
11024
+ kvNamespaceTitle: validateName(input.kvNamespaceTitle, "Cloudflare KV namespace title", 128, false),
11025
+ workerScriptName,
11026
+ serviceTokenName: validateName(input.serviceTokenName, "Cloudflare Access service-token name", 100),
11027
+ policyName: validateName(input.policyName, "Cloudflare Access policy name", 100),
11028
+ applicationName: first.applicationName,
11029
+ workerDirectory,
11030
+ workerMain,
11031
+ workerCompatibilityDate,
11032
+ publicDomains,
11033
+ createRuntimeTokens: input.createRuntimeTokens,
11034
+ createPrivateNetworkRuntimeToken: input.createPrivateNetworkRuntimeToken,
11035
+ runtimeTokenNamePrefix,
11036
+ nodes,
11037
+ ...privateNetwork ? { privateNetwork } : {}
11038
+ };
11039
+ }
11040
+ function planCloudflareBootstrap(input, outputPath) {
11041
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
11042
+ return {
11043
+ format: 1,
11044
+ kind: "forgezero-cloudflare-bootstrap-plan",
11045
+ mode: "attended-token-file",
11046
+ outputFile: resolve2(outputPath),
11047
+ coordinates,
11048
+ operations: [
11049
+ "create or reuse one Workers KV namespace",
11050
+ "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
11051
+ ...coordinates.createRuntimeTokens ? [
11052
+ "create exact-account least-privilege runtime tokens and checkpoint their one-time values"
11053
+ ] : [],
11054
+ "deploy the shared Worker once with the created NODES binding and stable custom domains",
11055
+ "create or reuse one shared Access service token/policy and one self-hosted application per node",
11056
+ ...coordinates.privateNetwork ? [
11057
+ "create or reuse the VNET, WARP enrollment application, locked service-token device profile and exact DB host routes"
11058
+ ] : [],
11059
+ "write the Access client id and secret to the existing Worker as encrypted secrets",
11060
+ "reconcile each node ingress rule and proxied CNAME only after all Access applications are ready"
11061
+ ],
11062
+ secrets: [
11063
+ "API tokens are read only from owner-only files and are never written to output",
11064
+ "the output contains connector, Access and requested runtime credentials and is atomically written with mode 0600",
11065
+ "the normal API process does not receive or import the management token files"
11066
+ ]
11067
+ };
11068
+ }
11069
+ var defaultWorkerCommandRunner = async ({ command, cwd, env }) => {
11070
+ const child = Bun.spawn([...command], {
11071
+ cwd,
11072
+ env: { ...env },
11073
+ stdin: "ignore",
11074
+ stdout: "pipe",
11075
+ stderr: "pipe"
11076
+ });
11077
+ const [exitCode, stdout, stderr] = await Promise.all([
11078
+ child.exited,
11079
+ new Response(child.stdout).text(),
11080
+ new Response(child.stderr).text()
11081
+ ]);
11082
+ return { exitCode, stdout, stderr };
11083
+ };
11084
+ var inheritedWorkerEnvironment = () => {
11085
+ const allowed = [
11086
+ "PATH",
11087
+ "HOME",
11088
+ "TMPDIR",
11089
+ "XDG_CONFIG_HOME",
11090
+ "XDG_CACHE_HOME",
11091
+ "SSL_CERT_FILE",
11092
+ "SSL_CERT_DIR",
11093
+ "NODE_EXTRA_CA_CERTS",
11094
+ "HTTPS_PROXY",
11095
+ "HTTP_PROXY",
11096
+ "NO_PROXY"
11097
+ ];
11098
+ return Object.fromEntries(allowed.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
11099
+ };
11100
+ var redact = (text3, secrets) => {
11101
+ let safe = text3.slice(0, 4096);
11102
+ for (const secret of secrets)
11103
+ if (secret)
11104
+ safe = safe.split(secret).join("[REDACTED]");
11105
+ return safe.trim();
11106
+ };
11107
+ async function deployCloudflareWorker(coordinates, kvNamespaceId, apiToken, runner = defaultWorkerCommandRunner) {
11108
+ const validated = validateCloudflareBootstrapCoordinates(coordinates);
11109
+ const workerDirectoryMetadata = await stat(validated.workerDirectory);
11110
+ if (!workerDirectoryMetadata.isDirectory())
11111
+ throw new Error("Cloudflare Worker directory is not a directory");
11112
+ const workerMain = resolve2(validated.workerDirectory, validated.workerMain);
11113
+ const workerMainMetadata = await stat(workerMain);
11114
+ if (!workerMainMetadata.isFile())
11115
+ throw new Error("Cloudflare Worker main is not a regular file");
11116
+ const wrangler = resolve2(validated.workerDirectory, "node_modules/.bin/wrangler");
11117
+ const wranglerMetadata = await stat(wrangler);
11118
+ if (!wranglerMetadata.isFile())
11119
+ throw new Error("Cloudflare Wrangler is not installed in the Worker project");
11120
+ if (!/^[a-f0-9]{32}$/.test(kvNamespaceId))
11121
+ throw new Error("Cloudflare KV namespace id is invalid");
11122
+ const temporaryDirectory = await mkdtemp(join5(tmpdir(), "fz-wrangler-"));
11123
+ await chmod(temporaryDirectory, 448);
11124
+ const configurationPath = join5(temporaryDirectory, "wrangler.json");
11125
+ try {
11126
+ const handle = await open(configurationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
11127
+ try {
11128
+ await handle.writeFile(`${JSON.stringify({
11129
+ name: validated.workerScriptName,
11130
+ main: workerMain,
11131
+ compatibility_date: validated.workerCompatibilityDate,
11132
+ workers_dev: false,
11133
+ routes: validated.publicDomains.map((pattern) => ({ pattern, custom_domain: true })),
11134
+ observability: { enabled: true },
11135
+ kv_namespaces: [{ binding: "NODES", id: kvNamespaceId }]
11136
+ }, null, 2)}
11137
+ `);
11138
+ await handle.sync();
11139
+ } finally {
11140
+ await handle.close();
11141
+ }
11142
+ const result = await runner({
11143
+ command: [wrangler, "deploy", "--config", configurationPath],
11144
+ cwd: validated.workerDirectory,
11145
+ env: {
11146
+ ...inheritedWorkerEnvironment(),
11147
+ XDG_CONFIG_HOME: temporaryDirectory,
11148
+ XDG_CACHE_HOME: temporaryDirectory,
11149
+ WRANGLER_LOG_PATH: join5(temporaryDirectory, "wrangler.log"),
11150
+ CLOUDFLARE_ACCOUNT_ID: validated.accountId,
11151
+ CLOUDFLARE_API_TOKEN: apiToken,
11152
+ WRANGLER_SEND_METRICS: "false"
11153
+ }
11154
+ });
11155
+ if (result.exitCode !== 0) {
11156
+ const detail = redact(result.stderr || result.stdout || "no Wrangler diagnostic", [apiToken]);
11157
+ throw new Error(`Cloudflare Worker deployment failed with exit ${result.exitCode}: ${detail}`);
11158
+ }
11159
+ } finally {
11160
+ await rm(temporaryDirectory, { recursive: true, force: true });
11161
+ }
11162
+ }
11163
+ async function readExistingOutput(path) {
11164
+ try {
11165
+ await lstat(path);
11166
+ } catch (cause) {
11167
+ if (cause.code === "ENOENT")
11168
+ return;
11169
+ throw cause;
11170
+ }
11171
+ const text3 = await readOwnerOnlyFile(path, 1048576);
11172
+ let output;
11173
+ try {
11174
+ output = JSON.parse(text3);
11175
+ } catch {
11176
+ throw new Error(`${resolve2(path)} is not valid bootstrap JSON`);
11177
+ }
11178
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !output.resources) {
11179
+ throw new Error(`${resolve2(path)} is not a ForgeZero Cloudflare bootstrap output`);
11180
+ }
11181
+ return output;
11182
+ }
11183
+ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
11184
+ const output = await readExistingOutput(resolve2(checkpointPath));
11185
+ if (!output || output.phase !== "complete") {
11186
+ throw new Error("Cloudflare connector handoff requires a completed bootstrap checkpoint");
11187
+ }
11188
+ const normalizedNodeName = nodeName.trim().toLowerCase();
11189
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalizedNodeName)) {
11190
+ throw new Error("Cloudflare connector handoff node name is invalid");
11191
+ }
11192
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
11193
+ const expected = coordinates.nodes.find((node) => node.nodeName === normalizedNodeName);
11194
+ const matches = output.resources.nodes?.filter((node) => node.nodeName === normalizedNodeName) ?? [];
11195
+ if (!expected || matches.length !== 1) {
11196
+ throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
11197
+ }
11198
+ const resource = matches[0];
11199
+ if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !resource.applicationId || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(resource.tunnelId) || !/^[A-Za-z0-9._-]{40,16384}$/.test(resource.connectorToken)) {
11200
+ throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
11201
+ }
11202
+ return {
11203
+ nodeName: resource.nodeName,
11204
+ hostname: resource.hostname,
11205
+ service: resource.service,
11206
+ tunnelId: resource.tunnelId,
11207
+ connectorToken: resource.connectorToken
11208
+ };
11209
+ }
11210
+ async function readCloudflareHostHandoff(checkpointPath, nodeName) {
11211
+ const connector = await readCloudflareConnectorHandoff(checkpointPath, nodeName);
11212
+ const output = await readExistingOutput(resolve2(checkpointPath));
11213
+ const kv = output?.resources.runtimeTokens?.kv;
11214
+ if (!output || output.phase !== "complete" || !/^[a-f0-9]{32}$/i.test(output.coordinates.accountId) || !/^[a-f0-9]{32}$/i.test(output.coordinates.zoneId) || !/^[a-f0-9]{32}$/i.test(output.resources.kvNamespaceId) || !kv || !/^[A-Za-z0-9._-]{40,80}$/.test(kv.value)) {
11215
+ throw new Error("Cloudflare host handoff is missing the exact-account KV runtime capability");
11216
+ }
11217
+ const network = output.resources.runtimeTokens?.privateNetwork?.value;
11218
+ if (network !== undefined && !/^[A-Za-z0-9._-]{40,80}$/.test(network)) {
11219
+ throw new Error("Cloudflare host handoff private-network capability is malformed");
11220
+ }
11221
+ const privateNetwork = output.resources.privateNetwork;
11222
+ const access = output.resources.access;
11223
+ if (Boolean(privateNetwork) !== Boolean(network)) {
11224
+ throw new Error("Cloudflare host handoff private-network resources and capability disagree");
11225
+ }
11226
+ if (privateNetwork && (!access?.clientId || !access.clientSecret || !/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(privateNetwork.warpOrganization) || !/^[0-9a-f-]{36}$/i.test(privateNetwork.virtualNetworkId) || !privateNetwork.deviceProfileId)) {
11227
+ throw new Error("Cloudflare host handoff WARP enrollment is malformed");
11228
+ }
11229
+ return {
11230
+ ...connector,
11231
+ accountId: output.coordinates.accountId,
11232
+ zoneId: output.coordinates.zoneId,
11233
+ kvNamespaceId: output.resources.kvNamespaceId,
11234
+ kvRuntimeToken: kv.value,
11235
+ ...network ? { privateNetworkRuntimeToken: network } : {},
11236
+ ...privateNetwork && access ? { warp: {
11237
+ organization: privateNetwork.warpOrganization,
11238
+ clientId: access.clientId,
11239
+ clientSecret: access.clientSecret,
11240
+ virtualNetworkId: privateNetwork.virtualNetworkId,
11241
+ deviceProfileId: privateNetwork.deviceProfileId
11242
+ } } : {}
11243
+ };
11244
+ }
11245
+ async function prepareOwnerOutputDirectory(absolutePath) {
11246
+ const directory = dirname3(absolutePath);
11247
+ await mkdir(directory, { recursive: true, mode: 448 });
11248
+ const directoryMetadata = await stat(directory);
11249
+ const uid = ownerUid();
11250
+ if (!directoryMetadata.isDirectory() || uid !== undefined && directoryMetadata.uid !== uid || (directoryMetadata.mode & 18) !== 0) {
11251
+ throw new Error(`bootstrap output directory ${directory} must be operator-owned and not group/other writable`);
11252
+ }
11253
+ return directory;
11254
+ }
11255
+ async function writeOwnerBootstrapOutput(path, output) {
11256
+ const absolute = resolve2(path);
11257
+ const directory = await prepareOwnerOutputDirectory(absolute);
11258
+ const temporary = `${absolute}.${randomUUID()}.tmp`;
11259
+ let handle;
11260
+ try {
11261
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
11262
+ await handle.writeFile(`${JSON.stringify(output, null, 2)}
11263
+ `, { encoding: "utf8" });
11264
+ await handle.sync();
11265
+ await handle.close();
11266
+ handle = undefined;
11267
+ await rename(temporary, absolute);
11268
+ await chmod(absolute, 384);
11269
+ const directoryHandle = await open(directory, constants.O_RDONLY);
11270
+ try {
11271
+ await directoryHandle.sync();
11272
+ } finally {
11273
+ await directoryHandle.close();
11274
+ }
11275
+ } finally {
11276
+ await handle?.close();
11277
+ await unlink(temporary).catch((cause) => {
11278
+ if (cause.code !== "ENOENT")
11279
+ throw cause;
11280
+ });
11281
+ }
11282
+ }
11283
+ var tokenFor = (tokens, key) => {
11284
+ const token = tokens[key]?.trim() || tokens.apiToken?.trim();
11285
+ if (!token)
11286
+ throw new Error(`Cloudflare ${key} is not configured`);
11287
+ return token;
11288
+ };
11289
+ var initialManagementToken = (tokens) => {
11290
+ const token = tokens.apiToken?.trim();
11291
+ if (!token) {
11292
+ throw new Error("initial Cloudflare --token-file is required to create account-owned runtime tokens");
11293
+ }
11294
+ return token;
11295
+ };
11296
+ var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
11297
+ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch, workerRunner = defaultWorkerCommandRunner) {
11298
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
11299
+ const absoluteOutput = resolve2(outputPath);
11300
+ const existing = await readExistingOutput(absoluteOutput);
11301
+ if (existing && !sameCoordinates(existing.coordinates, coordinates)) {
11302
+ throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
11303
+ }
11304
+ await prepareOwnerOutputDirectory(absoluteOutput);
11305
+ const namespace = await ensureCloudflareKvNamespace({
11306
+ accountId: coordinates.accountId,
11307
+ title: coordinates.kvNamespaceTitle,
11308
+ apiToken: tokenFor(tokens, "kvApiToken")
11309
+ }, fetcher);
11310
+ const nodeResources = [];
11311
+ const createdNodes = [];
11312
+ let resources;
11313
+ for (const node of coordinates.nodes) {
11314
+ const checkpointed = existing?.resources.nodes?.find(({ nodeName }) => nodeName === node.nodeName);
11315
+ let created = false;
11316
+ if (checkpointed) {
11317
+ nodeResources.push(checkpointed);
11318
+ } else {
11319
+ const tunnel = await ensureCloudflareTunnel({
11320
+ accountId: coordinates.accountId,
11321
+ name: node.tunnelName,
11322
+ apiToken: tokenFor(tokens, "tunnelApiToken")
11323
+ }, fetcher);
11324
+ created = tunnel.created;
11325
+ nodeResources.push({
11326
+ nodeName: node.nodeName,
11327
+ hostname: node.hostname,
11328
+ service: node.service,
11329
+ tunnelName: node.tunnelName,
11330
+ tunnelId: tunnel.tunnel.id,
11331
+ connectorToken: tunnel.connectorToken
11332
+ });
11333
+ }
11334
+ createdNodes.push({ nodeName: node.nodeName, tunnel: created, application: false });
11335
+ const firstNode = nodeResources[0];
11336
+ resources = {
11337
+ tunnelId: firstNode.tunnelId,
11338
+ kvNamespaceId: namespace.namespace.id,
11339
+ hostname: firstNode.hostname,
11340
+ service: firstNode.service,
11341
+ connectorToken: firstNode.connectorToken,
11342
+ nodes: [...nodeResources],
11343
+ ...existing?.resources.access ? { access: existing.resources.access } : {},
11344
+ ...existing?.resources.runtimeTokens ? { runtimeTokens: existing.resources.runtimeTokens } : {},
11345
+ ...existing?.resources.worker ? { worker: existing.resources.worker } : {},
11346
+ ...existing?.resources.privateNetwork ? { privateNetwork: existing.resources.privateNetwork } : {}
11347
+ };
11348
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11349
+ format: 1,
11350
+ kind: "forgezero-cloudflare-bootstrap",
11351
+ phase: resources.access ? "access-token-provisioned" : "edge-resources-provisioned",
11352
+ updatedAt: new Date().toISOString(),
11353
+ coordinates,
11354
+ resources
11355
+ });
11356
+ }
11357
+ if (!resources)
11358
+ throw new Error("Cloudflare fleet has no nodes");
11359
+ if (coordinates.createRuntimeTokens && !resources.runtimeTokens) {
11360
+ resources = {
11361
+ ...resources,
11362
+ runtimeTokens: { kv: await createCloudflareAccountRuntimeToken({
11363
+ accountId: coordinates.accountId,
11364
+ name: `${coordinates.runtimeTokenNamePrefix}-kv-runtime`,
11365
+ permissionNames: ["Workers KV Storage Write"],
11366
+ apiToken: initialManagementToken(tokens)
11367
+ }, fetcher) }
11368
+ };
11369
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11370
+ format: 1,
11371
+ kind: "forgezero-cloudflare-bootstrap",
11372
+ phase: "runtime-tokens-created",
11373
+ updatedAt: new Date().toISOString(),
11374
+ coordinates,
11375
+ resources
11376
+ });
11377
+ }
11378
+ if (coordinates.createPrivateNetworkRuntimeToken && resources.runtimeTokens && !resources.runtimeTokens.privateNetwork) {
11379
+ resources = {
11380
+ ...resources,
11381
+ runtimeTokens: {
11382
+ ...resources.runtimeTokens,
11383
+ privateNetwork: await createCloudflareAccountRuntimeToken({
11384
+ accountId: coordinates.accountId,
11385
+ name: `${coordinates.runtimeTokenNamePrefix}-private-network-runtime`,
11386
+ permissionNames: ["Cloudflare One Networks Write", "Zero Trust Write"],
11387
+ apiToken: initialManagementToken(tokens)
11388
+ }, fetcher)
11389
+ }
11390
+ };
11391
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11392
+ format: 1,
11393
+ kind: "forgezero-cloudflare-bootstrap",
11394
+ phase: "runtime-tokens-created",
11395
+ updatedAt: new Date().toISOString(),
11396
+ coordinates,
11397
+ resources
11398
+ });
11399
+ }
11400
+ if (!resources.worker) {
11401
+ await deployCloudflareWorker(coordinates, namespace.namespace.id, tokenFor(tokens, "workerApiToken"), workerRunner);
11402
+ resources = {
11403
+ ...resources,
11404
+ worker: {
11405
+ scriptName: coordinates.workerScriptName,
11406
+ publicDomains: coordinates.publicDomains,
11407
+ deployed: true
11408
+ }
11409
+ };
11410
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11411
+ format: 1,
11412
+ kind: "forgezero-cloudflare-bootstrap",
11413
+ phase: "worker-deployed",
11414
+ updatedAt: new Date().toISOString(),
11415
+ coordinates,
11416
+ resources
11417
+ });
11418
+ }
11419
+ const serviceToken = await ensureCloudflareAccessServiceToken({
11420
+ accountId: coordinates.accountId,
11421
+ name: coordinates.serviceTokenName,
11422
+ apiToken: tokenFor(tokens, "accessApiToken"),
11423
+ existing: resources.access
11424
+ }, fetcher);
11425
+ resources = { ...resources, access: serviceToken.credentials };
11426
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11427
+ format: 1,
11428
+ kind: "forgezero-cloudflare-bootstrap",
11429
+ phase: "access-token-provisioned",
11430
+ updatedAt: new Date().toISOString(),
11431
+ coordinates,
11432
+ resources
11433
+ });
11434
+ const policy = await ensureCloudflareAccessPolicy({
11435
+ accountId: coordinates.accountId,
11436
+ name: coordinates.policyName,
11437
+ serviceTokenId: serviceToken.credentials.tokenId,
11438
+ apiToken: tokenFor(tokens, "accessApiToken")
11439
+ }, fetcher);
11440
+ if (!policy.policy.id)
11441
+ throw new Error("Cloudflare did not return the Access policy id");
11442
+ resources = {
11443
+ ...resources,
11444
+ access: { ...serviceToken.credentials, policyId: policy.policy.id }
11445
+ };
11446
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11447
+ format: 1,
11448
+ kind: "forgezero-cloudflare-bootstrap",
11449
+ phase: "access-token-provisioned",
11450
+ updatedAt: new Date().toISOString(),
11451
+ coordinates,
11452
+ resources
11453
+ });
11454
+ let privateNetworkCreated = false;
11455
+ if (coordinates.privateNetwork && !resources.privateNetwork) {
11456
+ const managementToken = initialManagementToken(tokens);
11457
+ const virtualNetwork = await ensureCloudflareVirtualNetwork({
11458
+ accountId: coordinates.accountId,
11459
+ name: coordinates.privateNetwork.virtualNetworkName,
11460
+ comment: "ForgeZero private database network",
11461
+ apiToken: managementToken
11462
+ }, fetcher);
11463
+ const enrollment = await ensureCloudflareWarpEnrollmentApplication({
11464
+ accountId: coordinates.accountId,
11465
+ name: coordinates.privateNetwork.enrollmentApplicationName,
11466
+ policyId: policy.policy.id,
11467
+ apiToken: tokenFor(tokens, "accessApiToken")
11468
+ }, fetcher);
11469
+ const deviceProfile = await ensureCloudflareWarpDevicePolicy({
11470
+ accountId: coordinates.accountId,
11471
+ name: coordinates.privateNetwork.deviceProfileName,
11472
+ serviceTokenId: serviceToken.credentials.tokenId,
11473
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11474
+ precedence: coordinates.privateNetwork.deviceProfilePrecedence,
11475
+ apiToken: managementToken
11476
+ }, fetcher);
11477
+ const routes = [];
11478
+ for (const node of coordinates.nodes.filter((item) => item.privateAddress)) {
11479
+ const resource = resources.nodes.find((item) => item.nodeName === node.nodeName);
11480
+ const route = await ensureCloudflarePrivateDatabaseRoute({
11481
+ accountId: coordinates.accountId,
11482
+ tunnelId: resource.tunnelId,
11483
+ privateAddress: node.privateAddress,
11484
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11485
+ comment: `ForgeZero ${node.nodeName} database`,
11486
+ apiToken: managementToken
11487
+ }, fetcher);
11488
+ await ensureCloudflareWarpDatabaseInclude({
11489
+ accountId: coordinates.accountId,
11490
+ policyId: deviceProfile.policy.id,
11491
+ privateAddress: node.privateAddress,
11492
+ description: `ForgeZero ${node.nodeName} database`,
11493
+ apiToken: managementToken
11494
+ }, fetcher);
11495
+ routes.push({ nodeName: node.nodeName, routeId: route.route.id, privateAddress: node.privateAddress });
11496
+ }
11497
+ resources = {
11498
+ ...resources,
11499
+ privateNetwork: {
11500
+ warpOrganization: coordinates.privateNetwork.warpOrganization,
11501
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
11502
+ deviceProfileId: deviceProfile.policy.id,
11503
+ enrollmentApplicationId: enrollment.application.id,
11504
+ routes
11505
+ }
11506
+ };
11507
+ privateNetworkCreated = virtualNetwork.created || enrollment.created || deviceProfile.created || routes.length > 0;
11508
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11509
+ format: 1,
11510
+ kind: "forgezero-cloudflare-bootstrap",
11511
+ phase: "access-token-provisioned",
11512
+ updatedAt: new Date().toISOString(),
11513
+ coordinates,
11514
+ resources
11515
+ });
11516
+ }
11517
+ for (const node of coordinates.nodes) {
11518
+ const application = await ensureCloudflareAccessApplication({
11519
+ accountId: coordinates.accountId,
11520
+ name: node.applicationName,
11521
+ hostname: node.hostname,
11522
+ policyId: policy.policy.id,
11523
+ apiToken: tokenFor(tokens, "accessApiToken")
11524
+ }, fetcher);
11525
+ if (!application.application.id)
11526
+ throw new Error(`Cloudflare did not return the Access application id for ${node.nodeName}`);
11527
+ resources = {
11528
+ ...resources,
11529
+ nodes: resources.nodes.map((resource) => resource.nodeName === node.nodeName ? { ...resource, applicationId: application.application.id } : resource),
11530
+ access: {
11531
+ ...resources.access,
11532
+ ...node.nodeName === coordinates.nodes[0].nodeName ? { applicationId: application.application.id } : {}
11533
+ }
11534
+ };
11535
+ const createdNode = createdNodes.find(({ nodeName }) => nodeName === node.nodeName);
11536
+ createdNode.application = application.created;
11537
+ await writeOwnerBootstrapOutput(absoluteOutput, {
11538
+ format: 1,
11539
+ kind: "forgezero-cloudflare-bootstrap",
11540
+ phase: "access-token-provisioned",
11541
+ updatedAt: new Date().toISOString(),
11542
+ coordinates,
11543
+ resources
11544
+ });
11545
+ }
11546
+ await configureCloudflareWorkerAccessSecrets({
11547
+ accountId: coordinates.accountId,
11548
+ scriptName: coordinates.workerScriptName,
11549
+ credentials: serviceToken.credentials,
11550
+ apiToken: tokenFor(tokens, "workerApiToken")
11551
+ }, fetcher);
11552
+ for (const node of resources.nodes) {
11553
+ await configureCloudflareEdge({
11554
+ accountId: coordinates.accountId,
11555
+ zoneId: coordinates.zoneId,
11556
+ tunnelId: node.tunnelId,
11557
+ hostname: node.hostname,
11558
+ service: node.service,
11559
+ apiToken: tokenFor(tokens, "tunnelApiToken"),
11560
+ tunnelApiToken: tokenFor(tokens, "tunnelApiToken"),
11561
+ dnsApiToken: tokenFor(tokens, "dnsApiToken")
11562
+ }, fetcher);
11563
+ }
11564
+ const output = {
11565
+ format: 1,
11566
+ kind: "forgezero-cloudflare-bootstrap",
11567
+ phase: "complete",
11568
+ updatedAt: new Date().toISOString(),
11569
+ coordinates,
11570
+ resources,
11571
+ created: {
11572
+ tunnel: createdNodes.some(({ tunnel }) => tunnel),
11573
+ kvNamespace: namespace.created,
11574
+ serviceToken: serviceToken.created,
11575
+ policy: policy.created,
11576
+ application: createdNodes.some(({ application }) => application),
11577
+ privateNetwork: privateNetworkCreated,
11578
+ workerDeployed: true,
11579
+ nodes: createdNodes
11580
+ }
11581
+ };
11582
+ await writeOwnerBootstrapOutput(absoluteOutput, output);
11583
+ return output;
11584
+ }
11585
+ async function runAttendedCloudflareBootstrap(request2, dependencies = {}) {
11586
+ const plan = planCloudflareBootstrap(request2.coordinates, request2.checkpointPath);
11587
+ if (request2.mode === "plan") {
11588
+ return {
11589
+ format: 1,
11590
+ kind: "forgezero-cloudflare-bootstrap-evidence",
11591
+ phase: "planned",
11592
+ checkpointFile: plan.outputFile,
11593
+ workerScriptName: plan.coordinates.workerScriptName,
11594
+ publicDomains: plan.coordinates.publicDomains,
11595
+ nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
11596
+ };
11597
+ }
11598
+ if (!request2.tokenFiles || !Object.values(request2.tokenFiles).some(Boolean)) {
11599
+ throw new Error("Cloudflare apply requires owner-only management token file paths");
11600
+ }
11601
+ if (plan.coordinates.createRuntimeTokens && !request2.tokenFiles.apiTokenFile) {
11602
+ throw new Error("Cloudflare runtime-token creation requires the initial management token file");
11603
+ }
11604
+ const tokens = await readCloudflareBootstrapTokens(request2.tokenFiles);
11605
+ const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch, dependencies.workerRunner);
11606
+ return {
11607
+ format: 1,
11608
+ kind: "forgezero-cloudflare-bootstrap-evidence",
11609
+ phase: "complete",
11610
+ checkpointFile: plan.outputFile,
11611
+ kvNamespaceId: output.resources.kvNamespaceId,
11612
+ workerScriptName: output.coordinates.workerScriptName,
11613
+ publicDomains: output.resources.worker?.publicDomains ?? output.coordinates.publicDomains,
11614
+ runtimeTokenIds: {
11615
+ kv: output.resources.runtimeTokens?.kv.id,
11616
+ privateNetwork: output.resources.runtimeTokens?.privateNetwork?.id
11617
+ },
11618
+ nodes: output.resources.nodes.map(({ nodeName, hostname, tunnelId, applicationId }) => ({
11619
+ nodeName,
11620
+ hostname,
11621
+ tunnelId,
11622
+ applicationId
11623
+ }))
11624
+ };
11625
+ }
11626
+
11627
+ // src/bootstrap.ts
11628
+ init_dist();
11629
+ var PLATFORM_BOOTSTRAP_PROFILES = [
11630
+ "platform-db-api",
11631
+ "platform-api"
11632
+ ];
11633
+ var STATE_PATH = "/var/lib/forgezero/bootstrap.json";
11634
+ var CREDS = "/etc/forgezero/creds";
11635
+ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
11636
+ var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
11637
+ var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
11638
+ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
11639
+ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
11640
+ var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
11641
+ var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
11642
+ var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
11643
+ var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
11644
+ var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
11645
+ var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
11646
+ var privateOrigin = (value) => {
11647
+ let url;
11648
+ try {
11649
+ url = new URL(value);
11650
+ } catch {
11651
+ throw new Error(`database coordinator is malformed: ${value}`);
11652
+ }
11653
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
11654
+ throw new Error(`database coordinators must be credential-free HTTP(S) origins: ${value}`);
11655
+ }
11656
+ const host = url.hostname.replace(/^\[|\]$/g, "");
11657
+ const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
11658
+ const privateV4 = v4 && v4.every((part) => part >= 0 && part <= 255) && (v4[0] === 10 || v4[0] === 172 && v4[1] >= 16 && v4[1] <= 31 || v4[0] === 192 && v4[1] === 168);
11659
+ const privateV6 = host === "::1" || /^f[cd][0-9a-f]:/i.test(host);
11660
+ if (!privateV4 && !privateV6 && host !== "127.0.0.1" && host !== "localhost") {
11661
+ throw new Error(`database coordinator must use a private address: ${value}`);
11662
+ }
11663
+ return url.origin;
11664
+ };
11665
+ var privateCidr = (value) => {
11666
+ const match = value.match(/^(10\.\d{1,3}\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3})\/(\d|[12]\d|3[0-2])$/);
11667
+ if (!match || match[1].split(".").some((part) => Number(part) > 255))
11668
+ throw new Error(`private firewall CIDR is malformed: ${value}`);
11669
+ return value;
11670
+ };
11671
+ var privateFile = (host, path, label) => {
11672
+ if (!host.exists(path))
11673
+ throw new Error(`${label} file is missing: ${path}`);
11674
+ const metadata = host.inspect?.(path);
11675
+ if (metadata && (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 16 * 1024)) {
11676
+ throw new Error(`${label} must be a root-owned, owner-only regular file with one link and at most 16 KiB`);
11677
+ }
11678
+ const value = host.read(path).trim();
11679
+ if (!value)
11680
+ throw new Error(`${label} file is empty: ${path}`);
11681
+ return value;
11682
+ };
11683
+ function validateBootstrapConfig(value) {
11684
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?){2,}$/.test(value.nodeHostname)) {
11685
+ throw new Error("node hostname must be a lowercase public FQDN");
11686
+ }
11687
+ let telemetry;
11688
+ try {
11689
+ telemetry = new URL(value.telemetryEndpoint);
11690
+ } catch {
11691
+ throw new Error("telemetry endpoint is malformed");
11692
+ }
11693
+ if (telemetry.protocol !== "https:" || telemetry.port && telemetry.port !== "443" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
11694
+ throw new Error("telemetry endpoint must be public HTTPS on port 443 without credentials, query or fragment");
11695
+ }
11696
+ if (value.kind === "tenant") {
11697
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
11698
+ throw new Error("tenant realm is malformed");
11699
+ if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
11700
+ throw new Error("tenant API must be public HTTPS or loopback HTTP");
11701
+ }
11702
+ if (!value.enrolTokenFile)
11703
+ throw new Error("tenant bootstrap requires --enrol-token-file");
11704
+ return value;
11705
+ }
11706
+ if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
11707
+ throw new Error("unsupported platform software profile");
11708
+ if (!["production", "development"].includes(value.environment))
11709
+ throw new Error("platform environment must be production or development");
11710
+ let api;
11711
+ try {
11712
+ api = new URL(value.apiUrl);
11713
+ } catch {
11714
+ throw new Error("platform API URL is malformed");
11715
+ }
11716
+ if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash) {
11717
+ throw new Error("platform API must be public HTTPS without credentials, query or fragment");
11718
+ }
11719
+ const expected = value.profile === "platform-api" ? { role: "none", mode: "default" } : { role: undefined, mode: "default" };
11720
+ if (expected.role && value.database.role !== expected.role || value.database.serverMode !== expected.mode || value.profile === "platform-db-api" && !["master", "joiner"].includes(value.database.role)) {
11721
+ throw new Error("platform profile, database role and Coordinator mode disagree");
11722
+ }
11723
+ if (value.database.role !== "none" && !value.database.address)
11724
+ throw new Error("database nodes require a private address");
11725
+ if (value.database.role === "joiner" && !value.database.master)
11726
+ throw new Error("database joiners require the master starter address");
11727
+ if (!/^(?:dev-)?fz-n[1-9][0-9]{0,2}$/.test(value.computeReference))
11728
+ throw new Error("platform compute reference is malformed");
11729
+ if (!value.database.bootstrapSecretFile)
11730
+ throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
11731
+ const write = value.database.coordinators.map(privateOrigin);
11732
+ if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
11733
+ throw new Error("database coordinators must contain 1-16 unique private origins");
11734
+ }
11735
+ value.database.coordinators = write;
11736
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
11737
+ allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
11738
+ });
11739
+ if (runtime.softwareProfile !== value.profile || runtime.databaseRole !== value.database.role || runtime.nodeHostname !== value.nodeHostname || runtime.apiOrigin !== api.origin || runtime.repository !== value.repository || runtime.branch !== value.branch || runtime.databaseCoordinators.join(",") !== write.join(",")) {
11740
+ throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
11741
+ }
11742
+ if (runtime.deployProfile !== value.environment)
11743
+ throw new Error("runtime deployment profile disagrees with bootstrap environment");
11744
+ if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
11745
+ throw new Error("runtime database topology disagrees with bootstrap topology");
11746
+ }
11747
+ if (!Number.isSafeInteger(value.firewall.sshPort) || value.firewall.sshPort < 1 || value.firewall.sshPort > 65535) {
11748
+ throw new Error("firewall SSH port is invalid");
11749
+ }
11750
+ value.firewall.privateCidrs = value.firewall.privateCidrs.map(privateCidr);
11751
+ if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
11752
+ throw new Error("enabled firewall requires unique private cluster CIDRs");
11753
+ }
11754
+ if (Number(value.computeReference.match(/n(\d+)$/)?.[1]) > 3 && !value.platformEnrolTokenFile) {
11755
+ throw new Error("post-genesis platform computes require an API-issued enrolment token file");
11756
+ }
11757
+ if (value.cloudflareHandoff && (!value.installCloudflared || !value.cloudflareHandoff.checkpointFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
11758
+ throw new Error("Cloudflare handoff requires cloudflared installation, a checkpoint and a valid node name");
11759
+ }
11760
+ value.runtime.environment = runtime;
11761
+ return value;
11762
+ }
11763
+ function planBootstrap(input, initialized = false) {
11764
+ const config = validateBootstrapConfig(structuredClone(input));
11765
+ const software = config.kind === "tenant" ? [...config.software ?? [], ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []] : [
11766
+ ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
11767
+ { id: "bun", version: "1.3.14" },
11768
+ { id: "nginx", version: "ubuntu-26.04" },
11769
+ ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
11770
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
11771
+ ];
11772
+ return {
11773
+ kind: config.kind,
11774
+ mode: initialized ? "repair" : "install",
11775
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
11776
+ software,
11777
+ statePath: STATE_PATH,
11778
+ steps: [
11779
+ { id: "agent", label: "Install and verify the common Agent supervision boundary", mutation: true },
11780
+ { id: "software", label: "Install exact Agent-owned software coordinates in order", mutation: true },
11781
+ { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
11782
+ ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
11783
+ ...config.kind === "platform" ? [
11784
+ { id: "runtime", label: "Install the shared environment, API slots, edge and activation boundary", mutation: true },
11785
+ { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
11786
+ { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
11787
+ ] : [],
11788
+ ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
11789
+ { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
11790
+ { id: "status", label: "Verify services and immutable profile", mutation: false }
11791
+ ]
11792
+ };
11793
+ }
11794
+ var checked = async (host, argv, label, options) => {
11795
+ const result = await host.exec(argv, options);
11796
+ if (result.exitCode !== 0)
11797
+ throw new Error(`${label} failed: ${result.output.trim()}`);
11798
+ return result.output;
11799
+ };
11800
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
11801
+ var unitEscape = (value) => {
11802
+ if (/[^A-Za-z0-9_./:@,+-]/.test(value))
11803
+ throw new Error(`unsafe systemd coordinate: ${value}`);
11804
+ return value;
11805
+ };
11806
+ function databaseUnit(config) {
11807
+ const db = config.database;
11808
+ const join6 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
11809
+ return `[Unit]
11810
+ Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role})
11811
+ After=network-online.target
11812
+ Wants=network-online.target
11813
+
11814
+ [Service]
11815
+ Type=simple
11816
+ User=arangodb
11817
+ Group=arangodb
11818
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
11819
+ ExecStart=/usr/bin/arangodb --starter.mode=cluster --starter.address=${unitEscape(db.address)} --starter.host=${unitEscape(db.address)} --starter.data-dir=/var/lib/forgezero-cluster --auth.jwt-secret=%d/arangodb-jwt${join6}
11820
+ Restart=always
11821
+ RestartSec=5
11822
+ UMask=0077
11823
+ NoNewPrivileges=true
11824
+ PrivateTmp=true
11825
+ PrivateDevices=true
11826
+ ProtectSystem=strict
11827
+ ProtectHome=true
11828
+ ReadWritePaths=/var/lib/forgezero-cluster
11829
+ ProtectKernelTunables=true
11830
+ ProtectKernelModules=true
11831
+ ProtectControlGroups=true
11832
+ RestrictSUIDSGID=true
11833
+ LockPersonality=true
11834
+
11835
+ [Install]
11836
+ WantedBy=multi-user.target
11837
+ `;
11838
+ }
11839
+ function databaseVerifyUnit(config) {
11840
+ const { address } = config.database;
11841
+ return `[Unit]
11842
+ Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
11843
+ Requires=forgezero-db.service
11844
+ After=forgezero-db.service
11845
+ PartOf=forgezero-db.service
11846
+
11847
+ [Service]
11848
+ Type=oneshot
11849
+ User=arangodb
11850
+ Group=arangodb
11851
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
11852
+ ExecStart=/usr/bin/arangosh --server.endpoint tcp://${unitEscape(address)}:8529 --server.jwt-secret-keyfile %d/arangodb-jwt --javascript.execute-string 'const c=require("@arangodb").db._connection;let last;for(let i=0;i<90;i++){try{const v=c.GET("/_api/version?details=true");const s=c.GET("/_admin/status");const m=c.GET("/_admin/server/mode");if(!v.error&&v.version==="3.11.14"&&v.details&&v.details.license==="community"&&!s.error&&s.serverInfo&&s.serverInfo.role==="COORDINATOR"&&!m.error&&m.mode==="default")quit(0);last={v,s,m};}catch(e){last=String(e);}require("internal").wait(2);}throw new Error("writable Community Coordinator verification failed: "+JSON.stringify(last));'
11853
+ RemainAfterExit=yes
11854
+ TimeoutStartSec=200
11855
+ NoNewPrivileges=true
11856
+ PrivateTmp=true
11857
+ PrivateDevices=true
11858
+ ProtectSystem=strict
11859
+ ProtectHome=true
11860
+
11861
+ [Install]
11862
+ WantedBy=multi-user.target
11863
+ `;
11864
+ }
11865
+ function tunnelUnit() {
11866
+ return `[Unit]
11867
+ Description=ForgeZero Cloudflare Tunnel connector
11868
+ After=network-online.target
11869
+ Wants=network-online.target
11870
+
11871
+ [Service]
11872
+ Type=simple
11873
+ DynamicUser=yes
11874
+ LoadCredentialEncrypted=cloudflared-token:${TUNNEL_CREDENTIAL}
11875
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate run --token-file %d/cloudflared-token
11876
+ Restart=always
11877
+ RestartSec=5
11878
+ NoNewPrivileges=true
11879
+ PrivateTmp=true
11880
+ ProtectSystem=strict
11881
+ ProtectHome=true
11882
+
11883
+ [Install]
11884
+ WantedBy=multi-user.target
11885
+ `;
11886
+ }
11887
+ var derive = (root, label) => {
11888
+ if (!/^[a-f0-9]{64}$/i.test(root))
11889
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
11890
+ return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
11891
+ };
11892
+ async function seal(host, name, destination, value) {
11893
+ if (host.exists(destination))
11894
+ return;
11895
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
11896
+ if (result.exitCode !== 0)
11897
+ throw new Error(`could not seal ${name}: ${result.output.trim()}`);
11898
+ }
11899
+ function stateFor(config) {
11900
+ return `${JSON.stringify({
11901
+ format: 1,
11902
+ kind: config.kind,
11903
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
11904
+ nodeHostname: config.nodeHostname,
11905
+ apiUrl: config.apiUrl,
11906
+ ...config.kind === "platform" ? {
11907
+ environment: config.environment,
11908
+ databaseRole: config.database.role,
11909
+ databaseServerMode: config.database.serverMode,
11910
+ databaseAddress: config.database.address,
11911
+ databaseCoordinators: config.database.coordinators,
11912
+ databaseModeEvidence: config.database.role === "none" ? undefined : DB_MODE_EVIDENCE,
11913
+ collectorUnit: config.runtime.environment.otlpCollectorUnit,
11914
+ cloudflared: Boolean(config.cloudflareHandoff)
11915
+ } : { realm: config.realm }
11916
+ }, null, 2)}
11917
+ `;
11918
+ }
11919
+ async function bootstrapStatus(host = localBootstrapHost()) {
11920
+ if (!host.exists(STATE_PATH))
11921
+ return { initialized: false, services: {}, problems: ["bootstrap state is missing"] };
11922
+ let state;
11923
+ try {
11924
+ state = JSON.parse(host.read(STATE_PATH));
11925
+ } catch {
11926
+ return { initialized: false, services: {}, problems: ["bootstrap state is malformed"] };
11927
+ }
11928
+ const units = ["forgezero-agent.service", "forgezero-agent.socket"];
11929
+ if (state.kind === "platform")
11930
+ units.push("nginx.service");
11931
+ if (state.kind === "platform" && state.collectorUnit)
11932
+ units.push(state.collectorUnit);
11933
+ if (state.kind === "platform" && state.cloudflared)
11934
+ units.push("cloudflared.service");
11935
+ if (state.kind === "platform" && state.databaseRole !== "none")
11936
+ units.push("forgezero-db.service", "forgezero-db-verify.service");
11937
+ const services = {};
11938
+ const problems = [];
11939
+ for (const unit of units) {
11940
+ const result = await host.exec(["systemctl", "is-active", "--quiet", unit]);
11941
+ services[unit] = result.exitCode === 0;
11942
+ if (result.exitCode !== 0)
11943
+ problems.push(`${unit} is not active`);
11944
+ }
11945
+ if (state.kind === "platform") {
11946
+ const [blue, green] = await Promise.all([
11947
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
11948
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
11949
+ ]);
11950
+ services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
11951
+ if (!services["forgezero@active.service"])
11952
+ problems.push("neither API slot is active");
11953
+ }
11954
+ if (!host.exists("/var/lib/forgezero/enrolment.json"))
11955
+ problems.push("durable Agent enrolment state is missing");
11956
+ return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
11957
+ }
11958
+ async function applyBootstrap(input, host = localBootstrapHost()) {
11959
+ const config = validateBootstrapConfig(structuredClone(input));
11960
+ if (host.uid() !== 0)
11961
+ throw new Error("fz bootstrap --apply must run as root");
11962
+ if (config.kind === "tenant") {
11963
+ const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
11964
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
11965
+ throw new Error("tenant enrolment token is malformed");
11966
+ }
11967
+ let cloudflare;
11968
+ if (config.kind === "platform" && config.cloudflareHandoff) {
11969
+ cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.checkpointFile, config.cloudflareHandoff.nodeName);
11970
+ const expected = config.runtime.environment.cloudflare;
11971
+ if (cloudflare.hostname !== config.nodeHostname)
11972
+ throw new Error("Cloudflare checkpoint hostname disagrees with platform node hostname");
11973
+ if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
11974
+ throw new Error("Cloudflare checkpoint disagrees with immutable platform runtime coordinates");
11975
+ }
11976
+ const discovered = {
11977
+ accountId: cloudflare.accountId,
11978
+ zoneId: cloudflare.zoneId,
11979
+ kvNamespaceId: cloudflare.kvNamespaceId,
11980
+ tunnelId: cloudflare.tunnelId,
11981
+ tunnelService: cloudflare.service,
11982
+ ...cloudflare.warp ? { warp: {
11983
+ organization: cloudflare.warp.organization,
11984
+ virtualNetworkId: cloudflare.warp.virtualNetworkId,
11985
+ deviceProfileId: cloudflare.warp.deviceProfileId
11986
+ } } : {}
11987
+ };
11988
+ if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
11989
+ throw new Error("Cloudflare checkpoint disagrees with immutable WARP/runtime coordinates");
11990
+ }
11991
+ config.runtime.environment.cloudflare = expected ?? discovered;
11992
+ if (config.runtime.environment.databaseNetworkMode === "cloudflare-warp" !== Boolean(cloudflare.warp)) {
11993
+ throw new Error("Cloudflare checkpoint private-network mode disagrees with the platform database network mode");
11994
+ }
11995
+ }
11996
+ const plan = planBootstrap(config, host.exists(STATE_PATH));
11997
+ const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
11998
+ host.mkdir(CREDS, 448);
11999
+ host.mkdir("/var/lib/forgezero", 448);
12000
+ if (cloudflare?.warp) {
12001
+ await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
12002
+ await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
12003
+ }
12004
+ await host.installAgent(config, alreadyEnrolled && config.kind === "platform" ? PLATFORM_ENROL_SOURCE : undefined);
12005
+ if (config.kind === "platform" && config.firewall.enabled) {
12006
+ await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 === "ufw"));
12007
+ } else
12008
+ await host.ensureSoftware(plan.software);
12009
+ if (config.kind === "platform" && config.firewall.enabled) {
12010
+ await checked(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
12011
+ await checked(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
12012
+ await checked(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
12013
+ for (const cidr of config.firewall.privateCidrs) {
12014
+ if (config.database.role !== "none")
12015
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
12016
+ for (const port of [config.runtime.bluePort, config.runtime.greenPort])
12017
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
12018
+ }
12019
+ await checked(host, ["ufw", "--force", "enable"], "firewall activation");
12020
+ await host.ensureSoftware(plan.software.filter(({ id: id2 }) => id2 !== "ufw"));
12021
+ }
12022
+ if (config.kind === "platform") {
12023
+ const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
12024
+ if (!host.exists(JWT_CREDENTIAL)) {
12025
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
12026
+ }
12027
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
12028
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
12029
+ const credentialFiles = config.runtime.credentialFiles ?? {};
12030
+ for (const [name, source] of Object.entries({
12031
+ "bootstrap-smtp-password": credentialFiles.smtpPassword,
12032
+ "backup-s3-secret": credentialFiles.backupS3Secret
12033
+ })) {
12034
+ if (source) {
12035
+ const destination = `${CREDS}/${name}.cred`;
12036
+ if (!host.exists(destination)) {
12037
+ await seal(host, name, destination, privateFile(host, source, name));
12038
+ host.remove(source);
12039
+ }
12040
+ }
12041
+ }
12042
+ if (cloudflare) {
12043
+ await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
12044
+ if (cloudflare.privateNetworkRuntimeToken)
12045
+ await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
12046
+ }
12047
+ const runtime = config.runtime;
12048
+ const envPath = `${runtime.environment.sharedDirectory}/.env`;
12049
+ const credentials = platformApiCredentialSpecs({
12050
+ smtp: Boolean(credentialFiles.smtpPassword),
12051
+ cloudflareKv: Boolean(cloudflare),
12052
+ cloudflareNetwork: Boolean(cloudflare?.privateNetworkRuntimeToken)
12053
+ });
12054
+ const units = renderPlatformApiUnits({
12055
+ serviceUser: runtime.serviceUser,
12056
+ sharedDirectory: runtime.environment.sharedDirectory,
12057
+ sharedEnvironmentFile: envPath,
12058
+ slotsDirectory: runtime.slotsDirectory,
12059
+ bluePort: runtime.bluePort,
12060
+ greenPort: runtime.greenPort,
12061
+ collectorUnit: runtime.environment.otlpCollectorUnit,
12062
+ credentials
12063
+ });
12064
+ const edge = renderPlatformNginx({ publicPort: runtime.environment.publicApiPort, initialSlotPort: runtime.bluePort });
12065
+ const activation = renderPlatformActivationFiles({
12066
+ root: config.deployRoot ?? "/opt/forgezero",
12067
+ serviceUser: runtime.serviceUser,
12068
+ bluePort: runtime.bluePort,
12069
+ greenPort: runtime.greenPort,
12070
+ healthPath: runtime.healthPath,
12071
+ keepReleases: runtime.keepReleases
12072
+ });
12073
+ await checked(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
12074
+ await checked(host, ["id", runtime.serviceUser], "existing API service account");
12075
+ });
12076
+ host.mkdir(runtime.environment.sharedDirectory, 488);
12077
+ host.mkdir(runtime.slotsDirectory, 493);
12078
+ host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
12079
+ await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
12080
+ host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
12081
+ host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
12082
+ host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
12083
+ host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
12084
+ host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
12085
+ host.write("/etc/forgezero/deploy.env", activation.environment, 420);
12086
+ host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
12087
+ host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
12088
+ await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
12089
+ const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
12090
+ await checked(host, telemetry.unitCheck.argv, "OTLP collector supervision");
12091
+ const otlpStatus = (await checked(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
12092
+ if (!/^2\d\d$/.test(otlpStatus))
12093
+ throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
12094
+ await checked(host, ["nginx", "-t"], "nginx configuration");
12095
+ await checked(host, ["systemctl", "daemon-reload"], "systemd reload");
12096
+ await checked(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
12097
+ if (config.database.role === "master") {
12098
+ const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
12099
+ if (!host.exists(invite)) {
12100
+ host.write(invite, `plt_${randomBytes5(24).toString("hex")}
12101
+ `, 384);
12102
+ await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
12103
+ }
12104
+ }
12105
+ if (config.database.role !== "none") {
12106
+ host.mkdir("/var/lib/forgezero-cluster", 448);
12107
+ await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
12108
+ host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
12109
+ host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
12110
+ await checked(host, ["systemctl", "daemon-reload"], "database unit reload");
12111
+ await checked(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
12112
+ const evidence = {
12113
+ expectedMode: "default",
12114
+ role: "COORDINATOR",
12115
+ unit: "forgezero-db-verify.service",
12116
+ verifiedAt: new Date().toISOString()
12117
+ };
12118
+ host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
12119
+ `, 384);
12120
+ }
12121
+ if (!alreadyEnrolled) {
12122
+ const enrolToken = config.platformEnrolTokenFile ? privateFile(host, config.platformEnrolTokenFile, "platform enrolment token") : `fze_${derive(derive(root, "forgezero/cluster/arangodb-jwt/v1"), `forgezero/platform-enrolment/v1/${config.computeReference}`)}`;
12123
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
12124
+ throw new Error("platform enrolment token is malformed");
12125
+ host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
12126
+ `, 384);
12127
+ }
12128
+ await checked(host, [
12129
+ "runuser",
12130
+ "-u",
12131
+ "forgezero-agent",
12132
+ "--",
12133
+ "/usr/local/bin/fz-agent",
12134
+ "deploy",
12135
+ ...config.database.role === "master" ? ["--release-executor"] : []
12136
+ ], "initial Agent deployment");
12137
+ if (!alreadyEnrolled) {
12138
+ await host.installAgent(config, PLATFORM_ENROL_SOURCE);
12139
+ if (config.platformEnrolTokenFile)
12140
+ host.remove(config.platformEnrolTokenFile);
12141
+ }
12142
+ }
12143
+ if (config.kind === "platform" && cloudflare) {
12144
+ if (!host.exists(TUNNEL_CREDENTIAL)) {
12145
+ await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
12146
+ }
12147
+ host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
12148
+ await checked(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
12149
+ await checked(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
12150
+ }
12151
+ host.write(STATE_PATH, stateFor(config), 384);
12152
+ const status = await bootstrapStatus(host);
12153
+ if (!status.initialized)
12154
+ throw new Error(`bootstrap verification failed: ${status.problems.join("; ")}`);
12155
+ let launch;
12156
+ if (config.kind === "platform" && config.database.role === "master") {
12157
+ const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
12158
+ if (!host.exists(invitePath))
12159
+ throw new Error("platform invite is missing after deployment");
12160
+ const token = host.read(invitePath).trim();
12161
+ if (!/^plt_[a-f0-9]{48}$/.test(token))
12162
+ throw new Error("platform invite is malformed");
12163
+ launch = {
12164
+ command: `sudo -u ${shellQuote(config.runtime.serviceUser)} env FZ_SHARED_DIR=${shellQuote(config.runtime.environment.sharedDirectory)} /usr/local/bin/fz genesis --mode 2-of-3 --api ${shellQuote(config.apiUrl)} --app ${shellQuote(config.runtime.environment.appOrigin)}`,
12165
+ inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
12166
+ };
12167
+ }
12168
+ return { plan, applied: true, status, ...launch ? { launch } : {} };
12169
+ }
12170
+ var exactKeys2 = (value, allowed, label) => {
12171
+ if (!value || typeof value !== "object" || Array.isArray(value))
12172
+ throw new Error(`${label} must be an object`);
12173
+ const record2 = value;
12174
+ const unknown = Object.keys(record2).filter((key) => !allowed.includes(key));
12175
+ if (unknown.length)
12176
+ throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
12177
+ return record2;
12178
+ };
12179
+ function strictBootstrapDocument(value) {
12180
+ const root = exactKeys2(value, [
12181
+ "kind",
12182
+ "environment",
12183
+ "profile",
12184
+ "computeReference",
12185
+ "nodeHostname",
12186
+ "apiUrl",
12187
+ "repository",
12188
+ "branch",
12189
+ "deployRoot",
12190
+ "telemetryEndpoint",
12191
+ "database",
12192
+ "platformEnrolTokenFile",
12193
+ "runtime",
12194
+ "firewall",
12195
+ "installCloudflared",
12196
+ "cloudflareHandoff",
12197
+ "realm",
12198
+ "enrolTokenFile",
12199
+ "software"
12200
+ ], "bootstrap config");
12201
+ if (root.kind === "platform") {
12202
+ exactKeys2(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
12203
+ if (root.cloudflareHandoff !== undefined)
12204
+ exactKeys2(root.cloudflareHandoff, ["checkpointFile", "nodeName"], "Cloudflare handoff");
12205
+ exactKeys2(root.database, ["role", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
12206
+ const runtime = exactKeys2(root.runtime, [
12207
+ "environment",
12208
+ "serviceUser",
12209
+ "slotsDirectory",
12210
+ "bluePort",
12211
+ "greenPort",
12212
+ "healthPath",
12213
+ "keepReleases",
12214
+ "credentialFiles"
12215
+ ], "runtime config");
12216
+ exactKeys2(runtime.environment, [
12217
+ "softwareProfile",
12218
+ "databaseRole",
12219
+ "databaseCoordinators",
12220
+ "databaseAddress",
12221
+ "databaseMaster",
12222
+ "databaseNetworkMode",
12223
+ "databaseReplicationFactor",
12224
+ "databaseWriteConcern",
12225
+ "nodeHostname",
12226
+ "nodeRegion",
12227
+ "databaseUser",
12228
+ "nodeRole",
12229
+ "appOrigin",
12230
+ "apiOrigin",
12231
+ "publicApiPort",
12232
+ "sharedDirectory",
12233
+ "seedSyncPeers",
12234
+ "seedSyncMembers",
12235
+ "seedSyncEpoch",
12236
+ "concurrencyLimit",
12237
+ "drainDeadlineMs",
12238
+ "otlpEndpoint",
12239
+ "otlpCollectorUnit",
12240
+ "agentOtlpEndpoint",
12241
+ "custodianEmail",
12242
+ "smtp",
12243
+ "repository",
12244
+ "branch",
12245
+ "deployProfile",
12246
+ "otlpFlushIntervalMs",
12247
+ "otlpTraceSampleRatio",
12248
+ "backup",
12249
+ "cloudflare"
12250
+ ], "runtime environment");
12251
+ if (runtime.credentialFiles !== undefined)
12252
+ exactKeys2(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
12253
+ const environment = runtime.environment;
12254
+ if (environment.smtp !== undefined)
12255
+ exactKeys2(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
12256
+ if (environment.backup !== undefined)
12257
+ exactKeys2(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
12258
+ if (environment.cloudflare !== undefined)
12259
+ exactKeys2(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService", "warp"], "Cloudflare runtime config");
12260
+ if (environment.cloudflare && typeof environment.cloudflare === "object" && environment.cloudflare.warp !== undefined) {
12261
+ exactKeys2(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
12262
+ }
12263
+ } else if (root.kind === "tenant") {
12264
+ for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
12265
+ if (root[key] !== undefined && key !== "profile")
12266
+ throw new Error(`tenant bootstrap cannot contain ${key}`);
12267
+ }
12268
+ } else
12269
+ throw new Error("bootstrap config kind must be platform or tenant");
12270
+ return value;
12271
+ }
12272
+ function readBootstrapConfig(path) {
12273
+ const metadata = lstatSync2(path);
12274
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
12275
+ throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
12276
+ }
12277
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync4(path, "utf8"))));
12278
+ }
12279
+ function localBootstrapHost() {
12280
+ const execute = async (argv, options = {}) => {
12281
+ const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
12282
+ if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
12283
+ child.stdin.write(options.stdin);
12284
+ child.stdin.end();
12285
+ }
12286
+ const [stdout, stderr, exitCode] = await Promise.all([
12287
+ new Response(child.stdout).text(),
12288
+ new Response(child.stderr).text(),
12289
+ child.exited
12290
+ ]);
12291
+ return { exitCode, output: `${stdout}${stderr}` };
12292
+ };
12293
+ return {
12294
+ uid: () => process.getuid?.() ?? -1,
12295
+ exists: existsSync4,
12296
+ read: (path) => readFileSync4(path, "utf8"),
12297
+ write(path, content, mode) {
12298
+ mkdirSync4(dirname4(path), { recursive: true, mode: 493 });
12299
+ const temporary = `${path}.next.${process.pid}`;
12300
+ writeFileSync4(temporary, content, { mode });
12301
+ chmodSync2(temporary, mode);
12302
+ renameSync3(temporary, path);
12303
+ },
12304
+ mkdir: (path, mode) => mkdirSync4(path, { recursive: true, mode }),
12305
+ remove: (path) => rmSync(path, { force: true }),
12306
+ inspect(path) {
12307
+ const value = lstatSync2(path);
12308
+ return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
12309
+ },
12310
+ exec: execute,
12311
+ async ensureSoftware(requirements) {
12312
+ const result = await execute([
12313
+ "runuser",
12314
+ "-u",
12315
+ "forgezero-agent",
12316
+ "--",
12317
+ "/usr/local/bin/fz-agent",
12318
+ "software-ensure",
12319
+ ...requirements.map(({ id: id2, version }) => `--require=${id2}@${version}`)
12320
+ ]);
12321
+ if (result.exitCode !== 0)
12322
+ throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
12323
+ return result;
12324
+ },
12325
+ async installAgent(config, enrolTokenSourcePath) {
12326
+ const capabilities = await readCapabilities(localRunner);
12327
+ const deployRoot = config.deployRoot ?? "/opt/forgezero";
12328
+ if (config.kind === "platform") {
12329
+ const lifecycle = config.database.role === "none" ? {
12330
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
12331
+ apiHealthUrl: "http://127.0.0.1:3000/api/health"
12332
+ } : {
12333
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
12334
+ databaseUnit: "forgezero-db.service",
12335
+ apiHealthUrl: "http://127.0.0.1:3000/api/health",
12336
+ databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
12337
+ databasePorts: [8529]
12338
+ };
12339
+ mkdirSync4(dirname4(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
12340
+ writeFileSync4(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
12341
+ `, { mode: 256 });
12342
+ }
12343
+ const plan = planInstall({
12344
+ capabilities,
12345
+ socketPath: DEFAULT_SOCKET,
12346
+ seedPath: "/var/lib/forgezero/node.seed",
12347
+ controlSocketPath: "/run/forgezero/control.sock",
12348
+ repository: config.repository,
12349
+ branch: config.branch,
12350
+ profile: config.kind === "platform" ? config.profile : config.profile,
12351
+ deployRoot,
12352
+ publicApiUrl: config.apiUrl,
12353
+ gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
12354
+ gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
12355
+ generateGitIdentity: true,
12356
+ pullDeployments: true,
12357
+ pullMigrations: config.kind === "platform",
12358
+ lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
12359
+ ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
12360
+ warpOrganization: config.runtime.environment.cloudflare.warp.organization,
12361
+ warpClientIdCredentialPath: WARP_CLIENT_ID_CREDENTIAL,
12362
+ warpClientSecretCredentialPath: WARP_CLIENT_SECRET_CREDENTIAL,
12363
+ cloudflareAccountId: config.runtime.environment.cloudflare.accountId,
12364
+ cloudflareTunnelId: config.runtime.environment.cloudflare.tunnelId,
12365
+ cloudflareVirtualNetworkId: config.runtime.environment.cloudflare.warp.virtualNetworkId,
12366
+ cloudflareWarpPolicyId: config.runtime.environment.cloudflare.warp.deviceProfileId
12367
+ } : {},
12368
+ enforceEgress: true,
12369
+ nodeHostname: config.nodeHostname,
12370
+ telemetryEndpoint: config.telemetryEndpoint,
12371
+ binPath: "/usr/local/lib/forgezero/agent/fz-agent",
12372
+ sourceBinPath: PACKAGED_AGENT_BIN,
12373
+ ...config.kind === "tenant" || enrolTokenSourcePath ? {
12374
+ enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
12375
+ enrolTokenCredentialPath: ENROL_CREDENTIAL,
12376
+ enrolStatePath: "/var/lib/forgezero/enrolment.json",
12377
+ apiUrl: config.apiUrl,
12378
+ project: config.kind === "tenant" ? config.realm : "platform",
12379
+ environment: config.kind === "tenant" ? undefined : config.environment,
12380
+ nodeLabel: config.kind === "tenant" ? config.nodeHostname : config.computeReference
12381
+ } : {}
12382
+ });
12383
+ for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
12384
+ mkdirSync4(dirname4(unit.path), { recursive: true, mode: 493 });
12385
+ writeFileSync4(unit.path, unit.unit, { mode: 420 });
12386
+ }
12387
+ await applyPlan(plan, localRunner);
12388
+ return plan;
12389
+ }
12390
+ };
12391
+ }
12392
+
12393
+ // src/cli/cloudflare-bootstrap.ts
12394
+ import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync5 } from "fs";
12395
+ import { dirname as dirname5, resolve as resolve3 } from "path";
12396
+ var exactKeys3 = (value, allowed, label) => {
12397
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
12398
+ if (unknown.length)
12399
+ throw new Error(`${label} contains unsupported field ${unknown[0]}`);
12400
+ };
12401
+ var record2 = (value, label) => {
12402
+ if (!value || typeof value !== "object" || Array.isArray(value))
12403
+ throw new Error(`${label} must be an object`);
12404
+ return value;
12405
+ };
12406
+ function readOwnerConfig(path) {
12407
+ const absolute = resolve3(path);
12408
+ let descriptor;
12409
+ try {
12410
+ descriptor = openSync(absolute, constants2.O_RDONLY | constants2.O_NOFOLLOW);
12411
+ const metadata = fstatSync(descriptor);
12412
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
12413
+ if (!metadata.isFile() || metadata.nlink !== 1 || metadata.size < 2 || metadata.size > 131072 || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
12414
+ throw new Error(`${absolute} must be one operator-owned 0600 regular file`);
12415
+ }
12416
+ return JSON.parse(readFileSync5(descriptor, "utf8"));
12417
+ } catch (cause) {
12418
+ if (cause instanceof SyntaxError)
12419
+ throw new Error(`${absolute} is not valid Cloudflare bootstrap JSON`);
12420
+ if (cause instanceof Error && cause.message.startsWith(absolute))
12421
+ throw cause;
12422
+ throw new Error(`cannot securely read Cloudflare bootstrap config ${absolute}`);
12423
+ } finally {
12424
+ if (descriptor !== undefined)
12425
+ closeSync(descriptor);
12426
+ }
12427
+ }
12428
+ function readCloudflareBootstrapCommandConfig(path, mode) {
12429
+ const input = record2(readOwnerConfig(path), "Cloudflare bootstrap config");
12430
+ const baseDirectory = dirname5(resolve3(path));
12431
+ exactKeys3(input, ["format", "kind", "checkpointPath", "coordinates", "tokenFiles"], "Cloudflare bootstrap config");
12432
+ if (input.format !== 1 || input.kind !== "forgezero-cloudflare-bootstrap-request") {
12433
+ throw new Error("Cloudflare bootstrap config format/kind is invalid");
12434
+ }
12435
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
12436
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
12437
+ }
12438
+ const coordinateSource = record2(input.coordinates, "Cloudflare bootstrap coordinates");
12439
+ exactKeys3(coordinateSource, [
12440
+ "accountId",
12441
+ "zoneId",
12442
+ "hostname",
12443
+ "service",
12444
+ "tunnelName",
12445
+ "kvNamespaceTitle",
12446
+ "workerScriptName",
12447
+ "serviceTokenName",
12448
+ "policyName",
12449
+ "applicationName",
12450
+ "workerDirectory",
12451
+ "workerMain",
12452
+ "workerCompatibilityDate",
12453
+ "publicDomains",
12454
+ "createRuntimeTokens",
12455
+ "createPrivateNetworkRuntimeToken",
12456
+ "runtimeTokenNamePrefix",
12457
+ "nodes",
12458
+ "privateNetwork"
12459
+ ], "Cloudflare bootstrap coordinates");
12460
+ if (coordinateSource.nodes !== undefined) {
12461
+ if (!Array.isArray(coordinateSource.nodes))
12462
+ throw new Error("Cloudflare bootstrap coordinates.nodes must be an array");
12463
+ for (const node of coordinateSource.nodes) {
12464
+ exactKeys3(record2(node, "Cloudflare bootstrap node"), [
12465
+ "nodeName",
12466
+ "hostname",
12467
+ "service",
12468
+ "tunnelName",
12469
+ "applicationName",
12470
+ "privateAddress"
12471
+ ], "Cloudflare bootstrap node");
12472
+ }
12473
+ }
12474
+ if (coordinateSource.privateNetwork !== undefined) {
12475
+ exactKeys3(record2(coordinateSource.privateNetwork, "Cloudflare bootstrap privateNetwork"), [
12476
+ "warpOrganization",
12477
+ "virtualNetworkName",
12478
+ "deviceProfileName",
12479
+ "enrollmentApplicationName",
12480
+ "deviceProfilePrecedence"
12481
+ ], "Cloudflare bootstrap privateNetwork");
12482
+ }
12483
+ if (typeof coordinateSource.workerDirectory !== "string" || !coordinateSource.workerDirectory.trim()) {
12484
+ throw new Error("Cloudflare bootstrap coordinates.workerDirectory is required");
12485
+ }
12486
+ const coordinates = validateCloudflareBootstrapCoordinates({
12487
+ ...coordinateSource,
12488
+ workerDirectory: resolve3(baseDirectory, coordinateSource.workerDirectory)
12489
+ });
12490
+ let tokenFiles;
12491
+ if (input.tokenFiles !== undefined) {
12492
+ const source = record2(input.tokenFiles, "Cloudflare bootstrap tokenFiles");
12493
+ const keys = [
12494
+ "apiTokenFile",
12495
+ "tunnelApiTokenFile",
12496
+ "dnsApiTokenFile",
12497
+ "kvApiTokenFile",
12498
+ "accessApiTokenFile",
12499
+ "workerApiTokenFile"
12500
+ ];
12501
+ exactKeys3(source, keys, "Cloudflare bootstrap tokenFiles");
12502
+ for (const key of keys) {
12503
+ if (source[key] !== undefined && (typeof source[key] !== "string" || !source[key].trim())) {
12504
+ throw new Error(`Cloudflare bootstrap tokenFiles.${key} must be a non-empty file path`);
12505
+ }
12506
+ }
12507
+ tokenFiles = Object.fromEntries(keys.flatMap((key) => typeof source[key] === "string" ? [[key, resolve3(baseDirectory, source[key])]] : []));
12508
+ }
12509
+ if (mode === "apply" && (!tokenFiles || !Object.values(tokenFiles).some(Boolean))) {
12510
+ throw new Error("Cloudflare apply config requires tokenFiles with owner-only file paths");
12511
+ }
12512
+ return {
12513
+ mode,
12514
+ coordinates,
12515
+ checkpointPath: resolve3(baseDirectory, input.checkpointPath),
12516
+ ...tokenFiles ? { tokenFiles } : {}
12517
+ };
12518
+ }
12519
+ async function runCloudflareBootstrapCommand(configPath, apply, dependencies = {}) {
12520
+ const request2 = readCloudflareBootstrapCommandConfig(configPath, apply ? "apply" : "plan");
12521
+ const evidence = await (dependencies.run ?? runAttendedCloudflareBootstrap)(request2);
12522
+ (dependencies.write ?? ((text3) => process.stdout.write(text3)))(`${JSON.stringify(evidence, null, 2)}
12523
+ `);
12524
+ return evidence;
12525
+ }
12526
+
12527
+ // src/metal-bootstrap.ts
12528
+ import { createHash as createHash3, randomBytes as randomBytes6 } from "crypto";
12529
+ import {
12530
+ chmodSync as chmodSync3,
12531
+ chownSync,
12532
+ copyFileSync,
12533
+ existsSync as existsSync5,
12534
+ lstatSync as lstatSync3,
12535
+ mkdirSync as mkdirSync6,
12536
+ readFileSync as readFileSync6,
12537
+ realpathSync,
12538
+ renameSync as renameSync4,
12539
+ statSync,
12540
+ symlinkSync,
12541
+ unlinkSync as unlinkSync2,
12542
+ writeFileSync as writeFileSync6
12543
+ } from "fs";
12544
+ import { dirname as dirname7, isAbsolute as isAbsolute2, join as join8, resolve as resolve4 } from "path";
12545
+ import { isIP as isIP5 } from "net";
12546
+
12547
+ // src/metal-isolation.ts
12548
+ import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5 } from "fs";
12549
+ import { join as join7 } from "path";
12550
+
12551
+ // src/metal-provision.ts
12552
+ import { dirname as dirname6, isAbsolute, join as join6 } from "path";
12553
+ import { isIP as isIP4 } from "net";
12554
+
12555
+ // src/ubuntu.ts
12556
+ var SUPPORTED_GUEST_IMAGE = Object.freeze({
12557
+ key: "ubuntu-resolute-20260731",
12558
+ family: "ubuntu-26.04",
12559
+ version: "2026-07-31",
12560
+ label: "Ubuntu 26.04 LTS Resolute",
12561
+ url: "https://cloud-images.ubuntu.com/releases/resolute/release-20260731/ubuntu-26.04-server-cloudimg-amd64.img",
12562
+ sha256: "9dc7c5363c0146a08ba0c9aa834d82c2c6dfbb1c471ad9a2f0aba1189e21be05"
12563
+ });
12564
+
12565
+ // src/metal-provision.ts
12566
+ var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
12567
+ var DEVICE = /^[a-zA-Z][a-zA-Z0-9_.-]{0,14}$/;
12568
+ var IPV4_PREFIX = /^(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;
12569
+ var LINUX_LIST = /^\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*$/;
12570
+
12571
+ class MetalProvisionError extends Error {
12572
+ }
12573
+ function membersOfLinuxList(value, label) {
12574
+ if (!LINUX_LIST.test(value))
12575
+ throw new MetalProvisionError(`invalid ${label} list`);
12576
+ const members = [];
12577
+ for (const part of value.split(",")) {
12578
+ const [startText, endText = startText] = part.split("-");
12579
+ const start = Number(startText);
12580
+ const end = Number(endText);
12581
+ if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start || end > 65535) {
12582
+ throw new MetalProvisionError(`invalid ${label} list`);
12583
+ }
12584
+ for (let value2 = start;value2 <= end; value2 += 1)
12585
+ members.push(value2);
12586
+ }
12587
+ if (new Set(members).size !== members.length)
12588
+ throw new MetalProvisionError(`${label} list overlaps itself`);
12589
+ return members;
12590
+ }
12591
+ function validateMetalProfile(profile) {
12592
+ if (!SAFE_NAME.test(profile.volumeGroup))
12593
+ throw new MetalProvisionError("invalid volume group");
12594
+ if (!DEVICE.test(profile.bridge))
12595
+ throw new MetalProvisionError("invalid bridge");
12596
+ if (!IPV4_PREFIX.test(profile.subnetPrefix))
12597
+ throw new MetalProvisionError("invalid subnet prefix");
12598
+ if (!Number.isInteger(profile.addressStart) || !Number.isInteger(profile.addressEnd) || profile.addressStart < 2 || profile.addressEnd > 254 || profile.addressStart > profile.addressEnd)
12599
+ throw new MetalProvisionError("invalid guest address range");
12600
+ for (const path of [profile.stateDir, profile.seedDir, profile.unitDir]) {
12601
+ if (!isAbsolute(path))
12602
+ throw new MetalProvisionError("metal paths must be absolute");
12603
+ }
12604
+ new URL(profile.apiUrl);
12605
+ let telemetryEndpoint;
12606
+ try {
12607
+ telemetryEndpoint = new URL(profile.agentTelemetryEndpoint);
12608
+ } catch {
12609
+ throw new MetalProvisionError("Agent telemetry endpoint must be an absolute public HTTPS URL");
12610
+ }
12611
+ if (telemetryEndpoint.protocol !== "https:" || telemetryEndpoint.username || telemetryEndpoint.password || telemetryEndpoint.search || telemetryEndpoint.hash || isIP4(telemetryEndpoint.hostname) !== 0 || !telemetryEndpoint.hostname.includes(".") || telemetryEndpoint.hostname === "localhost" || telemetryEndpoint.hostname.endsWith(".local"))
12612
+ throw new MetalProvisionError("Agent telemetry endpoint must be a public HTTPS DNS coordinate without credentials, query or fragment");
12613
+ const imageKeys = Object.keys(profile.images);
12614
+ if (imageKeys.length !== 1 || imageKeys[0] !== SUPPORTED_GUEST_IMAGE.key || profile.images[SUPPORTED_GUEST_IMAGE.key]?.sha256 !== SUPPORTED_GUEST_IMAGE.sha256) {
12615
+ throw new MetalProvisionError(`metal profile must contain only the pinned ${SUPPORTED_GUEST_IMAGE.key} image contract`);
12616
+ }
12617
+ if (!Array.isArray(profile.cpuPools) || profile.cpuPools.length === 0) {
12618
+ throw new MetalProvisionError("at least one exclusive CPU pool is required");
12619
+ }
12620
+ const keys = new Set;
12621
+ const assigned = new Set;
12622
+ const assignedMemory = new Set;
12623
+ let poolsWithMemory = 0;
12624
+ for (const pool of profile.cpuPools) {
12625
+ if (!SAFE_NAME.test(pool.key) || keys.has(pool.key))
12626
+ throw new MetalProvisionError("invalid or duplicate CPU pool key");
12627
+ keys.add(pool.key);
12628
+ const cpus = membersOfLinuxList(pool.cpus, "CPU");
12629
+ if (!Number.isInteger(pool.physicalCores) || pool.physicalCores < 1 || pool.physicalCores > cpus.length) {
12630
+ throw new MetalProvisionError("invalid CPU pool physical-core count");
12631
+ }
12632
+ for (const cpu of cpus) {
12633
+ if (assigned.has(cpu))
12634
+ throw new MetalProvisionError("CPU pools overlap");
12635
+ assigned.add(cpu);
12636
+ }
12637
+ if (pool.memoryNodes) {
12638
+ poolsWithMemory += 1;
12639
+ for (const node of membersOfLinuxList(pool.memoryNodes, "memory-node")) {
12640
+ if (assignedMemory.has(node))
12641
+ throw new MetalProvisionError("guest memory-node pools overlap");
12642
+ assignedMemory.add(node);
12643
+ }
12644
+ }
12645
+ }
12646
+ if (poolsWithMemory !== 0 && poolsWithMemory !== profile.cpuPools.length) {
12647
+ throw new MetalProvisionError("every CPU pool must name memory nodes when NUMA isolation is enabled");
12648
+ }
12649
+ const housekeeping = membersOfLinuxList(profile.housekeepingCpus, "housekeeping CPU");
12650
+ if (housekeeping.some((cpu) => assigned.has(cpu))) {
12651
+ throw new MetalProvisionError("housekeeping CPUs overlap guest CPU pools");
12652
+ }
12653
+ if (profile.housekeepingMemoryNodes) {
12654
+ const housekeepingMemory = membersOfLinuxList(profile.housekeepingMemoryNodes, "housekeeping memory-node");
12655
+ if (housekeepingMemory.some((node) => assignedMemory.has(node))) {
12656
+ throw new MetalProvisionError("housekeeping memory nodes overlap guest memory-node pools");
12657
+ }
12658
+ } else if (assignedMemory.size > 0) {
12659
+ throw new MetalProvisionError("NUMA-isolated guest pools require housekeeping memory nodes");
12660
+ }
12661
+ }
12662
+
12663
+ // src/metal-isolation.ts
12664
+ var members = (list) => list.split(",").flatMap((part) => {
12665
+ const [first, last = first] = part.split("-").map(Number);
12666
+ return Array.from({ length: last - first + 1 }, (_, index) => first + index);
12667
+ });
12668
+ var compact = (values) => {
12669
+ const sorted = [...new Set(values)].sort((left, right) => left - right);
12670
+ const ranges = [];
12671
+ for (let index = 0;index < sorted.length; ) {
12672
+ const first = sorted[index];
12673
+ let last = first;
12674
+ while (sorted[index + 1] === last + 1)
12675
+ last = sorted[++index];
12676
+ ranges.push(first === last ? String(first) : `${first}-${last}`);
12677
+ index += 1;
12678
+ }
12679
+ return ranges.join(",");
12680
+ };
12681
+ var memoryDirective = (nodes) => nodes ? `AllowedMemoryNodes=${nodes}
12682
+ ` : "";
12683
+ function metalGuestSliceUnit(profile) {
12684
+ validateMetalProfile(profile);
12685
+ const cpus = compact(profile.cpuPools.flatMap((pool) => members(pool.cpus)));
12686
+ const nodes = compact(profile.cpuPools.flatMap((pool) => pool.memoryNodes ? members(pool.memoryNodes) : [])) || undefined;
12687
+ return `[Unit]
12688
+ Description=ForgeZero exclusive guest CPU and memory boundary
12689
+
12690
+ [Slice]
12691
+ AllowedCPUs=${cpus}
12692
+ ${memoryDirective(nodes)}`;
12693
+ }
12694
+ function metalHousekeepingDropIn(profile, kind) {
12695
+ validateMetalProfile(profile);
12696
+ return `[${kind === "slice" ? "Slice" : "Scope"}]
12697
+ AllowedCPUs=${profile.housekeepingCpus}
12698
+ ${memoryDirective(profile.housekeepingMemoryNodes)}`;
12699
+ }
12700
+ var defaultExec = async (argv) => {
12701
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe" });
12702
+ const [exitCode, stdout, stderr] = await Promise.all([
12703
+ child.exited,
12704
+ new Response(child.stdout).text(),
12705
+ new Response(child.stderr).text()
12706
+ ]);
12707
+ return { exitCode, stdout, stderr };
12708
+ };
12709
+ var checked2 = async (exec, argv) => {
12710
+ const result = await exec(argv);
12711
+ if (result.exitCode !== 0)
12712
+ throw new Error(`${argv[0]} failed: ${(result.stderr || result.stdout).trim()}`);
12713
+ return result;
12714
+ };
12715
+ var requireGuestsInSlice = async (exec) => {
12716
+ const active = await checked2(exec, [
12717
+ "systemctl",
12718
+ "list-units",
12719
+ "--type=service",
12720
+ "--state=running",
12721
+ "--plain",
12722
+ "--no-legend",
12723
+ "forgezero-guest@*.service"
12724
+ ]);
12725
+ for (const line of active.stdout.split(`
12726
+ `)) {
12727
+ const service = line.trim().split(/\s+/)[0];
12728
+ if (!service)
12729
+ continue;
12730
+ const cgroup = await checked2(exec, ["systemctl", "show", "-p", "ControlGroup", "--value", service]);
12731
+ if (!cgroup.stdout.trim().includes("/forgezero-guests.slice/")) {
12732
+ throw new Error(`${service} must be drained and restarted into forgezero-guests.slice`);
12733
+ }
12734
+ }
12735
+ };
12736
+ async function applyMetalIsolation(profile, exec = defaultExec) {
12737
+ validateMetalProfile(profile);
12738
+ await requireGuestsInSlice(exec);
12739
+ const unitDir = profile.unitDir;
12740
+ mkdirSync5(unitDir, { recursive: true });
12741
+ writeFileSync5(join7(unitDir, "forgezero-guests.slice"), metalGuestSliceUnit(profile), { mode: 420 });
12742
+ for (const unit of ["system.slice", "user.slice"]) {
12743
+ const directory = join7(unitDir, `${unit}.d`);
12744
+ mkdirSync5(directory, { recursive: true });
12745
+ writeFileSync5(join7(directory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "slice"), { mode: 420 });
12746
+ }
12747
+ const initDirectory = join7(unitDir, "init.scope.d");
12748
+ mkdirSync5(initDirectory, { recursive: true });
12749
+ writeFileSync5(join7(initDirectory, "50-forgezero-housekeeping.conf"), metalHousekeepingDropIn(profile, "scope"), { mode: 420 });
12750
+ await checked2(exec, ["systemctl", "daemon-reload"]);
12751
+ await requireGuestsInSlice(exec);
12752
+ const properties = [`AllowedCPUs=${profile.housekeepingCpus}`];
12753
+ if (profile.housekeepingMemoryNodes)
12754
+ properties.push(`AllowedMemoryNodes=${profile.housekeepingMemoryNodes}`);
12755
+ for (const unit of ["system.slice", "user.slice", "init.scope"]) {
12756
+ await checked2(exec, ["systemctl", "set-property", "--runtime", unit, ...properties]);
12757
+ }
12758
+ }
12759
+
12760
+ // src/metal-bootstrap.ts
12761
+ var PROFILE_PATH = "/etc/forgezero/metal.json";
12762
+ var STATE_PATH2 = "/etc/forgezero/metal.initialized.json";
12763
+ var SEED_CREDENTIAL_PATH = "/etc/forgezero/creds/metal-agent-seed.cred";
12764
+ var UNIT_DIRECTORY = "/etc/systemd/system";
12765
+ var AGENT_PATH = "/usr/local/bin/fz-agent";
12766
+ var HELPER_SOCKET = "/run/forgezero-metal/helper.sock";
12767
+ var UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
12768
+ var MAX_CONFIG_BYTES = 256 * 1024;
12769
+ var SUPPORTED_BUN_VERSION = "1.3.14";
12770
+ var SUPPORTED_BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
12771
+
12772
+ class MetalBootstrapError extends Error {
12773
+ }
12774
+ var defaultExec2 = async (argv, stdin) => {
12775
+ const child = Bun.spawn([...argv], {
12776
+ stdin: stdin === undefined ? undefined : new Blob([stdin]),
12777
+ stdout: "pipe",
12778
+ stderr: "pipe",
12779
+ env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" }
12780
+ });
12781
+ const [exitCode, stdout, stderr] = await Promise.all([
12782
+ child.exited,
12783
+ new Response(child.stdout).text(),
12784
+ new Response(child.stderr).text()
12785
+ ]);
12786
+ return { exitCode, stdout, stderr };
12787
+ };
12788
+ var runChecked = async (exec, argv, stdin) => {
12789
+ const result = await exec(argv, stdin);
12790
+ if (result.exitCode !== 0) {
12791
+ throw new MetalBootstrapError(`${argv.join(" ")} failed: ${(result.stderr || result.stdout).trim() || `exit ${result.exitCode}`}`);
12792
+ }
12793
+ return result;
12794
+ };
12795
+ var exactKeys4 = (value, allowed, label) => {
12796
+ const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
12797
+ if (unknown.length)
12798
+ throw new MetalBootstrapError(`${label} contains unknown fields: ${unknown.join(", ")}`);
12799
+ };
12800
+ var validUnit = (unit) => {
12801
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit)) {
12802
+ throw new MetalBootstrapError("hostTelemetryUnit must be a systemd .service unit");
12803
+ }
12804
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-metal-helper|forgezero-db)\.service$/.test(unit)) {
12805
+ throw new MetalBootstrapError("the OTLP collector must be independently supervised");
12806
+ }
12807
+ };
12808
+ function validateMetalBootstrapConfig(config) {
12809
+ if (!config || typeof config !== "object")
12810
+ throw new MetalBootstrapError("metal bootstrap config must be an object");
12811
+ exactKeys4(config, ["kind", "metalHostname", "profile", "hostTelemetryEndpoint", "hostTelemetryUnit", "agentSeedFile"], "metal bootstrap config");
12812
+ if (config.kind !== "metal")
12813
+ throw new MetalBootstrapError("metal bootstrap config kind must be metal");
12814
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(config.metalHostname)) {
12815
+ throw new MetalBootstrapError("invalid metal inventory hostname");
12816
+ }
12817
+ if (!config.profile || typeof config.profile !== "object")
12818
+ throw new MetalBootstrapError("metal profile must be an object");
12819
+ exactKeys4(config.profile, [
12820
+ "volumeGroup",
12821
+ "bridge",
12822
+ "subnetPrefix",
12823
+ "addressStart",
12824
+ "addressEnd",
12825
+ "gateway",
12826
+ "nameservers",
12827
+ "stateDir",
12828
+ "seedDir",
12829
+ "unitDir",
12830
+ "apiUrl",
12831
+ "agentTelemetryEndpoint",
12832
+ "images",
12833
+ "cpuPools",
12834
+ "housekeepingCpus",
12835
+ "housekeepingMemoryNodes",
12836
+ "bunVersion",
12837
+ "bunInstallerSha256",
12838
+ "agentVersion",
12839
+ "confidential"
12840
+ ], "metal profile");
12841
+ if (Array.isArray(config.profile.cpuPools)) {
12842
+ for (const pool of config.profile.cpuPools)
12843
+ exactKeys4(pool, ["key", "cpus", "physicalCores", "memoryNodes"], "metal CPU pool");
12844
+ }
12845
+ validateMetalProfile(config.profile);
12846
+ if (config.profile.stateDir !== "/etc/forgezero/metal-guests" || config.profile.seedDir !== "/var/lib/forgezero/seed" || config.profile.unitDir !== UNIT_DIRECTORY || config.profile.legacyStateDir !== undefined) {
12847
+ throw new MetalBootstrapError("metal bootstrap uses fixed state, seed, and systemd unit directories");
12848
+ }
12849
+ const image = config.profile.images[Object.keys(config.profile.images)[0]];
12850
+ if (!image.path.startsWith("/var/lib/forgezero/images/") || resolve4(image.path) !== image.path || /[\0\r\n]/.test(image.path)) {
12851
+ throw new MetalBootstrapError("the pinned guest image must use the fixed image directory");
12852
+ }
12853
+ if (config.profile.bunVersion !== SUPPORTED_BUN_VERSION || config.profile.bunInstallerSha256 !== SUPPORTED_BUN_INSTALLER_SHA256 || config.profile.agentVersion !== VERSION2) {
12854
+ throw new MetalBootstrapError("metal guest runtime must use the package-owned Bun and Agent release coordinates");
12855
+ }
12856
+ let api;
12857
+ try {
12858
+ api = new URL(config.profile.apiUrl);
12859
+ } catch {
12860
+ throw new MetalBootstrapError("metal API must be a public HTTPS origin");
12861
+ }
12862
+ if (api.protocol !== "https:" || api.username || api.password || api.pathname !== "/" || api.search || api.hash || isIP5(api.hostname) !== 0 || !api.hostname.includes(".") || api.hostname.endsWith(".local")) {
12863
+ throw new MetalBootstrapError("metal API must be a credential-free public HTTPS origin");
12864
+ }
12865
+ if (config.profile.nameservers?.some((address) => isIP5(address) === 0)) {
12866
+ throw new MetalBootstrapError("metal nameservers must be literal IP addresses");
12867
+ }
12868
+ if (isIP5(`${config.profile.subnetPrefix}.1`) !== 4 || isIP5(config.profile.gateway) !== 4 || !config.profile.gateway.startsWith(`${config.profile.subnetPrefix}.`)) {
12869
+ throw new MetalBootstrapError("metal gateway must be an IPv4 address in the reviewed subnet");
12870
+ }
12871
+ if (config.profile.confidential && (!Number.isSafeInteger(config.profile.confidential.cbitpos) || config.profile.confidential.cbitpos < 1 || config.profile.confidential.cbitpos > 63 || !Number.isSafeInteger(config.profile.confidential.reducedPhysBits) || config.profile.confidential.reducedPhysBits < 0 || config.profile.confidential.reducedPhysBits > 63 || !/^0x[0-9a-fA-F]{1,16}$/.test(config.profile.confidential.policy)))
12872
+ throw new MetalBootstrapError("invalid confidential-compute coordinate");
12873
+ if (config.hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
12874
+ throw new MetalBootstrapError("metal host OTLP must use exact loopback http://127.0.0.1:4318");
12875
+ }
12876
+ validUnit(config.hostTelemetryUnit);
12877
+ if (config.agentSeedFile)
12878
+ validateOwnerOnlyPath(config.agentSeedFile, false);
12879
+ return config;
12880
+ }
12881
+ function validateOwnerOnlyPath(path, requireRootOwner) {
12882
+ if (!isAbsolute2(path) || resolve4(path) !== path || path.includes("/../")) {
12883
+ throw new MetalBootstrapError("private bootstrap paths must be canonical absolute paths");
12884
+ }
12885
+ const metadata = lstatSync3(path);
12886
+ if (!metadata.isFile() || metadata.isSymbolicLink() || realpathSync(path) !== path) {
12887
+ throw new MetalBootstrapError("private bootstrap path must be a regular non-symlink file");
12888
+ }
12889
+ if ((metadata.mode & 63) !== 0)
12890
+ throw new MetalBootstrapError("private bootstrap file must be owner-only");
12891
+ const caller = typeof process.getuid === "function" ? process.getuid() : -1;
12892
+ if (requireRootOwner && metadata.uid !== 0 || !requireRootOwner && metadata.uid !== 0 && metadata.uid !== caller) {
12893
+ throw new MetalBootstrapError(requireRootOwner ? "private bootstrap file must be root-owned" : "private bootstrap file has an unexpected owner");
12894
+ }
12895
+ }
12896
+ function readMetalBootstrapConfig(path) {
12897
+ validateOwnerOnlyPath(path, false);
12898
+ const metadata = statSync(path);
12899
+ if (metadata.size < 2 || metadata.size > MAX_CONFIG_BYTES)
12900
+ throw new MetalBootstrapError("metal bootstrap config size is invalid");
12901
+ let parsed;
12902
+ try {
12903
+ parsed = JSON.parse(readFileSync6(path, "utf8"));
12904
+ } catch {
12905
+ throw new MetalBootstrapError("metal bootstrap config is not valid JSON");
12906
+ }
12907
+ return validateMetalBootstrapConfig(parsed);
12908
+ }
12909
+ function planMetalBootstrap(config) {
12910
+ validateMetalBootstrapConfig(config);
12911
+ return {
12912
+ kind: "metal",
12913
+ metalHostname: config.metalHostname,
12914
+ profilePath: PROFILE_PATH,
12915
+ units: [
12916
+ "forgezero-metal-helper.service",
12917
+ "forgezero-agent-update-helper.service",
12918
+ "forgezero-metal-agent-egress.service",
12919
+ "forgezero-metal-agent.service"
12920
+ ],
12921
+ steps: [
12922
+ "install fixed KVM/LVM/cloud-init/nftables prerequisites",
12923
+ "verify bridge, volume group, pinned guest image, KVM and optional SEV device",
12924
+ "install the published Agent binary and root-owned immutable metal profile",
12925
+ "seal or generate the metal identity seed as a systemd credential",
12926
+ "apply reviewed CPU/NUMA host and guest isolation",
12927
+ "install helper, update, egress and identity-only Agent units",
12928
+ "enable units and prove services, local sockets, egress policy and OTLP collector"
12929
+ ],
12930
+ requiresRoot: true,
12931
+ consumes: config.agentSeedFile ? [config.agentSeedFile] : []
12932
+ };
12933
+ }
12934
+ var atomicWrite2 = (path, body, mode) => {
12935
+ mkdirSync6(dirname7(path), { recursive: true, mode: 493 });
12936
+ const temporary = `${path}.next-${process.pid}`;
12937
+ writeFileSync6(temporary, body, { mode, flag: "wx" });
12938
+ chmodSync3(temporary, mode);
12939
+ chownSync(temporary, 0, 0);
12940
+ renameSync4(temporary, path);
12941
+ };
12942
+ var validateAgentSourcePath = (source) => {
12943
+ if (!isAbsolute2(source) || !lstatSync3(source).isFile() || lstatSync3(source).isSymbolicLink()) {
12944
+ throw new MetalBootstrapError("published Agent source path must be an absolute regular non-symlink file");
12945
+ }
12946
+ };
12947
+ var ensureAccount = async (exec) => {
12948
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-metal"])).exitCode !== 0) {
12949
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-metal"]);
12950
+ }
12951
+ if ((await exec(["/usr/bin/getent", "group", "forgezero-update"])).exitCode !== 0) {
12952
+ await runChecked(exec, ["/usr/sbin/groupadd", "--system", "forgezero-update"]);
12953
+ }
12954
+ if ((await exec(["/usr/bin/id", "forgezero-metal"])).exitCode !== 0) {
12955
+ await runChecked(exec, [
12956
+ "/usr/sbin/useradd",
12957
+ "--system",
12958
+ "--no-create-home",
12959
+ "--shell",
12960
+ "/usr/sbin/nologin",
12961
+ "--gid",
12962
+ "forgezero-metal",
12963
+ "forgezero-metal"
12964
+ ]);
12965
+ }
12966
+ await runChecked(exec, ["/usr/sbin/usermod", "-a", "-G", "forgezero-update", "forgezero-metal"]);
12967
+ };
12968
+ function renderMetalUnits(config) {
12969
+ validateMetalBootstrapConfig(config);
12970
+ const egress = systemdAgentEgressDirectives([4318]);
12971
+ const updateEgress = systemdAgentEgressDirectives();
12972
+ return {
12973
+ "forgezero-metal-helper.service": `[Unit]
12974
+ Description=ForgeZero constrained physical-host helper
12975
+ After=local-fs.target
12976
+
12977
+ [Service]
12978
+ Type=simple
12979
+ User=root
12980
+ Group=forgezero-metal
12981
+ UMask=0007
12982
+ RuntimeDirectory=forgezero-metal
12983
+ RuntimeDirectoryMode=0770
12984
+ ExecStart=${AGENT_PATH} metal-helper --profile=${PROFILE_PATH}
12985
+ Restart=on-failure
12986
+ RestartSec=5
12987
+ TimeoutStopSec=10min
12988
+ LimitCORE=0
12989
+ PrivateTmp=true
12990
+ ProtectHome=true
12991
+ RestrictAddressFamilies=AF_UNIX
12992
+
12993
+ [Install]
12994
+ WantedBy=multi-user.target
12995
+ `,
12996
+ "forgezero-agent-update-helper.service": `[Unit]
12997
+ Description=ForgeZero verified Agent update helper
12998
+ After=network-online.target
12999
+ Wants=network-online.target
13000
+
13001
+ [Service]
13002
+ Type=simple
13003
+ User=root
13004
+ Group=forgezero-update
13005
+ Environment=FZ_AGENT_UPDATE_SOCKET=${UPDATE_SOCKET}
13006
+ ExecStart=${AGENT_PATH} update-helper
13007
+ Restart=always
13008
+ RestartSec=2
13009
+ RuntimeDirectory=forgezero-update
13010
+ RuntimeDirectoryMode=0750
13011
+ UMask=0007
13012
+ LimitCORE=0
13013
+ NoNewPrivileges=true
13014
+ PrivateTmp=true
13015
+ ProtectSystem=strict
13016
+ ProtectHome=true
13017
+ ProtectKernelTunables=true
13018
+ ProtectKernelModules=true
13019
+ ProtectControlGroups=true
13020
+ RestrictSUIDSGID=true
13021
+ RestrictRealtime=true
13022
+ LockPersonality=true
13023
+ ReadWritePaths=/opt/forgezero/agent /var/lib/forgezero
13024
+ ${updateEgress}
13025
+
13026
+ [Install]
13027
+ WantedBy=multi-user.target
13028
+ `,
13029
+ "forgezero-metal-agent-egress.service": `[Unit]
13030
+ Description=ForgeZero physical Agent host egress policy
13031
+ After=systemd-resolved.service nftables.service
13032
+ Requires=systemd-resolved.service
13033
+ Before=forgezero-metal-agent.service
13034
+
13035
+ [Service]
13036
+ Type=notify
13037
+ NotifyAccess=all
13038
+ User=root
13039
+ Group=root
13040
+ ExecStart=${AGENT_PATH} egress-policy --user=forgezero-metal --loopback-user=forgezero-metal --loopback-tcp-port=4318 --public-tcp-port=443
13041
+ Restart=on-failure
13042
+ RestartSec=2
13043
+ LimitCORE=0
13044
+ NoNewPrivileges=true
13045
+ PrivateTmp=true
13046
+ ProtectSystem=strict
13047
+ ProtectHome=true
13048
+ ProtectKernelTunables=true
13049
+ ProtectKernelModules=true
13050
+ ProtectControlGroups=true
13051
+ RestrictSUIDSGID=true
13052
+ RestrictRealtime=true
13053
+ MemoryDenyWriteExecute=true
13054
+ LockPersonality=true
13055
+ CapabilityBoundingSet=CAP_NET_ADMIN
13056
+ RestrictAddressFamilies=AF_UNIX AF_NETLINK
13057
+
13058
+ [Install]
13059
+ WantedBy=multi-user.target
13060
+ `,
13061
+ "forgezero-metal-agent.service": `[Unit]
13062
+ Description=ForgeZero identity-only physical-host Agent
13063
+ After=network-online.target ${config.hostTelemetryUnit} forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
13064
+ Wants=network-online.target ${config.hostTelemetryUnit}
13065
+ Requires=forgezero-metal-helper.service forgezero-agent-update-helper.service forgezero-metal-agent-egress.service
13066
+ BindsTo=forgezero-metal-agent-egress.service
13067
+
13068
+ [Service]
13069
+ Type=simple
13070
+ User=forgezero-metal
13071
+ Group=forgezero-metal
13072
+ SupplementaryGroups=forgezero-update
13073
+ LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}
13074
+ Environment=FZ_SEED_CREDENTIAL=metal-agent-seed
13075
+ Environment=FZ_AGENT_ROLE=metal
13076
+ Environment=FZ_METAL_HOSTNAME=${config.metalHostname}
13077
+ Environment=FZ_API=${config.profile.apiUrl}
13078
+ Environment=FZ_METAL_HELPER_SOCKET=${HELPER_SOCKET}
13079
+ Environment=FZ_DRAIN_DEADLINE_MS=120000
13080
+ Environment=NODE_ENV=production
13081
+ Environment=OTEL_EXPORTER_OTLP_ENDPOINT=${config.hostTelemetryEndpoint}
13082
+ Environment=OTEL_SERVICE_NAME=forgezero-metal-agent
13083
+ ExecStart=${AGENT_PATH}
13084
+ Restart=on-failure
13085
+ RestartSec=5
13086
+ TimeoutStopSec=130s
13087
+ LimitCORE=0
13088
+ NoNewPrivileges=true
13089
+ PrivateTmp=true
13090
+ ProtectSystem=strict
13091
+ ProtectHome=true
13092
+ ProtectKernelTunables=true
13093
+ ProtectKernelModules=true
13094
+ ProtectControlGroups=true
13095
+ RestrictSUIDSGID=true
13096
+ LockPersonality=true
13097
+ ${egress}
13098
+
13099
+ [Install]
13100
+ WantedBy=multi-user.target
13101
+ `
13102
+ };
13103
+ }
13104
+ var installAgentBinary = (source, version) => {
13105
+ validateAgentSourcePath(source);
13106
+ const release = `/opt/forgezero/agent/versions/${version}/dist`;
13107
+ mkdirSync6(release, { recursive: true, mode: 493 });
13108
+ copyFileSync(source, join8(release, "fz-agent.js"));
13109
+ chmodSync3(join8(release, "fz-agent.js"), 493);
13110
+ chownSync(join8(release, "fz-agent.js"), 0, 0);
13111
+ mkdirSync6("/opt/forgezero/agent", { recursive: true, mode: 493 });
13112
+ for (const [link, target] of [
13113
+ ["/opt/forgezero/agent/current.next", `versions/${version}`],
13114
+ [AGENT_PATH, "/opt/forgezero/agent/current/dist/fz-agent.js"]
13115
+ ]) {
13116
+ try {
13117
+ unlinkSync2(link);
13118
+ } catch {}
13119
+ symlinkSync(target, link);
13120
+ if (link.endsWith("current.next"))
13121
+ renameSync4(link, "/opt/forgezero/agent/current");
13122
+ }
13123
+ };
13124
+ var preflight = async (config, exec) => {
13125
+ await runChecked(exec, ["/usr/bin/systemctl", "is-active", "--quiet", config.hostTelemetryUnit]);
13126
+ await runChecked(exec, [
13127
+ "/usr/bin/curl",
13128
+ "--silent",
13129
+ "--show-error",
13130
+ "--fail",
13131
+ "--max-time",
13132
+ "5",
13133
+ "--request",
13134
+ "POST",
13135
+ "--header",
13136
+ "Content-Type: application/json",
13137
+ "--data-binary",
13138
+ "{}",
13139
+ `${config.hostTelemetryEndpoint}/v1/metrics`
13140
+ ]);
13141
+ await runChecked(exec, ["/usr/sbin/vgs", config.profile.volumeGroup]);
13142
+ await runChecked(exec, ["/usr/sbin/ip", "link", "show", config.profile.bridge]);
13143
+ if (!existsSync5("/dev/kvm"))
13144
+ throw new MetalBootstrapError("/dev/kvm is required");
13145
+ if (config.profile.confidential) {
13146
+ if (!existsSync5("/dev/sev"))
13147
+ throw new MetalBootstrapError("/dev/sev is required by the confidential profile");
13148
+ await runChecked(exec, ["/usr/bin/qemu-system-x86_64", "-object", "sev-snp-guest,help"]);
13149
+ }
13150
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", config.profile.images[Object.keys(config.profile.images)[0]].path])).stdout.split(/\s+/)[0];
13151
+ if (digest !== config.profile.images[Object.keys(config.profile.images)[0]].sha256)
13152
+ throw new MetalBootstrapError("pinned guest image digest mismatch");
13153
+ };
13154
+ var assertSupportedMetalHost = () => {
13155
+ if (process.platform !== "linux" || process.arch !== "x64") {
13156
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
13157
+ }
13158
+ const release = readFileSync6("/etc/os-release", "utf8");
13159
+ if (!/^ID=ubuntu$/m.test(release) || !/^VERSION_ID="?26\.04"?$/m.test(release)) {
13160
+ throw new MetalBootstrapError("metal bootstrap supports only Ubuntu 26.04 x86_64 hosts");
13161
+ }
13162
+ };
13163
+ var ensurePinnedGuestImage = async (config, exec) => {
13164
+ const image = config.profile.images[SUPPORTED_GUEST_IMAGE.key];
13165
+ if (existsSync5(image.path))
13166
+ return;
13167
+ mkdirSync6(dirname7(image.path), { recursive: true, mode: 493 });
13168
+ const temporary = `${image.path}.next-${process.pid}`;
13169
+ try {
13170
+ await runChecked(exec, [
13171
+ "/usr/bin/curl",
13172
+ "--fail",
13173
+ "--location",
13174
+ "--proto",
13175
+ "=https",
13176
+ "--tlsv1.2",
13177
+ "--output",
13178
+ temporary,
13179
+ SUPPORTED_GUEST_IMAGE.url
13180
+ ]);
13181
+ const digest = (await runChecked(exec, ["/usr/bin/sha256sum", temporary])).stdout.split(/\s+/)[0];
13182
+ if (digest !== image.sha256)
13183
+ throw new MetalBootstrapError("downloaded guest image digest mismatch");
13184
+ chmodSync3(temporary, 292);
13185
+ chownSync(temporary, 0, 0);
13186
+ renameSync4(temporary, image.path);
13187
+ } catch (cause) {
13188
+ try {
13189
+ unlinkSync2(temporary);
13190
+ } catch {}
13191
+ throw cause;
13192
+ }
13193
+ };
13194
+ async function applyMetalBootstrap(config, options) {
13195
+ validateMetalBootstrapConfig(config);
13196
+ if ((options.getuid ?? process.getuid)?.() !== 0)
13197
+ throw new MetalBootstrapError("fz bootstrap metal --apply must run as root");
13198
+ assertSupportedMetalHost();
13199
+ if (existsSync5(STATE_PATH2) && !options.repair)
13200
+ throw new MetalBootstrapError("metal host is already initialized; use explicit repair");
13201
+ if (config.agentSeedFile)
13202
+ validateOwnerOnlyPath(config.agentSeedFile, true);
13203
+ if (config.agentSeedFile && existsSync5(SEED_CREDENTIAL_PATH)) {
13204
+ throw new MetalBootstrapError("repair refuses replacement seed material while the sealed metal identity exists");
13205
+ }
13206
+ validateAgentSourcePath(options.agentSourcePath);
13207
+ const exec = options.exec ?? defaultExec2;
13208
+ await runChecked(exec, ["/usr/bin/apt-get", "update"]);
13209
+ await runChecked(exec, [
13210
+ "/usr/bin/apt-get",
13211
+ "install",
13212
+ "-y",
13213
+ "--no-install-recommends",
13214
+ "qemu-system-x86",
13215
+ "qemu-utils",
13216
+ "cloud-image-utils",
13217
+ "lvm2",
13218
+ "nftables",
13219
+ "curl"
13220
+ ]);
13221
+ await ensurePinnedGuestImage(config, exec);
13222
+ await preflight(config, exec);
13223
+ await ensureAccount(exec);
13224
+ installAgentBinary(options.agentSourcePath, config.profile.agentVersion);
13225
+ mkdirSync6("/etc/forgezero/creds", { recursive: true, mode: 448 });
13226
+ mkdirSync6(config.profile.stateDir, { recursive: true, mode: 448 });
13227
+ mkdirSync6(config.profile.seedDir, { recursive: true, mode: 448 });
13228
+ const persistedProfile = {
13229
+ ...config.profile,
13230
+ metalHostname: config.metalHostname,
13231
+ hostTelemetryEndpoint: config.hostTelemetryEndpoint,
13232
+ hostTelemetryUnit: config.hostTelemetryUnit
13233
+ };
13234
+ atomicWrite2(PROFILE_PATH, `${JSON.stringify(persistedProfile, null, 2)}
13235
+ `, 384);
13236
+ if (!existsSync5(SEED_CREDENTIAL_PATH)) {
13237
+ const seed = config.agentSeedFile ? readFileSync6(config.agentSeedFile, "utf8").trim() : randomBytes6(32).toString("base64url");
13238
+ if (seed.length < 32 || /[\0\r\n]/.test(seed))
13239
+ throw new MetalBootstrapError("metal Agent seed is invalid");
13240
+ await runChecked(exec, ["/usr/bin/systemd-creds", "encrypt", "--name=metal-agent-seed", "-", SEED_CREDENTIAL_PATH], `${seed}
13241
+ `);
13242
+ chmodSync3(SEED_CREDENTIAL_PATH, 256);
13243
+ chownSync(SEED_CREDENTIAL_PATH, 0, 0);
13244
+ if (config.agentSeedFile)
13245
+ unlinkSync2(config.agentSeedFile);
13246
+ }
13247
+ await applyMetalIsolation(config.profile, (argv) => exec(argv));
13248
+ for (const [unit, body] of Object.entries(renderMetalUnits(config)))
13249
+ atomicWrite2(join8(UNIT_DIRECTORY, unit), body, 420);
13250
+ await runChecked(exec, ["/usr/bin/systemctl", "daemon-reload"]);
13251
+ await runChecked(exec, [
13252
+ "/usr/bin/systemctl",
13253
+ "enable",
13254
+ "--now",
13255
+ "forgezero-agent-update-helper.service",
13256
+ "forgezero-metal-helper.service",
13257
+ "forgezero-metal-agent-egress.service",
13258
+ "forgezero-metal-agent.service"
13259
+ ]);
13260
+ for (let attempt = 0;attempt < 100 && (!socketReady(HELPER_SOCKET) || !socketReady(UPDATE_SOCKET)); attempt += 1) {
13261
+ await Bun.sleep(100);
13262
+ }
13263
+ const status = await metalBootstrapStatus(exec);
13264
+ const operationalProblems = options.repair ? status.problems.filter((problem) => problem !== "metal initialized state does not bind the current profile") : status.problems;
13265
+ if (operationalProblems.length)
13266
+ throw new MetalBootstrapError(`metal bootstrap verification failed: ${operationalProblems.join("; ")}`);
13267
+ const identity = await runChecked(exec, [
13268
+ "/usr/bin/systemd-run",
13269
+ "--pipe",
13270
+ "--wait",
13271
+ "--quiet",
13272
+ "--collect",
13273
+ `--property=LoadCredentialEncrypted=metal-agent-seed:${SEED_CREDENTIAL_PATH}`,
13274
+ "--setenv=FZ_SEED_CREDENTIAL=metal-agent-seed",
13275
+ AGENT_PATH,
13276
+ "identity"
13277
+ ]);
13278
+ const publicIdentity = identity.stdout.trim();
13279
+ if (!publicIdentity || /[\r\n]/.test(publicIdentity))
13280
+ throw new MetalBootstrapError("metal public identity proof was invalid");
13281
+ const state = {
13282
+ initializedAt: new Date().toISOString(),
13283
+ role: "metal",
13284
+ metalHostname: config.metalHostname,
13285
+ profileSha256: createHash3("sha256").update(JSON.stringify(config.profile)).digest("hex")
13286
+ };
13287
+ atomicWrite2(STATE_PATH2, `${JSON.stringify(state, null, 2)}
13288
+ `, 384);
13289
+ return { ...status, initialized: true, problems: [], metalHostname: config.metalHostname, publicIdentity };
13290
+ }
13291
+ var socketReady = (path) => {
13292
+ try {
13293
+ return lstatSync3(path).isSocket();
13294
+ } catch {
13295
+ return false;
13296
+ }
13297
+ };
13298
+ async function metalBootstrapStatus(exec = defaultExec2) {
13299
+ const problems = [];
13300
+ let profileValid = false, imageVerified = false, metalHostname, profileSha256;
13301
+ let profileMode = null;
13302
+ if (existsSync5(PROFILE_PATH)) {
13303
+ try {
13304
+ const metadata = statSync(PROFILE_PATH);
13305
+ profileMode = metadata.mode & 511;
13306
+ if (profileMode !== 384 || metadata.uid !== 0)
13307
+ problems.push("metal profile is not root-owned mode 0600");
13308
+ const persisted = JSON.parse(readFileSync6(PROFILE_PATH, "utf8"));
13309
+ const { metalHostname: profileHostname, hostTelemetryEndpoint, hostTelemetryUnit, ...profile } = persisted;
13310
+ validateMetalProfile(profile);
13311
+ profileValid = true;
13312
+ profileSha256 = createHash3("sha256").update(JSON.stringify(profile)).digest("hex");
13313
+ if (!/^[A-Za-z0-9][A-Za-z0-9.-]{1,252}$/.test(profileHostname) || hostTelemetryEndpoint !== "http://127.0.0.1:4318") {
13314
+ problems.push("persisted metal host coordinates are invalid");
13315
+ }
13316
+ try {
13317
+ validUnit(hostTelemetryUnit);
13318
+ } catch {
13319
+ problems.push("persisted metal OTLP collector unit is invalid");
13320
+ }
13321
+ if ((await exec(["/usr/bin/systemctl", "is-active", "--quiet", hostTelemetryUnit])).exitCode !== 0) {
13322
+ problems.push(`${hostTelemetryUnit} is inactive`);
13323
+ } else if ((await exec([
13324
+ "/usr/bin/curl",
13325
+ "--silent",
13326
+ "--show-error",
13327
+ "--fail",
13328
+ "--max-time",
13329
+ "5",
13330
+ "--request",
13331
+ "POST",
13332
+ "--header",
13333
+ "Content-Type: application/json",
13334
+ "--data-binary",
13335
+ "{}",
13336
+ `${hostTelemetryEndpoint}/v1/metrics`
13337
+ ])).exitCode !== 0) {
13338
+ problems.push("local OTLP metrics receiver did not accept a proof request");
13339
+ }
13340
+ const image = profile.images[Object.keys(profile.images)[0]];
13341
+ if (existsSync5(image.path)) {
13342
+ const digest = (await exec(["/usr/bin/sha256sum", image.path])).stdout.split(/\s+/)[0];
13343
+ imageVerified = digest === image.sha256;
13344
+ }
13345
+ if (!imageVerified)
13346
+ problems.push("pinned guest image is absent or has the wrong digest");
13347
+ } catch (cause) {
13348
+ problems.push(`metal profile invalid: ${cause instanceof Error ? cause.message : String(cause)}`);
13349
+ }
13350
+ } else
13351
+ problems.push("metal profile is missing");
13352
+ if (existsSync5(STATE_PATH2)) {
13353
+ try {
13354
+ const state = JSON.parse(readFileSync6(STATE_PATH2, "utf8"));
13355
+ metalHostname = state.metalHostname;
13356
+ if (state.role !== "metal" || !metalHostname || state.profileSha256 !== profileSha256) {
13357
+ problems.push("metal initialized state does not bind the current profile");
13358
+ }
13359
+ } catch {
13360
+ problems.push("metal initialized state is invalid");
13361
+ }
13362
+ }
13363
+ const units = {};
13364
+ for (const unit of [
13365
+ "forgezero-metal-helper.service",
13366
+ "forgezero-agent-update-helper.service",
13367
+ "forgezero-metal-agent-egress.service",
13368
+ "forgezero-metal-agent.service"
13369
+ ]) {
13370
+ if (!existsSync5(join8(UNIT_DIRECTORY, unit)))
13371
+ units[unit] = "missing";
13372
+ else
13373
+ units[unit] = (await exec(["/usr/bin/systemctl", "is-active", "--quiet", unit])).exitCode === 0 ? "active" : "inactive";
13374
+ if (units[unit] !== "active")
13375
+ problems.push(`${unit} is ${units[unit]}`);
13376
+ }
13377
+ const helperSocketReady = socketReady(HELPER_SOCKET);
13378
+ const updateSocketReady = socketReady(UPDATE_SOCKET);
13379
+ if (!helperSocketReady)
13380
+ problems.push("metal helper socket is not ready");
13381
+ if (!updateSocketReady)
13382
+ problems.push("Agent update helper socket is not ready");
13383
+ return {
13384
+ initialized: existsSync5(STATE_PATH2),
13385
+ profileValid,
13386
+ profileMode,
13387
+ imageVerified,
13388
+ units,
13389
+ helperSocketReady,
13390
+ updateSocketReady,
13391
+ metalHostname,
13392
+ problems
13393
+ };
13394
+ }
13395
+
13396
+ // src/cli/index.ts
13397
+ var RECOMMENDED_MODE = (THRESHOLD_MODES.find((mode) => mode.recommended) ?? THRESHOLD_MODES[0]).id;
13398
+ var DEFAULT_MODE = RECOMMENDED_MODE;
13399
+ var PACKAGED_AGENT_BIN2 = fileURLToPath2(new URL("./fz-agent.js", import.meta.url));
13400
+ function parseOptions(argv) {
13401
+ const options = {
13402
+ api: process.env.FZ_API ?? "http://localhost:8787",
13403
+ apiExplicit: Boolean(process.env.FZ_API),
13404
+ app: process.env.FZ_APP,
13405
+ realm: process.env.FZ_REALM ?? "platform",
13406
+ realmExplicit: Boolean(process.env.FZ_REALM),
13407
+ json: false,
13408
+ apply: false,
13409
+ enrol: false,
13410
+ socket: process.env.SSH_AUTH_SOCK,
13411
+ mode: DEFAULT_MODE,
13412
+ user: process.env.FZ_USER ?? "operator",
13413
+ userExplicit: Boolean(process.env.FZ_USER),
13414
+ phraseStdin: false,
13415
+ preserveEnv: false,
13416
+ projectRoot: process.cwd(),
13417
+ deployProfile: "app",
13418
+ deploySoftware: [],
13419
+ deployChannel: "production",
13420
+ requireAttestation: false,
13421
+ force: false,
13422
+ noBrowser: false,
13423
+ provider: "github",
13424
+ branch: "main",
13425
+ authMode: "public",
13426
+ targets: [],
13427
+ wait: false,
13428
+ timeoutSeconds: 900,
13429
+ queries: []
13430
+ };
13431
+ const positional = [];
13432
+ for (let index = 0;index < argv.length; index += 1) {
13433
+ const token = argv[index];
13434
+ if (token === "--api") {
13435
+ options.api = argv[++index] ?? options.api;
13436
+ options.apiExplicit = true;
13437
+ } else if (token === "--app")
13438
+ options.app = argv[++index];
13439
+ else if (token === "--realm") {
13440
+ options.realm = argv[++index] ?? options.realm;
13441
+ options.realmExplicit = true;
13442
+ } else if (token === "--socket")
13443
+ options.socket = argv[++index];
13444
+ else if (token === "--json")
13445
+ options.json = true;
13446
+ else if (token === "--apply")
13447
+ options.apply = true;
13448
+ else if (token === "--enrol")
13449
+ options.enrol = true;
13450
+ else if (token === "--preserve-env")
13451
+ options.preserveEnv = true;
13452
+ else if (token === "--root")
13453
+ options.projectRoot = argv[++index] ?? options.projectRoot;
13454
+ else if (token === "--name")
13455
+ options.projectName = argv[++index];
13456
+ else if (token === "--purpose")
13457
+ options.projectPurpose = argv[++index];
13458
+ else if (token === "--profile")
13459
+ options.deployProfile = argv[++index] ?? options.deployProfile;
13460
+ else if (token === "--software")
13461
+ options.deploySoftware.push(argv[++index] ?? "");
13462
+ else if (token === "--channel") {
13463
+ const channel = argv[++index];
13464
+ if (channel === "development" || channel === "production")
13465
+ options.deployChannel = channel;
13466
+ else
13467
+ options.optionError = "--channel must be production or development.";
13468
+ } else if (token === "--attestation")
13469
+ options.requireAttestation = true;
13470
+ else if (token === "--force")
13471
+ options.force = true;
13472
+ else if (token === "--no-browser")
13473
+ options.noBrowser = true;
13474
+ else if (token === "--project")
13475
+ options.projectKey = argv[++index];
13476
+ else if (token === "--pipeline")
13477
+ options.pipelineKey = argv[++index];
13478
+ else if (token === "--revision")
13479
+ options.revision = argv[++index];
13480
+ else if (token === "--label")
13481
+ options.label = argv[++index];
13482
+ else if (token === "--provider") {
13483
+ const value = argv[++index];
13484
+ if (value === "github" || value === "gitlab" || value === "generic")
13485
+ options.provider = value;
13486
+ else
13487
+ options.optionError = "--provider must be github, gitlab, or generic.";
13488
+ } else if (token === "--repository")
13489
+ options.repository = argv[++index];
13490
+ else if (token === "--branch")
13491
+ options.branch = argv[++index] ?? options.branch;
13492
+ else if (token === "--clone-url")
13493
+ options.cloneUrl = argv[++index];
13494
+ else if (token === "--auth") {
13495
+ const value = argv[++index];
13496
+ if (value === "public" || value === "node-ssh" || value === "vault-token")
13497
+ options.authMode = value;
13498
+ else
13499
+ options.optionError = "--auth must be public, node-ssh, or vault-token.";
13500
+ } else if (token === "--git-secret")
13501
+ options.gitSecret = argv[++index];
13502
+ else if (token === "--git-username")
13503
+ options.gitUsername = argv[++index];
13504
+ else if (token === "--known-hosts")
13505
+ options.knownHostsFile = argv[++index];
13506
+ else if (token === "--target")
13507
+ options.targets.push(argv[++index] ?? "");
13508
+ else if (token === "--target-key")
13509
+ options.targetKey = argv[++index];
13510
+ else if (token === "--wait")
13511
+ options.wait = true;
13512
+ else if (token === "--timeout") {
13513
+ const value = Number(argv[++index] ?? "");
13514
+ if (Number.isFinite(value) && value > 0 && value <= 86400)
13515
+ options.timeoutSeconds = value;
13516
+ else
13517
+ options.optionError = "--timeout must be between 1 and 86400 seconds.";
13518
+ } else if (token === "--data")
13519
+ options.data = argv[++index];
13520
+ else if (token === "--data-file")
13521
+ options.dataFile = argv[++index];
13522
+ else if (token === "--bootstrap-config")
13523
+ options.bootstrapConfigPath = argv[++index];
13524
+ else if (token === "--query")
13525
+ options.queries.push(argv[++index] ?? "");
13526
+ else if (token === "--key")
13527
+ options.key = argv[++index];
13528
+ else if (token === "--mode")
13529
+ options.mode = argv[++index] ?? options.mode;
13530
+ else if (token === "--user") {
13531
+ options.user = argv[++index] ?? options.user;
13532
+ options.userExplicit = true;
13533
+ } else if (token === "--token-file")
13534
+ options.tokenFile = argv[++index];
13535
+ else if (token === "--phrase-file")
13536
+ options.phraseFile = argv[++index];
13537
+ else if (token === "--phrase-stdin")
13538
+ options.phraseStdin = true;
13539
+ else
13540
+ positional.push(token);
13541
+ }
13542
+ return { command: positional[0] ?? "help", args: positional.slice(1), options };
13543
+ }
13544
+ var out = {
13545
+ line: (text3 = "") => process.stdout.write(`${text3}
13546
+ `),
13547
+ step: (text3) => process.stdout.write(` ${text3}
13548
+ `),
13549
+ warn: (text3) => process.stderr.write(` ! ${text3}
13550
+ `),
13551
+ fail: (text3) => process.stderr.write(` \u2717 ${text3}
13552
+ `),
13553
+ ok: (text3) => process.stdout.write(` \u2713 ${text3}
13554
+ `)
13555
+ };
13556
+ var sessionCookie = null;
13557
+ var storedSession = null;
13558
+ function captureSession(response) {
13559
+ const setCookie = response.headers.get("set-cookie");
13560
+ if (!setCookie)
13561
+ return;
13562
+ const cookie = setCookie.split(";")[0];
13563
+ sessionCookie = cookie;
13564
+ if (storedSession && (cookie !== storedSession.cookie || Date.now() - storedSession.lastUsedAtTs >= 60000)) {
13565
+ storedSession = { ...storedSession, cookie, lastUsedAtTs: Date.now() };
13566
+ saveSession(storedSession);
13567
+ }
13568
+ }
13569
+ function requestHeaders(apiBase, cookie) {
13570
+ return {
13571
+ "content-type": "application/json",
13572
+ origin: new URL(apiBase).origin,
13573
+ ...cookie ? { cookie } : {}
13574
+ };
13575
+ }
13576
+ async function api(options, path, init) {
13577
+ const base = options.realm === "platform" ? "/api" : `/api/t/${encodeURIComponent(options.realm)}`;
13578
+ const response = await fetch(`${options.api}${base}${path}`, {
13579
+ method: init?.method ?? "GET",
13580
+ headers: { ...requestHeaders(options.api, sessionCookie), "user-agent": `forgezero-cli/${VERSION2}` },
13581
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13582
+ });
13583
+ captureSession(response);
13584
+ let body = null;
13585
+ try {
13586
+ body = await response.json();
13587
+ } catch {
13588
+ body = null;
13589
+ }
13590
+ const challenge = body?.security;
13591
+ if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && custodyIdentity && !path.startsWith("/security/step-up/")) {
13592
+ const proved = await proveWithAgent(options, challenge.requestKey);
13593
+ if (proved) {
13594
+ const replay = await fetch(`${options.api}${base}${path}`, {
13595
+ method: init?.method ?? "GET",
13596
+ headers: {
13597
+ ...requestHeaders(options.api, sessionCookie),
13598
+ "x-security-request-key": challenge.requestKey
13599
+ },
13600
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13601
+ });
13602
+ captureSession(replay);
13603
+ let replayed = null;
13604
+ try {
13605
+ replayed = await replay.json();
13606
+ } catch {
13607
+ replayed = null;
13608
+ }
13609
+ return { status: replay.status, body: replayed };
13610
+ }
13611
+ }
13612
+ if (response.status === 428 && challenge?.scope === "action" && challenge.requestKey && storedSession && !path.startsWith("/security/step-up/")) {
13613
+ const proved = await proveWithBrowser(options, challenge.requestKey, base);
13614
+ if (proved) {
13615
+ const replay = await fetch(`${options.api}${base}${path}`, {
13616
+ method: init?.method ?? "GET",
13617
+ headers: {
13618
+ ...requestHeaders(options.api, sessionCookie),
13619
+ "user-agent": `forgezero-cli/${VERSION2}`,
13620
+ "x-security-request-key": challenge.requestKey
13621
+ },
13622
+ body: init?.body === undefined ? undefined : JSON.stringify(init.body)
13623
+ });
13624
+ captureSession(replay);
13625
+ let replayed = null;
13626
+ try {
13627
+ replayed = await replay.json();
13628
+ } catch {}
13629
+ return { status: replay.status, body: replayed };
13630
+ }
13631
+ }
13632
+ if (response.status === 401 && storedSession) {
13633
+ removeSession(storedSession.api, storedSession.realm);
13634
+ storedSession = null;
13635
+ sessionCookie = null;
13636
+ }
13637
+ return { status: response.status, body };
13638
+ }
13639
+ function sleep(ms) {
13640
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
13641
+ }
13642
+ function browserCommand(url) {
13643
+ if (process.platform === "darwin")
13644
+ return ["open", url];
13645
+ if (process.platform === "win32")
13646
+ return null;
13647
+ return ["xdg-open", url];
10343
13648
  }
10344
13649
  function openBrowser(url, disabled, stream = "stdout") {
10345
13650
  const line = ` Open: ${url}
@@ -10438,7 +13743,7 @@ function requireSession() {
10438
13743
  function readOwnerOnlySecret(path, label) {
10439
13744
  let metadata;
10440
13745
  try {
10441
- metadata = lstatSync2(path);
13746
+ metadata = lstatSync4(path);
10442
13747
  } catch {
10443
13748
  throw new Error(`The ${label} file ${path} is unreadable.`);
10444
13749
  }
@@ -10452,7 +13757,7 @@ function readOwnerOnlySecret(path, label) {
10452
13757
  if (uid !== undefined && uid !== 0 && metadata.uid !== uid) {
10453
13758
  throw new Error(`The ${label} file ${path} is not owned by the current user.`);
10454
13759
  }
10455
- const value = readFileSync4(path, "utf8").trim();
13760
+ const value = readFileSync7(path, "utf8").trim();
10456
13761
  if (!value)
10457
13762
  throw new Error(`The ${label} file ${path} is empty.`);
10458
13763
  return value;
@@ -10625,7 +13930,7 @@ async function cmdApi(options, args) {
10625
13930
  let body = undefined;
10626
13931
  if (options.data !== undefined && options.dataFile)
10627
13932
  throw new Error("Use only one of --data or --data-file.");
10628
- const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync4(options.dataFile, "utf8") : options.data;
13933
+ const encoded = options.data === "-" ? await Bun.stdin.text() : options.dataFile ? readFileSync7(options.dataFile, "utf8") : options.data;
10629
13934
  if (encoded !== undefined)
10630
13935
  body = JSON.parse(encoded);
10631
13936
  const result = await api(options, `${url.pathname}${url.search}`, { method, body });
@@ -10750,7 +14055,7 @@ async function cmdAgent(options, args) {
10750
14055
  deployRoot: process.env.FZ_DEPLOY_ROOT ?? "/opt/forgezero"
10751
14056
  } : {},
10752
14057
  binPath: process.env.FZ_AGENT_BIN ?? "/usr/local/lib/forgezero/agent/fz-agent",
10753
- sourceBinPath: process.env.FZ_AGENT_SOURCE_BIN ?? PACKAGED_AGENT_BIN,
14058
+ sourceBinPath: process.env.FZ_AGENT_SOURCE_BIN ?? PACKAGED_AGENT_BIN2,
10754
14059
  user: process.env.FZ_AGENT_USER,
10755
14060
  apiUrl: options.api,
10756
14061
  project: process.env.FZ_PROJECT,
@@ -10774,17 +14079,17 @@ async function cmdAgent(options, args) {
10774
14079
  return 0;
10775
14080
  }
10776
14081
  try {
10777
- writeFileSync4(plan.unitPath, plan.unit, { mode: 420 });
14082
+ writeFileSync7(plan.unitPath, plan.unit, { mode: 420 });
10778
14083
  out.ok(`Wrote ${plan.unitPath}`);
10779
14084
  for (const auxiliary of plan.auxiliaryUnits) {
10780
- mkdirSync4(dirname3(auxiliary.path), { recursive: true, mode: 493 });
10781
- writeFileSync4(auxiliary.path, auxiliary.unit, { mode: 420 });
14085
+ mkdirSync7(dirname8(auxiliary.path), { recursive: true, mode: 493 });
14086
+ writeFileSync7(auxiliary.path, auxiliary.unit, { mode: 420 });
10782
14087
  out.ok(`Wrote ${auxiliary.path}`);
10783
14088
  }
10784
14089
  if (options.enrol) {
10785
- if (existsSync4(enrolTokenSourcePath)) {
10786
- const source = statSync(enrolTokenSourcePath);
10787
- const token = readFileSync4(enrolTokenSourcePath, "utf8").trim();
14090
+ if (existsSync6(enrolTokenSourcePath)) {
14091
+ const source = statSync2(enrolTokenSourcePath);
14092
+ const token = readFileSync7(enrolTokenSourcePath, "utf8").trim();
10788
14093
  if (!source.isFile() || (source.mode & 511) !== 384 || source.uid !== 0) {
10789
14094
  throw new Error("The preloaded enrolment token must be a root-owned 0600 file in /run.");
10790
14095
  }
@@ -10797,7 +14102,7 @@ async function cmdAgent(options, args) {
10797
14102
  if (await prompt.exited !== 0 || !/^fze_[A-Za-z0-9_-]{40,100}$/.test(token)) {
10798
14103
  throw new Error("A valid fze_ enrolment token was not provided.");
10799
14104
  }
10800
- writeFileSync4(enrolTokenSourcePath, `${token}
14105
+ writeFileSync7(enrolTokenSourcePath, `${token}
10801
14106
  `, { mode: 384, flag: "wx" });
10802
14107
  }
10803
14108
  }
@@ -10808,13 +14113,13 @@ async function cmdAgent(options, args) {
10808
14113
  out.line();
10809
14114
  out.line(" Add this machine-specific PUBLIC key as a read-only deploy key:");
10810
14115
  out.line();
10811
- out.line(` ${readFileSync4(gitPublicKeyPath, "utf8").trim()}`);
14116
+ out.line(` ${readFileSync7(gitPublicKeyPath, "utf8").trim()}`);
10812
14117
  out.line();
10813
14118
  return 0;
10814
14119
  } catch (cause) {
10815
14120
  if (options.enrol) {
10816
14121
  try {
10817
- unlinkSync2(enrolTokenSourcePath);
14122
+ unlinkSync3(enrolTokenSourcePath);
10818
14123
  } catch {}
10819
14124
  }
10820
14125
  out.fail(`Could not write ${plan.unitPath}: ${cause.message}`);
@@ -10822,6 +14127,198 @@ async function cmdAgent(options, args) {
10822
14127
  return 1;
10823
14128
  }
10824
14129
  }
14130
+ var bootstrapAnswer = (question, fallback) => {
14131
+ const suffix = fallback ? ` [${fallback}]` : "";
14132
+ const value = globalThis.prompt(`${question}${suffix}:`)?.trim();
14133
+ if (value)
14134
+ return value;
14135
+ if (fallback !== undefined)
14136
+ return fallback;
14137
+ throw new Error(`${question} is required (or use --bootstrap-config).`);
14138
+ };
14139
+ function interactiveBootstrap(kind) {
14140
+ if (!process.stdin.isTTY)
14141
+ throw new Error("non-interactive bootstrap requires --bootstrap-config <private-json-file>");
14142
+ if (kind === "tenant") {
14143
+ return {
14144
+ kind,
14145
+ apiUrl: bootstrapAnswer("Platform API URL"),
14146
+ realm: bootstrapAnswer("Tenant realm slug"),
14147
+ nodeHostname: bootstrapAnswer("Node hostname", hostname()),
14148
+ telemetryEndpoint: bootstrapAnswer("Public HTTPS OTLP endpoint"),
14149
+ enrolTokenFile: bootstrapAnswer("Root-only enrolment token file"),
14150
+ profile: bootstrapAnswer("Deploy profile", "app")
14151
+ };
14152
+ }
14153
+ const environment = bootstrapAnswer("Environment (production/development)", "production");
14154
+ const profile = bootstrapAnswer("Profile (platform-db-api/platform-api)", "platform-db-api");
14155
+ const role = profile === "platform-api" ? "none" : bootstrapAnswer("Database role (master/joiner)", "master");
14156
+ const serverMode = "default";
14157
+ const address = role === "none" ? undefined : bootstrapAnswer("Private database address");
14158
+ const master = role === "joiner" ? bootstrapAnswer("Master starter private address") : undefined;
14159
+ const coordinators = bootstrapAnswer("Writable Coordinator origins (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
14160
+ const computeReference = bootstrapAnswer("API-owned compute reference", environment === "production" ? "fz-n1" : "dev-fz-n1");
14161
+ const nodeHostname = bootstrapAnswer("Public node hostname");
14162
+ const apiUrl = bootstrapAnswer("Platform API URL");
14163
+ const appOrigin = bootstrapAnswer("Public App origin");
14164
+ const repository = bootstrapAnswer("API Git repository");
14165
+ const branch = bootstrapAnswer("Deployment branch", environment === "production" ? "main" : "dev");
14166
+ const telemetryEndpoint = bootstrapAnswer("Public HTTPS Agent OTLP endpoint");
14167
+ const collectorUnit = bootstrapAnswer("Independent local OTLP collector unit", "otelcol.service");
14168
+ const replicationFactor = Number(bootstrapAnswer("Database replication factor", "3"));
14169
+ const writeConcern = Number(bootstrapAnswer("Database write concern", "2"));
14170
+ const seedPeers = bootstrapAnswer("Seed WebSocket peers (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean);
14171
+ const smtpHost = bootstrapAnswer("Bootstrap SMTP host (blank to disable)", "");
14172
+ const smtpPort = smtpHost ? Number(bootstrapAnswer("Bootstrap SMTP port", "587")) : undefined;
14173
+ const smtpUser = smtpHost ? bootstrapAnswer("Bootstrap SMTP user (blank if none)", "") : undefined;
14174
+ const smtpFrom = smtpHost ? bootstrapAnswer("Bootstrap SMTP From address") : undefined;
14175
+ const smtpPassword = smtpHost ? bootstrapAnswer("Root-only SMTP password file") : undefined;
14176
+ const backupEndpoint = bootstrapAnswer("Backup S3 HTTPS endpoint (blank to disable)", "");
14177
+ const backupRegion = backupEndpoint ? bootstrapAnswer("Backup S3 region") : undefined;
14178
+ const backupBucket = backupEndpoint ? bootstrapAnswer("Backup S3 bucket") : undefined;
14179
+ const backupAccessKeyId = backupEndpoint ? bootstrapAnswer("Backup S3 access-key id") : undefined;
14180
+ const backupS3Secret = backupEndpoint ? bootstrapAnswer("Root-only backup S3 secret file") : undefined;
14181
+ const cloudflareCheckpoint = bootstrapAnswer("Completed Cloudflare bootstrap checkpoint (blank for no public edge)", "");
14182
+ const cloudflareNodeName = cloudflareCheckpoint ? bootstrapAnswer("Cloudflare checkpoint node name", computeReference) : undefined;
14183
+ const databaseNetworkMode = cloudflareCheckpoint ? bootstrapAnswer("Database network (private-lan/cloudflare-warp)", "private-lan") : "private-lan";
14184
+ const bootstrapSecretFile = bootstrapAnswer("Root-only shared cluster bootstrap-code file");
14185
+ const index = Number(computeReference.match(/n(\d+)$/)?.[1] ?? 0);
14186
+ const platformEnrolTokenFile = index > 3 ? bootstrapAnswer("Root-only API-issued platform enrolment-token file") : undefined;
14187
+ return {
14188
+ kind,
14189
+ environment,
14190
+ profile,
14191
+ computeReference,
14192
+ nodeHostname,
14193
+ apiUrl,
14194
+ repository,
14195
+ branch,
14196
+ telemetryEndpoint,
14197
+ database: {
14198
+ role,
14199
+ serverMode,
14200
+ address,
14201
+ master,
14202
+ coordinators,
14203
+ bootstrapSecretFile
14204
+ },
14205
+ platformEnrolTokenFile,
14206
+ runtime: {
14207
+ environment: {
14208
+ softwareProfile: profile,
14209
+ databaseRole: role,
14210
+ databaseCoordinators: coordinators,
14211
+ databaseAddress: address,
14212
+ databaseMaster: master,
14213
+ databaseNetworkMode,
14214
+ databaseReplicationFactor: replicationFactor,
14215
+ databaseWriteConcern: writeConcern,
14216
+ databaseUser: "forgezero-api",
14217
+ nodeHostname,
14218
+ nodeRegion: bootstrapAnswer("Published node region"),
14219
+ nodeRole: "guest",
14220
+ appOrigin,
14221
+ apiOrigin: apiUrl,
14222
+ publicApiPort: Number(bootstrapAnswer("Loopback public API port", "3000")),
14223
+ sharedDirectory: "/opt/forgezero/shared",
14224
+ seedSyncPeers: seedPeers,
14225
+ seedSyncMembers: Number(bootstrapAnswer("Seed quorum members", "3")),
14226
+ seedSyncEpoch: bootstrapAnswer("Seed epoch"),
14227
+ concurrencyLimit: Number(bootstrapAnswer("Initial concurrency limit", "128")),
14228
+ drainDeadlineMs: Number(bootstrapAnswer("Drain deadline milliseconds", "30000")),
14229
+ otlpEndpoint: "http://127.0.0.1:4318",
14230
+ otlpCollectorUnit: collectorUnit,
14231
+ agentOtlpEndpoint: telemetryEndpoint,
14232
+ otlpFlushIntervalMs: 1e4,
14233
+ otlpTraceSampleRatio: 0.1,
14234
+ custodianEmail: role === "master" ? bootstrapAnswer("First custodian email") : undefined,
14235
+ ...smtpHost && smtpPort && smtpFrom ? { smtp: { host: smtpHost, port: smtpPort, user: smtpUser || undefined, from: smtpFrom } } : {},
14236
+ ...backupEndpoint && backupRegion && backupBucket && backupAccessKeyId ? {
14237
+ backup: { endpoint: backupEndpoint, region: backupRegion, bucket: backupBucket, accessKeyId: backupAccessKeyId }
14238
+ } : {},
14239
+ repository,
14240
+ branch,
14241
+ deployProfile: environment
14242
+ },
14243
+ serviceUser: "forgezero-api",
14244
+ slotsDirectory: "/opt/forgezero/slots",
14245
+ bluePort: 3001,
14246
+ greenPort: 3002,
14247
+ healthPath: "/api/health",
14248
+ keepReleases: 5,
14249
+ credentialFiles: {
14250
+ smtpPassword,
14251
+ backupS3Secret
14252
+ }
14253
+ },
14254
+ firewall: {
14255
+ enabled: bootstrapAnswer("Enable host firewall? (yes/no)", "yes") === "yes",
14256
+ sshPort: Number(bootstrapAnswer("SSH port", "22")),
14257
+ privateCidrs: bootstrapAnswer("Private cluster CIDRs (comma-separated)").split(",").map((item) => item.trim()).filter(Boolean)
14258
+ },
14259
+ installCloudflared: cloudflareCheckpoint ? true : bootstrapAnswer("Install cloudflared binary only? (yes/no)", "no") === "yes",
14260
+ ...cloudflareCheckpoint && cloudflareNodeName ? {
14261
+ cloudflareHandoff: { checkpointFile: cloudflareCheckpoint, nodeName: cloudflareNodeName }
14262
+ } : {}
14263
+ };
14264
+ }
14265
+ async function cmdBootstrap(options, args) {
14266
+ try {
14267
+ const operation = args[0] ?? "status";
14268
+ if (operation === "platform" && args[1] === "cloudflare") {
14269
+ if (!options.bootstrapConfigPath) {
14270
+ throw new Error("Cloudflare bootstrap requires --bootstrap-config <owner-only-cloudflare-json>");
14271
+ }
14272
+ await runCloudflareBootstrapCommand(options.bootstrapConfigPath, options.apply);
14273
+ return 0;
14274
+ }
14275
+ if (operation === "status") {
14276
+ if (existsSync6("/etc/forgezero/metal.initialized.json")) {
14277
+ const status2 = await metalBootstrapStatus();
14278
+ out.line(JSON.stringify(status2, null, 2));
14279
+ return status2.initialized && status2.problems.length === 0 ? 0 : 1;
14280
+ }
14281
+ const status = await bootstrapStatus();
14282
+ out.line(JSON.stringify(status, null, 2));
14283
+ return status.initialized ? 0 : 1;
14284
+ }
14285
+ if (operation === "metal") {
14286
+ if (!options.bootstrapConfigPath) {
14287
+ throw new Error("metal bootstrap requires --bootstrap-config <owner-only-metal-json>");
14288
+ }
14289
+ const config2 = readMetalBootstrapConfig(options.bootstrapConfigPath);
14290
+ if (!options.apply) {
14291
+ out.line(JSON.stringify(planMetalBootstrap(config2), null, 2));
14292
+ out.step("Review the plan, then repeat with --apply as root.");
14293
+ return 0;
14294
+ }
14295
+ const result2 = await applyMetalBootstrap(config2, { agentSourcePath: PACKAGED_AGENT_BIN2 });
14296
+ out.line(JSON.stringify(result2, null, 2));
14297
+ return 0;
14298
+ }
14299
+ if (!["platform", "tenant", "repair"].includes(operation)) {
14300
+ throw new Error("Usage: fz bootstrap platform [cloudflare]|tenant|metal|status|repair [--bootstrap-config <path>] [--apply]");
14301
+ }
14302
+ const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
14303
+ throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
14304
+ })() : interactiveBootstrap(operation);
14305
+ if (operation !== "repair" && config.kind !== operation) {
14306
+ throw new Error(`bootstrap config kind ${config.kind} does not match requested ${operation}`);
14307
+ }
14308
+ const plan = planBootstrap(config, operation === "repair");
14309
+ if (!options.apply) {
14310
+ out.line(JSON.stringify(plan, null, 2));
14311
+ out.step("Review the plan, then repeat with --apply as root.");
14312
+ return 0;
14313
+ }
14314
+ const result = await applyBootstrap(config);
14315
+ out.line(JSON.stringify(result, null, 2));
14316
+ return 0;
14317
+ } catch (cause) {
14318
+ out.fail(cause instanceof Error ? cause.message : String(cause));
14319
+ return 1;
14320
+ }
14321
+ }
10825
14322
  async function cmdGenesis(options) {
10826
14323
  try {
10827
14324
  if (options.realm !== "platform") {
@@ -10840,10 +14337,10 @@ async function cmdGenesis(options) {
10840
14337
  ` + ` On the server: the token file is read automatically, or set FZ_SHARED_DIR
10841
14338
  ` + ` From a laptop: copy it to a chmod 600 file and pass --token-file <path>
10842
14339
 
10843
- ` + " The token is what setup.sh created. It is single use.");
14340
+ ` + " The token is what `fz bootstrap platform` created. It is single use.");
10844
14341
  }
10845
14342
  if (!/^plt_[A-Za-z0-9_-]{32,100}$/.test(token)) {
10846
- throw new Error("The platform invitation is malformed; expected the one-use plt_ token from setup.sh.");
14343
+ throw new Error("The platform invitation is malformed; expected the one-use plt_ token from `fz bootstrap platform`.");
10847
14344
  }
10848
14345
  const session = await authorizeDeviceSession(options, (approval, app) => {
10849
14346
  const ceremony = new URL("/custody/ceremony", app);
@@ -10880,7 +14377,7 @@ async function cmdUnlock(options) {
10880
14377
  if (options.key || options.userExplicit) {
10881
14378
  throw new Error("SSH/user unlock coordinates are not accepted. SSH keys cannot open a WebAuthn-PRF custody envelope; use --phrase-file or --phrase-stdin.");
10882
14379
  }
10883
- const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync4(0, "utf8").trim() : "";
14380
+ const phraseText = options.phraseFile ? readOwnerOnlySecret(options.phraseFile, "recovery phrase") : options.phraseStdin ? readFileSync7(0, "utf8").trim() : "";
10884
14381
  const phrase = phraseText ? phraseText.split(/\s+/) : [];
10885
14382
  if (phrase.length !== 24) {
10886
14383
  throw new Error(`Recovery phrase must contain exactly 24 words; received ${phrase.length}. Use --phrase-file or --phrase-stdin.`);
@@ -10999,7 +14496,7 @@ function projectFromCheckout(options) {
10999
14496
  try {
11000
14497
  return loadConfig({
11001
14498
  cwd: options.projectRoot,
11002
- readFile: (path) => existsSync4(path) ? readFileSync4(path, "utf8") : undefined
14499
+ readFile: (path) => existsSync6(path) ? readFileSync7(path, "utf8") : undefined
11003
14500
  }).config.project;
11004
14501
  } catch (cause) {
11005
14502
  throw new Error(`No --project was given and the checkout has no usable .fz/config.json: ${cause instanceof Error ? cause.message : String(cause)}`);
@@ -11118,7 +14615,7 @@ async function cmdDeploy(options, args) {
11118
14615
  branch: options.branch,
11119
14616
  cloneUrl: required(options.cloneUrl, "--clone-url"),
11120
14617
  sourceAuth,
11121
- ...options.knownHostsFile ? { knownHosts: readFileSync4(options.knownHostsFile, "utf8") } : {},
14618
+ ...options.knownHostsFile ? { knownHosts: readFileSync7(options.knownHostsFile, "utf8") } : {},
11122
14619
  projectKey
11123
14620
  } });
11124
14621
  const pipelineKey2 = String(created.pipelineKey);
@@ -11215,6 +14712,14 @@ function usage() {
11215
14712
  fz agent install Install the node agent as a systemd service, so
11216
14713
  applications on this box read secrets through a
11217
14714
  local socket instead of holding an API key
14715
+ fz bootstrap platform Install/repair a typed elastic platform compute
14716
+ fz bootstrap platform cloudflare
14717
+ Plan/apply token-file-only KV, Worker, Access, DNS
14718
+ and one Tunnel per explicit platform API node
14719
+ fz bootstrap tenant Install/enrol reusable tenant compute tooling
14720
+ fz bootstrap metal Plan/install an identity-only physical provisioner
14721
+ fz bootstrap status Verify persisted profile and supervised units
14722
+ fz bootstrap repair Reapply an explicitly supplied reviewed config
11218
14723
  fz project init Create vendor-neutral, Git-persisted AI context
11219
14724
  fz project sync Regenerate Claude/Codex/Gemini/Copilot/Cursor adapters
11220
14725
  fz project check Fail when truth sources or generated adapters drift
@@ -11252,6 +14757,8 @@ function usage() {
11252
14757
  --apply Write the unit rather than printing it (root)
11253
14758
  --enrol Bind this machine with a one-time token prompted
11254
14759
  securely by systemd (tenant-owned compute)
14760
+ --bootstrap-config <p> Private JSON coordinates for unattended bootstrap;
14761
+ secret values remain in separate owner-only files
11255
14762
  --root <path> Project root for project/deploy commands
11256
14763
  --name <name> Project or deploy name during init
11257
14764
  --purpose <text> Product outcome during project init
@@ -11330,6 +14837,9 @@ async function runCli() {
11330
14837
  case "agent":
11331
14838
  code = await cmdAgent(options, args);
11332
14839
  break;
14840
+ case "bootstrap":
14841
+ code = await cmdBootstrap(options, args);
14842
+ break;
11333
14843
  case "project":
11334
14844
  code = cmdProject(options, args);
11335
14845
  break;
@@ -11359,7 +14869,7 @@ async function runCli() {
11359
14869
  usage();
11360
14870
  code = 1;
11361
14871
  }
11362
- if (args.length > 0 && code === 0 && !["agent", "project", "deploy", "api", "ui", "routes"].includes(command)) {
14872
+ if (args.length > 0 && code === 0 && !["agent", "bootstrap", "project", "deploy", "api", "ui", "routes"].includes(command)) {
11363
14873
  out.warn(`Ignored: ${args.join(" ")}`);
11364
14874
  }
11365
14875
  process.exit(code);