@forgezero/agent 0.1.57 → 0.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,24 @@
1
- import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokenFiles } from '../cloudflare-bootstrap';
1
+ import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokens } from '../cloudflare-bootstrap';
2
+ import { discoverCloudflareBootstrapResources, type CloudflareBootstrapDiscovery } from '../cloudflare-edge';
2
3
  export interface CloudflareBootstrapCommandConfig {
3
4
  format: 1;
4
5
  kind: 'forgezero-cloudflare-bootstrap-request';
5
6
  checkpointPath: string;
6
7
  coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
7
- tokenFiles: CloudflareBootstrapTokenFiles;
8
8
  }
9
9
  export interface CloudflareBootstrapCommandDependencies {
10
10
  run?: CloudflareBootstrapPhaseRunner;
11
11
  write?: (text: string) => void;
12
12
  }
13
+ export declare function discoverCloudflareBootstrapCommandResources(input: {
14
+ zoneName: string;
15
+ kvNamespaceTitle: string;
16
+ workerScriptName?: string;
17
+ tokens: CloudflareBootstrapTokens;
18
+ }, dependencies?: {
19
+ fetcher?: typeof fetch;
20
+ discover?: typeof discoverCloudflareBootstrapResources;
21
+ }): Promise<CloudflareBootstrapDiscovery>;
13
22
  export interface CloudflareBootstrapFinalizeConfig {
14
23
  format: 1;
15
24
  kind: 'forgezero-cloudflare-bootstrap-finalize';
@@ -23,14 +32,13 @@ export interface CloudflareBootstrapFinalizeConfig {
23
32
  export declare function createCloudflareBootstrapCommandConfig(input: {
24
33
  checkpointPath: string;
25
34
  coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
26
- tokenFiles: CloudflareBootstrapTokenFiles;
27
35
  }): CloudflareBootstrapCommandConfig;
28
36
  /**
29
- * Read reviewed non-secret coordinates and token-file paths for the attended
30
- * operator phase. Raw token values and a persisted mode are not schema fields.
37
+ * Read reviewed non-secret coordinates for the attended operator phase. Raw
38
+ * token values and a persisted mode are not schema fields.
31
39
  */
32
40
  export declare function readCloudflareBootstrapCommandConfig(path: string, mode: 'plan' | 'apply'): AttendedCloudflareBootstrapRequest;
33
- export declare function runCloudflareBootstrapCommand(configPath: string, apply: boolean, dependencies?: CloudflareBootstrapCommandDependencies): Promise<CloudflareBootstrapEvidence>;
41
+ export declare function runCloudflareBootstrapCommand(configPath: string, apply: boolean, tokens?: CloudflareBootstrapTokens, dependencies?: CloudflareBootstrapCommandDependencies): Promise<CloudflareBootstrapEvidence>;
34
42
  export declare function runCloudflareBootstrapVerificationCommand(checkpointPath: string, dependencies?: {
35
43
  verify?: typeof verifyCloudflareBootstrapAcceptance;
36
44
  write?: (text: string) => void;
@@ -33,32 +33,18 @@ export interface CloudflareBootstrapCoordinates {
33
33
  realtime?: CloudflareRealtimeCoordinates;
34
34
  nodes: CloudflareBootstrapNodeCoordinates[];
35
35
  }
36
- export interface CloudflareBootstrapTokenFiles {
37
- /** CF_TUNNEL_TOKEN: Tunnel, DNS, Mesh connector/network and device-profile writes. */
38
- tunnelTokenFile: string;
39
- /** Exact-account Workers KV Storage Write plus existing-Worker Scripts Write. */
40
- apiTokenFile: string;
41
- }
42
36
  export interface CloudflareBootstrapTokens {
43
37
  tunnelToken: string;
44
38
  apiToken: string;
45
39
  }
46
40
  interface CloudflareBootstrapNodeResource extends CloudflareBootstrapNodeCoordinates {
47
41
  tunnelId: string;
48
- connectorToken: string;
49
42
  mesh?: CloudflareMeshCoordinates & {
50
43
  connectorId: string;
51
- connectorToken: string;
52
44
  };
53
45
  }
54
46
  interface CloudflareBootstrapResources {
55
47
  kvNamespaceId: string;
56
- /** Owner-supplied CF_API_TOKEN, sealed into each host handoff. */
57
- apiToken: string;
58
- realtime?: {
59
- publishSecret: string;
60
- ticketSecret: string;
61
- };
62
48
  nodes: CloudflareBootstrapNodeResource[];
63
49
  }
64
50
  export interface CloudflareBootstrapOutput {
@@ -79,7 +65,7 @@ export interface CloudflareBootstrapOutput {
79
65
  export interface CloudflareBootstrapPlan {
80
66
  format: 1;
81
67
  kind: 'forgezero-cloudflare-bootstrap-plan';
82
- mode: 'attended-token-file';
68
+ mode: 'attended-hidden-input';
83
69
  outputFile: string;
84
70
  coordinates: CloudflareBootstrapCoordinates;
85
71
  operations: readonly string[];
@@ -88,7 +74,7 @@ export interface CloudflareBootstrapPlan {
88
74
  export interface AttendedCloudflareBootstrapRequest {
89
75
  mode: 'plan' | 'apply';
90
76
  coordinates: CloudflareBootstrapCoordinates;
91
- tokenFiles?: CloudflareBootstrapTokenFiles;
77
+ tokens?: CloudflareBootstrapTokens;
92
78
  checkpointPath: string;
93
79
  }
94
80
  /** Secret-free evidence safe to embed in the platform bootstrap journal. */
@@ -128,9 +114,7 @@ export interface CloudflareBootstrapDependencies {
128
114
  fetcher?: typeof fetch;
129
115
  }
130
116
  export type CloudflareBootstrapPhaseRunner = (request: AttendedCloudflareBootstrapRequest) => Promise<CloudflareBootstrapEvidence>;
131
- /** Read exactly one API token from a non-symlinked, owner-only regular file. */
132
- export declare function readOwnerApiToken(path: string): Promise<string>;
133
- export declare function readCloudflareBootstrapTokens(files: CloudflareBootstrapTokenFiles): Promise<CloudflareBootstrapTokens>;
117
+ export declare function validateCloudflareBootstrapTokens(input: unknown): CloudflareBootstrapTokens;
134
118
  export declare function validateCloudflareBootstrapCoordinates(input: CloudflareBootstrapCoordinates): CloudflareBootstrapCoordinates;
135
119
  export declare function planCloudflareBootstrap(input: CloudflareBootstrapCoordinates, outputPath: string): CloudflareBootstrapPlan;
136
120
  export interface CloudflareConnectorHandoff {
@@ -138,10 +122,8 @@ export interface CloudflareConnectorHandoff {
138
122
  hostname: string;
139
123
  service: string;
140
124
  tunnelId: string;
141
- connectorToken: string;
142
125
  mesh?: {
143
126
  connectorId: string;
144
- connectorToken: string;
145
127
  routes: readonly string[];
146
128
  };
147
129
  }
@@ -149,30 +131,29 @@ export interface CloudflareHostHandoff extends CloudflareConnectorHandoff {
149
131
  accountId: string;
150
132
  zoneId: string;
151
133
  kvNamespaceId: string;
152
- /** CF_API_TOKEN: Workers KV Storage Write plus existing-Worker Scripts Write. */
153
- apiToken: string;
154
- realtime?: CloudflareRealtimeCoordinates & {
155
- publishSecret: string;
156
- ticketSecret: string;
157
- };
134
+ realtime?: CloudflareRealtimeCoordinates;
158
135
  }
159
136
  export declare function cloudflareHostHandoffPath(checkpointPath: string, nodeName: string): string;
160
137
  export declare function readCloudflareConnectorHandoff(checkpointPath: string, nodeName: string): Promise<CloudflareConnectorHandoff>;
161
138
  export declare function readCloudflareHostHandoff(handoffPath: string, nodeName: string): Promise<CloudflareHostHandoff>;
162
139
  export declare function writeOwnerBootstrapOutput(path: string, output: CloudflareBootstrapOutput): Promise<void>;
140
+ export declare const deriveCloudflareRealtimeSecrets: (apiToken: string) => {
141
+ publishSecret: string;
142
+ ticketSecret: string;
143
+ };
163
144
  /** Attended, resumable Tunnel/DNS/KV handoff bootstrap. */
164
145
  export declare function applyCloudflareBootstrap(input: CloudflareBootstrapCoordinates, tokens: CloudflareBootstrapTokens, outputPath: string, fetcher?: typeof fetch): Promise<CloudflareBootstrapOutput>;
165
146
  export declare function runAttendedCloudflareBootstrap(request: AttendedCloudflareBootstrapRequest, dependencies?: CloudflareBootstrapDependencies): Promise<CloudflareBootstrapEvidence>;
166
147
  export declare function verifyCloudflareBootstrapAcceptance(checkpointPath: string, fetcher?: typeof fetch): Promise<CloudflareBootstrapAcceptanceEvidence>;
167
148
  /**
168
- * Read finalized, secret-free Cloudflare acceptance after the capability-bearing
149
+ * Read finalized, secret-free Cloudflare acceptance after the working
169
150
  * checkpoint has been removed. This is the durable input consumed by the fleet
170
151
  * rehearsal; it never attempts to reopen the deleted handoff.
171
152
  */
172
153
  export declare function readCloudflareBootstrapAcceptanceEvidence(acceptancePath: string, expectedNodes?: readonly CloudflareBootstrapAcceptedNode[]): Promise<CloudflareBootstrapAcceptanceEvidence>;
173
154
  /**
174
155
  * Prove every public node, persist secret-free acceptance evidence, then remove
175
- * the resumable checkpoint and node handoffs that contain runtime capabilities.
156
+ * the resumable secret-free checkpoint and node handoffs.
176
157
  */
177
158
  export declare function finalizeCloudflareBootstrapAcceptance(request: CloudflareBootstrapFinalizeRequest, fetcher?: typeof fetch): Promise<CloudflareBootstrapAcceptanceEvidence>;
178
159
  export {};
@@ -22,7 +22,7 @@ function privateDatabaseHostRoute(value) {
22
22
  return `${address}/${isIP(address) === 4 ? 32 : 128}`;
23
23
  }
24
24
  var endpoint = "https://api.cloudflare.com/client/v4";
25
- async function cf(config, path, init = {}, fetcher = fetch) {
25
+ async function cfEnvelope(config, path, init = {}, fetcher = fetch) {
26
26
  const response = await fetcher(`${endpoint}${path}`, {
27
27
  ...init,
28
28
  headers: {
@@ -35,7 +35,68 @@ async function cf(config, path, init = {}, fetcher = fetch) {
35
35
  if (!response.ok || body.success !== true) {
36
36
  throw new Error(body.errors?.map(({ message }) => message).filter(Boolean).join("; ") || `Cloudflare returned HTTP ${response.status}`);
37
37
  }
38
- return body.result;
38
+ return body;
39
+ }
40
+ async function cf(config, path, init = {}, fetcher = fetch) {
41
+ return (await cfEnvelope(config, path, init, fetcher)).result;
42
+ }
43
+ async function cfPages(config, path, perPage, fetcher) {
44
+ const output = [];
45
+ for (let page = 1;page <= 100; page += 1) {
46
+ const separator = path.includes("?") ? "&" : "?";
47
+ const envelope = await cfEnvelope(config, `${path}${separator}page=${page}&per_page=${perPage}`, {}, fetcher);
48
+ const result = Array.isArray(envelope.result) ? envelope.result : [];
49
+ output.push(...result);
50
+ const totalPages = envelope.result_info?.total_pages;
51
+ if (Number.isInteger(totalPages) ? page >= totalPages : result.length < perPage)
52
+ return output;
53
+ }
54
+ throw new Error("Cloudflare pagination exceeded the reviewed 100-page bound");
55
+ }
56
+ var exactHexId = (value, label) => {
57
+ const normalized = String(value ?? "").trim().toLowerCase();
58
+ if (!/^[a-f0-9]{32}$/.test(normalized))
59
+ throw new Error(`${label} is malformed`);
60
+ return normalized;
61
+ };
62
+ async function discoverCloudflareBootstrapResources(config, fetcher = fetch) {
63
+ const zoneName = config.zoneName.trim().toLowerCase().replace(/\.$/, "");
64
+ const kvNamespaceTitle = config.kvNamespaceTitle.trim();
65
+ if (!/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(zoneName)) {
66
+ throw new Error("Cloudflare zone name is invalid");
67
+ }
68
+ if (!kvNamespaceTitle || kvNamespaceTitle.length > 512) {
69
+ throw new Error("Cloudflare KV namespace title is invalid");
70
+ }
71
+ const [managementStatus, runtimeStatus] = await Promise.all([
72
+ cf({ apiToken: config.tunnelToken }, "/user/tokens/verify", {}, fetcher),
73
+ cf({ apiToken: config.apiToken }, "/user/tokens/verify", {}, fetcher)
74
+ ]);
75
+ if (managementStatus.status !== "active")
76
+ throw new Error("CF_TUNNEL_TOKEN is not active");
77
+ if (runtimeStatus.status !== "active")
78
+ throw new Error("CF_API_TOKEN is not active");
79
+ const zones = await cfPages({ apiToken: config.tunnelToken }, `/zones?name=${encodeURIComponent(zoneName)}&match=all&status=active`, 50, fetcher);
80
+ const matchingZones = zones.filter(({ name }) => name?.trim().toLowerCase() === zoneName);
81
+ if (matchingZones.length !== 1) {
82
+ throw new Error(`Cloudflare zone ${zoneName} must resolve to exactly one active zone`);
83
+ }
84
+ const zoneId = exactHexId(matchingZones[0].id, "Cloudflare zone id");
85
+ const accountId = exactHexId(matchingZones[0].account?.id, "Cloudflare account id");
86
+ const namespaces = await cfPages({ apiToken: config.apiToken }, `/accounts/${accountId}/storage/kv/namespaces?order=title&direction=asc`, 1000, fetcher);
87
+ const matchingNamespaces = namespaces.filter(({ title }) => title === kvNamespaceTitle);
88
+ if (matchingNamespaces.length !== 1) {
89
+ throw new Error(`Cloudflare KV namespace ${kvNamespaceTitle} must resolve to exactly one namespace`);
90
+ }
91
+ const kvNamespaceId = exactHexId(matchingNamespaces[0].id, "Cloudflare KV namespace id");
92
+ if (config.workerScriptName) {
93
+ await verifyCloudflareWorkerDurableObjects({
94
+ accountId,
95
+ scriptName: config.workerScriptName,
96
+ apiToken: config.apiToken
97
+ }, fetcher);
98
+ }
99
+ return { accountId, zoneId, kvNamespaceId };
39
100
  }
40
101
  async function ensureCloudflarePrivateRoute(config, fetcher = fetch) {
41
102
  const [address, prefixText, ...extra] = config.network.split("/");
@@ -211,7 +272,7 @@ async function verifyCloudflareWorkerDurableObjects(config, fetcher = fetch) {
211
272
  if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,62}$/.test(config.scriptName)) {
212
273
  throw new Error("Cloudflare Worker script name is invalid");
213
274
  }
214
- const namespaces = await cf(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces?per_page=1000`, {}, fetcher);
275
+ const namespaces = await cfPages(config, `/accounts/${config.accountId}/workers/durable_objects/namespaces`, 1000, fetcher);
215
276
  const owned = namespaces.filter(({ script }) => script === config.scriptName);
216
277
  if (!owned.length)
217
278
  throw new Error(`Cloudflare Worker ${config.scriptName} has no Durable Object namespace`);
@@ -283,18 +344,28 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
283
344
  }
284
345
  return { tunnel, connectorToken, created };
285
346
  }
347
+ async function retrieveCloudflareConnectorTokens(config, fetcher = fetch) {
348
+ if (!/^[a-f0-9]{32}$/i.test(config.accountId) || !/^[0-9a-f-]{36}$/i.test(config.tunnelId) || config.meshConnectorId && !/^[0-9a-f-]{36}$/i.test(config.meshConnectorId)) {
349
+ throw new Error("Cloudflare connector retrieval coordinates are invalid");
350
+ }
351
+ const connectorToken = await cf(config, `/accounts/${config.accountId}/cfd_tunnel/${encodeURIComponent(config.tunnelId)}/token`, {}, fetcher);
352
+ const meshConnectorToken = config.meshConnectorId ? await cf(config, `/accounts/${config.accountId}/warp_connector/${encodeURIComponent(config.meshConnectorId)}/token`, {}, fetcher) : undefined;
353
+ for (const value of [connectorToken, meshConnectorToken]) {
354
+ if (value !== undefined && (!value || value.length > 16384))
355
+ throw new Error("Cloudflare returned an invalid connector token");
356
+ }
357
+ return { connectorToken, ...meshConnectorToken ? { meshConnectorToken } : {} };
358
+ }
286
359
 
287
360
  // src/cloudflare-bootstrap.ts
288
361
  import { constants } from "fs";
289
- import { randomBytes, randomUUID } from "crypto";
362
+ import { createHmac, randomUUID } from "crypto";
290
363
  import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
291
364
  import { dirname, join, resolve } from "path";
292
365
  import { isIP as isIP2 } from "net";
293
366
  var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
294
- var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
295
367
  var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
296
368
  var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
297
- var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
298
369
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
299
370
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
300
371
  const metadata = await handle.stat();
@@ -328,26 +399,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
328
399
  await handle?.close();
329
400
  }
330
401
  }
331
- async function readOwnerApiToken(path) {
332
- const token = (await readOwnerOnlyFile(path, 4096)).trim();
333
- if (!TOKEN.test(token))
334
- throw new Error(`${resolve(path)} must contain exactly one Cloudflare API token`);
335
- return token;
336
- }
337
- async function readCloudflareBootstrapTokens(files) {
338
- const unsupported = Object.keys(files).filter((key) => ![
339
- "tunnelTokenFile",
340
- "apiTokenFile"
341
- ].includes(key));
402
+ function validateCloudflareBootstrapTokens(input) {
403
+ if (!input || typeof input !== "object" || Array.isArray(input))
404
+ throw new Error("Cloudflare bootstrap tokens must be an object");
405
+ const source = input;
406
+ const unsupported = Object.keys(source).filter((key) => !["tunnelToken", "apiToken"].includes(key));
342
407
  if (unsupported.length)
343
- throw new Error(`Cloudflare bootstrap token files contain unsupported field ${unsupported[0]}`);
344
- if (!files.tunnelTokenFile?.trim() || !files.apiTokenFile?.trim()) {
345
- throw new Error("Cloudflare bootstrap requires exactly tunnelTokenFile and apiTokenFile");
346
- }
347
- const [tunnelToken, apiToken] = await Promise.all([
348
- readOwnerApiToken(files.tunnelTokenFile),
349
- readOwnerApiToken(files.apiTokenFile)
350
- ]);
408
+ throw new Error(`Cloudflare bootstrap tokens contain unsupported field ${unsupported[0]}`);
409
+ const tunnelToken = typeof source.tunnelToken === "string" ? source.tunnelToken.trim() : "";
410
+ const apiToken = typeof source.apiToken === "string" ? source.apiToken.trim() : "";
411
+ if (!TOKEN.test(tunnelToken) || !TOKEN.test(apiToken))
412
+ throw new Error("Cloudflare bootstrap requires two valid API tokens");
351
413
  if (tunnelToken === apiToken) {
352
414
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
353
415
  }
@@ -523,7 +585,7 @@ function planCloudflareBootstrap(input, outputPath) {
523
585
  return {
524
586
  format: 1,
525
587
  kind: "forgezero-cloudflare-bootstrap-plan",
526
- mode: "attended-token-file",
588
+ mode: "attended-hidden-input",
527
589
  outputFile: resolve(outputPath),
528
590
  coordinates,
529
591
  operations: [
@@ -531,19 +593,19 @@ function planCloudflareBootstrap(input, outputPath) {
531
593
  ...coordinates.realtime ? [
532
594
  "prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
533
595
  ] : [],
534
- "create or reuse one remotely-managed Tunnel per node and checkpoint every connector token",
596
+ "create or reuse one remotely-managed Tunnel per node and checkpoint only its non-secret id",
535
597
  ...coordinates.nodes.some(({ mesh }) => mesh) ? [
536
- "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its registration token",
598
+ "create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint only its non-secret id",
537
599
  "reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
538
600
  ] : [],
539
601
  "preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
540
602
  "reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
541
- "write one node-specific handoff containing the connector token and owner-supplied KV/Worker runtime token"
603
+ "write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
542
604
  ],
543
605
  secrets: [
544
- "API tokens are read only from owner-only files and are never placed in argv or stdout",
545
- "CF_TUNNEL_TOKEN is never persisted; cloudflared, Mesh, CF_API_TOKEN and generated realtime capabilities are atomically checkpointed with mode 0600",
546
- "the normal API process receives only CF_API_TOKEN and never Tunnel, DNS or Mesh management authority"
606
+ "API tokens are accepted only from a hidden prompt and are never placed in JSON, files, argv or stdout",
607
+ "checkpoint and host handoffs contain no connector, management, runtime or realtime secret",
608
+ "the target Agent retrieves its connector tokens and seals both Vault fallback tokens directly"
547
609
  ]
548
610
  };
549
611
  }
@@ -608,25 +670,15 @@ async function readExistingOutput(path) {
608
670
  const resources = output.resources;
609
671
  const unsupportedResource = Object.keys(resources).filter((key) => ![
610
672
  "kvNamespaceId",
611
- "apiToken",
612
- "realtime",
613
673
  "nodes"
614
674
  ].includes(key));
615
675
  if (unsupportedResource.length) {
616
676
  throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
617
677
  }
618
678
  const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
619
- if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !TOKEN.test(String(resources.apiToken ?? "")) || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
679
+ if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
620
680
  throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
621
681
  }
622
- if (coordinates.realtime) {
623
- const realtime = resources.realtime;
624
- if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
625
- throw new Error(`${resolve(path)} has malformed Cloudflare realtime resources`);
626
- }
627
- } else if (resources.realtime !== undefined) {
628
- throw new Error(`${resolve(path)} contains undeclared Cloudflare realtime resources`);
629
- }
630
682
  const seen = new Set;
631
683
  for (const item of resources.nodes) {
632
684
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -639,16 +691,15 @@ async function readExistingOutput(path) {
639
691
  "service",
640
692
  "tunnelName",
641
693
  "tunnelId",
642
- "connectorToken",
643
694
  "mesh"
644
695
  ].includes(key));
645
696
  const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
646
- if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? "")) || !CONNECTOR_TOKEN.test(String(node.connectorToken ?? ""))) {
697
+ if (unknownNode.length || !expected || seen.has(expected.nodeName) || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !UUID.test(String(node.tunnelId ?? ""))) {
647
698
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
648
699
  }
649
700
  if (expected.mesh) {
650
701
  const mesh = node.mesh;
651
- if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? "")) || !CONNECTOR_TOKEN.test(String(mesh.connectorToken ?? ""))) {
702
+ if (!mesh || mesh.connectorName !== expected.mesh.connectorName || JSON.stringify(mesh.routes) !== JSON.stringify(expected.mesh.routes) || mesh.highAvailability !== expected.mesh.highAvailability || !UUID.test(String(mesh.connectorId ?? ""))) {
652
703
  throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
653
704
  }
654
705
  } else if (node.mesh !== undefined) {
@@ -689,7 +740,7 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
689
740
  throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
690
741
  }
691
742
  const resource = matches[0];
692
- if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId) || !CONNECTOR_TOKEN.test(resource.connectorToken)) {
743
+ if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId)) {
693
744
  throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
694
745
  }
695
746
  return {
@@ -697,10 +748,8 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
697
748
  hostname: resource.hostname,
698
749
  service: resource.service,
699
750
  tunnelId: resource.tunnelId,
700
- connectorToken: resource.connectorToken,
701
751
  ...resource.mesh ? { mesh: {
702
752
  connectorId: resource.mesh.connectorId,
703
- connectorToken: resource.mesh.connectorToken,
704
753
  routes: resource.mesh.routes
705
754
  } } : {}
706
755
  };
@@ -724,11 +773,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
724
773
  "hostname",
725
774
  "service",
726
775
  "tunnelId",
727
- "connectorToken",
728
776
  "accountId",
729
777
  "zoneId",
730
778
  "kvNamespaceId",
731
- "apiToken",
732
779
  "mesh",
733
780
  "realtime"
734
781
  ].includes(key));
@@ -739,11 +786,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
739
786
  try {
740
787
  service = normalizeService(output.service ?? "");
741
788
  } catch {}
742
- if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !CONNECTOR_TOKEN.test(output.connectorToken ?? "") || !TOKEN.test(output.apiToken ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
789
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "")) {
743
790
  throw new Error("Cloudflare host handoff is malformed or belongs to another node");
744
791
  }
745
792
  if (output.mesh !== undefined) {
746
- if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !CONNECTOR_TOKEN.test(output.mesh.connectorToken) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
793
+ if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !Array.isArray(output.mesh.routes) || output.mesh.routes.length < 1 || output.mesh.routes.length > 64 || output.mesh.routes.some((route) => {
747
794
  try {
748
795
  return privateMeshCidr(route) !== route;
749
796
  } catch {
@@ -772,7 +819,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
772
819
  } catch {
773
820
  throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
774
821
  }
775
- if (!REALTIME_SECRET.test(output.realtime.publishSecret) || !REALTIME_SECRET.test(output.realtime.ticketSecret) || output.realtime.publishSecret === output.realtime.ticketSecret || realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
822
+ if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
776
823
  throw new Error("Cloudflare host handoff contains malformed realtime credentials");
777
824
  }
778
825
  }
@@ -837,18 +884,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
837
884
  hostname: node.hostname,
838
885
  service: node.service,
839
886
  tunnelId: node.tunnelId,
840
- connectorToken: node.connectorToken,
841
887
  accountId: output.coordinates.accountId,
842
888
  zoneId: output.coordinates.zoneId,
843
889
  kvNamespaceId: output.resources.kvNamespaceId,
844
- apiToken: output.resources.apiToken,
845
- ...output.coordinates.realtime && output.resources.realtime ? { realtime: {
846
- ...output.coordinates.realtime,
847
- ...output.resources.realtime
848
- } } : {},
890
+ ...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
849
891
  ...node.mesh ? { mesh: {
850
892
  connectorId: node.mesh.connectorId,
851
- connectorToken: node.mesh.connectorToken,
852
893
  routes: node.mesh.routes
853
894
  } } : {}
854
895
  };
@@ -856,7 +897,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
856
897
  }
857
898
  }
858
899
  var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
859
- var newRealtimeSecret = () => randomBytes(48).toString("base64url");
900
+ var deriveCloudflareRealtimeSecrets = (apiToken) => ({
901
+ publishSecret: createHmac("sha512", apiToken).update("forgezero/realtime/publish/v1").digest("base64url"),
902
+ ticketSecret: createHmac("sha512", apiToken).update("forgezero/realtime/ticket/v1").digest("base64url")
903
+ });
860
904
  async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
861
905
  const coordinates = validateCloudflareBootstrapCoordinates(input);
862
906
  const absoluteOutput = resolve(outputPath);
@@ -874,10 +918,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
874
918
  throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
875
919
  }
876
920
  await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
877
- const realtime = coordinates.realtime ? existing?.resources.realtime ?? {
878
- publishSecret: newRealtimeSecret(),
879
- ticketSecret: newRealtimeSecret()
880
- } : undefined;
921
+ const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
881
922
  if (coordinates.realtime && realtime) {
882
923
  await verifyCloudflareWorkerDurableObjects({
883
924
  accountId: coordinates.accountId,
@@ -892,8 +933,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
892
933
  coordinates,
893
934
  resources: {
894
935
  kvNamespaceId: coordinates.kvNamespaceId,
895
- apiToken,
896
- realtime,
897
936
  nodes: existing?.resources.nodes ?? []
898
937
  }
899
938
  });
@@ -912,7 +951,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
912
951
  let resource;
913
952
  let created = false;
914
953
  if (checkpointed) {
915
- if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId) || !CONNECTOR_TOKEN.test(checkpointed.connectorToken)) {
954
+ if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)) {
916
955
  throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
917
956
  }
918
957
  resource = checkpointed;
@@ -933,15 +972,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
933
972
  }, fetcher);
934
973
  mesh = {
935
974
  ...node.mesh,
936
- connectorId: ensured.connector.id,
937
- connectorToken: ensured.connectorToken
975
+ connectorId: ensured.connector.id
938
976
  };
939
977
  }
940
978
  const { mesh: _declaredMesh, ...publicNode } = node;
941
979
  resource = {
942
980
  ...publicNode,
943
981
  tunnelId: tunnel.tunnel.id,
944
- connectorToken: tunnel.connectorToken,
945
982
  ...mesh ? { mesh } : {}
946
983
  };
947
984
  }
@@ -955,8 +992,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
955
992
  coordinates,
956
993
  resources: {
957
994
  kvNamespaceId: coordinates.kvNamespaceId,
958
- apiToken,
959
- ...realtime ? { realtime } : {},
960
995
  nodes: [...nodeResources]
961
996
  }
962
997
  });
@@ -1000,8 +1035,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
1000
1035
  coordinates,
1001
1036
  resources: {
1002
1037
  kvNamespaceId: coordinates.kvNamespaceId,
1003
- apiToken,
1004
- ...realtime ? { realtime } : {},
1005
1038
  nodes: nodeResources
1006
1039
  },
1007
1040
  created: {
@@ -1023,10 +1056,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
1023
1056
  nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
1024
1057
  };
1025
1058
  }
1026
- if (!request.tokenFiles) {
1027
- throw new Error("Cloudflare apply requires exactly two owner-only token file paths");
1059
+ if (!request.tokens) {
1060
+ throw new Error("Cloudflare apply requires exactly two attended API tokens");
1028
1061
  }
1029
- const tokens = await readCloudflareBootstrapTokens(request.tokenFiles);
1062
+ const tokens = validateCloudflareBootstrapTokens(request.tokens);
1030
1063
  const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
1031
1064
  return {
1032
1065
  format: 1,
@@ -1178,15 +1211,15 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
1178
1211
  export {
1179
1212
  writeOwnerBootstrapOutput,
1180
1213
  verifyCloudflareBootstrapAcceptance,
1214
+ validateCloudflareBootstrapTokens,
1181
1215
  validateCloudflareBootstrapCoordinates,
1182
1216
  runAttendedCloudflareBootstrap,
1183
- readOwnerApiToken,
1184
1217
  readCloudflareHostHandoff,
1185
1218
  readCloudflareConnectorHandoff,
1186
- readCloudflareBootstrapTokens,
1187
1219
  readCloudflareBootstrapAcceptanceEvidence,
1188
1220
  planCloudflareBootstrap,
1189
1221
  finalizeCloudflareBootstrapAcceptance,
1222
+ deriveCloudflareRealtimeSecrets,
1190
1223
  cloudflareHostHandoffPath,
1191
1224
  applyCloudflareBootstrap
1192
1225
  };
@@ -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;
@@ -166,4 +184,14 @@ export declare function ensureCloudflareTunnel(config: {
166
184
  connectorToken: string;
167
185
  created: boolean;
168
186
  }>;
187
+ /** Retrieve node-scoped connector capabilities only in the target Agent process. */
188
+ export declare function retrieveCloudflareConnectorTokens(config: {
189
+ accountId: string;
190
+ tunnelId: string;
191
+ meshConnectorId?: string;
192
+ apiToken: string;
193
+ }, fetcher?: typeof fetch): Promise<{
194
+ connectorToken: string;
195
+ meshConnectorToken?: string;
196
+ }>;
169
197
  export {};