@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 +6 -5
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.js +65 -4
- package/dist/cli/cloudflare-bootstrap.d.ts +27 -1
- package/dist/cloudflare-bootstrap.js +64 -3
- package/dist/cloudflare-edge.d.ts +18 -0
- package/dist/cloudflare-edge.js +65 -3
- package/dist/fz-agent.js +1 -1
- package/dist/fz.js +163 -10
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.js +65 -4
- package/dist/platform-fleet-verification.js +1 -1
- package/dist/provision.js +1 -1
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
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
|
|
366
|
-
import { ensureCloudflareWarpDatabaseInclude, ensureCloudflareWarpNetworkIncludes, removeCloudflarePrivateDatabaseRoute, removeCloudflarePrivateRoute, removeCloudflareWarpDatabaseInclude
|
|
367
|
-
import
|
|
368
|
-
import type {
|
|
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>
|
package/dist/agent-heartbeat.js
CHANGED
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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;
|
package/dist/cloudflare-edge.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
|
|
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
|
|
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
|
|
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
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
package/dist/metal-bootstrap.js
CHANGED
|
@@ -292,7 +292,7 @@ function systemdAgentEgressDirectives(loopbackTcpPorts = []) {
|
|
|
292
292
|
}
|
|
293
293
|
|
|
294
294
|
// src/version.ts
|
|
295
|
-
var VERSION = "0.1.
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
2
|
+
export declare const VERSION = "0.1.58";
|