@theholocron/holocron-plugin-cloudflare 5.0.0-alpha.9 → 5.0.0-alpha.90
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 +25 -4
- package/dist/index.d.mts +8 -24
- package/dist/index.mjs +22 -77
- package/package.json +10 -9
package/README.md
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# `@theholocron/holocron-plugin-cloudflare`
|
|
4
4
|
|
|
5
|
-
Cloudflare plugin for [Holocron](../cli). Implements the `dns
|
|
6
|
-
capabilities against the
|
|
5
|
+
Cloudflare plugin for [Holocron](../cli). Implements the `dns`, `deployment`,
|
|
6
|
+
and `workers` capabilities against the
|
|
7
|
+
[Cloudflare v4 API](https://developers.cloudflare.com/api/).
|
|
7
8
|
|
|
8
9
|
## Install
|
|
9
10
|
|
|
@@ -40,12 +41,15 @@ Generate a scoped API token at **dash.cloudflare.com/profile/api-tokens**.
|
|
|
40
41
|
// Cloudflare Pages deployments (requires accountId)
|
|
41
42
|
// accountId falls back to CLOUDFLARE_ACCOUNT_ID env var when omitted
|
|
42
43
|
"deployment": "cloudflare",
|
|
44
|
+
|
|
45
|
+
// Workers deployment (also requires accountId)
|
|
46
|
+
"workers": "cloudflare",
|
|
43
47
|
},
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
```
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
Any combination of capabilities can be enabled together:
|
|
49
53
|
|
|
50
54
|
<!-- prettier-ignore -->
|
|
51
55
|
```jsonc
|
|
@@ -53,12 +57,15 @@ Both capabilities can be enabled together:
|
|
|
53
57
|
"providers": {
|
|
54
58
|
"dns": "cloudflare",
|
|
55
59
|
"deployment": "cloudflare",
|
|
60
|
+
"workers": "cloudflare",
|
|
56
61
|
},
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
```
|
|
60
65
|
|
|
61
|
-
The `deployment`
|
|
66
|
+
The `deployment` and `workers` capabilities are only exposed when `accountId`
|
|
67
|
+
is resolvable — either passed explicitly in options or set via the
|
|
68
|
+
`CLOUDFLARE_ACCOUNT_ID` env var.
|
|
62
69
|
|
|
63
70
|
## `dns` capability
|
|
64
71
|
|
|
@@ -83,6 +90,20 @@ Manages [Cloudflare Pages](https://developers.cloudflare.com/pages/) projects. U
|
|
|
83
90
|
| `triggerDeployment` | Triggers a new Pages deployment from the latest production branch commit. |
|
|
84
91
|
| `updateProjectSettings` | Updates project-level settings (currently a no-op; CF Pages REST API has no direct settings endpoint for these fields). |
|
|
85
92
|
|
|
93
|
+
## `workers` capability
|
|
94
|
+
|
|
95
|
+
Deploys and manages [Workers](https://developers.cloudflare.com/workers/) scripts.
|
|
96
|
+
|
|
97
|
+
| Method | What it does |
|
|
98
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `upsertProxy` | Deploys a generated reverse-proxy Worker forwarding `hostname/*` to `config.target`, injecting `config.headers`. Used by `setup` to wire wiki custom-domain proxies when the wiki provider requires one in addition to the CNAME. |
|
|
100
|
+
| `deployScript` | Deploys (or updates) an arbitrary Worker script — the caller's own code, not a generated proxy — with optional secret bindings and route patterns. For a consumer that isn't a reverse-proxy, e.g. a webhook receiver. |
|
|
101
|
+
|
|
102
|
+
Both methods are idempotent: re-running with the same inputs makes no
|
|
103
|
+
route-write call when nothing changed. Zone ids are cached per plugin
|
|
104
|
+
instance for the lifetime of the process — the `workers` capability's own
|
|
105
|
+
cache, separate from `dns`'s.
|
|
106
|
+
|
|
86
107
|
### Preview deployment setup
|
|
87
108
|
|
|
88
109
|
When `preview: true` (or `preview: { project, domain }`) is set in a repo's deploy config, `holocron setup` calls:
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AuthError, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, ResolveTokenInput, WikiProxyConfig, Workers } from "@theholocron/cli";
|
|
2
|
-
import { CloudflareClient,
|
|
1
|
+
import { AuthError, DeployScriptConfig, DeployScriptResult, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordRequest, ResolveTokenInput, WikiProxyConfig, Workers } from "@theholocron/cli";
|
|
2
|
+
import { CloudflareClient, CloudflareClient as CloudflareClient$1, CloudflareClientOptions, createCloudflareClient } from "@theholocron/cloudflare-client";
|
|
3
3
|
//#region src/auth.d.ts
|
|
4
4
|
declare const resolveToken: (input?: ResolveTokenInput) => string;
|
|
5
5
|
//#endregion
|
|
@@ -28,7 +28,7 @@ declare class CloudflareDeployment implements Deployment {
|
|
|
28
28
|
getDeployment(deploymentId: string): Promise<DeploymentRecord>;
|
|
29
29
|
listPreviewDeployments(projectId: string, branch: string): Promise<DeploymentRecord[]>;
|
|
30
30
|
deletePreviewDeployments(projectId: string, deploymentIds: string[]): Promise<number>;
|
|
31
|
-
ensureCustomDomain(projectId: string, hostname: string): Promise<
|
|
31
|
+
ensureCustomDomain(projectId: string, hostname: string): Promise<DnsRecordRequest | null>;
|
|
32
32
|
private getProjectByName;
|
|
33
33
|
}
|
|
34
34
|
//#endregion
|
|
@@ -53,33 +53,17 @@ declare class CloudflareDns implements Dns {
|
|
|
53
53
|
}
|
|
54
54
|
//#endregion
|
|
55
55
|
//#region src/capabilities/workers.d.ts
|
|
56
|
-
interface ZonesClient {
|
|
57
|
-
list(query?: {
|
|
58
|
-
name?: string;
|
|
59
|
-
per_page?: number;
|
|
60
|
-
}): Promise<Array<{
|
|
61
|
-
id: string;
|
|
62
|
-
}>>;
|
|
63
|
-
}
|
|
64
56
|
declare class CloudflareWorkers implements Workers {
|
|
65
|
-
private readonly
|
|
57
|
+
private readonly client;
|
|
66
58
|
private readonly accountId;
|
|
67
59
|
readonly key: "workers";
|
|
68
60
|
readonly providerName = "cloudflare";
|
|
69
61
|
private readonly zoneCache;
|
|
70
|
-
|
|
71
|
-
private readonly fetchImpl;
|
|
72
|
-
/** Memoized token thunk — resolved on the first authenticated request. */
|
|
73
|
-
private readonly token;
|
|
74
|
-
constructor(zones: () => ZonesClient, accountId: string, opts: {
|
|
75
|
-
token: () => string;
|
|
76
|
-
} & Pick<CloudflareClientOptions$1, "baseUrl" | "fetch">);
|
|
62
|
+
constructor(client: () => Pick<CloudflareClient$1, "workers" | "zones">, accountId: string);
|
|
77
63
|
upsertProxy(hostname: string, config: WikiProxyConfig): Promise<void>;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
private updateRoute;
|
|
82
|
-
private cfRequest;
|
|
64
|
+
deployScript(name: string, config: DeployScriptConfig): Promise<DeployScriptResult>;
|
|
65
|
+
/** Idempotent create-or-update for one route pattern — shared by both public methods. */
|
|
66
|
+
private upsertRoute;
|
|
83
67
|
private resolveZone;
|
|
84
68
|
}
|
|
85
69
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -74,8 +74,9 @@ var CloudflareDeployment = class {
|
|
|
74
74
|
return deploymentIds.length;
|
|
75
75
|
}
|
|
76
76
|
async ensureCustomDomain(projectId, hostname) {
|
|
77
|
-
if ((await this.client().pages.listDomains(this.accountId, projectId)).some((d) => d.name === hostname)) return;
|
|
77
|
+
if ((await this.client().pages.listDomains(this.accountId, projectId)).some((d) => d.name === hostname)) return null;
|
|
78
78
|
await this.client().pages.addDomain(this.accountId, projectId, hostname);
|
|
79
|
+
return null;
|
|
79
80
|
}
|
|
80
81
|
async getProjectByName(name) {
|
|
81
82
|
try {
|
|
@@ -187,87 +188,35 @@ function mapRecord(raw) {
|
|
|
187
188
|
//#endregion
|
|
188
189
|
//#region src/capabilities/workers.ts
|
|
189
190
|
var CloudflareWorkers = class {
|
|
190
|
-
|
|
191
|
+
client;
|
|
191
192
|
accountId;
|
|
192
193
|
key = "workers";
|
|
193
194
|
providerName = "cloudflare";
|
|
194
195
|
zoneCache = /* @__PURE__ */ new Map();
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
/** Memoized token thunk — resolved on the first authenticated request. */
|
|
198
|
-
token;
|
|
199
|
-
constructor(zones, accountId, opts) {
|
|
200
|
-
this.zones = zones;
|
|
196
|
+
constructor(client, accountId) {
|
|
197
|
+
this.client = client;
|
|
201
198
|
this.accountId = accountId;
|
|
202
|
-
this.token = opts.token;
|
|
203
|
-
this.baseUrl = opts.baseUrl ?? "https://api.cloudflare.com/client/v4";
|
|
204
|
-
this.fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
205
199
|
}
|
|
206
200
|
async upsertProxy(hostname, config) {
|
|
207
201
|
const scriptName = hostnameToScriptName(hostname);
|
|
208
202
|
const script = generateProxyScript(config);
|
|
209
|
-
await this.putScript(scriptName, script);
|
|
210
|
-
|
|
211
|
-
const pattern = `${hostname}/*`;
|
|
212
|
-
const existing = (await this.listRoutes(zoneId)).find((r) => r.pattern === pattern);
|
|
213
|
-
if (existing) {
|
|
214
|
-
if (existing.script !== scriptName) await this.updateRoute(zoneId, existing.id, pattern, scriptName);
|
|
215
|
-
} else await this.createRoute(zoneId, pattern, scriptName);
|
|
216
|
-
}
|
|
217
|
-
async putScript(scriptName, script) {
|
|
218
|
-
const form = new FormData();
|
|
219
|
-
form.append("metadata", new Blob([JSON.stringify({
|
|
220
|
-
main_module: "index.js",
|
|
221
|
-
compatibility_date: "2025-08-31"
|
|
222
|
-
})], { type: "application/json" }));
|
|
223
|
-
form.append("index.js", new Blob([script], { type: "application/javascript+module" }), "index.js");
|
|
224
|
-
const path = `/accounts/${this.accountId}/workers/scripts/${encodeURIComponent(scriptName)}`;
|
|
225
|
-
const res = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
226
|
-
method: "PUT",
|
|
227
|
-
headers: { authorization: `Bearer ${this.token()}` },
|
|
228
|
-
body: form
|
|
229
|
-
});
|
|
230
|
-
if (!res.ok) {
|
|
231
|
-
const body = await res.text().catch(() => "");
|
|
232
|
-
throw new ProviderApiError(`Cloudflare PUT ${path} → ${res.status}`, res.status, body);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
listRoutes(zoneId) {
|
|
236
|
-
return this.cfRequest("GET", `/zones/${zoneId}/workers/routes`);
|
|
237
|
-
}
|
|
238
|
-
createRoute(zoneId, pattern, script) {
|
|
239
|
-
return this.cfRequest("POST", `/zones/${zoneId}/workers/routes`, {
|
|
240
|
-
pattern,
|
|
241
|
-
script
|
|
242
|
-
});
|
|
203
|
+
await this.client().workers.putScript(this.accountId, scriptName, script);
|
|
204
|
+
await this.upsertRoute(`${hostname}/*`, scriptName);
|
|
243
205
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
206
|
+
async deployScript(name, config) {
|
|
207
|
+
await this.client().workers.putScript(this.accountId, name, config.code);
|
|
208
|
+
for (const [secretName, value] of Object.entries(config.secrets ?? {})) await this.client().workers.putSecret(this.accountId, name, secretName, value);
|
|
209
|
+
for (const pattern of config.routes ?? []) await this.upsertRoute(pattern, name);
|
|
210
|
+
return { scriptName: name };
|
|
249
211
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
};
|
|
259
|
-
if (body !== void 0) {
|
|
260
|
-
headers["content-type"] = "application/json";
|
|
261
|
-
init.body = JSON.stringify(body);
|
|
262
|
-
}
|
|
263
|
-
const res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
|
|
264
|
-
if (!res.ok) {
|
|
265
|
-
const text = await res.text().catch(() => "");
|
|
266
|
-
throw new ProviderApiError(`Cloudflare ${method} ${path} → ${res.status}`, res.status, text);
|
|
267
|
-
}
|
|
268
|
-
const envelope = await res.json();
|
|
269
|
-
if (!envelope.success) throw new ProviderApiError(`Cloudflare ${method} ${path} returned success:false`, 0, JSON.stringify(envelope.errors));
|
|
270
|
-
return envelope.result;
|
|
212
|
+
/** Idempotent create-or-update for one route pattern — shared by both public methods. */
|
|
213
|
+
async upsertRoute(pattern, scriptName) {
|
|
214
|
+
const hostname = pattern.split("/")[0];
|
|
215
|
+
const zoneId = await this.resolveZone(hostname);
|
|
216
|
+
const existing = (await this.client().workers.listRoutes(zoneId)).find((r) => r.pattern === pattern);
|
|
217
|
+
if (existing) {
|
|
218
|
+
if (existing.script !== scriptName) await this.client().workers.updateRoute(zoneId, existing.id, pattern, scriptName);
|
|
219
|
+
} else await this.client().workers.createRoute(zoneId, pattern, scriptName);
|
|
271
220
|
}
|
|
272
221
|
async resolveZone(domain) {
|
|
273
222
|
const cached = this.zoneCache.get(domain);
|
|
@@ -275,7 +224,7 @@ var CloudflareWorkers = class {
|
|
|
275
224
|
const parts = domain.split(".");
|
|
276
225
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
277
226
|
const candidate = parts.slice(i).join(".");
|
|
278
|
-
const zones = await this.
|
|
227
|
+
const zones = await this.client().zones.list({ name: candidate });
|
|
279
228
|
if (zones.length > 0) {
|
|
280
229
|
this.zoneCache.set(domain, zones[0].id);
|
|
281
230
|
return zones[0].id;
|
|
@@ -359,11 +308,7 @@ function deployment(ctx) {
|
|
|
359
308
|
}
|
|
360
309
|
function workersCapability(ctx) {
|
|
361
310
|
if (!ctx.options.accountId) throw new Error("Cloudflare accountId is required for Workers — set accountId in the plugin options or CLOUDFLARE_ACCOUNT_ID env var");
|
|
362
|
-
return new CloudflareWorkers(
|
|
363
|
-
token: ctx.token,
|
|
364
|
-
baseUrl: ctx.options.baseUrl,
|
|
365
|
-
fetch: ctx.options.fetch
|
|
366
|
-
});
|
|
311
|
+
return new CloudflareWorkers(ctx.client, ctx.options.accountId);
|
|
367
312
|
}
|
|
368
313
|
function createPlugin(options = {}) {
|
|
369
314
|
const ctx = createContext(options);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/holocron-plugin-cloudflare",
|
|
3
|
-
"version": "5.0.0-alpha.
|
|
3
|
+
"version": "5.0.0-alpha.90",
|
|
4
4
|
"description": "Holocron plugin for Cloudflare. Implements the dns capability against Cloudflare's REST API — zone resolution, DNS record management.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -30,11 +30,12 @@
|
|
|
30
30
|
"dist"
|
|
31
31
|
],
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"@theholocron/cloudflare-client": "^1.
|
|
34
|
-
"@theholocron/eslint-config": "^8.
|
|
35
|
-
"@theholocron/
|
|
36
|
-
"@theholocron/
|
|
37
|
-
"@theholocron/
|
|
33
|
+
"@theholocron/cloudflare-client": "^1.31.0",
|
|
34
|
+
"@theholocron/eslint-config": "^8.6.0",
|
|
35
|
+
"@theholocron/http-client": "^1.31.0",
|
|
36
|
+
"@theholocron/tsconfig": "^8.4.5",
|
|
37
|
+
"@theholocron/tsdown-config": "^8.4.5",
|
|
38
|
+
"@theholocron/vitest-config": "^8.4.5",
|
|
38
39
|
"@types/node": "^26",
|
|
39
40
|
"@vitest/coverage-v8": "^4.1.11",
|
|
40
41
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
@@ -46,11 +47,11 @@
|
|
|
46
47
|
"tsx": "4.23.12",
|
|
47
48
|
"typescript": "^5.9.3",
|
|
48
49
|
"vitest": "^4.1.11",
|
|
49
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
50
|
+
"@theholocron/cli": "5.0.0-alpha.90"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
|
-
"@theholocron/cloudflare-client": "^1.
|
|
53
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
53
|
+
"@theholocron/cloudflare-client": "^1.31.0",
|
|
54
|
+
"@theholocron/cli": "5.0.0-alpha.90"
|
|
54
55
|
},
|
|
55
56
|
"peerDependenciesMeta": {
|
|
56
57
|
"@theholocron/cloudflare-client": {}
|