@forgezero/agent 0.1.40 → 0.1.42

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.
Files changed (64) hide show
  1. package/README.md +573 -424
  2. package/dist/agent-heartbeat.js +6 -3
  3. package/dist/agent-update-helper.js +5 -2
  4. package/dist/agent-update.js +5 -2
  5. package/dist/bootstrap.d.ts +17 -8
  6. package/dist/bootstrap.js +1504 -512
  7. package/dist/cli/agent-install.d.ts +6 -5
  8. package/dist/cli/cloudflare-bootstrap.d.ts +12 -1
  9. package/dist/cli/maintenance.d.ts +23 -0
  10. package/dist/cli/run.d.ts +3 -1
  11. package/dist/cli/session-store.d.ts +5 -0
  12. package/dist/cloudflare-bootstrap.d.ts +73 -35
  13. package/dist/cloudflare-bootstrap.js +587 -90
  14. package/dist/cloudflare-edge.d.ts +64 -12
  15. package/dist/cloudflare-edge.js +103 -8
  16. package/dist/community-rehearsal-host.d.ts +51 -0
  17. package/dist/community-rehearsal-host.js +272 -0
  18. package/dist/credential-schema.d.ts +54 -0
  19. package/dist/credential-schema.js +47 -0
  20. package/dist/definition.d.ts +31 -5
  21. package/dist/definition.js +271 -44
  22. package/dist/deploy-file.js +294 -68
  23. package/dist/deployment-runner.js +18 -5
  24. package/dist/deployment.d.ts +13 -1
  25. package/dist/fz-agent.js +9162 -7564
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +5679 -5077
  28. package/dist/git-ssh.d.ts +5 -0
  29. package/dist/guest-enrolment.d.ts +2 -0
  30. package/dist/guest-enrolment.js +1 -0
  31. package/dist/host-maintenance.d.ts +39 -0
  32. package/dist/host-maintenance.js +135 -0
  33. package/dist/index.d.ts +4 -2
  34. package/dist/mesh-connector.d.ts +16 -0
  35. package/dist/mesh-connector.js +46 -0
  36. package/dist/metal-bootstrap.js +145 -7
  37. package/dist/metal-helper-socket.js +61 -31
  38. package/dist/metal-provision.d.ts +2 -2
  39. package/dist/metal-provision.js +62 -32
  40. package/dist/operator-bootstrap.d.ts +90 -0
  41. package/dist/operator-bootstrap.js +5704 -0
  42. package/dist/otel-collector.d.ts +18 -0
  43. package/dist/pipeline.d.ts +3 -2
  44. package/dist/pipeline.js +1 -1
  45. package/dist/platform-bootstrap-runtime.d.ts +39 -21
  46. package/dist/platform-bootstrap-runtime.js +182 -59
  47. package/dist/platform-fleet-verification.d.ts +19 -0
  48. package/dist/platform-fleet-verification.js +3873 -0
  49. package/dist/platform-genesis-config.d.ts +7 -0
  50. package/dist/platform-genesis.d.ts +17 -0
  51. package/dist/provision.d.ts +76 -3
  52. package/dist/provision.js +1061 -229
  53. package/dist/recovery-host.d.ts +7 -0
  54. package/dist/recovery-host.js +124 -0
  55. package/dist/service-supervisor.d.ts +42 -0
  56. package/dist/software-helper.d.ts +4 -0
  57. package/dist/software-helper.js +865 -63
  58. package/dist/software.d.ts +14 -3
  59. package/dist/software.js +163 -37
  60. package/dist/ssh-bootstrap.d.ts +97 -0
  61. package/dist/supervised-app.d.ts +2 -0
  62. package/dist/version.d.ts +1 -1
  63. package/package.json +175 -164
  64. package/schema/{deploy-v2.json → deploy-v3.json} +53 -6
package/dist/bootstrap.js CHANGED
@@ -94,6 +94,39 @@ async function removeCloudflarePrivateDatabaseRoute(config, fetcher = fetch) {
94
94
  network: privateDatabaseHostRoute(config.privateAddress)
95
95
  }, fetcher);
96
96
  }
97
+ var privateNetworkCidr = (value) => {
98
+ const [address, prefixText, ...extra] = value.trim().toLowerCase().split("/");
99
+ const family = isIP(address ?? "");
100
+ const prefix = Number(prefixText);
101
+ if (extra.length || !family || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128) || !isPrivateDatabaseAddress(address)) {
102
+ throw new Error("Cloudflare WARP include must be an explicit private IPv4 or IPv6 CIDR");
103
+ }
104
+ return `${address}/${prefix}`;
105
+ };
106
+ async function ensureCloudflareWarpNetworkIncludes(config, fetcher = fetch) {
107
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
108
+ throw new Error("Cloudflare WARP policy id is invalid");
109
+ if (!Array.isArray(config.networks) || config.networks.length < 1 || config.networks.length > 256) {
110
+ throw new Error("Cloudflare WARP networks must contain 1-256 explicit CIDRs");
111
+ }
112
+ const networks = config.networks.map(privateNetworkCidr);
113
+ if (new Set(networks).size !== networks.length)
114
+ throw new Error("Cloudflare WARP networks must be unique");
115
+ const path = `/accounts/${config.accountId}/devices/policy/${config.policyId}/include`;
116
+ const entries = await cf(config, path, {}, fetcher);
117
+ const present = new Set(entries.flatMap(({ address }) => address ? [address.toLowerCase()] : []));
118
+ const additions = networks.filter((network) => !present.has(network)).map((address) => ({
119
+ address,
120
+ description: `${config.descriptionPrefix}:${address}`.slice(0, 100)
121
+ }));
122
+ if (!additions.length)
123
+ return { entries, created: 0 };
124
+ const updated = await cf(config, path, {
125
+ method: "PUT",
126
+ body: JSON.stringify([...entries, ...additions])
127
+ }, fetcher);
128
+ return { entries: updated, created: additions.length };
129
+ }
97
130
  async function ensureCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
98
131
  if (config.policyId && !/^[A-Za-z0-9-]{1,64}$/.test(config.policyId))
99
132
  throw new Error("Cloudflare WARP policy id is invalid");
@@ -127,13 +160,11 @@ async function removeCloudflareWarpDatabaseInclude(config, fetcher = fetch) {
127
160
  return { entries: updated, removed: true };
128
161
  }
129
162
  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
163
  const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
133
164
  const dnsPath = `/zones/${config.zoneId}/dns_records`;
134
165
  const [current, records] = await Promise.all([
135
- cf(tunnelAuth, tunnelPath, {}, fetcher),
136
- cf(dnsAuth, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
166
+ cf(config, tunnelPath, {}, fetcher),
167
+ cf(config, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
137
168
  ]);
138
169
  if (records.length > 1) {
139
170
  throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
@@ -151,7 +182,7 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
151
182
  ...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
152
183
  ];
153
184
  if (JSON.stringify(existing) !== JSON.stringify(desiredIngress)) {
154
- await cf(tunnelAuth, tunnelPath, {
185
+ await cf(config, tunnelPath, {
155
186
  method: "PUT",
156
187
  body: JSON.stringify({ config: { ingress: desiredIngress } })
157
188
  }, fetcher);
@@ -165,12 +196,72 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
165
196
  };
166
197
  const dnsAlreadyCorrect = existingRecord?.type === record.type && existingRecord.name?.toLowerCase() === record.name.toLowerCase() && existingRecord.content?.toLowerCase() === record.content.toLowerCase() && existingRecord.proxied === true && existingRecord.ttl === 1;
167
198
  if (!dnsAlreadyCorrect) {
168
- await cf(dnsAuth, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
169
- method: existingRecord ? "PUT" : "POST",
170
- body: JSON.stringify(record)
199
+ await cf(config, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
200
+ method: existingRecord ? "PATCH" : "POST",
201
+ body: JSON.stringify(existingRecord ? {
202
+ ...record,
203
+ ...existingRecord.comment === undefined ? {} : { comment: existingRecord.comment },
204
+ ...existingRecord.tags === undefined ? {} : { tags: existingRecord.tags },
205
+ ...existingRecord.settings === undefined ? {} : { settings: existingRecord.settings }
206
+ } : record)
171
207
  }, fetcher);
172
208
  }
173
209
  }
210
+ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
211
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
212
+ throw new Error("Cloudflare Worker script name is invalid");
213
+ }
214
+ const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
215
+ const owned = namespaces.filter(({ script }) => script === config.scriptName);
216
+ if (!owned.length)
217
+ throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
218
+ return owned;
219
+ }
220
+ async function configureCloudflareRealtimeSecrets(config, fetcher = fetch) {
221
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName) || ![config.publishSecret, config.ticketSecret].every((value) => /^[A-Za-z0-9_-]{64,128}$/.test(value)) || config.publishSecret === config.ticketSecret) {
222
+ throw new Error("Cloudflare realtime secret coordinates are invalid");
223
+ }
224
+ await cf(config, `/accounts/${config.accountId}/workers/scripts/${encodeURIComponent(config.scriptName)}/secrets-bulk`, {
225
+ method: "PATCH",
226
+ body: JSON.stringify({
227
+ secrets: {
228
+ REALTIME_PUBLISH_SECRET: {
229
+ name: "REALTIME_PUBLISH_SECRET",
230
+ text: config.publishSecret,
231
+ type: "secret_text"
232
+ },
233
+ REALTIME_TICKET_SECRET: {
234
+ name: "REALTIME_TICKET_SECRET",
235
+ text: config.ticketSecret,
236
+ type: "secret_text"
237
+ }
238
+ }
239
+ })
240
+ }, fetcher);
241
+ }
242
+ async function ensureCloudflareMeshConnector(config, fetcher = fetch) {
243
+ const name = config.name.trim();
244
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
245
+ throw new Error("Cloudflare Mesh connector name is invalid");
246
+ }
247
+ const path = `/accounts/${config.accountId}/warp_connector`;
248
+ const connectors = await cf(config, `${path}?is_deleted=false&name=${encodeURIComponent(name)}&per_page=1000`, {}, fetcher);
249
+ const matches = connectors.filter((connector2) => connector2.name === name && !connector2.deleted_at);
250
+ if (matches.length > 1)
251
+ throw new Error(`Cloudflare Mesh connector ${name} is ambiguous`);
252
+ const created = !matches[0];
253
+ const connector = matches[0] ?? await cf(config, path, {
254
+ method: "POST",
255
+ body: JSON.stringify({ name, ha: config.highAvailability })
256
+ }, fetcher);
257
+ if (!connector.id)
258
+ throw new Error("Cloudflare returned an invalid Mesh connector");
259
+ const connectorToken = await cf(config, `${path}/${encodeURIComponent(connector.id)}/token`, {}, fetcher);
260
+ if (!connectorToken || connectorToken.length > 16384) {
261
+ throw new Error("Cloudflare returned an invalid Mesh connector token");
262
+ }
263
+ return { connector, connectorToken, created };
264
+ }
174
265
  async function ensureCloudflareTunnel(config, fetcher = fetch) {
175
266
  const name = config.name.trim();
176
267
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/.test(name)) {
@@ -195,13 +286,15 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
195
286
 
196
287
  // src/cloudflare-bootstrap.ts
197
288
  import { constants } from "fs";
198
- import { randomUUID } from "crypto";
199
- import { chmod, lstat, mkdir, open, rename, stat, unlink } from "fs/promises";
289
+ import { randomBytes, randomUUID } from "crypto";
290
+ import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
200
291
  import { dirname, join, resolve } from "path";
292
+ import { isIP as isIP2 } from "net";
201
293
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
202
294
  var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
203
295
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
204
296
  var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
297
+ var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
205
298
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
206
299
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
207
300
  const metadata = await handle.stat();
@@ -242,33 +335,23 @@ async function readOwnerApiToken(path) {
242
335
  return token;
243
336
  }
244
337
  async function readCloudflareBootstrapTokens(files) {
245
- const entries = await Promise.all([
246
- ["apiToken", files.apiTokenFile],
247
- ["managementApiToken", files.managementApiTokenFile],
248
- ["runtimeApiToken", files.runtimeApiTokenFile],
249
- ["tunnelApiToken", files.tunnelApiTokenFile],
250
- ["dnsApiToken", files.dnsApiTokenFile],
251
- ["kvApiToken", files.kvApiTokenFile]
252
- ].map(async ([key, path]) => [key, path ? await readOwnerApiToken(path) : undefined]));
253
- const supplied = Object.fromEntries(entries.filter(([, value]) => value !== undefined));
254
- const tokens = {
255
- ...supplied.apiToken ? { apiToken: supplied.apiToken } : {},
256
- ...supplied.tunnelApiToken || supplied.managementApiToken ? {
257
- tunnelApiToken: supplied.tunnelApiToken ?? supplied.managementApiToken
258
- } : {},
259
- ...supplied.dnsApiToken || supplied.managementApiToken ? {
260
- dnsApiToken: supplied.dnsApiToken ?? supplied.managementApiToken
261
- } : {},
262
- ...supplied.kvApiToken || supplied.runtimeApiToken ? {
263
- kvApiToken: supplied.kvApiToken ?? supplied.runtimeApiToken
264
- } : {}
265
- };
266
- for (const key of ["tunnelApiToken", "dnsApiToken", "kvApiToken"]) {
267
- if (!tokens[key] && !tokens.apiToken) {
268
- throw new Error(`Cloudflare ${key} file is required when apiTokenFile is omitted`);
269
- }
338
+ const unsupported = Object.keys(files).filter((key) => ![
339
+ "tunnelTokenFile",
340
+ "apiTokenFile"
341
+ ].includes(key));
342
+ if (unsupported.length)
343
+ throw new Error(`Cloudflare bootstrap token files contain unsupported field ${unsupported[0]}`);
344
+ if (!files.tunnelTokenFile?.trim() || !files.apiTokenFile?.trim()) {
345
+ throw new Error("Cloudflare bootstrap requires exactly tunnelTokenFile and apiTokenFile");
346
+ }
347
+ const [tunnelToken, apiToken] = await Promise.all([
348
+ readOwnerApiToken(files.tunnelTokenFile),
349
+ readOwnerApiToken(files.apiTokenFile)
350
+ ]);
351
+ if (tunnelToken === apiToken) {
352
+ throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
270
353
  }
271
- return tokens;
354
+ return { tunnelToken, apiToken };
272
355
  }
273
356
  var validateId = (value, label) => {
274
357
  const normalized = value.trim().toLowerCase();
@@ -294,28 +377,89 @@ var normalizeService = (value) => {
294
377
  }
295
378
  return service.toString().replace(/\/$/, "");
296
379
  };
380
+ var privateMeshCidr = (value) => {
381
+ const [address, prefixText, ...extra] = value.trim().toLowerCase().split("/");
382
+ const family = isIP2(address ?? "");
383
+ const prefix = Number(prefixText);
384
+ const v4 = family === 4 ? address.split(".").map(Number) : undefined;
385
+ const privateAddress = family === 4 ? Boolean(v4 && (v4[0] === 10 || v4[0] === 172 && v4[1] >= 16 && v4[1] <= 31 || v4[0] === 192 && v4[1] === 168)) : family === 6 && /^f[cd][0-9a-f]:/i.test(address);
386
+ if (extra.length || !family || !privateAddress || !Number.isInteger(prefix) || prefix < 0 || prefix > (family === 4 ? 32 : 128)) {
387
+ throw new Error("Cloudflare Mesh route must be an explicit private IPv4 or IPv6 CIDR");
388
+ }
389
+ return `${address}/${prefix}`;
390
+ };
297
391
  function validateCloudflareBootstrapCoordinates(input) {
298
- const nodeInputs = input.nodes?.length ? input.nodes : [{
299
- nodeName: input.tunnelName,
300
- hostname: input.hostname,
301
- service: input.service,
302
- tunnelName: input.tunnelName
303
- }];
392
+ if (!input || typeof input !== "object" || Array.isArray(input)) {
393
+ throw new Error("Cloudflare bootstrap coordinates must be an object");
394
+ }
395
+ const unsupported = Object.keys(input).filter((key) => ![
396
+ "accountId",
397
+ "zoneId",
398
+ "kvNamespaceId",
399
+ "meshDevicePolicyId",
400
+ "realtime",
401
+ "nodes"
402
+ ].includes(key));
403
+ if (unsupported.length)
404
+ throw new Error(`Cloudflare bootstrap coordinates contain unsupported field ${unsupported[0]}`);
405
+ const nodeInputs = input.nodes;
406
+ if (!Array.isArray(nodeInputs))
407
+ throw new Error("Cloudflare bootstrap requires an explicit nodes array");
304
408
  if (nodeInputs.length < 1 || nodeInputs.length > 32) {
305
409
  throw new Error("Cloudflare bootstrap requires between 1 and 32 explicit nodes");
306
410
  }
307
411
  const nodes = nodeInputs.map((node) => {
412
+ if (!node || typeof node !== "object" || Array.isArray(node)) {
413
+ throw new Error("Cloudflare bootstrap node must be an object");
414
+ }
415
+ const unknownNode = Object.keys(node).filter((key) => ![
416
+ "nodeName",
417
+ "hostname",
418
+ "service",
419
+ "tunnelName",
420
+ "mesh"
421
+ ].includes(key));
422
+ if (unknownNode.length)
423
+ throw new Error(`Cloudflare bootstrap node contains unsupported field ${unknownNode[0]}`);
308
424
  const nodeName = node.nodeName.trim().toLowerCase();
309
425
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName))
310
426
  throw new Error("Cloudflare node name is invalid");
311
427
  const hostname = node.hostname.trim().toLowerCase();
312
428
  if (!HOSTNAME.test(hostname))
313
429
  throw new Error("Cloudflare public node hostname is invalid");
430
+ let mesh;
431
+ if (node.mesh !== undefined) {
432
+ if (!node.mesh || typeof node.mesh !== "object" || Array.isArray(node.mesh)) {
433
+ throw new Error("Cloudflare Mesh coordinates must be an object");
434
+ }
435
+ const unknownMesh = Object.keys(node.mesh).filter((key) => ![
436
+ "connectorName",
437
+ "routes",
438
+ "highAvailability"
439
+ ].includes(key));
440
+ if (unknownMesh.length)
441
+ throw new Error(`Cloudflare Mesh contains unsupported field ${unknownMesh[0]}`);
442
+ if (!Array.isArray(node.mesh.routes) || node.mesh.routes.length < 1 || node.mesh.routes.length > 64) {
443
+ throw new Error("Cloudflare Mesh node requires 1-64 private routes");
444
+ }
445
+ const routes = node.mesh.routes.map(privateMeshCidr);
446
+ if (new Set(routes).size !== routes.length)
447
+ throw new Error("Cloudflare Mesh routes must be unique per node");
448
+ if (node.mesh.highAvailability !== undefined && typeof node.mesh.highAvailability !== "boolean") {
449
+ throw new Error("Cloudflare Mesh highAvailability must be a boolean");
450
+ }
451
+ mesh = {
452
+ connectorName: validateName(node.mesh.connectorName, "Cloudflare Mesh connector name"),
453
+ routes,
454
+ highAvailability: node.mesh.highAvailability === true
455
+ };
456
+ }
314
457
  return {
315
458
  nodeName,
316
459
  hostname,
317
460
  service: normalizeService(node.service),
318
- tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name")
461
+ tunnelName: validateName(node.tunnelName, "Cloudflare Tunnel name"),
462
+ ...mesh ? { mesh } : {}
319
463
  };
320
464
  });
321
465
  for (const [label, values] of [
@@ -326,14 +470,51 @@ function validateCloudflareBootstrapCoordinates(input) {
326
470
  if (new Set(values).size !== values.length)
327
471
  throw new Error(`Cloudflare fleet ${label} must be unique`);
328
472
  }
329
- const first = nodes[0];
473
+ const meshNodes = nodes.filter(({ mesh }) => mesh);
474
+ if (meshNodes.length && !input.meshDevicePolicyId?.trim()) {
475
+ throw new Error("Cloudflare Mesh requires an explicit dedicated device policy id");
476
+ }
477
+ if (!meshNodes.length && input.meshDevicePolicyId !== undefined) {
478
+ throw new Error("Cloudflare Mesh device policy is not allowed without Mesh nodes");
479
+ }
480
+ const connectorNames = meshNodes.map(({ mesh }) => mesh.connectorName);
481
+ if (new Set(connectorNames).size !== connectorNames.length) {
482
+ throw new Error("Cloudflare Mesh connector names must be unique");
483
+ }
484
+ const routeOwners = new Map;
485
+ for (const node of meshNodes)
486
+ for (const route of node.mesh.routes) {
487
+ const owner = routeOwners.get(route);
488
+ if (owner)
489
+ throw new Error(`Cloudflare Mesh route ${route} is declared by both ${owner} and ${node.nodeName}`);
490
+ routeOwners.set(route, node.nodeName);
491
+ }
492
+ let realtime;
493
+ if (input.realtime !== undefined) {
494
+ if (!input.realtime || typeof input.realtime !== "object" || Array.isArray(input.realtime) || Object.keys(input.realtime).some((key) => !["workerScriptName", "endpoint", "producer"].includes(key))) {
495
+ throw new Error("Cloudflare realtime coordinates are malformed");
496
+ }
497
+ const workerScriptName = input.realtime.workerScriptName.trim();
498
+ const producer = input.realtime.producer.trim();
499
+ let endpoint2;
500
+ try {
501
+ endpoint2 = new URL(input.realtime.endpoint);
502
+ } catch {
503
+ throw new Error("Cloudflare realtime endpoint is malformed");
504
+ }
505
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(workerScriptName) || !/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$/.test(producer) || endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.pathname !== "/" || endpoint2.search || endpoint2.hash) {
506
+ throw new Error("Cloudflare realtime Worker, endpoint, or producer is invalid");
507
+ }
508
+ realtime = { workerScriptName, endpoint: endpoint2.origin, producer };
509
+ }
330
510
  return {
331
511
  accountId: validateId(input.accountId, "Cloudflare account id"),
332
512
  zoneId: validateId(input.zoneId, "Cloudflare zone id"),
333
- hostname: first.hostname,
334
- service: first.service,
335
- tunnelName: first.tunnelName,
336
513
  kvNamespaceId: validateId(input.kvNamespaceId, "Cloudflare KV namespace id"),
514
+ ...input.meshDevicePolicyId ? {
515
+ meshDevicePolicyId: validateName(input.meshDevicePolicyId, "Cloudflare Mesh device policy id")
516
+ } : {},
517
+ ...realtime ? { realtime } : {},
337
518
  nodes
338
519
  };
339
520
  }
@@ -346,18 +527,50 @@ function planCloudflareBootstrap(input, outputPath) {
346
527
  outputFile: resolve(outputPath),
347
528
  coordinates,
348
529
  operations: [
530
+ "prove CF_API_TOKEN can write, read and remove one namespaced nonce in the existing KV namespace",
531
+ ...coordinates.realtime ? [
532
+ "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
533
+ ] : [],
349
534
  "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
535
+ ...coordinates.nodes.some(({ mesh }) => mesh) ? [
536
+ "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its registration token",
537
+ "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
538
+ ] : [],
350
539
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
351
540
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
352
- "write one node-specific handoff containing the connector token and owner-supplied KV-write token"
541
+ "write one node-specific handoff containing the connector token and owner-supplied KV/Worker runtime token"
353
542
  ],
354
543
  secrets: [
355
544
  "API tokens are read only from owner-only files and are never placed in argv or stdout",
356
- "the management token is never persisted; connector and KV runtime capabilities are atomically checkpointed with mode 0600",
357
- "the normal API process receives only its KV-write token and never Tunnel or DNS management authority"
545
+ "CF_TUNNEL_TOKEN is never persisted; cloudflared, Mesh, CF_API_TOKEN and generated realtime capabilities are atomically checkpointed with mode 0600",
546
+ "the normal API process receives only CF_API_TOKEN and never Tunnel, DNS or Mesh management authority"
358
547
  ]
359
548
  };
360
549
  }
550
+ async function preflightCloudflareKvRuntime(coordinates, apiToken, fetcher) {
551
+ const nonce = randomUUID();
552
+ const key = `forgezero/bootstrap-preflight/${nonce}`;
553
+ const url = `https://api.cloudflare.com/client/v4/accounts/${coordinates.accountId}` + `/storage/kv/namespaces/${coordinates.kvNamespaceId}/values/${encodeURIComponent(key)}`;
554
+ const headers = { authorization: `Bearer ${apiToken}`, "content-type": "text/plain" };
555
+ let written = false;
556
+ try {
557
+ const put = await fetcher(url, { method: "PUT", headers, body: nonce });
558
+ if (!put.ok)
559
+ throw new Error(`Cloudflare KV runtime preflight write returned HTTP ${put.status}`);
560
+ written = true;
561
+ const get = await fetcher(url, { method: "GET", headers: { authorization: `Bearer ${apiToken}` } });
562
+ if (!get.ok)
563
+ throw new Error(`Cloudflare KV runtime preflight read returned HTTP ${get.status}`);
564
+ if (await get.text() !== nonce)
565
+ throw new Error("Cloudflare KV runtime preflight read returned unexpected data");
566
+ } finally {
567
+ if (written) {
568
+ const removed = await fetcher(url, { method: "DELETE", headers: { authorization: `Bearer ${apiToken}` } });
569
+ if (!removed.ok)
570
+ throw new Error(`Cloudflare KV runtime preflight cleanup returned HTTP ${removed.status}`);
571
+ }
572
+ }
573
+ }
361
574
  async function readExistingOutput(path) {
362
575
  try {
363
576
  await lstat(path);
@@ -374,10 +587,84 @@ async function readExistingOutput(path) {
374
587
  throw new Error(`${resolve(path)} is not valid bootstrap JSON`);
375
588
  throw cause;
376
589
  }
590
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
591
+ throw new Error(`${resolve(path)} is not a ForgeZero Cloudflare bootstrap output`);
592
+ }
377
593
  const output = parsed;
378
- if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !output.resources) {
594
+ const unsupported = Object.keys(output).filter((key) => ![
595
+ "format",
596
+ "kind",
597
+ "phase",
598
+ "updatedAt",
599
+ "coordinates",
600
+ "resources",
601
+ "created"
602
+ ].includes(key));
603
+ if (unsupported.length)
604
+ throw new Error(`${resolve(path)} contains unsupported field ${unsupported[0]}`);
605
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-bootstrap" || !["edge-resources-provisioned", "complete"].includes(String(output.phase)) || typeof output.updatedAt !== "string" || Number.isNaN(Date.parse(output.updatedAt)) || !output.resources || typeof output.resources !== "object" || Array.isArray(output.resources)) {
379
606
  throw new Error(`${resolve(path)} is not a ForgeZero Cloudflare bootstrap output`);
380
607
  }
608
+ const resources = output.resources;
609
+ const unsupportedResource = Object.keys(resources).filter((key) => ![
610
+ "kvNamespaceId",
611
+ "apiToken",
612
+ "realtime",
613
+ "nodes"
614
+ ].includes(key));
615
+ if (unsupportedResource.length) {
616
+ throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
617
+ }
618
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
619
+ if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
620
+ throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
621
+ }
622
+ if (coordinates.realtime) {
623
+ const realtime = resources.realtime;
624
+ if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
625
+ throw new Error(`${resolve(path)} has malformed Cloudflare realtime resources`);
626
+ }
627
+ } else if (resources.realtime !== undefined) {
628
+ throw new Error(`${resolve(path)} contains undeclared Cloudflare realtime resources`);
629
+ }
630
+ const seen = new Set;
631
+ for (const item of resources.nodes) {
632
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
633
+ throw new Error(`${resolve(path)} has a malformed Cloudflare node resource`);
634
+ }
635
+ const node = item;
636
+ const unknownNode = Object.keys(node).filter((key) => ![
637
+ "nodeName",
638
+ "hostname",
639
+ "service",
640
+ "tunnelName",
641
+ "tunnelId",
642
+ "connectorToken",
643
+ "mesh"
644
+ ].includes(key));
645
+ const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
646
+ if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? "")) || !CONNECTOR_TOKEN.test(String(node.connectorToken ?? ""))) {
647
+ throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
648
+ }
649
+ if (expected.mesh) {
650
+ const mesh = node.mesh;
651
+ if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? "")) || !CONNECTOR_TOKEN.test(String(mesh.connectorToken ?? ""))) {
652
+ throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
653
+ }
654
+ } else if (node.mesh !== undefined) {
655
+ throw new Error(`${resolve(path)} contains an undeclared Cloudflare Mesh resource`);
656
+ }
657
+ seen.add(expected.nodeName);
658
+ }
659
+ if (output.phase === "complete" && resources.nodes.length !== coordinates.nodes.length) {
660
+ throw new Error(`${resolve(path)} completed output does not cover the declared node fleet`);
661
+ }
662
+ if (output.created !== undefined) {
663
+ if (!output.created || typeof output.created !== "object" || Array.isArray(output.created) || Object.keys(output.created).some((key) => key !== "nodes") || !Array.isArray(output.created.nodes) || output.created.nodes.some((node) => !node || typeof node !== "object" || Array.isArray(node) || Object.keys(node).some((key) => !["nodeName", "tunnel", "mesh"].includes(key)) || typeof node.nodeName !== "string" || typeof node.tunnel !== "boolean" || typeof node.mesh !== "boolean")) {
664
+ throw new Error(`${resolve(path)} has malformed Cloudflare creation evidence`);
665
+ }
666
+ }
667
+ output.coordinates = coordinates;
381
668
  return output;
382
669
  }
383
670
  function cloudflareHostHandoffPath(checkpointPath, nodeName) {
@@ -410,7 +697,12 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
410
697
  hostname: resource.hostname,
411
698
  service: resource.service,
412
699
  tunnelId: resource.tunnelId,
413
- connectorToken: resource.connectorToken
700
+ connectorToken: resource.connectorToken,
701
+ ...resource.mesh ? { mesh: {
702
+ connectorId: resource.mesh.connectorId,
703
+ connectorToken: resource.mesh.connectorToken,
704
+ routes: resource.mesh.routes
705
+ } } : {}
414
706
  };
415
707
  }
416
708
  async function readCloudflareHostHandoff(handoffPath, nodeName) {
@@ -436,9 +728,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
436
728
  "accountId",
437
729
  "zoneId",
438
730
  "kvNamespaceId",
439
- "kvRuntimeToken",
440
- "privateNetworkRuntimeToken",
441
- "warp"
731
+ "apiToken",
732
+ "mesh",
733
+ "realtime"
442
734
  ].includes(key));
443
735
  if (unknown.length)
444
736
  throw new Error(`Cloudflare host handoff contains unsupported field ${unknown[0]}`);
@@ -447,17 +739,53 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
447
739
  try {
448
740
  service = normalizeService(output.service ?? "");
449
741
  } catch {}
450
- if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !CONNECTOR_TOKEN.test(output.connectorToken ?? "") || !TOKEN.test(output.kvRuntimeToken ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
742
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !CONNECTOR_TOKEN.test(output.connectorToken ?? "") || !TOKEN.test(output.apiToken ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
451
743
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
452
744
  }
453
- if (output.privateNetworkRuntimeToken !== undefined && !TOKEN.test(output.privateNetworkRuntimeToken)) {
454
- throw new Error("Cloudflare host handoff private-network capability is malformed");
745
+ if (output.mesh !== undefined) {
746
+ if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !CONNECTOR_TOKEN.test(output.mesh.connectorToken) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
747
+ try {
748
+ return privateMeshCidr(route) !== route;
749
+ } catch {
750
+ return true;
751
+ }
752
+ })) {
753
+ throw new Error("Cloudflare host handoff contains malformed Mesh coordinates");
754
+ }
455
755
  }
456
- if (Boolean(output.warp) !== Boolean(output.privateNetworkRuntimeToken)) {
457
- throw new Error("Cloudflare host handoff private-network resources and capability disagree");
756
+ if (output.realtime !== undefined) {
757
+ let realtime;
758
+ try {
759
+ const { workerScriptName, endpoint: endpoint2, producer } = output.realtime;
760
+ realtime = validateCloudflareBootstrapCoordinates({
761
+ accountId: output.accountId,
762
+ zoneId: output.zoneId,
763
+ kvNamespaceId: output.kvNamespaceId,
764
+ realtime: { workerScriptName, endpoint: endpoint2, producer },
765
+ nodes: [{
766
+ nodeName: output.nodeName,
767
+ hostname: output.hostname,
768
+ service: output.service,
769
+ tunnelName: "handoff"
770
+ }]
771
+ }).realtime;
772
+ } catch {
773
+ throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
774
+ }
775
+ if (!REALTIME_SECRET.test(output.realtime.publishSecret) || !REALTIME_SECRET.test(output.realtime.ticketSecret) || output.realtime.publishSecret === output.realtime.ticketSecret || realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
776
+ throw new Error("Cloudflare host handoff contains malformed realtime credentials");
777
+ }
458
778
  }
459
779
  const { format: _format, kind: _kind, ...handoff } = output;
460
- return handoff;
780
+ return {
781
+ ...handoff,
782
+ nodeName: normalizedNodeName,
783
+ hostname: handoff.hostname.toLowerCase(),
784
+ service,
785
+ accountId: handoff.accountId.toLowerCase(),
786
+ zoneId: handoff.zoneId.toLowerCase(),
787
+ kvNamespaceId: handoff.kvNamespaceId.toLowerCase()
788
+ };
461
789
  }
462
790
  async function prepareOwnerOutputDirectory(absolutePath) {
463
791
  const directory = dirname(absolutePath);
@@ -513,18 +841,22 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
513
841
  accountId: output.coordinates.accountId,
514
842
  zoneId: output.coordinates.zoneId,
515
843
  kvNamespaceId: output.resources.kvNamespaceId,
516
- kvRuntimeToken: output.resources.kvRuntimeToken
844
+ apiToken: output.resources.apiToken,
845
+ ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
846
+ ...output.coordinates.realtime,
847
+ ...output.resources.realtime
848
+ } } : {},
849
+ ...node.mesh ? { mesh: {
850
+ connectorId: node.mesh.connectorId,
851
+ connectorToken: node.mesh.connectorToken,
852
+ routes: node.mesh.routes
853
+ } } : {}
517
854
  };
518
855
  await writeOwnerJson(cloudflareHostHandoffPath(checkpointPath, node.nodeName), handoff);
519
856
  }
520
857
  }
521
- var tokenFor = (tokens, key) => {
522
- const token = tokens[key]?.trim() || tokens.apiToken?.trim();
523
- if (!token || !TOKEN.test(token))
524
- throw new Error(`Cloudflare ${key} is not configured`);
525
- return token;
526
- };
527
858
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
859
+ var newRealtimeSecret = () => randomBytes(48).toString("base64url");
528
860
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
529
861
  const coordinates = validateCloudflareBootstrapCoordinates(input);
530
862
  const absoluteOutput = resolve(outputPath);
@@ -533,7 +865,46 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
533
865
  throw new Error("bootstrap output belongs to different Cloudflare coordinates; choose a different output file");
534
866
  }
535
867
  await prepareOwnerOutputDirectory(absoluteOutput);
536
- const kvRuntimeToken = tokenFor(tokens, "kvApiToken");
868
+ const tunnelToken = tokens.tunnelToken.trim();
869
+ const apiToken = tokens.apiToken.trim();
870
+ if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken)) {
871
+ throw new Error("Cloudflare bootstrap tokens are malformed");
872
+ }
873
+ if (tunnelToken === apiToken) {
874
+ throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
875
+ }
876
+ await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
877
+ const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
878
+ publishSecret: newRealtimeSecret(),
879
+ ticketSecret: newRealtimeSecret()
880
+ } : undefined;
881
+ if (coordinates.realtime && realtime) {
882
+ await verifyCloudflareWorkerDurableObjects({
883
+ accountId: coordinates.accountId,
884
+ scriptName: coordinates.realtime.workerScriptName,
885
+ apiToken
886
+ }, fetcher);
887
+ await writeOwnerBootstrapOutput(absoluteOutput, {
888
+ format: 1,
889
+ kind: "forgezero-cloudflare-bootstrap",
890
+ phase: "edge-resources-provisioned",
891
+ updatedAt: new Date().toISOString(),
892
+ coordinates,
893
+ resources: {
894
+ kvNamespaceId: coordinates.kvNamespaceId,
895
+ apiToken,
896
+ realtime,
897
+ nodes: existing?.resources.nodes ?? []
898
+ }
899
+ });
900
+ await configureCloudflareRealtimeSecrets({
901
+ accountId: coordinates.accountId,
902
+ scriptName: coordinates.realtime.workerScriptName,
903
+ publishSecret: realtime.publishSecret,
904
+ ticketSecret: realtime.ticketSecret,
905
+ apiToken
906
+ }, fetcher);
907
+ }
537
908
  const nodeResources = [];
538
909
  const createdNodes = [];
539
910
  for (const node of coordinates.nodes) {
@@ -549,14 +920,33 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
549
920
  const tunnel = await ensureCloudflareTunnel({
550
921
  accountId: coordinates.accountId,
551
922
  name: node.tunnelName,
552
- apiToken: tokenFor(tokens, "tunnelApiToken")
923
+ apiToken: tunnelToken
553
924
  }, fetcher);
554
925
  created = tunnel.created;
555
- resource = { ...node, tunnelId: tunnel.tunnel.id, connectorToken: tunnel.connectorToken };
926
+ let mesh;
927
+ if (node.mesh) {
928
+ const ensured = await ensureCloudflareMeshConnector({
929
+ accountId: coordinates.accountId,
930
+ name: node.mesh.connectorName,
931
+ highAvailability: node.mesh.highAvailability === true,
932
+ apiToken: tunnelToken
933
+ }, fetcher);
934
+ mesh = {
935
+ ...node.mesh,
936
+ connectorId: ensured.connector.id,
937
+ connectorToken: ensured.connectorToken
938
+ };
939
+ }
940
+ const { mesh: _declaredMesh, ...publicNode } = node;
941
+ resource = {
942
+ ...publicNode,
943
+ tunnelId: tunnel.tunnel.id,
944
+ connectorToken: tunnel.connectorToken,
945
+ ...mesh ? { mesh } : {}
946
+ };
556
947
  }
557
948
  nodeResources.push(resource);
558
- createdNodes.push({ nodeName: node.nodeName, tunnel: created });
559
- const first2 = nodeResources[0];
949
+ createdNodes.push({ nodeName: node.nodeName, tunnel: created, mesh: Boolean(resource.mesh) });
560
950
  await writeOwnerBootstrapOutput(absoluteOutput, {
561
951
  format: 1,
562
952
  kind: "forgezero-cloudflare-bootstrap",
@@ -564,16 +954,34 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
564
954
  updatedAt: new Date().toISOString(),
565
955
  coordinates,
566
956
  resources: {
567
- tunnelId: first2.tunnelId,
568
957
  kvNamespaceId: coordinates.kvNamespaceId,
569
- hostname: first2.hostname,
570
- service: first2.service,
571
- connectorToken: first2.connectorToken,
572
- kvRuntimeToken,
958
+ apiToken,
959
+ ...realtime ? { realtime } : {},
573
960
  nodes: [...nodeResources]
574
961
  }
575
962
  });
576
963
  }
964
+ const meshResources = nodeResources.filter((node) => node.mesh);
965
+ for (const node of meshResources) {
966
+ for (const network of node.mesh.routes) {
967
+ await ensureCloudflarePrivateRoute({
968
+ accountId: coordinates.accountId,
969
+ tunnelId: node.mesh.connectorId,
970
+ network,
971
+ comment: `ForgeZero Mesh ${node.nodeName}`,
972
+ apiToken: tunnelToken
973
+ }, fetcher);
974
+ }
975
+ }
976
+ if (meshResources.length) {
977
+ await ensureCloudflareWarpNetworkIncludes({
978
+ accountId: coordinates.accountId,
979
+ policyId: coordinates.meshDevicePolicyId,
980
+ networks: meshResources.flatMap((node) => node.mesh.routes),
981
+ descriptionPrefix: "ForgeZero Mesh",
982
+ apiToken: tunnelToken
983
+ }, fetcher);
984
+ }
577
985
  for (const node of nodeResources) {
578
986
  await configureCloudflareEdge({
579
987
  accountId: coordinates.accountId,
@@ -581,12 +989,9 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
581
989
  tunnelId: node.tunnelId,
582
990
  hostname: node.hostname,
583
991
  service: node.service,
584
- apiToken: tokenFor(tokens, "tunnelApiToken"),
585
- tunnelApiToken: tokenFor(tokens, "tunnelApiToken"),
586
- dnsApiToken: tokenFor(tokens, "dnsApiToken")
992
+ apiToken: tunnelToken
587
993
  }, fetcher);
588
994
  }
589
- const first = nodeResources[0];
590
995
  const output = {
591
996
  format: 1,
592
997
  kind: "forgezero-cloudflare-bootstrap",
@@ -594,16 +999,12 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
594
999
  updatedAt: new Date().toISOString(),
595
1000
  coordinates,
596
1001
  resources: {
597
- tunnelId: first.tunnelId,
598
1002
  kvNamespaceId: coordinates.kvNamespaceId,
599
- hostname: first.hostname,
600
- service: first.service,
601
- connectorToken: first.connectorToken,
602
- kvRuntimeToken,
1003
+ apiToken,
1004
+ ...realtime ? { realtime } : {},
603
1005
  nodes: nodeResources
604
1006
  },
605
1007
  created: {
606
- tunnel: createdNodes.some(({ tunnel }) => tunnel),
607
1008
  nodes: createdNodes
608
1009
  }
609
1010
  };
@@ -622,8 +1023,8 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
622
1023
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
623
1024
  };
624
1025
  }
625
- if (!request.tokenFiles || !Object.values(request.tokenFiles).some(Boolean)) {
626
- throw new Error("Cloudflare apply requires owner-only management token file paths");
1026
+ if (!request.tokenFiles) {
1027
+ throw new Error("Cloudflare apply requires exactly two owner-only token file paths");
627
1028
  }
628
1029
  const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
629
1030
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
@@ -680,20 +1081,114 @@ async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fet
680
1081
  nodes
681
1082
  };
682
1083
  }
1084
+ async function readCloudflareBootstrapAcceptanceEvidence(acceptancePath, expectedNodes) {
1085
+ let parsed;
1086
+ try {
1087
+ parsed = JSON.parse(await readOwnerOnlyFile(acceptancePath, 65536));
1088
+ } catch (cause) {
1089
+ if (cause instanceof SyntaxError)
1090
+ throw new Error("Cloudflare acceptance evidence is not valid JSON");
1091
+ throw cause;
1092
+ }
1093
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1094
+ throw new Error("Cloudflare acceptance evidence is malformed");
1095
+ }
1096
+ const evidence = parsed;
1097
+ if (Object.keys(evidence).some((key) => ![
1098
+ "format",
1099
+ "kind",
1100
+ "checkpointFile",
1101
+ "verifiedAt",
1102
+ "nodes"
1103
+ ].includes(key)) || evidence.format !== 1 || evidence.kind !== "forgezero-cloudflare-bootstrap-acceptance" || typeof evidence.checkpointFile !== "string" || !evidence.checkpointFile || typeof evidence.verifiedAt !== "string" || Number.isNaN(Date.parse(evidence.verifiedAt)) || !Array.isArray(evidence.nodes) || evidence.nodes.length < 1 || evidence.nodes.length > 32) {
1104
+ throw new Error("Cloudflare acceptance evidence is malformed");
1105
+ }
1106
+ const nodes = evidence.nodes.map((item) => {
1107
+ if (!item || typeof item !== "object" || Array.isArray(item) || Object.keys(item).some((key) => !["nodeName", "hostname", "status"].includes(key))) {
1108
+ throw new Error("Cloudflare acceptance evidence contains a malformed node");
1109
+ }
1110
+ const node = item;
1111
+ const nodeName = node.nodeName?.trim().toLowerCase();
1112
+ const hostname = node.hostname?.trim().toLowerCase();
1113
+ if (!nodeName || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(nodeName) || !hostname || !HOSTNAME.test(hostname) || !Number.isInteger(node.status) || node.status < 200 || node.status > 299) {
1114
+ throw new Error("Cloudflare acceptance evidence contains a malformed node");
1115
+ }
1116
+ return { nodeName, hostname, status: node.status };
1117
+ });
1118
+ if (new Set(nodes.map(({ nodeName }) => nodeName)).size !== nodes.length || new Set(nodes.map(({ hostname }) => hostname)).size !== nodes.length) {
1119
+ throw new Error("Cloudflare acceptance evidence contains duplicate nodes");
1120
+ }
1121
+ if (expectedNodes) {
1122
+ const expected = expectedNodes.map(({ nodeName, hostname }) => ({
1123
+ nodeName: nodeName.trim().toLowerCase(),
1124
+ hostname: hostname.trim().toLowerCase()
1125
+ })).sort((left, right) => left.nodeName.localeCompare(right.nodeName));
1126
+ const observed = nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname })).sort((left, right) => left.nodeName.localeCompare(right.nodeName));
1127
+ if (JSON.stringify(observed) !== JSON.stringify(expected)) {
1128
+ throw new Error("Cloudflare acceptance evidence does not cover the declared platform fleet");
1129
+ }
1130
+ }
1131
+ return {
1132
+ format: 1,
1133
+ kind: "forgezero-cloudflare-bootstrap-acceptance",
1134
+ checkpointFile: evidence.checkpointFile,
1135
+ verifiedAt: evidence.verifiedAt,
1136
+ nodes
1137
+ };
1138
+ }
1139
+ async function removeCloudflareBootstrapSecrets(checkpointPath, output) {
1140
+ const directory = `${resolve(checkpointPath)}.hosts`;
1141
+ let entries;
1142
+ try {
1143
+ const metadata = await lstat(directory);
1144
+ const uid = ownerUid();
1145
+ if (!metadata.isDirectory() || metadata.isSymbolicLink() || uid !== undefined && metadata.uid !== uid || (metadata.mode & 63) !== 0) {
1146
+ throw new Error(`Cloudflare host handoff directory ${directory} is not owner-only`);
1147
+ }
1148
+ entries = await readdir(directory);
1149
+ } catch (cause) {
1150
+ if (cause.code !== "ENOENT")
1151
+ throw cause;
1152
+ }
1153
+ if (entries) {
1154
+ const expected = output.coordinates.nodes.map(({ nodeName }) => `${nodeName}.json`).sort();
1155
+ if (JSON.stringify([...entries].sort()) !== JSON.stringify(expected)) {
1156
+ throw new Error("Cloudflare host handoff directory contains unexpected files; refusing secret cleanup");
1157
+ }
1158
+ for (const name of expected)
1159
+ await unlink(join(directory, name));
1160
+ await rmdir(directory);
1161
+ }
1162
+ await unlink(resolve(checkpointPath));
1163
+ }
1164
+ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1165
+ const checkpointPath = resolve(request.checkpointPath);
1166
+ const acceptancePath = resolve(request.acceptancePath);
1167
+ if (acceptancePath === checkpointPath || acceptancePath.startsWith(`${checkpointPath}.hosts/`)) {
1168
+ throw new Error("Cloudflare acceptance evidence must be outside the secret checkpoint and handoff directory");
1169
+ }
1170
+ const output = await readExistingOutput(checkpointPath);
1171
+ if (!output || output.phase !== "complete")
1172
+ throw new Error("Cloudflare finalization requires a completed owner checkpoint");
1173
+ const evidence = await verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher);
1174
+ await writeOwnerJson(acceptancePath, evidence);
1175
+ await removeCloudflareBootstrapSecrets(checkpointPath, output);
1176
+ return evidence;
1177
+ }
683
1178
 
684
1179
  // src/bootstrap.ts
685
- import { createHash, createHmac, randomBytes } from "crypto";
1180
+ import { createHash, createHmac, randomBytes as randomBytes3 } from "crypto";
686
1181
  import {
687
- chmodSync,
688
- existsSync,
689
- lstatSync,
690
- mkdirSync,
691
- readFileSync,
692
- renameSync,
693
- rmSync,
694
- writeFileSync
1182
+ chmodSync as chmodSync2,
1183
+ existsSync as existsSync3,
1184
+ lstatSync as lstatSync2,
1185
+ mkdirSync as mkdirSync3,
1186
+ readFileSync as readFileSync3,
1187
+ renameSync as renameSync3,
1188
+ rmSync as rmSync3,
1189
+ writeFileSync as writeFileSync3
695
1190
  } from "fs";
696
- import { dirname as dirname2 } from "path";
1191
+ import { dirname as dirname4 } from "path";
697
1192
  import { fileURLToPath } from "url";
698
1193
 
699
1194
  // src/agent-update-helper.ts
@@ -712,53 +1207,79 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
712
1207
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
713
1208
 
714
1209
  // src/version.ts
715
- var VERSION = "0.1.40";
1210
+ var VERSION = "0.1.42";
716
1211
 
717
1212
  // src/software.ts
718
- var BUN_INSTALLER_SHA256 = "bab8acfb046aac8c72407bdcce903957665d655d7acaa3e11c7c4616beae68dd";
719
- var ARANGO_SHA256 = "b5a9197b4343f2ed554e1ebc1ef8e6529c7c39cde0035cdc311a4747a3355066";
720
- var CLOUDFLARED_SHA256 = "9d71c677db00134c1bd4144b7783486b654ad281b1ea62b4972098d19f770f17";
721
- var UBUNTU_2604_X64 = [
722
- {
723
- requirement: { id: "bun", version: "1.3.14" },
724
- check: 'test "$(/usr/local/bin/bun --version 2>/dev/null)" = 1.3.14',
725
- 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`
726
- },
727
- {
728
- requirement: { id: "nginx", version: "ubuntu-26.04" },
729
- check: "command -v nginx >/dev/null && systemctl is-active --quiet nginx",
730
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y nginx && systemctl enable --now nginx"
731
- },
732
- {
733
- requirement: { id: "arangodb", version: "3.11.14" },
734
- check: `arangod --version 2>/dev/null | head -1 | grep -q '3.11.14' && ` + `! systemctl is-active --quiet arangodb3.service && ` + `! systemctl is-enabled --quiet arangodb3.service`,
735
- 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; ` + `systemctl disable --now arangodb3.service`
1213
+ var PINNED_BUN_VERSION = "1.3.14";
1214
+ var BUN_RELEASE_SHA256 = "951ee2aee855f08595aeec6225226a298d3fea83a3dcd6465c09cbccdf7e848f";
1215
+
1216
+ // src/service-supervisor.ts
1217
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "fs";
1218
+ import { dirname as dirname2, join as join2, resolve as resolve2, sep } from "path";
1219
+
1220
+ // src/definition.ts
1221
+ var RESERVED_STEP_ENV = new Set([
1222
+ "PATH",
1223
+ "HOME",
1224
+ "SHELL",
1225
+ "PWD",
1226
+ "BUN_INSTALL",
1227
+ "NODE_OPTIONS",
1228
+ "LD_PRELOAD",
1229
+ "LD_LIBRARY_PATH",
1230
+ "GIT_SSH",
1231
+ "GIT_SSH_COMMAND"
1232
+ ]);
1233
+
1234
+ // src/service-supervisor.ts
1235
+ var defaultHost = {
1236
+ write(path, content, mode) {
1237
+ mkdirSync(dirname2(path), { recursive: true, mode: 493 });
1238
+ const next = `${path}.next`;
1239
+ writeFileSync(next, content, { mode });
1240
+ renameSync(next, path);
736
1241
  },
737
- {
738
- requirement: { id: "cloudflared", version: "2026.7.3" },
739
- check: `cloudflared --version 2>/dev/null | grep -q '2026.7.3'`,
740
- 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`
1242
+ read: (path) => readFileSync(path, "utf8"),
1243
+ exists: existsSync,
1244
+ list: (path) => existsSync(path) ? readdirSync(path) : [],
1245
+ realpath: realpathSync,
1246
+ mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
1247
+ remove: (path) => rmSync(path, { force: true }),
1248
+ async exec(argv) {
1249
+ const child = Bun.spawn([...argv], { stdout: "pipe", stderr: "pipe", env: {
1250
+ PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
1251
+ LANG: "C",
1252
+ LC_ALL: "C"
1253
+ } });
1254
+ const [stdout, stderr, exitCode] = await Promise.all([
1255
+ new Response(child.stdout).text(),
1256
+ new Response(child.stderr).text(),
1257
+ child.exited
1258
+ ]);
1259
+ return { exitCode, output: `${stdout}${stderr}` };
741
1260
  },
742
- {
743
- requirement: { id: "ufw", version: "ubuntu-26.04" },
744
- check: "command -v ufw >/dev/null",
745
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y ufw"
1261
+ async health(port, path) {
1262
+ try {
1263
+ const response = await fetch(`http://127.0.0.1:${port}${path}`, {
1264
+ signal: AbortSignal.timeout(2000),
1265
+ redirect: "manual"
1266
+ });
1267
+ return response.status >= 200 && response.status < 300;
1268
+ } catch {
1269
+ return false;
1270
+ }
746
1271
  },
747
- {
748
- requirement: { id: "openssh-client", version: "ubuntu-26.04" },
749
- check: "command -v ssh >/dev/null && command -v scp >/dev/null && command -v ssh-keyscan >/dev/null && command -v ssh-keygen >/dev/null",
750
- install: "DEBIAN_FRONTEND=noninteractive apt-get update -qq && apt-get install -y openssh-client"
751
- }
752
- ];
1272
+ sleep: (ms) => Bun.sleep(ms),
1273
+ now: Date.now
1274
+ };
753
1275
 
754
1276
  // src/software-helper.ts
755
1277
  var DEFAULT_SOFTWARE_HELPER_SOCKET = "/run/forgezero-software/helper.sock";
756
1278
  var SOFTWARE_HELPER_GROUP = "forgezero-software";
757
1279
  var SOFTWARE_HELPER_UNIT_PATH = "/etc/systemd/system/forgezero-software-helper.service";
758
- var MAX_REQUEST_BYTES2 = 8 * 1024;
1280
+ var MAX_REQUEST_BYTES2 = 128 * 1024;
759
1281
 
760
1282
  // src/egress-policy.ts
761
- var AGENT_EGRESS_TABLE = "forgezero_agent_egress";
762
1283
  var SYSTEMD_RESOLVED_ADDRESS = "127.0.0.53";
763
1284
  var BLOCKED_IPV4 = [
764
1285
  "0.0.0.0/8",
@@ -818,7 +1339,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
818
1339
  }
819
1340
 
820
1341
  // src/provision.ts
821
- import { isIP as isIP2 } from "net";
1342
+ import { isIP as isIP3 } from "net";
822
1343
  function atLeast(version, floor) {
823
1344
  const parse = (value) => (value.trim().replace(/^v/, "").match(/\d+/g) ?? []).slice(0, 3).map(Number);
824
1345
  const got = parse(version);
@@ -837,22 +1358,26 @@ function atLeast(version, floor) {
837
1358
  }
838
1359
  var CAPABILITY_CHECKS = {
839
1360
  snpGuest: {
840
- command: "test -e /dev/sev-guest && echo yes || echo no",
1361
+ command: "fz host check-device /dev/sev-guest",
1362
+ operation: { kind: "path-exists", path: "/dev/sev-guest", nodeType: "file" },
841
1363
  satisfied: (stdout) => stdout.trim() === "yes",
842
1364
  remedy: "Not a confidential guest. The agent will run in `enrolled` mode, which is still stronger than an API key in the application."
843
1365
  },
844
1366
  systemd: {
845
- command: "test -d /run/systemd/system && echo yes || echo no",
1367
+ command: "fz host check-directory /run/systemd/system",
1368
+ operation: { kind: "path-exists", path: "/run/systemd/system", nodeType: "directory" },
846
1369
  satisfied: (stdout) => stdout.trim() === "yes",
847
1370
  remedy: "systemd is what supervises the agent. On a non-systemd host, run `fz-agent` under whatever supervises services there."
848
1371
  },
849
1372
  bun: {
850
- command: "bun --version 2>/dev/null || echo missing",
1373
+ command: "fz host check-version bun",
1374
+ operation: { kind: "version", argv: ["/usr/local/bin/bun", "--version"] },
851
1375
  satisfied: (stdout) => atLeast(stdout, "1.1.0"),
852
- remedy: "Install bun: curl -fsSL https://bun.sh/install | bash"
1376
+ remedy: "Install the pinned Bun release with `fz bootstrap`."
853
1377
  },
854
1378
  python: {
855
- command: "python3 --version 2>/dev/null || echo missing",
1379
+ command: "fz host check-version python3",
1380
+ operation: { kind: "version", argv: ["/usr/bin/python3", "--version"] },
856
1381
  satisfied: (stdout, exitCode) => exitCode === 0 && /^Python 3\./.test(stdout.trim()),
857
1382
  remedy: "Install Python 3. It supplies the standard-library ioctl boundary for SNP reports."
858
1383
  }
@@ -860,6 +1385,7 @@ var CAPABILITY_CHECKS = {
860
1385
  var modeFor = (capabilities) => capabilities.snpGuest ? "attested" : "enrolled";
861
1386
  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.";
862
1387
  var DEPLOYMENT_RUNNER_USER = "forgezero-runner";
1388
+ var APPLICATION_RUNTIME_USER = "forgezero-app";
863
1389
  var DEPLOYMENT_GROUP = "forgezero-deploy";
864
1390
  var VAULT_GROUP = "forgezero-vault";
865
1391
  var LIFECYCLE_GROUP = "forgezero-lifecycle";
@@ -889,11 +1415,7 @@ function agentEgressUnit(options) {
889
1415
  systemdAgentEgressDirectives(runnerLoopbackPorts);
890
1416
  const users = [user, ...deploymentEnabled ? [DEPLOYMENT_RUNNER_USER] : []];
891
1417
  const runnerGrant = deploymentEnabled ? ` --loopback-user=${DEPLOYMENT_RUNNER_USER}` + runnerLoopbackPorts.map((port) => ` --loopback-tcp-port=${port}`).join("") + runnerPublicTcpPorts.map((port) => ` --public-tcp-port=${port}`).join("") : "";
892
- const policyProofs = deploymentEnabled ? [
893
- ...runnerLoopbackPorts.length > 0 ? [`loopback=.*:${runnerLoopbackPorts.join(",")}`] : [],
894
- `public-tcp=${runnerPublicTcpPorts.join(",")}`
895
- ].map((pattern) => `ExecStartPost=/bin/sh -c '/usr/sbin/nft --numeric list table inet ${AGENT_EGRESS_TABLE} | /usr/bin/grep -q "${pattern}"'`).join(`
896
- `) : "";
1418
+ const policyProof = `ExecStartPost=${bin} egress-policy-check ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}`;
897
1419
  return `[Unit]
898
1420
  Description=ForgeZero Agent host egress policy
899
1421
  Documentation=https://www.forgezero.net/docs/agent
@@ -907,7 +1429,7 @@ NotifyAccess=all
907
1429
  User=root
908
1430
  Group=root
909
1431
  ExecStart=${bin} egress-policy ${users.map((name) => `--user=${name}`).join(" ")}${runnerGrant}
910
- ${policyProofs}
1432
+ ${policyProof}
911
1433
  Restart=on-failure
912
1434
  RestartSec=2
913
1435
  LimitCORE=0
@@ -931,6 +1453,7 @@ WantedBy=multi-user.target
931
1453
  }
932
1454
  function softwareHelperUnit(options) {
933
1455
  const bin = options.binPath ?? "fz-agent";
1456
+ const root = options.deployRoot ?? "/opt/forgezero";
934
1457
  return `[Unit]
935
1458
  Description=ForgeZero declarative software strategy helper
936
1459
  Documentation=https://www.forgezero.net/docs/agent
@@ -942,6 +1465,7 @@ Type=simple
942
1465
  User=root
943
1466
  Group=${SOFTWARE_HELPER_GROUP}
944
1467
  Environment=FZ_SOFTWARE_HELPER_SOCKET=${DEFAULT_SOFTWARE_HELPER_SOCKET}
1468
+ Environment=FZ_DEPLOY_ROOT=${root}
945
1469
  ExecStart=${bin} software-helper
946
1470
  Restart=always
947
1471
  RestartSec=2
@@ -1057,10 +1581,6 @@ var systemdPath = (value, label) => {
1057
1581
  throw new Error(`invalid ${label} path`);
1058
1582
  return value;
1059
1583
  };
1060
- var awaitSocketCommand = (path) => {
1061
- const socket = systemdPath(path, "readiness socket");
1062
- return `for attempt in $(seq 1 100); do test -S ${socket} && exit 0; sleep 0.1; done; exit 1`;
1063
- };
1064
1584
  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));
1065
1585
  function warpConfigUnit(options) {
1066
1586
  if (!options.warpOrganization || !/^[a-z0-9][a-z0-9-]{0,62}$/i.test(options.warpOrganization)) {
@@ -1161,6 +1681,8 @@ function agentEnrolmentUnit(options) {
1161
1681
  const hostname = options.nodeHostname ? `Environment=FZ_NODE_HOSTNAME=${options.nodeHostname}
1162
1682
  ` : "";
1163
1683
  const gitPublicKey = options.gitPublicKeyPath ? `Environment=FZ_GIT_PUBLIC_KEY_FILE=${options.gitPublicKeyPath}
1684
+ ` : "";
1685
+ const bootstrapSshPublicKey = options.bootstrapSshPublicKeyPath ? `Environment=FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE=${options.bootstrapSshPublicKeyPath}
1164
1686
  ` : "";
1165
1687
  const networkAttachment = [
1166
1688
  options.cloudflareAccountId ? `Environment=FZ_CF_ACCOUNT_ID=${options.cloudflareAccountId}
@@ -1195,7 +1717,7 @@ Environment=FZ_SEED_CREDENTIAL=agent-seed
1195
1717
  Environment=FZ_ENROL_TOKEN_CREDENTIAL=enrol-token
1196
1718
  Environment=FZ_ENROL_STATE_FILE=${options.enrolStatePath}
1197
1719
  Environment=FZ_API=${options.apiUrl}
1198
- ${label}${hostname}${gitPublicKey}${networkAttachment}ExecStart=${bin} enrol
1720
+ ${label}${hostname}${gitPublicKey}${bootstrapSshPublicKey}${networkAttachment}ExecStart=${bin} enrol
1199
1721
  # A '+' fixed command runs as root solely to remove the host-bound one-time
1200
1722
  # ciphertext. Tenant code and the agent never receive a privilege boundary.
1201
1723
  ExecStartPost=+/usr/bin/rm -f ${options.enrolTokenCredentialPath}
@@ -1275,7 +1797,7 @@ function agentUnit(options) {
1275
1797
  } catch {
1276
1798
  throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL");
1277
1799
  }
1278
- if (endpoint2.protocol !== "https:" || endpoint2.username || endpoint2.password || endpoint2.search || endpoint2.hash || isIP2(endpoint2.hostname) !== 0 || !endpoint2.hostname.includes(".") || endpoint2.hostname === "localhost" || endpoint2.hostname.endsWith(".local"))
1800
+ 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"))
1279
1801
  throw new Error("compute telemetry endpoint must be an absolute public HTTPS URL without credentials, query or fragment");
1280
1802
  telemetryEndpoint = endpoint2.toString().replace(/\/$/, "");
1281
1803
  }
@@ -1289,9 +1811,14 @@ function agentUnit(options) {
1289
1811
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1290
1812
  throw new Error("migration pull and lifecycle profile must be supplied together");
1291
1813
  }
1292
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1293
- if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1294
- throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
1814
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
1815
+ if ([
1816
+ options.pullBootstrap,
1817
+ options.bootstrapSshCredentialPath,
1818
+ options.bootstrapSshPublicKeyPath,
1819
+ options.bootstrapTargetTelemetryEndpoint
1820
+ ].some(Boolean) && !bootstrapEnabled) {
1821
+ throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
1295
1822
  }
1296
1823
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1297
1824
  const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
@@ -1362,6 +1889,7 @@ function agentUnit(options) {
1362
1889
  options.pullMigrations ? "FZ_MIGRATION_PULL=true" : null,
1363
1890
  options.pullBootstrap ? "FZ_BOOTSTRAP_PULL=true" : null,
1364
1891
  options.pullBootstrap ? "FZ_BOOTSTRAP_SSH_KEY_CREDENTIAL=bootstrap-ssh-key" : null,
1892
+ options.pullBootstrap && options.bootstrapSshPublicKeyPath ? `FZ_BOOTSTRAP_SSH_PUBLIC_KEY_FILE=${options.bootstrapSshPublicKeyPath}` : null,
1365
1893
  options.pullBootstrap ? `FZ_BOOTSTRAP_TARGET_OTLP_ENDPOINT=${options.bootstrapTargetTelemetryEndpoint}` : null,
1366
1894
  `FZ_AGENT_UPDATE_SOCKET=${DEFAULT_AGENT_UPDATE_SOCKET}`,
1367
1895
  options.pullMigrations ? `FZ_LIFECYCLE_HELPER_SOCKET=${lifecycleHelperSocketPath}` : null
@@ -1467,6 +1995,55 @@ ${deploymentWrites}
1467
1995
  WantedBy=multi-user.target
1468
1996
  `;
1469
1997
  }
1998
+ var renderOperation = (operation) => {
1999
+ if (operation.kind === "commands")
2000
+ return operation.commands.map(({ argv }) => argv.join(" ")).join(`
2001
+ `);
2002
+ if (operation.kind === "directories")
2003
+ return operation.directories.map((directory) => [
2004
+ "/usr/bin/install",
2005
+ "-d",
2006
+ ...directory.owner ? ["-o", directory.owner] : [],
2007
+ ...directory.group ? ["-g", directory.group] : [],
2008
+ "-m",
2009
+ directory.mode.toString(8).padStart(4, "0"),
2010
+ directory.path
2011
+ ].join(" ")).join(`
2012
+ `);
2013
+ if (operation.kind === "install-runtime")
2014
+ return `/usr/bin/install -m 0755 ${operation.source} /opt/forgezero/agent/versions/${operation.version}/dist/fz-agent.js`;
2015
+ if (operation.kind === "ensure-seed")
2016
+ return `/usr/bin/systemd-creds encrypt --name=agent-seed - ${operation.credential}`;
2017
+ if (operation.kind === "ensure-git-identity")
2018
+ return `/usr/bin/ssh-keygen -t ed25519
2019
+ /usr/bin/systemd-creds encrypt --name=git-deploy-key <private> ${operation.credential}
2020
+ fz host write-public-key ${operation.publicKey}`;
2021
+ if (operation.kind === "ensure-bootstrap-ssh-identity")
2022
+ return `/usr/bin/ssh-keygen -t ed25519
2023
+ /usr/bin/systemd-creds encrypt --name=bootstrap-ssh-key <private> ${operation.credential}
2024
+ fz host write-public-key ${operation.publicKey}`;
2025
+ if (operation.kind === "ensure-enrolment")
2026
+ return `/usr/bin/systemd-creds encrypt --name=enrol-token ${operation.source} ${operation.credential}
2027
+ /usr/bin/rm -f ${operation.source}`;
2028
+ if (operation.kind === "wait-socket")
2029
+ return `fz host wait-socket ${operation.path}`;
2030
+ if (operation.kind === "verify-file")
2031
+ return `fz host verify-file ${operation.path}`;
2032
+ if (operation.kind === "verify-egress")
2033
+ return `/usr/sbin/nft --numeric list table inet forgezero_agent_egress
2034
+ fz-agent egress-policy-check`;
2035
+ if (operation.kind === "verify-resolved-stub")
2036
+ return "fz host verify-resolved-stub /run/systemd/resolve/stub-resolv.conf";
2037
+ if (operation.kind === "install-warp")
2038
+ return "/usr/bin/apt-get install -y cloudflare-warp";
2039
+ return "/usr/bin/warp-cli --accept-tos status";
2040
+ };
2041
+ var step = (label, operation, optional = false) => ({
2042
+ label,
2043
+ operation,
2044
+ optional: optional || undefined,
2045
+ command: renderOperation(operation)
2046
+ });
1470
2047
  var UNIT_PATH = "/etc/systemd/system/forgezero-agent.service";
1471
2048
  function planProvision(options) {
1472
2049
  const mode = options.mode;
@@ -1481,9 +2058,15 @@ function planProvision(options) {
1481
2058
  if (Boolean(options.pullMigrations) !== Boolean(options.lifecycleProfilePath)) {
1482
2059
  throw new Error("migration pull and lifecycle profile must be supplied together");
1483
2060
  }
1484
- const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapTargetTelemetryEndpoint);
1485
- if ([options.pullBootstrap, options.bootstrapSshCredentialPath, options.bootstrapTargetTelemetryEndpoint].some(Boolean) && !bootstrapEnabled) {
1486
- throw new Error("bootstrap pull, SSH credential and target telemetry endpoint must be supplied together");
2061
+ const bootstrapEnabled = Boolean(options.pullBootstrap && options.bootstrapSshCredentialPath && options.bootstrapSshPublicKeyPath && options.bootstrapTargetTelemetryEndpoint);
2062
+ if ([
2063
+ options.pullBootstrap,
2064
+ options.bootstrapSshCredentialPath,
2065
+ options.bootstrapSshPublicKeyPath,
2066
+ options.bootstrapSshSourcePath,
2067
+ options.bootstrapTargetTelemetryEndpoint
2068
+ ].some(Boolean) && !bootstrapEnabled) {
2069
+ throw new Error("bootstrap pull, SSH credential, public key and target telemetry endpoint must be supplied together");
1487
2070
  }
1488
2071
  const warpValues = [
1489
2072
  options.warpOrganization,
@@ -1511,8 +2094,29 @@ function planProvision(options) {
1511
2094
  const lifecycleProfilePath = lifecycleEnabled ? systemdPath(options.lifecycleProfilePath, "lifecycle profile") : undefined;
1512
2095
  const lifecycleHelperSocketPath = lifecycleEnabled ? systemdPath(options.lifecycleHelperSocketPath ?? LIFECYCLE_HELPER_SOCKET, "lifecycle helper socket") : undefined;
1513
2096
  const bootstrapSshCredentialPath = bootstrapEnabled ? systemdPath(options.bootstrapSshCredentialPath, "bootstrap SSH credential") : undefined;
2097
+ const bootstrapSshPublicKeyPath = bootstrapEnabled ? systemdPath(options.bootstrapSshPublicKeyPath, "bootstrap SSH public key") : undefined;
2098
+ const bootstrapSshSourcePath = options.bootstrapSshSourcePath ? systemdPath(options.bootstrapSshSourcePath, "bootstrap SSH private-key source") : undefined;
2099
+ const bootstrapSshPublicKeyDir = bootstrapSshPublicKeyPath?.replace(/\/[^/]+$/, "");
1514
2100
  const warpClientIdCredentialPath = warpEnabled ? systemdPath(options.warpClientIdCredentialPath, "WARP client-id credential") : undefined;
1515
2101
  const warpClientSecretCredentialPath = warpEnabled ? systemdPath(options.warpClientSecretCredentialPath, "WARP client-secret credential") : undefined;
2102
+ const enabledUnits = [
2103
+ "forgezero-agent.socket",
2104
+ "forgezero-agent-update-helper.service",
2105
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2106
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2107
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2108
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2109
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
2110
+ "forgezero-agent.service"
2111
+ ];
2112
+ const restartedUnits = [
2113
+ "forgezero-agent-update-helper.service",
2114
+ ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
2115
+ ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
2116
+ ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
2117
+ ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
2118
+ ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
2119
+ ];
1516
2120
  return {
1517
2121
  mode,
1518
2122
  reason: reasonFor(mode),
@@ -1557,157 +2161,117 @@ function planProvision(options) {
1557
2161
  socketPath: options.socketPath,
1558
2162
  user,
1559
2163
  steps: [
1560
- ...options.enforceEgress ? [{
1561
- label: "Ubuntu Agent egress prerequisites",
1562
- 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`
1563
- }] : [],
1564
- {
1565
- label: "vault socket access group",
1566
- command: `groupadd --system ${VAULT_GROUP} || true`
1567
- },
1568
- {
1569
- label: "Agent update helper access group",
1570
- command: `groupadd --system ${AGENT_UPDATE_GROUP} || true`
1571
- },
1572
- ...sourceBinPath && binPath ? [{
1573
- label: "root-owned agent runtime",
1574
- 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}`
1575
- }] : [],
1576
- ...warpEnabled ? [{
1577
- label: "Cloudflare One client for Ubuntu 26.04",
1578
- 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`
1579
- }] : [],
1580
- ...deploymentEnabled ? [{
1581
- label: "deployment isolation group",
1582
- command: `groupadd --system ${DEPLOYMENT_GROUP} || true; groupadd --system ${SOFTWARE_HELPER_GROUP} || true`
1583
- }] : [],
1584
- ...lifecycleEnabled ? [{
1585
- label: "lifecycle helper access group",
1586
- command: `groupadd --system ${LIFECYCLE_GROUP} || true`
1587
- }] : [],
1588
- {
1589
- label: "service account",
1590
- command: `useradd --system --no-create-home --shell /usr/sbin/nologin ${user} || true`
1591
- },
1592
- {
1593
- label: "bind service account to vault group",
1594
- command: `usermod -g ${VAULT_GROUP} ${user}`
1595
- },
1596
- {
1597
- label: "grant verified Agent update access",
1598
- command: `usermod -a -G ${AGENT_UPDATE_GROUP} ${user}`
1599
- },
1600
- ...lifecycleEnabled ? [{
1601
- label: "grant lifecycle helper socket access",
1602
- command: `usermod -a -G ${LIFECYCLE_GROUP} ${user}`
1603
- }] : [],
1604
- ...deploymentEnabled ? [{
1605
- label: "credential-free deployment account",
1606
- 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}`
1607
- }] : [],
1608
- {
1609
- label: "credential directory",
1610
- command: `install -d -o root -g root -m 0700 ${credentialDir}`
1611
- },
1612
- {
1613
- label: "Agent state directory",
1614
- command: "install -d -o root -g root -m 0750 /var/lib/forgezero"
1615
- },
1616
- {
1617
- label: "encrypted node identity",
1618
- command: `test -s ${seedCredentialPath} || { ` + `openssl rand -base64 32 | tr '+/' '-_' | tr -d '=\\n' | ` + `systemd-creds encrypt --name=agent-seed - ${seedCredentialPath}; ` + `chmod 0400 ${seedCredentialPath}; }`
1619
- },
2164
+ ...options.enforceEgress ? [step("Ubuntu Agent egress prerequisites", { kind: "commands", commands: [
2165
+ { argv: ["/usr/bin/apt-get", "update", "-qq"] },
2166
+ { argv: ["/usr/bin/apt-get", "install", "-y", "nftables"] },
2167
+ { argv: ["/usr/bin/systemctl", "enable", "--now", "systemd-resolved.service"] }
2168
+ ] }), step("prove systemd-resolved stub ownership", { kind: "verify-resolved-stub" })] : [],
2169
+ step("vault socket access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", VAULT_GROUP], acceptedExitCodes: [0, 9] }] }),
2170
+ step("Agent update helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", AGENT_UPDATE_GROUP], acceptedExitCodes: [0, 9] }] }),
2171
+ ...sourceBinPath && binPath ? [step("root-owned agent runtime", { kind: "install-runtime", source: sourceBinPath, binary: binPath, version: VERSION })] : [],
2172
+ ...warpEnabled ? [step("Cloudflare One client for Ubuntu 26.04", { kind: "install-warp" })] : [],
2173
+ ...deploymentEnabled ? [step("deployment isolation group", { kind: "commands", commands: [
2174
+ { argv: ["/usr/sbin/groupadd", "--system", DEPLOYMENT_GROUP], acceptedExitCodes: [0, 9] },
2175
+ { argv: ["/usr/sbin/groupadd", "--system", SOFTWARE_HELPER_GROUP], acceptedExitCodes: [0, 9] }
2176
+ ] })] : [],
2177
+ ...lifecycleEnabled ? [step("lifecycle helper access group", { kind: "commands", commands: [{ argv: ["/usr/sbin/groupadd", "--system", LIFECYCLE_GROUP], acceptedExitCodes: [0, 9] }] })] : [],
2178
+ step("service account", { kind: "commands", commands: [{ argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", user], acceptedExitCodes: [0, 9] }] }),
2179
+ step("bind service account to vault group", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-g", VAULT_GROUP, user] }] }),
2180
+ step("grant verified Agent update access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", AGENT_UPDATE_GROUP, user] }] }),
2181
+ ...lifecycleEnabled ? [step("grant lifecycle helper socket access", { kind: "commands", commands: [{ argv: ["/usr/sbin/usermod", "-a", "-G", LIFECYCLE_GROUP, user] }] })] : [],
2182
+ ...deploymentEnabled ? [step("credential-free deployment account", { kind: "commands", commands: [
2183
+ { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", DEPLOYMENT_GROUP, DEPLOYMENT_RUNNER_USER], acceptedExitCodes: [0, 9] },
2184
+ { argv: ["/usr/sbin/useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", "--gid", VAULT_GROUP, APPLICATION_RUNTIME_USER], acceptedExitCodes: [0, 9] },
2185
+ { argv: ["/usr/sbin/usermod", "-a", "-G", DEPLOYMENT_GROUP, APPLICATION_RUNTIME_USER] },
2186
+ { argv: ["/usr/sbin/usermod", "-a", "-G", `${DEPLOYMENT_GROUP},${SOFTWARE_HELPER_GROUP}`, user] }
2187
+ ] })] : [],
2188
+ step("credential and state directories", { kind: "directories", directories: [
2189
+ { path: credentialDir, mode: 448, owner: "root", group: "root" },
2190
+ { path: "/var/lib/forgezero", mode: 488, owner: "root", group: "root" }
2191
+ ] }),
2192
+ step("encrypted node identity", { kind: "ensure-seed", credential: seedCredentialPath }),
1620
2193
  ...options.generateGitIdentity && gitCredentialPath && gitPublicKeyPath ? [
1621
- {
1622
- label: "Git deploy identity directory",
1623
- command: `install -d -o root -g root -m 0755 ${gitPublicKeyDir}`
1624
- },
1625
- {
1626
- label: "unique encrypted Git deploy identity",
1627
- 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}`
1628
- }
2194
+ step("Git deploy identity directory", { kind: "directories", directories: [{ path: gitPublicKeyDir, mode: 493, owner: "root", group: "root" }] }),
2195
+ step("unique encrypted Git deploy identity", { kind: "ensure-git-identity", credential: gitCredentialPath, publicKey: gitPublicKeyPath })
2196
+ ] : [],
2197
+ ...bootstrapEnabled ? [
2198
+ step("bootstrap SSH public identity directory", { kind: "directories", directories: [
2199
+ { path: bootstrapSshPublicKeyDir, mode: 493, owner: "root", group: "root" }
2200
+ ] }),
2201
+ step("unique encrypted bootstrap SSH identity", {
2202
+ kind: "ensure-bootstrap-ssh-identity",
2203
+ credential: bootstrapSshCredentialPath,
2204
+ publicKey: bootstrapSshPublicKeyPath,
2205
+ ...bootstrapSshSourcePath ? { source: bootstrapSshSourcePath } : {}
2206
+ })
1629
2207
  ] : [],
1630
2208
  ...enrolmentEnabled ? [
1631
- {
1632
- label: "enrolment state directory",
1633
- command: `install -d -o ${user} -g ${user} -m 0700 ${enrolStateDir}`
1634
- },
1635
- {
1636
- label: "encrypted one-time enrolment capability",
1637
- command: `test -s ${enrolStatePath} || test -s ${enrolTokenCredentialPath} || { test -r ${enrolTokenSourcePath}; ` + `systemd-creds encrypt --name=enrol-token ${enrolTokenSourcePath} ${enrolTokenCredentialPath}; ` + `chmod 0400 ${enrolTokenCredentialPath}; rm -f ${enrolTokenSourcePath}; }`
1638
- }
2209
+ step("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
2210
+ step("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
1639
2211
  ] : [],
1640
- ...deploymentEnabled ? [{
1641
- label: "deployment directories",
1642
- 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`
1643
- }] : [],
1644
- { label: "reload units", command: "systemctl daemon-reload" },
1645
- ...deploymentEnabled ? [{
1646
- label: "remove unsupported deployment socket activation",
1647
- command: "systemctl disable --now forgezero-deploy-runner.socket 2>/dev/null || true; rm -f /etc/systemd/system/forgezero-deploy-runner.socket; systemctl daemon-reload"
1648
- }] : [],
1649
- {
1650
- label: "enable and converge services",
1651
- command: `systemctl enable ${[
1652
- "forgezero-agent.socket",
1653
- "forgezero-agent-update-helper.service",
1654
- ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
1655
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
1656
- ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
1657
- ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
1658
- ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : [],
1659
- "forgezero-agent.service"
1660
- ].join(" ")}; systemctl reset-failed forgezero-agent.service || true; systemctl restart ${[
1661
- "forgezero-agent-update-helper.service",
1662
- ...options.enforceEgress ? ["forgezero-agent-egress.service"] : [],
1663
- ...deploymentEnabled ? ["forgezero-deploy-runner.service", "forgezero-software-helper.service"] : [],
1664
- ...lifecycleEnabled ? ["forgezero-lifecycle-helper.service"] : [],
1665
- ...warpEnabled ? ["forgezero-warp-config.service", "warp-svc.service"] : [],
1666
- ...enrolmentEnabled ? ["forgezero-agent-enrol.service"] : []
1667
- ].join(" ")}; systemctl restart forgezero-agent.socket; systemctl reset-failed forgezero-agent.service || true; systemctl restart forgezero-agent.service`
1668
- },
1669
- ...enrolmentEnabled ? [{
1670
- label: "prove the compute binding is durable",
1671
- command: `test -s ${enrolStatePath}`
1672
- }] : [],
1673
- ...options.enforceEgress ? [{
1674
- label: "prove the Agent egress policy is active",
1675
- 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(",")}( |")'` : "") : "")
1676
- }] : [],
1677
- { label: "prove it is running", command: "systemctl is-active forgezero-agent.service" },
1678
- { label: "prove the public Vault socket exists", command: awaitSocketCommand(options.socketPath) },
1679
- { label: "prove the Agent Vault backend exists", command: awaitSocketCommand(agentBackendSocketPath(options.socketPath)) },
1680
- { label: "prove the Agent update helper exists", command: awaitSocketCommand(DEFAULT_AGENT_UPDATE_SOCKET) },
1681
- ...deploymentEnabled ? [{
1682
- label: "prove the deployment runner socket exists",
1683
- command: awaitSocketCommand(DEPLOYMENT_RUNNER_SOCKET)
1684
- }, {
1685
- label: "prove the software strategy helper socket exists",
1686
- command: awaitSocketCommand(DEFAULT_SOFTWARE_HELPER_SOCKET)
1687
- }] : [],
1688
- ...lifecycleEnabled ? [{
1689
- label: "prove the lifecycle helper socket exists",
1690
- command: awaitSocketCommand(lifecycleHelperSocketPath)
1691
- }] : [],
1692
- ...warpEnabled ? [{
1693
- label: "prove Cloudflare WARP is connected",
1694
- command: `warp-cli --accept-tos status | grep -Eiq '(^|[[:space:]])Connected([[:space:]]|$)'`
1695
- }] : [],
1696
- ...options.repository ? [{
1697
- label: "prove the deployment control socket exists",
1698
- command: awaitSocketCommand(options.controlSocketPath ?? "/run/forgezero/control.sock")
1699
- }] : []
2212
+ ...deploymentEnabled ? [step("deployment directories", { kind: "directories", directories: [
2213
+ { path: deployRoot, mode: 493, owner: "root", group: "root" },
2214
+ { path: `${deployRoot}/releases`, mode: 2040, owner: "root", group: DEPLOYMENT_GROUP },
2215
+ { path: `${deployRoot}/cache`, mode: 488, owner: user, group: user },
2216
+ { path: `${deployRoot}/capacity`, mode: 448, owner: user, group: user },
2217
+ { path: `${deployRoot}/agent-home`, mode: 448, owner: user, group: user },
2218
+ { path: `${deployRoot}/runner-home`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
2219
+ { path: `${deployRoot}/runner-home/cache`, mode: 448, owner: DEPLOYMENT_RUNNER_USER, group: DEPLOYMENT_GROUP },
2220
+ { path: `${deployRoot}/app-home`, mode: 448, owner: APPLICATION_RUNTIME_USER, group: VAULT_GROUP }
2221
+ ] })] : [],
2222
+ step("reload units", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "daemon-reload"] }] }),
2223
+ ...deploymentEnabled ? [step("remove unsupported deployment socket activation", { kind: "commands", commands: [
2224
+ { argv: ["/usr/bin/systemctl", "disable", "--now", "forgezero-deploy-runner.socket"], acceptedExitCodes: [0, 1, 5] },
2225
+ { argv: ["/usr/bin/rm", "-f", "/etc/systemd/system/forgezero-deploy-runner.socket"] },
2226
+ { argv: ["/usr/bin/systemctl", "daemon-reload"] }
2227
+ ] })] : [],
2228
+ step("enable and converge services", { kind: "commands", commands: [
2229
+ { argv: ["/usr/bin/systemctl", "enable", ...enabledUnits] },
2230
+ { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2231
+ ...restartedUnits.length ? [{ argv: ["/usr/bin/systemctl", "restart", ...restartedUnits] }] : [],
2232
+ { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.socket"] },
2233
+ { argv: ["/usr/bin/systemctl", "reset-failed", "forgezero-agent.service"], acceptedExitCodes: [0, 1] },
2234
+ { argv: ["/usr/bin/systemctl", "restart", "forgezero-agent.service"] }
2235
+ ] }),
2236
+ ...enrolmentEnabled ? [step("prove the compute binding is durable", { kind: "verify-file", path: enrolStatePath })] : [],
2237
+ ...options.enforceEgress ? [step("prove the Agent egress policy is active", { kind: "verify-egress", runnerPublicTcpPorts, runnerLoopbackPorts })] : [],
2238
+ step("prove it is running", { kind: "commands", commands: [{ argv: ["/usr/bin/systemctl", "is-active", "forgezero-agent.service"] }] }),
2239
+ step("prove the public Vault socket exists", { kind: "wait-socket", path: options.socketPath, attempts: 100, intervalMs: 100 }),
2240
+ step("prove the Agent Vault backend exists", { kind: "wait-socket", path: agentBackendSocketPath(options.socketPath), attempts: 100, intervalMs: 100 }),
2241
+ step("prove the Agent update helper exists", { kind: "wait-socket", path: DEFAULT_AGENT_UPDATE_SOCKET, attempts: 100, intervalMs: 100 }),
2242
+ ...deploymentEnabled ? [
2243
+ step("prove the deployment runner socket exists", { kind: "wait-socket", path: DEPLOYMENT_RUNNER_SOCKET, attempts: 100, intervalMs: 100 }),
2244
+ step("prove the software strategy helper socket exists", { kind: "wait-socket", path: DEFAULT_SOFTWARE_HELPER_SOCKET, attempts: 100, intervalMs: 100 })
2245
+ ] : [],
2246
+ ...lifecycleEnabled ? [step("prove the lifecycle helper socket exists", { kind: "wait-socket", path: lifecycleHelperSocketPath, attempts: 100, intervalMs: 100 })] : [],
2247
+ ...warpEnabled ? [step("prove Cloudflare WARP is connected", { kind: "verify-warp" })] : [],
2248
+ ...options.repository ? [step("prove the deployment control socket exists", { kind: "wait-socket", path: options.controlSocketPath ?? "/run/forgezero/control.sock", attempts: 100, intervalMs: 100 })] : []
1700
2249
  ]
1701
2250
  };
1702
2251
  }
1703
2252
 
1704
2253
  // src/cli/agent-install.ts
2254
+ import { randomBytes as randomBytes2 } from "crypto";
2255
+ import {
2256
+ chmodSync,
2257
+ copyFileSync,
2258
+ existsSync as existsSync2,
2259
+ lstatSync,
2260
+ mkdirSync as mkdirSync2,
2261
+ readFileSync as readFileSync2,
2262
+ realpathSync as realpathSync2,
2263
+ renameSync as renameSync2,
2264
+ rmSync as rmSync2,
2265
+ symlinkSync,
2266
+ writeFileSync as writeFileSync2
2267
+ } from "fs";
2268
+ import { dirname as dirname3 } from "path";
1705
2269
  async function readCapabilities(run) {
1706
2270
  const answers = {};
1707
2271
  const checks = Object.entries(CAPABILITY_CHECKS);
1708
2272
  for (const [id, check] of checks) {
1709
2273
  try {
1710
- const result = await run(check.command);
2274
+ const result = await run(check.operation);
1711
2275
  answers[id] = check.satisfied(result.stdout, result.exitCode);
1712
2276
  } catch {
1713
2277
  answers[id] = false;
@@ -1715,10 +2279,253 @@ async function readCapabilities(run) {
1715
2279
  }
1716
2280
  return answers;
1717
2281
  }
1718
- async function localRunner(command) {
1719
- const proc = Bun.spawn(["sh", "-c", command], { stdout: "pipe", stderr: "pipe" });
1720
- const stdout = await new Response(proc.stdout).text();
1721
- return { stdout, exitCode: await proc.exited };
2282
+ var fixed = async (argv, stdin) => {
2283
+ const child = Bun.spawn([...argv], {
2284
+ stdin: stdin === undefined ? "ignore" : "pipe",
2285
+ stdout: "pipe",
2286
+ stderr: "pipe",
2287
+ env: { PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", LANG: "C", LC_ALL: "C", DEBIAN_FRONTEND: "noninteractive" }
2288
+ });
2289
+ if (stdin !== undefined && child.stdin && typeof child.stdin !== "number") {
2290
+ child.stdin.write(stdin);
2291
+ child.stdin.end();
2292
+ }
2293
+ const [stdout, stderr, exitCode] = await Promise.all([
2294
+ new Response(child.stdout).text(),
2295
+ new Response(child.stderr).text(),
2296
+ child.exited
2297
+ ]);
2298
+ return { stdout: `${stdout}${stderr}`, exitCode };
2299
+ };
2300
+ var runProvisionOperation = async (operation) => {
2301
+ if (operation.kind === "commands") {
2302
+ let output = "";
2303
+ for (const command of operation.commands) {
2304
+ const result = await fixed(command.argv);
2305
+ output += result.stdout;
2306
+ if (!(command.acceptedExitCodes ?? [0]).includes(result.exitCode))
2307
+ return { stdout: output, exitCode: result.exitCode };
2308
+ }
2309
+ return { stdout: output, exitCode: 0 };
2310
+ }
2311
+ if (operation.kind === "directories") {
2312
+ for (const directory of operation.directories) {
2313
+ const argv = ["/usr/bin/install", "-d", "-m", directory.mode.toString(8).padStart(4, "0")];
2314
+ if (directory.owner)
2315
+ argv.push("-o", directory.owner);
2316
+ if (directory.group)
2317
+ argv.push("-g", directory.group);
2318
+ argv.push(directory.path);
2319
+ const result = await fixed(argv);
2320
+ if (result.exitCode !== 0)
2321
+ return result;
2322
+ }
2323
+ return { stdout: "", exitCode: 0 };
2324
+ }
2325
+ if (operation.kind === "install-runtime") {
2326
+ const release = `/opt/forgezero/agent/versions/${operation.version}`;
2327
+ mkdirSync2(`${release}/dist`, { recursive: true, mode: 493 });
2328
+ mkdirSync2(dirname3(operation.binary), { recursive: true, mode: 493 });
2329
+ copyFileSync(operation.source, `${release}/dist/fz-agent.js`);
2330
+ chmodSync(`${release}/dist/fz-agent.js`, 493);
2331
+ const gitSshSource = `${dirname3(operation.source)}/fz-git-ssh.js`;
2332
+ if (!existsSync2(gitSshSource))
2333
+ return { stdout: "packaged fz-git-ssh.js is missing", exitCode: 1 };
2334
+ copyFileSync(gitSshSource, `${release}/dist/fz-git-ssh.js`);
2335
+ chmodSync(`${release}/dist/fz-git-ssh.js`, 493);
2336
+ const pending = "/opt/forgezero/agent/current.next";
2337
+ rmSync2(pending, { force: true });
2338
+ symlinkSync(`versions/${operation.version}`, pending);
2339
+ renameSync2(pending, "/opt/forgezero/agent/current");
2340
+ rmSync2(operation.binary, { force: true });
2341
+ symlinkSync("/opt/forgezero/agent/current/dist/fz-agent.js", operation.binary);
2342
+ const gitSshBinary = "/usr/local/lib/forgezero/agent/fz-git-ssh";
2343
+ rmSync2(gitSshBinary, { force: true });
2344
+ symlinkSync("/opt/forgezero/agent/current/dist/fz-git-ssh.js", gitSshBinary);
2345
+ return { stdout: "", exitCode: 0 };
2346
+ }
2347
+ if (operation.kind === "ensure-seed") {
2348
+ if (existsSync2(operation.credential) && lstatSync(operation.credential).size > 0)
2349
+ return { stdout: "", exitCode: 0 };
2350
+ const seed = randomBytes2(32).toString("base64url");
2351
+ const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=agent-seed", "-", operation.credential], seed);
2352
+ if (result.exitCode === 0)
2353
+ chmodSync(operation.credential, 256);
2354
+ return result;
2355
+ }
2356
+ if (operation.kind === "ensure-git-identity") {
2357
+ const key = "/run/forgezero-git-deploy-key";
2358
+ const publicKey = `${key}.pub`;
2359
+ try {
2360
+ if (!existsSync2(operation.credential) || lstatSync(operation.credential).size < 1) {
2361
+ rmSync2(key, { force: true });
2362
+ rmSync2(publicKey, { force: true });
2363
+ let result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-compute", "-f", key]);
2364
+ if (result.exitCode !== 0)
2365
+ return result;
2366
+ result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=git-deploy-key", key, operation.credential]);
2367
+ if (result.exitCode !== 0)
2368
+ return result;
2369
+ chmodSync(operation.credential, 256);
2370
+ }
2371
+ if (!existsSync2(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
2372
+ if (!existsSync2(key)) {
2373
+ const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=git-deploy-key", operation.credential, key]);
2374
+ if (decrypted.exitCode !== 0)
2375
+ return decrypted;
2376
+ }
2377
+ const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2378
+ if (derived.exitCode !== 0)
2379
+ return derived;
2380
+ writeFileSync2(operation.publicKey, `${derived.stdout.trim()} forgezero-compute
2381
+ `, { mode: 292 });
2382
+ }
2383
+ return { stdout: "", exitCode: 0 };
2384
+ } finally {
2385
+ rmSync2(key, { force: true });
2386
+ rmSync2(publicKey, { force: true });
2387
+ }
2388
+ }
2389
+ if (operation.kind === "ensure-bootstrap-ssh-identity") {
2390
+ const key = "/run/forgezero-bootstrap-ssh-key";
2391
+ const generatedPublicKey = `${key}.pub`;
2392
+ try {
2393
+ if (!existsSync2(operation.credential) || lstatSync(operation.credential).size < 1) {
2394
+ rmSync2(key, { force: true });
2395
+ rmSync2(generatedPublicKey, { force: true });
2396
+ let result;
2397
+ if (operation.source) {
2398
+ const source = existsSync2(operation.source) ? lstatSync(operation.source) : undefined;
2399
+ if (!source?.isFile() || source.isSymbolicLink() || source.uid !== 0 || source.nlink !== 1 || (source.mode & 63) !== 0 || source.size < 32 || source.size > 16 * 1024) {
2400
+ return { stdout: "bootstrap SSH private-key source is missing or unsafe", exitCode: 1 };
2401
+ }
2402
+ copyFileSync(operation.source, key);
2403
+ chmodSync(key, 384);
2404
+ } else {
2405
+ result = await fixed(["/usr/bin/ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-C", "forgezero-bootstrap-runner", "-f", key]);
2406
+ if (result.exitCode !== 0)
2407
+ return result;
2408
+ }
2409
+ result = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2410
+ if (result.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(result.stdout)) {
2411
+ return { stdout: "bootstrap SSH private key is not a valid Ed25519 OpenSSH key", exitCode: 1 };
2412
+ }
2413
+ result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=bootstrap-ssh-key", key, operation.credential]);
2414
+ if (result.exitCode !== 0)
2415
+ return result;
2416
+ chmodSync(operation.credential, 256);
2417
+ }
2418
+ if (!existsSync2(operation.publicKey) || lstatSync(operation.publicKey).size < 1) {
2419
+ if (!existsSync2(key)) {
2420
+ const decrypted = await fixed(["/usr/bin/systemd-creds", "decrypt", "--name=bootstrap-ssh-key", operation.credential, key]);
2421
+ if (decrypted.exitCode !== 0)
2422
+ return decrypted;
2423
+ chmodSync(key, 384);
2424
+ }
2425
+ const derived = await fixed(["/usr/bin/ssh-keygen", "-y", "-f", key]);
2426
+ if (derived.exitCode !== 0 || !/^ssh-ed25519 [A-Za-z0-9+/]+={0,3}\s*$/.test(derived.stdout)) {
2427
+ return { stdout: "bootstrap SSH public key derivation failed", exitCode: 1 };
2428
+ }
2429
+ mkdirSync2(dirname3(operation.publicKey), { recursive: true, mode: 493 });
2430
+ writeFileSync2(operation.publicKey, `${derived.stdout.trim()} forgezero-bootstrap-runner
2431
+ `, { mode: 292 });
2432
+ chmodSync(operation.publicKey, 292);
2433
+ }
2434
+ if (operation.source)
2435
+ rmSync2(operation.source, { force: true });
2436
+ return { stdout: "", exitCode: 0 };
2437
+ } finally {
2438
+ rmSync2(key, { force: true });
2439
+ rmSync2(generatedPublicKey, { force: true });
2440
+ }
2441
+ }
2442
+ if (operation.kind === "ensure-enrolment") {
2443
+ if (existsSync2(operation.state) && lstatSync(operation.state).size > 0 || existsSync2(operation.credential) && lstatSync(operation.credential).size > 0)
2444
+ return { stdout: "", exitCode: 0 };
2445
+ if (!existsSync2(operation.source))
2446
+ return { stdout: "enrolment source is missing", exitCode: 1 };
2447
+ const result = await fixed(["/usr/bin/systemd-creds", "encrypt", "--name=enrol-token", operation.source, operation.credential]);
2448
+ if (result.exitCode === 0) {
2449
+ chmodSync(operation.credential, 256);
2450
+ rmSync2(operation.source, { force: true });
2451
+ }
2452
+ return result;
2453
+ }
2454
+ if (operation.kind === "wait-socket") {
2455
+ for (let attempt = 0;attempt < operation.attempts; attempt += 1) {
2456
+ try {
2457
+ if (lstatSync(operation.path).isSocket())
2458
+ return { stdout: "", exitCode: 0 };
2459
+ } catch {}
2460
+ await Bun.sleep(operation.intervalMs);
2461
+ }
2462
+ return { stdout: `socket did not become ready: ${operation.path}`, exitCode: 1 };
2463
+ }
2464
+ if (operation.kind === "verify-file")
2465
+ return existsSync2(operation.path) && lstatSync(operation.path).size > 0 ? { stdout: "", exitCode: 0 } : { stdout: "required file is empty", exitCode: 1 };
2466
+ if (operation.kind === "verify-egress") {
2467
+ const active = await fixed(["/usr/bin/systemctl", "is-active", "forgezero-agent-egress.service"]);
2468
+ if (active.exitCode !== 0)
2469
+ return active;
2470
+ const policy = await fixed(["/usr/sbin/nft", "--numeric", "list", "table", "inet", "forgezero_agent_egress"]);
2471
+ const required = [
2472
+ "forgezero-agent-egress-v1",
2473
+ `public-tcp=${operation.runnerPublicTcpPorts.join(",")}`,
2474
+ ...operation.runnerLoopbackPorts.length ? [`:${operation.runnerLoopbackPorts.join(",")}`] : []
2475
+ ];
2476
+ return policy.exitCode === 0 && required.every((part) => policy.stdout.includes(part)) ? { stdout: policy.stdout, exitCode: 0 } : { stdout: policy.stdout, exitCode: 1 };
2477
+ }
2478
+ if (operation.kind === "verify-resolved-stub") {
2479
+ try {
2480
+ const expected = "/run/systemd/resolve/stub-resolv.conf";
2481
+ return realpathSync2("/etc/resolv.conf") === expected && realpathSync2(expected) === expected ? { stdout: expected, exitCode: 0 } : { stdout: "resolver stub mismatch", exitCode: 1 };
2482
+ } catch {
2483
+ return { stdout: "resolver stub missing", exitCode: 1 };
2484
+ }
2485
+ }
2486
+ if (operation.kind === "install-warp") {
2487
+ const os = readFileSync2("/etc/os-release", "utf8");
2488
+ if (!/^ID=ubuntu$/m.test(os) || !/^VERSION_ID="?26\.04"?$/m.test(os))
2489
+ return { stdout: "unsupported WARP host OS", exitCode: 1 };
2490
+ const response = await fetch("https://pkg.cloudflareclient.com/pubkey.gpg", { signal: AbortSignal.timeout(30000) });
2491
+ if (!response.ok)
2492
+ return { stdout: `WARP key HTTP ${response.status}`, exitCode: 1 };
2493
+ mkdirSync2("/usr/share/keyrings", { recursive: true, mode: 493 });
2494
+ mkdirSync2("/etc/apt/sources.list.d", { recursive: true, mode: 493 });
2495
+ mkdirSync2("/etc/systemd/system/warp-svc.service.d", { recursive: true, mode: 493 });
2496
+ const key = "/run/cloudflare-warp-key.gpg";
2497
+ writeFileSync2(key, new Uint8Array(await response.arrayBuffer()), { mode: 384 });
2498
+ let result = await fixed(["/usr/bin/gpg", "--batch", "--yes", "--dearmor", "-o", "/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg", key]);
2499
+ rmSync2(key, { force: true });
2500
+ if (result.exitCode !== 0)
2501
+ return result;
2502
+ const codename = os.match(/^VERSION_CODENAME=(.+)$/m)?.[1]?.replace(/^"|"$/g, "");
2503
+ if (!codename)
2504
+ return { stdout: "Ubuntu codename missing", exitCode: 1 };
2505
+ writeFileSync2("/etc/apt/sources.list.d/cloudflare-client.list", `deb [signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ ${codename} main
2506
+ `, { mode: 420 });
2507
+ result = await fixed(["/usr/bin/apt-get", "update", "-qq"]);
2508
+ return result.exitCode === 0 ? fixed(["/usr/bin/apt-get", "install", "-y", "cloudflare-warp"]) : result;
2509
+ }
2510
+ const status = await fixed(["/usr/bin/warp-cli", "--accept-tos", "status"]);
2511
+ return status.exitCode === 0 && /(^|\s)Connected(\s|$)/i.test(status.stdout) ? status : { ...status, exitCode: 1 };
2512
+ };
2513
+ async function localRunner(operation) {
2514
+ if (!["path-exists", "version"].includes(operation.kind))
2515
+ return runProvisionOperation(operation);
2516
+ const capability = operation;
2517
+ if (capability.kind === "version")
2518
+ return fixed(capability.argv);
2519
+ try {
2520
+ const metadata = lstatSync(capability.path);
2521
+ const present = capability.nodeType === "directory" ? metadata.isDirectory() : true;
2522
+ return { stdout: present ? `yes
2523
+ ` : `no
2524
+ `, exitCode: present ? 0 : 1 };
2525
+ } catch {
2526
+ return { stdout: `no
2527
+ `, exitCode: 1 };
2528
+ }
1722
2529
  }
1723
2530
  function planInstall(options) {
1724
2531
  const { capabilities, ...unit } = options;
@@ -1726,11 +2533,11 @@ function planInstall(options) {
1726
2533
  }
1727
2534
  async function applyPlan(plan, run) {
1728
2535
  const transcript = [];
1729
- for (const step of plan.steps) {
1730
- const result = await run(step.command);
1731
- transcript.push({ label: step.label, command: step.command, exitCode: result.exitCode });
1732
- if (result.exitCode !== 0 && !step.optional) {
1733
- throw new Error(`${step.label} failed (exit ${result.exitCode}): ${step.command}`);
2536
+ for (const step2 of plan.steps) {
2537
+ const result = await run(step2.operation);
2538
+ transcript.push({ label: step2.label, command: step2.command, exitCode: result.exitCode });
2539
+ if (result.exitCode !== 0 && !step2.optional) {
2540
+ throw new Error(`${step2.label} failed (exit ${result.exitCode}): ${step2.command}`);
1734
2541
  }
1735
2542
  }
1736
2543
  return transcript;
@@ -1773,7 +2580,7 @@ var systemdValue = (name, raw) => {
1773
2580
  const value = raw;
1774
2581
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$")}"`;
1775
2582
  };
1776
- function validatePlatformSharedEnvironment(input, options = {}) {
2583
+ function validatePlatformSharedEnvironment(input) {
1777
2584
  if (input.softwareProfile === "platform-api" !== (input.databaseRole === "none")) {
1778
2585
  throw new Error("platform-api requires database role none; platform-db-api requires master or joiner.");
1779
2586
  }
@@ -1783,14 +2590,26 @@ function validatePlatformSharedEnvironment(input, options = {}) {
1783
2590
  const coordinators = input.databaseCoordinators.map(privateCoordinator);
1784
2591
  if (new Set(coordinators).size !== coordinators.length)
1785
2592
  throw new Error("databaseCoordinators must be unique.");
1786
- if (!["private-lan", "cloudflare-warp"].includes(input.databaseNetworkMode)) {
1787
- throw new Error("Database networking must be private-lan or cloudflare-warp.");
2593
+ if (input.databaseNetworkMode !== "private-lan") {
2594
+ throw new Error("Attended platform bootstrap supports only private-lan database networking.");
1788
2595
  }
1789
2596
  boundedInteger("databaseReplicationFactor", input.databaseReplicationFactor, 1, 16);
1790
2597
  boundedInteger("databaseWriteConcern", input.databaseWriteConcern, 1, 16);
1791
2598
  if (input.databaseWriteConcern > input.databaseReplicationFactor) {
1792
2599
  throw new Error("databaseWriteConcern cannot exceed databaseReplicationFactor.");
1793
2600
  }
2601
+ if (input.email?.provider === "smtp") {
2602
+ safeAtom("email.host", input.email.host);
2603
+ boundedInteger("email.port", input.email.port, 1, 65535);
2604
+ safeAtom("email.user", input.email.user);
2605
+ safeAtom("email.from", input.email.from);
2606
+ } else if (input.email?.provider === "jetemail") {
2607
+ safeAtom("email.from", input.email.from);
2608
+ if (typeof input.email.eu !== "boolean")
2609
+ throw new Error("JetEmail eu must be boolean.");
2610
+ } else if (input.email !== undefined) {
2611
+ throw new Error("Bootstrap email provider must be smtp or jetemail.");
2612
+ }
1794
2613
  boundedInteger("publicApiPort", input.publicApiPort, 1024, 65533);
1795
2614
  boundedInteger("seedSyncMembers", input.seedSyncMembers, 1, 64);
1796
2615
  boundedInteger("concurrencyLimit", input.concurrencyLimit, 1, 1e6);
@@ -1823,13 +2642,6 @@ function validatePlatformSharedEnvironment(input, options = {}) {
1823
2642
  if (url.username || url.password || url.hash)
1824
2643
  throw new Error("Seed peers cannot contain credentials or fragments.");
1825
2644
  }
1826
- if (input.smtp) {
1827
- safeAtom("smtp.host", input.smtp.host);
1828
- boundedInteger("smtp.port", input.smtp.port, 1, 65535);
1829
- safeAtom("smtp.from", input.smtp.from);
1830
- if (input.smtp.user)
1831
- safeAtom("smtp.user", input.smtp.user);
1832
- }
1833
2645
  if (input.backup) {
1834
2646
  httpsOrigin("backup.endpoint", input.backup.endpoint);
1835
2647
  for (const [name, value] of Object.entries(input.backup))
@@ -1842,14 +2654,12 @@ function validatePlatformSharedEnvironment(input, options = {}) {
1842
2654
  const service = new URL(input.cloudflare.tunnelService);
1843
2655
  if (service.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(service.hostname) || service.username || service.password || service.search || service.hash)
1844
2656
  throw new Error("Cloudflare Tunnel service must be loopback HTTP.");
1845
- if (input.cloudflare.warp) {
1846
- 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)) {
1847
- throw new Error("Cloudflare WARP organization, VNET or device profile is malformed.");
1848
- }
1849
- }
1850
2657
  }
1851
- if (!options.allowPendingCloudflareHandoff && input.databaseNetworkMode === "cloudflare-warp" !== Boolean(input.cloudflare?.warp)) {
1852
- throw new Error("cloudflare-warp networking requires its exact enrolled Cloudflare coordinates.");
2658
+ if (input.realtime) {
2659
+ input.realtime.endpoint = httpsOrigin("realtime.endpoint", input.realtime.endpoint);
2660
+ if (!/^[a-z0-9][a-z0-9_-]{0,62}$/.test(input.realtime.workerScriptName) || !/^[A-Za-z0-9][A-Za-z0-9_.:@/-]{0,127}$/.test(input.realtime.producer)) {
2661
+ throw new Error("realtime.producer is invalid.");
2662
+ }
1853
2663
  }
1854
2664
  return {
1855
2665
  ...input,
@@ -1904,10 +2714,12 @@ function renderPlatformSharedEnvironment(input) {
1904
2714
  FZ_PROFILE: value.deployProfile,
1905
2715
  FZ_REPO: value.repository,
1906
2716
  FZ_BRANCH: value.branch,
1907
- FZ_SMTP_HOST: value.smtp?.host ?? "",
1908
- FZ_SMTP_PORT: value.smtp ? String(value.smtp.port) : "",
1909
- FZ_SMTP_USER: value.smtp?.user ?? "",
1910
- FZ_SMTP_FROM: value.smtp?.from ?? "",
2717
+ FZ_EMAIL_PROVIDER: value.email?.provider ?? "",
2718
+ FZ_SMTP_HOST: value.email?.provider === "smtp" ? value.email.host : "",
2719
+ FZ_SMTP_PORT: value.email?.provider === "smtp" ? String(value.email.port) : "",
2720
+ FZ_SMTP_USER: value.email?.provider === "smtp" ? value.email.user : "",
2721
+ FZ_EMAIL_FROM: value.email?.from ?? "",
2722
+ FZ_JETEMAIL_EU: value.email?.provider === "jetemail" ? String(value.email.eu) : "",
1911
2723
  BACKUP_S3_ENDPOINT: value.backup?.endpoint ?? "",
1912
2724
  BACKUP_S3_REGION: value.backup?.region ?? "",
1913
2725
  BACKUP_S3_BUCKET: value.backup?.bucket ?? "",
@@ -1917,9 +2729,9 @@ function renderPlatformSharedEnvironment(input) {
1917
2729
  FZ_CF_KV_NAMESPACE_ID: value.cloudflare?.kvNamespaceId ?? "",
1918
2730
  FZ_CF_TUNNEL_ID: value.cloudflare?.tunnelId ?? "",
1919
2731
  FZ_CF_TUNNEL_SERVICE: value.cloudflare?.tunnelService ?? "",
1920
- FZ_WARP_ORGANIZATION: value.cloudflare?.warp?.organization ?? "",
1921
- FZ_CF_VIRTUAL_NETWORK_ID: value.cloudflare?.warp?.virtualNetworkId ?? "",
1922
- FZ_CF_WARP_POLICY_ID: value.cloudflare?.warp?.deviceProfileId ?? ""
2732
+ FZ_REALTIME_WORKER_SCRIPT: value.realtime?.workerScriptName ?? "",
2733
+ FZ_REALTIME_ENDPOINT: value.realtime?.endpoint ?? "",
2734
+ FZ_REALTIME_PRODUCER: value.realtime?.producer ?? ""
1923
2735
  };
1924
2736
  return `# Generated by fz bootstrap platform. Non-secret coordinates only.
1925
2737
  ` + Object.entries(entries).map(([key, entry]) => `${key}=${systemdValue(key, entry)}`).join(`
@@ -1928,9 +2740,11 @@ function renderPlatformSharedEnvironment(input) {
1928
2740
  }
1929
2741
  function platformApiCredentialSpecs(options) {
1930
2742
  const optional = [
1931
- ["bootstrap-smtp-password", options.smtp],
1932
- ["cloudflare-kv-token", options.cloudflareKv],
1933
- ["cloudflare-network-token", options.cloudflareNetwork]
2743
+ ["bootstrap-smtp-password", options.emailProvider === "smtp"],
2744
+ ["bootstrap-jetemail-api-key", options.emailProvider === "jetemail"],
2745
+ ["CF_API_TOKEN", options.cloudflareKv],
2746
+ ["REALTIME_PUBLISH_SECRET", options.realtime],
2747
+ ["REALTIME_TICKET_SECRET", options.realtime]
1934
2748
  ];
1935
2749
  return [
1936
2750
  { name: "arangodb-jwt", encryptedPath: "/etc/forgezero/creds/arangodb-jwt.cred", required: true },
@@ -2054,40 +2868,12 @@ function renderPlatformActivationFiles(input) {
2054
2868
  ].join(`
2055
2869
  `) + `
2056
2870
  `;
2057
- const helper = `#!/usr/bin/env bash
2058
- set -Eeuo pipefail
2059
- source /etc/forgezero/deploy.env
2060
- [[ $# == 1 ]] || { echo "usage: forgezero-activate <release>" >&2; exit 2; }
2061
- release="$(realpath -e "$1")"; releases="$(realpath -e "$FZ_DIR/releases")"; slots="$FZ_DIR/slots"
2062
- install -d -o root -g root -m 0755 "$slots"
2063
- case "$release/" in "$releases"/*/) ;; *) echo "release is outside $releases" >&2; exit 2 ;; esac
2064
- [[ -f "$release/.fz/deploy.json" && -s "$release/src/index.ts" && -s "$release/bun.lock" ]] || { echo "release is incomplete" >&2; exit 2; }
2065
- slot_file="$FZ_DIR/.forge-slot"; previous_slot="$(cat "$slot_file" 2>/dev/null || true)"
2066
- if [[ "$previous_slot" == blue ]]; then target=green; port="$FZ_GREEN_PORT"; else target=blue; port="$FZ_BLUE_PORT"; fi
2067
- target_link="$slots/$target"; previous_target_link="$(readlink -f "$target_link" 2>/dev/null || true)"
2068
- 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 {} +
2069
- ln -sfn "$release" "$target_link"; systemctl restart "forgezero@\${target}.service"
2070
- 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
2071
- 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
2072
- upstream=/etc/nginx/conf.d/forgezero-upstream.conf; backup="$(mktemp -p /run forgezero-upstream.XXXXXX)"; [[ -f "$upstream" ]] && cp "$upstream" "$backup" || : >"$backup"
2073
- printf 'upstream forgezero { server 127.0.0.1:%s; }\\n' "$port" >"$upstream"
2074
- 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
2075
- rm -f "$backup"; printf '%s\\n' "$target" >"$slot_file"
2076
- # New nginx workers select the new slot after reload. Keep the old slot alive
2077
- # while old workers drain in-flight requests and upgraded connections.
2078
- if [[ -n "$previous_slot" && "$previous_slot" != "$target" ]]; then
2079
- sleep_seconds="$(( (FZ_DRAIN_DEADLINE_MS + 999) / 1000 ))"
2080
- sleep "$sleep_seconds"
2081
- systemctl stop "forgezero@\${previous_slot}.service" || true
2082
- fi
2083
- 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-)
2084
- for path in "\${old[@]}"; do [[ "$path" == "$release" ]] || rm -rf -- "$path"; done
2085
- printf 'promoted %s on %s\\n' "$release" "$target"
2871
+ const helper = `${JSON.stringify({ ...input, drainDeadlineMs }, null, 2)}
2086
2872
  `;
2087
2873
  return {
2088
2874
  environment,
2089
2875
  helper,
2090
- sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/libexec/forgezero-activate *
2876
+ sudoers: `forgezero-runner ALL=(root) NOPASSWD: /usr/local/lib/forgezero/agent/fz-agent platform-activate --config=/etc/forgezero/deploy-activation.json *
2091
2877
  `
2092
2878
  };
2093
2879
  }
@@ -2128,12 +2914,143 @@ function planLocalOtlpProof(endpoint2, collectorUnit) {
2128
2914
  };
2129
2915
  }
2130
2916
 
2917
+ // src/otel-collector.ts
2918
+ var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
2919
+ var FORGEZERO_OTEL_COLLECTOR_VERSION = "0.157.0";
2920
+ var FORGEZERO_OTEL_COLLECTOR_SHA256 = "2937cf24892af55b143c072fddece17862239cf78280620029276493eb81beae";
2921
+ var ARCHIVE = "/run/forgezero-otelcol.tar.gz";
2922
+ var EXTRACT = "/run/forgezero-otelcol-release";
2923
+ var BINARY = "/usr/local/lib/forgezero/otelcol/otelcol";
2924
+ var CONFIG = "/etc/forgezero/otelcol.yaml";
2925
+ var UNIT = `/etc/systemd/system/${FORGEZERO_OTEL_COLLECTOR_UNIT}`;
2926
+ var checked = async (host, argv) => {
2927
+ const result = await host.exec(argv);
2928
+ const output = `${result.output ?? result.stdout ?? ""}${result.stderr ?? ""}`;
2929
+ if (result.exitCode !== 0)
2930
+ throw new Error(`OTLP collector operation failed: ${argv.join(" ")}${output.trim() ? `: ${output.trim()}` : ""}`);
2931
+ return output.trim();
2932
+ };
2933
+ var destination = (value) => {
2934
+ let url;
2935
+ try {
2936
+ url = new URL(value);
2937
+ } catch {
2938
+ throw new Error("OTLP collector export endpoint must be an absolute HTTPS URL");
2939
+ }
2940
+ if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash || url.port && url.port !== "443") {
2941
+ throw new Error("OTLP collector export endpoint must be credential-free HTTPS");
2942
+ }
2943
+ return url.toString().replace(/\/$/, "");
2944
+ };
2945
+ function renderForgeZeroOtelCollector(exportEndpoint) {
2946
+ const endpoint2 = destination(exportEndpoint);
2947
+ return {
2948
+ config: `receivers:
2949
+ otlp:
2950
+ protocols:
2951
+ http:
2952
+ endpoint: 127.0.0.1:4318
2953
+ exporters:
2954
+ otlphttp:
2955
+ endpoint: ${JSON.stringify(endpoint2)}
2956
+ service:
2957
+ telemetry:
2958
+ metrics:
2959
+ address: 127.0.0.1:8888
2960
+ pipelines:
2961
+ traces:
2962
+ receivers: [otlp]
2963
+ exporters: [otlphttp]
2964
+ metrics:
2965
+ receivers: [otlp]
2966
+ exporters: [otlphttp]
2967
+ logs:
2968
+ receivers: [otlp]
2969
+ exporters: [otlphttp]
2970
+ `,
2971
+ unit: `[Unit]
2972
+ Description=ForgeZero independently supervised OpenTelemetry Collector
2973
+ After=network-online.target
2974
+ Wants=network-online.target
2975
+
2976
+ [Service]
2977
+ Type=simple
2978
+ User=forgezero-otel
2979
+ Group=forgezero-otel
2980
+ ExecStart=${BINARY} --config=${CONFIG}
2981
+ Restart=always
2982
+ RestartSec=2
2983
+ LimitCORE=0
2984
+ NoNewPrivileges=true
2985
+ PrivateTmp=true
2986
+ ProtectSystem=strict
2987
+ ProtectHome=true
2988
+ ProtectKernelTunables=true
2989
+ ProtectKernelModules=true
2990
+ ProtectControlGroups=true
2991
+ RestrictSUIDSGID=true
2992
+ RestrictRealtime=true
2993
+ LockPersonality=true
2994
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
2995
+
2996
+ [Install]
2997
+ WantedBy=multi-user.target
2998
+ `
2999
+ };
3000
+ }
3001
+ async function ensureForgeZeroOtelCollector(host, exportEndpoint) {
3002
+ const rendered = renderForgeZeroOtelCollector(exportEndpoint);
3003
+ const observed = await host.exec([BINARY, "--version"]);
3004
+ const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
3005
+ if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
3006
+ await checked(host, [
3007
+ "/usr/bin/curl",
3008
+ "--fail",
3009
+ "--silent",
3010
+ "--show-error",
3011
+ "--location",
3012
+ "--proto",
3013
+ "=https",
3014
+ "--output",
3015
+ ARCHIVE,
3016
+ `https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
3017
+ ]);
3018
+ const digest = (await checked(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
3019
+ if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
3020
+ throw new Error("OTLP collector archive checksum mismatch");
3021
+ await checked(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
3022
+ await checked(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
3023
+ await checked(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
3024
+ await checked(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
3025
+ }
3026
+ if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
3027
+ await checked(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
3028
+ }
3029
+ if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
3030
+ await checked(host, [
3031
+ "/usr/sbin/useradd",
3032
+ "--system",
3033
+ "--no-create-home",
3034
+ "--shell",
3035
+ "/usr/sbin/nologin",
3036
+ "--gid",
3037
+ "forgezero-otel",
3038
+ "forgezero-otel"
3039
+ ]);
3040
+ }
3041
+ host.write(CONFIG, rendered.config, 420);
3042
+ host.write(UNIT, rendered.unit, 420);
3043
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"]);
3044
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
3045
+ }
3046
+
2131
3047
  // src/bootstrap.ts
2132
3048
  import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
2133
3049
  var PLATFORM_BOOTSTRAP_PROFILES = [
2134
3050
  "platform-db-api",
2135
3051
  "platform-api"
2136
3052
  ];
3053
+ var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
2137
3054
  function resolveInstalledBootstrapKind(states) {
2138
3055
  if (states.compute && states.metal) {
2139
3056
  throw new Error("host has both metal and compute bootstrap state; refusing an ambiguous operation");
@@ -2146,12 +3063,15 @@ var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
2146
3063
  var CREDS = "/etc/forgezero/creds";
2147
3064
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
2148
3065
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
2149
- var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
3066
+ var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
3067
+ var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
3068
+ var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
3069
+ var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
3070
+ var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
2150
3071
  var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
2151
3072
  var BACKUP_RECOVERY_CREDENTIAL = `${CREDS}/backup-recovery-root.cred`;
2152
- var WARP_CLIENT_ID_CREDENTIAL = `${CREDS}/warp-auth-client-id.cred`;
2153
- var WARP_CLIENT_SECRET_CREDENTIAL = `${CREDS}/warp-auth-client-secret.cred`;
2154
3073
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
3074
+ var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
2155
3075
  var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
2156
3076
  var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
2157
3077
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
@@ -2219,19 +3139,22 @@ function validateBootstrapConfig(value) {
2219
3139
  throw new Error(`deployment credential ${name} must map to an absolute encrypted .cred path`);
2220
3140
  }
2221
3141
  }
2222
- if (value.cloudflareHandoff && (!value.installCloudflared || !value.cloudflareHandoff.handoffFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
2223
- throw new Error("Cloudflare handoff requires cloudflared installation, a checkpoint and a valid node name");
3142
+ if (Boolean(value.installCloudflared) !== Boolean(value.cloudflareHandoff) || value.cloudflareHandoff && (!value.cloudflareHandoff.handoffFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
3143
+ throw new Error("cloudflared requires a node-specific Cloudflare handoff and both must be supplied together");
2224
3144
  }
2225
- if (value.kind === "tenant") {
3145
+ if (value.installWarp && !value.cloudflareHandoff) {
3146
+ throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
3147
+ }
3148
+ if (value.kind === "enrolled-compute") {
2226
3149
  if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
2227
3150
  throw new Error("tenant realm is malformed");
2228
3151
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
2229
3152
  throw new Error("tenant API must be public HTTPS or loopback HTTP");
2230
3153
  }
2231
3154
  if (!value.enrolTokenFile)
2232
- throw new Error("tenant bootstrap requires --enrol-token-file");
3155
+ throw new Error("enrolled-compute activation requires an enrolment-token file");
2233
3156
  if (value.bootstrapRunner) {
2234
- if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
3157
+ if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
2235
3158
  throw new Error("bootstrap runner SSH private-key file must be absolute");
2236
3159
  }
2237
3160
  let targetTelemetry;
@@ -2280,14 +3203,21 @@ function validateBootstrapConfig(value) {
2280
3203
  throw new Error("database coordinators must contain 1-16 unique private origins");
2281
3204
  }
2282
3205
  value.database.coordinators = write;
2283
- const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
2284
- allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
2285
- });
3206
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment);
3207
+ if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
3208
+ throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
3209
+ }
2286
3210
  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(",")) {
2287
3211
  throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
2288
3212
  }
2289
3213
  if (runtime.deployProfile !== value.environment)
2290
3214
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
3215
+ if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
3216
+ throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3217
+ }
3218
+ if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
3219
+ throw new Error("Bootstrap email credential file must be an absolute single-line path");
3220
+ }
2291
3221
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
2292
3222
  throw new Error("runtime database topology disagrees with bootstrap topology");
2293
3223
  }
@@ -2306,16 +3236,19 @@ function validateBootstrapConfig(value) {
2306
3236
  }
2307
3237
  function planBootstrap(input, initialized = false) {
2308
3238
  const config = validateBootstrapConfig(structuredClone(input));
2309
- const software = config.kind === "tenant" ? [
3239
+ const software = config.kind === "enrolled-compute" ? [
2310
3240
  ...config.software ?? [],
2311
3241
  ...config.bootstrapRunner && !config.software?.some(({ id }) => id === "openssh-client") ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
2312
- ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
3242
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
3243
+ ...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
2313
3244
  ] : [
2314
3245
  ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
2315
3246
  { id: "bun", version: "1.3.14" },
2316
3247
  { id: "nginx", version: "ubuntu-26.04" },
3248
+ ...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
2317
3249
  ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
2318
- ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
3250
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
3251
+ ...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
2319
3252
  ];
2320
3253
  return {
2321
3254
  kind: config.kind,
@@ -2325,7 +3258,7 @@ function planBootstrap(input, initialized = false) {
2325
3258
  statePath: STATE_PATH,
2326
3259
  steps: [
2327
3260
  { id: "agent", label: "Install and verify the common Agent supervision boundary", mutation: true },
2328
- { id: "software", label: "Install exact Agent-owned software coordinates in order", mutation: true },
3261
+ { id: "software", label: "Install exact Agent-owned software coordinates and the checksum-verified OTLP collector in order", mutation: true },
2329
3262
  { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
2330
3263
  ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
2331
3264
  ...config.kind === "platform" ? [
@@ -2333,19 +3266,18 @@ function planBootstrap(input, initialized = false) {
2333
3266
  { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
2334
3267
  { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
2335
3268
  ] : [],
2336
- ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
3269
+ ...config.installCloudflared ? [{ id: "cloudflare-connectors", label: "Install cloudflared and optional Mesh/WARP connector from the attended handoff", mutation: true }] : [],
2337
3270
  { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
2338
3271
  { id: "status", label: "Verify services and immutable profile", mutation: false }
2339
3272
  ]
2340
3273
  };
2341
3274
  }
2342
- var checked = async (host, argv, label, options) => {
3275
+ var checked2 = async (host, argv, label, options) => {
2343
3276
  const result = await host.exec(argv, options);
2344
3277
  if (result.exitCode !== 0)
2345
3278
  throw new Error(`${label} failed: ${result.output.trim()}`);
2346
3279
  return result.output;
2347
3280
  };
2348
- var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
2349
3281
  var unitEscape = (value) => {
2350
3282
  if (/[^A-Za-z0-9_./:@,+-]/.test(value))
2351
3283
  throw new Error(`unsafe systemd coordinate: ${value}`);
@@ -2353,7 +3285,7 @@ var unitEscape = (value) => {
2353
3285
  };
2354
3286
  function databaseUnit(config) {
2355
3287
  const db = config.database;
2356
- const join2 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
3288
+ const join3 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
2357
3289
  const agency = db.agency === "none" ? " --cluster.start-agent=false --cluster.start-coordinator=true --cluster.start-dbserver=true" : "";
2358
3290
  return `[Unit]
2359
3291
  Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role}; agency=${db.agency})
@@ -2365,7 +3297,7 @@ Type=simple
2365
3297
  User=arangodb
2366
3298
  Group=arangodb
2367
3299
  LoadCredentialEncrypted=arangodb-jwt:${JWT_CREDENTIAL}
2368
- 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}${agency}
3300
+ 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${join3}${agency}
2369
3301
  Restart=always
2370
3302
  RestartSec=5
2371
3303
  UMask=0077
@@ -2420,8 +3352,8 @@ Wants=network-online.target
2420
3352
  [Service]
2421
3353
  Type=simple
2422
3354
  DynamicUser=yes
2423
- LoadCredentialEncrypted=cloudflared-token:${TUNNEL_CREDENTIAL}
2424
- ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics ${CLOUDFLARED_METRICS_ADDRESS} run --token-file %d/cloudflared-token
3355
+ LoadCredentialEncrypted=CF_TUNNEL_CONNECTOR_TOKEN:${TUNNEL_CREDENTIAL}
3356
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics ${CLOUDFLARED_METRICS_ADDRESS} run --token-file %d/CF_TUNNEL_CONNECTOR_TOKEN
2425
3357
  Restart=always
2426
3358
  RestartSec=5
2427
3359
  NoNewPrivileges=true
@@ -2429,6 +3361,28 @@ PrivateTmp=true
2429
3361
  ProtectSystem=strict
2430
3362
  ProtectHome=true
2431
3363
 
3364
+ [Install]
3365
+ WantedBy=multi-user.target
3366
+ `;
3367
+ }
3368
+ function meshConnectorUnit() {
3369
+ return `[Unit]
3370
+ Description=ForgeZero Cloudflare Mesh/WARP connector registration
3371
+ Requires=warp-svc.service
3372
+ After=network-online.target warp-svc.service
3373
+ Wants=network-online.target
3374
+
3375
+ [Service]
3376
+ Type=oneshot
3377
+ RemainAfterExit=yes
3378
+ LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${WARP_CONNECTOR_CREDENTIAL}
3379
+ ExecStart=/usr/local/bin/fz-agent mesh-config
3380
+ NoNewPrivileges=true
3381
+ PrivateTmp=true
3382
+ ProtectSystem=strict
3383
+ ProtectHome=true
3384
+ ReadWritePaths=/var/lib/cloudflare-warp
3385
+
2432
3386
  [Install]
2433
3387
  WantedBy=multi-user.target
2434
3388
  `;
@@ -2492,15 +3446,15 @@ var derive = (root, label) => {
2492
3446
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
2493
3447
  return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
2494
3448
  };
2495
- async function seal(host, name, destination, value) {
2496
- if (host.exists(destination))
3449
+ async function seal(host, name, destination2, value) {
3450
+ if (host.exists(destination2))
2497
3451
  return;
2498
- const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
3452
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination2], { stdin: value });
2499
3453
  if (result.exitCode !== 0)
2500
3454
  throw new Error(`could not seal ${name}: ${result.output.trim()}`);
2501
3455
  }
2502
3456
  function bootstrapIdentity(config) {
2503
- if (config.kind === "tenant")
3457
+ if (config.kind === "enrolled-compute")
2504
3458
  return {
2505
3459
  kind: config.kind,
2506
3460
  apiUrl: config.apiUrl,
@@ -2513,9 +3467,11 @@ function bootstrapIdentity(config) {
2513
3467
  deployRoot: config.deployRoot ?? "/opt/forgezero",
2514
3468
  software: (config.software ?? []).map(({ id, version }) => ({ id, version })),
2515
3469
  installCloudflared: Boolean(config.installCloudflared),
3470
+ installWarp: Boolean(config.installWarp),
2516
3471
  bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
2517
3472
  };
2518
3473
  const environment = config.runtime.environment;
3474
+ const { cloudflare: _cloudflare, realtime: _realtime, ...stableEnvironment } = environment;
2519
3475
  return {
2520
3476
  kind: config.kind,
2521
3477
  environment: config.environment,
@@ -2545,10 +3501,12 @@ function bootstrapIdentity(config) {
2545
3501
  publicApiPort: environment.publicApiPort,
2546
3502
  healthPath: config.runtime.healthPath,
2547
3503
  keepReleases: config.runtime.keepReleases,
2548
- environment
3504
+ environment: stableEnvironment
2549
3505
  },
2550
3506
  firewall: config.firewall,
2551
- installCloudflared: Boolean(config.installCloudflared)
3507
+ installCloudflared: Boolean(config.installCloudflared),
3508
+ installWarp: Boolean(config.installWarp),
3509
+ cloudflareNodeName: config.cloudflareHandoff?.nodeName ?? null
2552
3510
  };
2553
3511
  }
2554
3512
  function bootstrapIdentityDigest(config) {
@@ -2564,7 +3522,7 @@ function parseStoredState(raw) {
2564
3522
  if (!value || typeof value !== "object" || Array.isArray(value))
2565
3523
  throw new Error("bootstrap state is malformed");
2566
3524
  const state = value;
2567
- if (state.format !== 2 || !["platform", "tenant"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && !["member", "none"].includes(state.databaseAgency ?? "")) {
3525
+ if (state.format !== 2 || !["platform", "enrolled-compute"].includes(state.kind ?? "") || !/^[a-f0-9]{64}$/.test(state.identityDigest ?? "") || typeof state.profile !== "string" || typeof state.nodeHostname !== "string" || typeof state.apiUrl !== "string" || state.kind === "platform" && !["member", "none"].includes(state.databaseAgency ?? "")) {
2568
3526
  throw new Error("bootstrap state is legacy or incomplete; refusing an unbound repair");
2569
3527
  }
2570
3528
  return state;
@@ -2579,7 +3537,7 @@ function parseStoredIntent(raw) {
2579
3537
  if (!value || typeof value !== "object" || Array.isArray(value))
2580
3538
  throw new Error("bootstrap intent is malformed");
2581
3539
  const intent = value;
2582
- if (intent.format !== 1 || !["platform", "tenant"].includes(intent.kind ?? "") || !/^[a-f0-9]{64}$/.test(intent.identityDigest ?? "") || Number.isNaN(Date.parse(intent.createdAt ?? ""))) {
3540
+ if (intent.format !== 1 || !["platform", "enrolled-compute"].includes(intent.kind ?? "") || !/^[a-f0-9]{64}$/.test(intent.identityDigest ?? "") || Number.isNaN(Date.parse(intent.createdAt ?? ""))) {
2583
3541
  throw new Error("bootstrap intent is incomplete; refusing an unbound resume");
2584
3542
  }
2585
3543
  return intent;
@@ -2650,10 +3608,13 @@ function stateFor(config, cloudflare, previousCloudflareTunnelId) {
2650
3608
  publicApiPort: config.runtime.environment.publicApiPort,
2651
3609
  healthPath: config.runtime.healthPath,
2652
3610
  cloudflare: config.runtime.environment.cloudflare,
2653
- cloudflared: Boolean(config.cloudflareHandoff)
3611
+ realtime: config.runtime.environment.realtime,
3612
+ cloudflared: Boolean(config.cloudflareHandoff),
3613
+ warp: Boolean(config.installWarp)
2654
3614
  } : {
2655
3615
  realm: config.realm,
2656
3616
  cloudflared: Boolean(config.cloudflareHandoff),
3617
+ warp: Boolean(config.installWarp),
2657
3618
  cloudflareTunnelId: cloudflare?.tunnelId ?? previousCloudflareTunnelId
2658
3619
  }
2659
3620
  }, null, 2)}
@@ -2675,6 +3636,8 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2675
3636
  units.push(state.collectorUnit);
2676
3637
  if (state.cloudflared)
2677
3638
  units.push("cloudflared.service");
3639
+ if (state.warp)
3640
+ units.push("warp-svc.service", "forgezero-mesh-config.service");
2678
3641
  if (state.kind === "platform" && state.databaseRole !== "none")
2679
3642
  units.push("forgezero-db.service", "forgezero-db-verify.service");
2680
3643
  const services = {};
@@ -2691,7 +3654,7 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2691
3654
  problems.push(`${socket} is missing`);
2692
3655
  }
2693
3656
  if (state.cloudflared) {
2694
- for (const credential of [TUNNEL_CREDENTIAL, `${CREDS}/cloudflare-kv-token.cred`]) {
3657
+ for (const credential of [TUNNEL_CREDENTIAL, CF_API_CREDENTIAL]) {
2695
3658
  services[credential] = host.exists(credential);
2696
3659
  if (!services[credential])
2697
3660
  problems.push(`${credential} is missing`);
@@ -2707,20 +3670,16 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2707
3670
  problems.push(evidence.problem);
2708
3671
  }
2709
3672
  }
3673
+ if (state.warp) {
3674
+ services[WARP_CONNECTOR_CREDENTIAL] = host.exists(WARP_CONNECTOR_CREDENTIAL);
3675
+ if (!services[WARP_CONNECTOR_CREDENTIAL])
3676
+ problems.push(`${WARP_CONNECTOR_CREDENTIAL} is missing`);
3677
+ const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
3678
+ services["cloudflare-mesh"] = status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output);
3679
+ if (!services["cloudflare-mesh"])
3680
+ problems.push("Cloudflare Mesh/WARP is not connected");
3681
+ }
2710
3682
  if (state.kind === "platform") {
2711
- if (state.cloudflared) {
2712
- if (state.cloudflare?.warp) {
2713
- for (const credential of [
2714
- `${CREDS}/cloudflare-network-token.cred`,
2715
- WARP_CLIENT_ID_CREDENTIAL,
2716
- WARP_CLIENT_SECRET_CREDENTIAL
2717
- ]) {
2718
- services[credential] = host.exists(credential);
2719
- if (!services[credential])
2720
- problems.push(`${credential} is missing`);
2721
- }
2722
- }
2723
- }
2724
3683
  const [blue, green] = await Promise.all([
2725
3684
  host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
2726
3685
  host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
@@ -2779,7 +3738,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2779
3738
  const config = validateBootstrapConfig(structuredClone(input));
2780
3739
  if (host.uid() !== 0)
2781
3740
  throw new Error("fz bootstrap --apply must run as root");
2782
- if (config.kind === "tenant") {
3741
+ if (config.kind === "enrolled-compute") {
2783
3742
  const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
2784
3743
  if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
2785
3744
  throw new Error("tenant enrolment token is malformed");
@@ -2793,14 +3752,18 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2793
3752
  cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.handoffFile, config.cloudflareHandoff.nodeName);
2794
3753
  } else if (config.kind === "platform" && installed?.cloudflare) {
2795
3754
  config.runtime.environment.cloudflare = installed.cloudflare;
3755
+ config.runtime.environment.realtime = installed.realtime;
2796
3756
  config.runtime.environment = validatePlatformSharedEnvironment(config.runtime.environment);
2797
- } else if (!(config.kind === "tenant" && installed?.cloudflareTunnelId)) {
3757
+ } else if (!(config.kind === "enrolled-compute" && installed?.cloudflareTunnelId)) {
2798
3758
  throw new Error("node-specific Cloudflare host handoff is missing before credential sealing");
2799
3759
  }
2800
3760
  }
2801
3761
  if (cloudflare && cloudflare.hostname !== config.nodeHostname) {
2802
3762
  throw new Error("Cloudflare handoff hostname disagrees with node hostname");
2803
3763
  }
3764
+ if (cloudflare && Boolean(cloudflare.mesh) !== Boolean(config.installWarp)) {
3765
+ throw new Error("Cloudflare Mesh handoff and installWarp selection disagree");
3766
+ }
2804
3767
  if (cloudflare && config.kind === "platform") {
2805
3768
  const expected = config.runtime.environment.cloudflare;
2806
3769
  if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
@@ -2811,19 +3774,25 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2811
3774
  zoneId: cloudflare.zoneId,
2812
3775
  kvNamespaceId: cloudflare.kvNamespaceId,
2813
3776
  tunnelId: cloudflare.tunnelId,
2814
- tunnelService: cloudflare.service,
2815
- ...cloudflare.warp ? { warp: {
2816
- organization: cloudflare.warp.organization,
2817
- virtualNetworkId: cloudflare.warp.virtualNetworkId,
2818
- deviceProfileId: cloudflare.warp.deviceProfileId
2819
- } } : {}
3777
+ tunnelService: cloudflare.service
2820
3778
  };
2821
3779
  if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
2822
- throw new Error("Cloudflare handoff disagrees with immutable WARP/runtime coordinates");
3780
+ throw new Error("Cloudflare handoff disagrees with immutable runtime coordinates");
2823
3781
  }
2824
3782
  config.runtime.environment.cloudflare = expected ?? discovered;
2825
- if (config.runtime.environment.databaseNetworkMode === "cloudflare-warp" !== Boolean(cloudflare.warp)) {
2826
- throw new Error("Cloudflare handoff private-network mode disagrees with the platform database network mode");
3783
+ if (cloudflare.realtime) {
3784
+ const expectedRealtime = config.runtime.environment.realtime;
3785
+ const discoveredRealtime = {
3786
+ workerScriptName: cloudflare.realtime.workerScriptName,
3787
+ endpoint: cloudflare.realtime.endpoint,
3788
+ producer: cloudflare.realtime.producer
3789
+ };
3790
+ if (expectedRealtime && JSON.stringify(expectedRealtime) !== JSON.stringify(discoveredRealtime)) {
3791
+ throw new Error("Cloudflare realtime handoff disagrees with immutable runtime coordinates");
3792
+ }
3793
+ config.runtime.environment.realtime = expectedRealtime ?? discoveredRealtime;
3794
+ } else if (config.runtime.environment.realtime) {
3795
+ throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
2827
3796
  }
2828
3797
  }
2829
3798
  if (installed) {
@@ -2831,21 +3800,30 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2831
3800
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
2832
3801
  }
2833
3802
  }
3803
+ const platformPrivate = config.kind === "platform" ? {
3804
+ root: privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret"),
3805
+ email: config.runtime.credentialFiles?.emailSecret && !host.exists(`${CREDS}/${config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : "bootstrap-smtp-password"}.cred`) ? privateFile(host, config.runtime.credentialFiles.emailSecret, "bootstrap email credential") : undefined,
3806
+ backup: config.runtime.credentialFiles?.backupS3Secret && !host.exists(`${CREDS}/backup-s3-secret.cred`) ? privateFile(host, config.runtime.credentialFiles.backupS3Secret, "backup S3 credential") : undefined
3807
+ } : undefined;
3808
+ if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3809
+ privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
3810
+ }
2834
3811
  bindBootstrapIntent(host, config);
2835
3812
  const plan = planBootstrap(config, host.exists(STATE_PATH));
2836
3813
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
2837
3814
  host.mkdir(CREDS, 448);
2838
3815
  host.mkdir("/var/lib/forgezero", 448);
2839
- if (config.kind === "tenant" && config.bootstrapRunner && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
2840
- await seal(host, "bootstrap-ssh-key", BOOTSTRAP_SSH_CREDENTIAL, privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key"));
2841
- host.remove(config.bootstrapRunner.sshPrivateKeyFile);
3816
+ if (cloudflare && !host.exists(CF_API_CREDENTIAL)) {
3817
+ await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflare.apiToken);
2842
3818
  }
2843
- if (cloudflare?.warp) {
2844
- await seal(host, "warp-auth-client-id", WARP_CLIENT_ID_CREDENTIAL, cloudflare.warp.clientId);
2845
- await seal(host, "warp-auth-client-secret", WARP_CLIENT_SECRET_CREDENTIAL, cloudflare.warp.clientSecret);
3819
+ if (cloudflare?.realtime && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
3820
+ await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, cloudflare.realtime.publishSecret);
2846
3821
  }
2847
- if (cloudflare && !host.exists(`${CREDS}/cloudflare-kv-token.cred`)) {
2848
- await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
3822
+ if (cloudflare?.realtime && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
3823
+ await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, cloudflare.realtime.ticketSecret);
3824
+ }
3825
+ if (cloudflare?.mesh && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
3826
+ await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, cloudflare.mesh.connectorToken);
2849
3827
  }
2850
3828
  await host.installAgent(config);
2851
3829
  if (config.kind === "platform" && config.firewall.enabled) {
@@ -2853,50 +3831,52 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2853
3831
  } else
2854
3832
  await host.ensureSoftware(plan.software);
2855
3833
  if (config.kind === "platform" && config.firewall.enabled) {
2856
- await checked(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
2857
- await checked(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
2858
- await checked(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
3834
+ await checked2(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
3835
+ await checked2(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
3836
+ await checked2(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
2859
3837
  for (const cidr of config.firewall.privateCidrs) {
2860
3838
  if (config.database.role !== "none")
2861
- await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
3839
+ await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
2862
3840
  for (const port of [config.runtime.bluePort, config.runtime.greenPort])
2863
- await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
3841
+ await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
2864
3842
  }
2865
- await checked(host, ["ufw", "--force", "enable"], "firewall activation");
3843
+ await checked2(host, ["ufw", "--force", "enable"], "firewall activation");
2866
3844
  await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
2867
3845
  }
2868
3846
  if (config.kind === "platform") {
2869
- const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
3847
+ const root = platformPrivate.root;
2870
3848
  if (!host.exists(JWT_CREDENTIAL)) {
2871
3849
  await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
2872
3850
  }
2873
3851
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
2874
3852
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
2875
3853
  const credentialFiles = config.runtime.credentialFiles ?? {};
3854
+ const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
3855
+ if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
3856
+ throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3857
+ }
2876
3858
  for (const [name, source] of Object.entries({
2877
- "bootstrap-smtp-password": credentialFiles.smtpPassword,
2878
- "backup-s3-secret": credentialFiles.backupS3Secret
3859
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
3860
+ "backup-s3-secret": platformPrivate.backup
2879
3861
  })) {
2880
3862
  if (source) {
2881
- const destination = `${CREDS}/${name}.cred`;
2882
- if (!host.exists(destination)) {
2883
- await seal(host, name, destination, privateFile(host, source, name));
2884
- host.remove(source);
3863
+ const destination2 = `${CREDS}/${name}.cred`;
3864
+ if (!host.exists(destination2)) {
3865
+ await seal(host, name, destination2, source);
3866
+ const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
3867
+ if (sourcePath)
3868
+ host.remove(sourcePath);
2885
3869
  }
2886
3870
  }
2887
3871
  }
2888
- if (cloudflare) {
2889
- if (cloudflare.privateNetworkRuntimeToken)
2890
- await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
2891
- }
2892
3872
  const runtime = config.runtime;
3873
+ await ensureForgeZeroOtelCollector(host, config.telemetryEndpoint);
2893
3874
  const cloudflareConfigured = Boolean(runtime.environment.cloudflare);
2894
- const cloudflareNetworkConfigured = Boolean(runtime.environment.cloudflare?.warp);
2895
3875
  const envPath = `${runtime.environment.sharedDirectory}/.env`;
2896
3876
  const credentials = platformApiCredentialSpecs({
2897
- smtp: Boolean(credentialFiles.smtpPassword),
3877
+ emailProvider: runtime.environment.email?.provider,
2898
3878
  cloudflareKv: cloudflareConfigured,
2899
- cloudflareNetwork: cloudflareNetworkConfigured
3879
+ realtime: Boolean(runtime.environment.realtime)
2900
3880
  });
2901
3881
  const units = renderPlatformApiUnits({
2902
3882
  serviceUser: runtime.serviceUser,
@@ -2924,8 +3904,8 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2924
3904
  keepReleases: runtime.keepReleases,
2925
3905
  drainDeadlineMs: runtime.environment.drainDeadlineMs
2926
3906
  });
2927
- await checked(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
2928
- await checked(host, ["id", runtime.serviceUser], "existing API service account");
3907
+ await checked2(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
3908
+ await checked2(host, ["id", runtime.serviceUser], "existing API service account");
2929
3909
  });
2930
3910
  host.mkdir(runtime.environment.sharedDirectory, 488);
2931
3911
  host.mkdir(runtime.slotsDirectory, 493);
@@ -2934,39 +3914,39 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2934
3914
  host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
2935
3915
  `, 420);
2936
3916
  }
2937
- await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
3917
+ await checked2(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
2938
3918
  host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
2939
3919
  host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
2940
3920
  host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
2941
3921
  host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
2942
3922
  host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
2943
3923
  host.write("/etc/forgezero/deploy.env", activation.environment, 420);
2944
- host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
3924
+ host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
2945
3925
  host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
2946
- await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
3926
+ await checked2(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
2947
3927
  const telemetry = planLocalOtlpProof(runtime.environment.otlpEndpoint, runtime.environment.otlpCollectorUnit);
2948
- await checked(host, telemetry.unitCheck.argv, "OTLP collector supervision");
2949
- const otlpStatus = (await checked(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
3928
+ await checked2(host, telemetry.unitCheck.argv, "OTLP collector supervision");
3929
+ const otlpStatus = (await checked2(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
2950
3930
  if (!/^2\d\d$/.test(otlpStatus))
2951
3931
  throw new Error(`OTLP receiver returned HTTP ${otlpStatus || "unknown"}`);
2952
- await checked(host, ["nginx", "-t"], "nginx configuration");
2953
- await checked(host, ["systemctl", "daemon-reload"], "systemd reload");
2954
- await checked(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
3932
+ await checked2(host, ["nginx", "-t"], "nginx configuration");
3933
+ await checked2(host, ["systemctl", "daemon-reload"], "systemd reload");
3934
+ await checked2(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
2955
3935
  if (config.database.role === "master") {
2956
3936
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
2957
3937
  if (!host.exists(invite)) {
2958
- host.write(invite, `plt_${randomBytes(24).toString("hex")}
3938
+ host.write(invite, `plt_${randomBytes3(24).toString("hex")}
2959
3939
  `, 384);
2960
- await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
3940
+ await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
2961
3941
  }
2962
3942
  }
2963
3943
  if (config.database.role !== "none") {
2964
3944
  host.mkdir("/var/lib/forgezero-cluster", 448);
2965
- await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
3945
+ await checked2(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
2966
3946
  host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
2967
3947
  host.write("/etc/systemd/system/forgezero-db-verify.service", databaseVerifyUnit(config), 420);
2968
- await checked(host, ["systemctl", "daemon-reload"], "database unit reload");
2969
- await checked(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
3948
+ await checked2(host, ["systemctl", "daemon-reload"], "database unit reload");
3949
+ await checked2(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
2970
3950
  const evidence = {
2971
3951
  expectedMode: "default",
2972
3952
  role: "COORDINATOR",
@@ -2984,7 +3964,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2984
3964
  host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
2985
3965
  `, 384);
2986
3966
  }
2987
- await checked(host, [
3967
+ await checked2(host, [
2988
3968
  "runuser",
2989
3969
  "-u",
2990
3970
  "forgezero-agent",
@@ -3001,18 +3981,25 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3001
3981
  }
3002
3982
  if (config.cloudflareHandoff) {
3003
3983
  if (!host.exists(TUNNEL_CREDENTIAL) && cloudflare) {
3004
- await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3984
+ await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3005
3985
  }
3006
3986
  if (!host.exists(TUNNEL_CREDENTIAL))
3007
3987
  throw new Error("sealed cloudflared connector credential is missing");
3008
3988
  host.write("/etc/systemd/system/cloudflared.service", tunnelUnit(), 420);
3009
- await checked(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
3010
- await checked(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
3989
+ await checked2(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
3990
+ await checked2(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
3011
3991
  const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
3012
3992
  if (!tunnelId)
3013
3993
  throw new Error("Cloudflare tunnel identity is missing after handoff validation");
3014
3994
  await waitForCloudflaredTunnel(host, tunnelId);
3015
3995
  }
3996
+ if (config.installWarp) {
3997
+ if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
3998
+ throw new Error("sealed Cloudflare Mesh connector credential is missing");
3999
+ host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
4000
+ await checked2(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
4001
+ await checked2(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
4002
+ }
3016
4003
  host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
3017
4004
  const status = await bootstrapStatus(host);
3018
4005
  if (!status.initialized)
@@ -3030,7 +4017,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3030
4017
  if (!/^plt_[a-f0-9]{48}$/.test(token))
3031
4018
  throw new Error("platform invite is malformed");
3032
4019
  launch = {
3033
- 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)}`,
4020
+ executable: "/usr/local/bin/fz",
4021
+ argv: ["genesis", "--mode", "1-of-1", "--api", config.apiUrl, "--app", config.runtime.environment.appOrigin],
4022
+ environment: { FZ_SHARED_DIR: config.runtime.environment.sharedDirectory },
4023
+ runAs: config.runtime.serviceUser,
3034
4024
  inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
3035
4025
  };
3036
4026
  }
@@ -3062,6 +4052,7 @@ function strictBootstrapDocument(value) {
3062
4052
  "runtime",
3063
4053
  "firewall",
3064
4054
  "installCloudflared",
4055
+ "installWarp",
3065
4056
  "cloudflareHandoff",
3066
4057
  "realm",
3067
4058
  "enrolTokenFile",
@@ -3111,46 +4102,53 @@ function strictBootstrapDocument(value) {
3111
4102
  "otlpCollectorUnit",
3112
4103
  "agentOtlpEndpoint",
3113
4104
  "custodianEmail",
3114
- "smtp",
4105
+ "email",
3115
4106
  "repository",
3116
4107
  "branch",
3117
4108
  "deployProfile",
3118
4109
  "otlpFlushIntervalMs",
3119
4110
  "otlpTraceSampleRatio",
3120
4111
  "backup",
3121
- "cloudflare"
4112
+ "cloudflare",
4113
+ "realtime"
3122
4114
  ], "runtime environment");
3123
4115
  if (runtime.credentialFiles !== undefined)
3124
- exactKeys(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
4116
+ exactKeys(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
3125
4117
  const environment = runtime.environment;
3126
- if (environment.smtp !== undefined)
3127
- exactKeys(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
4118
+ if (environment.email !== undefined) {
4119
+ const email = exactKeys(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
4120
+ if (email.provider === "smtp")
4121
+ exactKeys(email, ["provider", "host", "port", "user", "from"], "SMTP config");
4122
+ else if (email.provider === "jetemail")
4123
+ exactKeys(email, ["provider", "from", "eu"], "JetEmail config");
4124
+ else
4125
+ throw new Error("email provider must be smtp or jetemail");
4126
+ }
3128
4127
  if (environment.backup !== undefined)
3129
4128
  exactKeys(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
3130
4129
  if (environment.cloudflare !== undefined)
3131
- exactKeys(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService", "warp"], "Cloudflare runtime config");
3132
- if (environment.cloudflare && typeof environment.cloudflare === "object" && environment.cloudflare.warp !== undefined) {
3133
- exactKeys(environment.cloudflare.warp, ["organization", "virtualNetworkId", "deviceProfileId"], "Cloudflare WARP runtime config");
3134
- }
3135
- } else if (root.kind === "tenant") {
4130
+ exactKeys(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService"], "Cloudflare runtime config");
4131
+ if (environment.realtime !== undefined)
4132
+ exactKeys(environment.realtime, ["workerScriptName", "endpoint", "producer"], "realtime runtime config");
4133
+ } else if (root.kind === "enrolled-compute") {
3136
4134
  if (root.cloudflareHandoff !== undefined)
3137
4135
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
3138
4136
  if (root.bootstrapRunner !== undefined)
3139
4137
  exactKeys(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
3140
4138
  for (const key of ["environment", "profile", "computeReference", "database", "enrolment", "runtime"]) {
3141
4139
  if (root[key] !== undefined && key !== "profile")
3142
- throw new Error(`tenant bootstrap cannot contain ${key}`);
4140
+ throw new Error(`enrolled-compute activation cannot contain ${key}`);
3143
4141
  }
3144
4142
  } else
3145
- throw new Error("bootstrap config kind must be platform or tenant");
4143
+ throw new Error("bootstrap config kind must be platform or enrolled-compute");
3146
4144
  return value;
3147
4145
  }
3148
4146
  function readBootstrapConfig(path) {
3149
- const metadata = lstatSync(path);
4147
+ const metadata = lstatSync2(path);
3150
4148
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
3151
4149
  throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
3152
4150
  }
3153
- return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync(path, "utf8"))));
4151
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync3(path, "utf8"))));
3154
4152
  }
3155
4153
  function localBootstrapHost() {
3156
4154
  const execute = async (argv, options = {}) => {
@@ -3168,19 +4166,19 @@ function localBootstrapHost() {
3168
4166
  };
3169
4167
  return {
3170
4168
  uid: () => process.getuid?.() ?? -1,
3171
- exists: existsSync,
3172
- read: (path) => readFileSync(path, "utf8"),
4169
+ exists: existsSync3,
4170
+ read: (path) => readFileSync3(path, "utf8"),
3173
4171
  write(path, content, mode) {
3174
- mkdirSync(dirname2(path), { recursive: true, mode: 493 });
4172
+ mkdirSync3(dirname4(path), { recursive: true, mode: 493 });
3175
4173
  const temporary = `${path}.next.${process.pid}`;
3176
- writeFileSync(temporary, content, { mode });
3177
- chmodSync(temporary, mode);
3178
- renameSync(temporary, path);
4174
+ writeFileSync3(temporary, content, { mode });
4175
+ chmodSync2(temporary, mode);
4176
+ renameSync3(temporary, path);
3179
4177
  },
3180
- mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
3181
- remove: (path) => rmSync(path, { force: true }),
4178
+ mkdir: (path, mode) => mkdirSync3(path, { recursive: true, mode }),
4179
+ remove: (path) => rmSync3(path, { force: true }),
3182
4180
  inspect(path) {
3183
- const value = lstatSync(path);
4181
+ const value = lstatSync2(path);
3184
4182
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
3185
4183
  },
3186
4184
  exec: execute,
@@ -3202,6 +4200,7 @@ function localBootstrapHost() {
3202
4200
  async installAgent(config, enrolTokenSourcePath) {
3203
4201
  const capabilities = await readCapabilities(localRunner);
3204
4202
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
4203
+ const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync3("/var/lib/forgezero/enrolment.json");
3205
4204
  if (config.kind === "platform") {
3206
4205
  const lifecycle = config.database.role === "none" ? {
3207
4206
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -3213,8 +4212,8 @@ function localBootstrapHost() {
3213
4212
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
3214
4213
  databasePorts: [8529]
3215
4214
  };
3216
- mkdirSync(dirname2(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
3217
- writeFileSync(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
4215
+ mkdirSync3(dirname4(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
4216
+ writeFileSync3(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
3218
4217
  `, { mode: 256 });
3219
4218
  }
3220
4219
  const plan = planInstall({
@@ -3231,39 +4230,32 @@ function localBootstrapHost() {
3231
4230
  gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
3232
4231
  gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
3233
4232
  generateGitIdentity: true,
3234
- pullDeployments: true,
3235
- pullMigrations: config.kind === "platform",
3236
- pullBootstrap: config.kind === "tenant" && Boolean(config.bootstrapRunner),
3237
- bootstrapSshCredentialPath: config.kind === "tenant" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
3238
- bootstrapTargetTelemetryEndpoint: config.kind === "tenant" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
4233
+ pullDeployments: hasBinding,
4234
+ pullMigrations: config.kind === "platform" && hasBinding,
4235
+ pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
4236
+ bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
4237
+ bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
4238
+ bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
4239
+ bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
3239
4240
  lifecycleProfilePath: config.kind === "platform" ? LIFECYCLE_PROFILE : undefined,
3240
- ...config.kind === "platform" && config.runtime.environment.cloudflare?.warp ? {
3241
- warpOrganization: config.runtime.environment.cloudflare.warp.organization,
3242
- warpClientIdCredentialPath: WARP_CLIENT_ID_CREDENTIAL,
3243
- warpClientSecretCredentialPath: WARP_CLIENT_SECRET_CREDENTIAL,
3244
- cloudflareAccountId: config.runtime.environment.cloudflare.accountId,
3245
- cloudflareTunnelId: config.runtime.environment.cloudflare.tunnelId,
3246
- cloudflareVirtualNetworkId: config.runtime.environment.cloudflare.warp.virtualNetworkId,
3247
- cloudflareWarpPolicyId: config.runtime.environment.cloudflare.warp.deviceProfileId
3248
- } : {},
3249
4241
  enforceEgress: true,
3250
4242
  nodeHostname: config.nodeHostname,
3251
4243
  telemetryEndpoint: config.telemetryEndpoint,
3252
4244
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
3253
4245
  sourceBinPath: PACKAGED_AGENT_BIN,
3254
- ...config.kind === "tenant" || enrolTokenSourcePath ? {
3255
- enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
4246
+ ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
4247
+ enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
3256
4248
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
3257
4249
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
3258
4250
  apiUrl: config.apiUrl,
3259
- project: config.kind === "tenant" ? config.realm : "platform",
3260
- environment: config.kind === "tenant" ? undefined : config.environment,
3261
- nodeLabel: config.kind === "tenant" ? config.nodeHostname : config.computeReference
4251
+ project: config.kind === "enrolled-compute" ? config.realm : "platform",
4252
+ environment: config.kind === "enrolled-compute" ? undefined : config.environment,
4253
+ nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
3262
4254
  } : {}
3263
4255
  });
3264
4256
  for (const unit of [{ path: plan.unitPath, unit: plan.unit }, ...plan.auxiliaryUnits]) {
3265
- mkdirSync(dirname2(unit.path), { recursive: true, mode: 493 });
3266
- writeFileSync(unit.path, unit.unit, { mode: 420 });
4257
+ mkdirSync3(dirname4(unit.path), { recursive: true, mode: 493 });
4258
+ writeFileSync3(unit.path, unit.unit, { mode: 420 });
3267
4259
  }
3268
4260
  await applyPlan(plan, localRunner);
3269
4261
  return plan;