@forgezero/agent 0.1.37 → 0.1.39

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,4 +1,4 @@
1
- import { type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
1
+ import { verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapPhaseRunner } from '../cloudflare-bootstrap';
2
2
  export interface CloudflareBootstrapCommandDependencies {
3
3
  run?: CloudflareBootstrapPhaseRunner;
4
4
  write?: (text: string) => void;
@@ -9,3 +9,7 @@ export interface CloudflareBootstrapCommandDependencies {
9
9
  */
10
10
  export declare function readCloudflareBootstrapCommandConfig(path: string, mode: 'plan' | 'apply'): AttendedCloudflareBootstrapRequest;
11
11
  export declare function runCloudflareBootstrapCommand(configPath: string, apply: boolean, dependencies?: CloudflareBootstrapCommandDependencies): Promise<CloudflareBootstrapEvidence>;
12
+ export declare function runCloudflareBootstrapVerificationCommand(checkpointPath: string, dependencies?: {
13
+ verify?: typeof verifyCloudflareBootstrapAcceptance;
14
+ write?: (text: string) => void;
15
+ }): Promise<CloudflareBootstrapAcceptanceEvidence>;
@@ -144,10 +144,28 @@ export interface CloudflareBootstrapEvidence {
144
144
  nodes: ReadonlyArray<{
145
145
  nodeName: string;
146
146
  hostname: string;
147
+ /** Owner-only, node-specific input for `fz bootstrap platform`. */
148
+ handoffFile?: string;
147
149
  tunnelId?: string;
148
150
  applicationId?: string;
149
151
  }>;
150
152
  }
153
+ /** Secret-free proof that the already-provisioned edge reaches live APIs. */
154
+ export interface CloudflareBootstrapAcceptanceEvidence {
155
+ format: 1;
156
+ kind: 'forgezero-cloudflare-bootstrap-acceptance';
157
+ checkpointFile: string;
158
+ verifiedAt: string;
159
+ nodes: ReadonlyArray<{
160
+ nodeName: string;
161
+ hostname: string;
162
+ status: number;
163
+ }>;
164
+ publicDomains: ReadonlyArray<{
165
+ hostname: string;
166
+ status: number;
167
+ }>;
168
+ }
151
169
  export interface CloudflareBootstrapDependencies {
152
170
  fetcher?: typeof fetch;
153
171
  workerRunner?: CloudflareWorkerCommandRunner;
@@ -191,6 +209,8 @@ export interface CloudflareHostHandoff extends CloudflareConnectorHandoff {
191
209
  deviceProfileId: string;
192
210
  };
193
211
  }
212
+ /** Deterministic private output path; safe to include in secret-free evidence. */
213
+ export declare function cloudflareHostHandoffPath(checkpointPath: string, nodeName: string): string;
194
214
  /**
195
215
  * Select one node's connector credential directly from the completed fleet
196
216
  * checkpoint. Host bootstrap can pipe this value into `systemd-creds` without
@@ -199,8 +219,7 @@ export interface CloudflareHostHandoff extends CloudflareConnectorHandoff {
199
219
  */
200
220
  export declare function readCloudflareConnectorHandoff(checkpointPath: string, nodeName: string): Promise<CloudflareConnectorHandoff>;
201
221
  /** Complete secret-bearing handoff consumed once by root host bootstrap. */
202
- export declare function readCloudflareHostHandoff(checkpointPath: string, nodeName: string): Promise<CloudflareHostHandoff>;
203
- /** Atomically persist sensitive bootstrap material without making it runtime configuration. */
222
+ export declare function readCloudflareHostHandoff(handoffPath: string, nodeName: string): Promise<CloudflareHostHandoff>;
204
223
  export declare function writeOwnerBootstrapOutput(path: string, output: CloudflareBootstrapOutput): Promise<void>;
205
224
  /**
206
225
  * Attended, resumable Cloudflare bootstrap. The output is checkpointed after
@@ -215,4 +234,14 @@ export declare function applyCloudflareBootstrap(input: CloudflareBootstrapCoord
215
234
  * a deliberately secret-free evidence record to the outer bootstrap journal.
216
235
  */
217
236
  export declare function runAttendedCloudflareBootstrap(request: AttendedCloudflareBootstrapRequest, dependencies?: CloudflareBootstrapDependencies): Promise<CloudflareBootstrapEvidence>;
237
+ /**
238
+ * Prove the post-bootstrap data path from an operator machine.
239
+ *
240
+ * This deliberately reads only the completed owner checkpoint. The Access
241
+ * service credential is used in request headers for each origin and is never
242
+ * returned, logged, copied to a host, or accepted on argv. Public Worker
243
+ * domains are then checked without privileged headers, proving the same route
244
+ * a client uses after KV heartbeats are active.
245
+ */
246
+ export declare function verifyCloudflareBootstrapAcceptance(checkpointPath: string, fetcher?: typeof fetch): Promise<CloudflareBootstrapAcceptanceEvidence>;
218
247
  export {};
@@ -411,6 +411,22 @@ import { dirname, join, resolve } from "path";
411
411
  import { tmpdir } from "os";
412
412
  import { randomUUID } from "crypto";
413
413
  import { isIP as isIP2 } from "net";
414
+ var acceptanceFetch = async (url, label, fetcher, headers) => {
415
+ let response;
416
+ try {
417
+ response = await fetcher(url, {
418
+ method: "GET",
419
+ headers,
420
+ redirect: "manual",
421
+ signal: AbortSignal.timeout(5000)
422
+ });
423
+ } catch {
424
+ throw new Error(`${label} is unreachable`);
425
+ }
426
+ if (!response.ok)
427
+ throw new Error(`${label} returned HTTP ${response.status}`);
428
+ return response.status;
429
+ };
414
430
  var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
415
431
  async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
416
432
  const metadata = await handle.stat();
@@ -739,6 +755,12 @@ async function readExistingOutput(path) {
739
755
  }
740
756
  return output;
741
757
  }
758
+ function cloudflareHostHandoffPath(checkpointPath, nodeName) {
759
+ const normalized = nodeName.trim().toLowerCase();
760
+ if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(normalized))
761
+ throw new Error("Cloudflare host handoff node name is invalid");
762
+ return join(`${resolve(checkpointPath)}.hosts`, `${normalized}.json`);
763
+ }
742
764
  async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
743
765
  const output = await readExistingOutput(resolve(checkpointPath));
744
766
  if (!output || output.phase !== "complete") {
@@ -766,40 +788,66 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
766
788
  connectorToken: resource.connectorToken
767
789
  };
768
790
  }
769
- async function readCloudflareHostHandoff(checkpointPath, nodeName) {
770
- const connector = await readCloudflareConnectorHandoff(checkpointPath, nodeName);
771
- const output = await readExistingOutput(resolve(checkpointPath));
772
- const kv = output?.resources.runtimeTokens?.kv;
773
- if (!output || output.phase !== "complete" || !/^[a-f0-9]{32}$/i.test(output.coordinates.accountId) || !/^[a-f0-9]{32}$/i.test(output.coordinates.zoneId) || !/^[a-f0-9]{32}$/i.test(output.resources.kvNamespaceId) || !kv || !/^[A-Za-z0-9._-]{40,80}$/.test(kv.value)) {
774
- throw new Error("Cloudflare host handoff is missing the exact-account KV runtime capability");
791
+ async function readCloudflareHostHandoff(handoffPath, nodeName) {
792
+ let parsed;
793
+ try {
794
+ parsed = JSON.parse(await readOwnerOnlyFile(handoffPath, 65536));
795
+ } catch (cause) {
796
+ if (cause instanceof SyntaxError)
797
+ throw new Error("Cloudflare host handoff is not valid JSON");
798
+ throw cause;
775
799
  }
776
- const network = output.resources.runtimeTokens?.privateNetwork?.value;
800
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
801
+ throw new Error("Cloudflare host handoff is malformed");
802
+ const output = parsed;
803
+ const unknown = Object.keys(output).filter((key) => ![
804
+ "format",
805
+ "kind",
806
+ "nodeName",
807
+ "hostname",
808
+ "service",
809
+ "tunnelId",
810
+ "connectorToken",
811
+ "accountId",
812
+ "zoneId",
813
+ "kvNamespaceId",
814
+ "kvRuntimeToken",
815
+ "privateNetworkRuntimeToken",
816
+ "warp"
817
+ ].includes(key));
818
+ if (unknown.length)
819
+ throw new Error(`Cloudflare host handoff contains unsupported field ${unknown[0]}`);
820
+ if (output.warp) {
821
+ const unknownWarp = Object.keys(output.warp).filter((key) => ![
822
+ "organization",
823
+ "clientId",
824
+ "clientSecret",
825
+ "virtualNetworkId",
826
+ "deviceProfileId"
827
+ ].includes(key));
828
+ if (unknownWarp.length)
829
+ throw new Error(`Cloudflare host handoff WARP contains unsupported field ${unknownWarp[0]}`);
830
+ }
831
+ const normalizedNodeName = nodeName.trim().toLowerCase();
832
+ let service;
833
+ try {
834
+ service = new URL(output.service ?? "");
835
+ } catch {}
836
+ if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !/^[a-f0-9]{32}$/i.test(output.accountId ?? "") || !/^[a-f0-9]{32}$/i.test(output.zoneId ?? "") || !/^[a-f0-9]{32}$/i.test(output.kvNamespaceId ?? "") || !/^[A-Za-z0-9._-]{40,80}$/.test(output.kvRuntimeToken ?? "") || !output.hostname || !/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/.test(output.hostname) || !service || service.protocol !== "http:" || service.hostname !== "127.0.0.1" || !service.port || service.pathname !== "/" || service.username || service.password || service.search || service.hash || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(output.tunnelId ?? "") || !/^[A-Za-z0-9._-]{40,16384}$/.test(output.connectorToken ?? "")) {
837
+ throw new Error("Cloudflare host handoff is malformed or belongs to another node");
838
+ }
839
+ const network = output.privateNetworkRuntimeToken;
777
840
  if (network !== undefined && !/^[A-Za-z0-9._-]{40,80}$/.test(network)) {
778
841
  throw new Error("Cloudflare host handoff private-network capability is malformed");
779
842
  }
780
- const privateNetwork = output.resources.privateNetwork;
781
- const access = output.resources.access;
782
- if (Boolean(privateNetwork) !== Boolean(network)) {
843
+ if (Boolean(output.warp) !== Boolean(network)) {
783
844
  throw new Error("Cloudflare host handoff private-network resources and capability disagree");
784
845
  }
785
- if (privateNetwork && (!access?.clientId || !access.clientSecret || !/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(privateNetwork.warpOrganization) || !/^[0-9a-f-]{36}$/i.test(privateNetwork.virtualNetworkId) || !privateNetwork.deviceProfileId)) {
846
+ if (output.warp && (!output.warp.clientId || !output.warp.clientSecret || !/^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(output.warp.organization) || !/^[0-9a-f-]{36}$/i.test(output.warp.virtualNetworkId) || !output.warp.deviceProfileId)) {
786
847
  throw new Error("Cloudflare host handoff WARP enrollment is malformed");
787
848
  }
788
- return {
789
- ...connector,
790
- accountId: output.coordinates.accountId,
791
- zoneId: output.coordinates.zoneId,
792
- kvNamespaceId: output.resources.kvNamespaceId,
793
- kvRuntimeToken: kv.value,
794
- ...network ? { privateNetworkRuntimeToken: network } : {},
795
- ...privateNetwork && access ? { warp: {
796
- organization: privateNetwork.warpOrganization,
797
- clientId: access.clientId,
798
- clientSecret: access.clientSecret,
799
- virtualNetworkId: privateNetwork.virtualNetworkId,
800
- deviceProfileId: privateNetwork.deviceProfileId
801
- } } : {}
802
- };
849
+ const { format: _format, kind: _kind, ...handoff } = output;
850
+ return handoff;
803
851
  }
804
852
  async function prepareOwnerOutputDirectory(absolutePath) {
805
853
  const directory = dirname(absolutePath);
@@ -811,7 +859,7 @@ async function prepareOwnerOutputDirectory(absolutePath) {
811
859
  }
812
860
  return directory;
813
861
  }
814
- async function writeOwnerBootstrapOutput(path, output) {
862
+ async function writeOwnerJson(path, output) {
815
863
  const absolute = resolve(path);
816
864
  const directory = await prepareOwnerOutputDirectory(absolute);
817
865
  const temporary = `${absolute}.${randomUUID()}.tmp`;
@@ -839,6 +887,42 @@ async function writeOwnerBootstrapOutput(path, output) {
839
887
  });
840
888
  }
841
889
  }
890
+ async function writeOwnerBootstrapOutput(path, output) {
891
+ await writeOwnerJson(path, output);
892
+ }
893
+ async function writeCloudflareHostHandoffs(checkpointPath, output) {
894
+ const kv = output.resources.runtimeTokens?.kv?.value;
895
+ if (!kv || !/^[A-Za-z0-9._-]{40,80}$/.test(kv)) {
896
+ return;
897
+ }
898
+ const network = output.resources.runtimeTokens?.privateNetwork?.value;
899
+ const privateNetwork = output.resources.privateNetwork;
900
+ const access = output.resources.access;
901
+ for (const node of output.resources.nodes) {
902
+ const handoff = {
903
+ format: 1,
904
+ kind: "forgezero-cloudflare-host-handoff",
905
+ nodeName: node.nodeName,
906
+ hostname: node.hostname,
907
+ service: node.service,
908
+ tunnelId: node.tunnelId,
909
+ connectorToken: node.connectorToken,
910
+ accountId: output.coordinates.accountId,
911
+ zoneId: output.coordinates.zoneId,
912
+ kvNamespaceId: output.resources.kvNamespaceId,
913
+ kvRuntimeToken: kv,
914
+ ...network ? { privateNetworkRuntimeToken: network } : {},
915
+ ...privateNetwork && access ? { warp: {
916
+ organization: privateNetwork.warpOrganization,
917
+ clientId: access.clientId,
918
+ clientSecret: access.clientSecret,
919
+ virtualNetworkId: privateNetwork.virtualNetworkId,
920
+ deviceProfileId: privateNetwork.deviceProfileId
921
+ } } : {}
922
+ };
923
+ await writeOwnerJson(cloudflareHostHandoffPath(checkpointPath, node.nodeName), handoff);
924
+ }
925
+ }
842
926
  var tokenFor = (tokens, key) => {
843
927
  const token = tokens[key]?.trim() || tokens.apiToken?.trim();
844
928
  if (!token)
@@ -1139,6 +1223,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
1139
1223
  }
1140
1224
  };
1141
1225
  await writeOwnerBootstrapOutput(absoluteOutput, output);
1226
+ await writeCloudflareHostHandoffs(absoluteOutput, output);
1142
1227
  return output;
1143
1228
  }
1144
1229
  async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
@@ -1177,13 +1262,68 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
1177
1262
  nodes: output.resources.nodes.map(({ nodeName, hostname, tunnelId, applicationId }) => ({
1178
1263
  nodeName,
1179
1264
  hostname,
1265
+ ...output.resources.runtimeTokens?.kv ? {
1266
+ handoffFile: cloudflareHostHandoffPath(plan.outputFile, nodeName)
1267
+ } : {},
1180
1268
  tunnelId,
1181
1269
  applicationId
1182
1270
  }))
1183
1271
  };
1184
1272
  }
1273
+ async function verifyCloudflareBootstrapAcceptance(checkpointPath, fetcher = fetch) {
1274
+ const absolute = resolve(checkpointPath);
1275
+ const output = await readExistingOutput(absolute);
1276
+ if (!output || output.phase !== "complete")
1277
+ throw new Error("Cloudflare acceptance requires a completed owner checkpoint");
1278
+ const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
1279
+ const access = output.resources.access;
1280
+ if (!access?.clientId?.trim() || !access.clientSecret?.trim()) {
1281
+ throw new Error("Cloudflare acceptance checkpoint is missing the Access service credential");
1282
+ }
1283
+ if (!output.resources.worker?.deployed || output.resources.worker.scriptName !== coordinates.workerScriptName || JSON.stringify(output.resources.worker.publicDomains) !== JSON.stringify(coordinates.publicDomains)) {
1284
+ throw new Error("Cloudflare acceptance checkpoint does not prove the expected Worker deployment");
1285
+ }
1286
+ if (output.resources.nodes.length !== coordinates.nodes.length) {
1287
+ throw new Error("Cloudflare acceptance checkpoint does not cover the declared node fleet");
1288
+ }
1289
+ const nodeNames = new Set;
1290
+ const hostnames = new Set;
1291
+ for (const node of output.resources.nodes) {
1292
+ const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
1293
+ if (!expected || node.hostname !== expected.hostname || node.service !== expected.service || node.tunnelName !== expected.tunnelName || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(node.tunnelId)) {
1294
+ throw new Error("Cloudflare acceptance checkpoint has an unbound node resource");
1295
+ }
1296
+ if (nodeNames.has(node.nodeName) || hostnames.has(node.hostname)) {
1297
+ throw new Error("Cloudflare acceptance checkpoint has duplicate node coordinates");
1298
+ }
1299
+ nodeNames.add(node.nodeName);
1300
+ hostnames.add(node.hostname);
1301
+ }
1302
+ const accessHeaders = {
1303
+ "CF-Access-Client-Id": access.clientId,
1304
+ "CF-Access-Client-Secret": access.clientSecret
1305
+ };
1306
+ const nodes = await Promise.all(output.resources.nodes.map(async ({ nodeName, hostname }) => ({
1307
+ nodeName,
1308
+ hostname,
1309
+ status: await acceptanceFetch(`https://${hostname}/api/health`, `Cloudflare origin ${nodeName}`, fetcher, accessHeaders)
1310
+ })));
1311
+ const publicDomains = await Promise.all(output.resources.worker.publicDomains.map(async (hostname) => ({
1312
+ hostname,
1313
+ status: await acceptanceFetch(`https://${hostname}/api/health`, `Cloudflare public domain ${hostname}`, fetcher)
1314
+ })));
1315
+ return {
1316
+ format: 1,
1317
+ kind: "forgezero-cloudflare-bootstrap-acceptance",
1318
+ checkpointFile: absolute,
1319
+ verifiedAt: new Date().toISOString(),
1320
+ nodes,
1321
+ publicDomains
1322
+ };
1323
+ }
1185
1324
  export {
1186
1325
  writeOwnerBootstrapOutput,
1326
+ verifyCloudflareBootstrapAcceptance,
1187
1327
  validateCloudflareBootstrapCoordinates,
1188
1328
  runAttendedCloudflareBootstrap,
1189
1329
  readOwnerApiToken,
@@ -1192,5 +1332,6 @@ export {
1192
1332
  readCloudflareBootstrapTokens,
1193
1333
  planCloudflareBootstrap,
1194
1334
  deployCloudflareWorker,
1335
+ cloudflareHostHandoffPath,
1195
1336
  applyCloudflareBootstrap
1196
1337
  };
@@ -38,6 +38,8 @@ export interface DeploymentPullOptions {
38
38
  clearTimer?: (handle: unknown) => void;
39
39
  onEvent?: (event: string, detail?: unknown) => void;
40
40
  telemetry?: AgentOperationTelemetry;
41
+ /** Fail-closed desired-state gate. False means do not contact the claim route. */
42
+ canClaim?: () => boolean;
41
43
  }
42
44
  export type PullResult = {
43
45
  status: 'idle';