@forgezero/agent 0.1.34 → 0.1.36

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.
@@ -0,0 +1,3416 @@
1
+ // @bun
2
+ // src/cloudflare-edge.ts
3
+ import { isIP } from "net";
4
+ function isPrivateDatabaseAddress(value) {
5
+ const address = value.trim().toLowerCase();
6
+ const family = isIP(address);
7
+ if (family === 4) {
8
+ const [a, b] = address.split(".").map(Number);
9
+ return a === 10 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168;
10
+ }
11
+ if (family === 6) {
12
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
13
+ return Number.isFinite(first) && (first & 65024) === 64512;
14
+ }
15
+ return false;
16
+ }
17
+ function privateDatabaseHostRoute(value) {
18
+ const address = value.trim().toLowerCase();
19
+ if (!isPrivateDatabaseAddress(address)) {
20
+ throw new Error("database address must be an RFC 1918 IPv4 or unique-local IPv6 address");
21
+ }
22
+ return `${address}/${isIP(address) === 4 ? 32 : 128}`;
23
+ }
24
+ var endpoint = "https://api.cloudflare.com/client/v4";
25
+ async function cf(config, path, init = {}, fetcher = fetch) {
26
+ const response = await fetcher(`${endpoint}${path}`, {
27
+ ...init,
28
+ headers: {
29
+ authorization: `Bearer ${config.apiToken}`,
30
+ "content-type": "application/json",
31
+ ...init.headers
32
+ }
33
+ });
34
+ const body = await response.json();
35
+ if (!response.ok || body.success !== true) {
36
+ throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
37
+ }
38
+ return body.result;
39
+ }
40
+ async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
41
+ const [address, prefixText, ...extra] = config.network.split("/");
42
+ const family = isIP(address ?? "");
43
+ const prefix = Number(prefixText);
44
+ if (extra.length > 0 || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
45
+ throw new Error("Cloudflare private route must be an explicit IPv4 or IPv6 CIDR");
46
+ }
47
+ const path = `/accounts/${config.accountId}/teamnet/routes`;
48
+ const routes = await cf(config, path, {}, fetcher);
49
+ const current = routes.find((route2) => !route2.deleted_at && route2.network === config.network && (route2.virtual_network_id ?? "") === (config.virtualNetworkId ?? ""));
50
+ if (current) {
51
+ if (current.tunnel_id !== config.tunnelId) {
52
+ throw new Error(`private route ${config.network} already belongs to another Tunnel`);
53
+ }
54
+ return { route: current, created: false };
55
+ }
56
+ const route = await cf(config, path, {
57
+ method: "POST",
58
+ body: JSON.stringify({
59
+ network: config.network,
60
+ tunnel_id: config.tunnelId,
61
+ comment: config.comment.slice(0, 100),
62
+ ...config.virtualNetworkId ? { virtual_network_id: config.virtualNetworkId } : {}
63
+ })
64
+ }, fetcher);
65
+ return { route, created: true };
66
+ }
67
+ async function ensureCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
68
+ return ensureCloudflarePrivateRoute({
69
+ ...config,
70
+ network: privateDatabaseHostRoute(config.privateAddress)
71
+ }, fetcher);
72
+ }
73
+ async function removeCloudflarePrivateRoute(config, fetcher = fetch) {
74
+ const [address, prefixText, ...extra] = config.network.split("/");
75
+ const family = isIP(address ?? "");
76
+ const prefix = Number(prefixText);
77
+ if (extra.length > 0 || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
78
+ throw new Error("Cloudflare private route must be an explicit IPv4 or IPv6 CIDR");
79
+ }
80
+ const path = `/accounts/${config.accountId}/teamnet/routes`;
81
+ const routes = await cf(config, path, {}, fetcher);
82
+ const current = routes.find((route) => !route.deleted_at && route.network === config.network && (route.virtual_network_id ?? "") === (config.virtualNetworkId ?? ""));
83
+ if (!current)
84
+ return { removed: false };
85
+ if (current.tunnel_id !== config.tunnelId) {
86
+ throw new Error(`private route ${config.network} belongs to another Tunnel`);
87
+ }
88
+ await cf(config, `${path}/${encodeURIComponent(current.id)}`, { method: "DELETE" }, fetcher);
89
+ return { removed: true, route: current };
90
+ }
91
+ async function removeCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
92
+ return removeCloudflarePrivateRoute({
93
+ ...config,
94
+ network: privateDatabaseHostRoute(config.privateAddress)
95
+ }, fetcher);
96
+ }
97
+ async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
98
+ if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
99
+ throw new Error("Cloudflare WARP policy id is invalid");
100
+ const route = privateDatabaseHostRoute(config.privateAddress);
101
+ const policy = config.policyId ? `/${config.policyId}` : "";
102
+ const path = `/accounts/${config.accountId}/devices/policy${policy}/include`;
103
+ const entries = await cf(config, path, {}, fetcher);
104
+ if (entries.some((entry) => entry.address === route))
105
+ return { entries, created: false };
106
+ const next = [...entries, { address: route, description: config.description.slice(0, 100) }];
107
+ const updated = await cf(config, path, {
108
+ method: "PUT",
109
+ body: JSON.stringify(next)
110
+ }, fetcher);
111
+ return { entries: updated, created: true };
112
+ }
113
+ async function removeCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
114
+ if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
115
+ throw new Error("Cloudflare WARP policy id is invalid");
116
+ const route = privateDatabaseHostRoute(config.privateAddress);
117
+ const policy = config.policyId ? `/${config.policyId}` : "";
118
+ const path = `/accounts/${config.accountId}/devices/policy${policy}/include`;
119
+ const entries = await cf(config, path, {}, fetcher);
120
+ const next = entries.filter((entry) => entry.address !== route);
121
+ if (next.length === entries.length)
122
+ return { entries, removed: false };
123
+ const updated = await cf(config, path, {
124
+ method: "PUT",
125
+ body: JSON.stringify(next)
126
+ }, fetcher);
127
+ return { entries: updated, removed: true };
128
+ }
129
+ async function configureCloudflareEdge(config, fetcher = fetch) {
130
+ const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
131
+ const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
132
+ const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
133
+ const current = await cf(tunnelAuth, tunnelPath, {}, fetcher);
134
+ const existing = current.config?.ingress ?? [];
135
+ const catchAll = existing.filter((rule) => !("hostname" in rule));
136
+ const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
137
+ await cf(tunnelAuth, tunnelPath, {
138
+ method: "PUT",
139
+ body: JSON.stringify({ config: { ingress: [
140
+ { hostname: config.hostname, service: config.service },
141
+ ...otherHosts,
142
+ ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
143
+ ] } })
144
+ }, fetcher);
145
+ const dnsPath = `/zones/${config.zoneId}/dns_records`;
146
+ const records = await cf(dnsAuth, `${dnsPath}?type=CNAME&name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher);
147
+ if (records.length > 1) {
148
+ throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
149
+ }
150
+ const record = {
151
+ type: "CNAME",
152
+ name: config.hostname,
153
+ content: `${config.tunnelId}.cfargotunnel.com`,
154
+ proxied: true,
155
+ ttl: 1
156
+ };
157
+ await cf(dnsAuth, records[0] ? `${dnsPath}/${records[0].id}` : dnsPath, {
158
+ method: records[0] ? "PUT" : "POST",
159
+ body: JSON.stringify(record)
160
+ }, fetcher);
161
+ }
162
+ var exactAccountPermissionGroup = async (config, name, fetcher) => {
163
+ const groups = await cf(config, `/accounts/${config.accountId}/tokens/permission_groups?name=${encodeURIComponent(name)}` + "&scope=com.cloudflare.api.account", {}, fetcher);
164
+ 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)));
165
+ if (matches.length !== 1) {
166
+ throw new Error(`Cloudflare account token permission group ${name} is ${matches.length === 0 ? "missing" : "ambiguous"}`);
167
+ }
168
+ return { id: matches[0].id, name };
169
+ };
170
+ async function createCloudflareAccountRuntimeToken(config, fetcher = fetch) {
171
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId))
172
+ throw new Error("Cloudflare account id is invalid");
173
+ const name = config.name.trim();
174
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,119}$/.test(name)) {
175
+ throw new Error("Cloudflare account runtime-token name is invalid");
176
+ }
177
+ const permissionNames = [...new Set(config.permissionNames)];
178
+ if (permissionNames.length === 0)
179
+ throw new Error("Cloudflare account runtime token needs a permission group");
180
+ const permissionGroups = await Promise.all(permissionNames.map((permissionName) => exactAccountPermissionGroup(config, permissionName, fetcher)));
181
+ const body = {
182
+ name,
183
+ policies: [{
184
+ effect: "allow",
185
+ permission_groups: permissionGroups.map(({ id }) => ({ id })),
186
+ resources: { [`com.cloudflare.api.account.${config.accountId}`]: "*" }
187
+ }]
188
+ };
189
+ const created = await cf(config, `/accounts/${config.accountId}/tokens`, { method: "POST", body: JSON.stringify(body) }, fetcher);
190
+ if (!created.id || !/^[a-f0-9]{32}$/i.test(created.id) || !created.value || !/^[A-Za-z0-9._-]{40,80}$/.test(created.value)) {
191
+ throw new Error("Cloudflare did not return the one-time account runtime-token id and value");
192
+ }
193
+ return { id: created.id, value: created.value, name, permissionNames };
194
+ }
195
+ async function ensureCloudflareAccessServiceToken(config, fetcher = fetch) {
196
+ const name = config.name.trim();
197
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
198
+ throw new Error("Cloudflare Access service-token name is invalid");
199
+ }
200
+ const path = `/accounts/${config.accountId}/access/service_tokens`;
201
+ const tokens = await cf(config, `${path}?per_page=1000`, {}, fetcher);
202
+ const matches = tokens.filter((token) => token.name === name);
203
+ if (matches.length > 1)
204
+ throw new Error(`Cloudflare Access service token ${name} is ambiguous`);
205
+ if (matches[0]) {
206
+ if (!config.existing || config.existing.tokenId !== matches[0].id || config.existing.clientId !== matches[0].client_id || !config.existing.clientSecret) {
207
+ throw new Error(`Cloudflare Access service token ${name} exists but its one-time client secret was not supplied`);
208
+ }
209
+ return { credentials: config.existing, created: false };
210
+ }
211
+ const created = await cf(config, path, {
212
+ method: "POST",
213
+ body: JSON.stringify({ name, duration: config.duration ?? "8760h" })
214
+ }, fetcher);
215
+ if (!created.id || !created.client_id || !created.client_secret) {
216
+ throw new Error("Cloudflare did not return the new Access service-token secret");
217
+ }
218
+ return {
219
+ credentials: {
220
+ tokenId: created.id,
221
+ clientId: created.client_id,
222
+ clientSecret: created.client_secret
223
+ },
224
+ created: true
225
+ };
226
+ }
227
+ async function ensureCloudflareAccessPolicy(config, fetcher = fetch) {
228
+ const path = `/accounts/${config.accountId}/access/policies`;
229
+ const policies = await cf(config, `${path}?per_page=1000`, {}, fetcher);
230
+ const matches = policies.filter((policy2) => policy2.name === config.name);
231
+ if (matches.length > 1)
232
+ throw new Error(`Cloudflare Access policy ${config.name} is ambiguous`);
233
+ const desired = {
234
+ name: config.name,
235
+ decision: "non_identity",
236
+ include: [{ service_token: { token_id: config.serviceTokenId } }]
237
+ };
238
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, {
239
+ method: matches[0] ? "PUT" : "POST",
240
+ body: JSON.stringify(desired)
241
+ }, fetcher);
242
+ return { policy, created: !matches[0] };
243
+ }
244
+ async function ensureCloudflareAccessApplication(config, fetcher = fetch) {
245
+ const path = `/accounts/${config.accountId}/access/apps`;
246
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
247
+ const matches = applications.filter((application2) => application2.domain === config.hostname || application2.self_hosted_domains?.includes(config.hostname));
248
+ if (matches.length > 1)
249
+ throw new Error(`Cloudflare Access application for ${config.hostname} is ambiguous`);
250
+ const desired = {
251
+ name: config.name,
252
+ type: "self_hosted",
253
+ domain: config.hostname,
254
+ session_duration: "24h",
255
+ service_auth_401_redirect: true,
256
+ policies: [{ id: config.policyId, precedence: 1 }]
257
+ };
258
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
259
+ return { application, created: !matches[0] };
260
+ }
261
+ async function ensureCloudflareWarpEnrollmentApplication(config, fetcher = fetch) {
262
+ const name = config.name.trim();
263
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
264
+ throw new Error("Cloudflare WARP enrollment application name is invalid");
265
+ }
266
+ const path = `/accounts/${config.accountId}/access/apps`;
267
+ const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
268
+ const matches = applications.filter((application2) => application2.type === "warp" || application2.name === name);
269
+ if (matches.length > 1)
270
+ throw new Error(`Cloudflare WARP enrollment application ${name} is ambiguous`);
271
+ if (matches[0] && (matches[0].type !== "warp" || matches[0].name !== name)) {
272
+ throw new Error(`Cloudflare Access application ${name} is not the owned WARP enrollment application`);
273
+ }
274
+ const desired = {
275
+ name,
276
+ type: "warp",
277
+ policies: [{ id: config.policyId, precedence: 1 }]
278
+ };
279
+ const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
280
+ if (!application.id || application.type && application.type !== "warp") {
281
+ throw new Error("Cloudflare did not return the WARP enrollment application");
282
+ }
283
+ return { application, created: !matches[0] };
284
+ }
285
+ async function ensureCloudflareVirtualNetwork(config, fetcher = fetch) {
286
+ const name = config.name.trim();
287
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
288
+ throw new Error("Cloudflare VNET name is invalid");
289
+ const path = `/accounts/${config.accountId}/teamnet/virtual_networks`;
290
+ const networks = await cf(config, `${path}?per_page=1000`, {}, fetcher);
291
+ const matches = networks.filter((network) => !network.deleted_at && network.name === name);
292
+ if (matches.length > 1)
293
+ throw new Error(`Cloudflare VNET ${name} is ambiguous`);
294
+ if (matches[0])
295
+ return { virtualNetwork: matches[0], created: false };
296
+ const virtualNetwork = await cf(config, path, {
297
+ method: "POST",
298
+ body: JSON.stringify({ name, comment: config.comment.slice(0, 256), is_default_network: false })
299
+ }, fetcher);
300
+ if (!virtualNetwork.id || !/^[0-9a-f-]{36}$/i.test(virtualNetwork.id)) {
301
+ throw new Error("Cloudflare did not return the VNET id");
302
+ }
303
+ return { virtualNetwork, created: true };
304
+ }
305
+ async function ensureCloudflareWarpDevicePolicy(config, fetcher = fetch) {
306
+ const name = config.name.trim();
307
+ if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
308
+ throw new Error("Cloudflare WARP device profile name is invalid");
309
+ if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.serviceTokenId))
310
+ throw new Error("Cloudflare service-token id is invalid");
311
+ if (!/^[0-9a-f-]{36}$/i.test(config.virtualNetworkId))
312
+ throw new Error("Cloudflare VNET id is invalid");
313
+ const precedence = config.precedence ?? 100;
314
+ if (!Number.isInteger(precedence) || precedence < 1 || precedence > 999999) {
315
+ throw new Error("Cloudflare WARP device profile precedence is invalid");
316
+ }
317
+ const match = `identity.service_token_uuid == "${config.serviceTokenId}"`;
318
+ const listPath = `/accounts/${config.accountId}/devices/policies`;
319
+ const path = `/accounts/${config.accountId}/devices/policy`;
320
+ const policies = await cf(config, `${listPath}?per_page=1000`, {}, fetcher);
321
+ const matches = policies.filter((policy2) => policy2.name === name);
322
+ if (matches.length > 1)
323
+ throw new Error(`Cloudflare WARP device profile ${name} is ambiguous`);
324
+ if (matches[0]?.match && matches[0].match !== match) {
325
+ throw new Error(`Cloudflare WARP device profile ${name} belongs to another enrollment identity`);
326
+ }
327
+ const desired = {
328
+ name,
329
+ match,
330
+ precedence,
331
+ description: "ForgeZero non-interactive compute enrollment",
332
+ enabled: true,
333
+ allow_mode_switch: false,
334
+ allowed_to_leave: false,
335
+ auto_connect: 0,
336
+ switch_locked: true,
337
+ service_mode_v2: { mode: "warp" },
338
+ virtual_networks: { allowed: [config.virtualNetworkId], default: config.virtualNetworkId }
339
+ };
340
+ const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PATCH" : "POST", body: JSON.stringify(desired) }, fetcher);
341
+ if (!policy.id)
342
+ throw new Error("Cloudflare did not return the WARP device profile id");
343
+ return { policy, created: !matches[0] };
344
+ }
345
+ async function configureCloudflareWorkerAccessSecrets(config, fetcher = fetch) {
346
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(config.scriptName)) {
347
+ throw new Error("Cloudflare Worker script name is invalid");
348
+ }
349
+ await cf(config, `/accounts/${config.accountId}/workers/scripts/${config.scriptName}/secrets-bulk`, {
350
+ method: "PATCH",
351
+ body: JSON.stringify({
352
+ secrets: {
353
+ CF_ACCESS_CLIENT_ID: {
354
+ name: "CF_ACCESS_CLIENT_ID",
355
+ type: "secret_text",
356
+ text: config.credentials.clientId
357
+ },
358
+ CF_ACCESS_CLIENT_SECRET: {
359
+ name: "CF_ACCESS_CLIENT_SECRET",
360
+ type: "secret_text",
361
+ text: config.credentials.clientSecret
362
+ }
363
+ }
364
+ })
365
+ }, fetcher);
366
+ }
367
+ async function ensureCloudflareKvNamespace(config, fetcher = fetch) {
368
+ const title = config.title.trim();
369
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(title)) {
370
+ throw new Error("Cloudflare KV namespace title is invalid");
371
+ }
372
+ const path = `/accounts/${config.accountId}/storage/kv/namespaces`;
373
+ const namespaces = await cf(config, `${path}?per_page=1000`, {}, fetcher);
374
+ const matches = namespaces.filter((namespace2) => namespace2.title === title);
375
+ if (matches.length > 1)
376
+ throw new Error(`Cloudflare KV namespace ${title} is ambiguous`);
377
+ if (matches[0])
378
+ return { namespace: matches[0], created: false };
379
+ const namespace = await cf(config, path, {
380
+ method: "POST",
381
+ body: JSON.stringify({ title })
382
+ }, fetcher);
383
+ return { namespace, created: true };
384
+ }
385
+ async function ensureCloudflareTunnel(config, fetcher = fetch) {
386
+ const name = config.name.trim();
387
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
388
+ throw new Error("Cloudflare Tunnel name is invalid");
389
+ }
390
+ const path = `/accounts/${config.accountId}/cfd_tunnel`;
391
+ const tunnels = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
392
+ const matches = tunnels.filter((tunnel2) => tunnel2.name === name && !tunnel2.deleted_at);
393
+ if (matches.length > 1)
394
+ throw new Error(`Cloudflare Tunnel ${name} is ambiguous`);
395
+ const created = !matches[0];
396
+ const tunnel = matches[0] ?? await cf(config, path, {
397
+ method: "POST",
398
+ body: JSON.stringify({ name, config_src: "cloudflare" })
399
+ }, fetcher);
400
+ const connectorToken = await cf(config, `${path}/${encodeURIComponent(tunnel.id)}/token`, {}, fetcher);
401
+ if (!connectorToken || connectorToken.length > 16384) {
402
+ throw new Error("Cloudflare returned an invalid Tunnel connector token");
403
+ }
404
+ return { tunnel, connectorToken, created };
405
+ }
406
+
407
+ // src/cloudflare-bootstrap.ts
408
+ import { constants } from "fs";
409
+ import { chmod, lstat, mkdir, mkdtemp, open, rename, rm, stat, unlink } from "fs/promises";
410
+ import { dirname, join, resolve } from "path";
411
+ import { tmpdir } from "os";
412
+ import { randomUUID } from "crypto";
413
+ import { isIP as isIP2 } from "net";
414
+ var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
415
+ async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
416
+ const metadata = await handle.stat();
417
+ if (!metadata.isFile())
418
+ throw new Error(`${path} must be a regular file`);
419
+ if (metadata.nlink !== 1)
420
+ throw new Error(`${path} must not have multiple hard links`);
421
+ const uid = ownerUid();
422
+ if (uid !== undefined && uid !== 0 && metadata.uid !== uid)
423
+ throw new Error(`${path} must be owned by the current operator`);
424
+ if ((metadata.mode & 63) !== 0)
425
+ throw new Error(`${path} must not be accessible by group or other users`);
426
+ if ((metadata.mode & 256) === 0)
427
+ throw new Error(`${path} must be readable by its owner`);
428
+ if (metadata.size < 1 || metadata.size > maximumBytes)
429
+ throw new Error(`${path} has an invalid size`);
430
+ }
431
+ async function readOwnerOnlyFile(path, maximumBytes) {
432
+ const absolute = resolve(path);
433
+ let handle;
434
+ try {
435
+ handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
436
+ await assertOwnerOnlyHandle(absolute, handle, maximumBytes);
437
+ return await handle.readFile({ encoding: "utf8" });
438
+ } catch (cause) {
439
+ if (cause instanceof Error && cause.message.startsWith(absolute))
440
+ throw cause;
441
+ throw new Error(`cannot securely read owner-only file ${absolute}`);
442
+ } finally {
443
+ await handle?.close();
444
+ }
445
+ }
446
+ async function readOwnerApiToken(path) {
447
+ const token = (await readOwnerOnlyFile(path, 4096)).trim();
448
+ if (!/^[A-Za-z0-9._-]{40,80}$/.test(token)) {
449
+ throw new Error(`${resolve(path)} must contain exactly one Cloudflare API token`);
450
+ }
451
+ return token;
452
+ }
453
+ async function readCloudflareBootstrapTokens(files) {
454
+ const entries = await Promise.all([
455
+ ["apiToken", files.apiTokenFile],
456
+ ["tunnelApiToken", files.tunnelApiTokenFile],
457
+ ["dnsApiToken", files.dnsApiTokenFile],
458
+ ["kvApiToken", files.kvApiTokenFile],
459
+ ["accessApiToken", files.accessApiTokenFile],
460
+ ["workerApiToken", files.workerApiTokenFile]
461
+ ].map(async ([key, path]) => [key, path ? await readOwnerApiToken(path) : undefined]));
462
+ const tokens = Object.fromEntries(entries.filter(([, value]) => value !== undefined));
463
+ const unified = tokens.apiToken;
464
+ for (const key of ["tunnelApiToken", "dnsApiToken", "kvApiToken", "accessApiToken", "workerApiToken"]) {
465
+ if (!tokens[key] && !unified)
466
+ throw new Error(`Cloudflare ${key} file is required when --token-file is omitted`);
467
+ }
468
+ return tokens;
469
+ }
470
+ var validateId = (value, label) => {
471
+ const normalized = value.trim().toLowerCase();
472
+ if (!/^[a-f0-9]{32}$/.test(normalized))
473
+ throw new Error(`${label} must be a 32-character hexadecimal id`);
474
+ return normalized;
475
+ };
476
+ var validateName = (value, label, maximum, allowSpaces = true) => {
477
+ const normalized = value.trim();
478
+ const pattern = allowSpaces ? /^[A-Za-z0-9][A-Za-z0-9 ._-]*$/ : /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
479
+ if (!normalized || normalized.length > maximum || !pattern.test(normalized)) {
480
+ throw new Error(`${label} is invalid`);
481
+ }
482
+ return normalized;
483
+ };
484
+ var privateAddress = (value) => {
485
+ const address = value.trim().toLowerCase();
486
+ const family = isIP2(address);
487
+ if (family === 4) {
488
+ const [a, b] = address.split(".").map(Number);
489
+ if (a === 10 || a === 192 && b === 168 || a === 172 && b >= 16 && b <= 31)
490
+ return address;
491
+ }
492
+ if (family === 6) {
493
+ const first = Number.parseInt(address.split(":", 1)[0], 16);
494
+ if (Number.isFinite(first) && (first & 65024) === 64512)
495
+ return address;
496
+ }
497
+ throw new Error("Cloudflare private database address must be RFC 1918 IPv4 or unique-local IPv6");
498
+ };
499
+ function validateCloudflareBootstrapCoordinates(input) {
500
+ const nodeInputs = input.nodes?.length ? input.nodes : [{
501
+ nodeName: input.tunnelName,
502
+ hostname: input.hostname,
503
+ service: input.service,
504
+ tunnelName: input.tunnelName,
505
+ applicationName: input.applicationName
506
+ }];
507
+ if (nodeInputs.length < 1 || nodeInputs.length > 32) {
508
+ throw new Error("Cloudflare bootstrap requires between 1 and 32 explicit nodes");
509
+ }
510
+ const nodes = nodeInputs.map((node) => {
511
+ const nodeName = node.nodeName.trim().toLowerCase();
512
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName))
513
+ throw new Error("Cloudflare node name is invalid");
514
+ const hostname = node.hostname.trim().toLowerCase();
515
+ if (!/^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(hostname)) {
516
+ throw new Error("Cloudflare public node hostname is invalid");
517
+ }
518
+ const serviceUrl = new URL(node.service);
519
+ if (serviceUrl.protocol !== "http:" || !["localhost", "127.0.0.1", "[::1]"].includes(serviceUrl.hostname) || !serviceUrl.port || serviceUrl.pathname !== "/" || serviceUrl.search || serviceUrl.hash) {
520
+ throw new Error("Cloudflare Tunnel service must be an explicit loopback HTTP port");
521
+ }
522
+ return {
523
+ nodeName,
524
+ hostname,
525
+ service: serviceUrl.toString().replace(/\/$/, ""),
526
+ tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name", 100, false),
527
+ applicationName: validateName(node.applicationName, "Cloudflare Access application name", 100),
528
+ ...node.privateAddress ? { privateAddress: privateAddress(node.privateAddress) } : {}
529
+ };
530
+ });
531
+ for (const [label, values] of [
532
+ ["node name", nodes.map(({ nodeName }) => nodeName)],
533
+ ["hostname", nodes.map(({ hostname }) => hostname)],
534
+ ["Tunnel name", nodes.map(({ tunnelName }) => tunnelName)],
535
+ ["Access application name", nodes.map(({ applicationName }) => applicationName)]
536
+ ]) {
537
+ if (new Set(values).size !== values.length)
538
+ throw new Error(`Cloudflare fleet ${label} must be unique`);
539
+ }
540
+ const first = nodes[0];
541
+ const workerScriptName = input.workerScriptName.trim();
542
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(workerScriptName))
543
+ throw new Error("Cloudflare Worker script name is invalid");
544
+ const workerCompatibilityDate = input.workerCompatibilityDate.trim();
545
+ if (!/^20\d{2}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/.test(workerCompatibilityDate)) {
546
+ throw new Error("Cloudflare Worker compatibility date is invalid");
547
+ }
548
+ const publicDomains = [...new Set(input.publicDomains.map((domain) => domain.trim().toLowerCase()))];
549
+ 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))) {
550
+ throw new Error("Cloudflare Worker public domains are invalid or include the private origin hostname");
551
+ }
552
+ const workerDirectory = resolve(input.workerDirectory);
553
+ const workerMain = input.workerMain.trim();
554
+ if (!workerMain || workerMain.startsWith("/") || workerMain.split(/[\\/]/).includes("..")) {
555
+ throw new Error("Cloudflare Worker main must be a project-relative path");
556
+ }
557
+ const runtimeTokenNamePrefix = input.runtimeTokenNamePrefix.trim();
558
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(runtimeTokenNamePrefix)) {
559
+ throw new Error("Cloudflare runtime-token name prefix is invalid");
560
+ }
561
+ if (input.createPrivateNetworkRuntimeToken && !input.createRuntimeTokens) {
562
+ throw new Error("private-network runtime token requires runtime-token creation");
563
+ }
564
+ const privateNetwork = input.privateNetwork ? {
565
+ warpOrganization: validateName(input.privateNetwork.warpOrganization, "Cloudflare WARP organization", 63, false).toLowerCase(),
566
+ virtualNetworkName: validateName(input.privateNetwork.virtualNetworkName, "Cloudflare VNET name", 100),
567
+ deviceProfileName: validateName(input.privateNetwork.deviceProfileName, "Cloudflare WARP device profile name", 100),
568
+ enrollmentApplicationName: validateName(input.privateNetwork.enrollmentApplicationName, "Cloudflare WARP enrollment application name", 100),
569
+ ...input.privateNetwork.deviceProfilePrecedence !== undefined ? { deviceProfilePrecedence: input.privateNetwork.deviceProfilePrecedence } : {}
570
+ } : undefined;
571
+ if (privateNetwork && (!input.createPrivateNetworkRuntimeToken || nodes.every((node) => !node.privateAddress))) {
572
+ throw new Error("Cloudflare private network requires its runtime token and at least one DB node private address");
573
+ }
574
+ if (!privateNetwork && nodes.some((node) => node.privateAddress)) {
575
+ throw new Error("Cloudflare node private addresses require privateNetwork coordinates");
576
+ }
577
+ return {
578
+ accountId: validateId(input.accountId, "Cloudflare account id"),
579
+ zoneId: validateId(input.zoneId, "Cloudflare zone id"),
580
+ hostname: first.hostname,
581
+ service: first.service,
582
+ tunnelName: first.tunnelName,
583
+ kvNamespaceTitle: validateName(input.kvNamespaceTitle, "Cloudflare KV namespace title", 128, false),
584
+ workerScriptName,
585
+ serviceTokenName: validateName(input.serviceTokenName, "Cloudflare Access service-token name", 100),
586
+ policyName: validateName(input.policyName, "Cloudflare Access policy name", 100),
587
+ applicationName: first.applicationName,
588
+ workerDirectory,
589
+ workerMain,
590
+ workerCompatibilityDate,
591
+ publicDomains,
592
+ createRuntimeTokens: input.createRuntimeTokens,
593
+ createPrivateNetworkRuntimeToken: input.createPrivateNetworkRuntimeToken,
594
+ runtimeTokenNamePrefix,
595
+ nodes,
596
+ ...privateNetwork ? { privateNetwork } : {}
597
+ };
598
+ }
599
+ function planCloudflareBootstrap(input, outputPath) {
600
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
601
+ return {
602
+ format: 1,
603
+ kind: "forgezero-cloudflare-bootstrap-plan",
604
+ mode: "attended-token-file",
605
+ outputFile: resolve(outputPath),
606
+ coordinates,
607
+ operations: [
608
+ "create or reuse one Workers KV namespace",
609
+ "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
610
+ ...coordinates.createRuntimeTokens ? [
611
+ "create exact-account least-privilege runtime tokens and checkpoint their one-time values"
612
+ ] : [],
613
+ "deploy the shared Worker once with the created NODES binding and stable custom domains",
614
+ "create or reuse one shared Access service token/policy and one self-hosted application per node",
615
+ ...coordinates.privateNetwork ? [
616
+ "create or reuse the VNET, WARP enrollment application, locked service-token device profile and exact DB host routes"
617
+ ] : [],
618
+ "write the Access client id and secret to the existing Worker as encrypted secrets",
619
+ "reconcile each node ingress rule and proxied CNAME only after all Access applications are ready"
620
+ ],
621
+ secrets: [
622
+ "API tokens are read only from owner-only files and are never written to output",
623
+ "the output contains connector, Access and requested runtime credentials and is atomically written with mode 0600",
624
+ "the normal API process does not receive or import the management token files"
625
+ ]
626
+ };
627
+ }
628
+ var defaultWorkerCommandRunner = async ({ command, cwd, env }) => {
629
+ const child = Bun.spawn([...command], {
630
+ cwd,
631
+ env: { ...env },
632
+ stdin: "ignore",
633
+ stdout: "pipe",
634
+ stderr: "pipe"
635
+ });
636
+ const [exitCode, stdout, stderr] = await Promise.all([
637
+ child.exited,
638
+ new Response(child.stdout).text(),
639
+ new Response(child.stderr).text()
640
+ ]);
641
+ return { exitCode, stdout, stderr };
642
+ };
643
+ var inheritedWorkerEnvironment = () => {
644
+ const allowed = [
645
+ "PATH",
646
+ "HOME",
647
+ "TMPDIR",
648
+ "XDG_CONFIG_HOME",
649
+ "XDG_CACHE_HOME",
650
+ "SSL_CERT_FILE",
651
+ "SSL_CERT_DIR",
652
+ "NODE_EXTRA_CA_CERTS",
653
+ "HTTPS_PROXY",
654
+ "HTTP_PROXY",
655
+ "NO_PROXY"
656
+ ];
657
+ return Object.fromEntries(allowed.flatMap((key) => process.env[key] ? [[key, process.env[key]]] : []));
658
+ };
659
+ var redact = (text, secrets) => {
660
+ let safe = text.slice(0, 4096);
661
+ for (const secret of secrets)
662
+ if (secret)
663
+ safe = safe.split(secret).join("[REDACTED]");
664
+ return safe.trim();
665
+ };
666
+ async function deployCloudflareWorker(coordinates, kvNamespaceId, apiToken, runner = defaultWorkerCommandRunner) {
667
+ const validated = validateCloudflareBootstrapCoordinates(coordinates);
668
+ const workerDirectoryMetadata = await stat(validated.workerDirectory);
669
+ if (!workerDirectoryMetadata.isDirectory())
670
+ throw new Error("Cloudflare Worker directory is not a directory");
671
+ const workerMain = resolve(validated.workerDirectory, validated.workerMain);
672
+ const workerMainMetadata = await stat(workerMain);
673
+ if (!workerMainMetadata.isFile())
674
+ throw new Error("Cloudflare Worker main is not a regular file");
675
+ const wrangler = resolve(validated.workerDirectory, "node_modules/.bin/wrangler");
676
+ const wranglerMetadata = await stat(wrangler);
677
+ if (!wranglerMetadata.isFile())
678
+ throw new Error("Cloudflare Wrangler is not installed in the Worker project");
679
+ if (!/^[a-f0-9]{32}$/.test(kvNamespaceId))
680
+ throw new Error("Cloudflare KV namespace id is invalid");
681
+ const temporaryDirectory = await mkdtemp(join(tmpdir(), "fz-wrangler-"));
682
+ await chmod(temporaryDirectory, 448);
683
+ const configurationPath = join(temporaryDirectory, "wrangler.json");
684
+ try {
685
+ const handle = await open(configurationPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 384);
686
+ try {
687
+ await handle.writeFile(`${JSON.stringify({
688
+ name: validated.workerScriptName,
689
+ main: workerMain,
690
+ compatibility_date: validated.workerCompatibilityDate,
691
+ workers_dev: false,
692
+ routes: validated.publicDomains.map((pattern) => ({ pattern, custom_domain: true })),
693
+ observability: { enabled: true },
694
+ kv_namespaces: [{ binding: "NODES", id: kvNamespaceId }]
695
+ }, null, 2)}
696
+ `);
697
+ await handle.sync();
698
+ } finally {
699
+ await handle.close();
700
+ }
701
+ const result = await runner({
702
+ command: [wrangler, "deploy", "--config", configurationPath],
703
+ cwd: validated.workerDirectory,
704
+ env: {
705
+ ...inheritedWorkerEnvironment(),
706
+ XDG_CONFIG_HOME: temporaryDirectory,
707
+ XDG_CACHE_HOME: temporaryDirectory,
708
+ WRANGLER_LOG_PATH: join(temporaryDirectory, "wrangler.log"),
709
+ CLOUDFLARE_ACCOUNT_ID: validated.accountId,
710
+ CLOUDFLARE_API_TOKEN: apiToken,
711
+ WRANGLER_SEND_METRICS: "false"
712
+ }
713
+ });
714
+ if (result.exitCode !== 0) {
715
+ const detail = redact(result.stderr || result.stdout || "no Wrangler diagnostic", [apiToken]);
716
+ throw new Error(`Cloudflare Worker deployment failed with exit ${result.exitCode}: ${detail}`);
717
+ }
718
+ } finally {
719
+ await rm(temporaryDirectory, { recursive: true, force: true });
720
+ }
721
+ }
722
+ async function readExistingOutput(path) {
723
+ try {
724
+ await lstat(path);
725
+ } catch (cause) {
726
+ if (cause.code === "ENOENT")
727
+ return;
728
+ throw cause;
729
+ }
730
+ const text = await readOwnerOnlyFile(path, 1048576);
731
+ let output;
732
+ try {
733
+ output = JSON.parse(text);
734
+ } catch {
735
+ throw new Error(`${resolve(path)} is not valid bootstrap JSON`);
736
+ }
737
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !output.resources) {
738
+ throw new Error(`${resolve(path)} is not a ForgeZero Cloudflare bootstrap output`);
739
+ }
740
+ return output;
741
+ }
742
+ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
743
+ const output = await readExistingOutput(resolve(checkpointPath));
744
+ if (!output || output.phase !== "complete") {
745
+ throw new Error("Cloudflare connector handoff requires a completed bootstrap checkpoint");
746
+ }
747
+ const normalizedNodeName = nodeName.trim().toLowerCase();
748
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalizedNodeName)) {
749
+ throw new Error("Cloudflare connector handoff node name is invalid");
750
+ }
751
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
752
+ const expected = coordinates.nodes.find((node) => node.nodeName === normalizedNodeName);
753
+ const matches = output.resources.nodes?.filter((node) => node.nodeName === normalizedNodeName) ?? [];
754
+ if (!expected || matches.length !== 1) {
755
+ throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
756
+ }
757
+ const resource = matches[0];
758
+ 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)) {
759
+ throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
760
+ }
761
+ return {
762
+ nodeName: resource.nodeName,
763
+ hostname: resource.hostname,
764
+ service: resource.service,
765
+ tunnelId: resource.tunnelId,
766
+ connectorToken: resource.connectorToken
767
+ };
768
+ }
769
+ async function readCloudflareHostHandoff(checkpointPath, nodeName) {
770
+ const connector = await readCloudflareConnectorHandoff(checkpointPath, nodeName);
771
+ const output = await readExistingOutput(resolve(checkpointPath));
772
+ const kv = output?.resources.runtimeTokens?.kv;
773
+ 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)) {
774
+ throw new Error("Cloudflare host handoff is missing the exact-account KV runtime capability");
775
+ }
776
+ const network = output.resources.runtimeTokens?.privateNetwork?.value;
777
+ if (network !== undefined && !/^[A-Za-z0-9._-]{40,80}$/.test(network)) {
778
+ throw new Error("Cloudflare host handoff private-network capability is malformed");
779
+ }
780
+ const privateNetwork = output.resources.privateNetwork;
781
+ const access = output.resources.access;
782
+ if (Boolean(privateNetwork) !== Boolean(network)) {
783
+ throw new Error("Cloudflare host handoff private-network resources and capability disagree");
784
+ }
785
+ 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)) {
786
+ throw new Error("Cloudflare host handoff WARP enrollment is malformed");
787
+ }
788
+ return {
789
+ ...connector,
790
+ accountId: output.coordinates.accountId,
791
+ zoneId: output.coordinates.zoneId,
792
+ kvNamespaceId: output.resources.kvNamespaceId,
793
+ kvRuntimeToken: kv.value,
794
+ ...network ? { privateNetworkRuntimeToken: network } : {},
795
+ ...privateNetwork && access ? { warp: {
796
+ organization: privateNetwork.warpOrganization,
797
+ clientId: access.clientId,
798
+ clientSecret: access.clientSecret,
799
+ virtualNetworkId: privateNetwork.virtualNetworkId,
800
+ deviceProfileId: privateNetwork.deviceProfileId
801
+ } } : {}
802
+ };
803
+ }
804
+ async function prepareOwnerOutputDirectory(absolutePath) {
805
+ const directory = dirname(absolutePath);
806
+ await mkdir(directory, { recursive: true, mode: 448 });
807
+ const directoryMetadata = await stat(directory);
808
+ const uid = ownerUid();
809
+ if (!directoryMetadata.isDirectory() || uid !== undefined && directoryMetadata.uid !== uid || (directoryMetadata.mode & 18) !== 0) {
810
+ throw new Error(`bootstrap output directory ${directory} must be operator-owned and not group/other writable`);
811
+ }
812
+ return directory;
813
+ }
814
+ async function writeOwnerBootstrapOutput(path, output) {
815
+ const absolute = resolve(path);
816
+ const directory = await prepareOwnerOutputDirectory(absolute);
817
+ const temporary = `${absolute}.${randomUUID()}.tmp`;
818
+ let handle;
819
+ try {
820
+ handle = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 384);
821
+ await handle.writeFile(`${JSON.stringify(output, null, 2)}
822
+ `, { encoding: "utf8" });
823
+ await handle.sync();
824
+ await handle.close();
825
+ handle = undefined;
826
+ await rename(temporary, absolute);
827
+ await chmod(absolute, 384);
828
+ const directoryHandle = await open(directory, constants.O_RDONLY);
829
+ try {
830
+ await directoryHandle.sync();
831
+ } finally {
832
+ await directoryHandle.close();
833
+ }
834
+ } finally {
835
+ await handle?.close();
836
+ await unlink(temporary).catch((cause) => {
837
+ if (cause.code !== "ENOENT")
838
+ throw cause;
839
+ });
840
+ }
841
+ }
842
+ var tokenFor = (tokens, key) => {
843
+ const token = tokens[key]?.trim() || tokens.apiToken?.trim();
844
+ if (!token)
845
+ throw new Error(`Cloudflare ${key} is not configured`);
846
+ return token;
847
+ };
848
+ var initialManagementToken = (tokens) => {
849
+ const token = tokens.apiToken?.trim();
850
+ if (!token) {
851
+ throw new Error("initial Cloudflare --token-file is required to create account-owned runtime tokens");
852
+ }
853
+ return token;
854
+ };
855
+ var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
856
+ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch, workerRunner = defaultWorkerCommandRunner) {
857
+ const coordinates = validateCloudflareBootstrapCoordinates(input);
858
+ const absoluteOutput = resolve(outputPath);
859
+ const existing = await readExistingOutput(absoluteOutput);
860
+ if (existing && !sameCoordinates(existing.coordinates, coordinates)) {
861
+ throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
862
+ }
863
+ await prepareOwnerOutputDirectory(absoluteOutput);
864
+ const namespace = await ensureCloudflareKvNamespace({
865
+ accountId: coordinates.accountId,
866
+ title: coordinates.kvNamespaceTitle,
867
+ apiToken: tokenFor(tokens, "kvApiToken")
868
+ }, fetcher);
869
+ const nodeResources = [];
870
+ const createdNodes = [];
871
+ let resources;
872
+ for (const node of coordinates.nodes) {
873
+ const checkpointed = existing?.resources.nodes?.find(({ nodeName }) => nodeName === node.nodeName);
874
+ let created = false;
875
+ if (checkpointed) {
876
+ nodeResources.push(checkpointed);
877
+ } else {
878
+ const tunnel = await ensureCloudflareTunnel({
879
+ accountId: coordinates.accountId,
880
+ name: node.tunnelName,
881
+ apiToken: tokenFor(tokens, "tunnelApiToken")
882
+ }, fetcher);
883
+ created = tunnel.created;
884
+ nodeResources.push({
885
+ nodeName: node.nodeName,
886
+ hostname: node.hostname,
887
+ service: node.service,
888
+ tunnelName: node.tunnelName,
889
+ tunnelId: tunnel.tunnel.id,
890
+ connectorToken: tunnel.connectorToken
891
+ });
892
+ }
893
+ createdNodes.push({ nodeName: node.nodeName, tunnel: created, application: false });
894
+ const firstNode = nodeResources[0];
895
+ resources = {
896
+ tunnelId: firstNode.tunnelId,
897
+ kvNamespaceId: namespace.namespace.id,
898
+ hostname: firstNode.hostname,
899
+ service: firstNode.service,
900
+ connectorToken: firstNode.connectorToken,
901
+ nodes: [...nodeResources],
902
+ ...existing?.resources.access ? { access: existing.resources.access } : {},
903
+ ...existing?.resources.runtimeTokens ? { runtimeTokens: existing.resources.runtimeTokens } : {},
904
+ ...existing?.resources.worker ? { worker: existing.resources.worker } : {},
905
+ ...existing?.resources.privateNetwork ? { privateNetwork: existing.resources.privateNetwork } : {}
906
+ };
907
+ await writeOwnerBootstrapOutput(absoluteOutput, {
908
+ format: 1,
909
+ kind: "forgezero-cloudflare-bootstrap",
910
+ phase: resources.access ? "access-token-provisioned" : "edge-resources-provisioned",
911
+ updatedAt: new Date().toISOString(),
912
+ coordinates,
913
+ resources
914
+ });
915
+ }
916
+ if (!resources)
917
+ throw new Error("Cloudflare fleet has no nodes");
918
+ if (coordinates.createRuntimeTokens && !resources.runtimeTokens) {
919
+ resources = {
920
+ ...resources,
921
+ runtimeTokens: { kv: await createCloudflareAccountRuntimeToken({
922
+ accountId: coordinates.accountId,
923
+ name: `${coordinates.runtimeTokenNamePrefix}-kv-runtime`,
924
+ permissionNames: ["Workers KV Storage Write"],
925
+ apiToken: initialManagementToken(tokens)
926
+ }, fetcher) }
927
+ };
928
+ await writeOwnerBootstrapOutput(absoluteOutput, {
929
+ format: 1,
930
+ kind: "forgezero-cloudflare-bootstrap",
931
+ phase: "runtime-tokens-created",
932
+ updatedAt: new Date().toISOString(),
933
+ coordinates,
934
+ resources
935
+ });
936
+ }
937
+ if (coordinates.createPrivateNetworkRuntimeToken && resources.runtimeTokens && !resources.runtimeTokens.privateNetwork) {
938
+ resources = {
939
+ ...resources,
940
+ runtimeTokens: {
941
+ ...resources.runtimeTokens,
942
+ privateNetwork: await createCloudflareAccountRuntimeToken({
943
+ accountId: coordinates.accountId,
944
+ name: `${coordinates.runtimeTokenNamePrefix}-private-network-runtime`,
945
+ permissionNames: ["Cloudflare One Networks Write", "Zero Trust Write"],
946
+ apiToken: initialManagementToken(tokens)
947
+ }, fetcher)
948
+ }
949
+ };
950
+ await writeOwnerBootstrapOutput(absoluteOutput, {
951
+ format: 1,
952
+ kind: "forgezero-cloudflare-bootstrap",
953
+ phase: "runtime-tokens-created",
954
+ updatedAt: new Date().toISOString(),
955
+ coordinates,
956
+ resources
957
+ });
958
+ }
959
+ if (!resources.worker) {
960
+ await deployCloudflareWorker(coordinates, namespace.namespace.id, tokenFor(tokens, "workerApiToken"), workerRunner);
961
+ resources = {
962
+ ...resources,
963
+ worker: {
964
+ scriptName: coordinates.workerScriptName,
965
+ publicDomains: coordinates.publicDomains,
966
+ deployed: true
967
+ }
968
+ };
969
+ await writeOwnerBootstrapOutput(absoluteOutput, {
970
+ format: 1,
971
+ kind: "forgezero-cloudflare-bootstrap",
972
+ phase: "worker-deployed",
973
+ updatedAt: new Date().toISOString(),
974
+ coordinates,
975
+ resources
976
+ });
977
+ }
978
+ const serviceToken = await ensureCloudflareAccessServiceToken({
979
+ accountId: coordinates.accountId,
980
+ name: coordinates.serviceTokenName,
981
+ apiToken: tokenFor(tokens, "accessApiToken"),
982
+ existing: resources.access
983
+ }, fetcher);
984
+ resources = { ...resources, access: serviceToken.credentials };
985
+ await writeOwnerBootstrapOutput(absoluteOutput, {
986
+ format: 1,
987
+ kind: "forgezero-cloudflare-bootstrap",
988
+ phase: "access-token-provisioned",
989
+ updatedAt: new Date().toISOString(),
990
+ coordinates,
991
+ resources
992
+ });
993
+ const policy = await ensureCloudflareAccessPolicy({
994
+ accountId: coordinates.accountId,
995
+ name: coordinates.policyName,
996
+ serviceTokenId: serviceToken.credentials.tokenId,
997
+ apiToken: tokenFor(tokens, "accessApiToken")
998
+ }, fetcher);
999
+ if (!policy.policy.id)
1000
+ throw new Error("Cloudflare did not return the Access policy id");
1001
+ resources = {
1002
+ ...resources,
1003
+ access: { ...serviceToken.credentials, policyId: policy.policy.id }
1004
+ };
1005
+ await writeOwnerBootstrapOutput(absoluteOutput, {
1006
+ format: 1,
1007
+ kind: "forgezero-cloudflare-bootstrap",
1008
+ phase: "access-token-provisioned",
1009
+ updatedAt: new Date().toISOString(),
1010
+ coordinates,
1011
+ resources
1012
+ });
1013
+ let privateNetworkCreated = false;
1014
+ if (coordinates.privateNetwork && !resources.privateNetwork) {
1015
+ const managementToken = initialManagementToken(tokens);
1016
+ const virtualNetwork = await ensureCloudflareVirtualNetwork({
1017
+ accountId: coordinates.accountId,
1018
+ name: coordinates.privateNetwork.virtualNetworkName,
1019
+ comment: "ForgeZero private database network",
1020
+ apiToken: managementToken
1021
+ }, fetcher);
1022
+ const enrollment = await ensureCloudflareWarpEnrollmentApplication({
1023
+ accountId: coordinates.accountId,
1024
+ name: coordinates.privateNetwork.enrollmentApplicationName,
1025
+ policyId: policy.policy.id,
1026
+ apiToken: tokenFor(tokens, "accessApiToken")
1027
+ }, fetcher);
1028
+ const deviceProfile = await ensureCloudflareWarpDevicePolicy({
1029
+ accountId: coordinates.accountId,
1030
+ name: coordinates.privateNetwork.deviceProfileName,
1031
+ serviceTokenId: serviceToken.credentials.tokenId,
1032
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
1033
+ precedence: coordinates.privateNetwork.deviceProfilePrecedence,
1034
+ apiToken: managementToken
1035
+ }, fetcher);
1036
+ const routes = [];
1037
+ for (const node of coordinates.nodes.filter((item) => item.privateAddress)) {
1038
+ const resource = resources.nodes.find((item) => item.nodeName === node.nodeName);
1039
+ const route = await ensureCloudflarePrivateDatabaseRoute({
1040
+ accountId: coordinates.accountId,
1041
+ tunnelId: resource.tunnelId,
1042
+ privateAddress: node.privateAddress,
1043
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
1044
+ comment: `ForgeZero ${node.nodeName} database`,
1045
+ apiToken: managementToken
1046
+ }, fetcher);
1047
+ await ensureCloudflareWarpDatabaseInclude({
1048
+ accountId: coordinates.accountId,
1049
+ policyId: deviceProfile.policy.id,
1050
+ privateAddress: node.privateAddress,
1051
+ description: `ForgeZero ${node.nodeName} database`,
1052
+ apiToken: managementToken
1053
+ }, fetcher);
1054
+ routes.push({ nodeName: node.nodeName, routeId: route.route.id, privateAddress: node.privateAddress });
1055
+ }
1056
+ resources = {
1057
+ ...resources,
1058
+ privateNetwork: {
1059
+ warpOrganization: coordinates.privateNetwork.warpOrganization,
1060
+ virtualNetworkId: virtualNetwork.virtualNetwork.id,
1061
+ deviceProfileId: deviceProfile.policy.id,
1062
+ enrollmentApplicationId: enrollment.application.id,
1063
+ routes
1064
+ }
1065
+ };
1066
+ privateNetworkCreated = virtualNetwork.created || enrollment.created || deviceProfile.created || routes.length > 0;
1067
+ await writeOwnerBootstrapOutput(absoluteOutput, {
1068
+ format: 1,
1069
+ kind: "forgezero-cloudflare-bootstrap",
1070
+ phase: "access-token-provisioned",
1071
+ updatedAt: new Date().toISOString(),
1072
+ coordinates,
1073
+ resources
1074
+ });
1075
+ }
1076
+ for (const node of coordinates.nodes) {
1077
+ const application = await ensureCloudflareAccessApplication({
1078
+ accountId: coordinates.accountId,
1079
+ name: node.applicationName,
1080
+ hostname: node.hostname,
1081
+ policyId: policy.policy.id,
1082
+ apiToken: tokenFor(tokens, "accessApiToken")
1083
+ }, fetcher);
1084
+ if (!application.application.id)
1085
+ throw new Error(`Cloudflare did not return the Access application id for ${node.nodeName}`);
1086
+ resources = {
1087
+ ...resources,
1088
+ nodes: resources.nodes.map((resource) => resource.nodeName === node.nodeName ? { ...resource, applicationId: application.application.id } : resource),
1089
+ access: {
1090
+ ...resources.access,
1091
+ ...node.nodeName === coordinates.nodes[0].nodeName ? { applicationId: application.application.id } : {}
1092
+ }
1093
+ };
1094
+ const createdNode = createdNodes.find(({ nodeName }) => nodeName === node.nodeName);
1095
+ createdNode.application = application.created;
1096
+ await writeOwnerBootstrapOutput(absoluteOutput, {
1097
+ format: 1,
1098
+ kind: "forgezero-cloudflare-bootstrap",
1099
+ phase: "access-token-provisioned",
1100
+ updatedAt: new Date().toISOString(),
1101
+ coordinates,
1102
+ resources
1103
+ });
1104
+ }
1105
+ await configureCloudflareWorkerAccessSecrets({
1106
+ accountId: coordinates.accountId,
1107
+ scriptName: coordinates.workerScriptName,
1108
+ credentials: serviceToken.credentials,
1109
+ apiToken: tokenFor(tokens, "workerApiToken")
1110
+ }, fetcher);
1111
+ for (const node of resources.nodes) {
1112
+ await configureCloudflareEdge({
1113
+ accountId: coordinates.accountId,
1114
+ zoneId: coordinates.zoneId,
1115
+ tunnelId: node.tunnelId,
1116
+ hostname: node.hostname,
1117
+ service: node.service,
1118
+ apiToken: tokenFor(tokens, "tunnelApiToken"),
1119
+ tunnelApiToken: tokenFor(tokens, "tunnelApiToken"),
1120
+ dnsApiToken: tokenFor(tokens, "dnsApiToken")
1121
+ }, fetcher);
1122
+ }
1123
+ const output = {
1124
+ format: 1,
1125
+ kind: "forgezero-cloudflare-bootstrap",
1126
+ phase: "complete",
1127
+ updatedAt: new Date().toISOString(),
1128
+ coordinates,
1129
+ resources,
1130
+ created: {
1131
+ tunnel: createdNodes.some(({ tunnel }) => tunnel),
1132
+ kvNamespace: namespace.created,
1133
+ serviceToken: serviceToken.created,
1134
+ policy: policy.created,
1135
+ application: createdNodes.some(({ application }) => application),
1136
+ privateNetwork: privateNetworkCreated,
1137
+ workerDeployed: true,
1138
+ nodes: createdNodes
1139
+ }
1140
+ };
1141
+ await writeOwnerBootstrapOutput(absoluteOutput, output);
1142
+ return output;
1143
+ }
1144
+ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
1145
+ const plan = planCloudflareBootstrap(request.coordinates, request.checkpointPath);
1146
+ if (request.mode === "plan") {
1147
+ return {
1148
+ format: 1,
1149
+ kind: "forgezero-cloudflare-bootstrap-evidence",
1150
+ phase: "planned",
1151
+ checkpointFile: plan.outputFile,
1152
+ workerScriptName: plan.coordinates.workerScriptName,
1153
+ publicDomains: plan.coordinates.publicDomains,
1154
+ nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
1155
+ };
1156
+ }
1157
+ if (!request.tokenFiles || !Object.values(request.tokenFiles).some(Boolean)) {
1158
+ throw new Error("Cloudflare apply requires owner-only management token file paths");
1159
+ }
1160
+ if (plan.coordinates.createRuntimeTokens && !request.tokenFiles.apiTokenFile) {
1161
+ throw new Error("Cloudflare runtime-token creation requires the initial management token file");
1162
+ }
1163
+ const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
1164
+ const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch, dependencies.workerRunner);
1165
+ return {
1166
+ format: 1,
1167
+ kind: "forgezero-cloudflare-bootstrap-evidence",
1168
+ phase: "complete",
1169
+ checkpointFile: plan.outputFile,
1170
+ kvNamespaceId: output.resources.kvNamespaceId,
1171
+ workerScriptName: output.coordinates.workerScriptName,
1172
+ publicDomains: output.resources.worker?.publicDomains ?? output.coordinates.publicDomains,
1173
+ runtimeTokenIds: {
1174
+ kv: output.resources.runtimeTokens?.kv.id,
1175
+ privateNetwork: output.resources.runtimeTokens?.privateNetwork?.id
1176
+ },
1177
+ nodes: output.resources.nodes.map(({ nodeName, hostname, tunnelId, applicationId }) => ({
1178
+ nodeName,
1179
+ hostname,
1180
+ tunnelId,
1181
+ applicationId
1182
+ }))
1183
+ };
1184
+ }
1185
+
1186
+ // src/bootstrap.ts
1187
+ import { createHmac, randomBytes } from "crypto";
1188
+ import {
1189
+ chmodSync,
1190
+ existsSync,
1191
+ lstatSync,
1192
+ mkdirSync,
1193
+ readFileSync,
1194
+ renameSync,
1195
+ rmSync,
1196
+ writeFileSync
1197
+ } from "fs";
1198
+ import { dirname as dirname2 } from "path";
1199
+ import { fileURLToPath } from "url";
1200
+
1201
+ // src/agent-update-helper.ts
1202
+ import { DEFAULT_SOCKET } from "@forgezero/vault";
1203
+
1204
+ // src/agent-update.ts
1205
+ var DEFAULT_AGENT_RELEASE_ROOT = "/opt/forgezero/agent";
1206
+ var DEFAULT_AGENT_UPDATE_SOCKET = "/run/forgezero-update/helper.sock";
1207
+ var MAX_AGENT_TARBALL_BYTES = 32 * 1024 * 1024;
1208
+
1209
+ // src/agent-update-helper.ts
1210
+ var AGENT_UPDATE_GROUP = "forgezero-update";
1211
+ var AGENT_UPDATE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-agent-update-helper.service";
1212
+ var MAX_REQUEST_BYTES = 8 * 1024;
1213
+ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1214
+ var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1215
+
1216
+ // src/version.ts
1217
+ var VERSION = "0.1.36";
1218
+
1219
+ // src/software.ts
1220
+ var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
1221
+ var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
1222
+ var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
1223
+ var UBUNTU_2604_X64 = [
1224
+ {
1225
+ requirement: { id: "bun", version: "1.3.14" },
1226
+ check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
1227
+ install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL https://bun.sh/install -o "$tmp/install"; ` + `echo "${BUN_INSTALLER_SHA256} $tmp/install" | sha256sum -c -; ` + `BUN_INSTALL="$tmp/bun" BUN_VERSION=1.3.14 bash "$tmp/install" >/dev/null; ` + `install -d -m 0755 /usr/local/lib/forgezero/runtime; ` + `install -m 0755 "$tmp/bun/bin/bun" /usr/local/lib/forgezero/runtime/bun.next; ` + `mv -Tf /usr/local/lib/forgezero/runtime/bun.next /usr/local/lib/forgezero/runtime/bun; ` + `ln -sfn /usr/local/lib/forgezero/runtime/bun /usr/local/bin/bun`
1228
+ },
1229
+ {
1230
+ requirement: { id: "nginx", version: "ubuntu-26.04" },
1231
+ check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
1232
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
1233
+ },
1234
+ {
1235
+ requirement: { id: "arangodb", version: "3.11.14" },
1236
+ check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14'`,
1237
+ install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://download.arangodb.com/arangodb311/DEBIAN/amd64/arangodb3_3.11.14-1_amd64.deb' -o "$tmp/arangodb.deb"; ` + `echo "${ARANGO_SHA256} $tmp/arangodb.deb" | sha256sum -c -; ` + `DEBIAN_FRONTEND=noninteractive dpkg -i "$tmp/arangodb.deb" >/dev/null 2>&1 || ` + `DEBIAN_FRONTEND=noninteractive apt-get -y -f install`
1238
+ },
1239
+ {
1240
+ requirement: { id: "cloudflared", version: "2026.7.3" },
1241
+ check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
1242
+ install: `tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT; ` + `curl -fsSL 'https://github.com/cloudflare/cloudflared/releases/download/2026.7.3/cloudflared-linux-amd64' -o "$tmp/cloudflared"; ` + `echo "${CLOUDFLARED_SHA256} $tmp/cloudflared" | sha256sum -c -; ` + `install -m 0755 "$tmp/cloudflared" /usr/local/bin/cloudflared`
1243
+ },
1244
+ {
1245
+ requirement: { id: "ufw", version: "ubuntu-26.04" },
1246
+ check: "command -v ufw >/dev/null",
1247
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
1248
+ },
1249
+ {
1250
+ requirement: { id: "openssh-client", version: "ubuntu-26.04" },
1251
+ check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
1252
+ install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
1253
+ }
1254
+ ];
1255
+
1256
+ // src/software-helper.ts
1257
+ var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
1258
+ var SOFTWARE_HELPER_GROUP = "forgezero-software";
1259
+ var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
1260
+ var MAX_REQUEST_BYTES2 = 8 * 1024;
1261
+
1262
+ // src/egress-policy.ts
1263
+ var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
1264
+ var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
1265
+ var BLOCKED_IPV4 = [
1266
+ "0.0.0.0/8",
1267
+ "10.0.0.0/8",
1268
+ "100.64.0.0/10",
1269
+ "127.0.0.0/8",
1270
+ "168.63.129.16/32",
1271
+ "169.254.0.0/16",
1272
+ "172.16.0.0/12",
1273
+ "192.0.0.0/24",
1274
+ "192.0.2.0/24",
1275
+ "192.88.99.0/24",
1276
+ "192.168.0.0/16",
1277
+ "198.18.0.0/15",
1278
+ "198.51.100.0/24",
1279
+ "203.0.113.0/24",
1280
+ "224.0.0.0/4",
1281
+ "240.0.0.0/4"
1282
+ ];
1283
+ var BLOCKED_IPV6 = [
1284
+ "::/128",
1285
+ "::1/128",
1286
+ "::ffff:0:0/96",
1287
+ "64:ff9b::/96",
1288
+ "64:ff9b:1::/48",
1289
+ "100::/64",
1290
+ "fc00::/7",
1291
+ "fec0::/10",
1292
+ "fe80::/10",
1293
+ "ff00::/8",
1294
+ "2001::/32",
1295
+ "2001:2::/48",
1296
+ "2001:10::/28",
1297
+ "2001:20::/28",
1298
+ "2001:db8::/32",
1299
+ "2002::/16",
1300
+ "3fff::/20"
1301
+ ];
1302
+ var normalizeEgressTcpPorts = (ports) => {
1303
+ for (const port of ports) {
1304
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
1305
+ throw new Error("Agent egress policy refuses an invalid loopback TCP port.");
1306
+ }
1307
+ }
1308
+ return [...new Set(ports)].sort((left, right) => left - right);
1309
+ };
1310
+ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
1311
+ const ports = normalizeEgressTcpPorts(loopbackTcpPorts);
1312
+ return [
1313
+ "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
1314
+ `IPAddressAllow=${SYSTEMD_RESOLVED_ADDRESS}/32`,
1315
+ ...ports.length > 0 ? ["IPAddressAllow=127.0.0.1/32", "IPAddressAllow=::1/128"] : [],
1316
+ ...BLOCKED_IPV4.map((network) => `IPAddressDeny=${network}`),
1317
+ ...BLOCKED_IPV6.map((network) => `IPAddressDeny=${network}`)
1318
+ ].join(`
1319
+ `);
1320
+ }
1321
+
1322
+ // src/provision.ts
1323
+ import { isIP as isIP3 } from "net";
1324
+ function atLeast(version, floor) {
1325
+ const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
1326
+ const got = parse(version);
1327
+ const want = parse(floor);
1328
+ if (got.length === 0)
1329
+ return false;
1330
+ for (let index = 0;index < want.length; index += 1) {
1331
+ const a = got[index] ?? 0;
1332
+ const b = want[index] ?? 0;
1333
+ if (a > b)
1334
+ return true;
1335
+ if (a < b)
1336
+ return false;
1337
+ }
1338
+ return true;
1339
+ }
1340
+ var CAPABILITY_CHECKS = {
1341
+ snpGuest: {
1342
+ command: "test -e /dev/sev-guest && echo yes || echo no",
1343
+ satisfied: (stdout) => stdout.trim() === "yes",
1344
+ remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
1345
+ },
1346
+ systemd: {
1347
+ command: "test -d /run/systemd/system && echo yes || echo no",
1348
+ satisfied: (stdout) => stdout.trim() === "yes",
1349
+ remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
1350
+ },
1351
+ bun: {
1352
+ command: "bun --version 2>/dev/null || echo missing",
1353
+ satisfied: (stdout) => atLeast(stdout, "1.1.0"),
1354
+ remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
1355
+ },
1356
+ python: {
1357
+ command: "python3 --version 2>/dev/null || echo missing",
1358
+ satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
1359
+ remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
1360
+ }
1361
+ };
1362
+ var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
1363
+ var reasonFor = (mode) => mode === "attested" ? "SEV-SNP guest device present, so the agent can prove what it is running and the platform can refuse it if the measurement is wrong." : "No SEV-SNP guest device. The agent authenticates with its enrolment token and hybrid Ed25519 + ML-DSA signature \u2014 weaker than attestation, stronger than an API key in the application.";
1364
+ var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
1365
+ var DEPLOYMENT_GROUP = "forgezero-deploy";
1366
+ var VAULT_GROUP = "forgezero-vault";
1367
+ var LIFECYCLE_GROUP = "forgezero-lifecycle";
1368
+ var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
1369
+ var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
1370
+ var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
1371
+ var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
1372
+ var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
1373
+ var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
1374
+ var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
1375
+ var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
1376
+ var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
1377
+ var AGENT_EGRESS_UNIT_PATH = "/etc/systemd/system/forgezero-agent-egress.service";
1378
+ var DEFAULT_RUNNER_PUBLIC_TCP_PORTS = [443];
1379
+ function agentEgressUnit(options) {
1380
+ const bin = options.binPath ?? "fz-agent";
1381
+ const user = options.user ?? "forgezero";
1382
+ if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
1383
+ throw new Error("invalid Agent service user");
1384
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1385
+ const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1386
+ const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1387
+ if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
1388
+ throw new Error("deployed project runner needs at least one vetted public TCP port");
1389
+ }
1390
+ if (deploymentEnabled)
1391
+ systemdAgentEgressDirectives(runnerLoopbackPorts);
1392
+ const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
1393
+ const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
1394
+ const policyProofs = deploymentEnabled ? [
1395
+ ...runnerLoopbackPorts.length > 0 ? [`loopback=.*:${runnerLoopbackPorts.join(",")}`] : [],
1396
+ `public-tcp=${runnerPublicTcpPorts.join(",")}`
1397
+ ].map((pattern) => `ExecStartPost=/bin/sh -c '/usr/sbin/nft --numeric list table inet ${AGENT_EGRESS_TABLE} | /usr/bin/grep -q "${pattern}"'`).join(`
1398
+ `) : "";
1399
+ return `[Unit]
1400
+ Description=ForgeZero Agent host egress policy
1401
+ Documentation=https://www.forgezero.net/docs/agent
1402
+ After=systemd-resolved.service nftables.service
1403
+ Requires=systemd-resolved.service
1404
+ Before=forgezero-agent-enrol.service forgezero-agent.service
1405
+
1406
+ [Service]
1407
+ Type=notify
1408
+ NotifyAccess=all
1409
+ User=root
1410
+ Group=root
1411
+ ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
1412
+ ${policyProofs}
1413
+ Restart=on-failure
1414
+ RestartSec=2
1415
+ LimitCORE=0
1416
+ NoNewPrivileges=true
1417
+ PrivateTmp=true
1418
+ ProtectSystem=strict
1419
+ ProtectHome=true
1420
+ ProtectKernelTunables=true
1421
+ ProtectKernelModules=true
1422
+ ProtectControlGroups=true
1423
+ RestrictSUIDSGID=true
1424
+ RestrictRealtime=true
1425
+ MemoryDenyWriteExecute=true
1426
+ LockPersonality=true
1427
+ CapabilityBoundingSet=CAP_NET_ADMIN
1428
+ RestrictAddressFamilies=AF_UNIX AF_NETLINK
1429
+
1430
+ [Install]
1431
+ WantedBy=multi-user.target
1432
+ `;
1433
+ }
1434
+ function softwareHelperUnit(options) {
1435
+ const bin = options.binPath ?? "fz-agent";
1436
+ return `[Unit]
1437
+ Description=ForgeZero declarative software strategy helper
1438
+ Documentation=https://www.forgezero.net/docs/agent
1439
+ After=network-online.target
1440
+ Wants=network-online.target
1441
+
1442
+ [Service]
1443
+ Type=simple
1444
+ User=root
1445
+ Group=${SOFTWARE_HELPER_GROUP}
1446
+ Environment=FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}
1447
+ ExecStart=${bin} software-helper
1448
+ Restart=always
1449
+ RestartSec=2
1450
+ RuntimeDirectory=forgezero-software
1451
+ RuntimeDirectoryMode=0750
1452
+ UMask=0007
1453
+ LimitCORE=0
1454
+ PrivateTmp=true
1455
+ ProtectHome=true
1456
+ ProtectKernelTunables=true
1457
+ ProtectKernelModules=true
1458
+ ProtectControlGroups=true
1459
+ RestrictRealtime=true
1460
+ LockPersonality=true
1461
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1462
+
1463
+ [Install]
1464
+ WantedBy=multi-user.target
1465
+ `;
1466
+ }
1467
+ function agentUpdateHelperUnit(options) {
1468
+ const bin = options.binPath ?? "fz-agent";
1469
+ return `[Unit]
1470
+ Description=ForgeZero verified Agent update helper
1471
+ Documentation=https://www.forgezero.net/docs/agent
1472
+ After=network-online.target
1473
+ Wants=network-online.target
1474
+
1475
+ [Service]
1476
+ Type=simple
1477
+ User=root
1478
+ Group=${AGENT_UPDATE_GROUP}
1479
+ Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
1480
+ ExecStart=${bin} update-helper
1481
+ Restart=always
1482
+ RestartSec=2
1483
+ RuntimeDirectory=forgezero-update
1484
+ RuntimeDirectoryMode=0750
1485
+ UMask=0007
1486
+ LimitCORE=0
1487
+ NoNewPrivileges=true
1488
+ PrivateTmp=true
1489
+ ProtectSystem=strict
1490
+ ProtectHome=true
1491
+ ProtectKernelTunables=true
1492
+ ProtectKernelModules=true
1493
+ ProtectControlGroups=true
1494
+ RestrictSUIDSGID=true
1495
+ RestrictRealtime=true
1496
+ LockPersonality=true
1497
+ ReadWritePaths=${DEFAULT_AGENT_RELEASE_ROOT} /var/lib/forgezero
1498
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1499
+
1500
+ [Install]
1501
+ WantedBy=multi-user.target
1502
+ `;
1503
+ }
1504
+ function agentSocketUnit(options) {
1505
+ const socket = systemdPath(options.socketPath, "agent socket");
1506
+ return `[Unit]
1507
+ Description=ForgeZero application Vault socket
1508
+ Documentation=https://www.forgezero.net/docs/agent
1509
+
1510
+ [Socket]
1511
+ ListenStream=${socket}
1512
+ SocketUser=root
1513
+ SocketGroup=${VAULT_GROUP}
1514
+ SocketMode=0660
1515
+ DirectoryMode=0750
1516
+ RemoveOnStop=true
1517
+ Service=forgezero-agent-proxy.service
1518
+
1519
+ [Install]
1520
+ WantedBy=sockets.target
1521
+ `;
1522
+ }
1523
+ function agentSocketProxyUnit(options) {
1524
+ const backend = agentBackendSocketPath(options.socketPath);
1525
+ const user = options.user ?? "forgezero";
1526
+ return `[Unit]
1527
+ Description=ForgeZero application Vault socket proxy
1528
+ Documentation=https://www.forgezero.net/docs/agent
1529
+ Requires=forgezero-agent.service
1530
+ After=forgezero-agent.service
1531
+
1532
+ [Service]
1533
+ User=${user}
1534
+ Group=${VAULT_GROUP}
1535
+ ExecStart=/usr/lib/systemd/systemd-socket-proxyd ${backend}
1536
+ NoNewPrivileges=true
1537
+ PrivateTmp=true
1538
+ ProtectSystem=strict
1539
+ ProtectHome=true
1540
+ ProtectKernelTunables=true
1541
+ ProtectKernelModules=true
1542
+ ProtectControlGroups=true
1543
+ RestrictSUIDSGID=true
1544
+ RestrictRealtime=true
1545
+ MemoryDenyWriteExecute=true
1546
+ LockPersonality=true
1547
+ RestrictAddressFamilies=AF_UNIX
1548
+ `;
1549
+ }
1550
+ function agentBackendSocketPath(publicSocketPath) {
1551
+ const socket = systemdPath(publicSocketPath, "agent socket");
1552
+ const backend = `${socket}.backend`;
1553
+ if (Buffer.byteLength(backend) > 100)
1554
+ throw new Error("agent socket path is too long for a Unix socket");
1555
+ return backend;
1556
+ }
1557
+ var systemdPath = (value, label) => {
1558
+ if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
1559
+ throw new Error(`invalid ${label} path`);
1560
+ return value;
1561
+ };
1562
+ var awaitSocketCommand = (path) => {
1563
+ const socket = systemdPath(path, "readiness socket");
1564
+ return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
1565
+ };
1566
+ var validNodeHostname = (value) => !value || value.length <= 253 && value === value.toLowerCase() && value.split(".").length >= 3 && value.split(".").every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
1567
+ function warpConfigUnit(options) {
1568
+ if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
1569
+ throw new Error("WARP organization is invalid");
1570
+ }
1571
+ if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
1572
+ throw new Error("WARP service-token credential paths are required");
1573
+ }
1574
+ const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
1575
+ const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
1576
+ const bin = options.binPath ?? "fz-agent";
1577
+ return `[Unit]
1578
+ Description=Materialize Cloudflare One enrollment in tmpfs
1579
+ Documentation=https://www.forgezero.net/docs/agent
1580
+ Before=warp-svc.service
1581
+
1582
+ [Service]
1583
+ Type=oneshot
1584
+ RemainAfterExit=yes
1585
+ LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
1586
+ LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
1587
+ Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
1588
+ Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
1589
+ ExecStart=${bin} warp-config --organization=${options.warpOrganization}
1590
+ RuntimeDirectory=forgezero-warp
1591
+ RuntimeDirectoryMode=0700
1592
+ RuntimeDirectoryPreserve=yes
1593
+ UMask=0077
1594
+ LimitCORE=0
1595
+ NoNewPrivileges=true
1596
+ PrivateTmp=true
1597
+ ProtectSystem=strict
1598
+ ProtectHome=true
1599
+ ReadWritePaths=/var/lib/cloudflare-warp
1600
+
1601
+ [Install]
1602
+ WantedBy=multi-user.target
1603
+ `;
1604
+ }
1605
+ function warpServiceDropIn() {
1606
+ return `[Unit]
1607
+ Requires=forgezero-warp-config.service
1608
+ After=forgezero-warp-config.service
1609
+ `;
1610
+ }
1611
+ function lifecycleHelperUnit(options) {
1612
+ if (!options.lifecycleProfilePath)
1613
+ throw new Error("lifecycle helper needs a root-owned profile");
1614
+ const bin = options.binPath ?? "fz-agent";
1615
+ const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
1616
+ const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
1617
+ return `[Unit]
1618
+ Description=ForgeZero fixed-operation compute lifecycle helper
1619
+ Documentation=https://www.forgezero.net/docs/agent
1620
+ After=network-online.target
1621
+ Wants=network-online.target
1622
+
1623
+ [Service]
1624
+ Type=simple
1625
+ User=root
1626
+ Group=${LIFECYCLE_GROUP}
1627
+ Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
1628
+ ExecStart=${bin} lifecycle-helper --profile=${profile}
1629
+ Restart=always
1630
+ RestartSec=2
1631
+ RuntimeDirectory=forgezero-lifecycle
1632
+ RuntimeDirectoryMode=0750
1633
+ UMask=0007
1634
+ LimitCORE=0
1635
+ NoNewPrivileges=true
1636
+ PrivateTmp=true
1637
+ ProtectSystem=strict
1638
+ ProtectHome=true
1639
+ ProtectKernelTunables=true
1640
+ ProtectKernelModules=true
1641
+ ProtectControlGroups=true
1642
+ RestrictSUIDSGID=true
1643
+ RestrictRealtime=true
1644
+ MemoryDenyWriteExecute=true
1645
+ LockPersonality=true
1646
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1647
+
1648
+ [Install]
1649
+ WantedBy=multi-user.target
1650
+ `;
1651
+ }
1652
+ function agentEnrolmentUnit(options) {
1653
+ if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
1654
+ throw new Error("direct enrolment needs API, credential and state paths");
1655
+ }
1656
+ if (!validNodeHostname(options.nodeHostname))
1657
+ throw new Error("node hostname is invalid");
1658
+ const bin = options.binPath ?? "fz-agent";
1659
+ const user = options.user ?? "forgezero";
1660
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1661
+ const label = options.nodeLabel ? `Environment=FZ_NODE_LABEL=${options.nodeLabel}
1662
+ ` : "";
1663
+ const hostname = options.nodeHostname ? `Environment=FZ_NODE_HOSTNAME=${options.nodeHostname}
1664
+ ` : "";
1665
+ const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
1666
+ ` : "";
1667
+ const networkAttachment = [
1668
+ options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
1669
+ ` : "",
1670
+ options.cloudflareTunnelId ? `Environment=FZ_CF_TUNNEL_ID=${options.cloudflareTunnelId}
1671
+ ` : "",
1672
+ options.cloudflareVirtualNetworkId ? `Environment=FZ_CF_VIRTUAL_NETWORK_ID=${options.cloudflareVirtualNetworkId}
1673
+ ` : "",
1674
+ options.cloudflareWarpPolicyId ? `Environment=FZ_CF_WARP_POLICY_ID=${options.cloudflareWarpPolicyId}
1675
+ ` : ""
1676
+ ].join("");
1677
+ const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
1678
+ const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
1679
+ Requires=forgezero-agent-egress.service
1680
+ BindsTo=forgezero-agent-egress.service
1681
+ ` : "";
1682
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
1683
+ return `[Unit]
1684
+ Description=Bind this machine to its ForgeZero compute
1685
+ After=network-online.target
1686
+ Wants=network-online.target
1687
+ ${egressDependency}Before=forgezero-agent.service
1688
+ ConditionPathExists=!${options.enrolStatePath}
1689
+
1690
+ [Service]
1691
+ Type=oneshot
1692
+ User=${user}
1693
+ Group=${user}
1694
+ LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1695
+ LoadCredentialEncrypted=enrol-token:${options.enrolTokenCredentialPath}
1696
+ Environment=FZ_SEED_CREDENTIAL=agent-seed
1697
+ Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
1698
+ Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
1699
+ Environment=FZ_API=${options.apiUrl}
1700
+ ${label}${hostname}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
1701
+ # A '+' fixed command runs as root solely to remove the host-bound one-time
1702
+ # ciphertext. Tenant code and the agent never receive a privilege boundary.
1703
+ ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
1704
+ NoNewPrivileges=true
1705
+ PrivateTmp=true
1706
+ ProtectSystem=strict
1707
+ ProtectHome=true
1708
+ ReadWritePaths=${stateDir}
1709
+ LimitCORE=0
1710
+ ${egressDirectives}
1711
+
1712
+ [Install]
1713
+ WantedBy=multi-user.target
1714
+ `;
1715
+ }
1716
+ function deploymentRunnerUnit(options) {
1717
+ const bin = options.binPath ?? "fz-agent";
1718
+ const root = options.deployRoot ?? "/opt/forgezero";
1719
+ const agentUser = options.user ?? "forgezero-agent";
1720
+ const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
1721
+ Requires=forgezero-agent-egress.service
1722
+ BindsTo=forgezero-agent-egress.service
1723
+ ` : "";
1724
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.runnerLoopbackPorts ?? []) : "";
1725
+ return `[Unit]
1726
+ Description=ForgeZero credential-free project command runner
1727
+ Documentation=https://www.forgezero.net/docs/agent
1728
+ ${egressDependency}
1729
+
1730
+ [Service]
1731
+ Type=notify
1732
+ NotifyAccess=all
1733
+ User=${DEPLOYMENT_RUNNER_USER}
1734
+ Group=${DEPLOYMENT_GROUP}
1735
+ Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
1736
+ RuntimeDirectory=forgezero-deploy
1737
+ RuntimeDirectoryMode=0710
1738
+ ExecStartPre=+/usr/bin/install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0710 /run/forgezero-deploy
1739
+ ExecStartPre=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
1740
+ ExecStart=${bin} deploy-runner --root=${root} --home=${root}/runner-home
1741
+ ExecStartPost=+/usr/bin/chown ${agentUser}:${agentUser} ${DEPLOYMENT_RUNNER_SOCKET}
1742
+ ExecStartPost=+/usr/bin/chmod 0600 ${DEPLOYMENT_RUNNER_SOCKET}
1743
+ ExecStartPost=+/usr/bin/chown root:${VAULT_GROUP} /run/forgezero-deploy
1744
+ ExecStopPost=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
1745
+ Restart=always
1746
+ RestartSec=2
1747
+ UMask=0007
1748
+ LimitCORE=0
1749
+ NoNewPrivileges=false
1750
+ PrivateTmp=true
1751
+ ProtectSystem=strict
1752
+ ProtectHome=true
1753
+ ProtectKernelTunables=true
1754
+ ProtectKernelModules=true
1755
+ ProtectControlGroups=true
1756
+ RestrictRealtime=true
1757
+ MemoryDenyWriteExecute=true
1758
+ LockPersonality=true
1759
+ ${egressDirectives}
1760
+ ReadWritePaths=${root}/releases ${root}/runner-home
1761
+
1762
+ [Install]
1763
+ WantedBy=multi-user.target
1764
+ `;
1765
+ }
1766
+ function agentUnit(options) {
1767
+ if (!validNodeHostname(options.nodeHostname))
1768
+ throw new Error("node hostname is invalid");
1769
+ if (!options.telemetryEndpoint) {
1770
+ throw new Error("compute Agent provisioning requires OTEL_EXPORTER_OTLP_ENDPOINT as an explicit public HTTPS collector coordinate");
1771
+ }
1772
+ let telemetryEndpoint;
1773
+ {
1774
+ let endpoint2;
1775
+ try {
1776
+ endpoint2 = new URL(options.telemetryEndpoint);
1777
+ } catch {
1778
+ throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL");
1779
+ }
1780
+ if (endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash || isIP3(endpoint2.hostname) !== 0 || !endpoint2.hostname.includes(".") || endpoint2.hostname === "localhost" || endpoint2.hostname.endsWith(".local"))
1781
+ throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
1782
+ telemetryEndpoint = endpoint2.toString().replace(/\/$/, "");
1783
+ }
1784
+ const bin = options.binPath ?? "fz-agent";
1785
+ const user = options.user ?? "forgezero";
1786
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1787
+ const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
1788
+ const deployRoot = options.deployRoot ?? "/opt/forgezero";
1789
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1790
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
1791
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1792
+ throw new Error("migration pull and lifecycle profile must be supplied together");
1793
+ }
1794
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1795
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1796
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1797
+ }
1798
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1799
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
1800
+ const warpValues = [
1801
+ options.warpOrganization,
1802
+ options.warpClientIdCredentialPath,
1803
+ options.warpClientSecretCredentialPath
1804
+ ];
1805
+ const warpEnabled = warpValues.every(Boolean);
1806
+ if (warpValues.some(Boolean) && !warpEnabled)
1807
+ throw new Error("WARP configuration must be supplied together");
1808
+ const networkAttachmentValues = [
1809
+ options.cloudflareAccountId,
1810
+ options.cloudflareTunnelId,
1811
+ options.cloudflareVirtualNetworkId,
1812
+ options.cloudflareWarpPolicyId
1813
+ ];
1814
+ if (networkAttachmentValues.some(Boolean)) {
1815
+ if (!options.cloudflareAccountId || !options.cloudflareTunnelId || !options.cloudflareWarpPolicyId) {
1816
+ throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
1817
+ }
1818
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
1819
+ if (!/^[a-f0-9]{32}$/i.test(options.cloudflareAccountId) || !uuid.test(options.cloudflareTunnelId) || options.cloudflareVirtualNetworkId && !uuid.test(options.cloudflareVirtualNetworkId) || !/^[A-Za-z0-9-]{1,64}$/.test(options.cloudflareWarpPolicyId)) {
1820
+ throw new Error("private-network attachment coordinates are invalid");
1821
+ }
1822
+ }
1823
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
1824
+ const deploymentEnvironment = options.deploymentEnvironment ?? {};
1825
+ const deploymentCredentials = options.deploymentCredentials ?? {};
1826
+ for (const [name, value] of Object.entries(deploymentEnvironment)) {
1827
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !/^[A-Za-z0-9._:\/@+-]+$/.test(value)) {
1828
+ throw new Error(`invalid deployment environment entry: ${name}`);
1829
+ }
1830
+ }
1831
+ for (const [name, path] of Object.entries(deploymentCredentials)) {
1832
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path.startsWith("/") || /[\r\n:]/.test(path)) {
1833
+ throw new Error(`invalid deployment credential entry: ${name}`);
1834
+ }
1835
+ }
1836
+ const environment = [
1837
+ "NODE_ENV=production",
1838
+ `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
1839
+ `FZ_CONTROL_SOCKET=${controlSocketPath}`,
1840
+ `FZ_SEED_CREDENTIAL=agent-seed`,
1841
+ `FZ_AGENT_MODE=${options.mode}`,
1842
+ options.apiUrl ? `FZ_API=${options.apiUrl}` : null,
1843
+ options.project ? `FZ_PROJECT=${options.project}` : null,
1844
+ options.environment ? `FZ_ENVIRONMENT=${options.environment}` : null,
1845
+ options.enrolStatePath ? `FZ_ENROL_STATE_FILE=${options.enrolStatePath}` : null,
1846
+ options.nodeLabel ? `FZ_NODE_LABEL=${options.nodeLabel}` : null,
1847
+ options.nodeHostname ? `FZ_NODE_HOSTNAME=${options.nodeHostname}` : null,
1848
+ `OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint}`,
1849
+ "OTEL_SERVICE_NAME=forgezero-agent",
1850
+ options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
1851
+ options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
1852
+ options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
1853
+ options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
1854
+ options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
1855
+ deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
1856
+ deploymentEnabled ? `FZ_CAPACITY_EVIDENCE_DIR=${deployRoot}/capacity` : null,
1857
+ deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
1858
+ deploymentEnabled ? `FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}` : null,
1859
+ Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
1860
+ Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
1861
+ ...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
1862
+ options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
1863
+ options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
1864
+ options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1865
+ options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
1866
+ options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
1867
+ options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
1868
+ `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1869
+ options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
1870
+ ].filter((line) => line !== null);
1871
+ if (deploymentEnabled) {
1872
+ environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
1873
+ }
1874
+ const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
1875
+ ` : "";
1876
+ const bootstrapCredential = bootstrapEnabled ? `LoadCredentialEncrypted=bootstrap-ssh-key:${bootstrapSshCredentialPath}
1877
+ ` : "";
1878
+ const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
1879
+ `);
1880
+ const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache ${deployRoot}/capacity` : "";
1881
+ const supplementaryGroups = [
1882
+ AGENT_UPDATE_GROUP,
1883
+ deploymentEnabled ? DEPLOYMENT_GROUP : null,
1884
+ deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
1885
+ lifecycleEnabled ? LIFECYCLE_GROUP : null
1886
+ ].filter((value) => value !== null);
1887
+ const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
1888
+ const after = [
1889
+ "network-online.target",
1890
+ "forgezero-agent-update-helper.service",
1891
+ options.enforceEgress ? "forgezero-agent-egress.service" : null,
1892
+ deploymentEnabled ? "forgezero-deploy-runner.service" : null,
1893
+ deploymentEnabled ? "forgezero-software-helper.service" : null,
1894
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
1895
+ warpEnabled ? "warp-svc.service" : null,
1896
+ enrolmentEnabled ? "forgezero-agent-enrol.service" : null
1897
+ ].filter((value) => value !== null);
1898
+ const requires = [
1899
+ "forgezero-agent-update-helper.service",
1900
+ options.enforceEgress ? "forgezero-agent-egress.service" : null,
1901
+ deploymentEnabled ? "forgezero-deploy-runner.service" : null,
1902
+ deploymentEnabled ? "forgezero-software-helper.service" : null,
1903
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
1904
+ warpEnabled ? "warp-svc.service" : null,
1905
+ enrolmentEnabled ? "forgezero-agent-enrol.service" : null
1906
+ ].filter((value) => value !== null);
1907
+ const deploymentDependency = [
1908
+ `After=${after.join(" ")}`,
1909
+ "Wants=network-online.target",
1910
+ requires.length > 0 ? `Requires=${requires.join(" ")}` : null,
1911
+ options.enforceEgress ? "BindsTo=forgezero-agent-egress.service" : null
1912
+ ].filter((value) => value !== null).join(`
1913
+ `);
1914
+ const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
1915
+ DeviceAllow=/dev/sev-guest rw` : "";
1916
+ const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
1917
+ ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
1918
+ ` : "";
1919
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
1920
+ return `[Unit]
1921
+ Description=ForgeZero node agent (${options.mode})
1922
+ Documentation=https://www.forgezero.net/docs/agent
1923
+ ${deploymentDependency}
1924
+
1925
+ [Service]
1926
+ Type=simple
1927
+ User=${user}
1928
+ Group=${VAULT_GROUP}
1929
+ ${deploymentGroup}
1930
+ LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1931
+ ${gitCredential}${bootstrapCredential}${projectCredentials}${projectCredentials ? `
1932
+ ` : ""}${snpPrepare}ExecStart=${bin}
1933
+ Restart=always
1934
+ RestartSec=2
1935
+
1936
+ ${environment.map((line) => `Environment=${line}`).join(`
1937
+ `)}
1938
+
1939
+ # The node seed and the vault replica live in this process's memory. A core dump
1940
+ # writes both to disk, which is the one artefact this design exists to remove.
1941
+ LimitCORE=0
1942
+
1943
+ # The socket is the entire interface: anything that can read it can read the
1944
+ # scope. So it lives in a directory systemd creates with a known owner rather
1945
+ # than wherever the process happened to have write access.
1946
+ RuntimeDirectory=forgezero
1947
+ RuntimeDirectoryMode=0750
1948
+ RuntimeDirectoryPreserve=yes
1949
+ UMask=0007
1950
+
1951
+ # Tenant-controlled commands execute in forgezero-deploy-runner.service. This
1952
+ # credential-bearing process never needs to cross a privilege boundary.
1953
+ NoNewPrivileges=true
1954
+ PrivateTmp=true
1955
+ ProtectSystem=strict
1956
+ ProtectHome=true
1957
+ ProtectKernelTunables=true
1958
+ ProtectKernelModules=true
1959
+ ProtectControlGroups=true
1960
+ RestrictSUIDSGID=true
1961
+ RestrictRealtime=true
1962
+ MemoryDenyWriteExecute=true
1963
+ LockPersonality=true
1964
+ ${egressDirectives}
1965
+ ${snpDevice}
1966
+ ${deploymentWrites}
1967
+
1968
+ [Install]
1969
+ WantedBy=multi-user.target
1970
+ `;
1971
+ }
1972
+ var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
1973
+ function planProvision(options) {
1974
+ const mode = options.mode;
1975
+ const user = options.user ?? "forgezero";
1976
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1977
+ const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
1978
+ const deployRoot = options.deployRoot ?? "/opt/forgezero";
1979
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1980
+ const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1981
+ const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1982
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
1983
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1984
+ throw new Error("migration pull and lifecycle profile must be supplied together");
1985
+ }
1986
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1987
+ if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1988
+ throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1989
+ }
1990
+ const warpValues = [
1991
+ options.warpOrganization,
1992
+ options.warpClientIdCredentialPath,
1993
+ options.warpClientSecretCredentialPath
1994
+ ];
1995
+ const warpEnabled = warpValues.every(Boolean);
1996
+ if (warpValues.some(Boolean) && !warpEnabled)
1997
+ throw new Error("WARP configuration must be supplied together");
1998
+ const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
1999
+ if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
2000
+ throw new Error("direct enrolment paths must be supplied together");
2001
+ const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
2002
+ const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
2003
+ const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
2004
+ const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
2005
+ const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
2006
+ const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
2007
+ const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
2008
+ const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
2009
+ if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
2010
+ throw new Error("generated Git identity needs credential and public-key paths");
2011
+ }
2012
+ const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
2013
+ const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
2014
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
2015
+ const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
2016
+ const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
2017
+ const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
2018
+ return {
2019
+ mode,
2020
+ reason: reasonFor(mode),
2021
+ unitPath: UNIT_PATH,
2022
+ unit: agentUnit({
2023
+ ...options,
2024
+ mode,
2025
+ lifecycleProfilePath,
2026
+ lifecycleHelperSocketPath,
2027
+ bootstrapSshCredentialPath
2028
+ }),
2029
+ auxiliaryUnits: [
2030
+ { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
2031
+ { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
2032
+ { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
2033
+ ...options.enforceEgress ? [
2034
+ { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
2035
+ ] : [],
2036
+ ...deploymentEnabled ? [
2037
+ { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
2038
+ { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
2039
+ ] : [],
2040
+ ...enrolmentEnabled ? [
2041
+ { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
2042
+ ] : [],
2043
+ ...lifecycleEnabled ? [
2044
+ { path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
2045
+ ...options,
2046
+ lifecycleProfilePath,
2047
+ lifecycleHelperSocketPath
2048
+ }) }
2049
+ ] : [],
2050
+ ...warpEnabled ? [
2051
+ { path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
2052
+ ...options,
2053
+ warpClientIdCredentialPath,
2054
+ warpClientSecretCredentialPath
2055
+ }) },
2056
+ { path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
2057
+ ] : []
2058
+ ],
2059
+ socketPath: options.socketPath,
2060
+ user,
2061
+ steps: [
2062
+ ...options.enforceEgress ? [{
2063
+ label: "Ubuntu Agent egress prerequisites",
2064
+ command: `. /etc/os-release; test "$ID" = ubuntu; ` + `DEBIAN_FRONTEND=noninteractive apt-get update -qq; ` + `DEBIAN_FRONTEND=noninteractive apt-get install -y nftables; ` + `systemctl enable --now systemd-resolved.service; ` + `test "$(readlink -f /etc/resolv.conf)" = /run/systemd/resolve/stub-resolv.conf; ` + `test -s /run/systemd/resolve/stub-resolv.conf`
2065
+ }] : [],
2066
+ {
2067
+ label: "vault socket access group",
2068
+ command: `groupadd --system ${VAULT_GROUP} || true`
2069
+ },
2070
+ {
2071
+ label: "Agent update helper access group",
2072
+ command: `groupadd --system ${AGENT_UPDATE_GROUP} || true`
2073
+ },
2074
+ ...sourceBinPath && binPath ? [{
2075
+ label: "root-owned agent runtime",
2076
+ command: `install -d -o root -g root -m 0755 ${binPath.replace(/\/[^/]+$/, "")} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION}/dist; ` + `install -o root -g root -m 0755 ${sourceBinPath} ` + `${DEFAULT_AGENT_RELEASE_ROOT}/versions/${VERSION}/dist/fz-agent.js; ` + `ln -sfn versions/${VERSION} ${DEFAULT_AGENT_RELEASE_ROOT}/current.next; ` + `mv -Tf ${DEFAULT_AGENT_RELEASE_ROOT}/current.next ${DEFAULT_AGENT_RELEASE_ROOT}/current; ` + `rm -f ${binPath}; ln -s ${DEFAULT_AGENT_RELEASE_ROOT}/current/dist/fz-agent.js ${binPath}`
2077
+ }] : [],
2078
+ ...warpEnabled ? [{
2079
+ label: "Cloudflare One client for Ubuntu 26.04",
2080
+ command: `. /etc/os-release; test "$ID" = ubuntu && test "$VERSION_ID" = 26.04; ` + `install -d -m 0755 /usr/share/keyrings /etc/apt/sources.list.d /etc/systemd/system/warp-svc.service.d; ` + `curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg -o /run/cloudflare-warp-key.gpg; ` + `gpg --batch --yes --dearmor -o /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg /run/cloudflare-warp-key.gpg; ` + `rm -f /run/cloudflare-warp-key.gpg; ` + `printf 'deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ %s main\\n' "$VERSION_CODENAME" > /etc/apt/sources.list.d/cloudflare-client.list; ` + `apt-get update -qq; DEBIAN_FRONTEND=noninteractive apt-get install -y cloudflare-warp`
2081
+ }] : [],
2082
+ ...deploymentEnabled ? [{
2083
+ label: "deployment isolation group",
2084
+ command: `groupadd --system ${DEPLOYMENT_GROUP} || true; groupadd --system ${SOFTWARE_HELPER_GROUP} || true`
2085
+ }] : [],
2086
+ ...lifecycleEnabled ? [{
2087
+ label: "lifecycle helper access group",
2088
+ command: `groupadd --system ${LIFECYCLE_GROUP} || true`
2089
+ }] : [],
2090
+ {
2091
+ label: "service account",
2092
+ command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
2093
+ },
2094
+ {
2095
+ label: "bind service account to vault group",
2096
+ command: `usermod -g ${VAULT_GROUP} ${user}`
2097
+ },
2098
+ {
2099
+ label: "grant verified Agent update access",
2100
+ command: `usermod -a -G ${AGENT_UPDATE_GROUP} ${user}`
2101
+ },
2102
+ ...lifecycleEnabled ? [{
2103
+ label: "grant lifecycle helper socket access",
2104
+ command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
2105
+ }] : [],
2106
+ ...deploymentEnabled ? [{
2107
+ label: "credential-free deployment account",
2108
+ command: `useradd --system --no-create-home --shell /usr/sbin/nologin --gid ${DEPLOYMENT_GROUP} ${DEPLOYMENT_RUNNER_USER} || true; ` + `usermod -a -G ${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP} ${user}`
2109
+ }] : [],
2110
+ {
2111
+ label: "credential directory",
2112
+ command: `install -d -o root -g root -m 0700 ${credentialDir}`
2113
+ },
2114
+ {
2115
+ label: "Agent state directory",
2116
+ command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
2117
+ },
2118
+ {
2119
+ label: "encrypted node identity",
2120
+ command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
2121
+ },
2122
+ ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
2123
+ {
2124
+ label: "Git deploy identity directory",
2125
+ command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
2126
+ },
2127
+ {
2128
+ label: "unique encrypted Git deploy identity",
2129
+ command: `test -s ${gitCredentialPath} || { ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `ssh-keygen -q -t ed25519 -N '' -C forgezero-compute -f /run/forgezero-git-deploy-key; ` + `systemd-creds encrypt --name=git-deploy-key /run/forgezero-git-deploy-key ${gitCredentialPath}; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; ` + `chmod 0400 ${gitCredentialPath}; }; ` + `test -s ${gitPublicKeyPath} || { ` + `systemd-creds decrypt --name=git-deploy-key ${gitCredentialPath} /run/forgezero-git-deploy-key; ` + `ssh-keygen -y -f /run/forgezero-git-deploy-key | ` + `sed 's/$/ forgezero-compute/' > /run/forgezero-git-deploy-key.pub; ` + `install -o root -g root -m 0444 /run/forgezero-git-deploy-key.pub ${gitPublicKeyPath}; ` + `rm -f /run/forgezero-git-deploy-key /run/forgezero-git-deploy-key.pub; }; ` + `test -s ${gitCredentialPath} && test -s ${gitPublicKeyPath}`
2130
+ }
2131
+ ] : [],
2132
+ ...enrolmentEnabled ? [
2133
+ {
2134
+ label: "enrolment state directory",
2135
+ command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
2136
+ },
2137
+ {
2138
+ label: "encrypted one-time enrolment capability",
2139
+ command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
2140
+ }
2141
+ ] : [],
2142
+ ...deploymentEnabled ? [{
2143
+ label: "deployment directories",
2144
+ command: `install -d -o root -g root -m 0755 ${deployRoot} && ` + `install -d -o root -g ${DEPLOYMENT_GROUP} -m 3770 ${deployRoot}/releases && ` + `install -d -o ${user} -g ${user} -m 0750 ${deployRoot}/cache && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/capacity && ` + `install -d -o ${user} -g ${user} -m 0700 ${deployRoot}/agent-home && ` + `install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0700 ${deployRoot}/runner-home ${deployRoot}/runner-home/cache`
2145
+ }] : [],
2146
+ { label: "reload units", command: "systemctl daemon-reload" },
2147
+ ...deploymentEnabled ? [{
2148
+ label: "remove unsupported deployment socket activation",
2149
+ command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
2150
+ }] : [],
2151
+ {
2152
+ label: "enable and converge services",
2153
+ command: `systemctl enable ${[
2154
+ "forgezero-agent.socket",
2155
+ "forgezero-agent-update-helper.service",
2156
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2157
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2158
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2159
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2160
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
2161
+ "forgezero-agent.service"
2162
+ ].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
2163
+ "forgezero-agent-update-helper.service",
2164
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2165
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2166
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2167
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2168
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
2169
+ ].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
2170
+ },
2171
+ ...enrolmentEnabled ? [{
2172
+ label: "prove the compute binding is durable",
2173
+ command: `test -s ${enrolStatePath}`
2174
+ }] : [],
2175
+ ...options.enforceEgress ? [{
2176
+ label: "prove the Agent egress policy is active",
2177
+ command: "systemctl is-active forgezero-agent-egress.service && " + "nft --numeric list table inet forgezero_agent_egress | grep -q forgezero-agent-egress-v1" + (deploymentEnabled ? ` && nft --numeric list table inet forgezero_agent_egress | grep -q 'public-tcp=${runnerPublicTcpPorts.join(",")}'` + (runnerLoopbackPorts.length > 0 ? ` && nft --numeric list table inet forgezero_agent_egress | grep -Eq 'loopback=[0-9]+:${runnerLoopbackPorts.join(",")}( |")'` : "") : "")
2178
+ }] : [],
2179
+ { label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
2180
+ { label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
2181
+ { label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
2182
+ { label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
2183
+ ...deploymentEnabled ? [{
2184
+ label: "prove the deployment runner socket exists",
2185
+ command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
2186
+ }, {
2187
+ label: "prove the software strategy helper socket exists",
2188
+ command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
2189
+ }] : [],
2190
+ ...lifecycleEnabled ? [{
2191
+ label: "prove the lifecycle helper socket exists",
2192
+ command: awaitSocketCommand(lifecycleHelperSocketPath)
2193
+ }] : [],
2194
+ ...warpEnabled ? [{
2195
+ label: "prove Cloudflare WARP is connected",
2196
+ command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
2197
+ }] : [],
2198
+ ...options.repository ? [{
2199
+ label: "prove the deployment control socket exists",
2200
+ command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
2201
+ }] : []
2202
+ ]
2203
+ };
2204
+ }
2205
+
2206
+ // src/cli/agent-install.ts
2207
+ async function readCapabilities(run) {
2208
+ const answers = {};
2209
+ const checks = Object.entries(CAPABILITY_CHECKS);
2210
+ for (const [id, check] of checks) {
2211
+ try {
2212
+ const result = await run(check.command);
2213
+ answers[id] = check.satisfied(result.stdout, result.exitCode);
2214
+ } catch {
2215
+ answers[id] = false;
2216
+ }
2217
+ }
2218
+ return answers;
2219
+ }
2220
+ async function localRunner(command) {
2221
+ const proc = Bun.spawn(["sh", "-c", command], { stdout: "pipe", stderr: "pipe" });
2222
+ const stdout = await new Response(proc.stdout).text();
2223
+ return { stdout, exitCode: await proc.exited };
2224
+ }
2225
+ function planInstall(options) {
2226
+ const { capabilities, ...unit } = options;
2227
+ return planProvision({ ...unit, mode: modeFor(capabilities) });
2228
+ }
2229
+ async function applyPlan(plan, run) {
2230
+ const transcript = [];
2231
+ for (const step of plan.steps) {
2232
+ const result = await run(step.command);
2233
+ transcript.push({ label: step.label, command: step.command, exitCode: result.exitCode });
2234
+ if (result.exitCode !== 0 && !step.optional) {
2235
+ throw new Error(`${step.label} failed (exit ${result.exitCode}): ${step.command}`);
2236
+ }
2237
+ }
2238
+ return transcript;
2239
+ }
2240
+
2241
+ // src/platform-bootstrap-runtime.ts
2242
+ var safeAtom = (name, value) => {
2243
+ if (!value || /[\0\r\n]/.test(value))
2244
+ throw new Error(`${name} must be non-empty and single-line.`);
2245
+ return value;
2246
+ };
2247
+ var boundedInteger = (name, value, minimum, maximum) => {
2248
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
2249
+ throw new Error(`${name} must be an integer from ${minimum} through ${maximum}.`);
2250
+ }
2251
+ return value;
2252
+ };
2253
+ var privateCoordinator = (raw) => {
2254
+ const url = new URL(raw);
2255
+ if (url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
2256
+ throw new Error("ArangoDB coordinator URLs must be credential-free private HTTP origins.");
2257
+ }
2258
+ const host = url.hostname.replace(/^\[|\]$/g, "");
2259
+ 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);
2260
+ if (!privateHost || url.port && url.port !== "8529") {
2261
+ throw new Error("ArangoDB coordinators must use private addresses and port 8529.");
2262
+ }
2263
+ return url.origin;
2264
+ };
2265
+ var httpsOrigin = (name, raw) => {
2266
+ const value = new URL(raw);
2267
+ if (value.protocol !== "https:" || value.username || value.password || value.search || value.hash || value.pathname !== "/") {
2268
+ throw new Error(`${name} must be a credential-free HTTPS origin.`);
2269
+ }
2270
+ return value.origin;
2271
+ };
2272
+ var systemdValue = (name, raw) => {
2273
+ if (/[\0\r\n]/.test(raw))
2274
+ throw new Error(`${name} must be single-line.`);
2275
+ const value = raw;
2276
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
2277
+ };
2278
+ function validatePlatformSharedEnvironment(input, options = {}) {
2279
+ if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
2280
+ throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
2281
+ }
2282
+ if (input.databaseCoordinators.length < 1 || input.databaseCoordinators.length > 16) {
2283
+ throw new Error("databaseCoordinators must contain 1 through 16 endpoints.");
2284
+ }
2285
+ const coordinators = input.databaseCoordinators.map(privateCoordinator);
2286
+ if (new Set(coordinators).size !== coordinators.length)
2287
+ throw new Error("databaseCoordinators must be unique.");
2288
+ if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
2289
+ throw new Error("Database networking must be private-lan or cloudflare-warp.");
2290
+ }
2291
+ boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
2292
+ boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
2293
+ if (input.databaseWriteConcern > input.databaseReplicationFactor) {
2294
+ throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
2295
+ }
2296
+ boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
2297
+ boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
2298
+ boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
2299
+ boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
2300
+ boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
2301
+ if (!Number.isFinite(input.otlpTraceSampleRatio) || input.otlpTraceSampleRatio < 0 || input.otlpTraceSampleRatio > 1) {
2302
+ throw new Error("otlpTraceSampleRatio must be from 0 through 1.");
2303
+ }
2304
+ if (input.otlpEndpoint !== "http://127.0.0.1:4318")
2305
+ throw new Error("OTLP must use the exact local collector endpoint.");
2306
+ validateCollectorUnit(input.otlpCollectorUnit);
2307
+ httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint);
2308
+ for (const [name, value] of Object.entries({
2309
+ nodeHostname: input.nodeHostname,
2310
+ nodeRegion: input.nodeRegion,
2311
+ databaseUser: input.databaseUser,
2312
+ sharedDirectory: input.sharedDirectory,
2313
+ seedSyncEpoch: input.seedSyncEpoch,
2314
+ repository: input.repository,
2315
+ branch: input.branch,
2316
+ deployProfile: input.deployProfile
2317
+ }))
2318
+ safeAtom(name, value);
2319
+ if (!input.sharedDirectory.startsWith("/"))
2320
+ throw new Error("sharedDirectory must be absolute.");
2321
+ for (const peer of input.seedSyncPeers) {
2322
+ const url = new URL(peer);
2323
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
2324
+ throw new Error("Seed peers must be WebSocket URLs.");
2325
+ if (url.username || url.password || url.hash)
2326
+ throw new Error("Seed peers cannot contain credentials or fragments.");
2327
+ }
2328
+ if (input.smtp) {
2329
+ safeAtom("smtp.host", input.smtp.host);
2330
+ boundedInteger("smtp.port", input.smtp.port, 1, 65535);
2331
+ safeAtom("smtp.from", input.smtp.from);
2332
+ if (input.smtp.user)
2333
+ safeAtom("smtp.user", input.smtp.user);
2334
+ }
2335
+ if (input.backup) {
2336
+ httpsOrigin("backup.endpoint", input.backup.endpoint);
2337
+ for (const [name, value] of Object.entries(input.backup))
2338
+ safeAtom(`backup.${name}`, value);
2339
+ }
2340
+ if (input.cloudflare) {
2341
+ 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)) {
2342
+ throw new Error("Cloudflare account, zone, KV and Tunnel ids are malformed.");
2343
+ }
2344
+ const service = new URL(input.cloudflare.tunnelService);
2345
+ if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
2346
+ throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
2347
+ if (input.cloudflare.warp) {
2348
+ 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)) {
2349
+ throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
2350
+ }
2351
+ }
2352
+ }
2353
+ if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
2354
+ throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
2355
+ }
2356
+ return {
2357
+ ...input,
2358
+ databaseCoordinators: coordinators,
2359
+ appOrigin: httpsOrigin("appOrigin", input.appOrigin),
2360
+ apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
2361
+ agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
2362
+ };
2363
+ }
2364
+ function renderPlatformSharedEnvironment(input) {
2365
+ const value = validatePlatformSharedEnvironment(input);
2366
+ const appHost = new URL(value.appOrigin).hostname.split(".").slice(-2).join(".");
2367
+ const apiHost = new URL(value.apiOrigin).hostname.split(".").slice(-2).join(".");
2368
+ const entries = {
2369
+ ARANGO_URL: value.databaseCoordinators[0],
2370
+ ARANGO_URLS: value.databaseCoordinators.join(","),
2371
+ ARANGO_DB: "fz",
2372
+ FZ_DATABASE_MODE: "platform",
2373
+ ARANGO_USER: value.databaseUser,
2374
+ ARANGO_REPLICATION_FACTOR: String(value.databaseReplicationFactor),
2375
+ ARANGO_WRITE_CONCERN: String(value.databaseWriteConcern),
2376
+ FZ_DB_ROLE: value.databaseRole,
2377
+ FZ_SOFTWARE_PROFILE: value.softwareProfile,
2378
+ FZ_ROLE: value.nodeRole,
2379
+ FZ_DB_ADDRESS: value.databaseAddress ?? "",
2380
+ FZ_DB_MASTER: value.databaseMaster ?? "",
2381
+ FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
2382
+ FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
2383
+ FZ_SEED_SYNC_MEMBERS: String(value.seedSyncMembers),
2384
+ FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
2385
+ FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
2386
+ FZ_SHARED_DIR: value.sharedDirectory,
2387
+ FZ_PUBLIC_API_PORT: String(value.publicApiPort),
2388
+ ORIGIN: value.appOrigin,
2389
+ API_ORIGIN: value.apiOrigin,
2390
+ HOST: "127.0.0.1",
2391
+ APP_ORIGINS: value.appOrigin,
2392
+ TRUST_CLOUDFLARE_IP: "1",
2393
+ SESSION_COOKIE_SAMESITE: appHost === apiHost ? "lax" : "none",
2394
+ SESSION_COOKIE_DOMAIN: "",
2395
+ FZ_NODE_HOSTNAME: value.nodeHostname,
2396
+ FZ_NODE_REGION: value.nodeRegion,
2397
+ FZ_CONCURRENCY_LIMIT: String(value.concurrencyLimit),
2398
+ FZ_DRAIN_DEADLINE_MS: String(value.drainDeadlineMs),
2399
+ OTEL_EXPORTER_OTLP_ENDPOINT: value.otlpEndpoint,
2400
+ FZ_OTLP_COLLECTOR_UNIT: value.otlpCollectorUnit,
2401
+ OTEL_SERVICE_NAME: "forgezero-api",
2402
+ FZ_OTLP_FLUSH_INTERVAL_MS: String(value.otlpFlushIntervalMs),
2403
+ FZ_OTLP_TRACE_SAMPLE_RATIO: String(value.otlpTraceSampleRatio),
2404
+ FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
2405
+ FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
2406
+ FZ_PROFILE: value.deployProfile,
2407
+ FZ_REPO: value.repository,
2408
+ FZ_BRANCH: value.branch,
2409
+ FZ_SMTP_HOST: value.smtp?.host ?? "",
2410
+ FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
2411
+ FZ_SMTP_USER: value.smtp?.user ?? "",
2412
+ FZ_SMTP_FROM: value.smtp?.from ?? "",
2413
+ BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
2414
+ BACKUP_S3_REGION: value.backup?.region ?? "",
2415
+ BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
2416
+ BACKUP_S3_ACCESS_KEY_ID: value.backup?.accessKeyId ?? "",
2417
+ FZ_CF_ACCOUNT_ID: value.cloudflare?.accountId ?? "",
2418
+ FZ_CF_ZONE_ID: value.cloudflare?.zoneId ?? "",
2419
+ FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
2420
+ FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
2421
+ FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
2422
+ FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
2423
+ FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
2424
+ FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
2425
+ };
2426
+ return `# Generated by fz bootstrap platform. Non-secret coordinates only.
2427
+ ` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
2428
+ `) + `
2429
+ `;
2430
+ }
2431
+ function platformApiCredentialSpecs(options) {
2432
+ const optional = [
2433
+ ["bootstrap-smtp-password", options.smtp],
2434
+ ["cloudflare-kv-token", options.cloudflareKv],
2435
+ ["cloudflare-network-token", options.cloudflareNetwork]
2436
+ ];
2437
+ return [
2438
+ { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
2439
+ { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
2440
+ ...optional.filter(([, present]) => present).map(([name]) => ({
2441
+ name,
2442
+ encryptedPath: `/etc/forgezero/creds/${name}.cred`,
2443
+ required: false
2444
+ }))
2445
+ ];
2446
+ }
2447
+ function renderPlatformApiUnits(input) {
2448
+ for (const path of [input.sharedDirectory, input.sharedEnvironmentFile, input.slotsDirectory]) {
2449
+ if (!path.startsWith("/") || /[\r\n]/.test(path))
2450
+ throw new Error("Runtime paths must be absolute and single-line.");
2451
+ }
2452
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
2453
+ throw new Error("Invalid service user.");
2454
+ validateCollectorUnit(input.collectorUnit);
2455
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
2456
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
2457
+ if (input.bluePort === input.greenPort)
2458
+ throw new Error("Blue and green ports must differ.");
2459
+ const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
2460
+ `);
2461
+ const template = `[Unit]
2462
+ Description=ForgeZero (%i slot)
2463
+ After=network-online.target ${input.collectorUnit}
2464
+ Wants=network-online.target ${input.collectorUnit}
2465
+
2466
+ [Service]
2467
+ Type=simple
2468
+ User=${input.serviceUser}
2469
+ WorkingDirectory=${input.slotsDirectory}/%i
2470
+ Environment=NODE_ENV=production
2471
+ Environment=FZ_SLOT=%i
2472
+ EnvironmentFile=${input.sharedEnvironmentFile}
2473
+ ${credentials}
2474
+ ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
2475
+ Restart=always
2476
+ RestartSec=2
2477
+ TimeoutStopSec=35s
2478
+ LimitCORE=0
2479
+ UMask=0077
2480
+ NoNewPrivileges=yes
2481
+ PrivateTmp=yes
2482
+ PrivateDevices=yes
2483
+ ProtectSystem=strict
2484
+ ProtectHome=yes
2485
+ ReadOnlyPaths=${input.sharedDirectory}
2486
+ ProtectKernelTunables=yes
2487
+ ProtectKernelModules=yes
2488
+ ProtectControlGroups=yes
2489
+ RestrictSUIDSGID=yes
2490
+ RestrictRealtime=yes
2491
+ LockPersonality=yes
2492
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
2493
+
2494
+ [Install]
2495
+ WantedBy=multi-user.target
2496
+ `;
2497
+ return { template, dropIns: {
2498
+ blue: `[Service]
2499
+ Environment=PORT=${input.bluePort}
2500
+ `,
2501
+ green: `[Service]
2502
+ Environment=PORT=${input.greenPort}
2503
+ `
2504
+ } };
2505
+ }
2506
+ function renderPlatformNginx(input) {
2507
+ boundedInteger("publicPort", input.publicPort, 1024, 65535);
2508
+ boundedInteger("initialSlotPort", input.initialSlotPort, 1024, 65535);
2509
+ if (input.publicPort === input.initialSlotPort)
2510
+ throw new Error("Edge and slot ports must differ.");
2511
+ return {
2512
+ upstream: `upstream forgezero { server 127.0.0.1:${input.initialSlotPort}; }
2513
+ `,
2514
+ site: `server {
2515
+ listen 127.0.0.1:${input.publicPort};
2516
+ server_name _;
2517
+ 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; }
2518
+ 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; }
2519
+ location / { return 404; }
2520
+ }
2521
+ `
2522
+ };
2523
+ }
2524
+ function renderPlatformActivationFiles(input) {
2525
+ if (!input.root.startsWith("/") || /[\0\r\n]/.test(input.root))
2526
+ throw new Error("Activation root must be absolute and single-line.");
2527
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
2528
+ throw new Error("Invalid activation service user.");
2529
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
2530
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
2531
+ if (input.bluePort === input.greenPort)
2532
+ throw new Error("Activation slot ports must differ.");
2533
+ boundedInteger("keepReleases", input.keepReleases, 2, 100);
2534
+ if (!/^\/[A-Za-z0-9/_-]{1,128}$/.test(input.healthPath) || input.healthPath.includes("..")) {
2535
+ throw new Error("Activation health path is malformed.");
2536
+ }
2537
+ const environment = [
2538
+ `FZ_DIR=${input.root}`,
2539
+ `FZ_USER=${input.serviceUser}`,
2540
+ `FZ_BLUE_PORT=${input.bluePort}`,
2541
+ `FZ_GREEN_PORT=${input.greenPort}`,
2542
+ `FZ_HEALTH_PATH=${input.healthPath}`,
2543
+ `FZ_KEEP_RELEASES=${input.keepReleases}`
2544
+ ].join(`
2545
+ `) + `
2546
+ `;
2547
+ const helper = `#!/usr/bin/env bash
2548
+ set -Eeuo pipefail
2549
+ source /etc/forgezero/deploy.env
2550
+ [[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
2551
+ release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
2552
+ install -d -o root -g root -m 0755 "$slots"
2553
+ case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
2554
+ [[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
2555
+ slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
2556
+ if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
2557
+ target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
2558
+ 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 {} +
2559
+ ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
2560
+ 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
2561
+ 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
2562
+ upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
2563
+ printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
2564
+ 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
2565
+ rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"; [[ -n "$previous_slot" && "$previous_slot" != "$target" ]] && systemctl stop "forgezero@\${previous_slot}.service" || true
2566
+ 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-)
2567
+ for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
2568
+ printf 'promoted %s on %s\\n' "$release" "$target"
2569
+ `;
2570
+ return {
2571
+ environment,
2572
+ helper,
2573
+ sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
2574
+ `
2575
+ };
2576
+ }
2577
+ function validateCollectorUnit(unit) {
2578
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit))
2579
+ throw new Error("Invalid OTLP collector service unit.");
2580
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-db)\.service$/.test(unit)) {
2581
+ throw new Error("OTLP collector must be independently supervised.");
2582
+ }
2583
+ }
2584
+ function planLocalOtlpProof(endpoint2, collectorUnit) {
2585
+ if (endpoint2 !== "http://127.0.0.1:4318")
2586
+ throw new Error("OTLP proof requires exact loopback endpoint http://127.0.0.1:4318.");
2587
+ validateCollectorUnit(collectorUnit);
2588
+ return {
2589
+ unitCheck: { command: "systemctl", argv: ["is-active", "--quiet", collectorUnit] },
2590
+ receiverCheck: {
2591
+ command: "curl",
2592
+ acceptedStatus: "2xx",
2593
+ argv: [
2594
+ "--silent",
2595
+ "--show-error",
2596
+ "--max-time",
2597
+ "5",
2598
+ "--output",
2599
+ "/dev/null",
2600
+ "--write-out",
2601
+ "%{http_code}",
2602
+ "--request",
2603
+ "POST",
2604
+ "--header",
2605
+ "Content-Type: application/json",
2606
+ "--data-binary",
2607
+ "{}",
2608
+ `${endpoint2}/v1/metrics`
2609
+ ]
2610
+ }
2611
+ };
2612
+ }
2613
+
2614
+ // src/bootstrap.ts
2615
+ import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
2616
+ var PLATFORM_BOOTSTRAP_PROFILES = [
2617
+ "platform-db-api",
2618
+ "platform-api"
2619
+ ];
2620
+ var STATE_PATH = "/var/lib/forgezero/bootstrap.json";
2621
+ var CREDS = "/etc/forgezero/creds";
2622
+ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
2623
+ var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
2624
+ var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
2625
+ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
2626
+ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
2627
+ var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
2628
+ var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
2629
+ var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
2630
+ var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
2631
+ var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
2632
+ var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
2633
+ var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
2634
+ var privateOrigin = (value) => {
2635
+ let url;
2636
+ try {
2637
+ url = new URL(value);
2638
+ } catch {
2639
+ throw new Error(`database coordinator is malformed: ${value}`);
2640
+ }
2641
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
2642
+ throw new Error(`database coordinators must be credential-free HTTP(S) origins: ${value}`);
2643
+ }
2644
+ const host = url.hostname.replace(/^\[|\]$/g, "");
2645
+ const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
2646
+ 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);
2647
+ const privateV6 = host === "::1" || /^f[cd][0-9a-f]:/i.test(host);
2648
+ if (!privateV4 && !privateV6 && host !== "127.0.0.1" && host !== "localhost") {
2649
+ throw new Error(`database coordinator must use a private address: ${value}`);
2650
+ }
2651
+ return url.origin;
2652
+ };
2653
+ var privateCidr = (value) => {
2654
+ 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])$/);
2655
+ if (!match || match[1].split(".").some((part) => Number(part) > 255))
2656
+ throw new Error(`private firewall CIDR is malformed: ${value}`);
2657
+ return value;
2658
+ };
2659
+ var privateFile = (host, path, label) => {
2660
+ if (!host.exists(path))
2661
+ throw new Error(`${label} file is missing: ${path}`);
2662
+ const metadata = host.inspect?.(path);
2663
+ if (metadata && (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 16 * 1024)) {
2664
+ throw new Error(`${label} must be a root-owned, owner-only regular file with one link and at most 16 KiB`);
2665
+ }
2666
+ const value = host.read(path).trim();
2667
+ if (!value)
2668
+ throw new Error(`${label} file is empty: ${path}`);
2669
+ return value;
2670
+ };
2671
+ function validateBootstrapConfig(value) {
2672
+ 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)) {
2673
+ throw new Error("node hostname must be a lowercase public FQDN");
2674
+ }
2675
+ let telemetry;
2676
+ try {
2677
+ telemetry = new URL(value.telemetryEndpoint);
2678
+ } catch {
2679
+ throw new Error("telemetry endpoint is malformed");
2680
+ }
2681
+ if (telemetry.protocol !== "https:" || telemetry.port && telemetry.port !== "443" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
2682
+ throw new Error("telemetry endpoint must be public HTTPS on port 443 without credentials, query or fragment");
2683
+ }
2684
+ if (value.kind === "tenant") {
2685
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
2686
+ throw new Error("tenant realm is malformed");
2687
+ if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
2688
+ throw new Error("tenant API must be public HTTPS or loopback HTTP");
2689
+ }
2690
+ if (!value.enrolTokenFile)
2691
+ throw new Error("tenant bootstrap requires --enrol-token-file");
2692
+ if (value.bootstrapRunner) {
2693
+ if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
2694
+ throw new Error("bootstrap runner SSH private-key file must be absolute");
2695
+ }
2696
+ let targetTelemetry;
2697
+ try {
2698
+ targetTelemetry = new URL(value.bootstrapRunner.targetTelemetryEndpoint);
2699
+ } catch {
2700
+ throw new Error("bootstrap target telemetry endpoint is malformed");
2701
+ }
2702
+ if (targetTelemetry.protocol !== "https:" || targetTelemetry.username || targetTelemetry.password || targetTelemetry.search || targetTelemetry.hash || targetTelemetry.port && targetTelemetry.port !== "443") {
2703
+ throw new Error("bootstrap target telemetry must be public HTTPS on port 443");
2704
+ }
2705
+ }
2706
+ return value;
2707
+ }
2708
+ if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
2709
+ throw new Error("unsupported platform software profile");
2710
+ if (!["production", "development"].includes(value.environment))
2711
+ throw new Error("platform environment must be production or development");
2712
+ let api;
2713
+ try {
2714
+ api = new URL(value.apiUrl);
2715
+ } catch {
2716
+ throw new Error("platform API URL is malformed");
2717
+ }
2718
+ if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash) {
2719
+ throw new Error("platform API must be public HTTPS without credentials, query or fragment");
2720
+ }
2721
+ const expected = value.profile === "platform-api" ? { role: "none", mode: "default" } : { role: undefined, mode: "default" };
2722
+ if (expected.role && value.database.role !== expected.role || value.database.serverMode !== expected.mode || value.profile === "platform-db-api" && !["master", "joiner"].includes(value.database.role)) {
2723
+ throw new Error("platform profile, database role and Coordinator mode disagree");
2724
+ }
2725
+ if (value.database.role !== "none" && !value.database.address)
2726
+ throw new Error("database nodes require a private address");
2727
+ if (value.database.role === "joiner" && !value.database.master)
2728
+ throw new Error("database joiners require the master starter address");
2729
+ if (!/^(?:dev-)?fz-n[1-9][0-9]{0,2}$/.test(value.computeReference))
2730
+ throw new Error("platform compute reference is malformed");
2731
+ if (!value.database.bootstrapSecretFile)
2732
+ throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
2733
+ const write = value.database.coordinators.map(privateOrigin);
2734
+ if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
2735
+ throw new Error("database coordinators must contain 1-16 unique private origins");
2736
+ }
2737
+ value.database.coordinators = write;
2738
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
2739
+ allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
2740
+ });
2741
+ 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(",")) {
2742
+ throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
2743
+ }
2744
+ if (runtime.deployProfile !== value.environment)
2745
+ throw new Error("runtime deployment profile disagrees with bootstrap environment");
2746
+ if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
2747
+ throw new Error("runtime database topology disagrees with bootstrap topology");
2748
+ }
2749
+ if (!Number.isSafeInteger(value.firewall.sshPort) || value.firewall.sshPort < 1 || value.firewall.sshPort > 65535) {
2750
+ throw new Error("firewall SSH port is invalid");
2751
+ }
2752
+ value.firewall.privateCidrs = value.firewall.privateCidrs.map(privateCidr);
2753
+ if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
2754
+ throw new Error("enabled firewall requires unique private cluster CIDRs");
2755
+ }
2756
+ if (Number(value.computeReference.match(/n(\d+)$/)?.[1]) > 3 && !value.platformEnrolTokenFile) {
2757
+ throw new Error("post-genesis platform computes require an API-issued enrolment token file");
2758
+ }
2759
+ if (value.cloudflareHandoff && (!value.installCloudflared || !value.cloudflareHandoff.checkpointFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
2760
+ throw new Error("Cloudflare handoff requires cloudflared installation, a checkpoint and a valid node name");
2761
+ }
2762
+ value.runtime.environment = runtime;
2763
+ return value;
2764
+ }
2765
+ function planBootstrap(input, initialized = false) {
2766
+ const config = validateBootstrapConfig(structuredClone(input));
2767
+ const software = config.kind === "tenant" ? [
2768
+ ...config.software ?? [],
2769
+ ...config.bootstrapRunner && !config.software?.some(({ id }) => id === "openssh-client") ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
2770
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
2771
+ ] : [
2772
+ ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
2773
+ { id: "bun", version: "1.3.14" },
2774
+ { id: "nginx", version: "ubuntu-26.04" },
2775
+ ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
2776
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
2777
+ ];
2778
+ return {
2779
+ kind: config.kind,
2780
+ mode: initialized ? "repair" : "install",
2781
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
2782
+ software,
2783
+ statePath: STATE_PATH,
2784
+ steps: [
2785
+ { id: "agent", label: "Install and verify the common Agent supervision boundary", mutation: true },
2786
+ { id: "software", label: "Install exact Agent-owned software coordinates in order", mutation: true },
2787
+ { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
2788
+ ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
2789
+ ...config.kind === "platform" ? [
2790
+ { id: "runtime", label: "Install the shared environment, API slots, edge and activation boundary", mutation: true },
2791
+ { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
2792
+ { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
2793
+ ] : [],
2794
+ ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
2795
+ { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
2796
+ { id: "status", label: "Verify services and immutable profile", mutation: false }
2797
+ ]
2798
+ };
2799
+ }
2800
+ var checked = async (host, argv, label, options) => {
2801
+ const result = await host.exec(argv, options);
2802
+ if (result.exitCode !== 0)
2803
+ throw new Error(`${label} failed: ${result.output.trim()}`);
2804
+ return result.output;
2805
+ };
2806
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
2807
+ var unitEscape = (value) => {
2808
+ if (/[^A-Za-z0-9_./:@,+-]/.test(value))
2809
+ throw new Error(`unsafe systemd coordinate: ${value}`);
2810
+ return value;
2811
+ };
2812
+ function databaseUnit(config) {
2813
+ const db = config.database;
2814
+ const join2 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
2815
+ return `[Unit]
2816
+ Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role})
2817
+ After=network-online.target
2818
+ Wants=network-online.target
2819
+
2820
+ [Service]
2821
+ Type=simple
2822
+ User=arangodb
2823
+ Group=arangodb
2824
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
2825
+ 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${join2}
2826
+ Restart=always
2827
+ RestartSec=5
2828
+ UMask=0077
2829
+ NoNewPrivileges=true
2830
+ PrivateTmp=true
2831
+ PrivateDevices=true
2832
+ ProtectSystem=strict
2833
+ ProtectHome=true
2834
+ ReadWritePaths=/var/lib/forgezero-cluster
2835
+ ProtectKernelTunables=true
2836
+ ProtectKernelModules=true
2837
+ ProtectControlGroups=true
2838
+ RestrictSUIDSGID=true
2839
+ LockPersonality=true
2840
+
2841
+ [Install]
2842
+ WantedBy=multi-user.target
2843
+ `;
2844
+ }
2845
+ function databaseVerifyUnit(config) {
2846
+ const { address } = config.database;
2847
+ return `[Unit]
2848
+ Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
2849
+ Requires=forgezero-db.service
2850
+ After=forgezero-db.service
2851
+ PartOf=forgezero-db.service
2852
+
2853
+ [Service]
2854
+ Type=oneshot
2855
+ User=arangodb
2856
+ Group=arangodb
2857
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
2858
+ 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));'
2859
+ RemainAfterExit=yes
2860
+ TimeoutStartSec=200
2861
+ NoNewPrivileges=true
2862
+ PrivateTmp=true
2863
+ PrivateDevices=true
2864
+ ProtectSystem=strict
2865
+ ProtectHome=true
2866
+
2867
+ [Install]
2868
+ WantedBy=multi-user.target
2869
+ `;
2870
+ }
2871
+ function tunnelUnit() {
2872
+ return `[Unit]
2873
+ Description=ForgeZero Cloudflare Tunnel connector
2874
+ After=network-online.target
2875
+ Wants=network-online.target
2876
+
2877
+ [Service]
2878
+ Type=simple
2879
+ DynamicUser=yes
2880
+ LoadCredentialEncrypted=cloudflared-token:${TUNNEL_CREDENTIAL}
2881
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate run --token-file %d/cloudflared-token
2882
+ Restart=always
2883
+ RestartSec=5
2884
+ NoNewPrivileges=true
2885
+ PrivateTmp=true
2886
+ ProtectSystem=strict
2887
+ ProtectHome=true
2888
+
2889
+ [Install]
2890
+ WantedBy=multi-user.target
2891
+ `;
2892
+ }
2893
+ var derive = (root, label) => {
2894
+ if (!/^[a-f0-9]{64}$/i.test(root))
2895
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
2896
+ return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
2897
+ };
2898
+ async function seal(host, name, destination, value) {
2899
+ if (host.exists(destination))
2900
+ return;
2901
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
2902
+ if (result.exitCode !== 0)
2903
+ throw new Error(`could not seal ${name}: ${result.output.trim()}`);
2904
+ }
2905
+ function stateFor(config) {
2906
+ return `${JSON.stringify({
2907
+ format: 1,
2908
+ kind: config.kind,
2909
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
2910
+ nodeHostname: config.nodeHostname,
2911
+ apiUrl: config.apiUrl,
2912
+ ...config.kind === "platform" ? {
2913
+ environment: config.environment,
2914
+ databaseRole: config.database.role,
2915
+ databaseServerMode: config.database.serverMode,
2916
+ databaseAddress: config.database.address,
2917
+ databaseCoordinators: config.database.coordinators,
2918
+ databaseModeEvidence: config.database.role === "none" ? undefined : DB_MODE_EVIDENCE,
2919
+ collectorUnit: config.runtime.environment.otlpCollectorUnit,
2920
+ cloudflared: Boolean(config.cloudflareHandoff)
2921
+ } : { realm: config.realm }
2922
+ }, null, 2)}
2923
+ `;
2924
+ }
2925
+ async function bootstrapStatus(host = localBootstrapHost()) {
2926
+ if (!host.exists(STATE_PATH))
2927
+ return { initialized: false, services: {}, problems: ["bootstrap state is missing"] };
2928
+ let state;
2929
+ try {
2930
+ state = JSON.parse(host.read(STATE_PATH));
2931
+ } catch {
2932
+ return { initialized: false, services: {}, problems: ["bootstrap state is malformed"] };
2933
+ }
2934
+ const units = ["forgezero-agent.service", "forgezero-agent.socket"];
2935
+ if (state.kind === "platform")
2936
+ units.push("nginx.service");
2937
+ if (state.kind === "platform" && state.collectorUnit)
2938
+ units.push(state.collectorUnit);
2939
+ if (state.kind === "platform" && state.cloudflared)
2940
+ units.push("cloudflared.service");
2941
+ if (state.kind === "platform" && state.databaseRole !== "none")
2942
+ units.push("forgezero-db.service", "forgezero-db-verify.service");
2943
+ const services = {};
2944
+ const problems = [];
2945
+ for (const unit of units) {
2946
+ const result = await host.exec(["systemctl", "is-active", "--quiet", unit]);
2947
+ services[unit] = result.exitCode === 0;
2948
+ if (result.exitCode !== 0)
2949
+ problems.push(`${unit} is not active`);
2950
+ }
2951
+ if (state.kind === "platform") {
2952
+ const [blue, green] = await Promise.all([
2953
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
2954
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
2955
+ ]);
2956
+ services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
2957
+ if (!services["forgezero@active.service"])
2958
+ problems.push("neither API slot is active");
2959
+ }
2960
+ if (!host.exists("/var/lib/forgezero/enrolment.json"))
2961
+ problems.push("durable Agent enrolment state is missing");
2962
+ return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
2963
+ }
2964
+ async function applyBootstrap(input, host = localBootstrapHost()) {
2965
+ const config = validateBootstrapConfig(structuredClone(input));
2966
+ if (host.uid() !== 0)
2967
+ throw new Error("fz bootstrap --apply must run as root");
2968
+ if (config.kind === "tenant") {
2969
+ const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
2970
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
2971
+ throw new Error("tenant enrolment token is malformed");
2972
+ }
2973
+ let cloudflare;
2974
+ if (config.kind === "platform" && config.cloudflareHandoff) {
2975
+ cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.checkpointFile, config.cloudflareHandoff.nodeName);
2976
+ const expected = config.runtime.environment.cloudflare;
2977
+ if (cloudflare.hostname !== config.nodeHostname)
2978
+ throw new Error("Cloudflare checkpoint hostname disagrees with platform node hostname");
2979
+ if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
2980
+ throw new Error("Cloudflare checkpoint disagrees with immutable platform runtime coordinates");
2981
+ }
2982
+ const discovered = {
2983
+ accountId: cloudflare.accountId,
2984
+ zoneId: cloudflare.zoneId,
2985
+ kvNamespaceId: cloudflare.kvNamespaceId,
2986
+ tunnelId: cloudflare.tunnelId,
2987
+ tunnelService: cloudflare.service,
2988
+ ...cloudflare.warp ? { warp: {
2989
+ organization: cloudflare.warp.organization,
2990
+ virtualNetworkId: cloudflare.warp.virtualNetworkId,
2991
+ deviceProfileId: cloudflare.warp.deviceProfileId
2992
+ } } : {}
2993
+ };
2994
+ if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
2995
+ throw new Error("Cloudflare checkpoint disagrees with immutable WARP/runtime coordinates");
2996
+ }
2997
+ config.runtime.environment.cloudflare = expected ?? discovered;
2998
+ if (config.runtime.environment.databaseNetworkMode === "cloudflare-warp" !== Boolean(cloudflare.warp)) {
2999
+ throw new Error("Cloudflare checkpoint private-network mode disagrees with the platform database network mode");
3000
+ }
3001
+ }
3002
+ const plan = planBootstrap(config, host.exists(STATE_PATH));
3003
+ const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
3004
+ host.mkdir(CREDS, 448);
3005
+ host.mkdir("/var/lib/forgezero", 448);
3006
+ if (config.kind === "tenant" && config.bootstrapRunner && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3007
+ await seal(host, "bootstrap-ssh-key", BOOTSTRAP_SSH_CREDENTIAL, privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key"));
3008
+ host.remove(config.bootstrapRunner.sshPrivateKeyFile);
3009
+ }
3010
+ if (cloudflare?.warp) {
3011
+ await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
3012
+ await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
3013
+ }
3014
+ await host.installAgent(config, alreadyEnrolled && config.kind === "platform" ? PLATFORM_ENROL_SOURCE : undefined);
3015
+ if (config.kind === "platform" && config.firewall.enabled) {
3016
+ await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
3017
+ } else
3018
+ await host.ensureSoftware(plan.software);
3019
+ if (config.kind === "platform" && config.firewall.enabled) {
3020
+ await checked(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
3021
+ await checked(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
3022
+ await checked(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
3023
+ for (const cidr of config.firewall.privateCidrs) {
3024
+ if (config.database.role !== "none")
3025
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
3026
+ for (const port of [config.runtime.bluePort, config.runtime.greenPort])
3027
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
3028
+ }
3029
+ await checked(host, ["ufw", "--force", "enable"], "firewall activation");
3030
+ await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
3031
+ }
3032
+ if (config.kind === "platform") {
3033
+ const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
3034
+ if (!host.exists(JWT_CREDENTIAL)) {
3035
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
3036
+ }
3037
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
3038
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
3039
+ const credentialFiles = config.runtime.credentialFiles ?? {};
3040
+ for (const [name, source] of Object.entries({
3041
+ "bootstrap-smtp-password": credentialFiles.smtpPassword,
3042
+ "backup-s3-secret": credentialFiles.backupS3Secret
3043
+ })) {
3044
+ if (source) {
3045
+ const destination = `${CREDS}/${name}.cred`;
3046
+ if (!host.exists(destination)) {
3047
+ await seal(host, name, destination, privateFile(host, source, name));
3048
+ host.remove(source);
3049
+ }
3050
+ }
3051
+ }
3052
+ if (cloudflare) {
3053
+ await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
3054
+ if (cloudflare.privateNetworkRuntimeToken)
3055
+ await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
3056
+ }
3057
+ const runtime = config.runtime;
3058
+ const envPath = `${runtime.environment.sharedDirectory}/.env`;
3059
+ const credentials = platformApiCredentialSpecs({
3060
+ smtp: Boolean(credentialFiles.smtpPassword),
3061
+ cloudflareKv: Boolean(cloudflare),
3062
+ cloudflareNetwork: Boolean(cloudflare?.privateNetworkRuntimeToken)
3063
+ });
3064
+ const units = renderPlatformApiUnits({
3065
+ serviceUser: runtime.serviceUser,
3066
+ sharedDirectory: runtime.environment.sharedDirectory,
3067
+ sharedEnvironmentFile: envPath,
3068
+ slotsDirectory: runtime.slotsDirectory,
3069
+ bluePort: runtime.bluePort,
3070
+ greenPort: runtime.greenPort,
3071
+ collectorUnit: runtime.environment.otlpCollectorUnit,
3072
+ credentials
3073
+ });
3074
+ const edge = renderPlatformNginx({ publicPort: runtime.environment.publicApiPort, initialSlotPort: runtime.bluePort });
3075
+ const activation = renderPlatformActivationFiles({
3076
+ root: config.deployRoot ?? "/opt/forgezero",
3077
+ serviceUser: runtime.serviceUser,
3078
+ bluePort: runtime.bluePort,
3079
+ greenPort: runtime.greenPort,
3080
+ healthPath: runtime.healthPath,
3081
+ keepReleases: runtime.keepReleases
3082
+ });
3083
+ await checked(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
3084
+ await checked(host, ["id", runtime.serviceUser], "existing API service account");
3085
+ });
3086
+ host.mkdir(runtime.environment.sharedDirectory, 488);
3087
+ host.mkdir(runtime.slotsDirectory, 493);
3088
+ host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
3089
+ await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
3090
+ host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
3091
+ host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
3092
+ host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
3093
+ host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
3094
+ host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
3095
+ host.write("/etc/forgezero/deploy.env", activation.environment, 420);
3096
+ host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
3097
+ host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
3098
+ await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
3099
+ const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
3100
+ await checked(host, telemetry.unitCheck.argv, "OTLP collector supervision");
3101
+ const otlpStatus = (await checked(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
3102
+ if (!/^2\d\d$/.test(otlpStatus))
3103
+ throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
3104
+ await checked(host, ["nginx", "-t"], "nginx configuration");
3105
+ await checked(host, ["systemctl", "daemon-reload"], "systemd reload");
3106
+ await checked(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
3107
+ if (config.database.role === "master") {
3108
+ const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
3109
+ if (!host.exists(invite)) {
3110
+ host.write(invite, `plt_${randomBytes(24).toString("hex")}
3111
+ `, 384);
3112
+ await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
3113
+ }
3114
+ }
3115
+ if (config.database.role !== "none") {
3116
+ host.mkdir("/var/lib/forgezero-cluster", 448);
3117
+ await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
3118
+ host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
3119
+ host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
3120
+ await checked(host, ["systemctl", "daemon-reload"], "database unit reload");
3121
+ await checked(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
3122
+ const evidence = {
3123
+ expectedMode: "default",
3124
+ role: "COORDINATOR",
3125
+ unit: "forgezero-db-verify.service",
3126
+ verifiedAt: new Date().toISOString()
3127
+ };
3128
+ host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
3129
+ `, 384);
3130
+ }
3131
+ if (!alreadyEnrolled) {
3132
+ 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}`)}`;
3133
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
3134
+ throw new Error("platform enrolment token is malformed");
3135
+ host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
3136
+ `, 384);
3137
+ }
3138
+ await checked(host, [
3139
+ "runuser",
3140
+ "-u",
3141
+ "forgezero-agent",
3142
+ "--",
3143
+ "/usr/local/bin/fz-agent",
3144
+ "deploy",
3145
+ ...config.database.role === "master" ? ["--release-executor"] : []
3146
+ ], "initial Agent deployment");
3147
+ if (!alreadyEnrolled) {
3148
+ await host.installAgent(config, PLATFORM_ENROL_SOURCE);
3149
+ if (config.platformEnrolTokenFile)
3150
+ host.remove(config.platformEnrolTokenFile);
3151
+ }
3152
+ }
3153
+ if (config.kind === "platform" && cloudflare) {
3154
+ if (!host.exists(TUNNEL_CREDENTIAL)) {
3155
+ await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3156
+ }
3157
+ host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
3158
+ await checked(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
3159
+ await checked(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
3160
+ }
3161
+ host.write(STATE_PATH, stateFor(config), 384);
3162
+ const status = await bootstrapStatus(host);
3163
+ if (!status.initialized)
3164
+ throw new Error(`bootstrap verification failed: ${status.problems.join("; ")}`);
3165
+ let launch;
3166
+ if (config.kind === "platform" && config.database.role === "master") {
3167
+ const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
3168
+ if (!host.exists(invitePath))
3169
+ throw new Error("platform invite is missing after deployment");
3170
+ const token = host.read(invitePath).trim();
3171
+ if (!/^plt_[a-f0-9]{48}$/.test(token))
3172
+ throw new Error("platform invite is malformed");
3173
+ launch = {
3174
+ 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)}`,
3175
+ inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
3176
+ };
3177
+ }
3178
+ return { plan, applied: true, status, ...launch ? { launch } : {} };
3179
+ }
3180
+ var exactKeys = (value, allowed, label) => {
3181
+ if (!value || typeof value !== "object" || Array.isArray(value))
3182
+ throw new Error(`${label} must be an object`);
3183
+ const record = value;
3184
+ const unknown = Object.keys(record).filter((key) => !allowed.includes(key));
3185
+ if (unknown.length)
3186
+ throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
3187
+ return record;
3188
+ };
3189
+ function strictBootstrapDocument(value) {
3190
+ const root = exactKeys(value, [
3191
+ "kind",
3192
+ "environment",
3193
+ "profile",
3194
+ "computeReference",
3195
+ "nodeHostname",
3196
+ "apiUrl",
3197
+ "repository",
3198
+ "branch",
3199
+ "deployRoot",
3200
+ "telemetryEndpoint",
3201
+ "database",
3202
+ "platformEnrolTokenFile",
3203
+ "runtime",
3204
+ "firewall",
3205
+ "installCloudflared",
3206
+ "cloudflareHandoff",
3207
+ "realm",
3208
+ "enrolTokenFile",
3209
+ "software",
3210
+ "bootstrapRunner"
3211
+ ], "bootstrap config");
3212
+ if (root.kind === "platform") {
3213
+ exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
3214
+ if (root.cloudflareHandoff !== undefined)
3215
+ exactKeys(root.cloudflareHandoff, ["checkpointFile", "nodeName"], "Cloudflare handoff");
3216
+ exactKeys(root.database, ["role", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
3217
+ const runtime = exactKeys(root.runtime, [
3218
+ "environment",
3219
+ "serviceUser",
3220
+ "slotsDirectory",
3221
+ "bluePort",
3222
+ "greenPort",
3223
+ "healthPath",
3224
+ "keepReleases",
3225
+ "credentialFiles"
3226
+ ], "runtime config");
3227
+ exactKeys(runtime.environment, [
3228
+ "softwareProfile",
3229
+ "databaseRole",
3230
+ "databaseCoordinators",
3231
+ "databaseAddress",
3232
+ "databaseMaster",
3233
+ "databaseNetworkMode",
3234
+ "databaseReplicationFactor",
3235
+ "databaseWriteConcern",
3236
+ "nodeHostname",
3237
+ "nodeRegion",
3238
+ "databaseUser",
3239
+ "nodeRole",
3240
+ "appOrigin",
3241
+ "apiOrigin",
3242
+ "publicApiPort",
3243
+ "sharedDirectory",
3244
+ "seedSyncPeers",
3245
+ "seedSyncMembers",
3246
+ "seedSyncEpoch",
3247
+ "concurrencyLimit",
3248
+ "drainDeadlineMs",
3249
+ "otlpEndpoint",
3250
+ "otlpCollectorUnit",
3251
+ "agentOtlpEndpoint",
3252
+ "custodianEmail",
3253
+ "smtp",
3254
+ "repository",
3255
+ "branch",
3256
+ "deployProfile",
3257
+ "otlpFlushIntervalMs",
3258
+ "otlpTraceSampleRatio",
3259
+ "backup",
3260
+ "cloudflare"
3261
+ ], "runtime environment");
3262
+ if (runtime.credentialFiles !== undefined)
3263
+ exactKeys(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
3264
+ const environment = runtime.environment;
3265
+ if (environment.smtp !== undefined)
3266
+ exactKeys(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
3267
+ if (environment.backup !== undefined)
3268
+ exactKeys(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
3269
+ if (environment.cloudflare !== undefined)
3270
+ exactKeys(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService", "warp"], "Cloudflare runtime config");
3271
+ if (environment.cloudflare && typeof environment.cloudflare === "object" && environment.cloudflare.warp !== undefined) {
3272
+ exactKeys(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
3273
+ }
3274
+ } else if (root.kind === "tenant") {
3275
+ if (root.bootstrapRunner !== undefined)
3276
+ exactKeys(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
3277
+ for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
3278
+ if (root[key] !== undefined && key !== "profile")
3279
+ throw new Error(`tenant bootstrap cannot contain ${key}`);
3280
+ }
3281
+ } else
3282
+ throw new Error("bootstrap config kind must be platform or tenant");
3283
+ return value;
3284
+ }
3285
+ function readBootstrapConfig(path) {
3286
+ const metadata = lstatSync(path);
3287
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
3288
+ throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
3289
+ }
3290
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync(path, "utf8"))));
3291
+ }
3292
+ function localBootstrapHost() {
3293
+ const execute = async (argv, options = {}) => {
3294
+ const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
3295
+ if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
3296
+ child.stdin.write(options.stdin);
3297
+ child.stdin.end();
3298
+ }
3299
+ const [stdout, stderr, exitCode] = await Promise.all([
3300
+ new Response(child.stdout).text(),
3301
+ new Response(child.stderr).text(),
3302
+ child.exited
3303
+ ]);
3304
+ return { exitCode, output: `${stdout}${stderr}` };
3305
+ };
3306
+ return {
3307
+ uid: () => process.getuid?.() ?? -1,
3308
+ exists: existsSync,
3309
+ read: (path) => readFileSync(path, "utf8"),
3310
+ write(path, content, mode) {
3311
+ mkdirSync(dirname2(path), { recursive: true, mode: 493 });
3312
+ const temporary = `${path}.next.${process.pid}`;
3313
+ writeFileSync(temporary, content, { mode });
3314
+ chmodSync(temporary, mode);
3315
+ renameSync(temporary, path);
3316
+ },
3317
+ mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
3318
+ remove: (path) => rmSync(path, { force: true }),
3319
+ inspect(path) {
3320
+ const value = lstatSync(path);
3321
+ return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
3322
+ },
3323
+ exec: execute,
3324
+ async ensureSoftware(requirements) {
3325
+ const result = await execute([
3326
+ "runuser",
3327
+ "-u",
3328
+ "forgezero-agent",
3329
+ "--",
3330
+ "/usr/local/bin/fz-agent",
3331
+ "software-ensure",
3332
+ ...requirements.map(({ id, version }) => `--require=${id}@${version}`)
3333
+ ]);
3334
+ if (result.exitCode !== 0)
3335
+ throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
3336
+ return result;
3337
+ },
3338
+ async installAgent(config, enrolTokenSourcePath) {
3339
+ const capabilities = await readCapabilities(localRunner);
3340
+ const deployRoot = config.deployRoot ?? "/opt/forgezero";
3341
+ if (config.kind === "platform") {
3342
+ const lifecycle = config.database.role === "none" ? {
3343
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
3344
+ apiHealthUrl: "http://127.0.0.1:3000/api/health"
3345
+ } : {
3346
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
3347
+ databaseUnit: "forgezero-db.service",
3348
+ apiHealthUrl: "http://127.0.0.1:3000/api/health",
3349
+ databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
3350
+ databasePorts: [8529]
3351
+ };
3352
+ mkdirSync(dirname2(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
3353
+ writeFileSync(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
3354
+ `, { mode: 256 });
3355
+ }
3356
+ const plan = planInstall({
3357
+ capabilities,
3358
+ socketPath: DEFAULT_SOCKET2,
3359
+ seedPath: "/var/lib/forgezero/node.seed",
3360
+ controlSocketPath: "/run/forgezero/control.sock",
3361
+ repository: config.repository,
3362
+ branch: config.branch,
3363
+ profile: config.kind === "platform" ? config.profile : config.profile,
3364
+ deployRoot,
3365
+ publicApiUrl: config.apiUrl,
3366
+ gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
3367
+ gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
3368
+ generateGitIdentity: true,
3369
+ pullDeployments: true,
3370
+ pullMigrations: config.kind === "platform",
3371
+ pullBootstrap: config.kind === "tenant" && Boolean(config.bootstrapRunner),
3372
+ bootstrapSshCredentialPath: config.kind === "tenant" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
3373
+ bootstrapTargetTelemetryEndpoint: config.kind === "tenant" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
3374
+ lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
3375
+ ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
3376
+ warpOrganization: config.runtime.environment.cloudflare.warp.organization,
3377
+ warpClientIdCredentialPath: WARP_CLIENT_ID_CREDENTIAL,
3378
+ warpClientSecretCredentialPath: WARP_CLIENT_SECRET_CREDENTIAL,
3379
+ cloudflareAccountId: config.runtime.environment.cloudflare.accountId,
3380
+ cloudflareTunnelId: config.runtime.environment.cloudflare.tunnelId,
3381
+ cloudflareVirtualNetworkId: config.runtime.environment.cloudflare.warp.virtualNetworkId,
3382
+ cloudflareWarpPolicyId: config.runtime.environment.cloudflare.warp.deviceProfileId
3383
+ } : {},
3384
+ enforceEgress: true,
3385
+ nodeHostname: config.nodeHostname,
3386
+ telemetryEndpoint: config.telemetryEndpoint,
3387
+ binPath: "/usr/local/lib/forgezero/agent/fz-agent",
3388
+ sourceBinPath: PACKAGED_AGENT_BIN,
3389
+ ...config.kind === "tenant" || enrolTokenSourcePath ? {
3390
+ enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
3391
+ enrolTokenCredentialPath: ENROL_CREDENTIAL,
3392
+ enrolStatePath: "/var/lib/forgezero/enrolment.json",
3393
+ apiUrl: config.apiUrl,
3394
+ project: config.kind === "tenant" ? config.realm : "platform",
3395
+ environment: config.kind === "tenant" ? undefined : config.environment,
3396
+ nodeLabel: config.kind === "tenant" ? config.nodeHostname : config.computeReference
3397
+ } : {}
3398
+ });
3399
+ for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
3400
+ mkdirSync(dirname2(unit.path), { recursive: true, mode: 493 });
3401
+ writeFileSync(unit.path, unit.unit, { mode: 420 });
3402
+ }
3403
+ await applyPlan(plan, localRunner);
3404
+ return plan;
3405
+ }
3406
+ };
3407
+ }
3408
+ export {
3409
+ validateBootstrapConfig,
3410
+ readBootstrapConfig,
3411
+ planBootstrap,
3412
+ localBootstrapHost,
3413
+ bootstrapStatus,
3414
+ applyBootstrap,
3415
+ PLATFORM_BOOTSTRAP_PROFILES
3416
+ };