@theholocron/holocron-plugin-cloudflare 3.31.1 → 3.33.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/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  # `@theholocron/holocron-plugin-cloudflare`
4
4
 
5
- Cloudflare plugin for [Holocron](../cli). Implements the `dns`
6
- capability against the [Cloudflare v4 API](https://developers.cloudflare.com/api/).
5
+ Cloudflare plugin for [Holocron](../cli). Implements the `dns` and `deployment`
6
+ capabilities against the [Cloudflare v4 API](https://developers.cloudflare.com/api/).
7
7
 
8
8
  ## Install
9
9
 
@@ -23,8 +23,10 @@ Token resolution order:
23
23
  4. Keyring `cloudflare.<org>` — tried first when an org is active via `--org`, `HOLOCRON_ORG`, or `org` in `holocron.config.ts`
24
24
  5. Keyring `cloudflare` — unnamespaced fallback; set via `holocron auth set cloudflare <token>`
25
25
 
26
- Generate a scoped API token at **dash.cloudflare.com/profile/api-tokens**
27
- with `Zone:Read` and `DNS:Edit` permissions.
26
+ Generate a scoped API token at **dash.cloudflare.com/profile/api-tokens**.
27
+
28
+ - **DNS only:** `Zone:Read`, `Zone:DNS:Edit`
29
+ - **DNS + Pages (deployment):** `Zone:Read`, `Zone:DNS:Edit`, `Cloudflare Pages:Edit`
28
30
 
29
31
  ## Config
30
32
 
@@ -32,16 +34,33 @@ with `Zone:Read` and `DNS:Edit` permissions.
32
34
  ```jsonc
33
35
  {
34
36
  "providers": {
35
- "dns": ["cloudflare", { "accountId": "optional-account-id" }],
37
+ // DNS management only
38
+ "dns": "cloudflare",
39
+
40
+ // Cloudflare Pages deployments (requires accountId)
41
+ // accountId falls back to CLOUDFLARE_ACCOUNT_ID env var when omitted
42
+ "deployment": "cloudflare",
36
43
  },
37
44
  }
38
45
 
39
46
  ```
40
47
 
41
- - `accountId` (optional) Cloudflare account id. Not required for DNS
42
- operations; needed only if you extend the plugin to tunnel management.
48
+ Both capabilities can be enabled together:
49
+
50
+ <!-- prettier-ignore -->
51
+ ```jsonc
52
+ {
53
+ "providers": {
54
+ "dns": "cloudflare",
55
+ "deployment": "cloudflare",
56
+ },
57
+ }
43
58
 
44
- ## What's implemented
59
+ ```
60
+
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.
62
+
63
+ ## `dns` capability
45
64
 
46
65
  | Method | What it does |
47
66
  | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -50,3 +69,26 @@ with `Zone:Read` and `DNS:Edit` permissions.
50
69
  | `deleteRecord` | Deletes a record by id within the zone that contains the given domain. |
51
70
 
52
71
  Zone ids are cached per plugin instance for the lifetime of the process.
72
+
73
+ ## `deployment` capability
74
+
75
+ Manages [Cloudflare Pages](https://developers.cloudflare.com/pages/) projects. Used by `holocron setup` to provision per-PR preview deployments.
76
+
77
+ | Method | What it does |
78
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
79
+ | `listProjects` | Lists all Cloudflare Pages projects in the account. |
80
+ | `ensureProject` | Creates the Pages project if it does not exist; returns the existing project otherwise. |
81
+ | `ensureCustomDomain` | Attaches a custom domain (or wildcard) to the project if not already attached; idempotent. |
82
+ | `listDeployments` | Lists recent deployments for the project. |
83
+ | `triggerDeployment` | Triggers a new Pages deployment from the latest production branch commit. |
84
+ | `updateProjectSettings` | Updates project-level settings (currently a no-op; CF Pages REST API has no direct settings endpoint for these fields). |
85
+
86
+ ### Preview deployment setup
87
+
88
+ When `preview: true` (or `preview: { project, domain }`) is set in a repo's deploy config, `holocron setup` calls:
89
+
90
+ 1. `ensureProject` — creates `<org>-preview` if it doesn't exist
91
+ 2. `ensureCustomDomain` — attaches `*.<domain>` to the project
92
+ 3. `dns.upsertRecord` — adds a wildcard CNAME `*.<domain>` → `<project>.pages.dev`
93
+
94
+ The combined `deploy.yml` thin caller then routes `push` events to GitHub Pages and `pull_request` events to Cloudflare Pages via `cloudflare/pages-action`. Preview URLs resolve as `<repo>-pr-<n>.<domain>`.
package/dist/index.d.mts CHANGED
@@ -1,8 +1,35 @@
1
- import { AuthError, Dns, DnsRecord, ResolveTokenInput } from "@theholocron/cli";
1
+ import { AuthError, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, ResolveTokenInput } from "@theholocron/cli";
2
2
  import { CloudflareClient, CloudflareClientOptions, createCloudflareClient } from "@theholocron/cloudflare-client";
3
3
  //#region src/auth.d.ts
4
4
  declare const resolveToken: (input?: ResolveTokenInput) => string;
5
5
  //#endregion
6
+ //#region src/capabilities/deployment.d.ts
7
+ declare class CloudflareDeployment implements Deployment {
8
+ private readonly client;
9
+ private readonly accountId;
10
+ readonly key: "deployment";
11
+ readonly providerName = "cloudflare";
12
+ constructor(client: CloudflareClient, accountId: string);
13
+ listProjects(): Promise<DeploymentProject[]>;
14
+ ensureProject(input: {
15
+ name: string;
16
+ framework?: string;
17
+ repo?: string;
18
+ rootDirectory?: string;
19
+ }): Promise<DeploymentProject>;
20
+ updateProjectSettings(projectId: string, settings: DeploymentProjectSettings): Promise<DeploymentProject>;
21
+ listEnvVars(projectId: string, target: DeploymentTarget): Promise<string[]>;
22
+ setEnvVar(projectId: string, target: DeploymentTarget, name: string, value: string): Promise<void>;
23
+ triggerDeployment(input: {
24
+ projectId: string;
25
+ branch: string;
26
+ target?: DeploymentTrigger;
27
+ }): Promise<DeploymentRecord>;
28
+ getDeployment(deploymentId: string): Promise<DeploymentRecord>;
29
+ ensureCustomDomain(projectId: string, hostname: string): Promise<void>;
30
+ private getProjectByName;
31
+ }
32
+ //#endregion
6
33
  //#region src/capabilities/dns.d.ts
7
34
  declare class CloudflareDns implements Dns {
8
35
  private readonly client;
@@ -42,8 +69,8 @@ declare function verifyToken(token: string, opts?: VerifyTokenOptions): Promise<
42
69
  //#region src/index.d.ts
43
70
  interface CloudflarePluginOptions extends ResolveTokenInput {
44
71
  /**
45
- * Cloudflare account id. Optional for DNS operations; required only
46
- * for account-scoped endpoints (tunnels, custom nameservers).
72
+ * Cloudflare account ID. Required for Pages (deployment) operations;
73
+ * optional for DNS-only use.
47
74
  */
48
75
  accountId?: string;
49
76
  /** Override base URL for tests. */
@@ -56,12 +83,14 @@ interface PluginContext {
56
83
  }
57
84
  declare function createContext(options?: CloudflarePluginOptions): PluginContext;
58
85
  declare function dns(ctx: PluginContext): Dns;
86
+ declare function deployment(ctx: PluginContext): Deployment;
59
87
  declare function createPlugin(options?: CloudflarePluginOptions): {
60
88
  name: string;
61
89
  capabilities: {
90
+ deployment?: (() => Deployment) | undefined;
62
91
  dns: () => Dns;
63
92
  };
64
93
  };
65
94
  declare const AUTH_HINT: string;
66
95
  //#endregion
67
- export { AUTH_HINT, AuthError, type CloudflareClient, type CloudflareClientOptions, CloudflareDns, CloudflarePluginOptions, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createCloudflareClient, createContext, createPlugin, dns, resolveToken, verifyToken };
96
+ export { AUTH_HINT, AuthError, type CloudflareClient, type CloudflareClientOptions, CloudflareDeployment, CloudflareDns, CloudflarePluginOptions, PluginContext, type ResolveTokenInput, type VerifyTokenFailure, type VerifyTokenResult, type VerifyTokenSuccess, createCloudflareClient, createContext, createPlugin, deployment, dns, resolveToken, verifyToken };
package/dist/index.mjs CHANGED
@@ -8,6 +8,102 @@ const resolveToken = createResolveToken({
8
8
  errorMessage: "no Cloudflare API token found. Pass --token <TOKEN>, set HOLOCRON_CLOUDFLARE_TOKEN / CLOUDFLARE_API_TOKEN, or run: holocron auth set cloudflare <TOKEN>"
9
9
  });
10
10
  //#endregion
11
+ //#region src/capabilities/deployment.ts
12
+ var CloudflareDeployment = class {
13
+ client;
14
+ accountId;
15
+ key = "deployment";
16
+ providerName = "cloudflare";
17
+ constructor(client, accountId) {
18
+ this.client = client;
19
+ this.accountId = accountId;
20
+ }
21
+ async listProjects() {
22
+ return (await this.client.pages.listProjects(this.accountId)).map(mapProject);
23
+ }
24
+ async ensureProject(input) {
25
+ const existing = await this.getProjectByName(input.name);
26
+ if (existing) return existing;
27
+ return mapProject(await this.client.pages.createProject(this.accountId, {
28
+ name: input.name,
29
+ production_branch: "main"
30
+ }));
31
+ }
32
+ async updateProjectSettings(projectId, settings) {
33
+ return mapProject(await this.client.pages.getProject(this.accountId, projectId));
34
+ }
35
+ async listEnvVars(projectId, target) {
36
+ const project = await this.client.pages.getProject(this.accountId, projectId);
37
+ const envConfig = target === "production" ? project.deployment_configs.production : project.deployment_configs.preview;
38
+ return Object.keys(envConfig.env_vars);
39
+ }
40
+ async setEnvVar(projectId, target, name, value) {
41
+ const cfg = (await this.client.pages.getProject(this.accountId, projectId)).deployment_configs;
42
+ const scope = target === "production" ? "production" : "preview";
43
+ const updated = {
44
+ ...cfg[scope].env_vars,
45
+ [name]: {
46
+ value,
47
+ type: "plain_text"
48
+ }
49
+ };
50
+ await this.client.pages.updateProject(this.accountId, projectId, { deployment_configs: {
51
+ ...cfg,
52
+ [scope]: { env_vars: updated }
53
+ } });
54
+ }
55
+ async triggerDeployment(input) {
56
+ return mapDeployment(await this.client.pages.createDeployment(this.accountId, input.projectId, input.branch), input.projectId, input.branch, input.target);
57
+ }
58
+ async getDeployment(deploymentId) {
59
+ const sep = deploymentId.indexOf(":");
60
+ if (sep === -1) throw new ProviderApiError(`Invalid Cloudflare Pages deployment id "${deploymentId}" — expected "projectName:deploymentId"`, 400, void 0);
61
+ const projectName = deploymentId.slice(0, sep);
62
+ const cfDeployId = deploymentId.slice(sep + 1);
63
+ const raw = await this.client.pages.getDeployment(this.accountId, projectName, cfDeployId);
64
+ return mapDeployment(raw, projectName, raw.deployment_trigger.metadata.branch, void 0);
65
+ }
66
+ async ensureCustomDomain(projectId, hostname) {
67
+ if ((await this.client.pages.listDomains(this.accountId, projectId)).some((d) => d.name === hostname)) return;
68
+ await this.client.pages.addDomain(this.accountId, projectId, hostname);
69
+ }
70
+ async getProjectByName(name) {
71
+ try {
72
+ return mapProject(await this.client.pages.getProject(this.accountId, name));
73
+ } catch (err) {
74
+ if (err instanceof ProviderApiError && err.status === 404) return null;
75
+ throw err;
76
+ }
77
+ }
78
+ };
79
+ function mapProject(raw) {
80
+ return {
81
+ id: raw.name,
82
+ name: raw.name
83
+ };
84
+ }
85
+ function mapDeployment(raw, projectName, branch, target) {
86
+ const record = {
87
+ id: `${projectName}:${raw.id}`,
88
+ url: raw.url,
89
+ branch,
90
+ status: normalizeStatus(raw.latest_stage.status)
91
+ };
92
+ if (target) record.target = target;
93
+ return record;
94
+ }
95
+ function normalizeStatus(status) {
96
+ switch (status) {
97
+ case "idle": return "queued";
98
+ case "active": return "building";
99
+ case "success": return "ready";
100
+ case "failure": return "error";
101
+ case "canceled": return "cancelled";
102
+ /* c8 ignore next */
103
+ default: return "error";
104
+ }
105
+ }
106
+ //#endregion
11
107
  //#region src/capabilities/dns.ts
12
108
  var CloudflareDns = class {
13
109
  client;
@@ -99,10 +195,15 @@ async function verifyToken(token, opts = {}) {
99
195
  //#endregion
100
196
  //#region src/index.ts
101
197
  function createContext(options = {}) {
198
+ const token = resolveToken(options);
199
+ const accountId = options.accountId ?? process.env.CLOUDFLARE_ACCOUNT_ID;
102
200
  return {
103
- options,
201
+ options: {
202
+ ...options,
203
+ accountId
204
+ },
104
205
  client: createCloudflareClient({
105
- token: resolveToken(options),
206
+ token,
106
207
  baseUrl: options.baseUrl,
107
208
  fetch: options.fetch
108
209
  })
@@ -111,13 +212,20 @@ function createContext(options = {}) {
111
212
  function dns(ctx) {
112
213
  return new CloudflareDns(ctx.client);
113
214
  }
215
+ function deployment(ctx) {
216
+ if (!ctx.options.accountId) throw new Error("Cloudflare accountId is required for Pages deployments — set accountId in the plugin options or CLOUDFLARE_ACCOUNT_ID env var");
217
+ return new CloudflareDeployment(ctx.client, ctx.options.accountId);
218
+ }
114
219
  function createPlugin(options = {}) {
115
220
  const ctx = createContext(options);
116
221
  return {
117
222
  name: "@theholocron/holocron-plugin-cloudflare",
118
- capabilities: { dns: () => dns(ctx) }
223
+ capabilities: {
224
+ dns: () => dns(ctx),
225
+ ...ctx.options.accountId ? { deployment: () => deployment(ctx) } : {}
226
+ }
119
227
  };
120
228
  }
121
- const AUTH_HINT = "create an API token at https://dash.cloudflare.com/profile/api-tokens with Zone:Read and DNS:Edit permissions, then run: holocron auth set cloudflare <TOKEN>";
229
+ const AUTH_HINT = "create an API token at https://dash.cloudflare.com/profile/api-tokens with Zone:Read, DNS:Edit, and Cloudflare Pages:Edit permissions, then run: holocron auth set cloudflare <TOKEN>";
122
230
  //#endregion
123
- export { AUTH_HINT, AuthError, CloudflareDns, createCloudflareClient, createContext, createPlugin, dns, resolveToken, verifyToken };
231
+ export { AUTH_HINT, AuthError, CloudflareDeployment, CloudflareDns, createCloudflareClient, createContext, createPlugin, deployment, dns, resolveToken, verifyToken };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/holocron-plugin-cloudflare",
3
- "version": "3.31.1",
3
+ "version": "3.33.0",
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,7 +30,7 @@
30
30
  "dist"
31
31
  ],
32
32
  "devDependencies": {
33
- "@theholocron/cloudflare-client": "^1.9.0",
33
+ "@theholocron/cloudflare-client": "^1.11.2",
34
34
  "@theholocron/eslint-config": "^7.25.3",
35
35
  "@theholocron/tsconfig": "^7.20.0",
36
36
  "@theholocron/tsdown-config": "^7.20.0",
@@ -46,11 +46,11 @@
46
46
  "tsx": "4.23.12",
47
47
  "typescript": "^5.9.3",
48
48
  "vitest": "^4.1.10",
49
- "@theholocron/cli": "3.31.1"
49
+ "@theholocron/cli": "3.33.0"
50
50
  },
51
51
  "peerDependencies": {
52
- "@theholocron/cloudflare-client": "^1.9.0",
53
- "@theholocron/cli": "3.31.1"
52
+ "@theholocron/cloudflare-client": "^1.11.2",
53
+ "@theholocron/cli": "3.33.0"
54
54
  },
55
55
  "peerDependenciesMeta": {
56
56
  "@theholocron/cloudflare-client": {}