alchemy 0.3.0 → 0.4.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.
Files changed (154) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/astro-file.js +7 -24
  3. package/lib/ai/client.d.ts +7 -0
  4. package/lib/ai/client.js +45 -0
  5. package/lib/ai/css-file.js +7 -24
  6. package/lib/ai/data.d.ts +26 -21
  7. package/lib/ai/data.js +33 -22
  8. package/lib/ai/document.d.ts +76 -5
  9. package/lib/ai/document.js +112 -56
  10. package/lib/ai/html-file.js +7 -24
  11. package/lib/ai/json-file.js +11 -32
  12. package/lib/ai/typescript-file.d.ts +1 -2
  13. package/lib/ai/typescript-file.js +8 -39
  14. package/lib/ai/vue-file.js +7 -24
  15. package/lib/ai/yaml-file.js +12 -48
  16. package/lib/alchemy.d.ts +5 -0
  17. package/lib/alchemy.js +61 -3
  18. package/lib/apply.js +18 -11
  19. package/lib/aws/account-id.d.ts +4 -1
  20. package/lib/aws/account-id.js +1 -1
  21. package/lib/aws/credentials.d.ts +5 -0
  22. package/lib/aws/credentials.js +0 -0
  23. package/lib/aws/oidc/oidc-provider.js +2 -2
  24. package/lib/aws/role.js +35 -23
  25. package/lib/cloudflare/account-api-token.d.ts +168 -0
  26. package/lib/cloudflare/account-api-token.js +179 -0
  27. package/lib/cloudflare/account-id.d.ts +1 -0
  28. package/lib/cloudflare/account-id.js +0 -0
  29. package/lib/cloudflare/api.d.ts +38 -1
  30. package/lib/cloudflare/api.js +55 -4
  31. package/lib/cloudflare/bucket.d.ts +118 -3
  32. package/lib/cloudflare/bucket.js +269 -89
  33. package/lib/cloudflare/custom-domain.d.ts +75 -0
  34. package/lib/cloudflare/custom-domain.js +154 -0
  35. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  36. package/lib/cloudflare/generate-asset-manifest.js +1 -1
  37. package/lib/cloudflare/index.d.ts +5 -1
  38. package/lib/cloudflare/index.js +5 -1
  39. package/lib/cloudflare/permission-groups.d.ts +78 -0
  40. package/lib/cloudflare/permission-groups.js +48 -0
  41. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  42. package/lib/cloudflare/r2-rest-state-store.js +3 -2
  43. package/lib/cloudflare/static-site-router.js +16 -7
  44. package/lib/cloudflare/static-site.d.ts +17 -18
  45. package/lib/cloudflare/static-site.js +19 -7
  46. package/lib/cloudflare/upload-asset-manifest.js +4 -2
  47. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  48. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  49. package/lib/fs/copy-file.d.ts +54 -0
  50. package/lib/fs/copy-file.js +63 -0
  51. package/lib/fs/file-system-state-store.js +1 -1
  52. package/lib/fs/file.d.ts +31 -0
  53. package/lib/fs/file.js +30 -5
  54. package/lib/fs/index.d.ts +6 -0
  55. package/lib/fs/index.js +6 -0
  56. package/lib/fs/static-astro-file.d.ts +34 -0
  57. package/lib/fs/static-astro-file.js +39 -0
  58. package/lib/fs/static-css-file.d.ts +24 -0
  59. package/lib/fs/static-css-file.js +29 -0
  60. package/lib/fs/static-html-file.d.ts +31 -0
  61. package/lib/fs/static-html-file.js +36 -0
  62. package/lib/fs/static-json-file.d.ts +2 -2
  63. package/lib/fs/static-json-file.js +3 -2
  64. package/lib/fs/static-text-file.d.ts +1 -1
  65. package/lib/fs/static-text-file.js +3 -2
  66. package/lib/fs/static-typescript-file.d.ts +2 -2
  67. package/lib/fs/static-typescript-file.js +3 -2
  68. package/lib/fs/static-vue-file.d.ts +28 -0
  69. package/lib/fs/static-vue-file.js +33 -0
  70. package/lib/fs/static-yaml-file.d.ts +11 -8
  71. package/lib/fs/static-yaml-file.js +3 -2
  72. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -4
  73. package/lib/internal/docs/providers.js +196 -0
  74. package/lib/secret.d.ts +3 -0
  75. package/lib/secret.js +13 -0
  76. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  77. package/lib/{util/serde.js → serde.js} +12 -4
  78. package/lib/test/bun.js +3 -2
  79. package/lib/util/sha256.d.ts +1 -0
  80. package/lib/util/sha256.js +4 -0
  81. package/lib/web/vitepress/config.d.ts +11 -0
  82. package/lib/web/vitepress/config.js +32 -0
  83. package/lib/web/vitepress/home-page.d.ts +5 -3
  84. package/lib/web/vitepress/home-page.js +48 -35
  85. package/lib/web/vitepress/index.d.ts +2 -0
  86. package/lib/web/vitepress/index.js +2 -0
  87. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  88. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  89. package/lib/web/vitepress/vitepress.d.ts +5 -4
  90. package/lib/web/vitepress/vitepress.js +20 -36
  91. package/package.json +8 -4
  92. package/src/ai/astro-file.ts +11 -30
  93. package/src/ai/client.ts +59 -0
  94. package/src/ai/css-file.ts +11 -30
  95. package/src/ai/data.ts +50 -25
  96. package/src/ai/document.ts +171 -66
  97. package/src/ai/html-file.ts +11 -30
  98. package/src/ai/json-file.ts +13 -37
  99. package/src/ai/typescript-file.ts +14 -48
  100. package/src/ai/vue-file.ts +11 -30
  101. package/src/ai/yaml-file.ts +13 -53
  102. package/src/alchemy.ts +74 -9
  103. package/src/apply.ts +27 -26
  104. package/src/aws/account-id.ts +6 -2
  105. package/src/aws/credentials.ts +6 -0
  106. package/src/aws/oidc/oidc-provider.ts +17 -17
  107. package/src/aws/role.ts +68 -54
  108. package/src/cloudflare/account-api-token.ts +365 -0
  109. package/src/cloudflare/account-id.ts +0 -0
  110. package/src/cloudflare/api.ts +88 -17
  111. package/src/cloudflare/bucket.ts +493 -133
  112. package/src/cloudflare/custom-domain.ts +318 -0
  113. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  114. package/src/cloudflare/generate-asset-manifest.ts +4 -4
  115. package/src/cloudflare/index.ts +5 -1
  116. package/src/cloudflare/permission-groups.ts +137 -0
  117. package/src/cloudflare/r2-rest-state-store.ts +14 -13
  118. package/src/cloudflare/static-site-router.ts +18 -8
  119. package/src/cloudflare/static-site.ts +27 -35
  120. package/src/cloudflare/upload-asset-manifest.ts +6 -4
  121. package/src/cloudflare/worker.ts +44 -43
  122. package/src/dns/import-dns.ts +4 -4
  123. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  124. package/src/fs/copy-file.ts +115 -0
  125. package/src/fs/file-system-state-store.ts +5 -5
  126. package/src/fs/file.ts +64 -9
  127. package/src/fs/index.ts +6 -0
  128. package/src/fs/static-astro-file.ts +45 -0
  129. package/src/fs/static-css-file.ts +35 -0
  130. package/src/fs/static-html-file.ts +42 -0
  131. package/src/fs/static-json-file.ts +4 -3
  132. package/src/fs/static-text-file.ts +5 -2
  133. package/src/fs/static-typescript-file.ts +4 -3
  134. package/src/fs/static-vue-file.ts +39 -0
  135. package/src/fs/static-yaml-file.ts +13 -9
  136. package/src/internal/docs/providers.ts +281 -0
  137. package/src/secret.ts +16 -0
  138. package/src/{util/serde.ts → serde.ts} +16 -10
  139. package/src/test/bun.ts +8 -7
  140. package/src/util/sha256.ts +5 -0
  141. package/src/web/vitepress/config.ts +50 -0
  142. package/src/web/vitepress/home-page.ts +54 -42
  143. package/src/web/vitepress/index.ts +2 -0
  144. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  145. package/src/web/vitepress/vitepress.ts +34 -50
  146. package/lib/internal/getting-started.d.ts +0 -10
  147. package/lib/internal/getting-started.js +0 -94
  148. package/lib/internal/project.d.ts +0 -13
  149. package/lib/internal/project.js +0 -101
  150. package/lib/internal/providers.js +0 -148
  151. package/src/internal/getting-started.ts +0 -107
  152. package/src/internal/project.ts +0 -119
  153. package/src/internal/providers.ts +0 -209
  154. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,11 +1,19 @@
1
+ import { AwsClient } from "aws4fetch";
1
2
  import type { Context } from "../context";
2
3
  import { Resource } from "../resource";
3
- import { createCloudflareApi } from "./api";
4
+ import {
5
+ CloudflareAccountId,
6
+ CloudflareApi,
7
+ CloudflareApiError,
8
+ createCloudflareApi,
9
+ handleApiError,
10
+ type CloudflareApiOptions,
11
+ } from "./api";
4
12
 
5
13
  /**
6
14
  * Properties for creating or updating an R2 Bucket
7
15
  */
8
- export interface BucketProps {
16
+ export interface BucketProps extends CloudflareApiOptions {
9
17
  /**
10
18
  * Name of the bucket
11
19
  * Names can only contain lowercase letters (a-z), numbers (0-9), and hyphens (-)
@@ -30,6 +38,13 @@ export interface BucketProps {
30
38
  * Only for development purposes - use custom domains for production
31
39
  */
32
40
  allowPublicAccess?: boolean;
41
+
42
+ /**
43
+ * Whether to delete the bucket and its contents during resource deletion
44
+ * If set to false, the bucket will remain but the resource will be removed from state
45
+ * Default: true
46
+ */
47
+ delete?: boolean;
33
48
  }
34
49
 
35
50
  /**
@@ -49,9 +64,9 @@ export interface R2Bucket
49
64
  location: string;
50
65
 
51
66
  /**
52
- * Time at which the bucket was created (Unix timestamp in seconds)
67
+ * Time at which the bucket was created
53
68
  */
54
- creationDate: number;
69
+ creationDate: Date;
55
70
  }
56
71
 
57
72
  /**
@@ -95,174 +110,519 @@ export const R2Bucket = Resource(
95
110
  async function (
96
111
  this: Context<R2Bucket>,
97
112
  id: string,
98
- props: BucketProps,
113
+ props: BucketProps
99
114
  ): Promise<R2Bucket> {
100
- // Create Cloudflare API client with automatic account discovery
101
- const api = await createCloudflareApi();
115
+ const [api, r2Client] = await Promise.all([
116
+ createCloudflareApi(props),
117
+ createR2Client(props),
118
+ ]);
102
119
 
103
- // Resource ID defaults to bucket name if provided
104
120
  const bucketName = props.name || this.id;
105
121
 
106
122
  if (this.phase === "delete") {
107
- if (this.output?.name) {
108
- // Delete R2 bucket
109
- const headers: Record<string, string> = {};
110
- if (props.jurisdiction && props.jurisdiction !== "default") {
111
- headers["cf-r2-jurisdiction"] = props.jurisdiction;
112
- }
123
+ console.log("Deleting R2 bucket:", bucketName);
124
+ if (props.delete !== false) {
125
+ // Empty the bucket first by deleting all objects
126
+ await emptyBucket(r2Client, bucketName, props.jurisdiction);
113
127
 
114
- const deleteResponse = await api.delete(
115
- `/accounts/${api.accountId}/r2/buckets/${this.output.name}`,
116
- { headers },
117
- );
118
-
119
- if (!deleteResponse.ok && deleteResponse.status !== 404) {
120
- const errorData: any = await deleteResponse.json().catch(() => ({
121
- errors: [{ message: deleteResponse.statusText }],
122
- }));
123
- throw new Error(
124
- `Error deleting R2 bucket '${bucketName}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
125
- );
126
- }
128
+ // Delete R2 bucket
129
+ console.log("Deleting R2 bucket:", bucketName);
130
+ await deleteBucket(api, bucketName, props);
127
131
  }
128
132
 
129
133
  // Return void (a deleted bucket has no content)
130
134
  return this.destroy();
131
135
  } else {
132
- /**
133
- * Cloudflare R2 bucket API response
134
- */
135
- interface CloudflareBucketResponse {
136
- result: {
137
- name: string;
138
- location?: string;
139
- creation_date: string;
140
- };
141
- success: boolean;
142
- errors: Array<{ code: number; message: string }>;
143
- messages: string[];
136
+ if (this.phase === "create") {
137
+ console.log("Creating R2 bucket:", bucketName);
138
+ await createBucket(api, bucketName, props);
144
139
  }
145
- try {
146
- let bucketOutput: R2Bucket;
147
-
148
- if (this.phase === "update" && this.output?.name) {
149
- // Get bucket details to verify it exists
150
- const headers: Record<string, string> = {};
151
- if (props.jurisdiction && props.jurisdiction !== "default") {
152
- headers["cf-r2-jurisdiction"] = props.jurisdiction;
153
- }
154
140
 
155
- const getResponse = await api.get(
156
- `/accounts/${api.accountId}/r2/buckets/${bucketName}`,
157
- { headers },
158
- );
141
+ await updatePublicAccess(
142
+ api,
143
+ bucketName,
144
+ props.allowPublicAccess === true,
145
+ props.jurisdiction
146
+ );
159
147
 
160
- if (!getResponse.ok) {
161
- const errorData: any = await getResponse.json().catch(() => ({
162
- errors: [{ message: getResponse.statusText }],
163
- }));
164
- throw new Error(
165
- `Error getting R2 bucket '${bucketName}': ${errorData.errors?.[0]?.message || getResponse.statusText}`,
166
- );
167
- }
148
+ return this({
149
+ name: bucketName,
150
+ location: props.locationHint || "default",
151
+ creationDate: new Date(),
152
+ jurisdiction: props.jurisdiction || "default",
153
+ type: "r2_bucket",
154
+ });
155
+ }
156
+ }
157
+ );
168
158
 
169
- const result = (await getResponse.json()) as CloudflareBucketResponse;
170
- bucketOutput = this({
171
- name: result.result.name,
172
- location: result.result.location || "default",
173
- creationDate: Math.floor(
174
- new Date(result.result.creation_date).getTime() / 1000,
175
- ),
176
- jurisdiction: props.jurisdiction || "default",
177
- type: "r2_bucket",
178
- });
159
+ /**
160
+ * Configuration for R2 client to connect to Cloudflare R2
161
+ */
162
+ export interface R2ClientConfig {
163
+ accountId?: string;
164
+ accessKeyId?: string;
165
+ secretAccessKey?: string;
166
+ jurisdiction?: string;
167
+ }
179
168
 
180
- // Update public access setting if it has changed
181
- if (props.allowPublicAccess !== undefined) {
182
- await updatePublicAccess(api, bucketName, props.allowPublicAccess);
183
- }
184
- } else {
185
- // Create new R2 bucket
186
- const createPayload: any = {
187
- name: bucketName,
188
- };
189
-
190
- if (props.locationHint) {
191
- createPayload.location_hint = props.locationHint;
192
- }
169
+ type R2Client = AwsClient & { accountId: string };
193
170
 
194
- const headers: Record<string, string> = {};
195
- if (props.jurisdiction && props.jurisdiction !== "default") {
196
- headers["cf-r2-jurisdiction"] = props.jurisdiction;
197
- }
171
+ /**
172
+ * Creates an aws4fetch client configured for Cloudflare R2
173
+ *
174
+ * @see https://developers.cloudflare.com/r2/examples/aws/aws-sdk-js-v3/
175
+ */
176
+ export async function createR2Client(
177
+ config: R2ClientConfig = {}
178
+ ): Promise<R2Client> {
179
+ const accountId = await CloudflareAccountId(config);
180
+ const accessKeyId = config.accessKeyId || process.env.R2_ACCESS_KEY_ID;
181
+ const secretAccessKey =
182
+ config.secretAccessKey || process.env.R2_SECRET_ACCESS_KEY;
183
+
184
+ if (!accountId) {
185
+ throw new Error("CLOUDFLARE_ACCOUNT_ID environment variable is required");
186
+ }
187
+
188
+ if (!accessKeyId || !secretAccessKey) {
189
+ throw new Error(
190
+ "R2_ACCESS_KEY_ID and R2_SECRET_ACCESS_KEY environment variables are required"
191
+ );
192
+ }
193
+
194
+ // Create aws4fetch client with Cloudflare R2 endpoint
195
+ const client: any = new AwsClient({
196
+ accessKeyId,
197
+ secretAccessKey,
198
+ service: "s3",
199
+ region: "auto",
200
+ });
201
+ client.accountId = accountId;
202
+ return client;
203
+ }
204
+
205
+ interface CloudflareBucketResponse {
206
+ result: {
207
+ name: string;
208
+ location?: string;
209
+ creation_date: string;
210
+ };
211
+ success: boolean;
212
+ errors: Array<{ code: number; message: string }>;
213
+ messages: string[];
214
+ }
215
+
216
+ /**
217
+ * Adds jurisdiction header to the headers object if specified in props
218
+ *
219
+ * @param headers Headers object to modify
220
+ * @param props Props or jurisdiction string
221
+ * @returns Modified headers object
222
+ */
223
+ export function withJurisdiction(
224
+ headers: Record<string, string> = {},
225
+ props: BucketProps | { jurisdiction?: string } | string | undefined
226
+ ): Record<string, string> {
227
+ // Clone the headers object to avoid modifying the original
228
+ const result = { ...headers };
229
+
230
+ let jurisdiction: string | undefined;
231
+ if (typeof props === "string") {
232
+ jurisdiction = props;
233
+ } else if (props && "jurisdiction" in props) {
234
+ jurisdiction = props.jurisdiction;
235
+ }
236
+
237
+ if (jurisdiction && jurisdiction !== "default") {
238
+ result["cf-r2-jurisdiction"] = jurisdiction;
239
+ }
240
+
241
+ return result;
242
+ }
243
+
244
+ /**
245
+ * Create a new bucket
246
+ */
247
+ export async function createBucket(
248
+ api: CloudflareApi,
249
+ bucketName: string,
250
+ props: BucketProps
251
+ ): Promise<CloudflareBucketResponse> {
252
+ // Create new R2 bucket
253
+ const createPayload: any = {
254
+ name: bucketName,
255
+ };
256
+
257
+ if (props.locationHint) {
258
+ createPayload.location_hint = props.locationHint;
259
+ }
260
+
261
+ const headers = withJurisdiction({}, props);
262
+
263
+ const createResponse = await api.post(
264
+ `/accounts/${api.accountId}/r2/buckets`,
265
+ createPayload,
266
+ { headers }
267
+ );
268
+
269
+ if (!createResponse.ok) {
270
+ const errorData: any = await createResponse.json().catch(() => ({
271
+ errors: [{ message: createResponse.statusText }],
272
+ }));
273
+ throw new CloudflareApiError(
274
+ `Error creating R2 bucket '${bucketName}': ${errorData.errors?.[0]?.message || createResponse.statusText}`,
275
+ createResponse
276
+ );
277
+ }
278
+
279
+ return (await createResponse.json()) as CloudflareBucketResponse;
280
+ }
281
+
282
+ /**
283
+ * Delete a bucket
284
+ */
285
+ export async function deleteBucket(
286
+ api: CloudflareApi,
287
+ bucketName: string,
288
+ props: BucketProps
289
+ ): Promise<void> {
290
+ // Delete R2 bucket
291
+ const headers = withJurisdiction({}, props);
292
+
293
+ const deleteResponse = await api.delete(
294
+ `/accounts/${api.accountId}/r2/buckets/${bucketName}`,
295
+ { headers }
296
+ );
297
+
298
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
299
+ const errorData: any = await deleteResponse.json().catch(() => ({
300
+ errors: [{ message: deleteResponse.statusText }],
301
+ }));
302
+ throw new CloudflareApiError(
303
+ `Error deleting R2 bucket '${bucketName}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
304
+ deleteResponse
305
+ );
306
+ }
307
+ }
308
+
309
+ /**
310
+ * List objects in an R2 bucket
311
+ *
312
+ * @param r2 R2Client instance
313
+ * @param bucketName Name of the bucket
314
+ * @param continuationToken Optional token for pagination
315
+ * @param jurisdiction Optional jurisdiction for the bucket
316
+ * @returns Object containing the list of objects and the next continuation token
317
+ */
318
+ export async function listObjects(
319
+ r2: R2Client,
320
+ bucketName: string,
321
+ continuationToken?: string,
322
+ jurisdiction?: string
323
+ ): Promise<{ objects: { Key: string }[]; continuationToken?: string }> {
324
+ // List objects in the bucket
325
+ const url = new URL(
326
+ `https://${r2.accountId}.r2.cloudflarestorage.com/${bucketName}`
327
+ );
328
+ if (continuationToken) {
329
+ url.searchParams.set("continuation-token", continuationToken);
330
+ }
331
+ url.searchParams.set("list-type", "2");
332
+
333
+ const headers = withJurisdiction({}, jurisdiction);
334
+
335
+ const listResponse = await r2.fetch(url.toString(), { headers });
336
+ if (!listResponse.ok) {
337
+ throw new CloudflareApiError(
338
+ `Failed to list objects: ${listResponse.statusText}`,
339
+ listResponse
340
+ );
341
+ }
342
+
343
+ const responseText = await listResponse.text();
344
+
345
+ // Extract objects from XML response using regex
346
+ const keyRegex = /<Key>([^<]+)<\/Key>/g;
347
+ const objects: { Key: string }[] = [];
348
+ let match;
349
+ while ((match = keyRegex.exec(responseText)) !== null) {
350
+ objects.push({ Key: match[1] });
351
+ }
198
352
 
199
- const createResponse = await api.post(
200
- `/accounts/${api.accountId}/r2/buckets`,
201
- createPayload,
202
- { headers },
353
+ // Get continuation token if present using regex
354
+ const tokenMatch =
355
+ /<NextContinuationToken>([^<]+)<\/NextContinuationToken>/.exec(
356
+ responseText
357
+ );
358
+ const nextContinuationToken = tokenMatch ? tokenMatch[1] : undefined;
359
+
360
+ return { objects, continuationToken: nextContinuationToken };
361
+ }
362
+
363
+ /**
364
+ * Helper function to empty a bucket by deleting all objects
365
+ */
366
+ export async function emptyBucket(
367
+ r2: R2Client,
368
+ bucketName: string,
369
+ jurisdiction?: string
370
+ ): Promise<void> {
371
+ let continuationToken: string | undefined;
372
+ let totalDeleted = 0;
373
+
374
+ try {
375
+ do {
376
+ // List objects in the bucket
377
+ const { objects, continuationToken: nextToken } = await listObjects(
378
+ r2,
379
+ bucketName,
380
+ continuationToken,
381
+ jurisdiction
382
+ );
383
+
384
+ continuationToken = nextToken;
385
+
386
+ console.log(`Found ${objects.length} objects in bucket ${bucketName}`);
387
+
388
+ // Delete objects in batches
389
+ if (objects.length > 0) {
390
+ // Process delete in batches of 1000 (S3 limit)
391
+ for (let i = 0; i < objects.length; i += 1000) {
392
+ const batch = objects.slice(i, i + 1000);
393
+
394
+ // Create DeleteObjects request XML
395
+ const deleteXml = `
396
+ <Delete>
397
+ ${batch.map((obj) => `<Object><Key>${obj.Key}</Key></Object>`).join("")}
398
+ </Delete>
399
+ `;
400
+
401
+ const deleteUrl = new URL(
402
+ `https://${r2.accountId}.r2.cloudflarestorage.com/${bucketName}?delete`
203
403
  );
204
404
 
205
- if (!createResponse.ok) {
206
- const errorData: any = await createResponse.json().catch(() => ({
207
- errors: [{ message: createResponse.statusText }],
208
- }));
209
- throw new Error(
210
- `Error creating R2 bucket '${bucketName}': ${errorData.errors?.[0]?.message || createResponse.statusText}`,
211
- );
212
- }
405
+ console.log(
406
+ `Deleting ${batch.length} objects from bucket ${bucketName}`
407
+ );
408
+
409
+ const headers = withJurisdiction(
410
+ { "Content-Type": "application/xml" },
411
+ jurisdiction
412
+ );
213
413
 
214
- const result =
215
- (await createResponse.json()) as CloudflareBucketResponse;
216
- bucketOutput = this({
217
- name: result.result.name,
218
- location: result.result.location || "default",
219
- creationDate: Math.floor(
220
- new Date(result.result.creation_date).getTime() / 1000,
221
- ),
222
- jurisdiction: props.jurisdiction || "default",
223
- type: "r2_bucket",
414
+ const deleteResponse = await r2.fetch(deleteUrl.toString(), {
415
+ method: "POST",
416
+ body: deleteXml,
417
+ headers,
224
418
  });
225
419
 
226
- // Set public access if requested
227
- if (props.allowPublicAccess) {
228
- await updatePublicAccess(api, bucketName, true);
420
+ if (!deleteResponse.ok) {
421
+ throw new CloudflareApiError(
422
+ `Failed to delete objects: ${deleteResponse.statusText}`,
423
+ deleteResponse
424
+ );
229
425
  }
230
- }
231
426
 
232
- return bucketOutput;
233
- } catch (error) {
234
- console.error("Error creating/updating R2 bucket:", error);
235
- throw error;
427
+ totalDeleted += batch.length;
428
+ }
236
429
  }
430
+ } while (continuationToken);
431
+
432
+ console.log(
433
+ `Successfully emptied bucket ${bucketName}, deleted ${totalDeleted} objects total`
434
+ );
435
+ } catch (error) {
436
+ if (error instanceof CloudflareApiError && error.status === 404) {
437
+ // the bucket was not found
438
+ return;
237
439
  }
238
- },
239
- );
440
+ console.error(`Failed to empty bucket ${bucketName}:`, error);
441
+ throw error;
442
+ }
443
+ }
240
444
 
241
445
  /**
242
- * Helper function to update the public access setting for a bucket
446
+ * Update public access setting for a bucket
447
+ *
448
+ * This operation is not available through the S3 API for R2,
449
+ * so we still use the Cloudflare API directly.
243
450
  */
244
- async function updatePublicAccess(
245
- api: any,
451
+ export async function updatePublicAccess(
452
+ api: CloudflareApi,
246
453
  bucketName: string,
247
454
  allowPublicAccess: boolean,
455
+ jurisdiction?: string
456
+ ): Promise<void> {
457
+ console.log(
458
+ `/accounts/${api.accountId}/r2/buckets/${bucketName}/domains/managed`
459
+ );
460
+
461
+ const headers = withJurisdiction({}, jurisdiction);
462
+
463
+ const response = await api.put(
464
+ `/accounts/${api.accountId}/r2/buckets/${bucketName}/domains/managed`,
465
+ {
466
+ enabled: allowPublicAccess,
467
+ },
468
+ { headers }
469
+ );
470
+
471
+ if (!response.ok) {
472
+ await handleApiError(
473
+ response,
474
+ "updating public access for",
475
+ "R2 bucket",
476
+ bucketName
477
+ );
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Set CORS configuration for a bucket using aws4fetch
483
+ */
484
+ export async function setCorsConfiguration(
485
+ r2: R2Client,
486
+ bucketName: string,
487
+ allowedOrigins: string[] = ["*"],
488
+ allowedMethods: string[] = ["GET", "HEAD", "PUT", "POST", "DELETE"],
489
+ allowedHeaders: string[] = ["*"],
490
+ maxAgeSeconds: number = 3600,
491
+ jurisdiction?: string
248
492
  ): Promise<void> {
249
493
  try {
250
- const response = await api.put(
251
- `/accounts/${api.accountId}/r2/buckets/${bucketName}/domains/managed`,
252
- {
253
- enabled: allowPublicAccess,
254
- },
494
+ // Construct CORS XML configuration
495
+ const corsXml = `
496
+ <CORSConfiguration>
497
+ <CORSRule>
498
+ ${allowedOrigins.map((origin) => `<AllowedOrigin>${origin}</AllowedOrigin>`).join("")}
499
+ ${allowedMethods.map((method) => `<AllowedMethod>${method}</AllowedMethod>`).join("")}
500
+ ${allowedHeaders.map((header) => `<AllowedHeader>${header}</AllowedHeader>`).join("")}
501
+ <ExposeHeader>ETag</ExposeHeader>
502
+ <MaxAgeSeconds>${maxAgeSeconds}</MaxAgeSeconds>
503
+ </CORSRule>
504
+ </CORSConfiguration>
505
+ `;
506
+
507
+ const url = new URL(
508
+ `https://${r2.accountId}.r2.cloudflarestorage.com/${bucketName}?cors`
255
509
  );
256
510
 
511
+ const headers = withJurisdiction(
512
+ { "Content-Type": "application/xml" },
513
+ jurisdiction
514
+ );
515
+
516
+ const response = await r2.fetch(url.toString(), {
517
+ method: "PUT",
518
+ body: corsXml,
519
+ headers,
520
+ });
521
+
257
522
  if (!response.ok) {
258
- const errorData: any = await response.json().catch(() => ({
259
- errors: [{ message: response.statusText }],
260
- }));
261
- throw new Error(
262
- `Error updating R2 bucket public access: ${errorData.errors?.[0]?.message || response.statusText}`,
523
+ throw new CloudflareApiError(
524
+ `Failed to set CORS configuration: ${response.statusText}`,
525
+ response
263
526
  );
264
527
  }
528
+
529
+ console.log(`Successfully set CORS configuration for bucket ${bucketName}`);
265
530
  } catch (error) {
266
- console.warn("Could not set public access:", error);
531
+ console.error(
532
+ `Failed to set CORS configuration for bucket ${bucketName}:`,
533
+ error
534
+ );
535
+ throw error;
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Information about an R2 bucket returned by list operations
541
+ */
542
+ export interface R2BucketInfo {
543
+ /**
544
+ * Name of the bucket
545
+ */
546
+ Name: string;
547
+
548
+ /**
549
+ * Creation date of the bucket
550
+ */
551
+ CreationDate: Date;
552
+ }
553
+
554
+ /**
555
+ * List all R2 buckets in an account
556
+ *
557
+ * @param api CloudflareApi instance
558
+ * @param options Optional listing options
559
+ * @returns Array of bucket information
560
+ */
561
+ export async function listBuckets(
562
+ api: CloudflareApi,
563
+ options: {
564
+ nameContains?: string;
565
+ perPage?: number;
566
+ cursor?: string;
567
+ direction?: "asc" | "desc";
568
+ jurisdiction?: string;
569
+ } = {}
570
+ ): Promise<R2BucketInfo[]> {
571
+ // Build query parameters
572
+ const params = new URLSearchParams();
573
+
574
+ if (options.nameContains) {
575
+ params.append("name_contains", options.nameContains);
576
+ }
577
+
578
+ if (options.perPage) {
579
+ params.append("per_page", options.perPage.toString());
580
+ }
581
+
582
+ if (options.cursor) {
583
+ params.append("cursor", options.cursor);
584
+ }
585
+
586
+ if (options.direction) {
587
+ params.append("direction", options.direction);
267
588
  }
589
+
590
+ // Build URL with query parameters
591
+ const path = `/accounts/${api.accountId}/r2/buckets${params.toString() ? "?" + params.toString() : ""}`;
592
+
593
+ // Set jurisdiction header if provided
594
+ const headers = withJurisdiction({}, options.jurisdiction);
595
+
596
+ // Make the API request
597
+ const response = await api.get(path, { headers });
598
+
599
+ if (!response.ok) {
600
+ throw new CloudflareApiError(
601
+ `Failed to list buckets: ${response.statusText}`,
602
+ response
603
+ );
604
+ }
605
+
606
+ const data = (await response.json()) as {
607
+ success: boolean;
608
+ errors?: Array<{ code: number; message: string }>;
609
+ result?: {
610
+ buckets: Array<{
611
+ name: string;
612
+ creation_date: string;
613
+ location?: string;
614
+ }>;
615
+ };
616
+ };
617
+
618
+ if (!data.success) {
619
+ const errorMessage = data.errors?.[0]?.message || "Unknown error";
620
+ throw new Error(`Failed to list buckets: ${errorMessage}`);
621
+ }
622
+
623
+ // Transform API response to R2BucketInfo objects
624
+ return (data.result?.buckets || []).map((bucket) => ({
625
+ Name: bucket.name,
626
+ CreationDate: new Date(bucket.creation_date),
627
+ }));
268
628
  }