@forgezero/agent 0.1.38 → 0.1.40
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 +118 -12
- package/dist/agent-heartbeat.d.ts +13 -0
- package/dist/agent-heartbeat.js +24 -14
- package/dist/agent-update-helper.js +15 -13
- package/dist/agent-update.js +15 -13
- package/dist/bootstrap.d.ts +49 -4
- package/dist/bootstrap.js +660 -791
- package/dist/cli/cloudflare-bootstrap.d.ts +5 -1
- package/dist/cli/index.d.ts +0 -2
- package/dist/cloudflare-bootstrap.d.ts +36 -114
- package/dist/cloudflare-bootstrap.js +237 -738
- package/dist/cloudflare-edge.d.ts +3 -151
- package/dist/cloudflare-edge.js +28 -248
- package/dist/credential-schema.d.ts +60 -0
- package/dist/credential-schema.js +335 -0
- package/dist/definition.js +2 -2
- package/dist/deploy-file.js +2 -2
- package/dist/deployment-pull.d.ts +2 -0
- package/dist/fz-agent.js +6407 -58
- package/dist/fz.js +10668 -1151
- package/dist/index.d.ts +33 -0
- package/dist/metal-bootstrap.d.ts +1 -0
- package/dist/metal-bootstrap.js +5 -3
- package/dist/platform-bootstrap-runtime.d.ts +5 -28
- package/dist/platform-bootstrap-runtime.js +26 -77
- package/dist/project-context.js +1 -1
- package/dist/provision.js +18 -16
- package/dist/software-helper.js +2 -2
- package/dist/software.js +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +8 -4
- package/dist/cli/custody.d.ts +0 -35
|
@@ -86,162 +86,14 @@ export declare function removeCloudflareWarpDatabaseInclude(config: Omit<Cloudfl
|
|
|
86
86
|
/** Reconcile one remotely-managed tunnel route and its proxied DNS record. */
|
|
87
87
|
export declare function configureCloudflareEdge(config: CloudflareEdgeConfig, fetcher?: typeof fetch): Promise<void>;
|
|
88
88
|
export interface CloudflareApiTokens {
|
|
89
|
-
/**
|
|
89
|
+
/** Backward-compatible single-token input; two scoped tokens are preferred. */
|
|
90
90
|
apiToken?: string;
|
|
91
|
-
/**
|
|
91
|
+
/** Attended management token mapped to Tunnel and DNS operations. */
|
|
92
92
|
tunnelApiToken?: string;
|
|
93
93
|
dnsApiToken?: string;
|
|
94
|
+
/** Runtime token used only for KV node-directory writes. */
|
|
94
95
|
kvApiToken?: string;
|
|
95
|
-
accessApiToken?: string;
|
|
96
|
-
workerApiToken?: string;
|
|
97
96
|
}
|
|
98
|
-
export interface CloudflareKvNamespace {
|
|
99
|
-
id: string;
|
|
100
|
-
title: string;
|
|
101
|
-
}
|
|
102
|
-
export interface CloudflareAccessServiceToken {
|
|
103
|
-
id: string;
|
|
104
|
-
name: string;
|
|
105
|
-
client_id: string;
|
|
106
|
-
client_secret?: string;
|
|
107
|
-
duration?: string;
|
|
108
|
-
}
|
|
109
|
-
export interface CloudflareAccessCredentials {
|
|
110
|
-
tokenId: string;
|
|
111
|
-
clientId: string;
|
|
112
|
-
clientSecret: string;
|
|
113
|
-
}
|
|
114
|
-
export interface CloudflareAccountRuntimeToken {
|
|
115
|
-
id: string;
|
|
116
|
-
value: string;
|
|
117
|
-
name: string;
|
|
118
|
-
permissionNames: string[];
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Create one account-owned API token from exact, dynamically resolved account
|
|
122
|
-
* permission groups. The value is returned once for immediate 0600 checkpoint.
|
|
123
|
-
*/
|
|
124
|
-
export declare function createCloudflareAccountRuntimeToken(config: {
|
|
125
|
-
accountId: string;
|
|
126
|
-
name: string;
|
|
127
|
-
permissionNames: readonly string[];
|
|
128
|
-
apiToken: string;
|
|
129
|
-
}, fetcher?: typeof fetch): Promise<CloudflareAccountRuntimeToken>;
|
|
130
|
-
/**
|
|
131
|
-
* Create the Worker's Access service token, or prove the caller supplied the
|
|
132
|
-
* sealed secret for the existing token. Cloudflare intentionally returns the
|
|
133
|
-
* client secret only once, so silently "reusing" an existing row without its
|
|
134
|
-
* secret would configure a Worker that can never authenticate.
|
|
135
|
-
*/
|
|
136
|
-
export declare function ensureCloudflareAccessServiceToken(config: {
|
|
137
|
-
accountId: string;
|
|
138
|
-
name: string;
|
|
139
|
-
duration?: string;
|
|
140
|
-
apiToken: string;
|
|
141
|
-
existing?: CloudflareAccessCredentials;
|
|
142
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
143
|
-
credentials: CloudflareAccessCredentials;
|
|
144
|
-
created: boolean;
|
|
145
|
-
}>;
|
|
146
|
-
interface CloudflareAccessPolicy {
|
|
147
|
-
id: string;
|
|
148
|
-
name: string;
|
|
149
|
-
decision?: string;
|
|
150
|
-
include?: unknown[];
|
|
151
|
-
}
|
|
152
|
-
/** Create or update one reusable Service Auth policy for the Worker token. */
|
|
153
|
-
export declare function ensureCloudflareAccessPolicy(config: {
|
|
154
|
-
accountId: string;
|
|
155
|
-
name: string;
|
|
156
|
-
serviceTokenId: string;
|
|
157
|
-
apiToken: string;
|
|
158
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
159
|
-
policy: CloudflareAccessPolicy;
|
|
160
|
-
created: boolean;
|
|
161
|
-
}>;
|
|
162
|
-
interface CloudflareAccessApplication {
|
|
163
|
-
id: string;
|
|
164
|
-
name: string;
|
|
165
|
-
type?: string;
|
|
166
|
-
domain?: string;
|
|
167
|
-
self_hosted_domains?: string[];
|
|
168
|
-
}
|
|
169
|
-
/** Protect one node hostname so only the public Worker can reach it. */
|
|
170
|
-
export declare function ensureCloudflareAccessApplication(config: {
|
|
171
|
-
accountId: string;
|
|
172
|
-
name: string;
|
|
173
|
-
hostname: string;
|
|
174
|
-
policyId: string;
|
|
175
|
-
apiToken: string;
|
|
176
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
177
|
-
application: CloudflareAccessApplication;
|
|
178
|
-
created: boolean;
|
|
179
|
-
}>;
|
|
180
|
-
/** Attach the same non-interactive service-auth policy to WARP enrollment. */
|
|
181
|
-
export declare function ensureCloudflareWarpEnrollmentApplication(config: {
|
|
182
|
-
accountId: string;
|
|
183
|
-
name: string;
|
|
184
|
-
policyId: string;
|
|
185
|
-
apiToken: string;
|
|
186
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
187
|
-
application: CloudflareAccessApplication;
|
|
188
|
-
created: boolean;
|
|
189
|
-
}>;
|
|
190
|
-
export interface CloudflareVirtualNetwork {
|
|
191
|
-
id: string;
|
|
192
|
-
name: string;
|
|
193
|
-
comment?: string;
|
|
194
|
-
is_default_network?: boolean;
|
|
195
|
-
deleted_at?: string | null;
|
|
196
|
-
}
|
|
197
|
-
/** Create or reuse one named VNET; never silently adopt an ambiguous/deleted row. */
|
|
198
|
-
export declare function ensureCloudflareVirtualNetwork(config: {
|
|
199
|
-
accountId: string;
|
|
200
|
-
name: string;
|
|
201
|
-
comment: string;
|
|
202
|
-
apiToken: string;
|
|
203
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
204
|
-
virtualNetwork: CloudflareVirtualNetwork;
|
|
205
|
-
created: boolean;
|
|
206
|
-
}>;
|
|
207
|
-
interface CloudflareDevicePolicy {
|
|
208
|
-
id: string;
|
|
209
|
-
name: string;
|
|
210
|
-
match?: string;
|
|
211
|
-
precedence?: number;
|
|
212
|
-
}
|
|
213
|
-
/**
|
|
214
|
-
* Create a locked WARP device profile selected only by this service token.
|
|
215
|
-
* Split-tunnel includes are reconciled separately so existing owned entries
|
|
216
|
-
* remain visible and reviewable.
|
|
217
|
-
*/
|
|
218
|
-
export declare function ensureCloudflareWarpDevicePolicy(config: {
|
|
219
|
-
accountId: string;
|
|
220
|
-
name: string;
|
|
221
|
-
serviceTokenId: string;
|
|
222
|
-
virtualNetworkId: string;
|
|
223
|
-
precedence?: number;
|
|
224
|
-
apiToken: string;
|
|
225
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
226
|
-
policy: CloudflareDevicePolicy;
|
|
227
|
-
created: boolean;
|
|
228
|
-
}>;
|
|
229
|
-
/** Atomically replace both Worker-side Access credentials. */
|
|
230
|
-
export declare function configureCloudflareWorkerAccessSecrets(config: {
|
|
231
|
-
accountId: string;
|
|
232
|
-
scriptName: string;
|
|
233
|
-
credentials: CloudflareAccessCredentials;
|
|
234
|
-
apiToken: string;
|
|
235
|
-
}, fetcher?: typeof fetch): Promise<void>;
|
|
236
|
-
/** Create or reuse exactly one named Worker KV namespace. */
|
|
237
|
-
export declare function ensureCloudflareKvNamespace(config: {
|
|
238
|
-
accountId: string;
|
|
239
|
-
title: string;
|
|
240
|
-
apiToken: string;
|
|
241
|
-
}, fetcher?: typeof fetch): Promise<{
|
|
242
|
-
namespace: CloudflareKvNamespace;
|
|
243
|
-
created: boolean;
|
|
244
|
-
}>;
|
|
245
97
|
export interface CloudflareTunnel {
|
|
246
98
|
id: string;
|
|
247
99
|
name: string;
|
package/dist/cloudflare-edge.js
CHANGED
|
@@ -130,23 +130,32 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
130
130
|
const tunnelAuth = { apiToken: config.tunnelApiToken?.trim() || config.apiToken };
|
|
131
131
|
const dnsAuth = { apiToken: config.dnsApiToken?.trim() || config.apiToken };
|
|
132
132
|
const tunnelPath = `/accounts/${config.accountId}/cfd_tunnel/${config.tunnelId}/configurations`;
|
|
133
|
-
const current = await cf(tunnelAuth, tunnelPath, {}, fetcher);
|
|
134
|
-
const existing = current.config?.ingress ?? [];
|
|
135
|
-
const catchAll = existing.filter((rule) => !("hostname" in rule));
|
|
136
|
-
const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
|
|
137
|
-
await cf(tunnelAuth, tunnelPath, {
|
|
138
|
-
method: "PUT",
|
|
139
|
-
body: JSON.stringify({ config: { ingress: [
|
|
140
|
-
{ hostname: config.hostname, service: config.service },
|
|
141
|
-
...otherHosts,
|
|
142
|
-
...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
|
|
143
|
-
] } })
|
|
144
|
-
}, fetcher);
|
|
145
133
|
const dnsPath = `/zones/${config.zoneId}/dns_records`;
|
|
146
|
-
const records = await
|
|
134
|
+
const [current, records] = await Promise.all([
|
|
135
|
+
cf(tunnelAuth, tunnelPath, {}, fetcher),
|
|
136
|
+
cf(dnsAuth, `${dnsPath}?name=${encodeURIComponent(config.hostname)}&per_page=1000`, {}, fetcher)
|
|
137
|
+
]);
|
|
147
138
|
if (records.length > 1) {
|
|
148
139
|
throw new Error(`Cloudflare DNS record for ${config.hostname} is ambiguous`);
|
|
149
140
|
}
|
|
141
|
+
const existingRecord = records[0];
|
|
142
|
+
if (existingRecord && (existingRecord.type !== "CNAME" || existingRecord.name && existingRecord.name.toLowerCase() !== config.hostname.toLowerCase())) {
|
|
143
|
+
throw new Error(`Cloudflare DNS hostname ${config.hostname} is already owned by an incompatible ${existingRecord.type ?? "unknown"} record`);
|
|
144
|
+
}
|
|
145
|
+
const existing = current.config?.ingress ?? [];
|
|
146
|
+
const catchAll = existing.filter((rule) => !("hostname" in rule));
|
|
147
|
+
const otherHosts = existing.filter((rule) => ("hostname" in rule) && rule.hostname !== config.hostname);
|
|
148
|
+
const desiredIngress = [
|
|
149
|
+
{ hostname: config.hostname, service: config.service },
|
|
150
|
+
...otherHosts,
|
|
151
|
+
...catchAll.length > 0 ? catchAll : [{ service: "http_status:404" }]
|
|
152
|
+
];
|
|
153
|
+
if (JSON.stringify(existing) !== JSON.stringify(desiredIngress)) {
|
|
154
|
+
await cf(tunnelAuth, tunnelPath, {
|
|
155
|
+
method: "PUT",
|
|
156
|
+
body: JSON.stringify({ config: { ingress: desiredIngress } })
|
|
157
|
+
}, fetcher);
|
|
158
|
+
}
|
|
150
159
|
const record = {
|
|
151
160
|
type: "CNAME",
|
|
152
161
|
name: config.hostname,
|
|
@@ -154,233 +163,13 @@ async function configureCloudflareEdge(config, fetcher = fetch) {
|
|
|
154
163
|
proxied: true,
|
|
155
164
|
ttl: 1
|
|
156
165
|
};
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const groups = await cf(config, `/accounts/${config.accountId}/tokens/permission_groups?name=${encodeURIComponent(name)}` + "&scope=com.cloudflare.api.account", {}, fetcher);
|
|
164
|
-
const matches = groups.filter((group) => group.name === name && group.scopes?.includes("com.cloudflare.api.account") && Boolean(group.id && /^[a-f0-9]{32}$/i.test(group.id)));
|
|
165
|
-
if (matches.length !== 1) {
|
|
166
|
-
throw new Error(`Cloudflare account token permission group ${name} is ${matches.length === 0 ? "missing" : "ambiguous"}`);
|
|
167
|
-
}
|
|
168
|
-
return { id: matches[0].id, name };
|
|
169
|
-
};
|
|
170
|
-
async function createCloudflareAccountRuntimeToken(config, fetcher = fetch) {
|
|
171
|
-
if (!/^[a-f0-9]{32}$/i.test(config.accountId))
|
|
172
|
-
throw new Error("Cloudflare account id is invalid");
|
|
173
|
-
const name = config.name.trim();
|
|
174
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,119}$/.test(name)) {
|
|
175
|
-
throw new Error("Cloudflare account runtime-token name is invalid");
|
|
176
|
-
}
|
|
177
|
-
const permissionNames = [...new Set(config.permissionNames)];
|
|
178
|
-
if (permissionNames.length === 0)
|
|
179
|
-
throw new Error("Cloudflare account runtime token needs a permission group");
|
|
180
|
-
const permissionGroups = await Promise.all(permissionNames.map((permissionName) => exactAccountPermissionGroup(config, permissionName, fetcher)));
|
|
181
|
-
const body = {
|
|
182
|
-
name,
|
|
183
|
-
policies: [{
|
|
184
|
-
effect: "allow",
|
|
185
|
-
permission_groups: permissionGroups.map(({ id }) => ({ id })),
|
|
186
|
-
resources: { [`com.cloudflare.api.account.${config.accountId}`]: "*" }
|
|
187
|
-
}]
|
|
188
|
-
};
|
|
189
|
-
const created = await cf(config, `/accounts/${config.accountId}/tokens`, { method: "POST", body: JSON.stringify(body) }, fetcher);
|
|
190
|
-
if (!created.id || !/^[a-f0-9]{32}$/i.test(created.id) || !created.value || !/^[A-Za-z0-9._-]{40,80}$/.test(created.value)) {
|
|
191
|
-
throw new Error("Cloudflare did not return the one-time account runtime-token id and value");
|
|
192
|
-
}
|
|
193
|
-
return { id: created.id, value: created.value, name, permissionNames };
|
|
194
|
-
}
|
|
195
|
-
async function ensureCloudflareAccessServiceToken(config, fetcher = fetch) {
|
|
196
|
-
const name = config.name.trim();
|
|
197
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
|
|
198
|
-
throw new Error("Cloudflare Access service-token name is invalid");
|
|
199
|
-
}
|
|
200
|
-
const path = `/accounts/${config.accountId}/access/service_tokens`;
|
|
201
|
-
const tokens = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
202
|
-
const matches = tokens.filter((token) => token.name === name);
|
|
203
|
-
if (matches.length > 1)
|
|
204
|
-
throw new Error(`Cloudflare Access service token ${name} is ambiguous`);
|
|
205
|
-
if (matches[0]) {
|
|
206
|
-
if (!config.existing || config.existing.tokenId !== matches[0].id || config.existing.clientId !== matches[0].client_id || !config.existing.clientSecret) {
|
|
207
|
-
throw new Error(`Cloudflare Access service token ${name} exists but its one-time client secret was not supplied`);
|
|
208
|
-
}
|
|
209
|
-
return { credentials: config.existing, created: false };
|
|
210
|
-
}
|
|
211
|
-
const created = await cf(config, path, {
|
|
212
|
-
method: "POST",
|
|
213
|
-
body: JSON.stringify({ name, duration: config.duration ?? "8760h" })
|
|
214
|
-
}, fetcher);
|
|
215
|
-
if (!created.id || !created.client_id || !created.client_secret) {
|
|
216
|
-
throw new Error("Cloudflare did not return the new Access service-token secret");
|
|
166
|
+
const dnsAlreadyCorrect = existingRecord?.type === record.type && existingRecord.name?.toLowerCase() === record.name.toLowerCase() && existingRecord.content?.toLowerCase() === record.content.toLowerCase() && existingRecord.proxied === true && existingRecord.ttl === 1;
|
|
167
|
+
if (!dnsAlreadyCorrect) {
|
|
168
|
+
await cf(dnsAuth, existingRecord ? `${dnsPath}/${encodeURIComponent(existingRecord.id)}` : dnsPath, {
|
|
169
|
+
method: existingRecord ? "PUT" : "POST",
|
|
170
|
+
body: JSON.stringify(record)
|
|
171
|
+
}, fetcher);
|
|
217
172
|
}
|
|
218
|
-
return {
|
|
219
|
-
credentials: {
|
|
220
|
-
tokenId: created.id,
|
|
221
|
-
clientId: created.client_id,
|
|
222
|
-
clientSecret: created.client_secret
|
|
223
|
-
},
|
|
224
|
-
created: true
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
async function ensureCloudflareAccessPolicy(config, fetcher = fetch) {
|
|
228
|
-
const path = `/accounts/${config.accountId}/access/policies`;
|
|
229
|
-
const policies = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
230
|
-
const matches = policies.filter((policy2) => policy2.name === config.name);
|
|
231
|
-
if (matches.length > 1)
|
|
232
|
-
throw new Error(`Cloudflare Access policy ${config.name} is ambiguous`);
|
|
233
|
-
const desired = {
|
|
234
|
-
name: config.name,
|
|
235
|
-
decision: "non_identity",
|
|
236
|
-
include: [{ service_token: { token_id: config.serviceTokenId } }]
|
|
237
|
-
};
|
|
238
|
-
const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, {
|
|
239
|
-
method: matches[0] ? "PUT" : "POST",
|
|
240
|
-
body: JSON.stringify(desired)
|
|
241
|
-
}, fetcher);
|
|
242
|
-
return { policy, created: !matches[0] };
|
|
243
|
-
}
|
|
244
|
-
async function ensureCloudflareAccessApplication(config, fetcher = fetch) {
|
|
245
|
-
const path = `/accounts/${config.accountId}/access/apps`;
|
|
246
|
-
const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
247
|
-
const matches = applications.filter((application2) => application2.domain === config.hostname || application2.self_hosted_domains?.includes(config.hostname));
|
|
248
|
-
if (matches.length > 1)
|
|
249
|
-
throw new Error(`Cloudflare Access application for ${config.hostname} is ambiguous`);
|
|
250
|
-
const desired = {
|
|
251
|
-
name: config.name,
|
|
252
|
-
type: "self_hosted",
|
|
253
|
-
domain: config.hostname,
|
|
254
|
-
session_duration: "24h",
|
|
255
|
-
service_auth_401_redirect: true,
|
|
256
|
-
policies: [{ id: config.policyId, precedence: 1 }]
|
|
257
|
-
};
|
|
258
|
-
const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
|
|
259
|
-
return { application, created: !matches[0] };
|
|
260
|
-
}
|
|
261
|
-
async function ensureCloudflareWarpEnrollmentApplication(config, fetcher = fetch) {
|
|
262
|
-
const name = config.name.trim();
|
|
263
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name)) {
|
|
264
|
-
throw new Error("Cloudflare WARP enrollment application name is invalid");
|
|
265
|
-
}
|
|
266
|
-
const path = `/accounts/${config.accountId}/access/apps`;
|
|
267
|
-
const applications = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
268
|
-
const matches = applications.filter((application2) => application2.type === "warp" || application2.name === name);
|
|
269
|
-
if (matches.length > 1)
|
|
270
|
-
throw new Error(`Cloudflare WARP enrollment application ${name} is ambiguous`);
|
|
271
|
-
if (matches[0] && (matches[0].type !== "warp" || matches[0].name !== name)) {
|
|
272
|
-
throw new Error(`Cloudflare Access application ${name} is not the owned WARP enrollment application`);
|
|
273
|
-
}
|
|
274
|
-
const desired = {
|
|
275
|
-
name,
|
|
276
|
-
type: "warp",
|
|
277
|
-
policies: [{ id: config.policyId, precedence: 1 }]
|
|
278
|
-
};
|
|
279
|
-
const application = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PUT" : "POST", body: JSON.stringify(desired) }, fetcher);
|
|
280
|
-
if (!application.id || application.type && application.type !== "warp") {
|
|
281
|
-
throw new Error("Cloudflare did not return the WARP enrollment application");
|
|
282
|
-
}
|
|
283
|
-
return { application, created: !matches[0] };
|
|
284
|
-
}
|
|
285
|
-
async function ensureCloudflareVirtualNetwork(config, fetcher = fetch) {
|
|
286
|
-
const name = config.name.trim();
|
|
287
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
|
|
288
|
-
throw new Error("Cloudflare VNET name is invalid");
|
|
289
|
-
const path = `/accounts/${config.accountId}/teamnet/virtual_networks`;
|
|
290
|
-
const networks = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
291
|
-
const matches = networks.filter((network) => !network.deleted_at && network.name === name);
|
|
292
|
-
if (matches.length > 1)
|
|
293
|
-
throw new Error(`Cloudflare VNET ${name} is ambiguous`);
|
|
294
|
-
if (matches[0])
|
|
295
|
-
return { virtualNetwork: matches[0], created: false };
|
|
296
|
-
const virtualNetwork = await cf(config, path, {
|
|
297
|
-
method: "POST",
|
|
298
|
-
body: JSON.stringify({ name, comment: config.comment.slice(0, 256), is_default_network: false })
|
|
299
|
-
}, fetcher);
|
|
300
|
-
if (!virtualNetwork.id || !/^[0-9a-f-]{36}$/i.test(virtualNetwork.id)) {
|
|
301
|
-
throw new Error("Cloudflare did not return the VNET id");
|
|
302
|
-
}
|
|
303
|
-
return { virtualNetwork, created: true };
|
|
304
|
-
}
|
|
305
|
-
async function ensureCloudflareWarpDevicePolicy(config, fetcher = fetch) {
|
|
306
|
-
const name = config.name.trim();
|
|
307
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]{0,99}$/.test(name))
|
|
308
|
-
throw new Error("Cloudflare WARP device profile name is invalid");
|
|
309
|
-
if (!/^[A-Za-z0-9_-]{1,128}$/.test(config.serviceTokenId))
|
|
310
|
-
throw new Error("Cloudflare service-token id is invalid");
|
|
311
|
-
if (!/^[0-9a-f-]{36}$/i.test(config.virtualNetworkId))
|
|
312
|
-
throw new Error("Cloudflare VNET id is invalid");
|
|
313
|
-
const precedence = config.precedence ?? 100;
|
|
314
|
-
if (!Number.isInteger(precedence) || precedence < 1 || precedence > 999999) {
|
|
315
|
-
throw new Error("Cloudflare WARP device profile precedence is invalid");
|
|
316
|
-
}
|
|
317
|
-
const match = `identity.service_token_uuid == "${config.serviceTokenId}"`;
|
|
318
|
-
const listPath = `/accounts/${config.accountId}/devices/policies`;
|
|
319
|
-
const path = `/accounts/${config.accountId}/devices/policy`;
|
|
320
|
-
const policies = await cf(config, `${listPath}?per_page=1000`, {}, fetcher);
|
|
321
|
-
const matches = policies.filter((policy2) => policy2.name === name);
|
|
322
|
-
if (matches.length > 1)
|
|
323
|
-
throw new Error(`Cloudflare WARP device profile ${name} is ambiguous`);
|
|
324
|
-
if (matches[0]?.match && matches[0].match !== match) {
|
|
325
|
-
throw new Error(`Cloudflare WARP device profile ${name} belongs to another enrollment identity`);
|
|
326
|
-
}
|
|
327
|
-
const desired = {
|
|
328
|
-
name,
|
|
329
|
-
match,
|
|
330
|
-
precedence,
|
|
331
|
-
description: "ForgeZero non-interactive compute enrollment",
|
|
332
|
-
enabled: true,
|
|
333
|
-
allow_mode_switch: false,
|
|
334
|
-
allowed_to_leave: false,
|
|
335
|
-
auto_connect: 0,
|
|
336
|
-
switch_locked: true,
|
|
337
|
-
service_mode_v2: { mode: "warp" },
|
|
338
|
-
virtual_networks: { allowed: [config.virtualNetworkId], default: config.virtualNetworkId }
|
|
339
|
-
};
|
|
340
|
-
const policy = await cf(config, matches[0] ? `${path}/${encodeURIComponent(matches[0].id)}` : path, { method: matches[0] ? "PATCH" : "POST", body: JSON.stringify(desired) }, fetcher);
|
|
341
|
-
if (!policy.id)
|
|
342
|
-
throw new Error("Cloudflare did not return the WARP device profile id");
|
|
343
|
-
return { policy, created: !matches[0] };
|
|
344
|
-
}
|
|
345
|
-
async function configureCloudflareWorkerAccessSecrets(config, fetcher = fetch) {
|
|
346
|
-
if (!/^[a-z][a-z0-9-]{0,62}$/.test(config.scriptName)) {
|
|
347
|
-
throw new Error("Cloudflare Worker script name is invalid");
|
|
348
|
-
}
|
|
349
|
-
await cf(config, `/accounts/${config.accountId}/workers/scripts/${config.scriptName}/secrets-bulk`, {
|
|
350
|
-
method: "PATCH",
|
|
351
|
-
body: JSON.stringify({
|
|
352
|
-
secrets: {
|
|
353
|
-
CF_ACCESS_CLIENT_ID: {
|
|
354
|
-
name: "CF_ACCESS_CLIENT_ID",
|
|
355
|
-
type: "secret_text",
|
|
356
|
-
text: config.credentials.clientId
|
|
357
|
-
},
|
|
358
|
-
CF_ACCESS_CLIENT_SECRET: {
|
|
359
|
-
name: "CF_ACCESS_CLIENT_SECRET",
|
|
360
|
-
type: "secret_text",
|
|
361
|
-
text: config.credentials.clientSecret
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
})
|
|
365
|
-
}, fetcher);
|
|
366
|
-
}
|
|
367
|
-
async function ensureCloudflareKvNamespace(config, fetcher = fetch) {
|
|
368
|
-
const title = config.title.trim();
|
|
369
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(title)) {
|
|
370
|
-
throw new Error("Cloudflare KV namespace title is invalid");
|
|
371
|
-
}
|
|
372
|
-
const path = `/accounts/${config.accountId}/storage/kv/namespaces`;
|
|
373
|
-
const namespaces = await cf(config, `${path}?per_page=1000`, {}, fetcher);
|
|
374
|
-
const matches = namespaces.filter((namespace2) => namespace2.title === title);
|
|
375
|
-
if (matches.length > 1)
|
|
376
|
-
throw new Error(`Cloudflare KV namespace ${title} is ambiguous`);
|
|
377
|
-
if (matches[0])
|
|
378
|
-
return { namespace: matches[0], created: false };
|
|
379
|
-
const namespace = await cf(config, path, {
|
|
380
|
-
method: "POST",
|
|
381
|
-
body: JSON.stringify({ title })
|
|
382
|
-
}, fetcher);
|
|
383
|
-
return { namespace, created: true };
|
|
384
173
|
}
|
|
385
174
|
async function ensureCloudflareTunnel(config, fetcher = fetch) {
|
|
386
175
|
const name = config.name.trim();
|
|
@@ -407,18 +196,9 @@ export {
|
|
|
407
196
|
removeCloudflareWarpDatabaseInclude,
|
|
408
197
|
removeCloudflarePrivateRoute,
|
|
409
198
|
removeCloudflarePrivateDatabaseRoute,
|
|
410
|
-
ensureCloudflareWarpEnrollmentApplication,
|
|
411
|
-
ensureCloudflareWarpDevicePolicy,
|
|
412
199
|
ensureCloudflareWarpDatabaseInclude,
|
|
413
|
-
ensureCloudflareVirtualNetwork,
|
|
414
200
|
ensureCloudflareTunnel,
|
|
415
201
|
ensureCloudflarePrivateRoute,
|
|
416
202
|
ensureCloudflarePrivateDatabaseRoute,
|
|
417
|
-
ensureCloudflareKvNamespace,
|
|
418
|
-
ensureCloudflareAccessServiceToken,
|
|
419
|
-
ensureCloudflareAccessPolicy,
|
|
420
|
-
ensureCloudflareAccessApplication,
|
|
421
|
-
createCloudflareAccountRuntimeToken,
|
|
422
|
-
configureCloudflareWorkerAccessSecrets,
|
|
423
203
|
configureCloudflareEdge
|
|
424
204
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One credential policy for every place the Agent can run.
|
|
3
|
+
*
|
|
4
|
+
* Operator credentials are attended input files and are never installed as a
|
|
5
|
+
* runtime source. Physical-metal credentials are fixed host identities loaded
|
|
6
|
+
* only by systemd. Enrolled platform and tenant computes read their assigned
|
|
7
|
+
* project/environment Vault replica first and may use only an explicitly
|
|
8
|
+
* installed, same-name systemd credential while that replica cannot answer.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AGENT_CREDENTIAL_LOCATIONS: readonly ["operator", "metal", "platform-compute", "tenant-compute"];
|
|
11
|
+
export type AgentCredentialLocation = (typeof AGENT_CREDENTIAL_LOCATIONS)[number];
|
|
12
|
+
export declare const AGENT_CREDENTIAL_POLICY: {
|
|
13
|
+
readonly operator: {
|
|
14
|
+
readonly vault: false;
|
|
15
|
+
readonly systemdFallback: false;
|
|
16
|
+
readonly attendedFile: true;
|
|
17
|
+
};
|
|
18
|
+
readonly metal: {
|
|
19
|
+
readonly vault: false;
|
|
20
|
+
readonly systemdFallback: true;
|
|
21
|
+
readonly attendedFile: false;
|
|
22
|
+
};
|
|
23
|
+
readonly 'platform-compute': {
|
|
24
|
+
readonly vault: true;
|
|
25
|
+
readonly systemdFallback: true;
|
|
26
|
+
readonly attendedFile: false;
|
|
27
|
+
};
|
|
28
|
+
readonly 'tenant-compute': {
|
|
29
|
+
readonly vault: true;
|
|
30
|
+
readonly systemdFallback: true;
|
|
31
|
+
readonly attendedFile: false;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
export interface DeploymentCredentialBinding {
|
|
35
|
+
/** Exact environment variable requested by a checked-in pipeline step. */
|
|
36
|
+
name: string;
|
|
37
|
+
/** Exact RAM-replica cache key; never a secret value. */
|
|
38
|
+
vaultCacheKey: string;
|
|
39
|
+
/** Same name exposed by systemd, or absent when no fallback was installed. */
|
|
40
|
+
systemdCredential?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface DeploymentCredentialSchema {
|
|
43
|
+
version: 1;
|
|
44
|
+
location: 'platform-compute' | 'tenant-compute';
|
|
45
|
+
projectKey: string;
|
|
46
|
+
environmentKey: string;
|
|
47
|
+
credentials: readonly DeploymentCredentialBinding[];
|
|
48
|
+
}
|
|
49
|
+
/** Build the runtime-only schema from the server-owned enrolment binding. */
|
|
50
|
+
export declare function deploymentCredentialSchema(input: {
|
|
51
|
+
realm: 'platform' | 'tenant';
|
|
52
|
+
projectKey: string;
|
|
53
|
+
environmentKey: string;
|
|
54
|
+
systemdCredentials?: Iterable<string>;
|
|
55
|
+
}): DeploymentCredentialSchema;
|
|
56
|
+
export declare function credentialBinding(schema: DeploymentCredentialSchema, name: string): DeploymentCredentialBinding;
|
|
57
|
+
/** Fixed host credentials are schema entries too, but can never enter a deploy step. */
|
|
58
|
+
export declare const METAL_SYSTEMD_CREDENTIALS: {
|
|
59
|
+
readonly identity: "metal-agent-seed";
|
|
60
|
+
};
|