@forgezero/agent 0.1.57 → 0.1.58

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.
package/README.md CHANGED
@@ -359,13 +359,14 @@ import type { CloudflareBootstrapTokens, CloudflareConnectorHandoff, CloudflareH
359
359
  <a id="forgezero-agent-cloudflare-edge"></a>
360
360
  ## @forgezero/agent/cloudflare-edge
361
361
 
362
- Strict Cloudflare REST operations used by the attended bootstrap controller. Named value imports: configureCloudflareEdge, configureCloudflareRealtimeSecrets, ensureCloudflareMeshConnector, ensureCloudflarePrivateDatabaseRoute, ensureCloudflarePrivateRoute, ensureCloudflareTunnel, ensureCloudflareWarpDatabaseInclude, ensureCloudflareWarpNetworkIncludes, removeCloudflarePrivateDatabaseRoute, removeCloudflarePrivateRoute, removeCloudflareWarpDatabaseInclude, verifyCloudflareWorkerDurableObjects. Named type imports: CloudflareDurableObjectNamespace, CloudflareEdgeConfig, CloudflareMeshConnector, CloudflarePrivateRoute, CloudflarePrivateRouteConfig, CloudflareTunnel, CloudflareWarpIncludeConfig. Import only the names used by this file.
362
+ Strict Cloudflare REST operations used by the attended bootstrap controller. Named value imports: configureCloudflareEdge, configureCloudflareRealtimeSecrets, discoverCloudflareBootstrapResources, ensureCloudflareMeshConnector, ensureCloudflarePrivateDatabaseRoute, ensureCloudflarePrivateRoute, ensureCloudflareTunnel, ensureCloudflareWarpDatabaseInclude, ensureCloudflareWarpNetworkIncludes, removeCloudflarePrivateDatabaseRoute, removeCloudflarePrivateRoute, removeCloudflareWarpDatabaseInclude, verifyCloudflareWorkerDurableObjects. Named type imports: CloudflareBootstrapDiscovery, CloudflareDurableObjectNamespace, CloudflareEdgeConfig, CloudflareMeshConnector, CloudflarePrivateRoute, CloudflarePrivateRouteConfig, CloudflareTunnel, CloudflareWarpIncludeConfig. Import only the names used by this file.
363
363
 
364
364
  ```text
365
- import { configureCloudflareEdge, configureCloudflareRealtimeSecrets, ensureCloudflareMeshConnector, ensureCloudflarePrivateDatabaseRoute, ensureCloudflarePrivateRoute, ensureCloudflareTunnel } from '@forgezero/agent/cloudflare-edge';
366
- import { ensureCloudflareWarpDatabaseInclude, ensureCloudflareWarpNetworkIncludes, removeCloudflarePrivateDatabaseRoute, removeCloudflarePrivateRoute, removeCloudflareWarpDatabaseInclude, verifyCloudflareWorkerDurableObjects } from '@forgezero/agent/cloudflare-edge';
367
- import type { CloudflareDurableObjectNamespace, CloudflareEdgeConfig, CloudflareMeshConnector, CloudflarePrivateRoute, CloudflarePrivateRouteConfig, CloudflareTunnel } from '@forgezero/agent/cloudflare-edge';
368
- import type { CloudflareWarpIncludeConfig } from '@forgezero/agent/cloudflare-edge';
365
+ import { configureCloudflareEdge, configureCloudflareRealtimeSecrets, discoverCloudflareBootstrapResources, ensureCloudflareMeshConnector, ensureCloudflarePrivateDatabaseRoute, ensureCloudflarePrivateRoute } from '@forgezero/agent/cloudflare-edge';
366
+ import { ensureCloudflareTunnel, ensureCloudflareWarpDatabaseInclude, ensureCloudflareWarpNetworkIncludes, removeCloudflarePrivateDatabaseRoute, removeCloudflarePrivateRoute, removeCloudflareWarpDatabaseInclude } from '@forgezero/agent/cloudflare-edge';
367
+ import { verifyCloudflareWorkerDurableObjects } from '@forgezero/agent/cloudflare-edge';
368
+ import type { CloudflareBootstrapDiscovery, CloudflareDurableObjectNamespace, CloudflareEdgeConfig, CloudflareMeshConnector, CloudflarePrivateRoute, CloudflarePrivateRouteConfig } from '@forgezero/agent/cloudflare-edge';
369
+ import type { CloudflareTunnel, CloudflareWarpIncludeConfig } from '@forgezero/agent/cloudflare-edge';
369
370
  ```
370
371
 
371
372
  <a id="forgezero-agent-mesh-connector"></a>
@@ -749,7 +749,7 @@ async function postSignedNode(options, path, body) {
749
749
  }
750
750
 
751
751
  // src/version.ts
752
- var VERSION3 = "0.1.57";
752
+ var VERSION3 = "0.1.58";
753
753
 
754
754
  // src/agent-heartbeat.ts
755
755
  var unquote = (value) => value.replace(/^['"]|['"]$/g, "");
package/dist/bootstrap.js CHANGED
@@ -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`);
@@ -1207,7 +1268,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1207
1268
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1208
1269
 
1209
1270
  // src/version.ts
1210
- var VERSION = "0.1.57";
1271
+ var VERSION = "0.1.58";
1211
1272
 
1212
1273
  // src/software.ts
1213
1274
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1,4 +1,5 @@
1
1
  import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokenFiles } from '../cloudflare-bootstrap';
2
+ import { discoverCloudflareBootstrapResources, type CloudflareBootstrapDiscovery } from '../cloudflare-edge';
2
3
  export interface CloudflareBootstrapCommandConfig {
3
4
  format: 1;
4
5
  kind: 'forgezero-cloudflare-bootstrap-request';
@@ -10,6 +11,15 @@ export interface CloudflareBootstrapCommandDependencies {
10
11
  run?: CloudflareBootstrapPhaseRunner;
11
12
  write?: (text: string) => void;
12
13
  }
14
+ export declare function discoverCloudflareBootstrapCommandResources(input: {
15
+ zoneName: string;
16
+ kvNamespaceTitle: string;
17
+ workerScriptName?: string;
18
+ tokenFiles: CloudflareBootstrapTokenFiles;
19
+ }, dependencies?: {
20
+ fetcher?: typeof fetch;
21
+ discover?: typeof discoverCloudflareBootstrapResources;
22
+ }): Promise<CloudflareBootstrapDiscovery>;
13
23
  export interface CloudflareBootstrapFinalizeConfig {
14
24
  format: 1;
15
25
  kind: 'forgezero-cloudflare-bootstrap-finalize';
@@ -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`);
@@ -7,6 +7,24 @@ export interface CloudflareEdgeConfig {
7
7
  /** The single attended management capability used for both Tunnel and DNS writes. */
8
8
  apiToken: string;
9
9
  }
10
+ export interface CloudflareBootstrapDiscovery {
11
+ accountId: string;
12
+ zoneId: string;
13
+ kvNamespaceId: string;
14
+ }
15
+ /**
16
+ * Resolve stable Cloudflare ids from owner-recognizable names without ever
17
+ * persisting or printing either token. The management token is used only for
18
+ * the exact zone lookup; the runtime token is used only for the existing KV
19
+ * namespace and optional Worker/DO ownership proof.
20
+ */
21
+ export declare function discoverCloudflareBootstrapResources(config: {
22
+ zoneName: string;
23
+ kvNamespaceTitle: string;
24
+ workerScriptName?: string;
25
+ tunnelToken: string;
26
+ apiToken: string;
27
+ }, fetcher?: typeof fetch): Promise<CloudflareBootstrapDiscovery>;
10
28
  export interface CloudflarePrivateRouteConfig {
11
29
  accountId: string;
12
30
  tunnelId: string;
@@ -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`);
@@ -294,6 +355,7 @@ export {
294
355
  ensureCloudflarePrivateRoute,
295
356
  ensureCloudflarePrivateDatabaseRoute,
296
357
  ensureCloudflareMeshConnector,
358
+ discoverCloudflareBootstrapResources,
297
359
  configureCloudflareRealtimeSecrets,
298
360
  configureCloudflareEdge
299
361
  };
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.58";
8452
8452
 
8453
8453
  // src/ssh-bootstrap.ts
8454
8454
  class SshBootstrapError extends Error {
package/dist/fz.js CHANGED
@@ -4830,7 +4830,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
4830
4830
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
4831
4831
 
4832
4832
  // src/version.ts
4833
- var VERSION2 = "0.1.57";
4833
+ var VERSION2 = "0.1.58";
4834
4834
 
4835
4835
  // src/software.ts
4836
4836
  var PINNED_BUN_VERSION = "1.3.14";
@@ -11958,7 +11958,7 @@ function isPrivateDatabaseAddress(value) {
11958
11958
  return false;
11959
11959
  }
11960
11960
  var endpoint = "https://api.cloudflare.com/client/v4";
11961
- async function cf(config, path, init = {}, fetcher = fetch) {
11961
+ async function cfEnvelope(config, path, init = {}, fetcher = fetch) {
11962
11962
  const response = await fetcher(`${endpoint}${path}`, {
11963
11963
  ...init,
11964
11964
  headers: {
@@ -11971,7 +11971,68 @@ async function cf(config, path, init = {}, fetcher = fetch) {
11971
11971
  if (!response.ok || body.success !== true) {
11972
11972
  throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
11973
11973
  }
11974
- return body.result;
11974
+ return body;
11975
+ }
11976
+ async function cf(config, path, init = {}, fetcher = fetch) {
11977
+ return (await cfEnvelope(config, path, init, fetcher)).result;
11978
+ }
11979
+ async function cfPages(config, path, perPage, fetcher) {
11980
+ const output = [];
11981
+ for (let page = 1;page <= 100; page += 1) {
11982
+ const separator = path.includes("?") ? "&" : "?";
11983
+ const envelope = await cfEnvelope(config, `${path}${separator}page=${page}&per_page=${perPage}`, {}, fetcher);
11984
+ const result = Array.isArray(envelope.result) ? envelope.result : [];
11985
+ output.push(...result);
11986
+ const totalPages = envelope.result_info?.total_pages;
11987
+ if (Number.isInteger(totalPages) ? page >= totalPages : result.length < perPage)
11988
+ return output;
11989
+ }
11990
+ throw new Error("Cloudflare pagination exceeded the reviewed 100-page bound");
11991
+ }
11992
+ var exactHexId = (value, label) => {
11993
+ const normalized = String(value ?? "").trim().toLowerCase();
11994
+ if (!/^[a-f0-9]{32}$/.test(normalized))
11995
+ throw new Error(`${label} is malformed`);
11996
+ return normalized;
11997
+ };
11998
+ async function discoverCloudflareBootstrapResources(config, fetcher = fetch) {
11999
+ const zoneName = config.zoneName.trim().toLowerCase().replace(/\.$/, "");
12000
+ const kvNamespaceTitle = config.kvNamespaceTitle.trim();
12001
+ 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)) {
12002
+ throw new Error("Cloudflare zone name is invalid");
12003
+ }
12004
+ if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
12005
+ throw new Error("Cloudflare KV namespace title is invalid");
12006
+ }
12007
+ const [managementStatus, runtimeStatus] = await Promise.all([
12008
+ cf({ apiToken: config.tunnelToken }, "/user/tokens/verify", {}, fetcher),
12009
+ cf({ apiToken: config.apiToken }, "/user/tokens/verify", {}, fetcher)
12010
+ ]);
12011
+ if (managementStatus.status !== "active")
12012
+ throw new Error("CF_TUNNEL_TOKEN is not active");
12013
+ if (runtimeStatus.status !== "active")
12014
+ throw new Error("CF_API_TOKEN is not active");
12015
+ const zones = await cfPages({ apiToken: config.tunnelToken }, `/zones?name=${encodeURIComponent(zoneName)}&match=all&status=active`, 50, fetcher);
12016
+ const matchingZones = zones.filter(({ name }) => name?.trim().toLowerCase() === zoneName);
12017
+ if (matchingZones.length !== 1) {
12018
+ throw new Error(`Cloudflare zone ${zoneName} must resolve to exactly one active zone`);
12019
+ }
12020
+ const zoneId = exactHexId(matchingZones[0].id, "Cloudflare zone id");
12021
+ const accountId = exactHexId(matchingZones[0].account?.id, "Cloudflare account id");
12022
+ const namespaces = await cfPages({ apiToken: config.apiToken }, `/accounts/${accountId}/storage/kv/namespaces?order=title&direction=asc`, 1000, fetcher);
12023
+ const matchingNamespaces = namespaces.filter(({ title }) => title === kvNamespaceTitle);
12024
+ if (matchingNamespaces.length !== 1) {
12025
+ throw new Error(`Cloudflare KV namespace ${kvNamespaceTitle} must resolve to exactly one namespace`);
12026
+ }
12027
+ const kvNamespaceId = exactHexId(matchingNamespaces[0].id, "Cloudflare KV namespace id");
12028
+ if (config.workerScriptName) {
12029
+ await verifyCloudflareWorkerDurableObjects({
12030
+ accountId,
12031
+ scriptName: config.workerScriptName,
12032
+ apiToken: config.apiToken
12033
+ }, fetcher);
12034
+ }
12035
+ return { accountId, zoneId, kvNamespaceId };
11975
12036
  }
11976
12037
  async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
11977
12038
  const [address, prefixText, ...extra] = config.network.split("/");
@@ -12085,7 +12146,7 @@ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
12085
12146
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
12086
12147
  throw new Error("Cloudflare Worker script name is invalid");
12087
12148
  }
12088
- const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
12149
+ const namespaces = await cfPages(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces`, 1000, fetcher);
12089
12150
  const owned = namespaces.filter(({ script }) => script === config.scriptName);
12090
12151
  if (!owned.length)
12091
12152
  throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
@@ -14337,6 +14398,15 @@ function localBootstrapHost() {
14337
14398
  // src/cli/cloudflare-bootstrap.ts
14338
14399
  import { constants as constants2, closeSync, fstatSync, openSync, readFileSync as readFileSync9 } from "fs";
14339
14400
  import { dirname as dirname9, resolve as resolve6 } from "path";
14401
+ async function discoverCloudflareBootstrapCommandResources(input, dependencies = {}) {
14402
+ const tokens = await readCloudflareBootstrapTokens(input.tokenFiles);
14403
+ return (dependencies.discover ?? discoverCloudflareBootstrapResources)({
14404
+ zoneName: input.zoneName,
14405
+ kvNamespaceTitle: input.kvNamespaceTitle,
14406
+ ...input.workerScriptName ? { workerScriptName: input.workerScriptName } : {},
14407
+ ...tokens
14408
+ }, dependencies.fetcher);
14409
+ }
14340
14410
  var exactKeys3 = (value, allowed, label) => {
14341
14411
  const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
14342
14412
  if (unknown.length)
@@ -17138,13 +17208,26 @@ function writeBootstrapConfig(path, config) {
17138
17208
  `, { mode: 384, flag: "wx" });
17139
17209
  return path;
17140
17210
  }
17141
- function interactiveCloudflareBootstrap() {
17211
+ async function interactiveCloudflareBootstrap() {
17142
17212
  if (!process.stdin.isTTY)
17143
17213
  throw new Error("interactive Cloudflare config generation requires a terminal");
17214
+ const tokenFiles = {
17215
+ tunnelTokenFile: bootstrapAnswer("Owner-only CF_TUNNEL_TOKEN file", "./CF_TUNNEL_TOKEN"),
17216
+ apiTokenFile: bootstrapAnswer("Owner-only CF_API_TOKEN file", "./CF_API_TOKEN")
17217
+ };
17218
+ const zoneName = bootstrapAnswer("Cloudflare DNS zone name", "forgezero.net");
17219
+ const kvNamespaceTitle = bootstrapAnswer("Existing Worker-bound KV namespace title");
17220
+ const realtimeEnabled = bootstrapAnswer("Configure existing Worker realtime fan-out? (yes/no)", "yes") === "yes";
17221
+ const workerScriptName = realtimeEnabled ? bootstrapAnswer("Existing Worker script name") : undefined;
17222
+ const discovered = await discoverCloudflareBootstrapCommandResources({
17223
+ zoneName,
17224
+ kvNamespaceTitle,
17225
+ ...workerScriptName ? { workerScriptName } : {},
17226
+ tokenFiles
17227
+ });
17144
17228
  const nodeCount = bootstrapNumber("Number of public API nodes", "3");
17145
17229
  if (nodeCount < 1 || nodeCount > 32)
17146
17230
  throw new Error("Number of public API nodes must be between 1 and 32");
17147
- const realtimeEnabled = bootstrapAnswer("Configure existing Worker realtime fan-out? (yes/no)", "yes") === "yes";
17148
17231
  const meshEnabled = bootstrapAnswer("Configure private Mesh/WARP routes? (yes/no)", "no") === "yes";
17149
17232
  const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
17150
17233
  const nodes = Array.from({ length: nodeCount }, (_, index) => {
@@ -17168,23 +17251,18 @@ function interactiveCloudflareBootstrap() {
17168
17251
  return createCloudflareBootstrapCommandConfig({
17169
17252
  checkpointPath: bootstrapAnswer("Owner-only resumable checkpoint path", "./cloudflare-handoff.json"),
17170
17253
  coordinates: {
17171
- accountId: bootstrapAnswer("Cloudflare account id"),
17172
- zoneId: bootstrapAnswer("Cloudflare zone id"),
17173
- kvNamespaceId: bootstrapAnswer("Existing Worker-bound KV namespace id"),
17254
+ ...discovered,
17174
17255
  ...meshDevicePolicyId ? { meshDevicePolicyId } : {},
17175
17256
  ...realtimeEnabled ? {
17176
17257
  realtime: {
17177
- workerScriptName: bootstrapAnswer("Existing Worker script name"),
17258
+ workerScriptName,
17178
17259
  endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
17179
17260
  producer: bootstrapAnswer("Realtime producer identity", "platform-api")
17180
17261
  }
17181
17262
  } : {},
17182
17263
  nodes
17183
17264
  },
17184
- tokenFiles: {
17185
- tunnelTokenFile: bootstrapAnswer("Owner-only CF_TUNNEL_TOKEN file", "./CF_TUNNEL_TOKEN"),
17186
- apiTokenFile: bootstrapAnswer("Owner-only CF_API_TOKEN file", "./CF_API_TOKEN")
17187
- }
17265
+ tokenFiles
17188
17266
  });
17189
17267
  }
17190
17268
  function genesisOutputDirectory(path) {
@@ -17367,7 +17445,7 @@ async function cmdBootstrap(options, args) {
17367
17445
  return 0;
17368
17446
  }
17369
17447
  if (kind === "cloudflare") {
17370
- const path = writeBootstrapConfig(options.outputPath, interactiveCloudflareBootstrap());
17448
+ const path = writeBootstrapConfig(options.outputPath, await interactiveCloudflareBootstrap());
17371
17449
  readCloudflareBootstrapCommandConfig(path, "plan");
17372
17450
  out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
17373
17451
  return 0;
@@ -292,7 +292,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
292
292
  }
293
293
 
294
294
  // src/version.ts
295
- var VERSION = "0.1.57";
295
+ var VERSION = "0.1.58";
296
296
 
297
297
  // src/otel-collector.ts
298
298
  var FORGEZERO_OTEL_COLLECTOR_UNIT = "forgezero-otel-collector.service";
@@ -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`);
@@ -1207,7 +1268,7 @@ var UPDATE_RETRY_BASE_MS = 5 * 60000;
1207
1268
  var UPDATE_RETRY_MAX_MS = 24 * 60 * 60000;
1208
1269
 
1209
1270
  // src/version.ts
1210
- var VERSION = "0.1.57";
1271
+ var VERSION = "0.1.58";
1211
1272
 
1212
1273
  // src/software.ts
1213
1274
  var PINNED_BUN_VERSION = "1.3.14";
@@ -2086,7 +2086,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2086
2086
  }
2087
2087
 
2088
2088
  // src/version.ts
2089
- var VERSION3 = "0.1.57";
2089
+ var VERSION3 = "0.1.58";
2090
2090
 
2091
2091
  // src/egress-policy.ts
2092
2092
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/provision.js CHANGED
@@ -2086,7 +2086,7 @@ function requestSoftware(requirements, socketPath = DEFAULT_SOFTWARE_HELPER_SOCK
2086
2086
  }
2087
2087
 
2088
2088
  // src/version.ts
2089
- var VERSION3 = "0.1.57";
2089
+ var VERSION3 = "0.1.58";
2090
2090
 
2091
2091
  // src/egress-policy.ts
2092
2092
  import { realpathSync as realpathSync3 } from "node:fs";
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** One package version shared by both public binaries. Pinned to package.json by tests. */
2
- export declare const VERSION = "0.1.57";
2
+ export declare const VERSION = "0.1.58";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgezero/agent",
3
- "version": "0.1.57",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",