@theholocron/holocron-plugin-cloudflare 5.0.0-alpha.18 → 5.0.0-alpha.181

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 CHANGED
@@ -2,8 +2,9 @@
2
2
 
3
3
  # `@theholocron/holocron-plugin-cloudflare`
4
4
 
5
- Cloudflare plugin for [Holocron](../cli). Implements the `dns` and `deployment`
6
- capabilities against the [Cloudflare v4 API](https://developers.cloudflare.com/api/).
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
- Both capabilities can be enabled together:
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` capability is only exposed when `accountId` is resolvable — either passed explicitly in options or set via the `CLOUDFLARE_ACCOUNT_ID` env var.
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
 
@@ -72,7 +79,7 @@ Zone ids are cached per plugin instance for the lifetime of the process.
72
79
 
73
80
  ## `deployment` capability
74
81
 
75
- Manages [Cloudflare Pages](https://developers.cloudflare.com/pages/) projects. Used by `holocron setup` to provision per-PR preview deployments.
82
+ Manages [Cloudflare Pages](https://developers.cloudflare.com/pages/) projects. Used by `holocron repo setup` to provision per-PR preview deployments.
76
83
 
77
84
  | Method | What it does |
78
85
  | ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
@@ -83,9 +90,23 @@ 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
- When `preview: true` (or `preview: { project, domain }`) is set in a repo's deploy config, `holocron setup` calls:
109
+ When `preview: true` (or `preview: { project, domain }`) is set in a repo's deploy config, `holocron repo setup` calls:
89
110
 
90
111
  1. `ensureProject` — creates `<org>-preview` if it doesn't exist
91
112
  2. `ensureCustomDomain` — attaches `*.<domain>` to the project
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, CloudflareClientOptions, CloudflareClientOptions as CloudflareClientOptions$1, createCloudflareClient } from "@theholocron/cloudflare-client";
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<void>;
31
+ ensureCustomDomain(projectId: string, hostname: string): Promise<DnsRecordRequest | null>;
32
32
  private getProjectByName;
33
33
  }
34
34
  //#endregion
@@ -42,6 +42,11 @@ declare class CloudflareDns implements Dns {
42
42
  listRecords(domain: string): Promise<DnsRecord[]>;
43
43
  upsertRecord(domain: string, record: DnsRecord): Promise<DnsRecord>;
44
44
  deleteRecord(domain: string, id: string): Promise<void>;
45
+ /** `holocron doctor` has no specific domain to check — lists every zone the token can see instead. */
46
+ whoami(): Promise<{
47
+ ok: boolean;
48
+ zones: number;
49
+ }>;
45
50
  /**
46
51
  * Walk from full domain up to apex to find the Cloudflare zone.
47
52
  * E.g. "api.staging.example.com" tries:
@@ -53,51 +58,31 @@ declare class CloudflareDns implements Dns {
53
58
  }
54
59
  //#endregion
55
60
  //#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
61
  declare class CloudflareWorkers implements Workers {
65
- private readonly zones;
62
+ private readonly client;
66
63
  private readonly accountId;
67
64
  readonly key: "workers";
68
65
  readonly providerName = "cloudflare";
69
66
  private readonly zoneCache;
70
- private readonly baseUrl;
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">);
67
+ constructor(client: () => Pick<CloudflareClient$1, "workers" | "zones">, accountId: string);
77
68
  upsertProxy(hostname: string, config: WikiProxyConfig): Promise<void>;
78
- private putScript;
79
- private listRoutes;
80
- private createRoute;
81
- private updateRoute;
82
- private cfRequest;
69
+ /** `holocron doctor` has no specific script to check — lists every script the token can see instead. */
70
+ whoami(): Promise<{
71
+ ok: boolean;
72
+ scripts: number;
73
+ }>;
74
+ deployScript(name: string, config: DeployScriptConfig): Promise<DeployScriptResult>;
75
+ /** Idempotent create-or-update for one route pattern — shared by both public methods. */
76
+ private upsertRoute;
83
77
  private resolveZone;
84
78
  }
85
79
  //#endregion
86
80
  //#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
81
  interface VerifyTokenOptions {
97
82
  baseUrl?: string;
98
83
  fetch?: typeof fetch;
99
84
  }
100
- declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult>;
85
+ declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<VerifyTokenResult$1>;
101
86
  //#endregion
102
87
  //#region src/index.d.ts
103
88
  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 {
@@ -153,6 +155,13 @@ var CloudflareDns = class {
153
155
  const zoneId = await this.resolveZone(domain);
154
156
  await this.client().dns.delete(zoneId, id);
155
157
  }
158
+ /** `holocron doctor` has no specific domain to check — lists every zone the token can see instead. */
159
+ async whoami() {
160
+ return {
161
+ ok: true,
162
+ zones: (await this.client().zones.list()).length
163
+ };
164
+ }
156
165
  /**
157
166
  * Walk from full domain up to apex to find the Cloudflare zone.
158
167
  * E.g. "api.staging.example.com" tries:
@@ -187,87 +196,42 @@ function mapRecord(raw) {
187
196
  //#endregion
188
197
  //#region src/capabilities/workers.ts
189
198
  var CloudflareWorkers = class {
190
- zones;
199
+ client;
191
200
  accountId;
192
201
  key = "workers";
193
202
  providerName = "cloudflare";
194
203
  zoneCache = /* @__PURE__ */ new Map();
195
- baseUrl;
196
- fetchImpl;
197
- /** Memoized token thunk — resolved on the first authenticated request. */
198
- token;
199
- constructor(zones, accountId, opts) {
200
- this.zones = zones;
204
+ constructor(client, accountId) {
205
+ this.client = client;
201
206
  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
207
  }
206
208
  async upsertProxy(hostname, config) {
207
209
  const scriptName = hostnameToScriptName(hostname);
208
210
  const script = generateProxyScript(config);
209
- await this.putScript(scriptName, script);
210
- const zoneId = await this.resolveZone(hostname);
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);
211
+ await this.client().workers.putScript(this.accountId, scriptName, script);
212
+ await this.upsertRoute(`${hostname}/*`, scriptName);
216
213
  }
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
- });
214
+ /** `holocron doctor` has no specific script to check — lists every script the token can see instead. */
215
+ async whoami() {
216
+ return {
217
+ ok: true,
218
+ scripts: (await this.client().workers.listScripts(this.accountId)).length
219
+ };
243
220
  }
244
- updateRoute(zoneId, routeId, pattern, script) {
245
- return this.cfRequest("PUT", `/zones/${zoneId}/workers/routes/${routeId}`, {
246
- pattern,
247
- script
248
- });
221
+ async deployScript(name, config) {
222
+ await this.client().workers.putScript(this.accountId, name, config.code);
223
+ for (const [secretName, value] of Object.entries(config.secrets ?? {})) await this.client().workers.putSecret(this.accountId, name, secretName, value);
224
+ for (const pattern of config.routes ?? []) await this.upsertRoute(pattern, name);
225
+ return { scriptName: name };
249
226
  }
250
- async cfRequest(method, path, body) {
251
- const headers = {
252
- authorization: `Bearer ${this.token()}`,
253
- accept: "application/json"
254
- };
255
- const init = {
256
- method,
257
- headers
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;
227
+ /** Idempotent create-or-update for one route pattern — shared by both public methods. */
228
+ async upsertRoute(pattern, scriptName) {
229
+ const hostname = pattern.split("/")[0];
230
+ const zoneId = await this.resolveZone(hostname);
231
+ const existing = (await this.client().workers.listRoutes(zoneId)).find((r) => r.pattern === pattern);
232
+ if (existing) {
233
+ if (existing.script !== scriptName) await this.client().workers.updateRoute(zoneId, existing.id, pattern, scriptName);
234
+ } else await this.client().workers.createRoute(zoneId, pattern, scriptName);
271
235
  }
272
236
  async resolveZone(domain) {
273
237
  const cached = this.zoneCache.get(domain);
@@ -275,7 +239,7 @@ var CloudflareWorkers = class {
275
239
  const parts = domain.split(".");
276
240
  for (let i = 0; i < parts.length - 1; i++) {
277
241
  const candidate = parts.slice(i).join(".");
278
- const zones = await this.zones().list({ name: candidate });
242
+ const zones = await this.client().zones.list({ name: candidate });
279
243
  if (zones.length > 0) {
280
244
  this.zoneCache.set(domain, zones[0].id);
281
245
  return zones[0].id;
@@ -318,14 +282,19 @@ async function verifyToken(token, opts = {}) {
318
282
  };
319
283
  } catch {}
320
284
  try {
285
+ const names = (await client.zones.list()).map((z) => z.name);
286
+ if (names.length === 0) return {
287
+ ok: true,
288
+ subject: "no zones visible"
289
+ };
321
290
  return {
322
291
  ok: true,
323
- subject: `zone: ${(await client.zones.list({ per_page: 1 }))[0]?.name ?? "unknown"}`
292
+ subject: `zones: ${names.length <= 5 ? names.join(", ") : `${names.slice(0, 5).join(", ")} (+${names.length - 5} more)`}`
324
293
  };
325
294
  } catch (err) {
326
295
  return {
327
296
  ok: false,
328
- message: err instanceof Error ? err.message : String(err)
297
+ message: errorMessage(err)
329
298
  };
330
299
  }
331
300
  }
@@ -359,11 +328,7 @@ function deployment(ctx) {
359
328
  }
360
329
  function workersCapability(ctx) {
361
330
  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(() => ctx.client().zones, ctx.options.accountId, {
363
- token: ctx.token,
364
- baseUrl: ctx.options.baseUrl,
365
- fetch: ctx.options.fetch
366
- });
331
+ return new CloudflareWorkers(ctx.client, ctx.options.accountId);
367
332
  }
368
333
  function createPlugin(options = {}) {
369
334
  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.18",
3
+ "version": "5.0.0-alpha.181",
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",
@@ -8,12 +8,12 @@
8
8
  "holocron",
9
9
  "plugin"
10
10
  ],
11
- "homepage": "https://github.com/theholocron/holocron/tree/main/packages/holocron-plugin-cloudflare#readme",
11
+ "homepage": "https://github.com/theholocron/holocron/tree/main/plugins/holocron-plugin-cloudflare#readme",
12
12
  "bugs": "https://github.com/theholocron/holocron/issues",
13
13
  "repository": {
14
14
  "type": "git",
15
15
  "url": "git+https://github.com/theholocron/holocron.git",
16
- "directory": "packages/holocron-plugin-cloudflare"
16
+ "directory": "plugins/holocron-plugin-cloudflare"
17
17
  },
18
18
  "license": "MIT",
19
19
  "author": "Newton Koumantzelis",
@@ -30,12 +30,13 @@
30
30
  "dist"
31
31
  ],
32
32
  "devDependencies": {
33
- "@theholocron/cloudflare-client": "^1.18.0",
34
- "@theholocron/eslint-config": "^8.0.0",
35
- "@theholocron/http-client": "^1.18.0",
36
- "@theholocron/tsconfig": "^8.0.0",
37
- "@theholocron/tsdown-config": "^8.0.0",
38
- "@theholocron/vitest-config": "^8.0.0",
33
+ "@theholocron/cloudflare-client": "^1.43.1",
34
+ "@theholocron/eslint-config": "^8.8.2",
35
+ "@theholocron/http-client": "^1.43.1",
36
+ "@theholocron/misc-utils": "^1.16.0",
37
+ "@theholocron/tsconfig": "^8.8.2",
38
+ "@theholocron/tsdown-config": "^8.8.2",
39
+ "@theholocron/vitest-config": "^8.8.2",
39
40
  "@types/node": "^26",
40
41
  "@vitest/coverage-v8": "^4.1.11",
41
42
  "@vitest/eslint-plugin": "^1.6.27",
@@ -44,14 +45,15 @@
44
45
  "eslint-plugin-n": "^18.3.0",
45
46
  "globals": "^17.11.0",
46
47
  "tsdown": "^0.22.14",
47
- "tsx": "4.23.12",
48
+ "tsx": "4.23.15",
48
49
  "typescript": "^5.9.3",
49
50
  "vitest": "^4.1.11",
50
- "@theholocron/cli": "5.0.0-alpha.18"
51
+ "@theholocron/cli": "5.0.0-alpha.181"
51
52
  },
52
53
  "peerDependencies": {
53
- "@theholocron/cloudflare-client": "^1.18.0",
54
- "@theholocron/cli": "5.0.0-alpha.18"
54
+ "@theholocron/cloudflare-client": "^1.43.1",
55
+ "@theholocron/misc-utils": "^1.16.0",
56
+ "@theholocron/cli": "5.0.0-alpha.181"
55
57
  },
56
58
  "peerDependenciesMeta": {
57
59
  "@theholocron/cloudflare-client": {}