@theholocron/holocron-plugin-cloudflare 5.0.0-alpha.12 → 5.0.0-alpha.121
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 +9 -34
- package/dist/index.mjs +24 -78
- package/package.json +12 -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, VerifyTokenFailure, VerifyTokenResult, VerifyTokenResult as VerifyTokenResult$1, VerifyTokenSuccess, 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,51 +53,26 @@ 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
|
|
86
70
|
//#region src/verify-token.d.ts
|
|
87
|
-
interface VerifyTokenSuccess {
|
|
88
|
-
ok: true;
|
|
89
|
-
subject: string;
|
|
90
|
-
}
|
|
91
|
-
interface VerifyTokenFailure {
|
|
92
|
-
ok: false;
|
|
93
|
-
message: string;
|
|
94
|
-
}
|
|
95
|
-
type VerifyTokenResult = VerifyTokenSuccess | VerifyTokenFailure;
|
|
96
71
|
interface VerifyTokenOptions {
|
|
97
72
|
baseUrl?: string;
|
|
98
73
|
fetch?: typeof fetch;
|
|
99
74
|
}
|
|
100
|
-
declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
|
|
75
|
+
declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
|
|
101
76
|
//#endregion
|
|
102
77
|
//#region src/index.d.ts
|
|
103
78
|
interface CloudflarePluginOptions extends ResolveTokenInput {
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { AuthError, ProviderApiError, createResolveToken } from "@theholocron/cli";
|
|
2
2
|
import { createCloudflareClient } from "@theholocron/cloudflare-client";
|
|
3
|
+
import { errorMessage } from "@theholocron/misc-utils";
|
|
3
4
|
//#region src/auth.ts
|
|
4
5
|
const resolveToken = createResolveToken({
|
|
5
6
|
envName: "HOLOCRON_CLOUDFLARE_TOKEN",
|
|
@@ -74,8 +75,9 @@ var CloudflareDeployment = class {
|
|
|
74
75
|
return deploymentIds.length;
|
|
75
76
|
}
|
|
76
77
|
async ensureCustomDomain(projectId, hostname) {
|
|
77
|
-
if ((await this.client().pages.listDomains(this.accountId, projectId)).some((d) => d.name === hostname)) return;
|
|
78
|
+
if ((await this.client().pages.listDomains(this.accountId, projectId)).some((d) => d.name === hostname)) return null;
|
|
78
79
|
await this.client().pages.addDomain(this.accountId, projectId, hostname);
|
|
80
|
+
return null;
|
|
79
81
|
}
|
|
80
82
|
async getProjectByName(name) {
|
|
81
83
|
try {
|
|
@@ -187,87 +189,35 @@ function mapRecord(raw) {
|
|
|
187
189
|
//#endregion
|
|
188
190
|
//#region src/capabilities/workers.ts
|
|
189
191
|
var CloudflareWorkers = class {
|
|
190
|
-
|
|
192
|
+
client;
|
|
191
193
|
accountId;
|
|
192
194
|
key = "workers";
|
|
193
195
|
providerName = "cloudflare";
|
|
194
196
|
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;
|
|
197
|
+
constructor(client, accountId) {
|
|
198
|
+
this.client = client;
|
|
201
199
|
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
200
|
}
|
|
206
201
|
async upsertProxy(hostname, config) {
|
|
207
202
|
const scriptName = hostnameToScriptName(hostname);
|
|
208
203
|
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
|
-
});
|
|
204
|
+
await this.client().workers.putScript(this.accountId, scriptName, script);
|
|
205
|
+
await this.upsertRoute(`${hostname}/*`, scriptName);
|
|
243
206
|
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
207
|
+
async deployScript(name, config) {
|
|
208
|
+
await this.client().workers.putScript(this.accountId, name, config.code);
|
|
209
|
+
for (const [secretName, value] of Object.entries(config.secrets ?? {})) await this.client().workers.putSecret(this.accountId, name, secretName, value);
|
|
210
|
+
for (const pattern of config.routes ?? []) await this.upsertRoute(pattern, name);
|
|
211
|
+
return { scriptName: name };
|
|
249
212
|
}
|
|
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;
|
|
213
|
+
/** Idempotent create-or-update for one route pattern — shared by both public methods. */
|
|
214
|
+
async upsertRoute(pattern, scriptName) {
|
|
215
|
+
const hostname = pattern.split("/")[0];
|
|
216
|
+
const zoneId = await this.resolveZone(hostname);
|
|
217
|
+
const existing = (await this.client().workers.listRoutes(zoneId)).find((r) => r.pattern === pattern);
|
|
218
|
+
if (existing) {
|
|
219
|
+
if (existing.script !== scriptName) await this.client().workers.updateRoute(zoneId, existing.id, pattern, scriptName);
|
|
220
|
+
} else await this.client().workers.createRoute(zoneId, pattern, scriptName);
|
|
271
221
|
}
|
|
272
222
|
async resolveZone(domain) {
|
|
273
223
|
const cached = this.zoneCache.get(domain);
|
|
@@ -275,7 +225,7 @@ var CloudflareWorkers = class {
|
|
|
275
225
|
const parts = domain.split(".");
|
|
276
226
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
277
227
|
const candidate = parts.slice(i).join(".");
|
|
278
|
-
const zones = await this.
|
|
228
|
+
const zones = await this.client().zones.list({ name: candidate });
|
|
279
229
|
if (zones.length > 0) {
|
|
280
230
|
this.zoneCache.set(domain, zones[0].id);
|
|
281
231
|
return zones[0].id;
|
|
@@ -325,7 +275,7 @@ async function verifyToken(token, opts = {}) {
|
|
|
325
275
|
} catch (err) {
|
|
326
276
|
return {
|
|
327
277
|
ok: false,
|
|
328
|
-
message:
|
|
278
|
+
message: errorMessage(err)
|
|
329
279
|
};
|
|
330
280
|
}
|
|
331
281
|
}
|
|
@@ -359,11 +309,7 @@ function deployment(ctx) {
|
|
|
359
309
|
}
|
|
360
310
|
function workersCapability(ctx) {
|
|
361
311
|
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
|
-
});
|
|
312
|
+
return new CloudflareWorkers(ctx.client, ctx.options.accountId);
|
|
367
313
|
}
|
|
368
314
|
function createPlugin(options = {}) {
|
|
369
315
|
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.121",
|
|
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,13 @@
|
|
|
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.38.0",
|
|
34
|
+
"@theholocron/eslint-config": "^8.7.0",
|
|
35
|
+
"@theholocron/http-client": "^1.38.0",
|
|
36
|
+
"@theholocron/misc-utils": "^1.9.2",
|
|
37
|
+
"@theholocron/tsconfig": "^8.7.0",
|
|
38
|
+
"@theholocron/tsdown-config": "^8.7.0",
|
|
39
|
+
"@theholocron/vitest-config": "^8.7.0",
|
|
38
40
|
"@types/node": "^26",
|
|
39
41
|
"@vitest/coverage-v8": "^4.1.11",
|
|
40
42
|
"@vitest/eslint-plugin": "^1.6.27",
|
|
@@ -46,11 +48,12 @@
|
|
|
46
48
|
"tsx": "4.23.12",
|
|
47
49
|
"typescript": "^5.9.3",
|
|
48
50
|
"vitest": "^4.1.11",
|
|
49
|
-
"@theholocron/cli": "5.0.0-alpha.
|
|
51
|
+
"@theholocron/cli": "5.0.0-alpha.121"
|
|
50
52
|
},
|
|
51
53
|
"peerDependencies": {
|
|
52
|
-
"@theholocron/cloudflare-client": "^1.
|
|
53
|
-
"@theholocron/
|
|
54
|
+
"@theholocron/cloudflare-client": "^1.38.0",
|
|
55
|
+
"@theholocron/misc-utils": "^1.9.2",
|
|
56
|
+
"@theholocron/cli": "5.0.0-alpha.121"
|
|
54
57
|
},
|
|
55
58
|
"peerDependenciesMeta": {
|
|
56
59
|
"@theholocron/cloudflare-client": {}
|