@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 +50 -8
- package/dist/index.d.mts +33 -4
- package/dist/index.mjs +113 -5
- package/package.json +5 -5
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
* for
|
|
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
|
|
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: {
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
49
|
+
"@theholocron/cli": "3.33.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@theholocron/cloudflare-client": "^1.
|
|
53
|
-
"@theholocron/cli": "3.
|
|
52
|
+
"@theholocron/cloudflare-client": "^1.11.2",
|
|
53
|
+
"@theholocron/cli": "3.33.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependenciesMeta": {
|
|
56
56
|
"@theholocron/cloudflare-client": {}
|