@framers/agentos-ext-cloud-cloudflare-pages 0.1.0
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/LICENSE +98 -0
- package/dist/CloudflareService.d.ts +191 -0
- package/dist/CloudflareService.d.ts.map +1 -0
- package/dist/CloudflareService.js +383 -0
- package/dist/CloudflareService.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/configureDns.d.ts +83 -0
- package/dist/tools/configureDns.d.ts.map +1 -0
- package/dist/tools/configureDns.js +108 -0
- package/dist/tools/configureDns.js.map +1 -0
- package/dist/tools/createWorker.d.ts +57 -0
- package/dist/tools/createWorker.d.ts.map +1 -0
- package/dist/tools/createWorker.js +48 -0
- package/dist/tools/createWorker.js.map +1 -0
- package/dist/tools/deploy.d.ts +59 -0
- package/dist/tools/deploy.d.ts.map +1 -0
- package/dist/tools/deploy.js +45 -0
- package/dist/tools/deploy.js.map +1 -0
- package/dist/tools/listProjects.d.ts +33 -0
- package/dist/tools/listProjects.d.ts.map +1 -0
- package/dist/tools/listProjects.js +34 -0
- package/dist/tools/listProjects.js.map +1 -0
- package/manifest.json +31 -0
- package/package.json +67 -0
- package/src/CloudflareService.ts +606 -0
- package/src/index.ts +129 -0
- package/src/tools/configureDns.ts +130 -0
- package/src/tools/createWorker.ts +58 -0
- package/src/tools/deploy.ts +56 -0
- package/src/tools/listProjects.ts +35 -0
- package/test/CloudflareService.test.ts +409 -0
- package/test/index.test.ts +56 -0
- package/test/tools.test.ts +336 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Cloudflare REST API service layer.
|
|
4
|
+
*
|
|
5
|
+
* Wraps the Cloudflare API v4 for Pages project deployment, DNS record
|
|
6
|
+
* management, and Worker script deployment.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Types
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface CloudflareConfig {
|
|
14
|
+
/** Cloudflare API token (from https://dash.cloudflare.com/profile/api-tokens) */
|
|
15
|
+
apiToken: string;
|
|
16
|
+
/** Cloudflare Account ID */
|
|
17
|
+
accountId: string;
|
|
18
|
+
/** API base URL (defaults to https://api.cloudflare.com/client/v4) */
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CloudflarePagesProject {
|
|
23
|
+
id: string;
|
|
24
|
+
name: string;
|
|
25
|
+
subdomain: string;
|
|
26
|
+
productionBranch: string;
|
|
27
|
+
createdOn: string;
|
|
28
|
+
domains: string[];
|
|
29
|
+
source?: { type: string; config?: { owner: string; repo_name: string; production_branch: string } };
|
|
30
|
+
latestDeployment?: CloudflareDeployment;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CloudflareDeployment {
|
|
34
|
+
id: string;
|
|
35
|
+
projectId: string;
|
|
36
|
+
projectName: string;
|
|
37
|
+
url: string;
|
|
38
|
+
environment: 'production' | 'preview';
|
|
39
|
+
createdOn: string;
|
|
40
|
+
modifiedOn: string;
|
|
41
|
+
latestStage: { name: string; status: 'idle' | 'active' | 'success' | 'failure' };
|
|
42
|
+
deploymentTrigger?: { type: string; metadata?: { branch: string; commitHash: string; commitMessage: string } };
|
|
43
|
+
stages: Array<{ name: string; status: string; startedOn?: string; endedOn?: string }>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CloudflareDnsRecord {
|
|
47
|
+
id: string;
|
|
48
|
+
type: 'A' | 'AAAA' | 'CNAME' | 'TXT' | 'MX' | 'NS' | 'SRV' | 'CAA';
|
|
49
|
+
name: string;
|
|
50
|
+
content: string;
|
|
51
|
+
ttl: number;
|
|
52
|
+
proxied?: boolean;
|
|
53
|
+
priority?: number;
|
|
54
|
+
comment?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CloudflareZone {
|
|
58
|
+
id: string;
|
|
59
|
+
name: string;
|
|
60
|
+
status: string;
|
|
61
|
+
nameServers: string[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CloudflareWorker {
|
|
65
|
+
id: string;
|
|
66
|
+
tag: string;
|
|
67
|
+
etag: string;
|
|
68
|
+
size: number;
|
|
69
|
+
createdOn: string;
|
|
70
|
+
modifiedOn: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DeployPagesResult {
|
|
74
|
+
id: string;
|
|
75
|
+
url: string;
|
|
76
|
+
environment: string;
|
|
77
|
+
projectName: string;
|
|
78
|
+
latestStage: { name: string; status: string };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CreatePagesProjectOptions {
|
|
82
|
+
/** Git repository URL (e.g. "https://github.com/user/repo") */
|
|
83
|
+
gitUrl: string;
|
|
84
|
+
/** Production branch (defaults to "main") */
|
|
85
|
+
productionBranch?: string;
|
|
86
|
+
/** Build command (e.g. "npm run build") */
|
|
87
|
+
buildCommand?: string;
|
|
88
|
+
/** Build output directory (e.g. "dist", "build", ".next") */
|
|
89
|
+
buildOutputDirectory?: string;
|
|
90
|
+
/** Environment variables for the build */
|
|
91
|
+
envVars?: Record<string, string>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DeployWorkerOptions {
|
|
95
|
+
/** Worker script name */
|
|
96
|
+
name: string;
|
|
97
|
+
/** JavaScript/TypeScript source code */
|
|
98
|
+
script: string;
|
|
99
|
+
/** Route patterns (e.g. ["example.com/*"]) */
|
|
100
|
+
routes?: string[];
|
|
101
|
+
/** Compatibility date (e.g. "2024-01-01") */
|
|
102
|
+
compatibilityDate?: string;
|
|
103
|
+
/** Environment variables / secrets bindings */
|
|
104
|
+
bindings?: Record<string, string>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Service
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
export class CloudflareService {
|
|
112
|
+
private config: CloudflareConfig;
|
|
113
|
+
private running = false;
|
|
114
|
+
|
|
115
|
+
constructor(config: CloudflareConfig) {
|
|
116
|
+
this.config = {
|
|
117
|
+
...config,
|
|
118
|
+
baseUrl: config.baseUrl ?? 'https://api.cloudflare.com/client/v4',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async initialize(): Promise<void> {
|
|
123
|
+
// Validate token by verifying the token
|
|
124
|
+
const res = await this.fetch('/user/tokens/verify');
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const body = await res.text();
|
|
127
|
+
throw new Error(`Cloudflare auth failed: ${res.status} ${body}`);
|
|
128
|
+
}
|
|
129
|
+
this.running = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async shutdown(): Promise<void> {
|
|
133
|
+
this.running = false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
get isRunning(): boolean {
|
|
137
|
+
return this.running;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Pages Projects ──────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
/** List all Pages projects. */
|
|
143
|
+
async listProjects(limit = 25): Promise<CloudflarePagesProject[]> {
|
|
144
|
+
const res = await this.fetch(
|
|
145
|
+
`/accounts/${this.config.accountId}/pages/projects?per_page=${limit}`,
|
|
146
|
+
);
|
|
147
|
+
const data = await this.json(res);
|
|
148
|
+
return (data.result ?? []).map((p: any) => this.mapProject(p));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Get a Pages project by name. */
|
|
152
|
+
async getProject(projectName: string): Promise<CloudflarePagesProject> {
|
|
153
|
+
const res = await this.fetch(
|
|
154
|
+
`/accounts/${this.config.accountId}/pages/projects/${encodeURIComponent(projectName)}`,
|
|
155
|
+
);
|
|
156
|
+
const data = await this.json(res);
|
|
157
|
+
return this.mapProject(data.result);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Create a new Pages project. */
|
|
161
|
+
async createProject(
|
|
162
|
+
name: string,
|
|
163
|
+
opts?: {
|
|
164
|
+
productionBranch?: string;
|
|
165
|
+
buildCommand?: string;
|
|
166
|
+
buildOutputDirectory?: string;
|
|
167
|
+
gitUrl?: string;
|
|
168
|
+
},
|
|
169
|
+
): Promise<CloudflarePagesProject> {
|
|
170
|
+
const body: Record<string, unknown> = {
|
|
171
|
+
name,
|
|
172
|
+
production_branch: opts?.productionBranch ?? 'main',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
if (opts?.buildCommand || opts?.buildOutputDirectory) {
|
|
176
|
+
body.build_config = {
|
|
177
|
+
...(opts?.buildCommand ? { build_command: opts.buildCommand } : {}),
|
|
178
|
+
...(opts?.buildOutputDirectory ? { destination_dir: opts.buildOutputDirectory } : {}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts?.gitUrl) {
|
|
183
|
+
const { owner, repo } = this.parseGitUrl(opts.gitUrl);
|
|
184
|
+
body.source = {
|
|
185
|
+
type: 'github',
|
|
186
|
+
config: {
|
|
187
|
+
owner,
|
|
188
|
+
repo_name: repo,
|
|
189
|
+
production_branch: opts?.productionBranch ?? 'main',
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const res = await this.fetch(
|
|
195
|
+
`/accounts/${this.config.accountId}/pages/projects`,
|
|
196
|
+
{ method: 'POST', body: JSON.stringify(body) },
|
|
197
|
+
);
|
|
198
|
+
const data = await this.json(res);
|
|
199
|
+
return this.mapProject(data.result);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Deployments ─────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
/** Create a deployment for a Pages project (triggers build from latest commit). */
|
|
205
|
+
async createDeployment(
|
|
206
|
+
projectName: string,
|
|
207
|
+
opts?: { branch?: string },
|
|
208
|
+
): Promise<DeployPagesResult> {
|
|
209
|
+
const formData = new FormData();
|
|
210
|
+
if (opts?.branch) {
|
|
211
|
+
formData.append('branch', opts.branch);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const res = await this.fetch(
|
|
215
|
+
`/accounts/${this.config.accountId}/pages/projects/${encodeURIComponent(projectName)}/deployments`,
|
|
216
|
+
{
|
|
217
|
+
method: 'POST',
|
|
218
|
+
body: formData,
|
|
219
|
+
headers: {}, // Let fetch set content-type for FormData
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
const data = await this.json(res);
|
|
223
|
+
const d = data.result;
|
|
224
|
+
return {
|
|
225
|
+
id: d.id,
|
|
226
|
+
url: d.url ? (d.url.startsWith('http') ? d.url : `https://${d.url}`) : '',
|
|
227
|
+
environment: d.environment ?? 'production',
|
|
228
|
+
projectName: d.project_name ?? projectName,
|
|
229
|
+
latestStage: d.latest_stage ?? { name: 'queued', status: 'idle' },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Deploy from a Git URL — creates project if needed, then triggers deployment. */
|
|
234
|
+
async deployFromGit(opts: CreatePagesProjectOptions): Promise<DeployPagesResult> {
|
|
235
|
+
const projectName = this.inferProjectName(opts.gitUrl);
|
|
236
|
+
|
|
237
|
+
// Try to get existing project
|
|
238
|
+
let _project: CloudflarePagesProject | null = null;
|
|
239
|
+
try {
|
|
240
|
+
_project = await this.getProject(projectName);
|
|
241
|
+
} catch {
|
|
242
|
+
// Project doesn't exist — create it
|
|
243
|
+
_project = await this.createProject(projectName, {
|
|
244
|
+
gitUrl: opts.gitUrl,
|
|
245
|
+
productionBranch: opts.productionBranch,
|
|
246
|
+
buildCommand: opts.buildCommand,
|
|
247
|
+
buildOutputDirectory: opts.buildOutputDirectory,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Set env vars if provided
|
|
252
|
+
if (opts.envVars && Object.keys(opts.envVars).length > 0) {
|
|
253
|
+
await this.setProjectEnvVars(projectName, opts.envVars);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Trigger deployment
|
|
257
|
+
return this.createDeployment(projectName, { branch: opts.productionBranch });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Get deployment status. */
|
|
261
|
+
async getDeployment(
|
|
262
|
+
projectName: string,
|
|
263
|
+
deploymentId: string,
|
|
264
|
+
): Promise<CloudflareDeployment> {
|
|
265
|
+
const res = await this.fetch(
|
|
266
|
+
`/accounts/${this.config.accountId}/pages/projects/${encodeURIComponent(projectName)}/deployments/${deploymentId}`,
|
|
267
|
+
);
|
|
268
|
+
const data = await this.json(res);
|
|
269
|
+
return this.mapDeployment(data.result);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** List recent deployments for a project. */
|
|
273
|
+
async listDeployments(
|
|
274
|
+
projectName: string,
|
|
275
|
+
limit = 10,
|
|
276
|
+
): Promise<CloudflareDeployment[]> {
|
|
277
|
+
const res = await this.fetch(
|
|
278
|
+
`/accounts/${this.config.accountId}/pages/projects/${encodeURIComponent(projectName)}/deployments?per_page=${limit}`,
|
|
279
|
+
);
|
|
280
|
+
const data = await this.json(res);
|
|
281
|
+
return (data.result ?? []).map((d: any) => this.mapDeployment(d));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Set environment variables on a Pages project. */
|
|
285
|
+
async setProjectEnvVars(
|
|
286
|
+
projectName: string,
|
|
287
|
+
vars: Record<string, string>,
|
|
288
|
+
environment: 'production' | 'preview' = 'production',
|
|
289
|
+
): Promise<void> {
|
|
290
|
+
const envVarMap: Record<string, { value: string; type: string }> = {};
|
|
291
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
292
|
+
envVarMap[key] = { value, type: 'plain_text' };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const body = {
|
|
296
|
+
deployment_configs: {
|
|
297
|
+
[environment]: {
|
|
298
|
+
env_vars: envVarMap,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await this.fetch(
|
|
304
|
+
`/accounts/${this.config.accountId}/pages/projects/${encodeURIComponent(projectName)}`,
|
|
305
|
+
{ method: 'PATCH', body: JSON.stringify(body) },
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ── DNS ─────────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
311
|
+
/** Look up a zone by domain name. */
|
|
312
|
+
async getZoneByDomain(domain: string): Promise<CloudflareZone> {
|
|
313
|
+
const res = await this.fetch(`/zones?name=${encodeURIComponent(domain)}`);
|
|
314
|
+
const data = await this.json(res);
|
|
315
|
+
const zones = data.result ?? [];
|
|
316
|
+
if (zones.length === 0) {
|
|
317
|
+
throw new Error(`No Cloudflare zone found for domain: ${domain}`);
|
|
318
|
+
}
|
|
319
|
+
return this.mapZone(zones[0]);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** List DNS records for a zone. */
|
|
323
|
+
async listDnsRecords(
|
|
324
|
+
zoneId: string,
|
|
325
|
+
opts?: { type?: string; name?: string },
|
|
326
|
+
): Promise<CloudflareDnsRecord[]> {
|
|
327
|
+
let path = `/zones/${zoneId}/dns_records?per_page=100`;
|
|
328
|
+
if (opts?.type) path += `&type=${opts.type}`;
|
|
329
|
+
if (opts?.name) path += `&name=${encodeURIComponent(opts.name)}`;
|
|
330
|
+
|
|
331
|
+
const res = await this.fetch(path);
|
|
332
|
+
const data = await this.json(res);
|
|
333
|
+
return (data.result ?? []).map((r: any) => this.mapDnsRecord(r));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Create a DNS record. */
|
|
337
|
+
async createDnsRecord(
|
|
338
|
+
zoneId: string,
|
|
339
|
+
record: {
|
|
340
|
+
type: string;
|
|
341
|
+
name: string;
|
|
342
|
+
content: string;
|
|
343
|
+
ttl?: number;
|
|
344
|
+
proxied?: boolean;
|
|
345
|
+
priority?: number;
|
|
346
|
+
comment?: string;
|
|
347
|
+
},
|
|
348
|
+
): Promise<CloudflareDnsRecord> {
|
|
349
|
+
const body: Record<string, unknown> = {
|
|
350
|
+
type: record.type,
|
|
351
|
+
name: record.name,
|
|
352
|
+
content: record.content,
|
|
353
|
+
ttl: record.ttl ?? 1, // 1 = auto
|
|
354
|
+
proxied: record.proxied ?? false,
|
|
355
|
+
};
|
|
356
|
+
if (record.priority !== undefined) body.priority = record.priority;
|
|
357
|
+
if (record.comment) body.comment = record.comment;
|
|
358
|
+
|
|
359
|
+
const res = await this.fetch(`/zones/${zoneId}/dns_records`, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
body: JSON.stringify(body),
|
|
362
|
+
});
|
|
363
|
+
const data = await this.json(res);
|
|
364
|
+
return this.mapDnsRecord(data.result);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** Update a DNS record. */
|
|
368
|
+
async updateDnsRecord(
|
|
369
|
+
zoneId: string,
|
|
370
|
+
recordId: string,
|
|
371
|
+
record: {
|
|
372
|
+
type?: string;
|
|
373
|
+
name?: string;
|
|
374
|
+
content?: string;
|
|
375
|
+
ttl?: number;
|
|
376
|
+
proxied?: boolean;
|
|
377
|
+
priority?: number;
|
|
378
|
+
comment?: string;
|
|
379
|
+
},
|
|
380
|
+
): Promise<CloudflareDnsRecord> {
|
|
381
|
+
const res = await this.fetch(`/zones/${zoneId}/dns_records/${recordId}`, {
|
|
382
|
+
method: 'PUT',
|
|
383
|
+
body: JSON.stringify(record),
|
|
384
|
+
});
|
|
385
|
+
const data = await this.json(res);
|
|
386
|
+
return this.mapDnsRecord(data.result);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** Delete a DNS record. */
|
|
390
|
+
async deleteDnsRecord(zoneId: string, recordId: string): Promise<void> {
|
|
391
|
+
await this.fetch(`/zones/${zoneId}/dns_records/${recordId}`, {
|
|
392
|
+
method: 'DELETE',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Workers ─────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
/** Deploy a Worker script. */
|
|
399
|
+
async deployWorker(opts: DeployWorkerOptions): Promise<CloudflareWorker> {
|
|
400
|
+
// Upload the script
|
|
401
|
+
const formData = new FormData();
|
|
402
|
+
|
|
403
|
+
const metadata: Record<string, unknown> = {
|
|
404
|
+
main_module: 'worker.js',
|
|
405
|
+
compatibility_date: opts.compatibilityDate ?? '2024-01-01',
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Add bindings for env vars / secrets
|
|
409
|
+
if (opts.bindings && Object.keys(opts.bindings).length > 0) {
|
|
410
|
+
metadata.bindings = Object.entries(opts.bindings).map(([name, text]) => ({
|
|
411
|
+
type: 'plain_text',
|
|
412
|
+
name,
|
|
413
|
+
text,
|
|
414
|
+
}));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
formData.append(
|
|
418
|
+
'metadata',
|
|
419
|
+
new Blob([JSON.stringify(metadata)], { type: 'application/json' }),
|
|
420
|
+
);
|
|
421
|
+
formData.append(
|
|
422
|
+
'worker.js',
|
|
423
|
+
new Blob([opts.script], { type: 'application/javascript+module' }),
|
|
424
|
+
'worker.js',
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const res = await this.fetch(
|
|
428
|
+
`/accounts/${this.config.accountId}/workers/scripts/${encodeURIComponent(opts.name)}`,
|
|
429
|
+
{
|
|
430
|
+
method: 'PUT',
|
|
431
|
+
body: formData,
|
|
432
|
+
headers: {}, // Let fetch set content-type for FormData
|
|
433
|
+
},
|
|
434
|
+
);
|
|
435
|
+
const data = await this.json(res);
|
|
436
|
+
const result = data.result;
|
|
437
|
+
|
|
438
|
+
// Set up routes if provided
|
|
439
|
+
if (opts.routes && opts.routes.length > 0) {
|
|
440
|
+
for (const pattern of opts.routes) {
|
|
441
|
+
try {
|
|
442
|
+
// Determine zone from route pattern
|
|
443
|
+
const routeDomain = pattern.replace(/\/\*$/, '').replace(/^\*\./, '');
|
|
444
|
+
const zone = await this.getZoneByDomain(routeDomain);
|
|
445
|
+
await this.fetch(`/zones/${zone.id}/workers/routes`, {
|
|
446
|
+
method: 'POST',
|
|
447
|
+
body: JSON.stringify({ pattern, script: opts.name }),
|
|
448
|
+
});
|
|
449
|
+
} catch {
|
|
450
|
+
// Route setup is best-effort — zone may not be managed
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
id: result?.id ?? opts.name,
|
|
457
|
+
tag: result?.tag ?? '',
|
|
458
|
+
etag: result?.etag ?? '',
|
|
459
|
+
size: result?.size ?? 0,
|
|
460
|
+
createdOn: result?.created_on ?? new Date().toISOString(),
|
|
461
|
+
modifiedOn: result?.modified_on ?? new Date().toISOString(),
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** List Worker scripts. */
|
|
466
|
+
async listWorkers(): Promise<CloudflareWorker[]> {
|
|
467
|
+
const res = await this.fetch(
|
|
468
|
+
`/accounts/${this.config.accountId}/workers/scripts`,
|
|
469
|
+
);
|
|
470
|
+
const data = await this.json(res);
|
|
471
|
+
return (data.result ?? []).map((w: any) => ({
|
|
472
|
+
id: w.id,
|
|
473
|
+
tag: w.tag ?? '',
|
|
474
|
+
etag: w.etag ?? '',
|
|
475
|
+
size: w.size ?? 0,
|
|
476
|
+
createdOn: w.created_on ?? '',
|
|
477
|
+
modifiedOn: w.modified_on ?? '',
|
|
478
|
+
}));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** Delete a Worker script. */
|
|
482
|
+
async deleteWorker(name: string): Promise<void> {
|
|
483
|
+
await this.fetch(
|
|
484
|
+
`/accounts/${this.config.accountId}/workers/scripts/${encodeURIComponent(name)}`,
|
|
485
|
+
{ method: 'DELETE' },
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
490
|
+
|
|
491
|
+
private async fetch(path: string, init?: RequestInit & { headers?: Record<string, string> }): Promise<Response> {
|
|
492
|
+
const url = `${this.config.baseUrl}${path}`;
|
|
493
|
+
const headers: Record<string, string> = {
|
|
494
|
+
'Authorization': `Bearer ${this.config.apiToken}`,
|
|
495
|
+
...(init?.headers ?? {}),
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
// Only set Content-Type for non-FormData requests
|
|
499
|
+
if (!(init?.body instanceof FormData) && !headers['Content-Type']) {
|
|
500
|
+
headers['Content-Type'] = 'application/json';
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return globalThis.fetch(url, {
|
|
504
|
+
...init,
|
|
505
|
+
headers,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private async json(res: Response): Promise<any> {
|
|
510
|
+
if (!res.ok) {
|
|
511
|
+
const text = await res.text();
|
|
512
|
+
throw new Error(`Cloudflare API ${res.status}: ${text.slice(0, 500)}`);
|
|
513
|
+
}
|
|
514
|
+
return res.json();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private parseGitUrl(url: string): { owner: string; repo: string } {
|
|
518
|
+
const match = url.match(/(?:github\.com|gitlab\.com|bitbucket\.org)[/:]([^/]+)\/([^/.]+)/);
|
|
519
|
+
if (!match) throw new Error(`Cannot parse git URL: ${url}`);
|
|
520
|
+
return { owner: match[1], repo: match[2] };
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private inferProjectName(gitUrl: string): string {
|
|
524
|
+
const { repo } = this.parseGitUrl(gitUrl);
|
|
525
|
+
return repo.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private mapProject(data: any): CloudflarePagesProject {
|
|
529
|
+
return {
|
|
530
|
+
id: data.id,
|
|
531
|
+
name: data.name,
|
|
532
|
+
subdomain: data.subdomain ?? `${data.name}.pages.dev`,
|
|
533
|
+
productionBranch: data.production_branch ?? 'main',
|
|
534
|
+
createdOn: data.created_on ?? '',
|
|
535
|
+
domains: data.domains ?? [],
|
|
536
|
+
source: data.source
|
|
537
|
+
? {
|
|
538
|
+
type: data.source.type,
|
|
539
|
+
config: data.source.config
|
|
540
|
+
? {
|
|
541
|
+
owner: data.source.config.owner,
|
|
542
|
+
repo_name: data.source.config.repo_name,
|
|
543
|
+
production_branch: data.source.config.production_branch,
|
|
544
|
+
}
|
|
545
|
+
: undefined,
|
|
546
|
+
}
|
|
547
|
+
: undefined,
|
|
548
|
+
latestDeployment: data.latest_deployment
|
|
549
|
+
? this.mapDeployment(data.latest_deployment)
|
|
550
|
+
: undefined,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private mapDeployment(data: any): CloudflareDeployment {
|
|
555
|
+
return {
|
|
556
|
+
id: data.id,
|
|
557
|
+
projectId: data.project_id ?? '',
|
|
558
|
+
projectName: data.project_name ?? '',
|
|
559
|
+
url: data.url ? (data.url.startsWith('http') ? data.url : `https://${data.url}`) : '',
|
|
560
|
+
environment: data.environment ?? 'production',
|
|
561
|
+
createdOn: data.created_on ?? '',
|
|
562
|
+
modifiedOn: data.modified_on ?? '',
|
|
563
|
+
latestStage: data.latest_stage ?? { name: 'queued', status: 'idle' },
|
|
564
|
+
deploymentTrigger: data.deployment_trigger
|
|
565
|
+
? {
|
|
566
|
+
type: data.deployment_trigger.type,
|
|
567
|
+
metadata: data.deployment_trigger.metadata
|
|
568
|
+
? {
|
|
569
|
+
branch: data.deployment_trigger.metadata.branch ?? '',
|
|
570
|
+
commitHash: data.deployment_trigger.metadata.commit_hash ?? '',
|
|
571
|
+
commitMessage: data.deployment_trigger.metadata.commit_message ?? '',
|
|
572
|
+
}
|
|
573
|
+
: undefined,
|
|
574
|
+
}
|
|
575
|
+
: undefined,
|
|
576
|
+
stages: (data.stages ?? []).map((s: any) => ({
|
|
577
|
+
name: s.name,
|
|
578
|
+
status: s.status,
|
|
579
|
+
startedOn: s.started_on,
|
|
580
|
+
endedOn: s.ended_on,
|
|
581
|
+
})),
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private mapDnsRecord(data: any): CloudflareDnsRecord {
|
|
586
|
+
return {
|
|
587
|
+
id: data.id,
|
|
588
|
+
type: data.type,
|
|
589
|
+
name: data.name,
|
|
590
|
+
content: data.content,
|
|
591
|
+
ttl: data.ttl ?? 1,
|
|
592
|
+
proxied: data.proxied,
|
|
593
|
+
priority: data.priority,
|
|
594
|
+
comment: data.comment,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private mapZone(data: any): CloudflareZone {
|
|
599
|
+
return {
|
|
600
|
+
id: data.id,
|
|
601
|
+
name: data.name,
|
|
602
|
+
status: data.status,
|
|
603
|
+
nameServers: data.name_servers ?? [],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
}
|