@forgezero/agent 0.1.41 → 0.1.43

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 +299 -86
  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 +1509 -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 +3932 -582
  26. package/dist/fz-git-ssh.js +122 -0
  27. package/dist/fz.js +3636 -1263
  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 +150 -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 +5709 -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.41";
1210
+ var VERSION = "0.1.43";
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 })
2211
+ ] : [],
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 })
1639
2245
  ] : [],
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
- }] : []
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,148 @@ 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"]).catch((cause) => {
3004
+ if (cause?.code === "ENOENT") {
3005
+ return { exitCode: 127, output: "", stdout: "", stderr: "" };
3006
+ }
3007
+ throw cause;
3008
+ });
3009
+ const versionOutput = `${observed.output ?? observed.stdout ?? ""}${observed.stderr ?? ""}`;
3010
+ if (observed.exitCode !== 0 || !versionOutput.includes(`otelcol version ${FORGEZERO_OTEL_COLLECTOR_VERSION}`)) {
3011
+ await checked(host, [
3012
+ "/usr/bin/curl",
3013
+ "--fail",
3014
+ "--silent",
3015
+ "--show-error",
3016
+ "--location",
3017
+ "--proto",
3018
+ "=https",
3019
+ "--output",
3020
+ ARCHIVE,
3021
+ `https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${FORGEZERO_OTEL_COLLECTOR_VERSION}/otelcol_${FORGEZERO_OTEL_COLLECTOR_VERSION}_linux_amd64.tar.gz`
3022
+ ]);
3023
+ const digest = (await checked(host, ["/usr/bin/sha256sum", ARCHIVE])).split(/\s+/)[0];
3024
+ if (digest !== FORGEZERO_OTEL_COLLECTOR_SHA256)
3025
+ throw new Error("OTLP collector archive checksum mismatch");
3026
+ await checked(host, ["/usr/bin/install", "-d", "-m", "0755", "/usr/local/lib/forgezero/otelcol", EXTRACT]);
3027
+ await checked(host, ["/usr/bin/tar", "-xzf", ARCHIVE, "-C", EXTRACT, "otelcol"]);
3028
+ await checked(host, ["/usr/bin/install", "-m", "0755", `${EXTRACT}/otelcol`, BINARY]);
3029
+ await checked(host, ["/usr/bin/rm", "-f", ARCHIVE, `${EXTRACT}/otelcol`]);
3030
+ }
3031
+ if ((await host.exec(["/usr/bin/getent", "group", "forgezero-otel"])).exitCode !== 0) {
3032
+ await checked(host, ["/usr/sbin/groupadd", "--system", "forgezero-otel"]);
3033
+ }
3034
+ if ((await host.exec(["/usr/bin/id", "forgezero-otel"])).exitCode !== 0) {
3035
+ await checked(host, [
3036
+ "/usr/sbin/useradd",
3037
+ "--system",
3038
+ "--no-create-home",
3039
+ "--shell",
3040
+ "/usr/sbin/nologin",
3041
+ "--gid",
3042
+ "forgezero-otel",
3043
+ "forgezero-otel"
3044
+ ]);
3045
+ }
3046
+ host.write(CONFIG, rendered.config, 420);
3047
+ host.write(UNIT, rendered.unit, 420);
3048
+ await checked(host, ["/usr/bin/systemctl", "daemon-reload"]);
3049
+ await checked(host, ["/usr/bin/systemctl", "enable", "--now", FORGEZERO_OTEL_COLLECTOR_UNIT]);
3050
+ }
3051
+
2131
3052
  // src/bootstrap.ts
2132
3053
  import { DEFAULT_SOCKET as DEFAULT_SOCKET2 } from "@forgezero/vault";
2133
3054
  var PLATFORM_BOOTSTRAP_PROFILES = [
2134
3055
  "platform-db-api",
2135
3056
  "platform-api"
2136
3057
  ];
3058
+ var platformBootstrapRunner = (config) => config.kind === "platform" && config.database.role === "master";
2137
3059
  function resolveInstalledBootstrapKind(states) {
2138
3060
  if (states.compute && states.metal) {
2139
3061
  throw new Error("host has both metal and compute bootstrap state; refusing an ambiguous operation");
@@ -2146,12 +3068,15 @@ var INTENT_PATH = "/var/lib/forgezero/bootstrap.intent.json";
2146
3068
  var CREDS = "/etc/forgezero/creds";
2147
3069
  var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
2148
3070
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
2149
- var TUNNEL_CREDENTIAL = `${CREDS}/cloudflared-token.cred`;
3071
+ var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
3072
+ var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
3073
+ var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
3074
+ var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
3075
+ var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
2150
3076
  var SEED_CREDENTIAL = `${CREDS}/seed-sync-root.cred`;
2151
3077
  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
3078
  var BOOTSTRAP_SSH_CREDENTIAL = `${CREDS}/bootstrap-ssh-key.cred`;
3079
+ var BOOTSTRAP_SSH_PUBLIC_KEY = "/etc/forgezero/bootstrap/runner.pub";
2155
3080
  var GIT_PUBLIC_KEY = "/etc/forgezero/git/deploy.pub";
2156
3081
  var PLATFORM_ENROL_SOURCE = "/run/forgezero-platform-enrol-token";
2157
3082
  var DB_MODE_EVIDENCE = "/var/lib/forgezero-cluster/server-mode.json";
@@ -2219,19 +3144,22 @@ function validateBootstrapConfig(value) {
2219
3144
  throw new Error(`deployment credential ${name} must map to an absolute encrypted .cred path`);
2220
3145
  }
2221
3146
  }
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");
3147
+ if (Boolean(value.installCloudflared) !== Boolean(value.cloudflareHandoff) || value.cloudflareHandoff && (!value.cloudflareHandoff.handoffFile || !/^[a-z0-9][a-z0-9-]{0,62}$/.test(value.cloudflareHandoff.nodeName))) {
3148
+ throw new Error("cloudflared requires a node-specific Cloudflare handoff and both must be supplied together");
3149
+ }
3150
+ if (value.installWarp && !value.cloudflareHandoff) {
3151
+ throw new Error("Cloudflare Mesh/WARP requires a node-specific Cloudflare handoff");
2224
3152
  }
2225
- if (value.kind === "tenant") {
3153
+ if (value.kind === "enrolled-compute") {
2226
3154
  if (!/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(value.realm))
2227
3155
  throw new Error("tenant realm is malformed");
2228
3156
  if (!/^https:\/\//.test(value.apiUrl) && !/^http:\/\/(?:127\.0\.0\.1|localhost)(?::\d+)?$/.test(value.apiUrl)) {
2229
3157
  throw new Error("tenant API must be public HTTPS or loopback HTTP");
2230
3158
  }
2231
3159
  if (!value.enrolTokenFile)
2232
- throw new Error("tenant bootstrap requires --enrol-token-file");
3160
+ throw new Error("enrolled-compute activation requires an enrolment-token file");
2233
3161
  if (value.bootstrapRunner) {
2234
- if (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile)) {
3162
+ if (value.bootstrapRunner.sshPrivateKeyFile !== undefined && (!value.bootstrapRunner.sshPrivateKeyFile.startsWith("/") || /[\r\n]/.test(value.bootstrapRunner.sshPrivateKeyFile))) {
2235
3163
  throw new Error("bootstrap runner SSH private-key file must be absolute");
2236
3164
  }
2237
3165
  let targetTelemetry;
@@ -2280,14 +3208,21 @@ function validateBootstrapConfig(value) {
2280
3208
  throw new Error("database coordinators must contain 1-16 unique private origins");
2281
3209
  }
2282
3210
  value.database.coordinators = write;
2283
- const runtime = validatePlatformSharedEnvironment(value.runtime.environment, {
2284
- allowPendingCloudflareHandoff: Boolean(value.cloudflareHandoff)
2285
- });
3211
+ const runtime = validatePlatformSharedEnvironment(value.runtime.environment);
3212
+ if (runtime.otlpCollectorUnit !== FORGEZERO_OTEL_COLLECTOR_UNIT) {
3213
+ throw new Error(`platform bootstrap requires ${FORGEZERO_OTEL_COLLECTOR_UNIT}`);
3214
+ }
2286
3215
  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
3216
  throw new Error("platform runtime coordinates disagree with immutable bootstrap coordinates");
2288
3217
  }
2289
3218
  if (runtime.deployProfile !== value.environment)
2290
3219
  throw new Error("runtime deployment profile disagrees with bootstrap environment");
3220
+ if (Boolean(runtime.email) !== Boolean(value.runtime.credentialFiles?.emailSecret)) {
3221
+ throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3222
+ }
3223
+ if (value.runtime.credentialFiles?.emailSecret && (!value.runtime.credentialFiles.emailSecret.startsWith("/") || /[\r\n]/.test(value.runtime.credentialFiles.emailSecret))) {
3224
+ throw new Error("Bootstrap email credential file must be an absolute single-line path");
3225
+ }
2291
3226
  if (value.database.address !== runtime.databaseAddress || value.database.master !== runtime.databaseMaster) {
2292
3227
  throw new Error("runtime database topology disagrees with bootstrap topology");
2293
3228
  }
@@ -2306,16 +3241,19 @@ function validateBootstrapConfig(value) {
2306
3241
  }
2307
3242
  function planBootstrap(input, initialized = false) {
2308
3243
  const config = validateBootstrapConfig(structuredClone(input));
2309
- const software = config.kind === "tenant" ? [
3244
+ const software = config.kind === "enrolled-compute" ? [
2310
3245
  ...config.software ?? [],
2311
3246
  ...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" }] : []
3247
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
3248
+ ...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
2313
3249
  ] : [
2314
3250
  ...config.firewall.enabled ? [{ id: "ufw", version: "ubuntu-26.04" }] : [],
2315
3251
  { id: "bun", version: "1.3.14" },
2316
3252
  { id: "nginx", version: "ubuntu-26.04" },
3253
+ ...platformBootstrapRunner(config) ? [{ id: "openssh-client", version: "ubuntu-26.04" }] : [],
2317
3254
  ...config.profile === "platform-api" ? [] : [{ id: "arangodb", version: "3.11.14" }],
2318
- ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : []
3255
+ ...config.installCloudflared ? [{ id: "cloudflared", version: "2026.7.3" }] : [],
3256
+ ...config.installWarp ? [{ id: "cloudflare-warp", version: "2026.6.822.0-min" }] : []
2319
3257
  ];
2320
3258
  return {
2321
3259
  kind: config.kind,
@@ -2325,7 +3263,7 @@ function planBootstrap(input, initialized = false) {
2325
3263
  statePath: STATE_PATH,
2326
3264
  steps: [
2327
3265
  { 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 },
3266
+ { id: "software", label: "Install exact Agent-owned software coordinates and the checksum-verified OTLP collector in order", mutation: true },
2329
3267
  { id: "credentials", label: "Seal bootstrap credentials with systemd-creds", mutation: true },
2330
3268
  ...config.kind === "platform" && config.database.role !== "none" ? [{ id: "database", label: "Provision Community 3.11.14 cluster and Coordinator mode", mutation: true }] : [],
2331
3269
  ...config.kind === "platform" ? [
@@ -2333,19 +3271,18 @@ function planBootstrap(input, initialized = false) {
2333
3271
  { id: "deploy", label: "Create the first invite when required and health-gate the initial deployment", mutation: true },
2334
3272
  { id: "enrol", label: "Consume the API-bound platform capability and enable signed control", mutation: true }
2335
3273
  ] : [],
2336
- ...config.installCloudflared ? [{ id: "cloudflared-install-only", label: "Install cloudflared without creating Cloudflare resources", mutation: true }] : [],
3274
+ ...config.installCloudflared ? [{ id: "cloudflare-connectors", label: "Install cloudflared and optional Mesh/WARP connector from the attended handoff", mutation: true }] : [],
2337
3275
  { id: "state", label: "Persist immutable bootstrap profile and evidence coordinates", mutation: true },
2338
3276
  { id: "status", label: "Verify services and immutable profile", mutation: false }
2339
3277
  ]
2340
3278
  };
2341
3279
  }
2342
- var checked = async (host, argv, label, options) => {
3280
+ var checked2 = async (host, argv, label, options) => {
2343
3281
  const result = await host.exec(argv, options);
2344
3282
  if (result.exitCode !== 0)
2345
3283
  throw new Error(`${label} failed: ${result.output.trim()}`);
2346
3284
  return result.output;
2347
3285
  };
2348
- var shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
2349
3286
  var unitEscape = (value) => {
2350
3287
  if (/[^A-Za-z0-9_./:@,+-]/.test(value))
2351
3288
  throw new Error(`unsafe systemd coordinate: ${value}`);
@@ -2353,7 +3290,7 @@ var unitEscape = (value) => {
2353
3290
  };
2354
3291
  function databaseUnit(config) {
2355
3292
  const db = config.database;
2356
- const join2 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
3293
+ const join3 = db.role === "joiner" ? ` --starter.join=${unitEscape(db.master)}` : "";
2357
3294
  const agency = db.agency === "none" ? " --cluster.start-agent=false --cluster.start-coordinator=true --cluster.start-dbserver=true" : "";
2358
3295
  return `[Unit]
2359
3296
  Description=ForgeZero ArangoDB Community 3.11.14 cluster (${db.role}; agency=${db.agency})
@@ -2365,7 +3302,7 @@ Type=simple
2365
3302
  User=arangodb
2366
3303
  Group=arangodb
2367
3304
  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}
3305
+ 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
3306
  Restart=always
2370
3307
  RestartSec=5
2371
3308
  UMask=0077
@@ -2420,8 +3357,8 @@ Wants=network-online.target
2420
3357
  [Service]
2421
3358
  Type=simple
2422
3359
  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
3360
+ LoadCredentialEncrypted=CF_TUNNEL_CONNECTOR_TOKEN:${TUNNEL_CREDENTIAL}
3361
+ ExecStart=/usr/local/bin/cloudflared tunnel --no-autoupdate --metrics ${CLOUDFLARED_METRICS_ADDRESS} run --token-file %d/CF_TUNNEL_CONNECTOR_TOKEN
2425
3362
  Restart=always
2426
3363
  RestartSec=5
2427
3364
  NoNewPrivileges=true
@@ -2429,6 +3366,28 @@ PrivateTmp=true
2429
3366
  ProtectSystem=strict
2430
3367
  ProtectHome=true
2431
3368
 
3369
+ [Install]
3370
+ WantedBy=multi-user.target
3371
+ `;
3372
+ }
3373
+ function meshConnectorUnit() {
3374
+ return `[Unit]
3375
+ Description=ForgeZero Cloudflare Mesh/WARP connector registration
3376
+ Requires=warp-svc.service
3377
+ After=network-online.target warp-svc.service
3378
+ Wants=network-online.target
3379
+
3380
+ [Service]
3381
+ Type=oneshot
3382
+ RemainAfterExit=yes
3383
+ LoadCredentialEncrypted=CF_WARP_CONNECTOR_TOKEN:${WARP_CONNECTOR_CREDENTIAL}
3384
+ ExecStart=/usr/local/bin/fz-agent mesh-config
3385
+ NoNewPrivileges=true
3386
+ PrivateTmp=true
3387
+ ProtectSystem=strict
3388
+ ProtectHome=true
3389
+ ReadWritePaths=/var/lib/cloudflare-warp
3390
+
2432
3391
  [Install]
2433
3392
  WantedBy=multi-user.target
2434
3393
  `;
@@ -2492,15 +3451,15 @@ var derive = (root, label) => {
2492
3451
  throw new Error("cluster bootstrap code must contain exactly 64 hexadecimal characters");
2493
3452
  return createHmac("sha256", Buffer.from(root, "hex")).update(label).digest("hex");
2494
3453
  };
2495
- async function seal(host, name, destination, value) {
2496
- if (host.exists(destination))
3454
+ async function seal(host, name, destination2, value) {
3455
+ if (host.exists(destination2))
2497
3456
  return;
2498
- const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination], { stdin: value });
3457
+ const result = await host.exec(["systemd-creds", "encrypt", `--name=${name}`, "-", destination2], { stdin: value });
2499
3458
  if (result.exitCode !== 0)
2500
3459
  throw new Error(`could not seal ${name}: ${result.output.trim()}`);
2501
3460
  }
2502
3461
  function bootstrapIdentity(config) {
2503
- if (config.kind === "tenant")
3462
+ if (config.kind === "enrolled-compute")
2504
3463
  return {
2505
3464
  kind: config.kind,
2506
3465
  apiUrl: config.apiUrl,
@@ -2513,9 +3472,11 @@ function bootstrapIdentity(config) {
2513
3472
  deployRoot: config.deployRoot ?? "/opt/forgezero",
2514
3473
  software: (config.software ?? []).map(({ id, version }) => ({ id, version })),
2515
3474
  installCloudflared: Boolean(config.installCloudflared),
3475
+ installWarp: Boolean(config.installWarp),
2516
3476
  bootstrapRunner: config.bootstrapRunner ? { targetTelemetryEndpoint: config.bootstrapRunner.targetTelemetryEndpoint } : null
2517
3477
  };
2518
3478
  const environment = config.runtime.environment;
3479
+ const { cloudflare: _cloudflare, realtime: _realtime, ...stableEnvironment } = environment;
2519
3480
  return {
2520
3481
  kind: config.kind,
2521
3482
  environment: config.environment,
@@ -2545,10 +3506,12 @@ function bootstrapIdentity(config) {
2545
3506
  publicApiPort: environment.publicApiPort,
2546
3507
  healthPath: config.runtime.healthPath,
2547
3508
  keepReleases: config.runtime.keepReleases,
2548
- environment
3509
+ environment: stableEnvironment
2549
3510
  },
2550
3511
  firewall: config.firewall,
2551
- installCloudflared: Boolean(config.installCloudflared)
3512
+ installCloudflared: Boolean(config.installCloudflared),
3513
+ installWarp: Boolean(config.installWarp),
3514
+ cloudflareNodeName: config.cloudflareHandoff?.nodeName ?? null
2552
3515
  };
2553
3516
  }
2554
3517
  function bootstrapIdentityDigest(config) {
@@ -2564,7 +3527,7 @@ function parseStoredState(raw) {
2564
3527
  if (!value || typeof value !== "object" || Array.isArray(value))
2565
3528
  throw new Error("bootstrap state is malformed");
2566
3529
  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 ?? "")) {
3530
+ 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
3531
  throw new Error("bootstrap state is legacy or incomplete; refusing an unbound repair");
2569
3532
  }
2570
3533
  return state;
@@ -2579,7 +3542,7 @@ function parseStoredIntent(raw) {
2579
3542
  if (!value || typeof value !== "object" || Array.isArray(value))
2580
3543
  throw new Error("bootstrap intent is malformed");
2581
3544
  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 ?? ""))) {
3545
+ if (intent.format !== 1 || !["platform", "enrolled-compute"].includes(intent.kind ?? "") || !/^[a-f0-9]{64}$/.test(intent.identityDigest ?? "") || Number.isNaN(Date.parse(intent.createdAt ?? ""))) {
2583
3546
  throw new Error("bootstrap intent is incomplete; refusing an unbound resume");
2584
3547
  }
2585
3548
  return intent;
@@ -2650,10 +3613,13 @@ function stateFor(config, cloudflare, previousCloudflareTunnelId) {
2650
3613
  publicApiPort: config.runtime.environment.publicApiPort,
2651
3614
  healthPath: config.runtime.healthPath,
2652
3615
  cloudflare: config.runtime.environment.cloudflare,
2653
- cloudflared: Boolean(config.cloudflareHandoff)
3616
+ realtime: config.runtime.environment.realtime,
3617
+ cloudflared: Boolean(config.cloudflareHandoff),
3618
+ warp: Boolean(config.installWarp)
2654
3619
  } : {
2655
3620
  realm: config.realm,
2656
3621
  cloudflared: Boolean(config.cloudflareHandoff),
3622
+ warp: Boolean(config.installWarp),
2657
3623
  cloudflareTunnelId: cloudflare?.tunnelId ?? previousCloudflareTunnelId
2658
3624
  }
2659
3625
  }, null, 2)}
@@ -2675,6 +3641,8 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2675
3641
  units.push(state.collectorUnit);
2676
3642
  if (state.cloudflared)
2677
3643
  units.push("cloudflared.service");
3644
+ if (state.warp)
3645
+ units.push("warp-svc.service", "forgezero-mesh-config.service");
2678
3646
  if (state.kind === "platform" && state.databaseRole !== "none")
2679
3647
  units.push("forgezero-db.service", "forgezero-db-verify.service");
2680
3648
  const services = {};
@@ -2691,7 +3659,7 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2691
3659
  problems.push(`${socket} is missing`);
2692
3660
  }
2693
3661
  if (state.cloudflared) {
2694
- for (const credential of [TUNNEL_CREDENTIAL, `${CREDS}/cloudflare-kv-token.cred`]) {
3662
+ for (const credential of [TUNNEL_CREDENTIAL, CF_API_CREDENTIAL]) {
2695
3663
  services[credential] = host.exists(credential);
2696
3664
  if (!services[credential])
2697
3665
  problems.push(`${credential} is missing`);
@@ -2707,20 +3675,16 @@ async function bootstrapStatus(host = localBootstrapHost()) {
2707
3675
  problems.push(evidence.problem);
2708
3676
  }
2709
3677
  }
3678
+ if (state.warp) {
3679
+ services[WARP_CONNECTOR_CREDENTIAL] = host.exists(WARP_CONNECTOR_CREDENTIAL);
3680
+ if (!services[WARP_CONNECTOR_CREDENTIAL])
3681
+ problems.push(`${WARP_CONNECTOR_CREDENTIAL} is missing`);
3682
+ const status = await host.exec(["/usr/bin/warp-cli", "--accept-tos", "status"]);
3683
+ services["cloudflare-mesh"] = status.exitCode === 0 && /\bconnected\b/i.test(status.output) && !/\bdisconnected\b/i.test(status.output);
3684
+ if (!services["cloudflare-mesh"])
3685
+ problems.push("Cloudflare Mesh/WARP is not connected");
3686
+ }
2710
3687
  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
3688
  const [blue, green] = await Promise.all([
2725
3689
  host.exec(["systemctl", "is-active", "--quiet", "forgezero@blue.service"]),
2726
3690
  host.exec(["systemctl", "is-active", "--quiet", "forgezero@green.service"])
@@ -2779,7 +3743,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2779
3743
  const config = validateBootstrapConfig(structuredClone(input));
2780
3744
  if (host.uid() !== 0)
2781
3745
  throw new Error("fz bootstrap --apply must run as root");
2782
- if (config.kind === "tenant") {
3746
+ if (config.kind === "enrolled-compute") {
2783
3747
  const token = privateFile(host, config.enrolTokenFile, "tenant enrolment token");
2784
3748
  if (!/^fze_[A-Za-z0-9_-]{32,128}$/.test(token))
2785
3749
  throw new Error("tenant enrolment token is malformed");
@@ -2793,14 +3757,18 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2793
3757
  cloudflare = await readCloudflareHostHandoff(config.cloudflareHandoff.handoffFile, config.cloudflareHandoff.nodeName);
2794
3758
  } else if (config.kind === "platform" && installed?.cloudflare) {
2795
3759
  config.runtime.environment.cloudflare = installed.cloudflare;
3760
+ config.runtime.environment.realtime = installed.realtime;
2796
3761
  config.runtime.environment = validatePlatformSharedEnvironment(config.runtime.environment);
2797
- } else if (!(config.kind === "tenant" && installed?.cloudflareTunnelId)) {
3762
+ } else if (!(config.kind === "enrolled-compute" && installed?.cloudflareTunnelId)) {
2798
3763
  throw new Error("node-specific Cloudflare host handoff is missing before credential sealing");
2799
3764
  }
2800
3765
  }
2801
3766
  if (cloudflare && cloudflare.hostname !== config.nodeHostname) {
2802
3767
  throw new Error("Cloudflare handoff hostname disagrees with node hostname");
2803
3768
  }
3769
+ if (cloudflare && Boolean(cloudflare.mesh) !== Boolean(config.installWarp)) {
3770
+ throw new Error("Cloudflare Mesh handoff and installWarp selection disagree");
3771
+ }
2804
3772
  if (cloudflare && config.kind === "platform") {
2805
3773
  const expected = config.runtime.environment.cloudflare;
2806
3774
  if (expected && (cloudflare.service !== expected.tunnelService || cloudflare.accountId !== expected.accountId || cloudflare.zoneId !== expected.zoneId || cloudflare.kvNamespaceId !== expected.kvNamespaceId || cloudflare.tunnelId !== expected.tunnelId)) {
@@ -2811,19 +3779,25 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2811
3779
  zoneId: cloudflare.zoneId,
2812
3780
  kvNamespaceId: cloudflare.kvNamespaceId,
2813
3781
  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
- } } : {}
3782
+ tunnelService: cloudflare.service
2820
3783
  };
2821
3784
  if (expected && JSON.stringify(expected) !== JSON.stringify(discovered)) {
2822
- throw new Error("Cloudflare handoff disagrees with immutable WARP/runtime coordinates");
3785
+ throw new Error("Cloudflare handoff disagrees with immutable runtime coordinates");
2823
3786
  }
2824
3787
  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");
3788
+ if (cloudflare.realtime) {
3789
+ const expectedRealtime = config.runtime.environment.realtime;
3790
+ const discoveredRealtime = {
3791
+ workerScriptName: cloudflare.realtime.workerScriptName,
3792
+ endpoint: cloudflare.realtime.endpoint,
3793
+ producer: cloudflare.realtime.producer
3794
+ };
3795
+ if (expectedRealtime && JSON.stringify(expectedRealtime) !== JSON.stringify(discoveredRealtime)) {
3796
+ throw new Error("Cloudflare realtime handoff disagrees with immutable runtime coordinates");
3797
+ }
3798
+ config.runtime.environment.realtime = expectedRealtime ?? discoveredRealtime;
3799
+ } else if (config.runtime.environment.realtime) {
3800
+ throw new Error("platform realtime coordinates require a realtime-enabled Cloudflare handoff");
2827
3801
  }
2828
3802
  }
2829
3803
  if (installed) {
@@ -2831,21 +3805,30 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2831
3805
  throw new Error("bootstrap repair coordinates do not match the installed host identity");
2832
3806
  }
2833
3807
  }
3808
+ const platformPrivate = config.kind === "platform" ? {
3809
+ root: privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret"),
3810
+ 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,
3811
+ backup: config.runtime.credentialFiles?.backupS3Secret && !host.exists(`${CREDS}/backup-s3-secret.cred`) ? privateFile(host, config.runtime.credentialFiles.backupS3Secret, "backup S3 credential") : undefined
3812
+ } : undefined;
3813
+ if (config.kind === "enrolled-compute" && config.bootstrapRunner?.sshPrivateKeyFile && !host.exists(BOOTSTRAP_SSH_CREDENTIAL)) {
3814
+ privateFile(host, config.bootstrapRunner.sshPrivateKeyFile, "bootstrap runner SSH private key");
3815
+ }
2834
3816
  bindBootstrapIntent(host, config);
2835
3817
  const plan = planBootstrap(config, host.exists(STATE_PATH));
2836
3818
  const alreadyEnrolled = host.exists("/var/lib/forgezero/enrolment.json");
2837
3819
  host.mkdir(CREDS, 448);
2838
3820
  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);
3821
+ if (cloudflare && !host.exists(CF_API_CREDENTIAL)) {
3822
+ await seal(host, "CF_API_TOKEN", CF_API_CREDENTIAL, cloudflare.apiToken);
2842
3823
  }
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);
3824
+ if (cloudflare?.realtime && !host.exists(REALTIME_PUBLISH_CREDENTIAL)) {
3825
+ await seal(host, "REALTIME_PUBLISH_SECRET", REALTIME_PUBLISH_CREDENTIAL, cloudflare.realtime.publishSecret);
2846
3826
  }
2847
- if (cloudflare && !host.exists(`${CREDS}/cloudflare-kv-token.cred`)) {
2848
- await seal(host, "cloudflare-kv-token", `${CREDS}/cloudflare-kv-token.cred`, cloudflare.kvRuntimeToken);
3827
+ if (cloudflare?.realtime && !host.exists(REALTIME_TICKET_CREDENTIAL)) {
3828
+ await seal(host, "REALTIME_TICKET_SECRET", REALTIME_TICKET_CREDENTIAL, cloudflare.realtime.ticketSecret);
3829
+ }
3830
+ if (cloudflare?.mesh && !host.exists(WARP_CONNECTOR_CREDENTIAL)) {
3831
+ await seal(host, "CF_WARP_CONNECTOR_TOKEN", WARP_CONNECTOR_CREDENTIAL, cloudflare.mesh.connectorToken);
2849
3832
  }
2850
3833
  await host.installAgent(config);
2851
3834
  if (config.kind === "platform" && config.firewall.enabled) {
@@ -2853,50 +3836,52 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2853
3836
  } else
2854
3837
  await host.ensureSoftware(plan.software);
2855
3838
  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");
3839
+ await checked2(host, ["ufw", "--force", "default", "deny", "incoming"], "firewall inbound policy");
3840
+ await checked2(host, ["ufw", "--force", "default", "allow", "outgoing"], "firewall outbound policy");
3841
+ await checked2(host, ["ufw", "allow", `${config.firewall.sshPort}/tcp`], "firewall SSH rule");
2859
3842
  for (const cidr of config.firewall.privateCidrs) {
2860
3843
  if (config.database.role !== "none")
2861
- await checked(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
3844
+ await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", "8528:8539", "proto", "tcp"], "database firewall rule");
2862
3845
  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");
3846
+ await checked2(host, ["ufw", "allow", "from", cidr, "to", "any", "port", String(port), "proto", "tcp"], "seed-mesh firewall rule");
2864
3847
  }
2865
- await checked(host, ["ufw", "--force", "enable"], "firewall activation");
3848
+ await checked2(host, ["ufw", "--force", "enable"], "firewall activation");
2866
3849
  await host.ensureSoftware(plan.software.filter(({ id }) => id !== "ufw"));
2867
3850
  }
2868
3851
  if (config.kind === "platform") {
2869
- const root = privateFile(host, config.database.bootstrapSecretFile, "database bootstrap secret");
3852
+ const root = platformPrivate.root;
2870
3853
  if (!host.exists(JWT_CREDENTIAL)) {
2871
3854
  await seal(host, "arangodb-jwt", JWT_CREDENTIAL, derive(root, "forgezero/cluster/arangodb-jwt/v1"));
2872
3855
  }
2873
3856
  await seal(host, "seed-sync-root", SEED_CREDENTIAL, derive(root, "forgezero/cluster/seed-mesh/v1"));
2874
3857
  await seal(host, "backup-recovery-root", BACKUP_RECOVERY_CREDENTIAL, derive(root, "forgezero/backup/recovery-root/v1"));
2875
3858
  const credentialFiles = config.runtime.credentialFiles ?? {};
3859
+ const emailCredentialName = config.runtime.environment.email?.provider === "smtp" ? "bootstrap-smtp-password" : config.runtime.environment.email?.provider === "jetemail" ? "bootstrap-jetemail-api-key" : undefined;
3860
+ if (Boolean(config.runtime.environment.email) !== Boolean(credentialFiles.emailSecret)) {
3861
+ throw new Error("Bootstrap email configuration and its owner-only credential file must be supplied together");
3862
+ }
2876
3863
  for (const [name, source] of Object.entries({
2877
- "bootstrap-smtp-password": credentialFiles.smtpPassword,
2878
- "backup-s3-secret": credentialFiles.backupS3Secret
3864
+ ...emailCredentialName ? { [emailCredentialName]: platformPrivate.email } : {},
3865
+ "backup-s3-secret": platformPrivate.backup
2879
3866
  })) {
2880
3867
  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);
3868
+ const destination2 = `${CREDS}/${name}.cred`;
3869
+ if (!host.exists(destination2)) {
3870
+ await seal(host, name, destination2, source);
3871
+ const sourcePath = name === "backup-s3-secret" ? credentialFiles.backupS3Secret : credentialFiles.emailSecret;
3872
+ if (sourcePath)
3873
+ host.remove(sourcePath);
2885
3874
  }
2886
3875
  }
2887
3876
  }
2888
- if (cloudflare) {
2889
- if (cloudflare.privateNetworkRuntimeToken)
2890
- await seal(host, "cloudflare-network-token", `${CREDS}/cloudflare-network-token.cred`, cloudflare.privateNetworkRuntimeToken);
2891
- }
2892
3877
  const runtime = config.runtime;
3878
+ await ensureForgeZeroOtelCollector(host, config.telemetryEndpoint);
2893
3879
  const cloudflareConfigured = Boolean(runtime.environment.cloudflare);
2894
- const cloudflareNetworkConfigured = Boolean(runtime.environment.cloudflare?.warp);
2895
3880
  const envPath = `${runtime.environment.sharedDirectory}/.env`;
2896
3881
  const credentials = platformApiCredentialSpecs({
2897
- smtp: Boolean(credentialFiles.smtpPassword),
3882
+ emailProvider: runtime.environment.email?.provider,
2898
3883
  cloudflareKv: cloudflareConfigured,
2899
- cloudflareNetwork: cloudflareNetworkConfigured
3884
+ realtime: Boolean(runtime.environment.realtime)
2900
3885
  });
2901
3886
  const units = renderPlatformApiUnits({
2902
3887
  serviceUser: runtime.serviceUser,
@@ -2924,8 +3909,8 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2924
3909
  keepReleases: runtime.keepReleases,
2925
3910
  drainDeadlineMs: runtime.environment.drainDeadlineMs
2926
3911
  });
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");
3912
+ await checked2(host, ["useradd", "--system", "--no-create-home", "--shell", "/usr/sbin/nologin", runtime.serviceUser], "API service account").catch(async () => {
3913
+ await checked2(host, ["id", runtime.serviceUser], "existing API service account");
2929
3914
  });
2930
3915
  host.mkdir(runtime.environment.sharedDirectory, 488);
2931
3916
  host.mkdir(runtime.slotsDirectory, 493);
@@ -2934,39 +3919,39 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2934
3919
  host.write("/etc/forgezero/capacity.env", `FZ_CONCURRENCY_LIMIT=${runtime.environment.concurrencyLimit}
2935
3920
  `, 420);
2936
3921
  }
2937
- await checked(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
3922
+ await checked2(host, ["chown", `root:${runtime.serviceUser}`, runtime.environment.sharedDirectory, envPath], "runtime ownership");
2938
3923
  host.write("/etc/systemd/system/forgezero@.service", units.template, 420);
2939
3924
  host.write("/etc/systemd/system/forgezero@blue.service.d/port.conf", units.dropIns.blue, 420);
2940
3925
  host.write("/etc/systemd/system/forgezero@green.service.d/port.conf", units.dropIns.green, 420);
2941
3926
  host.write("/etc/nginx/conf.d/forgezero-upstream.conf", edge.upstream, 420);
2942
3927
  host.write("/etc/nginx/conf.d/forgezero.conf", edge.site, 420);
2943
3928
  host.write("/etc/forgezero/deploy.env", activation.environment, 420);
2944
- host.write("/usr/local/libexec/forgezero-activate", activation.helper, 493);
3929
+ host.write("/etc/forgezero/deploy-activation.json", activation.helper, 384);
2945
3930
  host.write("/etc/sudoers.d/forgezero-runner", activation.sudoers, 288);
2946
- await checked(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
3931
+ await checked2(host, ["visudo", "-cf", "/etc/sudoers.d/forgezero-runner"], "activation sudo policy");
2947
3932
  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();
3933
+ await checked2(host, telemetry.unitCheck.argv, "OTLP collector supervision");
3934
+ const otlpStatus = (await checked2(host, [telemetry.receiverCheck.command, ...telemetry.receiverCheck.argv], "OTLP receiver")).trim();
2950
3935
  if (!/^2\d\d$/.test(otlpStatus))
2951
3936
  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");
3937
+ await checked2(host, ["nginx", "-t"], "nginx configuration");
3938
+ await checked2(host, ["systemctl", "daemon-reload"], "systemd reload");
3939
+ await checked2(host, ["systemctl", "enable", "--now", "nginx.service"], "nginx supervision");
2955
3940
  if (config.database.role === "master") {
2956
3941
  const invite = `${runtime.environment.sharedDirectory}/platform-invite.token`;
2957
3942
  if (!host.exists(invite)) {
2958
- host.write(invite, `plt_${randomBytes(24).toString("hex")}
3943
+ host.write(invite, `plt_${randomBytes3(24).toString("hex")}
2959
3944
  `, 384);
2960
- await checked(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
3945
+ await checked2(host, ["chown", `${runtime.serviceUser}:${runtime.serviceUser}`, invite], "platform invite ownership");
2961
3946
  }
2962
3947
  }
2963
3948
  if (config.database.role !== "none") {
2964
3949
  host.mkdir("/var/lib/forgezero-cluster", 448);
2965
- await checked(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
3950
+ await checked2(host, ["chown", "arangodb:arangodb", "/var/lib/forgezero-cluster"], "database state ownership");
2966
3951
  host.write("/etc/systemd/system/forgezero-db.service", databaseUnit(config), 420);
2967
3952
  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");
3953
+ await checked2(host, ["systemctl", "daemon-reload"], "database unit reload");
3954
+ await checked2(host, ["systemctl", "enable", "--now", "forgezero-db.service", "forgezero-db-verify.service"], "database supervision");
2970
3955
  const evidence = {
2971
3956
  expectedMode: "default",
2972
3957
  role: "COORDINATOR",
@@ -2984,7 +3969,7 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
2984
3969
  host.write(PLATFORM_ENROL_SOURCE, `${enrolToken}
2985
3970
  `, 384);
2986
3971
  }
2987
- await checked(host, [
3972
+ await checked2(host, [
2988
3973
  "runuser",
2989
3974
  "-u",
2990
3975
  "forgezero-agent",
@@ -3001,18 +3986,25 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3001
3986
  }
3002
3987
  if (config.cloudflareHandoff) {
3003
3988
  if (!host.exists(TUNNEL_CREDENTIAL) && cloudflare) {
3004
- await seal(host, "cloudflared-token", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3989
+ await seal(host, "CF_TUNNEL_CONNECTOR_TOKEN", TUNNEL_CREDENTIAL, cloudflare.connectorToken);
3005
3990
  }
3006
3991
  if (!host.exists(TUNNEL_CREDENTIAL))
3007
3992
  throw new Error("sealed cloudflared connector credential is missing");
3008
3993
  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");
3994
+ await checked2(host, ["systemctl", "daemon-reload"], "cloudflared unit reload");
3995
+ await checked2(host, ["systemctl", "enable", "--now", "cloudflared.service"], "cloudflared connector supervision");
3011
3996
  const tunnelId = cloudflare?.tunnelId ?? installed?.cloudflareTunnelId ?? (config.kind === "platform" ? config.runtime.environment.cloudflare?.tunnelId : undefined);
3012
3997
  if (!tunnelId)
3013
3998
  throw new Error("Cloudflare tunnel identity is missing after handoff validation");
3014
3999
  await waitForCloudflaredTunnel(host, tunnelId);
3015
4000
  }
4001
+ if (config.installWarp) {
4002
+ if (!host.exists(WARP_CONNECTOR_CREDENTIAL))
4003
+ throw new Error("sealed Cloudflare Mesh connector credential is missing");
4004
+ host.write("/etc/systemd/system/forgezero-mesh-config.service", meshConnectorUnit(), 420);
4005
+ await checked2(host, ["systemctl", "daemon-reload"], "Cloudflare Mesh unit reload");
4006
+ await checked2(host, ["systemctl", "enable", "--now", "warp-svc.service", "forgezero-mesh-config.service"], "Cloudflare Mesh connector supervision");
4007
+ }
3016
4008
  host.write(STATE_PATH, stateFor(config, cloudflare, installed?.cloudflareTunnelId), 384);
3017
4009
  const status = await bootstrapStatus(host);
3018
4010
  if (!status.initialized)
@@ -3030,7 +4022,10 @@ async function applyBootstrap(input, host = localBootstrapHost()) {
3030
4022
  if (!/^plt_[a-f0-9]{48}$/.test(token))
3031
4023
  throw new Error("platform invite is malformed");
3032
4024
  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)}`,
4025
+ executable: "/usr/local/bin/fz",
4026
+ argv: ["genesis", "--mode", "1-of-1", "--api", config.apiUrl, "--app", config.runtime.environment.appOrigin],
4027
+ environment: { FZ_SHARED_DIR: config.runtime.environment.sharedDirectory },
4028
+ runAs: config.runtime.serviceUser,
3034
4029
  inviteUrl: `${config.runtime.environment.appOrigin}/invite?token=${encodeURIComponent(token)}`
3035
4030
  };
3036
4031
  }
@@ -3062,6 +4057,7 @@ function strictBootstrapDocument(value) {
3062
4057
  "runtime",
3063
4058
  "firewall",
3064
4059
  "installCloudflared",
4060
+ "installWarp",
3065
4061
  "cloudflareHandoff",
3066
4062
  "realm",
3067
4063
  "enrolTokenFile",
@@ -3111,46 +4107,53 @@ function strictBootstrapDocument(value) {
3111
4107
  "otlpCollectorUnit",
3112
4108
  "agentOtlpEndpoint",
3113
4109
  "custodianEmail",
3114
- "smtp",
4110
+ "email",
3115
4111
  "repository",
3116
4112
  "branch",
3117
4113
  "deployProfile",
3118
4114
  "otlpFlushIntervalMs",
3119
4115
  "otlpTraceSampleRatio",
3120
4116
  "backup",
3121
- "cloudflare"
4117
+ "cloudflare",
4118
+ "realtime"
3122
4119
  ], "runtime environment");
3123
4120
  if (runtime.credentialFiles !== undefined)
3124
- exactKeys(runtime.credentialFiles, ["smtpPassword", "backupS3Secret"], "runtime credential files");
4121
+ exactKeys(runtime.credentialFiles, ["emailSecret", "backupS3Secret"], "runtime credential files");
3125
4122
  const environment = runtime.environment;
3126
- if (environment.smtp !== undefined)
3127
- exactKeys(environment.smtp, ["host", "port", "user", "from"], "SMTP config");
4123
+ if (environment.email !== undefined) {
4124
+ const email = exactKeys(environment.email, ["provider", "host", "port", "user", "from", "eu"], "email config");
4125
+ if (email.provider === "smtp")
4126
+ exactKeys(email, ["provider", "host", "port", "user", "from"], "SMTP config");
4127
+ else if (email.provider === "jetemail")
4128
+ exactKeys(email, ["provider", "from", "eu"], "JetEmail config");
4129
+ else
4130
+ throw new Error("email provider must be smtp or jetemail");
4131
+ }
3128
4132
  if (environment.backup !== undefined)
3129
4133
  exactKeys(environment.backup, ["endpoint", "region", "bucket", "accessKeyId"], "backup config");
3130
4134
  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") {
4135
+ exactKeys(environment.cloudflare, ["accountId", "zoneId", "kvNamespaceId", "tunnelId", "tunnelService"], "Cloudflare runtime config");
4136
+ if (environment.realtime !== undefined)
4137
+ exactKeys(environment.realtime, ["workerScriptName", "endpoint", "producer"], "realtime runtime config");
4138
+ } else if (root.kind === "enrolled-compute") {
3136
4139
  if (root.cloudflareHandoff !== undefined)
3137
4140
  exactKeys(root.cloudflareHandoff, ["handoffFile", "nodeName"], "Cloudflare handoff");
3138
4141
  if (root.bootstrapRunner !== undefined)
3139
4142
  exactKeys(root.bootstrapRunner, ["sshPrivateKeyFile", "targetTelemetryEndpoint"], "bootstrap runner config");
3140
4143
  for (const key of ["environment", "profile", "computeReference", "database", "enrolment", "runtime"]) {
3141
4144
  if (root[key] !== undefined && key !== "profile")
3142
- throw new Error(`tenant bootstrap cannot contain ${key}`);
4145
+ throw new Error(`enrolled-compute activation cannot contain ${key}`);
3143
4146
  }
3144
4147
  } else
3145
- throw new Error("bootstrap config kind must be platform or tenant");
4148
+ throw new Error("bootstrap config kind must be platform or enrolled-compute");
3146
4149
  return value;
3147
4150
  }
3148
4151
  function readBootstrapConfig(path) {
3149
- const metadata = lstatSync(path);
4152
+ const metadata = lstatSync2(path);
3150
4153
  if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.uid !== (process.getuid?.() ?? metadata.uid) || metadata.nlink !== 1 || (metadata.mode & 63) !== 0 || metadata.size > 64 * 1024) {
3151
4154
  throw new Error("bootstrap config must be an owner-only regular file with one link and at most 64 KiB");
3152
4155
  }
3153
- return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync(path, "utf8"))));
4156
+ return validateBootstrapConfig(strictBootstrapDocument(JSON.parse(readFileSync3(path, "utf8"))));
3154
4157
  }
3155
4158
  function localBootstrapHost() {
3156
4159
  const execute = async (argv, options = {}) => {
@@ -3168,19 +4171,19 @@ function localBootstrapHost() {
3168
4171
  };
3169
4172
  return {
3170
4173
  uid: () => process.getuid?.() ?? -1,
3171
- exists: existsSync,
3172
- read: (path) => readFileSync(path, "utf8"),
4174
+ exists: existsSync3,
4175
+ read: (path) => readFileSync3(path, "utf8"),
3173
4176
  write(path, content, mode) {
3174
- mkdirSync(dirname2(path), { recursive: true, mode: 493 });
4177
+ mkdirSync3(dirname4(path), { recursive: true, mode: 493 });
3175
4178
  const temporary = `${path}.next.${process.pid}`;
3176
- writeFileSync(temporary, content, { mode });
3177
- chmodSync(temporary, mode);
3178
- renameSync(temporary, path);
4179
+ writeFileSync3(temporary, content, { mode });
4180
+ chmodSync2(temporary, mode);
4181
+ renameSync3(temporary, path);
3179
4182
  },
3180
- mkdir: (path, mode) => mkdirSync(path, { recursive: true, mode }),
3181
- remove: (path) => rmSync(path, { force: true }),
4183
+ mkdir: (path, mode) => mkdirSync3(path, { recursive: true, mode }),
4184
+ remove: (path) => rmSync3(path, { force: true }),
3182
4185
  inspect(path) {
3183
- const value = lstatSync(path);
4186
+ const value = lstatSync2(path);
3184
4187
  return { regular: value.isFile(), symbolic: value.isSymbolicLink(), uid: value.uid, mode: value.mode, links: value.nlink, size: value.size };
3185
4188
  },
3186
4189
  exec: execute,
@@ -3202,6 +4205,7 @@ function localBootstrapHost() {
3202
4205
  async installAgent(config, enrolTokenSourcePath) {
3203
4206
  const capabilities = await readCapabilities(localRunner);
3204
4207
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
4208
+ const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync3("/var/lib/forgezero/enrolment.json");
3205
4209
  if (config.kind === "platform") {
3206
4210
  const lifecycle = config.database.role === "none" ? {
3207
4211
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -3213,8 +4217,8 @@ function localBootstrapHost() {
3213
4217
  databaseHealthUrl: `http://${config.database.address}:8529/_api/version`,
3214
4218
  databasePorts: [8529]
3215
4219
  };
3216
- mkdirSync(dirname2(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
3217
- writeFileSync(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
4220
+ mkdirSync3(dirname4(LIFECYCLE_PROFILE), { recursive: true, mode: 493 });
4221
+ writeFileSync3(LIFECYCLE_PROFILE, `${JSON.stringify(lifecycle, null, 2)}
3218
4222
  `, { mode: 256 });
3219
4223
  }
3220
4224
  const plan = planInstall({
@@ -3231,39 +4235,32 @@ function localBootstrapHost() {
3231
4235
  gitCredentialPath: "/etc/forgezero/creds/git-deploy-key.cred",
3232
4236
  gitPublicKeyPath: "/etc/forgezero/git/deploy.pub",
3233
4237
  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,
4238
+ pullDeployments: hasBinding,
4239
+ pullMigrations: config.kind === "platform" && hasBinding,
4240
+ pullBootstrap: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && Boolean(config.bootstrapRunner),
4241
+ bootstrapSshCredentialPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_CREDENTIAL : undefined,
4242
+ bootstrapSshSourcePath: config.kind === "enrolled-compute" ? config.bootstrapRunner?.sshPrivateKeyFile : undefined,
4243
+ bootstrapSshPublicKeyPath: platformBootstrapRunner(config) || config.kind === "enrolled-compute" && config.bootstrapRunner ? BOOTSTRAP_SSH_PUBLIC_KEY : undefined,
4244
+ bootstrapTargetTelemetryEndpoint: platformBootstrapRunner(config) ? config.telemetryEndpoint : config.kind === "enrolled-compute" ? config.bootstrapRunner?.targetTelemetryEndpoint : undefined,
3239
4245
  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
4246
  enforceEgress: true,
3250
4247
  nodeHostname: config.nodeHostname,
3251
4248
  telemetryEndpoint: config.telemetryEndpoint,
3252
4249
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
3253
4250
  sourceBinPath: PACKAGED_AGENT_BIN,
3254
- ...config.kind === "tenant" || enrolTokenSourcePath ? {
3255
- enrolTokenSourcePath: config.kind === "tenant" ? config.enrolTokenFile : enrolTokenSourcePath,
4251
+ ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
4252
+ enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
3256
4253
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
3257
4254
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
3258
4255
  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
4256
+ project: config.kind === "enrolled-compute" ? config.realm : "platform",
4257
+ environment: config.kind === "enrolled-compute" ? undefined : config.environment,
4258
+ nodeLabel: config.kind === "enrolled-compute" ? config.nodeHostname : config.computeReference
3262
4259
  } : {}
3263
4260
  });
3264
4261
  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 });
4262
+ mkdirSync3(dirname4(unit.path), { recursive: true, mode: 493 });
4263
+ writeFileSync3(unit.path, unit.unit, { mode: 420 });
3267
4264
  }
3268
4265
  await applyPlan(plan, localRunner);
3269
4266
  return plan;