@forgezero/agent 0.1.56 → 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.56";
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.56";
1271
+ var VERSION = "0.1.58";
1211
1272
 
1212
1273
  // src/software.ts
1213
1274
  var PINNED_BUN_VERSION = "1.3.14";
@@ -1,14 +1,40 @@
1
- import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
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';
3
+ export interface CloudflareBootstrapCommandConfig {
4
+ format: 1;
5
+ kind: 'forgezero-cloudflare-bootstrap-request';
6
+ checkpointPath: string;
7
+ coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
8
+ tokenFiles: CloudflareBootstrapTokenFiles;
9
+ }
2
10
  export interface CloudflareBootstrapCommandDependencies {
3
11
  run?: CloudflareBootstrapPhaseRunner;
4
12
  write?: (text: string) => void;
5
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>;
6
23
  export interface CloudflareBootstrapFinalizeConfig {
7
24
  format: 1;
8
25
  kind: 'forgezero-cloudflare-bootstrap-finalize';
9
26
  checkpointPath: string;
10
27
  acceptancePath: string;
11
28
  }
29
+ /**
30
+ * Construct the exact reviewed JSON shape used by both the interactive CLI and
31
+ * the strict file reader. Token values are deliberately not accepted here.
32
+ */
33
+ export declare function createCloudflareBootstrapCommandConfig(input: {
34
+ checkpointPath: string;
35
+ coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
36
+ tokenFiles: CloudflareBootstrapTokenFiles;
37
+ }): CloudflareBootstrapCommandConfig;
12
38
  /**
13
39
  * Read reviewed non-secret coordinates and token-file paths for the attended
14
40
  * operator phase. Raw token values and a persisted mode are not schema fields.
@@ -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.56";
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.56";
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)
@@ -14347,6 +14417,26 @@ var record2 = (value, label) => {
14347
14417
  throw new Error(`${label} must be an object`);
14348
14418
  return value;
14349
14419
  };
14420
+ function createCloudflareBootstrapCommandConfig(input) {
14421
+ if (typeof input.checkpointPath !== "string" || !input.checkpointPath.trim()) {
14422
+ throw new Error("Cloudflare bootstrap checkpointPath is required");
14423
+ }
14424
+ for (const key of ["tunnelTokenFile", "apiTokenFile"]) {
14425
+ if (typeof input.tokenFiles?.[key] !== "string" || !input.tokenFiles[key].trim()) {
14426
+ throw new Error(`Cloudflare bootstrap tokenFiles.${key} is required and must be a non-empty file path`);
14427
+ }
14428
+ }
14429
+ return {
14430
+ format: 1,
14431
+ kind: "forgezero-cloudflare-bootstrap-request",
14432
+ checkpointPath: input.checkpointPath.trim(),
14433
+ coordinates: validateCloudflareBootstrapCoordinates(input.coordinates),
14434
+ tokenFiles: {
14435
+ tunnelTokenFile: input.tokenFiles.tunnelTokenFile.trim(),
14436
+ apiTokenFile: input.tokenFiles.apiTokenFile.trim()
14437
+ }
14438
+ };
14439
+ }
14350
14440
  function readOwnerConfig(path) {
14351
14441
  const absolute2 = resolve6(path);
14352
14442
  let descriptor;
@@ -17118,6 +17208,63 @@ function writeBootstrapConfig(path, config) {
17118
17208
  `, { mode: 384, flag: "wx" });
17119
17209
  return path;
17120
17210
  }
17211
+ async function interactiveCloudflareBootstrap() {
17212
+ if (!process.stdin.isTTY)
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
+ });
17228
+ const nodeCount = bootstrapNumber("Number of public API nodes", "3");
17229
+ if (nodeCount < 1 || nodeCount > 32)
17230
+ throw new Error("Number of public API nodes must be between 1 and 32");
17231
+ const meshEnabled = bootstrapAnswer("Configure private Mesh/WARP routes? (yes/no)", "no") === "yes";
17232
+ const meshDevicePolicyId = meshEnabled ? bootstrapAnswer("Existing Mesh device policy id") : undefined;
17233
+ const nodes = Array.from({ length: nodeCount }, (_, index) => {
17234
+ const ordinal = index + 1;
17235
+ const nodeName = bootstrapAnswer(`Node ${ordinal} inventory name`);
17236
+ const nodeMesh = meshEnabled && bootstrapAnswer(`Configure Mesh routes for ${nodeName}? (yes/no)`, "yes") === "yes";
17237
+ return {
17238
+ nodeName,
17239
+ hostname: bootstrapAnswer(`${nodeName} public hostname`),
17240
+ service: bootstrapAnswer(`${nodeName} loopback API service`, "http://127.0.0.1:3000"),
17241
+ tunnelName: bootstrapAnswer(`${nodeName} Tunnel name`, nodeName),
17242
+ ...nodeMesh ? {
17243
+ mesh: {
17244
+ connectorName: bootstrapAnswer(`${nodeName} Mesh connector name`, `${nodeName}-mesh`),
17245
+ routes: bootstrapList(`${nodeName} private CIDR routes`),
17246
+ highAvailability: bootstrapAnswer(`${nodeName} shares a highly available Mesh connector? (yes/no)`, "no") === "yes"
17247
+ }
17248
+ } : {}
17249
+ };
17250
+ });
17251
+ return createCloudflareBootstrapCommandConfig({
17252
+ checkpointPath: bootstrapAnswer("Owner-only resumable checkpoint path", "./cloudflare-handoff.json"),
17253
+ coordinates: {
17254
+ ...discovered,
17255
+ ...meshDevicePolicyId ? { meshDevicePolicyId } : {},
17256
+ ...realtimeEnabled ? {
17257
+ realtime: {
17258
+ workerScriptName,
17259
+ endpoint: bootstrapAnswer("Stable public Worker HTTPS endpoint"),
17260
+ producer: bootstrapAnswer("Realtime producer identity", "platform-api")
17261
+ }
17262
+ } : {},
17263
+ nodes
17264
+ },
17265
+ tokenFiles
17266
+ });
17267
+ }
17121
17268
  function genesisOutputDirectory(path) {
17122
17269
  if (!isAbsolute5(path) || resolve9(path) !== path)
17123
17270
  throw new Error("--output must be a canonical absolute directory");
@@ -17290,13 +17437,19 @@ async function cmdBootstrap(options, args) {
17290
17437
  const operation = args[0] ?? "status";
17291
17438
  if (operation === "config") {
17292
17439
  const kind = args[1];
17293
- if (kind !== "metal" && kind !== "platform" || args[2] !== undefined || !options.outputPath) {
17294
- throw new Error("Usage: fz bootstrap config metal --output <absolute-file> | fz bootstrap config platform --output <absolute-owner-only-directory>");
17440
+ if (kind !== "metal" && kind !== "platform" && kind !== "cloudflare" || args[2] !== undefined || !options.outputPath) {
17441
+ throw new Error("Usage: fz bootstrap config metal|cloudflare --output <absolute-file> | fz bootstrap config platform --output <absolute-owner-only-directory>");
17295
17442
  }
17296
17443
  if (kind === "platform") {
17297
17444
  out.line(JSON.stringify({ kind, ...writePlatformGenesisFleet(options.outputPath), mode: "0600" }, null, 2));
17298
17445
  return 0;
17299
17446
  }
17447
+ if (kind === "cloudflare") {
17448
+ const path = writeBootstrapConfig(options.outputPath, await interactiveCloudflareBootstrap());
17449
+ readCloudflareBootstrapCommandConfig(path, "plan");
17450
+ out.line(JSON.stringify({ kind, path, mode: "0600" }, null, 2));
17451
+ return 0;
17452
+ }
17300
17453
  const config2 = interactiveMetalBootstrap();
17301
17454
  out.line(JSON.stringify({ kind, path: writeBootstrapConfig(options.outputPath, config2), mode: "0600" }, null, 2));
17302
17455
  return 0;
@@ -17432,7 +17585,7 @@ async function cmdBootstrap(options, args) {
17432
17585
  return 0;
17433
17586
  }
17434
17587
  if (!["platform", "repair"].includes(operation)) {
17435
- throw new Error("Usage: fz bootstrap config <metal|platform>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|status>]|status|repair [--bootstrap-config <path>] [--apply]");
17588
+ throw new Error("Usage: fz bootstrap config <metal|platform|cloudflare>|platform [prepare|remote <prepare|apply|status>|cloudflare [verify|finalize]]|metal [remote <apply|genesis|rehearsal|status>]|status|repair [--bootstrap-config <path>] [--apply]");
17436
17589
  }
17437
17590
  const config = options.bootstrapConfigPath ? readBootstrapConfig(options.bootstrapConfigPath) : operation === "repair" ? (() => {
17438
17591
  throw new Error("repair requires --bootstrap-config so immutable coordinates are revalidated");
@@ -17997,9 +18150,9 @@ function usage() {
17997
18150
  Run typed bootstrap from the operator laptop through
17998
18151
  a pinned host and caller-approved SSH agent
17999
18152
  fz bootstrap platform Install/repair a typed elastic platform compute
18000
- fz bootstrap config <metal|platform>
18001
- Write a validated owner-only metal file or strict
18002
- three-node platform genesis directory; never apply locally
18153
+ fz bootstrap config <metal|platform|cloudflare>
18154
+ Write a validated owner-only metal/Cloudflare file or
18155
+ strict three-node platform genesis directory; never apply locally
18003
18156
  fz bootstrap platform cloudflare
18004
18157
  Plan/apply attended Tunnel and DNS reconciliation
18005
18158
  using supplied management and KV/Worker runtime token files
@@ -292,7 +292,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
292
292
  }
293
293
 
294
294
  // src/version.ts
295
- var VERSION = "0.1.56";
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.56";
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.56";
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.56";
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.56";
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.56",
3
+ "version": "0.1.58",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "check": "tsc --noEmit",