@forgezero/agent 0.1.58 → 0.1.60
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 +13 -12
- package/dist/agent-heartbeat.js +1 -1
- package/dist/bootstrap.d.ts +22 -11
- package/dist/bootstrap.js +193 -156
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -7
- package/dist/cloudflare-bootstrap.d.ts +10 -29
- package/dist/cloudflare-bootstrap.js +50 -78
- package/dist/cloudflare-edge.d.ts +10 -0
- package/dist/cloudflare-edge.js +13 -0
- package/dist/credential-schema.d.ts +9 -9
- package/dist/credential-schema.js +7 -7
- package/dist/fz-agent.js +27 -22
- package/dist/fz.js +330 -282
- package/dist/metal-bootstrap.js +1 -1
- package/dist/operator-bootstrap.d.ts +5 -2
- package/dist/operator-bootstrap.js +226 -181
- package/dist/platform-bootstrap-runtime.d.ts +1 -1
- package/dist/platform-bootstrap-runtime.js +3 -2
- package/dist/platform-fleet-verification.js +21 -14
- package/dist/platform-genesis.d.ts +2 -0
- package/dist/provision.js +12 -6
- package/dist/recovery-host.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type
|
|
1
|
+
import { finalizeCloudflareBootstrapAcceptance, verifyCloudflareBootstrapAcceptance, type AttendedCloudflareBootstrapRequest, type CloudflareBootstrapEvidence, type CloudflareBootstrapAcceptanceEvidence, type CloudflareBootstrapFinalizeRequest, type CloudflareBootstrapPhaseRunner, type CloudflareBootstrapTokens } from '../cloudflare-bootstrap';
|
|
2
2
|
import { discoverCloudflareBootstrapResources, type CloudflareBootstrapDiscovery } from '../cloudflare-edge';
|
|
3
3
|
export interface CloudflareBootstrapCommandConfig {
|
|
4
4
|
format: 1;
|
|
5
5
|
kind: 'forgezero-cloudflare-bootstrap-request';
|
|
6
6
|
checkpointPath: string;
|
|
7
7
|
coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
|
|
8
|
-
tokenFiles: CloudflareBootstrapTokenFiles;
|
|
9
8
|
}
|
|
10
9
|
export interface CloudflareBootstrapCommandDependencies {
|
|
11
10
|
run?: CloudflareBootstrapPhaseRunner;
|
|
@@ -15,7 +14,7 @@ export declare function discoverCloudflareBootstrapCommandResources(input: {
|
|
|
15
14
|
zoneName: string;
|
|
16
15
|
kvNamespaceTitle: string;
|
|
17
16
|
workerScriptName?: string;
|
|
18
|
-
|
|
17
|
+
tokens: CloudflareBootstrapTokens;
|
|
19
18
|
}, dependencies?: {
|
|
20
19
|
fetcher?: typeof fetch;
|
|
21
20
|
discover?: typeof discoverCloudflareBootstrapResources;
|
|
@@ -33,14 +32,13 @@ export interface CloudflareBootstrapFinalizeConfig {
|
|
|
33
32
|
export declare function createCloudflareBootstrapCommandConfig(input: {
|
|
34
33
|
checkpointPath: string;
|
|
35
34
|
coordinates: AttendedCloudflareBootstrapRequest['coordinates'];
|
|
36
|
-
tokenFiles: CloudflareBootstrapTokenFiles;
|
|
37
35
|
}): CloudflareBootstrapCommandConfig;
|
|
38
36
|
/**
|
|
39
|
-
* Read reviewed non-secret coordinates
|
|
40
|
-
*
|
|
37
|
+
* Read reviewed non-secret coordinates for the attended operator phase. Raw
|
|
38
|
+
* token values and a persisted mode are not schema fields.
|
|
41
39
|
*/
|
|
42
40
|
export declare function readCloudflareBootstrapCommandConfig(path: string, mode: 'plan' | 'apply'): AttendedCloudflareBootstrapRequest;
|
|
43
|
-
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>;
|
|
44
42
|
export declare function runCloudflareBootstrapVerificationCommand(checkpointPath: string, dependencies?: {
|
|
45
43
|
verify?: typeof verifyCloudflareBootstrapAcceptance;
|
|
46
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 {};
|
|
@@ -344,18 +344,28 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
|
344
344
|
}
|
|
345
345
|
return { tunnel, connectorToken, created };
|
|
346
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
|
+
}
|
|
347
359
|
|
|
348
360
|
// src/cloudflare-bootstrap.ts
|
|
349
361
|
import { constants } from "fs";
|
|
350
|
-
import {
|
|
362
|
+
import { createHmac, randomUUID } from "crypto";
|
|
351
363
|
import { chmod, lstat, mkdir, open, readdir, rename, rmdir, stat, unlink } from "fs/promises";
|
|
352
364
|
import { dirname, join, resolve } from "path";
|
|
353
365
|
import { isIP as isIP2 } from "net";
|
|
354
366
|
var TOKEN = /^[A-Za-z0-9._-]{40,80}$/;
|
|
355
|
-
var CONNECTOR_TOKEN = /^[A-Za-z0-9._-]{40,16384}$/;
|
|
356
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;
|
|
357
368
|
var HOSTNAME = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
358
|
-
var REALTIME_SECRET = /^[A-Za-z0-9_-]{64,128}$/;
|
|
359
369
|
var ownerUid = () => typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
360
370
|
async function assertOwnerOnlyHandle(path, handle, maximumBytes) {
|
|
361
371
|
const metadata = await handle.stat();
|
|
@@ -389,26 +399,17 @@ async function readOwnerOnlyFile(path, maximumBytes) {
|
|
|
389
399
|
await handle?.close();
|
|
390
400
|
}
|
|
391
401
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
async function readCloudflareBootstrapTokens(files) {
|
|
399
|
-
const unsupported = Object.keys(files).filter((key) => ![
|
|
400
|
-
"tunnelTokenFile",
|
|
401
|
-
"apiTokenFile"
|
|
402
|
-
].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));
|
|
403
407
|
if (unsupported.length)
|
|
404
|
-
throw new Error(`Cloudflare bootstrap
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
readOwnerApiToken(files.tunnelTokenFile),
|
|
410
|
-
readOwnerApiToken(files.apiTokenFile)
|
|
411
|
-
]);
|
|
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");
|
|
412
413
|
if (tunnelToken === apiToken) {
|
|
413
414
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege tokens");
|
|
414
415
|
}
|
|
@@ -584,7 +585,7 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
584
585
|
return {
|
|
585
586
|
format: 1,
|
|
586
587
|
kind: "forgezero-cloudflare-bootstrap-plan",
|
|
587
|
-
mode: "attended-
|
|
588
|
+
mode: "attended-hidden-input",
|
|
588
589
|
outputFile: resolve(outputPath),
|
|
589
590
|
coordinates,
|
|
590
591
|
operations: [
|
|
@@ -592,19 +593,19 @@ function planCloudflareBootstrap(input, outputPath) {
|
|
|
592
593
|
...coordinates.realtime ? [
|
|
593
594
|
"prove the existing Worker owns a Durable Object namespace and install its publish/ticket secrets with CF_API_TOKEN"
|
|
594
595
|
] : [],
|
|
595
|
-
"create or reuse one remotely-managed Tunnel per node and checkpoint
|
|
596
|
+
"create or reuse one remotely-managed Tunnel per node and checkpoint only its non-secret id",
|
|
596
597
|
...coordinates.nodes.some(({ mesh }) => mesh) ? [
|
|
597
|
-
"create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint its
|
|
598
|
+
"create or reuse one Mesh/WARP Connector per declared private-network node and checkpoint only its non-secret id",
|
|
598
599
|
"reconcile every unique private CIDR to its Mesh connector and include all declared CIDRs in the dedicated Mesh device profile"
|
|
599
600
|
] : [],
|
|
600
601
|
"preflight each exact DNS hostname, refuse ambiguous or incompatible records, and update its existing CNAME or create it only when absent",
|
|
601
602
|
"reconcile each Tunnel public-hostname ingress rule to the declared loopback API service",
|
|
602
|
-
"write one
|
|
603
|
+
"write one secret-free node handoff containing only bound Cloudflare ids and public coordinates"
|
|
603
604
|
],
|
|
604
605
|
secrets: [
|
|
605
|
-
"API tokens are
|
|
606
|
-
"
|
|
607
|
-
"the
|
|
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"
|
|
608
609
|
]
|
|
609
610
|
};
|
|
610
611
|
}
|
|
@@ -669,25 +670,15 @@ async function readExistingOutput(path) {
|
|
|
669
670
|
const resources = output.resources;
|
|
670
671
|
const unsupportedResource = Object.keys(resources).filter((key) => ![
|
|
671
672
|
"kvNamespaceId",
|
|
672
|
-
"apiToken",
|
|
673
|
-
"realtime",
|
|
674
673
|
"nodes"
|
|
675
674
|
].includes(key));
|
|
676
675
|
if (unsupportedResource.length) {
|
|
677
676
|
throw new Error(`${resolve(path)} resources contain unsupported field ${unsupportedResource[0]}`);
|
|
678
677
|
}
|
|
679
678
|
const coordinates = validateCloudflareBootstrapCoordinates(output.coordinates);
|
|
680
|
-
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !
|
|
679
|
+
if (resources.kvNamespaceId !== coordinates.kvNamespaceId || !Array.isArray(resources.nodes) || resources.nodes.length > coordinates.nodes.length) {
|
|
681
680
|
throw new Error(`${resolve(path)} has malformed Cloudflare bootstrap resources`);
|
|
682
681
|
}
|
|
683
|
-
if (coordinates.realtime) {
|
|
684
|
-
const realtime = resources.realtime;
|
|
685
|
-
if (!realtime || !REALTIME_SECRET.test(String(realtime.publishSecret ?? "")) || !REALTIME_SECRET.test(String(realtime.ticketSecret ?? "")) || realtime.publishSecret === realtime.ticketSecret) {
|
|
686
|
-
throw new Error(`${resolve(path)} has malformed Cloudflare realtime resources`);
|
|
687
|
-
}
|
|
688
|
-
} else if (resources.realtime !== undefined) {
|
|
689
|
-
throw new Error(`${resolve(path)} contains undeclared Cloudflare realtime resources`);
|
|
690
|
-
}
|
|
691
682
|
const seen = new Set;
|
|
692
683
|
for (const item of resources.nodes) {
|
|
693
684
|
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
@@ -700,16 +691,15 @@ async function readExistingOutput(path) {
|
|
|
700
691
|
"service",
|
|
701
692
|
"tunnelName",
|
|
702
693
|
"tunnelId",
|
|
703
|
-
"connectorToken",
|
|
704
694
|
"mesh"
|
|
705
695
|
].includes(key));
|
|
706
696
|
const expected = coordinates.nodes.find((candidate) => candidate.nodeName === node.nodeName);
|
|
707
|
-
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 ?? ""))
|
|
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 ?? ""))) {
|
|
708
698
|
throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare node resource`);
|
|
709
699
|
}
|
|
710
700
|
if (expected.mesh) {
|
|
711
701
|
const mesh = node.mesh;
|
|
712
|
-
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 ?? ""))
|
|
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 ?? ""))) {
|
|
713
703
|
throw new Error(`${resolve(path)} has a malformed or unbound Cloudflare Mesh resource`);
|
|
714
704
|
}
|
|
715
705
|
} else if (node.mesh !== undefined) {
|
|
@@ -750,7 +740,7 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
|
|
|
750
740
|
throw new Error(`Cloudflare connector handoff has no unique completed node ${normalizedNodeName}`);
|
|
751
741
|
}
|
|
752
742
|
const resource = matches[0];
|
|
753
|
-
if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId)
|
|
743
|
+
if (resource.hostname !== expected.hostname || resource.service !== expected.service || resource.tunnelName !== expected.tunnelName || !UUID.test(resource.tunnelId)) {
|
|
754
744
|
throw new Error(`Cloudflare connector handoff for ${normalizedNodeName} is malformed or incomplete`);
|
|
755
745
|
}
|
|
756
746
|
return {
|
|
@@ -758,10 +748,8 @@ async function readCloudflareConnectorHandoff(checkpointPath, nodeName) {
|
|
|
758
748
|
hostname: resource.hostname,
|
|
759
749
|
service: resource.service,
|
|
760
750
|
tunnelId: resource.tunnelId,
|
|
761
|
-
connectorToken: resource.connectorToken,
|
|
762
751
|
...resource.mesh ? { mesh: {
|
|
763
752
|
connectorId: resource.mesh.connectorId,
|
|
764
|
-
connectorToken: resource.mesh.connectorToken,
|
|
765
753
|
routes: resource.mesh.routes
|
|
766
754
|
} } : {}
|
|
767
755
|
};
|
|
@@ -785,11 +773,9 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
785
773
|
"hostname",
|
|
786
774
|
"service",
|
|
787
775
|
"tunnelId",
|
|
788
|
-
"connectorToken",
|
|
789
776
|
"accountId",
|
|
790
777
|
"zoneId",
|
|
791
778
|
"kvNamespaceId",
|
|
792
|
-
"apiToken",
|
|
793
779
|
"mesh",
|
|
794
780
|
"realtime"
|
|
795
781
|
].includes(key));
|
|
@@ -800,11 +786,11 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
800
786
|
try {
|
|
801
787
|
service = normalizeService(output.service ?? "");
|
|
802
788
|
} catch {}
|
|
803
|
-
if (output.format !== 1 || output.kind !== "forgezero-cloudflare-host-handoff" || output.nodeName !== normalizedNodeName || !HOSTNAME.test(output.hostname ?? "") || !service || !UUID.test(output.tunnelId ?? "") ||
|
|
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 ?? "")) {
|
|
804
790
|
throw new Error("Cloudflare host handoff is malformed or belongs to another node");
|
|
805
791
|
}
|
|
806
792
|
if (output.mesh !== undefined) {
|
|
807
|
-
if (!output.mesh || typeof output.mesh !== "object" || !UUID.test(output.mesh.connectorId) || !
|
|
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) => {
|
|
808
794
|
try {
|
|
809
795
|
return privateMeshCidr(route) !== route;
|
|
810
796
|
} catch {
|
|
@@ -833,7 +819,7 @@ async function readCloudflareHostHandoff(handoffPath, nodeName) {
|
|
|
833
819
|
} catch {
|
|
834
820
|
throw new Error("Cloudflare host handoff contains malformed realtime coordinates");
|
|
835
821
|
}
|
|
836
|
-
if (
|
|
822
|
+
if (realtime.workerScriptName !== output.realtime.workerScriptName || realtime.endpoint !== output.realtime.endpoint || realtime.producer !== output.realtime.producer) {
|
|
837
823
|
throw new Error("Cloudflare host handoff contains malformed realtime credentials");
|
|
838
824
|
}
|
|
839
825
|
}
|
|
@@ -898,18 +884,12 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
898
884
|
hostname: node.hostname,
|
|
899
885
|
service: node.service,
|
|
900
886
|
tunnelId: node.tunnelId,
|
|
901
|
-
connectorToken: node.connectorToken,
|
|
902
887
|
accountId: output.coordinates.accountId,
|
|
903
888
|
zoneId: output.coordinates.zoneId,
|
|
904
889
|
kvNamespaceId: output.resources.kvNamespaceId,
|
|
905
|
-
|
|
906
|
-
...output.coordinates.realtime && output.resources.realtime ? { realtime: {
|
|
907
|
-
...output.coordinates.realtime,
|
|
908
|
-
...output.resources.realtime
|
|
909
|
-
} } : {},
|
|
890
|
+
...output.coordinates.realtime ? { realtime: output.coordinates.realtime } : {},
|
|
910
891
|
...node.mesh ? { mesh: {
|
|
911
892
|
connectorId: node.mesh.connectorId,
|
|
912
|
-
connectorToken: node.mesh.connectorToken,
|
|
913
893
|
routes: node.mesh.routes
|
|
914
894
|
} } : {}
|
|
915
895
|
};
|
|
@@ -917,7 +897,10 @@ async function writeCloudflareHostHandoffs(checkpointPath, output) {
|
|
|
917
897
|
}
|
|
918
898
|
}
|
|
919
899
|
var sameCoordinates = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
920
|
-
var
|
|
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
|
+
});
|
|
921
904
|
async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fetch) {
|
|
922
905
|
const coordinates = validateCloudflareBootstrapCoordinates(input);
|
|
923
906
|
const absoluteOutput = resolve(outputPath);
|
|
@@ -935,10 +918,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
935
918
|
throw new Error("CF_TUNNEL_TOKEN and CF_API_TOKEN must be distinct least-privilege capabilities");
|
|
936
919
|
}
|
|
937
920
|
await preflightCloudflareKvRuntime(coordinates, apiToken, fetcher);
|
|
938
|
-
const realtime = coordinates.realtime ?
|
|
939
|
-
publishSecret: newRealtimeSecret(),
|
|
940
|
-
ticketSecret: newRealtimeSecret()
|
|
941
|
-
} : undefined;
|
|
921
|
+
const realtime = coordinates.realtime ? deriveCloudflareRealtimeSecrets(apiToken) : undefined;
|
|
942
922
|
if (coordinates.realtime && realtime) {
|
|
943
923
|
await verifyCloudflareWorkerDurableObjects({
|
|
944
924
|
accountId: coordinates.accountId,
|
|
@@ -953,8 +933,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
953
933
|
coordinates,
|
|
954
934
|
resources: {
|
|
955
935
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
956
|
-
apiToken,
|
|
957
|
-
realtime,
|
|
958
936
|
nodes: existing?.resources.nodes ?? []
|
|
959
937
|
}
|
|
960
938
|
});
|
|
@@ -973,7 +951,7 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
973
951
|
let resource;
|
|
974
952
|
let created = false;
|
|
975
953
|
if (checkpointed) {
|
|
976
|
-
if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)
|
|
954
|
+
if (checkpointed.hostname !== node.hostname || checkpointed.service !== node.service || checkpointed.tunnelName !== node.tunnelName || !UUID.test(checkpointed.tunnelId)) {
|
|
977
955
|
throw new Error(`checkpointed Cloudflare node ${node.nodeName} is malformed`);
|
|
978
956
|
}
|
|
979
957
|
resource = checkpointed;
|
|
@@ -994,15 +972,13 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
994
972
|
}, fetcher);
|
|
995
973
|
mesh = {
|
|
996
974
|
...node.mesh,
|
|
997
|
-
connectorId: ensured.connector.id
|
|
998
|
-
connectorToken: ensured.connectorToken
|
|
975
|
+
connectorId: ensured.connector.id
|
|
999
976
|
};
|
|
1000
977
|
}
|
|
1001
978
|
const { mesh: _declaredMesh, ...publicNode } = node;
|
|
1002
979
|
resource = {
|
|
1003
980
|
...publicNode,
|
|
1004
981
|
tunnelId: tunnel.tunnel.id,
|
|
1005
|
-
connectorToken: tunnel.connectorToken,
|
|
1006
982
|
...mesh ? { mesh } : {}
|
|
1007
983
|
};
|
|
1008
984
|
}
|
|
@@ -1016,8 +992,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
1016
992
|
coordinates,
|
|
1017
993
|
resources: {
|
|
1018
994
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
1019
|
-
apiToken,
|
|
1020
|
-
...realtime ? { realtime } : {},
|
|
1021
995
|
nodes: [...nodeResources]
|
|
1022
996
|
}
|
|
1023
997
|
});
|
|
@@ -1061,8 +1035,6 @@ async function applyCloudflareBootstrap(input, tokens, outputPath, fetcher = fet
|
|
|
1061
1035
|
coordinates,
|
|
1062
1036
|
resources: {
|
|
1063
1037
|
kvNamespaceId: coordinates.kvNamespaceId,
|
|
1064
|
-
apiToken,
|
|
1065
|
-
...realtime ? { realtime } : {},
|
|
1066
1038
|
nodes: nodeResources
|
|
1067
1039
|
},
|
|
1068
1040
|
created: {
|
|
@@ -1084,10 +1056,10 @@ async function runAttendedCloudflareBootstrap(request, dependencies = {}) {
|
|
|
1084
1056
|
nodes: plan.coordinates.nodes.map(({ nodeName, hostname }) => ({ nodeName, hostname }))
|
|
1085
1057
|
};
|
|
1086
1058
|
}
|
|
1087
|
-
if (!request.
|
|
1088
|
-
throw new Error("Cloudflare apply requires exactly two
|
|
1059
|
+
if (!request.tokens) {
|
|
1060
|
+
throw new Error("Cloudflare apply requires exactly two attended API tokens");
|
|
1089
1061
|
}
|
|
1090
|
-
const tokens =
|
|
1062
|
+
const tokens = validateCloudflareBootstrapTokens(request.tokens);
|
|
1091
1063
|
const output = await applyCloudflareBootstrap(plan.coordinates, tokens, plan.outputFile, dependencies.fetcher ?? fetch);
|
|
1092
1064
|
return {
|
|
1093
1065
|
format: 1,
|
|
@@ -1239,15 +1211,15 @@ async function finalizeCloudflareBootstrapAcceptance(request, fetcher = fetch) {
|
|
|
1239
1211
|
export {
|
|
1240
1212
|
writeOwnerBootstrapOutput,
|
|
1241
1213
|
verifyCloudflareBootstrapAcceptance,
|
|
1214
|
+
validateCloudflareBootstrapTokens,
|
|
1242
1215
|
validateCloudflareBootstrapCoordinates,
|
|
1243
1216
|
runAttendedCloudflareBootstrap,
|
|
1244
|
-
readOwnerApiToken,
|
|
1245
1217
|
readCloudflareHostHandoff,
|
|
1246
1218
|
readCloudflareConnectorHandoff,
|
|
1247
|
-
readCloudflareBootstrapTokens,
|
|
1248
1219
|
readCloudflareBootstrapAcceptanceEvidence,
|
|
1249
1220
|
planCloudflareBootstrap,
|
|
1250
1221
|
finalizeCloudflareBootstrapAcceptance,
|
|
1222
|
+
deriveCloudflareRealtimeSecrets,
|
|
1251
1223
|
cloudflareHostHandoffPath,
|
|
1252
1224
|
applyCloudflareBootstrap
|
|
1253
1225
|
};
|
|
@@ -184,4 +184,14 @@ export declare function ensureCloudflareTunnel(config: {
|
|
|
184
184
|
connectorToken: string;
|
|
185
185
|
created: boolean;
|
|
186
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
|
+
}>;
|
|
187
197
|
export {};
|
package/dist/cloudflare-edge.js
CHANGED
|
@@ -344,8 +344,21 @@ async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
|
344
344
|
}
|
|
345
345
|
return { tunnel, connectorToken, created };
|
|
346
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
|
+
}
|
|
347
359
|
export {
|
|
348
360
|
verifyCloudflareWorkerDurableObjects,
|
|
361
|
+
retrieveCloudflareConnectorTokens,
|
|
349
362
|
removeCloudflareWarpDatabaseInclude,
|
|
350
363
|
removeCloudflarePrivateRoute,
|
|
351
364
|
removeCloudflarePrivateDatabaseRoute,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* One credential policy for every place the Agent can run.
|
|
3
3
|
*
|
|
4
|
-
* Operator credentials are attended input
|
|
5
|
-
*
|
|
4
|
+
* Operator credentials are hidden attended input and are never persisted as a
|
|
5
|
+
* plaintext source. Physical-metal credentials are fixed host identities loaded
|
|
6
6
|
* only by systemd. Enrolled platform and tenant computes read their assigned
|
|
7
7
|
* project/environment Vault replica first and may use only an explicitly
|
|
8
8
|
* installed, same-name systemd credential while that replica cannot answer.
|
|
@@ -34,13 +34,13 @@ export declare const CLOUDFLARE_CREDENTIAL_NAMES: {
|
|
|
34
34
|
export declare const CLOUDFLARE_CREDENTIAL_SCHEMA: {
|
|
35
35
|
readonly CF_TUNNEL_TOKEN: {
|
|
36
36
|
readonly permissions: readonly ["Account:Cloudflare Tunnel Write", "Account:Cloudflare One Connector: WARP Write", "Account:Cloudflare One Networks Write", "Account:Zero Trust Write", "Zone:DNS Write"];
|
|
37
|
-
readonly platformBootstrap: "
|
|
38
|
-
readonly platformRuntime: "vault";
|
|
37
|
+
readonly platformBootstrap: "hidden-prompt-to-systemd";
|
|
38
|
+
readonly platformRuntime: "vault-then-systemd";
|
|
39
39
|
readonly tenantControl: "vault";
|
|
40
40
|
};
|
|
41
41
|
readonly CF_API_TOKEN: {
|
|
42
42
|
readonly permissions: readonly ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"];
|
|
43
|
-
readonly platformBootstrap: "
|
|
43
|
+
readonly platformBootstrap: "hidden-prompt-to-systemd";
|
|
44
44
|
readonly platformRuntime: "vault-then-systemd";
|
|
45
45
|
readonly tenantControl: "vault";
|
|
46
46
|
};
|
|
@@ -67,22 +67,22 @@ export declare const AGENT_CREDENTIAL_POLICY: {
|
|
|
67
67
|
readonly operator: {
|
|
68
68
|
readonly vault: false;
|
|
69
69
|
readonly systemdFallback: false;
|
|
70
|
-
readonly
|
|
70
|
+
readonly hiddenInput: true;
|
|
71
71
|
};
|
|
72
72
|
readonly metal: {
|
|
73
73
|
readonly vault: false;
|
|
74
74
|
readonly systemdFallback: true;
|
|
75
|
-
readonly
|
|
75
|
+
readonly hiddenInput: false;
|
|
76
76
|
};
|
|
77
77
|
readonly 'platform-compute': {
|
|
78
78
|
readonly vault: true;
|
|
79
79
|
readonly systemdFallback: true;
|
|
80
|
-
readonly
|
|
80
|
+
readonly hiddenInput: false;
|
|
81
81
|
};
|
|
82
82
|
readonly 'tenant-compute': {
|
|
83
83
|
readonly vault: true;
|
|
84
84
|
readonly systemdFallback: true;
|
|
85
|
-
readonly
|
|
85
|
+
readonly hiddenInput: false;
|
|
86
86
|
};
|
|
87
87
|
};
|
|
88
88
|
export interface DeploymentCredentialBinding {
|
|
@@ -300,13 +300,13 @@ var CLOUDFLARE_CREDENTIAL_SCHEMA = {
|
|
|
300
300
|
"Account:Zero Trust Write",
|
|
301
301
|
"Zone:DNS Write"
|
|
302
302
|
],
|
|
303
|
-
platformBootstrap: "
|
|
304
|
-
platformRuntime: "vault",
|
|
303
|
+
platformBootstrap: "hidden-prompt-to-systemd",
|
|
304
|
+
platformRuntime: "vault-then-systemd",
|
|
305
305
|
tenantControl: "vault"
|
|
306
306
|
},
|
|
307
307
|
CF_API_TOKEN: {
|
|
308
308
|
permissions: ["Account:Workers KV Storage Write", "Account:Workers Scripts Write"],
|
|
309
|
-
platformBootstrap: "
|
|
309
|
+
platformBootstrap: "hidden-prompt-to-systemd",
|
|
310
310
|
platformRuntime: "vault-then-systemd",
|
|
311
311
|
tenantControl: "vault"
|
|
312
312
|
},
|
|
@@ -330,10 +330,10 @@ var CLOUDFLARE_CREDENTIAL_SCHEMA = {
|
|
|
330
330
|
}
|
|
331
331
|
};
|
|
332
332
|
var AGENT_CREDENTIAL_POLICY = {
|
|
333
|
-
operator: { vault: false, systemdFallback: false,
|
|
334
|
-
metal: { vault: false, systemdFallback: true,
|
|
335
|
-
"platform-compute": { vault: true, systemdFallback: true,
|
|
336
|
-
"tenant-compute": { vault: true, systemdFallback: true,
|
|
333
|
+
operator: { vault: false, systemdFallback: false, hiddenInput: true },
|
|
334
|
+
metal: { vault: false, systemdFallback: true, hiddenInput: false },
|
|
335
|
+
"platform-compute": { vault: true, systemdFallback: true, hiddenInput: false },
|
|
336
|
+
"tenant-compute": { vault: true, systemdFallback: true, hiddenInput: false }
|
|
337
337
|
};
|
|
338
338
|
var CREDENTIAL_NAME = /^[A-Z_][A-Z0-9_]*$/;
|
|
339
339
|
var SCOPE_PART2 = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|