alchemy 0.3.1 → 0.4.1
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 +8 -3
- package/lib/ai/document.d.ts +12 -1
- package/lib/ai/document.js +10 -1
- package/lib/ai/index.d.ts +0 -2
- package/lib/ai/index.js +0 -2
- package/lib/alchemy.d.ts +5 -0
- package/lib/alchemy.js +44 -1
- package/lib/apply.js +17 -10
- package/lib/aws/account-id.d.ts +4 -1
- package/lib/aws/account-id.js +1 -1
- package/lib/aws/credentials.d.ts +5 -0
- package/lib/aws/credentials.js +0 -0
- package/lib/aws/oidc/oidc-provider.js +2 -2
- package/lib/aws/role.js +35 -23
- package/lib/cloudflare/account-api-token.d.ts +168 -0
- package/lib/cloudflare/account-api-token.js +179 -0
- package/lib/cloudflare/account-id.d.ts +1 -0
- package/lib/cloudflare/account-id.js +0 -0
- package/lib/cloudflare/api.d.ts +38 -1
- package/lib/cloudflare/api.js +55 -4
- package/lib/cloudflare/assets.d.ts +74 -0
- package/lib/cloudflare/assets.js +73 -0
- package/lib/cloudflare/bindings.d.ts +6 -1
- package/lib/cloudflare/bindings.js +6 -0
- package/lib/cloudflare/bucket.d.ts +118 -3
- package/lib/cloudflare/bucket.js +269 -89
- package/lib/cloudflare/custom-domain.d.ts +75 -0
- package/lib/cloudflare/custom-domain.js +154 -0
- package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
- package/lib/cloudflare/index.d.ts +6 -1
- package/lib/cloudflare/index.js +6 -1
- package/lib/cloudflare/permission-groups.d.ts +78 -0
- package/lib/cloudflare/permission-groups.js +48 -0
- package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
- package/lib/cloudflare/r2-rest-state-store.js +55 -24
- package/lib/cloudflare/static-site.d.ts +17 -18
- package/lib/cloudflare/static-site.js +19 -10
- package/lib/cloudflare/worker.d.ts +14 -0
- package/lib/cloudflare/worker.js +150 -3
- package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
- package/lib/{util/encrypt.js → encrypt.js} +1 -1
- package/lib/fs/file-system-state-store.js +1 -1
- package/lib/fs/file.d.ts +18 -0
- package/lib/fs/file.js +6 -0
- package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
- package/lib/internal/docs/providers.js +196 -0
- package/lib/secret.d.ts +3 -0
- package/lib/secret.js +13 -0
- package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
- package/lib/{util/serde.js → serde.js} +12 -4
- package/lib/test/bun.js +3 -2
- package/lib/util/sha256.d.ts +1 -0
- package/lib/util/sha256.js +4 -0
- package/lib/web/vitepress/config.d.ts +2 -1
- package/lib/web/vitepress/config.js +1 -0
- package/lib/web/vitepress/index.d.ts +1 -0
- package/lib/web/vitepress/index.js +1 -0
- package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
- package/lib/web/vitepress/process-front-matter-files.js +68 -0
- package/lib/web/vitepress/vitepress.js +3 -2
- package/package.json +8 -4
- package/src/ai/document.ts +26 -2
- package/src/ai/index.ts +0 -2
- package/src/alchemy.ts +56 -2
- package/src/apply.ts +21 -20
- package/src/aws/account-id.ts +6 -2
- package/src/aws/credentials.ts +6 -0
- package/src/aws/oidc/oidc-provider.ts +17 -17
- package/src/aws/role.ts +68 -54
- package/src/cloudflare/account-api-token.ts +365 -0
- package/src/cloudflare/account-id.ts +0 -0
- package/src/cloudflare/api.ts +88 -17
- package/src/cloudflare/assets.ts +155 -0
- package/src/cloudflare/bindings.ts +11 -2
- package/src/cloudflare/bucket.ts +493 -133
- package/src/cloudflare/custom-domain.ts +318 -0
- package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
- package/src/cloudflare/index.ts +6 -1
- package/src/cloudflare/permission-groups.ts +137 -0
- package/src/cloudflare/r2-rest-state-store.ts +87 -42
- package/src/cloudflare/static-site.ts +20 -32
- package/src/cloudflare/worker.ts +253 -5
- package/src/dns/import-dns.ts +4 -4
- package/src/{util/encrypt.ts → encrypt.ts} +7 -10
- package/src/fs/file-system-state-store.ts +5 -5
- package/src/fs/file.ts +29 -0
- package/src/internal/docs/providers.ts +281 -0
- package/src/secret.ts +16 -0
- package/src/{util/serde.ts → serde.ts} +16 -10
- package/src/test/bun.ts +8 -7
- package/src/util/sha256.ts +5 -0
- package/src/web/vitepress/config.ts +3 -1
- package/src/web/vitepress/index.ts +1 -0
- package/src/web/vitepress/process-front-matter-files.ts +98 -0
- package/src/web/vitepress/vitepress.ts +3 -2
- package/lib/ai/approve.d.ts +0 -99
- package/lib/ai/approve.js +0 -76
- package/lib/ai/review.d.ts +0 -122
- package/lib/ai/review.js +0 -101
- package/lib/internal/getting-started.d.ts +0 -21
- package/lib/internal/getting-started.js +0 -87
- package/lib/internal/index.d.ts +0 -3
- package/lib/internal/index.js +0 -3
- package/lib/internal/providers.js +0 -172
- package/lib/internal/tutorial.d.ts +0 -104
- package/lib/internal/tutorial.js +0 -251
- package/src/ai/approve.ts +0 -163
- package/src/ai/review.ts +0 -213
- package/src/internal/getting-started.ts +0 -115
- package/src/internal/index.ts +0 -3
- package/src/internal/providers.ts +0 -241
- package/src/internal/tutorial.ts +0 -392
- /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { alchemy } from "../alchemy";
|
|
2
|
+
import type { Context } from "../context";
|
|
3
|
+
import { Resource } from "../resource";
|
|
4
|
+
import { Secret } from "../secret";
|
|
5
|
+
import { sha256 } from "../util/sha256";
|
|
6
|
+
import { createCloudflareApi } from "./api";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Permission group for a token policy
|
|
10
|
+
*/
|
|
11
|
+
export interface TokenPolicyPermissionGroup {
|
|
12
|
+
/**
|
|
13
|
+
* ID of the permission group
|
|
14
|
+
*/
|
|
15
|
+
id: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Optional metadata for the permission group
|
|
19
|
+
*/
|
|
20
|
+
meta?: Record<string, any>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Policy that defines what the token can access
|
|
25
|
+
*/
|
|
26
|
+
export interface TokenPolicy {
|
|
27
|
+
/**
|
|
28
|
+
* Effect of the policy
|
|
29
|
+
*/
|
|
30
|
+
effect: "allow" | "deny";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Permission groups to include in the policy
|
|
34
|
+
*/
|
|
35
|
+
permissionGroups: TokenPolicyPermissionGroup[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resources the policy applies to
|
|
39
|
+
*/
|
|
40
|
+
resources: Record<string, string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Condition for token usage (e.g., IP restrictions)
|
|
45
|
+
*/
|
|
46
|
+
export interface TokenCondition {
|
|
47
|
+
/**
|
|
48
|
+
* IP address conditions
|
|
49
|
+
*/
|
|
50
|
+
requestIp?: {
|
|
51
|
+
/**
|
|
52
|
+
* IP ranges to allow
|
|
53
|
+
*/
|
|
54
|
+
in?: string[];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* IP ranges to deny
|
|
58
|
+
*/
|
|
59
|
+
notIn?: string[];
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Properties for creating or updating an Account API Token
|
|
65
|
+
*/
|
|
66
|
+
export interface AccountApiTokenProps {
|
|
67
|
+
/**
|
|
68
|
+
* Name of the token
|
|
69
|
+
*/
|
|
70
|
+
name: string;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Policies that define what the token can access
|
|
74
|
+
*/
|
|
75
|
+
policies: TokenPolicy[];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Optional expiration date for the token (ISO format)
|
|
79
|
+
*/
|
|
80
|
+
expiresOn?: string;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Optional "not before" date (token is not valid before this date) (ISO format)
|
|
84
|
+
*/
|
|
85
|
+
notBefore?: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Optional conditions for token use (like IP restrictions)
|
|
89
|
+
*/
|
|
90
|
+
condition?: TokenCondition;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cloudflare API token format as returned by the API
|
|
95
|
+
*/
|
|
96
|
+
interface CloudflareApiToken {
|
|
97
|
+
id: string;
|
|
98
|
+
name: string;
|
|
99
|
+
status: string;
|
|
100
|
+
policies: {
|
|
101
|
+
effect: "allow" | "deny";
|
|
102
|
+
permission_groups: {
|
|
103
|
+
id: string;
|
|
104
|
+
meta: Record<string, any>;
|
|
105
|
+
}[];
|
|
106
|
+
resources: Record<string, string>;
|
|
107
|
+
}[];
|
|
108
|
+
expires_on?: string;
|
|
109
|
+
not_before?: string;
|
|
110
|
+
condition?: {
|
|
111
|
+
request_ip?: {
|
|
112
|
+
in?: string[];
|
|
113
|
+
not_in?: string[];
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
value?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Output returned after Account API Token creation/update
|
|
121
|
+
*/
|
|
122
|
+
export interface AccountApiToken
|
|
123
|
+
extends Resource<"cloudflare::AccountApiToken">,
|
|
124
|
+
AccountApiTokenProps {
|
|
125
|
+
/**
|
|
126
|
+
* The ID of the token
|
|
127
|
+
*
|
|
128
|
+
* Equiv. to ACCESS_KEY_ID
|
|
129
|
+
*/
|
|
130
|
+
id: string;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Status of the token
|
|
134
|
+
*/
|
|
135
|
+
status: string;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Actual token value (only available on creation)
|
|
139
|
+
* Stored as a Secret for security
|
|
140
|
+
*
|
|
141
|
+
* Equiv. to SECRET_ACCESS_KEY
|
|
142
|
+
*/
|
|
143
|
+
value?: Secret;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Access key ID for the token
|
|
147
|
+
*
|
|
148
|
+
* An alias of {@link id}
|
|
149
|
+
*/
|
|
150
|
+
accessKeyId: string;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Secret access key for the token
|
|
154
|
+
*
|
|
155
|
+
* The SHA-256 hash of the token {@link value}
|
|
156
|
+
*
|
|
157
|
+
* @see https://developers.cloudflare.com/r2/api/tokens/#get-s3-api-credentials-from-an-api-token
|
|
158
|
+
*/
|
|
159
|
+
secretAccessKey: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Creates a Cloudflare Account API Token with specified permissions.
|
|
164
|
+
*
|
|
165
|
+
* @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* // First, fetch all permission groups
|
|
169
|
+
* const permissions = await PermissionGroups("cloudflare-permissions", {
|
|
170
|
+
* accountId: cfAccountId,
|
|
171
|
+
* });
|
|
172
|
+
*
|
|
173
|
+
* // Create a token with read-only permissions for specific zones
|
|
174
|
+
* const readOnlyToken = await AccountApiToken("readonly-token", {
|
|
175
|
+
* name: "Readonly Zone Token",
|
|
176
|
+
* policies: [
|
|
177
|
+
* {
|
|
178
|
+
* effect: "allow",
|
|
179
|
+
* permissionGroups: [
|
|
180
|
+
* { id: permissions["Zone Read"].id },
|
|
181
|
+
* { id: permissions["Analytics Read"].id }
|
|
182
|
+
* ],
|
|
183
|
+
* resources: {
|
|
184
|
+
* "com.cloudflare.api.account.zone.22b1de5f1c0e4b3ea97bb1e963b06a43": "*",
|
|
185
|
+
* "com.cloudflare.api.account.zone.eb78d65290b24279ba6f44721b3ea3c4": "*"
|
|
186
|
+
* }
|
|
187
|
+
* }
|
|
188
|
+
* ],
|
|
189
|
+
* expiresOn: "2024-12-31T23:59:59Z"
|
|
190
|
+
* });
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* // Create a token with time and IP restrictions
|
|
194
|
+
* const restrictedToken = await AccountApiToken("restricted-token", {
|
|
195
|
+
* name: "Restricted Access Token",
|
|
196
|
+
* policies: [
|
|
197
|
+
* {
|
|
198
|
+
* effect: "allow",
|
|
199
|
+
* permissionGroups: [
|
|
200
|
+
* { id: permissions["Worker Routes Edit"].id }
|
|
201
|
+
* ],
|
|
202
|
+
* resources: {
|
|
203
|
+
* "com.cloudflare.api.account.worker.route.*": "*"
|
|
204
|
+
* }
|
|
205
|
+
* }
|
|
206
|
+
* ],
|
|
207
|
+
* notBefore: "2023-01-01T00:00:00Z",
|
|
208
|
+
* expiresOn: "2023-12-31T23:59:59Z",
|
|
209
|
+
* condition: {
|
|
210
|
+
* requestIp: {
|
|
211
|
+
* in: ["192.168.1.0/24", "10.0.0.0/8"],
|
|
212
|
+
* notIn: ["192.168.1.100/32"]
|
|
213
|
+
* }
|
|
214
|
+
* }
|
|
215
|
+
* });
|
|
216
|
+
*/
|
|
217
|
+
export const AccountApiToken = Resource(
|
|
218
|
+
"cloudflare::AccountApiToken",
|
|
219
|
+
async function (
|
|
220
|
+
this: Context<AccountApiToken>,
|
|
221
|
+
id: string,
|
|
222
|
+
props: AccountApiTokenProps
|
|
223
|
+
): Promise<AccountApiToken> {
|
|
224
|
+
// Create Cloudflare API client with automatic account discovery
|
|
225
|
+
const api = await createCloudflareApi();
|
|
226
|
+
|
|
227
|
+
if (this.phase === "delete") {
|
|
228
|
+
// Delete token if we have an ID
|
|
229
|
+
if (this.output?.id) {
|
|
230
|
+
try {
|
|
231
|
+
const deleteResponse = await api.delete(
|
|
232
|
+
`/accounts/${api.accountId}/tokens/${this.output.id}`
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
if (!deleteResponse.ok && deleteResponse.status !== 404) {
|
|
236
|
+
const errorData: any = await deleteResponse.json().catch(() => ({
|
|
237
|
+
errors: [{ message: deleteResponse.statusText }],
|
|
238
|
+
}));
|
|
239
|
+
console.error(`Error deleting token '${props.name}':`, errorData);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.error(`Error deleting token '${props.name}':`, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Return destroyed state
|
|
247
|
+
return this.destroy();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Transform our properties to API format
|
|
251
|
+
const apiPayload = {
|
|
252
|
+
name: props.name,
|
|
253
|
+
policies: props.policies.map((policy) => ({
|
|
254
|
+
effect: policy.effect,
|
|
255
|
+
permission_groups: policy.permissionGroups.map((pg) => ({
|
|
256
|
+
id: pg.id,
|
|
257
|
+
meta: pg.meta || {},
|
|
258
|
+
})),
|
|
259
|
+
resources: policy.resources,
|
|
260
|
+
})),
|
|
261
|
+
// Format dates for Cloudflare API (removing milliseconds)
|
|
262
|
+
...(props.expiresOn
|
|
263
|
+
? { expires_on: formatCloudflareDate(props.expiresOn) }
|
|
264
|
+
: {}),
|
|
265
|
+
...(props.notBefore
|
|
266
|
+
? { not_before: formatCloudflareDate(props.notBefore) }
|
|
267
|
+
: {}),
|
|
268
|
+
...(props.condition
|
|
269
|
+
? {
|
|
270
|
+
condition: {
|
|
271
|
+
request_ip: props.condition.requestIp
|
|
272
|
+
? {
|
|
273
|
+
in: props.condition.requestIp.in || [],
|
|
274
|
+
not_in: props.condition.requestIp.notIn || [],
|
|
275
|
+
}
|
|
276
|
+
: undefined,
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Formats a date string for Cloudflare API by removing milliseconds
|
|
284
|
+
* Converts from "2023-01-01T00:00:00.000Z" to "2023-01-01T00:00:00Z"
|
|
285
|
+
*/
|
|
286
|
+
function formatCloudflareDate(dateStr: string): string {
|
|
287
|
+
return dateStr.replace(/\.\d{3}Z$/, "Z");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
let response;
|
|
291
|
+
let tokenValue;
|
|
292
|
+
|
|
293
|
+
if (this.phase === "update" && this.output?.id) {
|
|
294
|
+
// Update existing token
|
|
295
|
+
response = await api.put(
|
|
296
|
+
`/accounts/${api.accountId}/tokens/${this.output.id}`,
|
|
297
|
+
apiPayload
|
|
298
|
+
);
|
|
299
|
+
} else {
|
|
300
|
+
// Create new token
|
|
301
|
+
response = await api.post(
|
|
302
|
+
`/accounts/${api.accountId}/tokens`,
|
|
303
|
+
apiPayload
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!response.ok) {
|
|
308
|
+
const errorData: any = await response.json().catch(() => ({
|
|
309
|
+
errors: [{ message: response.statusText }],
|
|
310
|
+
}));
|
|
311
|
+
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Error ${this.phase === "update" ? "updating" : "creating"} token '${props.name}': ${
|
|
314
|
+
errorData.errors?.[0]?.message || response.statusText
|
|
315
|
+
}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const result: { result: CloudflareApiToken } = await response.json();
|
|
320
|
+
const tokenData = result.result;
|
|
321
|
+
|
|
322
|
+
if (tokenData.value) {
|
|
323
|
+
tokenValue = alchemy.secret(tokenData.value);
|
|
324
|
+
} else {
|
|
325
|
+
if (!this.output?.value) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Token '${props.name}' was created but we have no record of its value. Try deleting and recreating the token.`
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
tokenValue = this.output?.value;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Transform API response to our format
|
|
334
|
+
return this({
|
|
335
|
+
id: tokenData.id,
|
|
336
|
+
name: tokenData.name,
|
|
337
|
+
status: tokenData.status,
|
|
338
|
+
policies: tokenData.policies.map((policy) => ({
|
|
339
|
+
effect: policy.effect,
|
|
340
|
+
permissionGroups: policy.permission_groups.map((pg) => ({
|
|
341
|
+
id: pg.id,
|
|
342
|
+
meta: pg.meta,
|
|
343
|
+
})),
|
|
344
|
+
resources: policy.resources,
|
|
345
|
+
})),
|
|
346
|
+
...(tokenData.expires_on ? { expiresOn: tokenData.expires_on } : {}),
|
|
347
|
+
...(tokenData.not_before ? { notBefore: tokenData.not_before } : {}),
|
|
348
|
+
...(tokenData.condition
|
|
349
|
+
? {
|
|
350
|
+
condition: {
|
|
351
|
+
requestIp: tokenData.condition.request_ip
|
|
352
|
+
? {
|
|
353
|
+
in: tokenData.condition.request_ip.in || [],
|
|
354
|
+
notIn: tokenData.condition.request_ip.not_in || [],
|
|
355
|
+
}
|
|
356
|
+
: undefined,
|
|
357
|
+
},
|
|
358
|
+
}
|
|
359
|
+
: {}),
|
|
360
|
+
value: tokenValue,
|
|
361
|
+
accessKeyId: tokenData.id,
|
|
362
|
+
secretAccessKey: sha256(tokenValue.unencrypted),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
);
|
|
File without changes
|
package/src/cloudflare/api.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Secret } from "../secret";
|
|
1
2
|
import { type CloudflareAuthOptions, getCloudflareHeaders } from "./auth";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,7 +8,7 @@ export interface CloudflareApiOptions {
|
|
|
7
8
|
/**
|
|
8
9
|
* API Key to use (overrides CLOUDFLARE_API_KEY env var)
|
|
9
10
|
*/
|
|
10
|
-
apiKey?:
|
|
11
|
+
apiKey?: Secret;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
|
|
@@ -35,6 +36,65 @@ interface CloudflareAccount {
|
|
|
35
36
|
name: string;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Custom error class for Cloudflare API errors
|
|
41
|
+
* Includes HTTP status information from the Response
|
|
42
|
+
*/
|
|
43
|
+
export class CloudflareApiError extends Error {
|
|
44
|
+
/**
|
|
45
|
+
* HTTP status code
|
|
46
|
+
*/
|
|
47
|
+
status: number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* HTTP status text
|
|
51
|
+
*/
|
|
52
|
+
statusText: string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Raw error data from the API
|
|
56
|
+
*/
|
|
57
|
+
errorData?: any;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a new CloudflareApiError
|
|
61
|
+
*/
|
|
62
|
+
constructor(message: string, response: Response, errorData?: any) {
|
|
63
|
+
super(message);
|
|
64
|
+
this.name = "CloudflareApiError";
|
|
65
|
+
this.status = response.status;
|
|
66
|
+
this.statusText = response.statusText;
|
|
67
|
+
this.errorData = errorData;
|
|
68
|
+
|
|
69
|
+
// Ensure instanceof works correctly
|
|
70
|
+
Object.setPrototypeOf(this, CloudflareApiError.prototype);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Helper function to handle API errors
|
|
76
|
+
*
|
|
77
|
+
* @param response The fetch Response object
|
|
78
|
+
* @param action The action being performed (e.g., "creating", "deleting")
|
|
79
|
+
* @param resourceType The type of resource being acted upon (e.g., "R2 bucket", "Worker")
|
|
80
|
+
* @param resourceName The name/identifier of the specific resource
|
|
81
|
+
* @returns Never returns - always throws an error
|
|
82
|
+
*/
|
|
83
|
+
export async function handleApiError(
|
|
84
|
+
response: Response,
|
|
85
|
+
action: string,
|
|
86
|
+
resourceType: string,
|
|
87
|
+
resourceName: string
|
|
88
|
+
): Promise<never> {
|
|
89
|
+
const json: any = await response.json();
|
|
90
|
+
console.log(json);
|
|
91
|
+
const errorData: any = json.errors || [{ message: response.statusText }];
|
|
92
|
+
|
|
93
|
+
const errorMessage = `Error ${action} ${resourceType} '${resourceName}': ${errorData.errors?.[0]?.message || response.statusText}`;
|
|
94
|
+
|
|
95
|
+
throw new CloudflareApiError(errorMessage, response, errorData);
|
|
96
|
+
}
|
|
97
|
+
|
|
38
98
|
/**
|
|
39
99
|
* Creates a CloudflareApi instance with automatic account ID discovery if not provided
|
|
40
100
|
*
|
|
@@ -42,24 +102,33 @@ interface CloudflareAccount {
|
|
|
42
102
|
* @returns Promise resolving to a CloudflareApi instance
|
|
43
103
|
*/
|
|
44
104
|
export async function createCloudflareApi(
|
|
45
|
-
options: CloudflareApiOptions = {}
|
|
105
|
+
options: CloudflareApiOptions = {}
|
|
46
106
|
): Promise<CloudflareApi> {
|
|
47
107
|
try {
|
|
48
108
|
return new CloudflareApi({
|
|
49
109
|
...options,
|
|
50
|
-
accountId:
|
|
51
|
-
options.accountId ||
|
|
52
|
-
process.env.CLOUDFLARE_ACCOUNT_ID ||
|
|
53
|
-
(await fetchAccountId()),
|
|
110
|
+
accountId: await CloudflareAccountId(options),
|
|
54
111
|
});
|
|
55
112
|
} catch (error) {
|
|
56
113
|
console.error("Error during Cloudflare account ID discovery:", error);
|
|
57
114
|
throw new Error(
|
|
58
|
-
"Failed to automatically discover Cloudflare account ID. Please provide an account ID explicitly or ensure your API token/key has sufficient permissions."
|
|
115
|
+
"Failed to automatically discover Cloudflare account ID. Please provide an account ID explicitly or ensure your API token/key has sufficient permissions."
|
|
59
116
|
);
|
|
60
117
|
}
|
|
61
118
|
}
|
|
62
119
|
|
|
120
|
+
export type CloudflareAccountId = string & {
|
|
121
|
+
readonly __brand: "CloudflareAccountId";
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export async function CloudflareAccountId(
|
|
125
|
+
options: CloudflareApiOptions = {}
|
|
126
|
+
): Promise<CloudflareAccountId> {
|
|
127
|
+
return (options.accountId ||
|
|
128
|
+
process.env.CLOUDFLARE_ACCOUNT_ID ||
|
|
129
|
+
(await fetchAccountId())) as CloudflareAccountId;
|
|
130
|
+
}
|
|
131
|
+
|
|
63
132
|
/**
|
|
64
133
|
* Cloudflare API client using raw fetch
|
|
65
134
|
*/
|
|
@@ -93,10 +162,12 @@ export class CloudflareApi {
|
|
|
93
162
|
this.baseUrl = "https://api.cloudflare.com/client/v4";
|
|
94
163
|
|
|
95
164
|
const apiKey =
|
|
96
|
-
options.apiKey ||
|
|
165
|
+
options.apiKey?.unencrypted ||
|
|
166
|
+
process.env.CLOUDFLARE_API_KEY ||
|
|
167
|
+
undefined;
|
|
97
168
|
if (!apiKey) {
|
|
98
169
|
throw new Error(
|
|
99
|
-
"No API key provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
170
|
+
"No API key provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
100
171
|
);
|
|
101
172
|
}
|
|
102
173
|
this.apiKey = apiKey;
|
|
@@ -104,7 +175,7 @@ export class CloudflareApi {
|
|
|
104
175
|
this.email = options.email || process.env.CLOUDFLARE_EMAIL;
|
|
105
176
|
if (!this.email) {
|
|
106
177
|
throw new Error(
|
|
107
|
-
"No email provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
178
|
+
"No email provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
108
179
|
);
|
|
109
180
|
}
|
|
110
181
|
|
|
@@ -113,7 +184,7 @@ export class CloudflareApi {
|
|
|
113
184
|
options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID || "";
|
|
114
185
|
if (!this.accountId) {
|
|
115
186
|
throw new Error(
|
|
116
|
-
"No account ID provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
187
|
+
"No account ID provided. Use createCloudflareApi() instead for automatic account discovery."
|
|
117
188
|
);
|
|
118
189
|
}
|
|
119
190
|
|
|
@@ -146,7 +217,7 @@ export class CloudflareApi {
|
|
|
146
217
|
// Get auth headers (async now)
|
|
147
218
|
const authHeaders = await getCloudflareHeaders(
|
|
148
219
|
contentType,
|
|
149
|
-
this.authOptions
|
|
220
|
+
this.authOptions
|
|
150
221
|
);
|
|
151
222
|
|
|
152
223
|
// Combine all headers
|
|
@@ -195,7 +266,7 @@ export class CloudflareApi {
|
|
|
195
266
|
async post(
|
|
196
267
|
path: string,
|
|
197
268
|
body: any,
|
|
198
|
-
init: RequestInit = {}
|
|
269
|
+
init: RequestInit = {}
|
|
199
270
|
): Promise<Response> {
|
|
200
271
|
const requestBody =
|
|
201
272
|
body instanceof FormData
|
|
@@ -212,7 +283,7 @@ export class CloudflareApi {
|
|
|
212
283
|
async put(
|
|
213
284
|
path: string,
|
|
214
285
|
body: any,
|
|
215
|
-
init: RequestInit = {}
|
|
286
|
+
init: RequestInit = {}
|
|
216
287
|
): Promise<Response> {
|
|
217
288
|
const requestBody = body instanceof FormData ? body : JSON.stringify(body);
|
|
218
289
|
return this.fetch(path, { ...init, method: "PUT", body: requestBody });
|
|
@@ -224,7 +295,7 @@ export class CloudflareApi {
|
|
|
224
295
|
async patch(
|
|
225
296
|
path: string,
|
|
226
297
|
body: any,
|
|
227
|
-
init: RequestInit = {}
|
|
298
|
+
init: RequestInit = {}
|
|
228
299
|
): Promise<Response> {
|
|
229
300
|
return this.fetch(path, {
|
|
230
301
|
...init,
|
|
@@ -264,7 +335,7 @@ async function fetchAccountId(): Promise<string> {
|
|
|
264
335
|
}));
|
|
265
336
|
|
|
266
337
|
throw new Error(
|
|
267
|
-
`Error fetching Cloudflare accounts: ${errorData.errors?.[0]?.message || response.statusText}
|
|
338
|
+
`Error fetching Cloudflare accounts: ${errorData.errors?.[0]?.message || response.statusText}`
|
|
268
339
|
);
|
|
269
340
|
}
|
|
270
341
|
|
|
@@ -273,7 +344,7 @@ async function fetchAccountId(): Promise<string> {
|
|
|
273
344
|
|
|
274
345
|
if (!accounts || accounts.length === 0) {
|
|
275
346
|
throw new Error(
|
|
276
|
-
"No Cloudflare accounts found. Check your API token/key permissions."
|
|
347
|
+
"No Cloudflare accounts found. Check your API token/key permissions."
|
|
277
348
|
);
|
|
278
349
|
}
|
|
279
350
|
|