@forgezero/agent 0.1.57 → 0.1.59

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.
@@ -22,7 +22,7 @@ function privateDatabaseHostRoute(value) {
22
22
  return `${address}/${isIP(address) === 4 ? 32 : 128}`;
23
23
  }
24
24
  var endpoint = "https://api.cloudflare.com/client/v4";
25
- async function cf(config, path, init = {}, fetcher = fetch) {
25
+ async function cfEnvelope(config, path, init = {}, fetcher = fetch) {
26
26
  const response = await fetcher(`${endpoint}${path}`, {
27
27
  ...init,
28
28
  headers: {
@@ -35,7 +35,68 @@ async function cf(config, path, init = {}, fetcher = fetch) {
35
35
  if (!response.ok || body.success !== true) {
36
36
  throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
37
37
  }
38
- return body.result;
38
+ return body;
39
+ }
40
+ async function cf(config, path, init = {}, fetcher = fetch) {
41
+ return (await cfEnvelope(config, path, init, fetcher)).result;
42
+ }
43
+ async function cfPages(config, path, perPage, fetcher) {
44
+ const output = [];
45
+ for (let page = 1;page <= 100; page += 1) {
46
+ const separator = path.includes("?") ? "&" : "?";
47
+ const envelope = await cfEnvelope(config, `${path}${separator}page=${page}&per_page=${perPage}`, {}, fetcher);
48
+ const result = Array.isArray(envelope.result) ? envelope.result : [];
49
+ output.push(...result);
50
+ const totalPages = envelope.result_info?.total_pages;
51
+ if (Number.isInteger(totalPages) ? page >= totalPages : result.length < perPage)
52
+ return output;
53
+ }
54
+ throw new Error("Cloudflare pagination exceeded the reviewed 100-page bound");
55
+ }
56
+ var exactHexId = (value, label) => {
57
+ const normalized = String(value ?? "").trim().toLowerCase();
58
+ if (!/^[a-f0-9]{32}$/.test(normalized))
59
+ throw new Error(`${label} is malformed`);
60
+ return normalized;
61
+ };
62
+ async function discoverCloudflareBootstrapResources(config, fetcher = fetch) {
63
+ const zoneName = config.zoneName.trim().toLowerCase().replace(/\.$/, "");
64
+ const kvNamespaceTitle = config.kvNamespaceTitle.trim();
65
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(zoneName)) {
66
+ throw new Error("Cloudflare zone name is invalid");
67
+ }
68
+ if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
69
+ throw new Error("Cloudflare KV namespace title is invalid");
70
+ }
71
+ const [managementStatus, runtimeStatus] = await Promise.all([
72
+ cf({ apiToken: config.tunnelToken }, "/user/tokens/verify", {}, fetcher),
73
+ cf({ apiToken: config.apiToken }, "/user/tokens/verify", {}, fetcher)
74
+ ]);
75
+ if (managementStatus.status !== "active")
76
+ throw new Error("CF_TUNNEL_TOKEN is not active");
77
+ if (runtimeStatus.status !== "active")
78
+ throw new Error("CF_API_TOKEN is not active");
79
+ const zones = await cfPages({ apiToken: config.tunnelToken }, `/zones?name=${encodeURIComponent(zoneName)}&match=all&status=active`, 50, fetcher);
80
+ const matchingZones = zones.filter(({ name }) => name?.trim().toLowerCase() === zoneName);
81
+ if (matchingZones.length !== 1) {
82
+ throw new Error(`Cloudflare zone ${zoneName} must resolve to exactly one active zone`);
83
+ }
84
+ const zoneId = exactHexId(matchingZones[0].id, "Cloudflare zone id");
85
+ const accountId = exactHexId(matchingZones[0].account?.id, "Cloudflare account id");
86
+ const namespaces = await cfPages({ apiToken: config.apiToken }, `/accounts/${accountId}/storage/kv/namespaces?order=title&direction=asc`, 1000, fetcher);
87
+ const matchingNamespaces = namespaces.filter(({ title }) => title === kvNamespaceTitle);
88
+ if (matchingNamespaces.length !== 1) {
89
+ throw new Error(`Cloudflare KV namespace ${kvNamespaceTitle} must resolve to exactly one namespace`);
90
+ }
91
+ const kvNamespaceId = exactHexId(matchingNamespaces[0].id, "Cloudflare KV namespace id");
92
+ if (config.workerScriptName) {
93
+ await verifyCloudflareWorkerDurableObjects({
94
+ accountId,
95
+ scriptName: config.workerScriptName,
96
+ apiToken: config.apiToken
97
+ }, fetcher);
98
+ }
99
+ return { accountId, zoneId, kvNamespaceId };
39
100
  }
40
101
  async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
41
102
  const [address, prefixText, ...extra] = config.network.split("/");
@@ -211,7 +272,7 @@ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
211
272
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
212
273
  throw new Error("Cloudflare Worker script name is invalid");
213
274
  }
214
- const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
275
+ const namespaces = await cfPages(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces`, 1000, fetcher);
215
276
  const owned = namespaces.filter(({ script }) => script === config.scriptName);
216
277
  if (!owned.length)
217
278
  throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
@@ -283,8 +344,21 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
283
344
  }
284
345
  return { tunnel, connectorToken, created };
285
346
  }
347
+ async function retrieveCloudflareConnectorTokens(config, fetcher = fetch) {
348
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId) || !/^[0-9a-f-]{36}$/i.test(config.tunnelId) || config.meshConnectorId && !/^[0-9a-f-]{36}$/i.test(config.meshConnectorId)) {
349
+ throw new Error("Cloudflare connector retrieval coordinates are invalid");
350
+ }
351
+ const connectorToken = await cf(config, `/accounts/${config.accountId}/cfd_tunnel/${encodeURIComponent(config.tunnelId)}/token`, {}, fetcher);
352
+ const meshConnectorToken = config.meshConnectorId ? await cf(config, `/accounts/${config.accountId}/warp_connector/${encodeURIComponent(config.meshConnectorId)}/token`, {}, fetcher) : undefined;
353
+ for (const value of [connectorToken, meshConnectorToken]) {
354
+ if (value !== undefined && (!value || value.length > 16384))
355
+ throw new Error("Cloudflare returned an invalid connector token");
356
+ }
357
+ return { connectorToken, ...meshConnectorToken ? { meshConnectorToken } : {} };
358
+ }
286
359
  export {
287
360
  verifyCloudflareWorkerDurableObjects,
361
+ retrieveCloudflareConnectorTokens,
288
362
  removeCloudflareWarpDatabaseInclude,
289
363
  removeCloudflarePrivateRoute,
290
364
  removeCloudflarePrivateDatabaseRoute,
@@ -294,6 +368,7 @@ export {
294
368
  ensureCloudflarePrivateRoute,
295
369
  ensureCloudflarePrivateDatabaseRoute,
296
370
  ensureCloudflareMeshConnector,
371
+ discoverCloudflareBootstrapResources,
297
372
  configureCloudflareRealtimeSecrets,
298
373
  configureCloudflareEdge
299
374
  };
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * One credential policy for every place the Agent can run.
3
3
  *
4
- * Operator credentials are attended input files and are never installed as a
5
- * runtime source. Physical-metal credentials are fixed host identities loaded
4
+ * Operator credentials are hidden attended input and are never persisted as a
5
+ * plaintext source. Physical-metal credentials are fixed host identities loaded
6
6
  * only by systemd. Enrolled platform and tenant computes read their assigned
7
7
  * project/environment Vault replica first and may use only an explicitly
8
8
  * installed, same-name systemd credential while that replica cannot answer.
@@ -34,13 +34,13 @@ export declare const CLOUDFLARE_CREDENTIAL_NAMES: {
34
34
  export declare const CLOUDFLARE_CREDENTIAL_SCHEMA: {
35
35
  readonly CF_TUNNEL_TOKEN: {
36
36
  readonly permissions: readonly ["Account:Cloudflare Tunnel Write", "Account:Cloudflare One Connector: WARP Write", "Account:Cloudflare One Networks Write", "Account:Zero Trust Write", "Zone:DNS Write"];
37
- readonly platformBootstrap: "attended-file";
38
- readonly platformRuntime: "vault";
37
+ readonly platformBootstrap: "hidden-prompt-to-systemd";
38
+ readonly platformRuntime: "vault-then-systemd";
39
39
  readonly tenantControl: "vault";
40
40
  };
41
41
  readonly CF_API_TOKEN: {
42
42
  readonly permissions: readonly ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"];
43
- readonly platformBootstrap: "attended-file";
43
+ readonly platformBootstrap: "hidden-prompt-to-systemd";
44
44
  readonly platformRuntime: "vault-then-systemd";
45
45
  readonly tenantControl: "vault";
46
46
  };
@@ -67,22 +67,22 @@ export declare const AGENT_CREDENTIAL_POLICY: {
67
67
  readonly operator: {
68
68
  readonly vault: false;
69
69
  readonly systemdFallback: false;
70
- readonly attendedFile: true;
70
+ readonly hiddenInput: true;
71
71
  };
72
72
  readonly metal: {
73
73
  readonly vault: false;
74
74
  readonly systemdFallback: true;
75
- readonly attendedFile: false;
75
+ readonly hiddenInput: false;
76
76
  };
77
77
  readonly 'platform-compute': {
78
78
  readonly vault: true;
79
79
  readonly systemdFallback: true;
80
- readonly attendedFile: false;
80
+ readonly hiddenInput: false;
81
81
  };
82
82
  readonly 'tenant-compute': {
83
83
  readonly vault: true;
84
84
  readonly systemdFallback: true;
85
- readonly attendedFile: false;
85
+ readonly hiddenInput: false;
86
86
  };
87
87
  };
88
88
  export interface DeploymentCredentialBinding {
@@ -300,13 +300,13 @@ var CLOUDFLARE_CREDENTIAL_SCHEMA = {
300
300
  "Account:Zero Trust Write",
301
301
  "Zone:DNS Write"
302
302
  ],
303
- platformBootstrap: "attended-file",
304
- platformRuntime: "vault",
303
+ platformBootstrap: "hidden-prompt-to-systemd",
304
+ platformRuntime: "vault-then-systemd",
305
305
  tenantControl: "vault"
306
306
  },
307
307
  CF_API_TOKEN: {
308
308
  permissions: ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"],
309
- platformBootstrap: "attended-file",
309
+ platformBootstrap: "hidden-prompt-to-systemd",
310
310
  platformRuntime: "vault-then-systemd",
311
311
  tenantControl: "vault"
312
312
  },
@@ -330,10 +330,10 @@ var CLOUDFLARE_CREDENTIAL_SCHEMA = {
330
330
  }
331
331
  };
332
332
  var AGENT_CREDENTIAL_POLICY = {
333
- operator: { vault: false, systemdFallback: false, attendedFile: true },
334
- metal: { vault: false, systemdFallback: true, attendedFile: false },
335
- "platform-compute": { vault: true, systemdFallback: true, attendedFile: false },
336
- "tenant-compute": { vault: true, systemdFallback: true, attendedFile: false }
333
+ operator: { vault: false, systemdFallback: false, hiddenInput: true },
334
+ metal: { vault: false, systemdFallback: true, hiddenInput: false },
335
+ "platform-compute": { vault: true, systemdFallback: true, hiddenInput: false },
336
+ "tenant-compute": { vault: true, systemdFallback: true, hiddenInput: false }
337
337
  };
338
338
  var CREDENTIAL_NAME = /^[A-Z_][A-Z0-9_]*$/;
339
339
  var SCOPE_PART2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
package/dist/fz-agent.js CHANGED
@@ -8448,7 +8448,7 @@ function assertSupportedGuestImage(imageKey) {
8448
8448
  }
8449
8449
 
8450
8450
  // src/version.ts
8451
- var VERSION2 = "0.1.57";
8451
+ var VERSION2 = "0.1.59";
8452
8452
 
8453
8453
  // src/ssh-bootstrap.ts
8454
8454
  class SshBootstrapError extends Error {
@@ -8583,7 +8583,7 @@ var verifiedBunArchive = async (directory) => {
8583
8583
  writeFileSync4(archive, bytes, { mode: 384, flag: "wx" });
8584
8584
  return archive;
8585
8585
  };
8586
- var remote = (exec, ssh, destination, code, argv2, secret = false) => checked(exec, ["ssh", ...ssh, destination, "--", ...argv2], code, { secret });
8586
+ var remote = (exec, ssh, destination, code, argv2, secret = false, stdin) => checked(exec, ["ssh", ...ssh, destination, "--", ...argv2], code, { secret, ...stdin !== undefined ? { stdin } : {} });
8587
8587
  async function executeSshBootstrap(claim, options) {
8588
8588
  validateClaim(claim);
8589
8589
  if (!options.sshKeyPath.startsWith("/") || /[\r\n]/.test(options.sshKeyPath))
@@ -8622,14 +8622,11 @@ async function executeSshBootstrap(claim, options) {
8622
8622
  realm: claim.realm,
8623
8623
  nodeHostname: claim.target.nodeHostname,
8624
8624
  telemetryEndpoint: telemetry.origin,
8625
- enrolTokenFile: "/run/forgezero-bootstrap/enrol.token",
8626
8625
  profile: "app",
8627
8626
  software: [{ id: "bun", version: PINNED_BUN_VERSION }]
8628
8627
  };
8629
8628
  const files = {
8630
8629
  "config.json": `${JSON.stringify(config, null, 2)}
8631
- `,
8632
- "enrol.token": `${claim.enrolmentToken}
8633
8630
  `
8634
8631
  };
8635
8632
  for (const [name, content] of Object.entries(files))
@@ -8753,7 +8750,7 @@ async function executeSshBootstrap(claim, options) {
8753
8750
  "/usr/local/lib/forgezero/agent/fz.js",
8754
8751
  "/usr/local/bin/fz"
8755
8752
  ]);
8756
- for (const name of ["config.json", "enrol.token"])
8753
+ for (const name of ["config.json"])
8757
8754
  await remote(exec, ssh, destination, "REMOTE_CONFIG_INSTALL_FAILED", [
8758
8755
  "/usr/bin/sudo",
8759
8756
  "-n",
@@ -8769,10 +8766,12 @@ async function executeSshBootstrap(claim, options) {
8769
8766
  "/usr/local/bin/fz",
8770
8767
  "agent",
8771
8768
  "activate",
8769
+ "credentials-stdin",
8772
8770
  "--bootstrap-config",
8773
8771
  "/run/forgezero-bootstrap/config.json",
8774
8772
  "--apply"
8775
- ], true);
8773
+ ], true, `${JSON.stringify({ enrolmentToken: claim.enrolmentToken })}
8774
+ `);
8776
8775
  await remote(exec, ssh, destination, "REMOTE_CLEANUP_FAILED", [
8777
8776
  "/usr/bin/sudo",
8778
8777
  "-n",
@@ -13078,10 +13077,10 @@ var AGENT_CREDENTIAL_LOCATIONS = [
13078
13077
  "tenant-compute"
13079
13078
  ];
13080
13079
  var AGENT_CREDENTIAL_POLICY = {
13081
- operator: { vault: false, systemdFallback: false, attendedFile: true },
13082
- metal: { vault: false, systemdFallback: true, attendedFile: false },
13083
- "platform-compute": { vault: true, systemdFallback: true, attendedFile: false },
13084
- "tenant-compute": { vault: true, systemdFallback: true, attendedFile: false }
13080
+ operator: { vault: false, systemdFallback: false, hiddenInput: true },
13081
+ metal: { vault: false, systemdFallback: true, hiddenInput: false },
13082
+ "platform-compute": { vault: true, systemdFallback: true, hiddenInput: false },
13083
+ "tenant-compute": { vault: true, systemdFallback: true, hiddenInput: false }
13085
13084
  };
13086
13085
  var CREDENTIAL_NAME = /^[A-Z_][A-Z0-9_]*$/;
13087
13086
  var SCOPE_PART2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
@@ -13365,7 +13364,7 @@ async function runRecoveryHost(args, run2 = execute2) {
13365
13364
  nonEmpty("/etc/forgezero/creds/arangodb-jwt.cred");
13366
13365
  const hasDatabase = role !== "none";
13367
13366
  if (hasDatabase) {
13368
- nonEmpty("/etc/forgezero/creds/backup-s3-secret.cred");
13367
+ nonEmpty("/etc/forgezero/creds/backup.s3.secretAccessKey.cred");
13369
13368
  nonEmpty("/etc/forgezero/creds/backup-recovery-root.cred");
13370
13369
  }
13371
13370
  const slot = activeSlot();
@@ -13412,7 +13411,7 @@ async function runRecoveryHost(args, run2 = execute2) {
13412
13411
  "--property=LoadCredentialEncrypted=arangodb-jwt:/etc/forgezero/creds/arangodb-jwt.cred",
13413
13412
  ...args.includes("--fleet-fenced") ? ["--property=Environment=FZ_RECOVERY_FLEET_FENCED=all-api-and-scheduler-replicas-are-stopped"] : [],
13414
13413
  ...args.includes("--backup-credentials") ? [
13415
- "--property=LoadCredentialEncrypted=backup-s3-secret:/etc/forgezero/creds/backup-s3-secret.cred",
13414
+ "--property=LoadCredentialEncrypted=backup.s3.secretAccessKey:/etc/forgezero/creds/backup.s3.secretAccessKey.cred",
13416
13415
  "--property=LoadCredentialEncrypted=backup-recovery-root:/etc/forgezero/creds/backup-recovery-root.cred"
13417
13416
  ] : []
13418
13417
  ];
@@ -14177,10 +14176,11 @@ function planProvision(options) {
14177
14176
  const warpEnabled = warpValues.every(Boolean);
14178
14177
  if (warpValues.some(Boolean) && !warpEnabled)
14179
14178
  throw new Error("WARP configuration must be supplied together");
14180
- const enrolmentEnabled = Boolean(options.enrolTokenSourcePath && options.enrolTokenCredentialPath && options.enrolStatePath);
14181
- if (Boolean(options.enrolTokenSourcePath) !== Boolean(options.enrolTokenCredentialPath) || Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath))
14182
- throw new Error("direct enrolment paths must be supplied together");
14183
- const enrolTokenSourcePath = enrolmentEnabled ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
14179
+ const enrolmentEnabled = Boolean(options.enrolTokenCredentialPath && options.enrolStatePath);
14180
+ if (Boolean(options.enrolTokenCredentialPath) !== Boolean(options.enrolStatePath) || options.enrolTokenSourcePath && !enrolmentEnabled) {
14181
+ throw new Error("direct enrolment credential and state paths must be supplied together");
14182
+ }
14183
+ const enrolTokenSourcePath = options.enrolTokenSourcePath ? systemdPath(options.enrolTokenSourcePath, "enrolment source") : undefined;
14184
14184
  const enrolTokenCredentialPath = enrolmentEnabled ? systemdPath(options.enrolTokenCredentialPath, "enrolment credential") : undefined;
14185
14185
  const enrolStatePath = enrolmentEnabled ? systemdPath(options.enrolStatePath, "enrolment state") : undefined;
14186
14186
  const enrolStateDir = enrolStatePath?.replace(/\/[^/]+$/, "");
@@ -14308,7 +14308,12 @@ function planProvision(options) {
14308
14308
  ] : [],
14309
14309
  ...enrolmentEnabled ? [
14310
14310
  step2("enrolment state directory", { kind: "directories", directories: [{ path: enrolStateDir, mode: 448, owner: user, group: user }] }),
14311
- step2("encrypted one-time enrolment capability", { kind: "ensure-enrolment", state: enrolStatePath, source: enrolTokenSourcePath, credential: enrolTokenCredentialPath })
14311
+ ...enrolTokenSourcePath ? [step2("encrypted one-time enrolment capability", {
14312
+ kind: "ensure-enrolment",
14313
+ state: enrolStatePath,
14314
+ source: enrolTokenSourcePath,
14315
+ credential: enrolTokenCredentialPath
14316
+ })] : []
14312
14317
  ] : [],
14313
14318
  ...deploymentEnabled ? [step2("deployment directories", { kind: "directories", directories: [
14314
14319
  { path: deployRoot, mode: 493, owner: "root", group: "root" },
@@ -14657,6 +14662,7 @@ var JWT_CREDENTIAL = `${CREDS}/arangodb-jwt.cred`;
14657
14662
  var ENROL_CREDENTIAL = `${CREDS}/enrol-token.cred`;
14658
14663
  var TUNNEL_CREDENTIAL = `${CREDS}/CF_TUNNEL_CONNECTOR_TOKEN.cred`;
14659
14664
  var CF_API_CREDENTIAL = `${CREDS}/CF_API_TOKEN.cred`;
14665
+ var CF_TUNNEL_API_CREDENTIAL = `${CREDS}/CF_TUNNEL_TOKEN.cred`;
14660
14666
  var WARP_CONNECTOR_CREDENTIAL = `${CREDS}/CF_WARP_CONNECTOR_TOKEN.cred`;
14661
14667
  var REALTIME_PUBLISH_CREDENTIAL = `${CREDS}/REALTIME_PUBLISH_SECRET.cred`;
14662
14668
  var REALTIME_TICKET_CREDENTIAL = `${CREDS}/REALTIME_TICKET_SECRET.cred`;
@@ -14887,10 +14893,10 @@ function localBootstrapHost() {
14887
14893
  throw new Error(`Agent software requirements failed: ${result.output.trim()}`);
14888
14894
  return result;
14889
14895
  },
14890
- async installAgent(config, enrolTokenSourcePath) {
14896
+ async installAgent(config) {
14891
14897
  const capabilities = await readCapabilities(localRunner);
14892
14898
  const deployRoot = config.deployRoot ?? "/opt/forgezero";
14893
- const hasBinding = config.kind === "enrolled-compute" || Boolean(enrolTokenSourcePath) || existsSync19("/var/lib/forgezero/enrolment.json");
14899
+ const hasBinding = config.kind === "enrolled-compute" || existsSync19(ENROL_CREDENTIAL) || existsSync19("/var/lib/forgezero/enrolment.json");
14894
14900
  if (config.kind === "platform") {
14895
14901
  const lifecycle = config.database.role === "none" ? {
14896
14902
  apiUnits: ["forgezero@blue.service", "forgezero@green.service"],
@@ -14933,8 +14939,7 @@ function localBootstrapHost() {
14933
14939
  telemetryEndpoint: config.telemetryEndpoint,
14934
14940
  binPath: "/usr/local/lib/forgezero/agent/fz-agent",
14935
14941
  sourceBinPath: PACKAGED_AGENT_BIN,
14936
- ...config.kind === "enrolled-compute" || enrolTokenSourcePath ? {
14937
- enrolTokenSourcePath: config.kind === "enrolled-compute" ? config.enrolTokenFile : enrolTokenSourcePath,
14942
+ ...hasBinding ? {
14938
14943
  enrolTokenCredentialPath: ENROL_CREDENTIAL,
14939
14944
  enrolStatePath: "/var/lib/forgezero/enrolment.json",
14940
14945
  apiUrl: config.apiUrl,