@forgezero/agent 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3361 @@
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.35";
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
+
1251
+ // src/software-helper.ts
1252
+ var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
1253
+ var SOFTWARE_HELPER_GROUP = "forgezero-software";
1254
+ var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
1255
+ var MAX_REQUEST_BYTES2 = 8 * 1024;
1256
+
1257
+ // src/egress-policy.ts
1258
+ var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
1259
+ var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
1260
+ var BLOCKED_IPV4 = [
1261
+ "0.0.0.0/8",
1262
+ "10.0.0.0/8",
1263
+ "100.64.0.0/10",
1264
+ "127.0.0.0/8",
1265
+ "168.63.129.16/32",
1266
+ "169.254.0.0/16",
1267
+ "172.16.0.0/12",
1268
+ "192.0.0.0/24",
1269
+ "192.0.2.0/24",
1270
+ "192.88.99.0/24",
1271
+ "192.168.0.0/16",
1272
+ "198.18.0.0/15",
1273
+ "198.51.100.0/24",
1274
+ "203.0.113.0/24",
1275
+ "224.0.0.0/4",
1276
+ "240.0.0.0/4"
1277
+ ];
1278
+ var BLOCKED_IPV6 = [
1279
+ "::/128",
1280
+ "::1/128",
1281
+ "::ffff:0:0/96",
1282
+ "64:ff9b::/96",
1283
+ "64:ff9b:1::/48",
1284
+ "100::/64",
1285
+ "fc00::/7",
1286
+ "fec0::/10",
1287
+ "fe80::/10",
1288
+ "ff00::/8",
1289
+ "2001::/32",
1290
+ "2001:2::/48",
1291
+ "2001:10::/28",
1292
+ "2001:20::/28",
1293
+ "2001:db8::/32",
1294
+ "2002::/16",
1295
+ "3fff::/20"
1296
+ ];
1297
+ var normalizeEgressTcpPorts = (ports) => {
1298
+ for (const port of ports) {
1299
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
1300
+ throw new Error("Agent egress policy refuses an invalid loopback TCP port.");
1301
+ }
1302
+ }
1303
+ return [...new Set(ports)].sort((left, right) => left - right);
1304
+ };
1305
+ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
1306
+ const ports = normalizeEgressTcpPorts(loopbackTcpPorts);
1307
+ return [
1308
+ "RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6",
1309
+ `IPAddressAllow=${SYSTEMD_RESOLVED_ADDRESS}/32`,
1310
+ ...ports.length > 0 ? ["IPAddressAllow=127.0.0.1/32", "IPAddressAllow=::1/128"] : [],
1311
+ ...BLOCKED_IPV4.map((network) => `IPAddressDeny=${network}`),
1312
+ ...BLOCKED_IPV6.map((network) => `IPAddressDeny=${network}`)
1313
+ ].join(`
1314
+ `);
1315
+ }
1316
+
1317
+ // src/provision.ts
1318
+ import { isIP as isIP3 } from "net";
1319
+ function atLeast(version, floor) {
1320
+ const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
1321
+ const got = parse(version);
1322
+ const want = parse(floor);
1323
+ if (got.length === 0)
1324
+ return false;
1325
+ for (let index = 0;index < want.length; index += 1) {
1326
+ const a = got[index] ?? 0;
1327
+ const b = want[index] ?? 0;
1328
+ if (a > b)
1329
+ return true;
1330
+ if (a < b)
1331
+ return false;
1332
+ }
1333
+ return true;
1334
+ }
1335
+ var CAPABILITY_CHECKS = {
1336
+ snpGuest: {
1337
+ command: "test -e /dev/sev-guest && echo yes || echo no",
1338
+ satisfied: (stdout) => stdout.trim() === "yes",
1339
+ remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
1340
+ },
1341
+ systemd: {
1342
+ command: "test -d /run/systemd/system && echo yes || echo no",
1343
+ satisfied: (stdout) => stdout.trim() === "yes",
1344
+ remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
1345
+ },
1346
+ bun: {
1347
+ command: "bun --version 2>/dev/null || echo missing",
1348
+ satisfied: (stdout) => atLeast(stdout, "1.1.0"),
1349
+ remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
1350
+ },
1351
+ python: {
1352
+ command: "python3 --version 2>/dev/null || echo missing",
1353
+ satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
1354
+ remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
1355
+ }
1356
+ };
1357
+ var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
1358
+ 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.";
1359
+ var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
1360
+ var DEPLOYMENT_GROUP = "forgezero-deploy";
1361
+ var VAULT_GROUP = "forgezero-vault";
1362
+ var LIFECYCLE_GROUP = "forgezero-lifecycle";
1363
+ var DEPLOYMENT_RUNNER_UNIT_PATH = "/etc/systemd/system/forgezero-deploy-runner.service";
1364
+ var AGENT_SOCKET_UNIT_PATH = "/etc/systemd/system/forgezero-agent.socket";
1365
+ var AGENT_SOCKET_PROXY_UNIT_PATH = "/etc/systemd/system/forgezero-agent-proxy.service";
1366
+ var DEPLOYMENT_RUNNER_SOCKET = "/run/forgezero-deploy/runner.sock";
1367
+ var ENROLMENT_UNIT_PATH = "/etc/systemd/system/forgezero-agent-enrol.service";
1368
+ var LIFECYCLE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-lifecycle-helper.service";
1369
+ var LIFECYCLE_HELPER_SOCKET = "/run/forgezero-lifecycle/helper.sock";
1370
+ var WARP_CONFIG_UNIT_PATH = "/etc/systemd/system/forgezero-warp-config.service";
1371
+ var WARP_SERVICE_DROP_IN_PATH = "/etc/systemd/system/warp-svc.service.d/forgezero.conf";
1372
+ var AGENT_EGRESS_UNIT_PATH = "/etc/systemd/system/forgezero-agent-egress.service";
1373
+ var DEFAULT_RUNNER_PUBLIC_TCP_PORTS = [443];
1374
+ function agentEgressUnit(options) {
1375
+ const bin = options.binPath ?? "fz-agent";
1376
+ const user = options.user ?? "forgezero";
1377
+ if (!/^[a-z_][a-z0-9_-]{0,30}$/.test(user))
1378
+ throw new Error("invalid Agent service user");
1379
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1380
+ const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1381
+ const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1382
+ if (deploymentEnabled && runnerPublicTcpPorts.length < 1) {
1383
+ throw new Error("deployed project runner needs at least one vetted public TCP port");
1384
+ }
1385
+ if (deploymentEnabled)
1386
+ systemdAgentEgressDirectives(runnerLoopbackPorts);
1387
+ const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
1388
+ const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
1389
+ const policyProofs = deploymentEnabled ? [
1390
+ ...runnerLoopbackPorts.length > 0 ? [`loopback=.*:${runnerLoopbackPorts.join(",")}`] : [],
1391
+ `public-tcp=${runnerPublicTcpPorts.join(",")}`
1392
+ ].map((pattern) => `ExecStartPost=/bin/sh -c '/usr/sbin/nft --numeric list table inet ${AGENT_EGRESS_TABLE} | /usr/bin/grep -q "${pattern}"'`).join(`
1393
+ `) : "";
1394
+ return `[Unit]
1395
+ Description=ForgeZero Agent host egress policy
1396
+ Documentation=https://www.forgezero.net/docs/agent
1397
+ After=systemd-resolved.service nftables.service
1398
+ Requires=systemd-resolved.service
1399
+ Before=forgezero-agent-enrol.service forgezero-agent.service
1400
+
1401
+ [Service]
1402
+ Type=notify
1403
+ NotifyAccess=all
1404
+ User=root
1405
+ Group=root
1406
+ ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
1407
+ ${policyProofs}
1408
+ Restart=on-failure
1409
+ RestartSec=2
1410
+ LimitCORE=0
1411
+ NoNewPrivileges=true
1412
+ PrivateTmp=true
1413
+ ProtectSystem=strict
1414
+ ProtectHome=true
1415
+ ProtectKernelTunables=true
1416
+ ProtectKernelModules=true
1417
+ ProtectControlGroups=true
1418
+ RestrictSUIDSGID=true
1419
+ RestrictRealtime=true
1420
+ MemoryDenyWriteExecute=true
1421
+ LockPersonality=true
1422
+ CapabilityBoundingSet=CAP_NET_ADMIN
1423
+ RestrictAddressFamilies=AF_UNIX AF_NETLINK
1424
+
1425
+ [Install]
1426
+ WantedBy=multi-user.target
1427
+ `;
1428
+ }
1429
+ function softwareHelperUnit(options) {
1430
+ const bin = options.binPath ?? "fz-agent";
1431
+ return `[Unit]
1432
+ Description=ForgeZero declarative software strategy helper
1433
+ Documentation=https://www.forgezero.net/docs/agent
1434
+ After=network-online.target
1435
+ Wants=network-online.target
1436
+
1437
+ [Service]
1438
+ Type=simple
1439
+ User=root
1440
+ Group=${SOFTWARE_HELPER_GROUP}
1441
+ Environment=FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}
1442
+ ExecStart=${bin} software-helper
1443
+ Restart=always
1444
+ RestartSec=2
1445
+ RuntimeDirectory=forgezero-software
1446
+ RuntimeDirectoryMode=0750
1447
+ UMask=0007
1448
+ LimitCORE=0
1449
+ PrivateTmp=true
1450
+ ProtectHome=true
1451
+ ProtectKernelTunables=true
1452
+ ProtectKernelModules=true
1453
+ ProtectControlGroups=true
1454
+ RestrictRealtime=true
1455
+ LockPersonality=true
1456
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1457
+
1458
+ [Install]
1459
+ WantedBy=multi-user.target
1460
+ `;
1461
+ }
1462
+ function agentUpdateHelperUnit(options) {
1463
+ const bin = options.binPath ?? "fz-agent";
1464
+ return `[Unit]
1465
+ Description=ForgeZero verified Agent update helper
1466
+ Documentation=https://www.forgezero.net/docs/agent
1467
+ After=network-online.target
1468
+ Wants=network-online.target
1469
+
1470
+ [Service]
1471
+ Type=simple
1472
+ User=root
1473
+ Group=${AGENT_UPDATE_GROUP}
1474
+ Environment=FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}
1475
+ ExecStart=${bin} update-helper
1476
+ Restart=always
1477
+ RestartSec=2
1478
+ RuntimeDirectory=forgezero-update
1479
+ RuntimeDirectoryMode=0750
1480
+ UMask=0007
1481
+ LimitCORE=0
1482
+ NoNewPrivileges=true
1483
+ PrivateTmp=true
1484
+ ProtectSystem=strict
1485
+ ProtectHome=true
1486
+ ProtectKernelTunables=true
1487
+ ProtectKernelModules=true
1488
+ ProtectControlGroups=true
1489
+ RestrictSUIDSGID=true
1490
+ RestrictRealtime=true
1491
+ LockPersonality=true
1492
+ ReadWritePaths=${DEFAULT_AGENT_RELEASE_ROOT} /var/lib/forgezero
1493
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1494
+
1495
+ [Install]
1496
+ WantedBy=multi-user.target
1497
+ `;
1498
+ }
1499
+ function agentSocketUnit(options) {
1500
+ const socket = systemdPath(options.socketPath, "agent socket");
1501
+ return `[Unit]
1502
+ Description=ForgeZero application Vault socket
1503
+ Documentation=https://www.forgezero.net/docs/agent
1504
+
1505
+ [Socket]
1506
+ ListenStream=${socket}
1507
+ SocketUser=root
1508
+ SocketGroup=${VAULT_GROUP}
1509
+ SocketMode=0660
1510
+ DirectoryMode=0750
1511
+ RemoveOnStop=true
1512
+ Service=forgezero-agent-proxy.service
1513
+
1514
+ [Install]
1515
+ WantedBy=sockets.target
1516
+ `;
1517
+ }
1518
+ function agentSocketProxyUnit(options) {
1519
+ const backend = agentBackendSocketPath(options.socketPath);
1520
+ const user = options.user ?? "forgezero";
1521
+ return `[Unit]
1522
+ Description=ForgeZero application Vault socket proxy
1523
+ Documentation=https://www.forgezero.net/docs/agent
1524
+ Requires=forgezero-agent.service
1525
+ After=forgezero-agent.service
1526
+
1527
+ [Service]
1528
+ User=${user}
1529
+ Group=${VAULT_GROUP}
1530
+ ExecStart=/usr/lib/systemd/systemd-socket-proxyd ${backend}
1531
+ NoNewPrivileges=true
1532
+ PrivateTmp=true
1533
+ ProtectSystem=strict
1534
+ ProtectHome=true
1535
+ ProtectKernelTunables=true
1536
+ ProtectKernelModules=true
1537
+ ProtectControlGroups=true
1538
+ RestrictSUIDSGID=true
1539
+ RestrictRealtime=true
1540
+ MemoryDenyWriteExecute=true
1541
+ LockPersonality=true
1542
+ RestrictAddressFamilies=AF_UNIX
1543
+ `;
1544
+ }
1545
+ function agentBackendSocketPath(publicSocketPath) {
1546
+ const socket = systemdPath(publicSocketPath, "agent socket");
1547
+ const backend = `${socket}.backend`;
1548
+ if (Buffer.byteLength(backend) > 100)
1549
+ throw new Error("agent socket path is too long for a Unix socket");
1550
+ return backend;
1551
+ }
1552
+ var systemdPath = (value, label) => {
1553
+ if (!value || !/^\/[A-Za-z0-9._@/-]+$/.test(value))
1554
+ throw new Error(`invalid ${label} path`);
1555
+ return value;
1556
+ };
1557
+ var awaitSocketCommand = (path) => {
1558
+ const socket = systemdPath(path, "readiness socket");
1559
+ return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
1560
+ };
1561
+ 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));
1562
+ function warpConfigUnit(options) {
1563
+ if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
1564
+ throw new Error("WARP organization is invalid");
1565
+ }
1566
+ if (!options.warpClientIdCredentialPath || !options.warpClientSecretCredentialPath) {
1567
+ throw new Error("WARP service-token credential paths are required");
1568
+ }
1569
+ const clientIdPath = systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential");
1570
+ const clientSecretPath = systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential");
1571
+ const bin = options.binPath ?? "fz-agent";
1572
+ return `[Unit]
1573
+ Description=Materialize Cloudflare One enrollment in tmpfs
1574
+ Documentation=https://www.forgezero.net/docs/agent
1575
+ Before=warp-svc.service
1576
+
1577
+ [Service]
1578
+ Type=oneshot
1579
+ RemainAfterExit=yes
1580
+ LoadCredentialEncrypted=warp-auth-client-id:${clientIdPath}
1581
+ LoadCredentialEncrypted=warp-auth-client-secret:${clientSecretPath}
1582
+ Environment=FZ_WARP_CLIENT_ID_CREDENTIAL=warp-auth-client-id
1583
+ Environment=FZ_WARP_CLIENT_SECRET_CREDENTIAL=warp-auth-client-secret
1584
+ ExecStart=${bin} warp-config --organization=${options.warpOrganization}
1585
+ RuntimeDirectory=forgezero-warp
1586
+ RuntimeDirectoryMode=0700
1587
+ RuntimeDirectoryPreserve=yes
1588
+ UMask=0077
1589
+ LimitCORE=0
1590
+ NoNewPrivileges=true
1591
+ PrivateTmp=true
1592
+ ProtectSystem=strict
1593
+ ProtectHome=true
1594
+ ReadWritePaths=/var/lib/cloudflare-warp
1595
+
1596
+ [Install]
1597
+ WantedBy=multi-user.target
1598
+ `;
1599
+ }
1600
+ function warpServiceDropIn() {
1601
+ return `[Unit]
1602
+ Requires=forgezero-warp-config.service
1603
+ After=forgezero-warp-config.service
1604
+ `;
1605
+ }
1606
+ function lifecycleHelperUnit(options) {
1607
+ if (!options.lifecycleProfilePath)
1608
+ throw new Error("lifecycle helper needs a root-owned profile");
1609
+ const bin = options.binPath ?? "fz-agent";
1610
+ const profile = systemdPath(options.lifecycleProfilePath, "lifecycle profile");
1611
+ const socket = systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket");
1612
+ return `[Unit]
1613
+ Description=ForgeZero fixed-operation compute lifecycle helper
1614
+ Documentation=https://www.forgezero.net/docs/agent
1615
+ After=network-online.target
1616
+ Wants=network-online.target
1617
+
1618
+ [Service]
1619
+ Type=simple
1620
+ User=root
1621
+ Group=${LIFECYCLE_GROUP}
1622
+ Environment=FZ_LIFECYCLE_HELPER_SOCKET=${socket}
1623
+ ExecStart=${bin} lifecycle-helper --profile=${profile}
1624
+ Restart=always
1625
+ RestartSec=2
1626
+ RuntimeDirectory=forgezero-lifecycle
1627
+ RuntimeDirectoryMode=0750
1628
+ UMask=0007
1629
+ LimitCORE=0
1630
+ NoNewPrivileges=true
1631
+ PrivateTmp=true
1632
+ ProtectSystem=strict
1633
+ ProtectHome=true
1634
+ ProtectKernelTunables=true
1635
+ ProtectKernelModules=true
1636
+ ProtectControlGroups=true
1637
+ RestrictSUIDSGID=true
1638
+ RestrictRealtime=true
1639
+ MemoryDenyWriteExecute=true
1640
+ LockPersonality=true
1641
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
1642
+
1643
+ [Install]
1644
+ WantedBy=multi-user.target
1645
+ `;
1646
+ }
1647
+ function agentEnrolmentUnit(options) {
1648
+ if (!options.apiUrl || !options.enrolTokenCredentialPath || !options.enrolStatePath) {
1649
+ throw new Error("direct enrolment needs API, credential and state paths");
1650
+ }
1651
+ if (!validNodeHostname(options.nodeHostname))
1652
+ throw new Error("node hostname is invalid");
1653
+ const bin = options.binPath ?? "fz-agent";
1654
+ const user = options.user ?? "forgezero";
1655
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1656
+ const label = options.nodeLabel ? `Environment=FZ_NODE_LABEL=${options.nodeLabel}
1657
+ ` : "";
1658
+ const hostname = options.nodeHostname ? `Environment=FZ_NODE_HOSTNAME=${options.nodeHostname}
1659
+ ` : "";
1660
+ const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
1661
+ ` : "";
1662
+ const networkAttachment = [
1663
+ options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
1664
+ ` : "",
1665
+ options.cloudflareTunnelId ? `Environment=FZ_CF_TUNNEL_ID=${options.cloudflareTunnelId}
1666
+ ` : "",
1667
+ options.cloudflareVirtualNetworkId ? `Environment=FZ_CF_VIRTUAL_NETWORK_ID=${options.cloudflareVirtualNetworkId}
1668
+ ` : "",
1669
+ options.cloudflareWarpPolicyId ? `Environment=FZ_CF_WARP_POLICY_ID=${options.cloudflareWarpPolicyId}
1670
+ ` : ""
1671
+ ].join("");
1672
+ const stateDir = options.enrolStatePath.replace(/\/[^/]+$/, "");
1673
+ const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
1674
+ Requires=forgezero-agent-egress.service
1675
+ BindsTo=forgezero-agent-egress.service
1676
+ ` : "";
1677
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
1678
+ return `[Unit]
1679
+ Description=Bind this machine to its ForgeZero compute
1680
+ After=network-online.target
1681
+ Wants=network-online.target
1682
+ ${egressDependency}Before=forgezero-agent.service
1683
+ ConditionPathExists=!${options.enrolStatePath}
1684
+
1685
+ [Service]
1686
+ Type=oneshot
1687
+ User=${user}
1688
+ Group=${user}
1689
+ LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1690
+ LoadCredentialEncrypted=enrol-token:${options.enrolTokenCredentialPath}
1691
+ Environment=FZ_SEED_CREDENTIAL=agent-seed
1692
+ Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
1693
+ Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
1694
+ Environment=FZ_API=${options.apiUrl}
1695
+ ${label}${hostname}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
1696
+ # A '+' fixed command runs as root solely to remove the host-bound one-time
1697
+ # ciphertext. Tenant code and the agent never receive a privilege boundary.
1698
+ ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
1699
+ NoNewPrivileges=true
1700
+ PrivateTmp=true
1701
+ ProtectSystem=strict
1702
+ ProtectHome=true
1703
+ ReadWritePaths=${stateDir}
1704
+ LimitCORE=0
1705
+ ${egressDirectives}
1706
+
1707
+ [Install]
1708
+ WantedBy=multi-user.target
1709
+ `;
1710
+ }
1711
+ function deploymentRunnerUnit(options) {
1712
+ const bin = options.binPath ?? "fz-agent";
1713
+ const root = options.deployRoot ?? "/opt/forgezero";
1714
+ const agentUser = options.user ?? "forgezero-agent";
1715
+ const egressDependency = options.enforceEgress ? `After=forgezero-agent-egress.service
1716
+ Requires=forgezero-agent-egress.service
1717
+ BindsTo=forgezero-agent-egress.service
1718
+ ` : "";
1719
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives(options.runnerLoopbackPorts ?? []) : "";
1720
+ return `[Unit]
1721
+ Description=ForgeZero credential-free project command runner
1722
+ Documentation=https://www.forgezero.net/docs/agent
1723
+ ${egressDependency}
1724
+
1725
+ [Service]
1726
+ Type=notify
1727
+ NotifyAccess=all
1728
+ User=${DEPLOYMENT_RUNNER_USER}
1729
+ Group=${DEPLOYMENT_GROUP}
1730
+ Environment=FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}
1731
+ RuntimeDirectory=forgezero-deploy
1732
+ RuntimeDirectoryMode=0710
1733
+ ExecStartPre=+/usr/bin/install -d -o ${DEPLOYMENT_RUNNER_USER} -g ${DEPLOYMENT_GROUP} -m 0710 /run/forgezero-deploy
1734
+ ExecStartPre=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
1735
+ ExecStart=${bin} deploy-runner --root=${root} --home=${root}/runner-home
1736
+ ExecStartPost=+/usr/bin/chown ${agentUser}:${agentUser} ${DEPLOYMENT_RUNNER_SOCKET}
1737
+ ExecStartPost=+/usr/bin/chmod 0600 ${DEPLOYMENT_RUNNER_SOCKET}
1738
+ ExecStartPost=+/usr/bin/chown root:${VAULT_GROUP} /run/forgezero-deploy
1739
+ ExecStopPost=+/usr/bin/rm -f ${DEPLOYMENT_RUNNER_SOCKET}
1740
+ Restart=always
1741
+ RestartSec=2
1742
+ UMask=0007
1743
+ LimitCORE=0
1744
+ NoNewPrivileges=false
1745
+ PrivateTmp=true
1746
+ ProtectSystem=strict
1747
+ ProtectHome=true
1748
+ ProtectKernelTunables=true
1749
+ ProtectKernelModules=true
1750
+ ProtectControlGroups=true
1751
+ RestrictRealtime=true
1752
+ MemoryDenyWriteExecute=true
1753
+ LockPersonality=true
1754
+ ${egressDirectives}
1755
+ ReadWritePaths=${root}/releases ${root}/runner-home
1756
+
1757
+ [Install]
1758
+ WantedBy=multi-user.target
1759
+ `;
1760
+ }
1761
+ function agentUnit(options) {
1762
+ if (!validNodeHostname(options.nodeHostname))
1763
+ throw new Error("node hostname is invalid");
1764
+ if (!options.telemetryEndpoint) {
1765
+ throw new Error("compute Agent provisioning requires OTEL_EXPORTER_OTLP_ENDPOINT as an explicit public HTTPS collector coordinate");
1766
+ }
1767
+ let telemetryEndpoint;
1768
+ {
1769
+ let endpoint2;
1770
+ try {
1771
+ endpoint2 = new URL(options.telemetryEndpoint);
1772
+ } catch {
1773
+ throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL");
1774
+ }
1775
+ 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"))
1776
+ throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
1777
+ telemetryEndpoint = endpoint2.toString().replace(/\/$/, "");
1778
+ }
1779
+ const bin = options.binPath ?? "fz-agent";
1780
+ const user = options.user ?? "forgezero";
1781
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1782
+ const controlSocketPath = options.controlSocketPath ?? "/run/forgezero/control.sock";
1783
+ const deployRoot = options.deployRoot ?? "/opt/forgezero";
1784
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1785
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
1786
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1787
+ throw new Error("migration pull and lifecycle profile must be supplied together");
1788
+ }
1789
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1790
+ const warpValues = [
1791
+ options.warpOrganization,
1792
+ options.warpClientIdCredentialPath,
1793
+ options.warpClientSecretCredentialPath
1794
+ ];
1795
+ const warpEnabled = warpValues.every(Boolean);
1796
+ if (warpValues.some(Boolean) && !warpEnabled)
1797
+ throw new Error("WARP configuration must be supplied together");
1798
+ const networkAttachmentValues = [
1799
+ options.cloudflareAccountId,
1800
+ options.cloudflareTunnelId,
1801
+ options.cloudflareVirtualNetworkId,
1802
+ options.cloudflareWarpPolicyId
1803
+ ];
1804
+ if (networkAttachmentValues.some(Boolean)) {
1805
+ if (!options.cloudflareAccountId || !options.cloudflareTunnelId || !options.cloudflareWarpPolicyId) {
1806
+ throw new Error("private-network attachment requires account, Tunnel and WARP policy ids");
1807
+ }
1808
+ 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;
1809
+ 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)) {
1810
+ throw new Error("private-network attachment coordinates are invalid");
1811
+ }
1812
+ }
1813
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
1814
+ const deploymentEnvironment = options.deploymentEnvironment ?? {};
1815
+ const deploymentCredentials = options.deploymentCredentials ?? {};
1816
+ for (const [name, value] of Object.entries(deploymentEnvironment)) {
1817
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !/^[A-Za-z0-9._:\/@+-]+$/.test(value)) {
1818
+ throw new Error(`invalid deployment environment entry: ${name}`);
1819
+ }
1820
+ }
1821
+ for (const [name, path] of Object.entries(deploymentCredentials)) {
1822
+ if (!/^[A-Z_][A-Z0-9_]*$/.test(name) || !path.startsWith("/") || /[\r\n:]/.test(path)) {
1823
+ throw new Error(`invalid deployment credential entry: ${name}`);
1824
+ }
1825
+ }
1826
+ const environment = [
1827
+ "NODE_ENV=production",
1828
+ `FZ_SOCKET_PATH=${agentBackendSocketPath(options.socketPath)}`,
1829
+ `FZ_CONTROL_SOCKET=${controlSocketPath}`,
1830
+ `FZ_SEED_CREDENTIAL=agent-seed`,
1831
+ `FZ_AGENT_MODE=${options.mode}`,
1832
+ options.apiUrl ? `FZ_API=${options.apiUrl}` : null,
1833
+ options.project ? `FZ_PROJECT=${options.project}` : null,
1834
+ options.environment ? `FZ_ENVIRONMENT=${options.environment}` : null,
1835
+ options.enrolStatePath ? `FZ_ENROL_STATE_FILE=${options.enrolStatePath}` : null,
1836
+ options.nodeLabel ? `FZ_NODE_LABEL=${options.nodeLabel}` : null,
1837
+ options.nodeHostname ? `FZ_NODE_HOSTNAME=${options.nodeHostname}` : null,
1838
+ `OTEL_EXPORTER_OTLP_ENDPOINT=${telemetryEndpoint}`,
1839
+ "OTEL_SERVICE_NAME=forgezero-agent",
1840
+ options.gitPublicKeyPath ? `FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}` : null,
1841
+ options.repository ? `FZ_DEPLOY_REPO=${options.repository}` : null,
1842
+ options.branch ? `FZ_DEPLOY_BRANCH=${options.branch}` : null,
1843
+ options.profile ? `FZ_DEPLOY_PROFILE=${options.profile}` : null,
1844
+ options.repository && options.branch ? `FZ_DEPLOY_KEY=${options.project ?? "platform"}:${options.environment ?? "production"}` : null,
1845
+ deploymentEnabled ? `FZ_DEPLOY_ROOT=${deployRoot}` : null,
1846
+ deploymentEnabled ? `FZ_CAPACITY_EVIDENCE_DIR=${deployRoot}/capacity` : null,
1847
+ deploymentEnabled ? `FZ_DEPLOY_RUNNER_SOCKET=${DEPLOYMENT_RUNNER_SOCKET}` : null,
1848
+ deploymentEnabled ? `FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}` : null,
1849
+ Object.keys(deploymentCredentials).length > 0 ? `FZ_DEPLOY_SYSTEMD_SECRETS=${Object.keys(deploymentCredentials).join(",")}` : null,
1850
+ Object.keys(deploymentEnvironment).length > 0 ? `FZ_DEPLOY_ENV_NAMES=${Object.keys(deploymentEnvironment).join(",")}` : null,
1851
+ ...Object.entries(deploymentEnvironment).map(([name, value]) => `${name}=${value}`),
1852
+ options.publicApiUrl ? `FZ_PUBLIC_API_URL=${options.publicApiUrl}` : null,
1853
+ options.pullDeployments ? "FZ_DEPLOY_PULL=true" : null,
1854
+ options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1855
+ `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1856
+ options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
1857
+ ].filter((line) => line !== null);
1858
+ if (deploymentEnabled) {
1859
+ environment.push(`HOME=${deployRoot}/agent-home`, `XDG_CACHE_HOME=${deployRoot}/cache`);
1860
+ }
1861
+ const gitCredential = options.gitCredentialPath ? `LoadCredentialEncrypted=git-deploy-key:${options.gitCredentialPath}
1862
+ ` : "";
1863
+ const projectCredentials = Object.entries(deploymentCredentials).map(([name, path]) => `LoadCredentialEncrypted=${name}:${path}`).join(`
1864
+ `);
1865
+ const deploymentWrites = deploymentEnabled ? `ReadWritePaths=${deployRoot}/releases ${deployRoot}/agent-home ${deployRoot}/cache ${deployRoot}/capacity` : "";
1866
+ const supplementaryGroups = [
1867
+ AGENT_UPDATE_GROUP,
1868
+ deploymentEnabled ? DEPLOYMENT_GROUP : null,
1869
+ deploymentEnabled ? SOFTWARE_HELPER_GROUP : null,
1870
+ lifecycleEnabled ? LIFECYCLE_GROUP : null
1871
+ ].filter((value) => value !== null);
1872
+ const deploymentGroup = supplementaryGroups.length > 0 ? `SupplementaryGroups=${supplementaryGroups.join(" ")}` : "";
1873
+ const after = [
1874
+ "network-online.target",
1875
+ "forgezero-agent-update-helper.service",
1876
+ options.enforceEgress ? "forgezero-agent-egress.service" : null,
1877
+ deploymentEnabled ? "forgezero-deploy-runner.service" : null,
1878
+ deploymentEnabled ? "forgezero-software-helper.service" : null,
1879
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
1880
+ warpEnabled ? "warp-svc.service" : null,
1881
+ enrolmentEnabled ? "forgezero-agent-enrol.service" : null
1882
+ ].filter((value) => value !== null);
1883
+ const requires = [
1884
+ "forgezero-agent-update-helper.service",
1885
+ options.enforceEgress ? "forgezero-agent-egress.service" : null,
1886
+ deploymentEnabled ? "forgezero-deploy-runner.service" : null,
1887
+ deploymentEnabled ? "forgezero-software-helper.service" : null,
1888
+ lifecycleEnabled ? "forgezero-lifecycle-helper.service" : null,
1889
+ warpEnabled ? "warp-svc.service" : null,
1890
+ enrolmentEnabled ? "forgezero-agent-enrol.service" : null
1891
+ ].filter((value) => value !== null);
1892
+ const deploymentDependency = [
1893
+ `After=${after.join(" ")}`,
1894
+ "Wants=network-online.target",
1895
+ requires.length > 0 ? `Requires=${requires.join(" ")}` : null,
1896
+ options.enforceEgress ? "BindsTo=forgezero-agent-egress.service" : null
1897
+ ].filter((value) => value !== null).join(`
1898
+ `);
1899
+ const snpDevice = options.mode === "attested" ? `DevicePolicy=closed
1900
+ DeviceAllow=/dev/sev-guest rw` : "";
1901
+ const snpPrepare = options.mode === "attested" ? `ExecStartPre=+/bin/chgrp ${VAULT_GROUP} /dev/sev-guest
1902
+ ExecStartPre=+/bin/chmod 0640 /dev/sev-guest
1903
+ ` : "";
1904
+ const egressDirectives = options.enforceEgress ? systemdAgentEgressDirectives() : "";
1905
+ return `[Unit]
1906
+ Description=ForgeZero node agent (${options.mode})
1907
+ Documentation=https://www.forgezero.net/docs/agent
1908
+ ${deploymentDependency}
1909
+
1910
+ [Service]
1911
+ Type=simple
1912
+ User=${user}
1913
+ Group=${VAULT_GROUP}
1914
+ ${deploymentGroup}
1915
+ LoadCredentialEncrypted=agent-seed:${seedCredentialPath}
1916
+ ${gitCredential}${projectCredentials}${projectCredentials ? `
1917
+ ` : ""}${snpPrepare}ExecStart=${bin}
1918
+ Restart=always
1919
+ RestartSec=2
1920
+
1921
+ ${environment.map((line) => `Environment=${line}`).join(`
1922
+ `)}
1923
+
1924
+ # The node seed and the vault replica live in this process's memory. A core dump
1925
+ # writes both to disk, which is the one artefact this design exists to remove.
1926
+ LimitCORE=0
1927
+
1928
+ # The socket is the entire interface: anything that can read it can read the
1929
+ # scope. So it lives in a directory systemd creates with a known owner rather
1930
+ # than wherever the process happened to have write access.
1931
+ RuntimeDirectory=forgezero
1932
+ RuntimeDirectoryMode=0750
1933
+ RuntimeDirectoryPreserve=yes
1934
+ UMask=0007
1935
+
1936
+ # Tenant-controlled commands execute in forgezero-deploy-runner.service. This
1937
+ # credential-bearing process never needs to cross a privilege boundary.
1938
+ NoNewPrivileges=true
1939
+ PrivateTmp=true
1940
+ ProtectSystem=strict
1941
+ ProtectHome=true
1942
+ ProtectKernelTunables=true
1943
+ ProtectKernelModules=true
1944
+ ProtectControlGroups=true
1945
+ RestrictSUIDSGID=true
1946
+ RestrictRealtime=true
1947
+ MemoryDenyWriteExecute=true
1948
+ LockPersonality=true
1949
+ ${egressDirectives}
1950
+ ${snpDevice}
1951
+ ${deploymentWrites}
1952
+
1953
+ [Install]
1954
+ WantedBy=multi-user.target
1955
+ `;
1956
+ }
1957
+ var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
1958
+ function planProvision(options) {
1959
+ const mode = options.mode;
1960
+ const user = options.user ?? "forgezero";
1961
+ const seedCredentialPath = options.seedCredentialPath ?? "/etc/forgezero/creds/agent-seed.cred";
1962
+ const credentialDir = seedCredentialPath.replace(/\/[^/]+$/, "");
1963
+ const deployRoot = options.deployRoot ?? "/opt/forgezero";
1964
+ const deploymentEnabled = Boolean(options.repository || options.pullDeployments);
1965
+ const runnerLoopbackPorts = normalizeEgressTcpPorts(options.runnerLoopbackPorts ?? []);
1966
+ const runnerPublicTcpPorts = normalizeEgressTcpPorts(options.runnerPublicTcpPorts ?? DEFAULT_RUNNER_PUBLIC_TCP_PORTS);
1967
+ const lifecycleEnabled = Boolean(options.pullMigrations && options.lifecycleProfilePath);
1968
+ if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1969
+ throw new Error("migration pull and lifecycle profile must be supplied together");
1970
+ }
1971
+ const warpValues = [
1972
+ options.warpOrganization,
1973
+ options.warpClientIdCredentialPath,
1974
+ options.warpClientSecretCredentialPath
1975
+ ];
1976
+ const warpEnabled = warpValues.every(Boolean);
1977
+ if (warpValues.some(Boolean) && !warpEnabled)
1978
+ throw new Error("WARP configuration must be supplied together");
1979
+ const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
1980
+ if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
1981
+ throw new Error("direct enrolment paths must be supplied together");
1982
+ const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
1983
+ const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
1984
+ const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
1985
+ const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
1986
+ const sourceBinPath = options.sourceBinPath ? systemdPath(options.sourceBinPath, "agent source binary") : undefined;
1987
+ const binPath = options.binPath ? systemdPath(options.binPath, "agent binary") : undefined;
1988
+ const gitCredentialPath = options.gitCredentialPath ? systemdPath(options.gitCredentialPath, "Git credential") : undefined;
1989
+ const gitPublicKeyPath = options.gitPublicKeyPath ? systemdPath(options.gitPublicKeyPath, "Git public key") : undefined;
1990
+ if (options.generateGitIdentity && (!gitCredentialPath || !gitPublicKeyPath)) {
1991
+ throw new Error("generated Git identity needs credential and public-key paths");
1992
+ }
1993
+ const gitPublicKeyDir = gitPublicKeyPath?.replace(/\/[^/]+$/, "");
1994
+ const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
1995
+ const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1996
+ const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
1997
+ const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
1998
+ return {
1999
+ mode,
2000
+ reason: reasonFor(mode),
2001
+ unitPath: UNIT_PATH,
2002
+ unit: agentUnit({ ...options, mode, lifecycleProfilePath, lifecycleHelperSocketPath }),
2003
+ auxiliaryUnits: [
2004
+ { path: AGENT_SOCKET_UNIT_PATH, unit: agentSocketUnit(options) },
2005
+ { path: AGENT_SOCKET_PROXY_UNIT_PATH, unit: agentSocketProxyUnit(options) },
2006
+ { path: AGENT_UPDATE_HELPER_UNIT_PATH, unit: agentUpdateHelperUnit(options) },
2007
+ ...options.enforceEgress ? [
2008
+ { path: AGENT_EGRESS_UNIT_PATH, unit: agentEgressUnit(options) }
2009
+ ] : [],
2010
+ ...deploymentEnabled ? [
2011
+ { path: DEPLOYMENT_RUNNER_UNIT_PATH, unit: deploymentRunnerUnit(options) },
2012
+ { path: SOFTWARE_HELPER_UNIT_PATH, unit: softwareHelperUnit(options) }
2013
+ ] : [],
2014
+ ...enrolmentEnabled ? [
2015
+ { path: ENROLMENT_UNIT_PATH, unit: agentEnrolmentUnit(options) }
2016
+ ] : [],
2017
+ ...lifecycleEnabled ? [
2018
+ { path: LIFECYCLE_HELPER_UNIT_PATH, unit: lifecycleHelperUnit({
2019
+ ...options,
2020
+ lifecycleProfilePath,
2021
+ lifecycleHelperSocketPath
2022
+ }) }
2023
+ ] : [],
2024
+ ...warpEnabled ? [
2025
+ { path: WARP_CONFIG_UNIT_PATH, unit: warpConfigUnit({
2026
+ ...options,
2027
+ warpClientIdCredentialPath,
2028
+ warpClientSecretCredentialPath
2029
+ }) },
2030
+ { path: WARP_SERVICE_DROP_IN_PATH, unit: warpServiceDropIn() }
2031
+ ] : []
2032
+ ],
2033
+ socketPath: options.socketPath,
2034
+ user,
2035
+ steps: [
2036
+ ...options.enforceEgress ? [{
2037
+ label: "Ubuntu Agent egress prerequisites",
2038
+ 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`
2039
+ }] : [],
2040
+ {
2041
+ label: "vault socket access group",
2042
+ command: `groupadd --system ${VAULT_GROUP} || true`
2043
+ },
2044
+ {
2045
+ label: "Agent update helper access group",
2046
+ command: `groupadd --system ${AGENT_UPDATE_GROUP} || true`
2047
+ },
2048
+ ...sourceBinPath && binPath ? [{
2049
+ label: "root-owned agent runtime",
2050
+ 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}`
2051
+ }] : [],
2052
+ ...warpEnabled ? [{
2053
+ label: "Cloudflare One client for Ubuntu 26.04",
2054
+ 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`
2055
+ }] : [],
2056
+ ...deploymentEnabled ? [{
2057
+ label: "deployment isolation group",
2058
+ command: `groupadd --system ${DEPLOYMENT_GROUP} || true; groupadd --system ${SOFTWARE_HELPER_GROUP} || true`
2059
+ }] : [],
2060
+ ...lifecycleEnabled ? [{
2061
+ label: "lifecycle helper access group",
2062
+ command: `groupadd --system ${LIFECYCLE_GROUP} || true`
2063
+ }] : [],
2064
+ {
2065
+ label: "service account",
2066
+ command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
2067
+ },
2068
+ {
2069
+ label: "bind service account to vault group",
2070
+ command: `usermod -g ${VAULT_GROUP} ${user}`
2071
+ },
2072
+ {
2073
+ label: "grant verified Agent update access",
2074
+ command: `usermod -a -G ${AGENT_UPDATE_GROUP} ${user}`
2075
+ },
2076
+ ...lifecycleEnabled ? [{
2077
+ label: "grant lifecycle helper socket access",
2078
+ command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
2079
+ }] : [],
2080
+ ...deploymentEnabled ? [{
2081
+ label: "credential-free deployment account",
2082
+ 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}`
2083
+ }] : [],
2084
+ {
2085
+ label: "credential directory",
2086
+ command: `install -d -o root -g root -m 0700 ${credentialDir}`
2087
+ },
2088
+ {
2089
+ label: "Agent state directory",
2090
+ command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
2091
+ },
2092
+ {
2093
+ label: "encrypted node identity",
2094
+ command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
2095
+ },
2096
+ ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
2097
+ {
2098
+ label: "Git deploy identity directory",
2099
+ command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
2100
+ },
2101
+ {
2102
+ label: "unique encrypted Git deploy identity",
2103
+ 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}`
2104
+ }
2105
+ ] : [],
2106
+ ...enrolmentEnabled ? [
2107
+ {
2108
+ label: "enrolment state directory",
2109
+ command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
2110
+ },
2111
+ {
2112
+ label: "encrypted one-time enrolment capability",
2113
+ command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
2114
+ }
2115
+ ] : [],
2116
+ ...deploymentEnabled ? [{
2117
+ label: "deployment directories",
2118
+ 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`
2119
+ }] : [],
2120
+ { label: "reload units", command: "systemctl daemon-reload" },
2121
+ ...deploymentEnabled ? [{
2122
+ label: "remove unsupported deployment socket activation",
2123
+ command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
2124
+ }] : [],
2125
+ {
2126
+ label: "enable and converge services",
2127
+ command: `systemctl enable ${[
2128
+ "forgezero-agent.socket",
2129
+ "forgezero-agent-update-helper.service",
2130
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2131
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2132
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2133
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2134
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
2135
+ "forgezero-agent.service"
2136
+ ].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
2137
+ "forgezero-agent-update-helper.service",
2138
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2139
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2140
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2141
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2142
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
2143
+ ].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
2144
+ },
2145
+ ...enrolmentEnabled ? [{
2146
+ label: "prove the compute binding is durable",
2147
+ command: `test -s ${enrolStatePath}`
2148
+ }] : [],
2149
+ ...options.enforceEgress ? [{
2150
+ label: "prove the Agent egress policy is active",
2151
+ 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(",")}( |")'` : "") : "")
2152
+ }] : [],
2153
+ { label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
2154
+ { label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
2155
+ { label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
2156
+ { label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
2157
+ ...deploymentEnabled ? [{
2158
+ label: "prove the deployment runner socket exists",
2159
+ command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
2160
+ }, {
2161
+ label: "prove the software strategy helper socket exists",
2162
+ command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
2163
+ }] : [],
2164
+ ...lifecycleEnabled ? [{
2165
+ label: "prove the lifecycle helper socket exists",
2166
+ command: awaitSocketCommand(lifecycleHelperSocketPath)
2167
+ }] : [],
2168
+ ...warpEnabled ? [{
2169
+ label: "prove Cloudflare WARP is connected",
2170
+ command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
2171
+ }] : [],
2172
+ ...options.repository ? [{
2173
+ label: "prove the deployment control socket exists",
2174
+ command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
2175
+ }] : []
2176
+ ]
2177
+ };
2178
+ }
2179
+
2180
+ // src/cli/agent-install.ts
2181
+ async function readCapabilities(run) {
2182
+ const answers = {};
2183
+ const checks = Object.entries(CAPABILITY_CHECKS);
2184
+ for (const [id, check] of checks) {
2185
+ try {
2186
+ const result = await run(check.command);
2187
+ answers[id] = check.satisfied(result.stdout, result.exitCode);
2188
+ } catch {
2189
+ answers[id] = false;
2190
+ }
2191
+ }
2192
+ return answers;
2193
+ }
2194
+ async function localRunner(command) {
2195
+ const proc = Bun.spawn(["sh", "-c", command], { stdout: "pipe", stderr: "pipe" });
2196
+ const stdout = await new Response(proc.stdout).text();
2197
+ return { stdout, exitCode: await proc.exited };
2198
+ }
2199
+ function planInstall(options) {
2200
+ const { capabilities, ...unit } = options;
2201
+ return planProvision({ ...unit, mode: modeFor(capabilities) });
2202
+ }
2203
+ async function applyPlan(plan, run) {
2204
+ const transcript = [];
2205
+ for (const step of plan.steps) {
2206
+ const result = await run(step.command);
2207
+ transcript.push({ label: step.label, command: step.command, exitCode: result.exitCode });
2208
+ if (result.exitCode !== 0 && !step.optional) {
2209
+ throw new Error(`${step.label} failed (exit ${result.exitCode}): ${step.command}`);
2210
+ }
2211
+ }
2212
+ return transcript;
2213
+ }
2214
+
2215
+ // src/platform-bootstrap-runtime.ts
2216
+ var safeAtom = (name, value) => {
2217
+ if (!value || /[\0\r\n]/.test(value))
2218
+ throw new Error(`${name} must be non-empty and single-line.`);
2219
+ return value;
2220
+ };
2221
+ var boundedInteger = (name, value, minimum, maximum) => {
2222
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
2223
+ throw new Error(`${name} must be an integer from ${minimum} through ${maximum}.`);
2224
+ }
2225
+ return value;
2226
+ };
2227
+ var privateCoordinator = (raw) => {
2228
+ const url = new URL(raw);
2229
+ if (url.protocol !== "http:" || url.username || url.password || url.search || url.hash || url.pathname !== "/") {
2230
+ throw new Error("ArangoDB coordinator URLs must be credential-free private HTTP origins.");
2231
+ }
2232
+ const host = url.hostname.replace(/^\[|\]$/g, "");
2233
+ 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);
2234
+ if (!privateHost || url.port && url.port !== "8529") {
2235
+ throw new Error("ArangoDB coordinators must use private addresses and port 8529.");
2236
+ }
2237
+ return url.origin;
2238
+ };
2239
+ var httpsOrigin = (name, raw) => {
2240
+ const value = new URL(raw);
2241
+ if (value.protocol !== "https:" || value.username || value.password || value.search || value.hash || value.pathname !== "/") {
2242
+ throw new Error(`${name} must be a credential-free HTTPS origin.`);
2243
+ }
2244
+ return value.origin;
2245
+ };
2246
+ var systemdValue = (name, raw) => {
2247
+ if (/[\0\r\n]/.test(raw))
2248
+ throw new Error(`${name} must be single-line.`);
2249
+ const value = raw;
2250
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
2251
+ };
2252
+ function validatePlatformSharedEnvironment(input, options = {}) {
2253
+ if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
2254
+ throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
2255
+ }
2256
+ if (input.databaseCoordinators.length < 1 || input.databaseCoordinators.length > 16) {
2257
+ throw new Error("databaseCoordinators must contain 1 through 16 endpoints.");
2258
+ }
2259
+ const coordinators = input.databaseCoordinators.map(privateCoordinator);
2260
+ if (new Set(coordinators).size !== coordinators.length)
2261
+ throw new Error("databaseCoordinators must be unique.");
2262
+ if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
2263
+ throw new Error("Database networking must be private-lan or cloudflare-warp.");
2264
+ }
2265
+ boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
2266
+ boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
2267
+ if (input.databaseWriteConcern > input.databaseReplicationFactor) {
2268
+ throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
2269
+ }
2270
+ boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
2271
+ boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
2272
+ boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
2273
+ boundedInteger("drainDeadlineMs", input.drainDeadlineMs, 1000, 300000);
2274
+ boundedInteger("otlpFlushIntervalMs", input.otlpFlushIntervalMs, 1000, 300000);
2275
+ if (!Number.isFinite(input.otlpTraceSampleRatio) || input.otlpTraceSampleRatio < 0 || input.otlpTraceSampleRatio > 1) {
2276
+ throw new Error("otlpTraceSampleRatio must be from 0 through 1.");
2277
+ }
2278
+ if (input.otlpEndpoint !== "http://127.0.0.1:4318")
2279
+ throw new Error("OTLP must use the exact local collector endpoint.");
2280
+ validateCollectorUnit(input.otlpCollectorUnit);
2281
+ httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint);
2282
+ for (const [name, value] of Object.entries({
2283
+ nodeHostname: input.nodeHostname,
2284
+ nodeRegion: input.nodeRegion,
2285
+ databaseUser: input.databaseUser,
2286
+ sharedDirectory: input.sharedDirectory,
2287
+ seedSyncEpoch: input.seedSyncEpoch,
2288
+ repository: input.repository,
2289
+ branch: input.branch,
2290
+ deployProfile: input.deployProfile
2291
+ }))
2292
+ safeAtom(name, value);
2293
+ if (!input.sharedDirectory.startsWith("/"))
2294
+ throw new Error("sharedDirectory must be absolute.");
2295
+ for (const peer of input.seedSyncPeers) {
2296
+ const url = new URL(peer);
2297
+ if (url.protocol !== "ws:" && url.protocol !== "wss:")
2298
+ throw new Error("Seed peers must be WebSocket URLs.");
2299
+ if (url.username || url.password || url.hash)
2300
+ throw new Error("Seed peers cannot contain credentials or fragments.");
2301
+ }
2302
+ if (input.smtp) {
2303
+ safeAtom("smtp.host", input.smtp.host);
2304
+ boundedInteger("smtp.port", input.smtp.port, 1, 65535);
2305
+ safeAtom("smtp.from", input.smtp.from);
2306
+ if (input.smtp.user)
2307
+ safeAtom("smtp.user", input.smtp.user);
2308
+ }
2309
+ if (input.backup) {
2310
+ httpsOrigin("backup.endpoint", input.backup.endpoint);
2311
+ for (const [name, value] of Object.entries(input.backup))
2312
+ safeAtom(`backup.${name}`, value);
2313
+ }
2314
+ if (input.cloudflare) {
2315
+ 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)) {
2316
+ throw new Error("Cloudflare account, zone, KV and Tunnel ids are malformed.");
2317
+ }
2318
+ const service = new URL(input.cloudflare.tunnelService);
2319
+ if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
2320
+ throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
2321
+ if (input.cloudflare.warp) {
2322
+ 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)) {
2323
+ throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
2324
+ }
2325
+ }
2326
+ }
2327
+ if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
2328
+ throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
2329
+ }
2330
+ return {
2331
+ ...input,
2332
+ databaseCoordinators: coordinators,
2333
+ appOrigin: httpsOrigin("appOrigin", input.appOrigin),
2334
+ apiOrigin: httpsOrigin("apiOrigin", input.apiOrigin),
2335
+ agentOtlpEndpoint: httpsOrigin("agentOtlpEndpoint", input.agentOtlpEndpoint)
2336
+ };
2337
+ }
2338
+ function renderPlatformSharedEnvironment(input) {
2339
+ const value = validatePlatformSharedEnvironment(input);
2340
+ const appHost = new URL(value.appOrigin).hostname.split(".").slice(-2).join(".");
2341
+ const apiHost = new URL(value.apiOrigin).hostname.split(".").slice(-2).join(".");
2342
+ const entries = {
2343
+ ARANGO_URL: value.databaseCoordinators[0],
2344
+ ARANGO_URLS: value.databaseCoordinators.join(","),
2345
+ ARANGO_DB: "fz",
2346
+ FZ_DATABASE_MODE: "platform",
2347
+ ARANGO_USER: value.databaseUser,
2348
+ ARANGO_REPLICATION_FACTOR: String(value.databaseReplicationFactor),
2349
+ ARANGO_WRITE_CONCERN: String(value.databaseWriteConcern),
2350
+ FZ_DB_ROLE: value.databaseRole,
2351
+ FZ_SOFTWARE_PROFILE: value.softwareProfile,
2352
+ FZ_ROLE: value.nodeRole,
2353
+ FZ_DB_ADDRESS: value.databaseAddress ?? "",
2354
+ FZ_DB_MASTER: value.databaseMaster ?? "",
2355
+ FZ_DB_NETWORK_MODE: value.databaseNetworkMode,
2356
+ FZ_SEED_SYNC_PEERS: value.seedSyncPeers.join(","),
2357
+ FZ_SEED_SYNC_MEMBERS: String(value.seedSyncMembers),
2358
+ FZ_SEED_SYNC_EPOCH: value.seedSyncEpoch,
2359
+ FZ_SEED_SYNC_CREDENTIAL: "seed-sync-root",
2360
+ FZ_SHARED_DIR: value.sharedDirectory,
2361
+ FZ_PUBLIC_API_PORT: String(value.publicApiPort),
2362
+ ORIGIN: value.appOrigin,
2363
+ API_ORIGIN: value.apiOrigin,
2364
+ HOST: "127.0.0.1",
2365
+ APP_ORIGINS: value.appOrigin,
2366
+ TRUST_CLOUDFLARE_IP: "1",
2367
+ SESSION_COOKIE_SAMESITE: appHost === apiHost ? "lax" : "none",
2368
+ SESSION_COOKIE_DOMAIN: "",
2369
+ FZ_NODE_HOSTNAME: value.nodeHostname,
2370
+ FZ_NODE_REGION: value.nodeRegion,
2371
+ FZ_CONCURRENCY_LIMIT: String(value.concurrencyLimit),
2372
+ FZ_DRAIN_DEADLINE_MS: String(value.drainDeadlineMs),
2373
+ OTEL_EXPORTER_OTLP_ENDPOINT: value.otlpEndpoint,
2374
+ FZ_OTLP_COLLECTOR_UNIT: value.otlpCollectorUnit,
2375
+ OTEL_SERVICE_NAME: "forgezero-api",
2376
+ FZ_OTLP_FLUSH_INTERVAL_MS: String(value.otlpFlushIntervalMs),
2377
+ FZ_OTLP_TRACE_SAMPLE_RATIO: String(value.otlpTraceSampleRatio),
2378
+ FZ_AGENT_OTLP_ENDPOINT: value.agentOtlpEndpoint,
2379
+ FZ_CUSTODIAN_EMAIL: value.custodianEmail ?? "",
2380
+ FZ_PROFILE: value.deployProfile,
2381
+ FZ_REPO: value.repository,
2382
+ FZ_BRANCH: value.branch,
2383
+ FZ_SMTP_HOST: value.smtp?.host ?? "",
2384
+ FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
2385
+ FZ_SMTP_USER: value.smtp?.user ?? "",
2386
+ FZ_SMTP_FROM: value.smtp?.from ?? "",
2387
+ BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
2388
+ BACKUP_S3_REGION: value.backup?.region ?? "",
2389
+ BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
2390
+ BACKUP_S3_ACCESS_KEY_ID: value.backup?.accessKeyId ?? "",
2391
+ FZ_CF_ACCOUNT_ID: value.cloudflare?.accountId ?? "",
2392
+ FZ_CF_ZONE_ID: value.cloudflare?.zoneId ?? "",
2393
+ FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
2394
+ FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
2395
+ FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
2396
+ FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
2397
+ FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
2398
+ FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
2399
+ };
2400
+ return `# Generated by fz bootstrap platform. Non-secret coordinates only.
2401
+ ` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
2402
+ `) + `
2403
+ `;
2404
+ }
2405
+ function platformApiCredentialSpecs(options) {
2406
+ const optional = [
2407
+ ["bootstrap-smtp-password", options.smtp],
2408
+ ["cloudflare-kv-token", options.cloudflareKv],
2409
+ ["cloudflare-network-token", options.cloudflareNetwork]
2410
+ ];
2411
+ return [
2412
+ { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
2413
+ { name: "seed-sync-root", encryptedPath: "/etc/forgezero/creds/seed-sync-root.cred", required: true },
2414
+ ...optional.filter(([, present]) => present).map(([name]) => ({
2415
+ name,
2416
+ encryptedPath: `/etc/forgezero/creds/${name}.cred`,
2417
+ required: false
2418
+ }))
2419
+ ];
2420
+ }
2421
+ function renderPlatformApiUnits(input) {
2422
+ for (const path of [input.sharedDirectory, input.sharedEnvironmentFile, input.slotsDirectory]) {
2423
+ if (!path.startsWith("/") || /[\r\n]/.test(path))
2424
+ throw new Error("Runtime paths must be absolute and single-line.");
2425
+ }
2426
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
2427
+ throw new Error("Invalid service user.");
2428
+ validateCollectorUnit(input.collectorUnit);
2429
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
2430
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
2431
+ if (input.bluePort === input.greenPort)
2432
+ throw new Error("Blue and green ports must differ.");
2433
+ const credentials = input.credentials.map((credential) => `LoadCredentialEncrypted=${credential.name}:${credential.encryptedPath}`).join(`
2434
+ `);
2435
+ const template = `[Unit]
2436
+ Description=ForgeZero (%i slot)
2437
+ After=network-online.target ${input.collectorUnit}
2438
+ Wants=network-online.target ${input.collectorUnit}
2439
+
2440
+ [Service]
2441
+ Type=simple
2442
+ User=${input.serviceUser}
2443
+ WorkingDirectory=${input.slotsDirectory}/%i
2444
+ Environment=NODE_ENV=production
2445
+ Environment=FZ_SLOT=%i
2446
+ EnvironmentFile=${input.sharedEnvironmentFile}
2447
+ ${credentials}
2448
+ ExecStart=/usr/local/bin/bun run ${input.slotsDirectory}/%i/src/index.ts
2449
+ Restart=always
2450
+ RestartSec=2
2451
+ TimeoutStopSec=35s
2452
+ LimitCORE=0
2453
+ UMask=0077
2454
+ NoNewPrivileges=yes
2455
+ PrivateTmp=yes
2456
+ PrivateDevices=yes
2457
+ ProtectSystem=strict
2458
+ ProtectHome=yes
2459
+ ReadOnlyPaths=${input.sharedDirectory}
2460
+ ProtectKernelTunables=yes
2461
+ ProtectKernelModules=yes
2462
+ ProtectControlGroups=yes
2463
+ RestrictSUIDSGID=yes
2464
+ RestrictRealtime=yes
2465
+ LockPersonality=yes
2466
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
2467
+
2468
+ [Install]
2469
+ WantedBy=multi-user.target
2470
+ `;
2471
+ return { template, dropIns: {
2472
+ blue: `[Service]
2473
+ Environment=PORT=${input.bluePort}
2474
+ `,
2475
+ green: `[Service]
2476
+ Environment=PORT=${input.greenPort}
2477
+ `
2478
+ } };
2479
+ }
2480
+ function renderPlatformNginx(input) {
2481
+ boundedInteger("publicPort", input.publicPort, 1024, 65535);
2482
+ boundedInteger("initialSlotPort", input.initialSlotPort, 1024, 65535);
2483
+ if (input.publicPort === input.initialSlotPort)
2484
+ throw new Error("Edge and slot ports must differ.");
2485
+ return {
2486
+ upstream: `upstream forgezero { server 127.0.0.1:${input.initialSlotPort}; }
2487
+ `,
2488
+ site: `server {
2489
+ listen 127.0.0.1:${input.publicPort};
2490
+ server_name _;
2491
+ 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; }
2492
+ 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; }
2493
+ location / { return 404; }
2494
+ }
2495
+ `
2496
+ };
2497
+ }
2498
+ function renderPlatformActivationFiles(input) {
2499
+ if (!input.root.startsWith("/") || /[\0\r\n]/.test(input.root))
2500
+ throw new Error("Activation root must be absolute and single-line.");
2501
+ if (!/^[a-z_][a-z0-9_-]{0,31}$/.test(input.serviceUser))
2502
+ throw new Error("Invalid activation service user.");
2503
+ boundedInteger("bluePort", input.bluePort, 1024, 65535);
2504
+ boundedInteger("greenPort", input.greenPort, 1024, 65535);
2505
+ if (input.bluePort === input.greenPort)
2506
+ throw new Error("Activation slot ports must differ.");
2507
+ boundedInteger("keepReleases", input.keepReleases, 2, 100);
2508
+ if (!/^\/[A-Za-z0-9/_-]{1,128}$/.test(input.healthPath) || input.healthPath.includes("..")) {
2509
+ throw new Error("Activation health path is malformed.");
2510
+ }
2511
+ const environment = [
2512
+ `FZ_DIR=${input.root}`,
2513
+ `FZ_USER=${input.serviceUser}`,
2514
+ `FZ_BLUE_PORT=${input.bluePort}`,
2515
+ `FZ_GREEN_PORT=${input.greenPort}`,
2516
+ `FZ_HEALTH_PATH=${input.healthPath}`,
2517
+ `FZ_KEEP_RELEASES=${input.keepReleases}`
2518
+ ].join(`
2519
+ `) + `
2520
+ `;
2521
+ const helper = `#!/usr/bin/env bash
2522
+ set -Eeuo pipefail
2523
+ source /etc/forgezero/deploy.env
2524
+ [[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
2525
+ release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
2526
+ install -d -o root -g root -m 0755 "$slots"
2527
+ case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
2528
+ [[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
2529
+ slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
2530
+ if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
2531
+ target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
2532
+ 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 {} +
2533
+ ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
2534
+ 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
2535
+ 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
2536
+ upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
2537
+ printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
2538
+ 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
2539
+ rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"; [[ -n "$previous_slot" && "$previous_slot" != "$target" ]] && systemctl stop "forgezero@\${previous_slot}.service" || true
2540
+ 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-)
2541
+ for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
2542
+ printf 'promoted %s on %s\\n' "$release" "$target"
2543
+ `;
2544
+ return {
2545
+ environment,
2546
+ helper,
2547
+ sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
2548
+ `
2549
+ };
2550
+ }
2551
+ function validateCollectorUnit(unit) {
2552
+ if (!/^[A-Za-z0-9][A-Za-z0-9_.@-]{0,127}\.service$/.test(unit))
2553
+ throw new Error("Invalid OTLP collector service unit.");
2554
+ if (/^(forgezero@.*|forgezero-agent|forgezero-metal-agent|forgezero-db)\.service$/.test(unit)) {
2555
+ throw new Error("OTLP collector must be independently supervised.");
2556
+ }
2557
+ }
2558
+ function planLocalOtlpProof(endpoint2, collectorUnit) {
2559
+ if (endpoint2 !== "http://127.0.0.1:4318")
2560
+ throw new Error("OTLP proof requires exact loopback endpoint http://127.0.0.1:4318.");
2561
+ validateCollectorUnit(collectorUnit);
2562
+ return {
2563
+ unitCheck: { command: "systemctl", argv: ["is-active", "--quiet", collectorUnit] },
2564
+ receiverCheck: {
2565
+ command: "curl",
2566
+ acceptedStatus: "2xx",
2567
+ argv: [
2568
+ "--silent",
2569
+ "--show-error",
2570
+ "--max-time",
2571
+ "5",
2572
+ "--output",
2573
+ "/dev/null",
2574
+ "--write-out",
2575
+ "%{http_code}",
2576
+ "--request",
2577
+ "POST",
2578
+ "--header",
2579
+ "Content-Type: application/json",
2580
+ "--data-binary",
2581
+ "{}",
2582
+ `${endpoint2}/v1/metrics`
2583
+ ]
2584
+ }
2585
+ };
2586
+ }
2587
+
2588
+ // src/bootstrap.ts
2589
+ import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
2590
+ var PLATFORM_BOOTSTRAP_PROFILES = [
2591
+ "platform-db-api",
2592
+ "platform-api"
2593
+ ];
2594
+ var STATE_PATH = "/var/lib/forgezero/bootstrap.json";
2595
+ var CREDS = "/etc/forgezero/creds";
2596
+ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
2597
+ var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
2598
+ var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
2599
+ var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
2600
+ var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
2601
+ var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
2602
+ var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
2603
+ var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
2604
+ var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
2605
+ var LIFECYCLE_PROFILE = "/etc/forgezero/lifecycle.json";
2606
+ var PACKAGED_AGENT_BIN = fileURLToPath(new URL("./fz-agent.js", import.meta.url));
2607
+ var privateOrigin = (value) => {
2608
+ let url;
2609
+ try {
2610
+ url = new URL(value);
2611
+ } catch {
2612
+ throw new Error(`database coordinator is malformed: ${value}`);
2613
+ }
2614
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
2615
+ throw new Error(`database coordinators must be credential-free HTTP(S) origins: ${value}`);
2616
+ }
2617
+ const host = url.hostname.replace(/^\[|\]$/g, "");
2618
+ const v4 = host.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/)?.slice(1).map(Number);
2619
+ 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);
2620
+ const privateV6 = host === "::1" || /^f[cd][0-9a-f]:/i.test(host);
2621
+ if (!privateV4 && !privateV6 && host !== "127.0.0.1" && host !== "localhost") {
2622
+ throw new Error(`database coordinator must use a private address: ${value}`);
2623
+ }
2624
+ return url.origin;
2625
+ };
2626
+ var privateCidr = (value) => {
2627
+ 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])$/);
2628
+ if (!match || match[1].split(".").some((part) => Number(part) > 255))
2629
+ throw new Error(`private firewall CIDR is malformed: ${value}`);
2630
+ return value;
2631
+ };
2632
+ var privateFile = (host, path, label) => {
2633
+ if (!host.exists(path))
2634
+ throw new Error(`${label} file is missing: ${path}`);
2635
+ const metadata = host.inspect?.(path);
2636
+ if (metadata && (!metadata.regular || metadata.symbolic || metadata.uid !== 0 || metadata.links !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 16 * 1024)) {
2637
+ throw new Error(`${label} must be a root-owned, owner-only regular file with one link and at most 16 KiB`);
2638
+ }
2639
+ const value = host.read(path).trim();
2640
+ if (!value)
2641
+ throw new Error(`${label} file is empty: ${path}`);
2642
+ return value;
2643
+ };
2644
+ function validateBootstrapConfig(value) {
2645
+ 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)) {
2646
+ throw new Error("node hostname must be a lowercase public FQDN");
2647
+ }
2648
+ let telemetry;
2649
+ try {
2650
+ telemetry = new URL(value.telemetryEndpoint);
2651
+ } catch {
2652
+ throw new Error("telemetry endpoint is malformed");
2653
+ }
2654
+ if (telemetry.protocol !== "https:" || telemetry.port && telemetry.port !== "443" || telemetry.username || telemetry.password || telemetry.search || telemetry.hash) {
2655
+ throw new Error("telemetry endpoint must be public HTTPS on port 443 without credentials, query or fragment");
2656
+ }
2657
+ if (value.kind === "tenant") {
2658
+ if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
2659
+ throw new Error("tenant realm is malformed");
2660
+ if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
2661
+ throw new Error("tenant API must be public HTTPS or loopback HTTP");
2662
+ }
2663
+ if (!value.enrolTokenFile)
2664
+ throw new Error("tenant bootstrap requires --enrol-token-file");
2665
+ return value;
2666
+ }
2667
+ if (!PLATFORM_BOOTSTRAP_PROFILES.includes(value.profile))
2668
+ throw new Error("unsupported platform software profile");
2669
+ if (!["production", "development"].includes(value.environment))
2670
+ throw new Error("platform environment must be production or development");
2671
+ let api;
2672
+ try {
2673
+ api = new URL(value.apiUrl);
2674
+ } catch {
2675
+ throw new Error("platform API URL is malformed");
2676
+ }
2677
+ if (api.protocol !== "https:" || api.username || api.password || api.search || api.hash) {
2678
+ throw new Error("platform API must be public HTTPS without credentials, query or fragment");
2679
+ }
2680
+ const expected = value.profile === "platform-api" ? { role: "none", mode: "default" } : { role: undefined, mode: "default" };
2681
+ if (expected.role && value.database.role !== expected.role || value.database.serverMode !== expected.mode || value.profile === "platform-db-api" && !["master", "joiner"].includes(value.database.role)) {
2682
+ throw new Error("platform profile, database role and Coordinator mode disagree");
2683
+ }
2684
+ if (value.database.role !== "none" && !value.database.address)
2685
+ throw new Error("database nodes require a private address");
2686
+ if (value.database.role === "joiner" && !value.database.master)
2687
+ throw new Error("database joiners require the master starter address");
2688
+ if (!/^(?:dev-)?fz-n[1-9][0-9]{0,2}$/.test(value.computeReference))
2689
+ throw new Error("platform compute reference is malformed");
2690
+ if (!value.database.bootstrapSecretFile)
2691
+ throw new Error("platform bootstrap requires the shared cluster bootstrap-code file");
2692
+ const write = value.database.coordinators.map(privateOrigin);
2693
+ if (write.length < 1 || write.length > 16 || new Set(write).size !== write.length) {
2694
+ throw new Error("database coordinators must contain 1-16 unique private origins");
2695
+ }
2696
+ value.database.coordinators = write;
2697
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
2698
+ allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
2699
+ });
2700
+ 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(",")) {
2701
+ throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
2702
+ }
2703
+ if (runtime.deployProfile !== value.environment)
2704
+ throw new Error("runtime deployment profile disagrees with bootstrap environment");
2705
+ if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
2706
+ throw new Error("runtime database topology disagrees with bootstrap topology");
2707
+ }
2708
+ if (!Number.isSafeInteger(value.firewall.sshPort) || value.firewall.sshPort < 1 || value.firewall.sshPort > 65535) {
2709
+ throw new Error("firewall SSH port is invalid");
2710
+ }
2711
+ value.firewall.privateCidrs = value.firewall.privateCidrs.map(privateCidr);
2712
+ if (value.firewall.enabled && (value.firewall.privateCidrs.length < 1 || new Set(value.firewall.privateCidrs).size !== value.firewall.privateCidrs.length)) {
2713
+ throw new Error("enabled firewall requires unique private cluster CIDRs");
2714
+ }
2715
+ if (Number(value.computeReference.match(/n(\d+)$/)?.[1]) > 3 && !value.platformEnrolTokenFile) {
2716
+ throw new Error("post-genesis platform computes require an API-issued enrolment token file");
2717
+ }
2718
+ if (value.cloudflareHandoff && (!value.installCloudflared || !value.cloudflareHandoff.checkpointFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
2719
+ throw new Error("Cloudflare handoff requires cloudflared installation, a checkpoint and a valid node name");
2720
+ }
2721
+ value.runtime.environment = runtime;
2722
+ return value;
2723
+ }
2724
+ function planBootstrap(input, initialized = false) {
2725
+ const config = validateBootstrapConfig(structuredClone(input));
2726
+ const software = config.kind === "tenant" ? [...config.software ?? [], ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []] : [
2727
+ ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
2728
+ { id: "bun", version: "1.3.14" },
2729
+ { id: "nginx", version: "ubuntu-26.04" },
2730
+ ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
2731
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
2732
+ ];
2733
+ return {
2734
+ kind: config.kind,
2735
+ mode: initialized ? "repair" : "install",
2736
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
2737
+ software,
2738
+ statePath: STATE_PATH,
2739
+ steps: [
2740
+ { id: "agent", label: "Install and verify the common Agent supervision boundary", mutation: true },
2741
+ { id: "software", label: "Install exact Agent-owned software coordinates in order", mutation: true },
2742
+ { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
2743
+ ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
2744
+ ...config.kind === "platform" ? [
2745
+ { id: "runtime", label: "Install the shared environment, API slots, edge and activation boundary", mutation: true },
2746
+ { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
2747
+ { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
2748
+ ] : [],
2749
+ ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
2750
+ { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
2751
+ { id: "status", label: "Verify services and immutable profile", mutation: false }
2752
+ ]
2753
+ };
2754
+ }
2755
+ var checked = async (host, argv, label, options) => {
2756
+ const result = await host.exec(argv, options);
2757
+ if (result.exitCode !== 0)
2758
+ throw new Error(`${label} failed: ${result.output.trim()}`);
2759
+ return result.output;
2760
+ };
2761
+ var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
2762
+ var unitEscape = (value) => {
2763
+ if (/[^A-Za-z0-9_./:@,+-]/.test(value))
2764
+ throw new Error(`unsafe systemd coordinate: ${value}`);
2765
+ return value;
2766
+ };
2767
+ function databaseUnit(config) {
2768
+ const db = config.database;
2769
+ const join2 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
2770
+ return `[Unit]
2771
+ Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role})
2772
+ After=network-online.target
2773
+ Wants=network-online.target
2774
+
2775
+ [Service]
2776
+ Type=simple
2777
+ User=arangodb
2778
+ Group=arangodb
2779
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
2780
+ 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}
2781
+ Restart=always
2782
+ RestartSec=5
2783
+ UMask=0077
2784
+ NoNewPrivileges=true
2785
+ PrivateTmp=true
2786
+ PrivateDevices=true
2787
+ ProtectSystem=strict
2788
+ ProtectHome=true
2789
+ ReadWritePaths=/var/lib/forgezero-cluster
2790
+ ProtectKernelTunables=true
2791
+ ProtectKernelModules=true
2792
+ ProtectControlGroups=true
2793
+ RestrictSUIDSGID=true
2794
+ LockPersonality=true
2795
+
2796
+ [Install]
2797
+ WantedBy=multi-user.target
2798
+ `;
2799
+ }
2800
+ function databaseVerifyUnit(config) {
2801
+ const { address } = config.database;
2802
+ return `[Unit]
2803
+ Description=Verify ForgeZero ArangoDB Community 3.11.14 writable Coordinator
2804
+ Requires=forgezero-db.service
2805
+ After=forgezero-db.service
2806
+ PartOf=forgezero-db.service
2807
+
2808
+ [Service]
2809
+ Type=oneshot
2810
+ User=arangodb
2811
+ Group=arangodb
2812
+ LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
2813
+ 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));'
2814
+ RemainAfterExit=yes
2815
+ TimeoutStartSec=200
2816
+ NoNewPrivileges=true
2817
+ PrivateTmp=true
2818
+ PrivateDevices=true
2819
+ ProtectSystem=strict
2820
+ ProtectHome=true
2821
+
2822
+ [Install]
2823
+ WantedBy=multi-user.target
2824
+ `;
2825
+ }
2826
+ function tunnelUnit() {
2827
+ return `[Unit]
2828
+ Description=ForgeZero Cloudflare Tunnel connector
2829
+ After=network-online.target
2830
+ Wants=network-online.target
2831
+
2832
+ [Service]
2833
+ Type=simple
2834
+ DynamicUser=yes
2835
+ LoadCredentialEncrypted=cloudflared-token:${TUNNEL_CREDENTIAL}
2836
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate run --token-file %d/cloudflared-token
2837
+ Restart=always
2838
+ RestartSec=5
2839
+ NoNewPrivileges=true
2840
+ PrivateTmp=true
2841
+ ProtectSystem=strict
2842
+ ProtectHome=true
2843
+
2844
+ [Install]
2845
+ WantedBy=multi-user.target
2846
+ `;
2847
+ }
2848
+ var derive = (root, label) => {
2849
+ if (!/^[a-f0-9]{64}$/i.test(root))
2850
+ throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
2851
+ return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
2852
+ };
2853
+ async function seal(host, name, destination, value) {
2854
+ if (host.exists(destination))
2855
+ return;
2856
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
2857
+ if (result.exitCode !== 0)
2858
+ throw new Error(`could not seal ${name}: ${result.output.trim()}`);
2859
+ }
2860
+ function stateFor(config) {
2861
+ return `${JSON.stringify({
2862
+ format: 1,
2863
+ kind: config.kind,
2864
+ profile: config.kind === "platform" ? config.profile : config.profile ?? "tenant-managed",
2865
+ nodeHostname: config.nodeHostname,
2866
+ apiUrl: config.apiUrl,
2867
+ ...config.kind === "platform" ? {
2868
+ environment: config.environment,
2869
+ databaseRole: config.database.role,
2870
+ databaseServerMode: config.database.serverMode,
2871
+ databaseAddress: config.database.address,
2872
+ databaseCoordinators: config.database.coordinators,
2873
+ databaseModeEvidence: config.database.role === "none" ? undefined : DB_MODE_EVIDENCE,
2874
+ collectorUnit: config.runtime.environment.otlpCollectorUnit,
2875
+ cloudflared: Boolean(config.cloudflareHandoff)
2876
+ } : { realm: config.realm }
2877
+ }, null, 2)}
2878
+ `;
2879
+ }
2880
+ async function bootstrapStatus(host = localBootstrapHost()) {
2881
+ if (!host.exists(STATE_PATH))
2882
+ return { initialized: false, services: {}, problems: ["bootstrap state is missing"] };
2883
+ let state;
2884
+ try {
2885
+ state = JSON.parse(host.read(STATE_PATH));
2886
+ } catch {
2887
+ return { initialized: false, services: {}, problems: ["bootstrap state is malformed"] };
2888
+ }
2889
+ const units = ["forgezero-agent.service", "forgezero-agent.socket"];
2890
+ if (state.kind === "platform")
2891
+ units.push("nginx.service");
2892
+ if (state.kind === "platform" && state.collectorUnit)
2893
+ units.push(state.collectorUnit);
2894
+ if (state.kind === "platform" && state.cloudflared)
2895
+ units.push("cloudflared.service");
2896
+ if (state.kind === "platform" && state.databaseRole !== "none")
2897
+ units.push("forgezero-db.service", "forgezero-db-verify.service");
2898
+ const services = {};
2899
+ const problems = [];
2900
+ for (const unit of units) {
2901
+ const result = await host.exec(["systemctl", "is-active", "--quiet", unit]);
2902
+ services[unit] = result.exitCode === 0;
2903
+ if (result.exitCode !== 0)
2904
+ problems.push(`${unit} is not active`);
2905
+ }
2906
+ if (state.kind === "platform") {
2907
+ const [blue, green] = await Promise.all([
2908
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
2909
+ host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
2910
+ ]);
2911
+ services["forgezero@active.service"] = blue.exitCode === 0 || green.exitCode === 0;
2912
+ if (!services["forgezero@active.service"])
2913
+ problems.push("neither API slot is active");
2914
+ }
2915
+ if (!host.exists("/var/lib/forgezero/enrolment.json"))
2916
+ problems.push("durable Agent enrolment state is missing");
2917
+ return { initialized: problems.length === 0, kind: state.kind, profile: state.profile, services, problems };
2918
+ }
2919
+ async function applyBootstrap(input, host = localBootstrapHost()) {
2920
+ const config = validateBootstrapConfig(structuredClone(input));
2921
+ if (host.uid() !== 0)
2922
+ throw new Error("fz bootstrap --apply must run as root");
2923
+ if (config.kind === "tenant") {
2924
+ const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
2925
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
2926
+ throw new Error("tenant enrolment token is malformed");
2927
+ }
2928
+ let cloudflare;
2929
+ if (config.kind === "platform" && config.cloudflareHandoff) {
2930
+ cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.checkpointFile, config.cloudflareHandoff.nodeName);
2931
+ const expected = config.runtime.environment.cloudflare;
2932
+ if (cloudflare.hostname !== config.nodeHostname)
2933
+ throw new Error("Cloudflare checkpoint hostname disagrees with platform node hostname");
2934
+ if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
2935
+ throw new Error("Cloudflare checkpoint disagrees with immutable platform runtime coordinates");
2936
+ }
2937
+ const discovered = {
2938
+ accountId: cloudflare.accountId,
2939
+ zoneId: cloudflare.zoneId,
2940
+ kvNamespaceId: cloudflare.kvNamespaceId,
2941
+ tunnelId: cloudflare.tunnelId,
2942
+ tunnelService: cloudflare.service,
2943
+ ...cloudflare.warp ? { warp: {
2944
+ organization: cloudflare.warp.organization,
2945
+ virtualNetworkId: cloudflare.warp.virtualNetworkId,
2946
+ deviceProfileId: cloudflare.warp.deviceProfileId
2947
+ } } : {}
2948
+ };
2949
+ if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
2950
+ throw new Error("Cloudflare checkpoint disagrees with immutable WARP/runtime coordinates");
2951
+ }
2952
+ config.runtime.environment.cloudflare = expected ?? discovered;
2953
+ if (config.runtime.environment.databaseNetworkMode === "cloudflare-warp" !== Boolean(cloudflare.warp)) {
2954
+ throw new Error("Cloudflare checkpoint private-network mode disagrees with the platform database network mode");
2955
+ }
2956
+ }
2957
+ const plan = planBootstrap(config, host.exists(STATE_PATH));
2958
+ const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
2959
+ host.mkdir(CREDS, 448);
2960
+ host.mkdir("/var/lib/forgezero", 448);
2961
+ if (cloudflare?.warp) {
2962
+ await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
2963
+ await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
2964
+ }
2965
+ await host.installAgent(config, alreadyEnrolled && config.kind === "platform" ? PLATFORM_ENROL_SOURCE : undefined);
2966
+ if (config.kind === "platform" && config.firewall.enabled) {
2967
+ await host.ensureSoftware(plan.software.filter(({ id }) => id === "ufw"));
2968
+ } else
2969
+ await host.ensureSoftware(plan.software);
2970
+ if (config.kind === "platform" && config.firewall.enabled) {
2971
+ await checked(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
2972
+ await checked(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
2973
+ await checked(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
2974
+ for (const cidr of config.firewall.privateCidrs) {
2975
+ if (config.database.role !== "none")
2976
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
2977
+ for (const port of [config.runtime.bluePort, config.runtime.greenPort])
2978
+ await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
2979
+ }
2980
+ await checked(host, ["ufw", "--force", "enable"], "firewall activation");
2981
+ await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
2982
+ }
2983
+ if (config.kind === "platform") {
2984
+ const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
2985
+ if (!host.exists(JWT_CREDENTIAL)) {
2986
+ await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
2987
+ }
2988
+ await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
2989
+ await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
2990
+ const credentialFiles = config.runtime.credentialFiles ?? {};
2991
+ for (const [name, source] of Object.entries({
2992
+ "bootstrap-smtp-password": credentialFiles.smtpPassword,
2993
+ "backup-s3-secret": credentialFiles.backupS3Secret
2994
+ })) {
2995
+ if (source) {
2996
+ const destination = `${CREDS}/${name}.cred`;
2997
+ if (!host.exists(destination)) {
2998
+ await seal(host, name, destination, privateFile(host, source, name));
2999
+ host.remove(source);
3000
+ }
3001
+ }
3002
+ }
3003
+ if (cloudflare) {
3004
+ await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
3005
+ if (cloudflare.privateNetworkRuntimeToken)
3006
+ await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
3007
+ }
3008
+ const runtime = config.runtime;
3009
+ const envPath = `${runtime.environment.sharedDirectory}/.env`;
3010
+ const credentials = platformApiCredentialSpecs({
3011
+ smtp: Boolean(credentialFiles.smtpPassword),
3012
+ cloudflareKv: Boolean(cloudflare),
3013
+ cloudflareNetwork: Boolean(cloudflare?.privateNetworkRuntimeToken)
3014
+ });
3015
+ const units = renderPlatformApiUnits({
3016
+ serviceUser: runtime.serviceUser,
3017
+ sharedDirectory: runtime.environment.sharedDirectory,
3018
+ sharedEnvironmentFile: envPath,
3019
+ slotsDirectory: runtime.slotsDirectory,
3020
+ bluePort: runtime.bluePort,
3021
+ greenPort: runtime.greenPort,
3022
+ collectorUnit: runtime.environment.otlpCollectorUnit,
3023
+ credentials
3024
+ });
3025
+ const edge = renderPlatformNginx({ publicPort: runtime.environment.publicApiPort, initialSlotPort: runtime.bluePort });
3026
+ const activation = renderPlatformActivationFiles({
3027
+ root: config.deployRoot ?? "/opt/forgezero",
3028
+ serviceUser: runtime.serviceUser,
3029
+ bluePort: runtime.bluePort,
3030
+ greenPort: runtime.greenPort,
3031
+ healthPath: runtime.healthPath,
3032
+ keepReleases: runtime.keepReleases
3033
+ });
3034
+ await checked(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
3035
+ await checked(host, ["id", runtime.serviceUser], "existing API service account");
3036
+ });
3037
+ host.mkdir(runtime.environment.sharedDirectory, 488);
3038
+ host.mkdir(runtime.slotsDirectory, 493);
3039
+ host.write(envPath, renderPlatformSharedEnvironment(runtime.environment), 416);
3040
+ await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
3041
+ host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
3042
+ host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
3043
+ host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
3044
+ host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
3045
+ host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
3046
+ host.write("/etc/forgezero/deploy.env", activation.environment, 420);
3047
+ host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
3048
+ host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
3049
+ await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
3050
+ const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
3051
+ await checked(host, telemetry.unitCheck.argv, "OTLP collector supervision");
3052
+ const otlpStatus = (await checked(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
3053
+ if (!/^2\d\d$/.test(otlpStatus))
3054
+ throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
3055
+ await checked(host, ["nginx", "-t"], "nginx configuration");
3056
+ await checked(host, ["systemctl", "daemon-reload"], "systemd reload");
3057
+ await checked(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
3058
+ if (config.database.role === "master") {
3059
+ const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
3060
+ if (!host.exists(invite)) {
3061
+ host.write(invite, `plt_${randomBytes(24).toString("hex")}
3062
+ `, 384);
3063
+ await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
3064
+ }
3065
+ }
3066
+ if (config.database.role !== "none") {
3067
+ host.mkdir("/var/lib/forgezero-cluster", 448);
3068
+ await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
3069
+ host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
3070
+ host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
3071
+ await checked(host, ["systemctl", "daemon-reload"], "database unit reload");
3072
+ await checked(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
3073
+ const evidence = {
3074
+ expectedMode: "default",
3075
+ role: "COORDINATOR",
3076
+ unit: "forgezero-db-verify.service",
3077
+ verifiedAt: new Date().toISOString()
3078
+ };
3079
+ host.write(DB_MODE_EVIDENCE, `${JSON.stringify(evidence, null, 2)}
3080
+ `, 384);
3081
+ }
3082
+ if (!alreadyEnrolled) {
3083
+ 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}`)}`;
3084
+ if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(enrolToken))
3085
+ throw new Error("platform enrolment token is malformed");
3086
+ host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
3087
+ `, 384);
3088
+ }
3089
+ await checked(host, [
3090
+ "runuser",
3091
+ "-u",
3092
+ "forgezero-agent",
3093
+ "--",
3094
+ "/usr/local/bin/fz-agent",
3095
+ "deploy",
3096
+ ...config.database.role === "master" ? ["--release-executor"] : []
3097
+ ], "initial Agent deployment");
3098
+ if (!alreadyEnrolled) {
3099
+ await host.installAgent(config, PLATFORM_ENROL_SOURCE);
3100
+ if (config.platformEnrolTokenFile)
3101
+ host.remove(config.platformEnrolTokenFile);
3102
+ }
3103
+ }
3104
+ if (config.kind === "platform" && cloudflare) {
3105
+ if (!host.exists(TUNNEL_CREDENTIAL)) {
3106
+ await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3107
+ }
3108
+ host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
3109
+ await checked(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
3110
+ await checked(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
3111
+ }
3112
+ host.write(STATE_PATH, stateFor(config), 384);
3113
+ const status = await bootstrapStatus(host);
3114
+ if (!status.initialized)
3115
+ throw new Error(`bootstrap verification failed: ${status.problems.join("; ")}`);
3116
+ let launch;
3117
+ if (config.kind === "platform" && config.database.role === "master") {
3118
+ const invitePath = `${config.runtime.environment.sharedDirectory}/platform-invite.token`;
3119
+ if (!host.exists(invitePath))
3120
+ throw new Error("platform invite is missing after deployment");
3121
+ const token = host.read(invitePath).trim();
3122
+ if (!/^plt_[a-f0-9]{48}$/.test(token))
3123
+ throw new Error("platform invite is malformed");
3124
+ launch = {
3125
+ 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)}`,
3126
+ inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
3127
+ };
3128
+ }
3129
+ return { plan, applied: true, status, ...launch ? { launch } : {} };
3130
+ }
3131
+ var exactKeys = (value, allowed, label) => {
3132
+ if (!value || typeof value !== "object" || Array.isArray(value))
3133
+ throw new Error(`${label} must be an object`);
3134
+ const record = value;
3135
+ const unknown = Object.keys(record).filter((key) => !allowed.includes(key));
3136
+ if (unknown.length)
3137
+ throw new Error(`${label} contains unknown fields: ${unknown.join(", ")}`);
3138
+ return record;
3139
+ };
3140
+ function strictBootstrapDocument(value) {
3141
+ const root = exactKeys(value, [
3142
+ "kind",
3143
+ "environment",
3144
+ "profile",
3145
+ "computeReference",
3146
+ "nodeHostname",
3147
+ "apiUrl",
3148
+ "repository",
3149
+ "branch",
3150
+ "deployRoot",
3151
+ "telemetryEndpoint",
3152
+ "database",
3153
+ "platformEnrolTokenFile",
3154
+ "runtime",
3155
+ "firewall",
3156
+ "installCloudflared",
3157
+ "cloudflareHandoff",
3158
+ "realm",
3159
+ "enrolTokenFile",
3160
+ "software"
3161
+ ], "bootstrap config");
3162
+ if (root.kind === "platform") {
3163
+ exactKeys(root.firewall, ["enabled", "sshPort", "privateCidrs"], "firewall config");
3164
+ if (root.cloudflareHandoff !== undefined)
3165
+ exactKeys(root.cloudflareHandoff, ["checkpointFile", "nodeName"], "Cloudflare handoff");
3166
+ exactKeys(root.database, ["role", "serverMode", "address", "master", "coordinators", "bootstrapSecretFile"], "database config");
3167
+ const runtime = exactKeys(root.runtime, [
3168
+ "environment",
3169
+ "serviceUser",
3170
+ "slotsDirectory",
3171
+ "bluePort",
3172
+ "greenPort",
3173
+ "healthPath",
3174
+ "keepReleases",
3175
+ "credentialFiles"
3176
+ ], "runtime config");
3177
+ exactKeys(runtime.environment, [
3178
+ "softwareProfile",
3179
+ "databaseRole",
3180
+ "databaseCoordinators",
3181
+ "databaseAddress",
3182
+ "databaseMaster",
3183
+ "databaseNetworkMode",
3184
+ "databaseReplicationFactor",
3185
+ "databaseWriteConcern",
3186
+ "nodeHostname",
3187
+ "nodeRegion",
3188
+ "databaseUser",
3189
+ "nodeRole",
3190
+ "appOrigin",
3191
+ "apiOrigin",
3192
+ "publicApiPort",
3193
+ "sharedDirectory",
3194
+ "seedSyncPeers",
3195
+ "seedSyncMembers",
3196
+ "seedSyncEpoch",
3197
+ "concurrencyLimit",
3198
+ "drainDeadlineMs",
3199
+ "otlpEndpoint",
3200
+ "otlpCollectorUnit",
3201
+ "agentOtlpEndpoint",
3202
+ "custodianEmail",
3203
+ "smtp",
3204
+ "repository",
3205
+ "branch",
3206
+ "deployProfile",
3207
+ "otlpFlushIntervalMs",
3208
+ "otlpTraceSampleRatio",
3209
+ "backup",
3210
+ "cloudflare"
3211
+ ], "runtime environment");
3212
+ if (runtime.credentialFiles !== undefined)
3213
+ exactKeys(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
3214
+ const environment = runtime.environment;
3215
+ if (environment.smtp !== undefined)
3216
+ exactKeys(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
3217
+ if (environment.backup !== undefined)
3218
+ exactKeys(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
3219
+ if (environment.cloudflare !== undefined)
3220
+ exactKeys(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService", "warp"], "Cloudflare runtime config");
3221
+ if (environment.cloudflare && typeof environment.cloudflare === "object" && environment.cloudflare.warp !== undefined) {
3222
+ exactKeys(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
3223
+ }
3224
+ } else if (root.kind === "tenant") {
3225
+ for (const key of ["environment", "profile", "computeReference", "database", "platformEnrolTokenFile", "runtime", "cloudflareHandoff"]) {
3226
+ if (root[key] !== undefined && key !== "profile")
3227
+ throw new Error(`tenant bootstrap cannot contain ${key}`);
3228
+ }
3229
+ } else
3230
+ throw new Error("bootstrap config kind must be platform or tenant");
3231
+ return value;
3232
+ }
3233
+ function readBootstrapConfig(path) {
3234
+ const metadata = lstatSync(path);
3235
+ if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
3236
+ throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
3237
+ }
3238
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync(path, "utf8"))));
3239
+ }
3240
+ function localBootstrapHost() {
3241
+ const execute = async (argv, options = {}) => {
3242
+ const child = Bun.spawn([...argv], { stdin: options.stdin === undefined ? "ignore" : "pipe", stdout: "pipe", stderr: "pipe" });
3243
+ if (options.stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
3244
+ child.stdin.write(options.stdin);
3245
+ child.stdin.end();
3246
+ }
3247
+ const [stdout, stderr, exitCode] = await Promise.all([
3248
+ new Response(child.stdout).text(),
3249
+ new Response(child.stderr).text(),
3250
+ child.exited
3251
+ ]);
3252
+ return { exitCode, output: `${stdout}${stderr}` };
3253
+ };
3254
+ return {
3255
+ uid: () => process.getuid?.() ?? -1,
3256
+ exists: existsSync,
3257
+ read: (path) => readFileSync(path, "utf8"),
3258
+ write(path, content, mode) {
3259
+ mkdirSync(dirname2(path), { recursive: true, mode: 493 });
3260
+ const temporary = `${path}.next.${process.pid}`;
3261
+ writeFileSync(temporary, content, { mode });
3262
+ chmodSync(temporary, mode);
3263
+ renameSync(temporary, path);
3264
+ },
3265
+ mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
3266
+ remove: (path) => rmSync(path, { force: true }),
3267
+ inspect(path) {
3268
+ const value = lstatSync(path);
3269
+ return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
3270
+ },
3271
+ exec: execute,
3272
+ async ensureSoftware(requirements) {
3273
+ const result = await execute([
3274
+ "runuser",
3275
+ "-u",
3276
+ "forgezero-agent",
3277
+ "--",
3278
+ "/usr/local/bin/fz-agent",
3279
+ "software-ensure",
3280
+ ...requirements.map(({ id, version }) => `--require=${id}@${version}`)
3281
+ ]);
3282
+ if (result.exitCode !== 0)
3283
+ throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
3284
+ return result;
3285
+ },
3286
+ async installAgent(config, enrolTokenSourcePath) {
3287
+ const capabilities = await readCapabilities(localRunner);
3288
+ const deployRoot = config.deployRoot ?? "/opt/forgezero";
3289
+ if (config.kind === "platform") {
3290
+ const lifecycle = config.database.role === "none" ? {
3291
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
3292
+ apiHealthUrl: "http://127.0.0.1:3000/api/health"
3293
+ } : {
3294
+ apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
3295
+ databaseUnit: "forgezero-db.service",
3296
+ apiHealthUrl: "http://127.0.0.1:3000/api/health",
3297
+ databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
3298
+ databasePorts: [8529]
3299
+ };
3300
+ mkdirSync(dirname2(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
3301
+ writeFileSync(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
3302
+ `, { mode: 256 });
3303
+ }
3304
+ const plan = planInstall({
3305
+ capabilities,
3306
+ socketPath: DEFAULT_SOCKET2,
3307
+ seedPath: "/var/lib/forgezero/node.seed",
3308
+ controlSocketPath: "/run/forgezero/control.sock",
3309
+ repository: config.repository,
3310
+ branch: config.branch,
3311
+ profile: config.kind === "platform" ? config.profile : config.profile,
3312
+ deployRoot,
3313
+ publicApiUrl: config.apiUrl,
3314
+ gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
3315
+ gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
3316
+ generateGitIdentity: true,
3317
+ pullDeployments: true,
3318
+ pullMigrations: config.kind === "platform",
3319
+ lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
3320
+ ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
3321
+ warpOrganization: config.runtime.environment.cloudflare.warp.organization,
3322
+ warpClientIdCredentialPath: WARP_CLIENT_ID_CREDENTIAL,
3323
+ warpClientSecretCredentialPath: WARP_CLIENT_SECRET_CREDENTIAL,
3324
+ cloudflareAccountId: config.runtime.environment.cloudflare.accountId,
3325
+ cloudflareTunnelId: config.runtime.environment.cloudflare.tunnelId,
3326
+ cloudflareVirtualNetworkId: config.runtime.environment.cloudflare.warp.virtualNetworkId,
3327
+ cloudflareWarpPolicyId: config.runtime.environment.cloudflare.warp.deviceProfileId
3328
+ } : {},
3329
+ enforceEgress: true,
3330
+ nodeHostname: config.nodeHostname,
3331
+ telemetryEndpoint: config.telemetryEndpoint,
3332
+ binPath: "/usr/local/lib/forgezero/agent/fz-agent",
3333
+ sourceBinPath: PACKAGED_AGENT_BIN,
3334
+ ...config.kind === "tenant" || enrolTokenSourcePath ? {
3335
+ enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
3336
+ enrolTokenCredentialPath: ENROL_CREDENTIAL,
3337
+ enrolStatePath: "/var/lib/forgezero/enrolment.json",
3338
+ apiUrl: config.apiUrl,
3339
+ project: config.kind === "tenant" ? config.realm : "platform",
3340
+ environment: config.kind === "tenant" ? undefined : config.environment,
3341
+ nodeLabel: config.kind === "tenant" ? config.nodeHostname : config.computeReference
3342
+ } : {}
3343
+ });
3344
+ for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
3345
+ mkdirSync(dirname2(unit.path), { recursive: true, mode: 493 });
3346
+ writeFileSync(unit.path, unit.unit, { mode: 420 });
3347
+ }
3348
+ await applyPlan(plan, localRunner);
3349
+ return plan;
3350
+ }
3351
+ };
3352
+ }
3353
+ export {
3354
+ validateBootstrapConfig,
3355
+ readBootstrapConfig,
3356
+ planBootstrap,
3357
+ localBootstrapHost,
3358
+ bootstrapStatus,
3359
+ applyBootstrap,
3360
+ PLATFORM_BOOTSTRAP_PROFILES
3361
+ };