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.
- package/README.md +8 -3
- package/lib/ai/astro-file.js +7 -24
- package/lib/ai/client.d.ts +7 -0
- package/lib/ai/client.js +45 -0
- package/lib/ai/css-file.js +7 -24
- package/lib/ai/data.d.ts +26 -21
- package/lib/ai/data.js +33 -22
- package/lib/ai/document.d.ts +76 -5
- package/lib/ai/document.js +112 -56
- package/lib/ai/html-file.js +7 -24
- package/lib/ai/json-file.js +11 -32
- package/lib/ai/typescript-file.d.ts +1 -2
- package/lib/ai/typescript-file.js +8 -39
- package/lib/ai/vue-file.js +7 -24
- package/lib/ai/yaml-file.js +12 -48
- package/lib/alchemy.d.ts +5 -0
- package/lib/alchemy.js +61 -3
- package/lib/apply.js +18 -11
- 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/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/generate-asset-manifest.js +1 -1
- package/lib/cloudflare/index.d.ts +5 -1
- package/lib/cloudflare/index.js +5 -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 +3 -2
- package/lib/cloudflare/static-site-router.js +16 -7
- package/lib/cloudflare/static-site.d.ts +17 -18
- package/lib/cloudflare/static-site.js +19 -7
- package/lib/cloudflare/upload-asset-manifest.js +4 -2
- package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
- package/lib/{util/encrypt.js → encrypt.js} +1 -1
- package/lib/fs/copy-file.d.ts +54 -0
- package/lib/fs/copy-file.js +63 -0
- package/lib/fs/file-system-state-store.js +1 -1
- package/lib/fs/file.d.ts +31 -0
- package/lib/fs/file.js +30 -5
- package/lib/fs/index.d.ts +6 -0
- package/lib/fs/index.js +6 -0
- package/lib/fs/static-astro-file.d.ts +34 -0
- package/lib/fs/static-astro-file.js +39 -0
- package/lib/fs/static-css-file.d.ts +24 -0
- package/lib/fs/static-css-file.js +29 -0
- package/lib/fs/static-html-file.d.ts +31 -0
- package/lib/fs/static-html-file.js +36 -0
- package/lib/fs/static-json-file.d.ts +2 -2
- package/lib/fs/static-json-file.js +3 -2
- package/lib/fs/static-text-file.d.ts +1 -1
- package/lib/fs/static-text-file.js +3 -2
- package/lib/fs/static-typescript-file.d.ts +2 -2
- package/lib/fs/static-typescript-file.js +3 -2
- package/lib/fs/static-vue-file.d.ts +28 -0
- package/lib/fs/static-vue-file.js +33 -0
- package/lib/fs/static-yaml-file.d.ts +11 -8
- package/lib/fs/static-yaml-file.js +3 -2
- package/lib/internal/{providers.d.ts → docs/providers.d.ts} +14 -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 +11 -0
- package/lib/web/vitepress/config.js +32 -0
- package/lib/web/vitepress/home-page.d.ts +5 -3
- package/lib/web/vitepress/home-page.js +48 -35
- package/lib/web/vitepress/index.d.ts +2 -0
- package/lib/web/vitepress/index.js +2 -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.d.ts +5 -4
- package/lib/web/vitepress/vitepress.js +20 -36
- package/package.json +8 -4
- package/src/ai/astro-file.ts +11 -30
- package/src/ai/client.ts +59 -0
- package/src/ai/css-file.ts +11 -30
- package/src/ai/data.ts +50 -25
- package/src/ai/document.ts +171 -66
- package/src/ai/html-file.ts +11 -30
- package/src/ai/json-file.ts +13 -37
- package/src/ai/typescript-file.ts +14 -48
- package/src/ai/vue-file.ts +11 -30
- package/src/ai/yaml-file.ts +13 -53
- package/src/alchemy.ts +74 -9
- package/src/apply.ts +27 -26
- 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/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/generate-asset-manifest.ts +4 -4
- package/src/cloudflare/index.ts +5 -1
- package/src/cloudflare/permission-groups.ts +137 -0
- package/src/cloudflare/r2-rest-state-store.ts +14 -13
- package/src/cloudflare/static-site-router.ts +18 -8
- package/src/cloudflare/static-site.ts +27 -35
- package/src/cloudflare/upload-asset-manifest.ts +6 -4
- package/src/cloudflare/worker.ts +44 -43
- package/src/dns/import-dns.ts +4 -4
- package/src/{util/encrypt.ts → encrypt.ts} +7 -10
- package/src/fs/copy-file.ts +115 -0
- package/src/fs/file-system-state-store.ts +5 -5
- package/src/fs/file.ts +64 -9
- package/src/fs/index.ts +6 -0
- package/src/fs/static-astro-file.ts +45 -0
- package/src/fs/static-css-file.ts +35 -0
- package/src/fs/static-html-file.ts +42 -0
- package/src/fs/static-json-file.ts +4 -3
- package/src/fs/static-text-file.ts +5 -2
- package/src/fs/static-typescript-file.ts +4 -3
- package/src/fs/static-vue-file.ts +39 -0
- package/src/fs/static-yaml-file.ts +13 -9
- 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 +50 -0
- package/src/web/vitepress/home-page.ts +54 -42
- package/src/web/vitepress/index.ts +2 -0
- package/src/web/vitepress/process-front-matter-files.ts +98 -0
- package/src/web/vitepress/vitepress.ts +34 -50
- package/lib/internal/getting-started.d.ts +0 -10
- package/lib/internal/getting-started.js +0 -94
- package/lib/internal/project.d.ts +0 -13
- package/lib/internal/project.js +0 -101
- package/lib/internal/providers.js +0 -148
- package/src/internal/getting-started.ts +0 -107
- package/src/internal/project.ts +0 -119
- package/src/internal/providers.ts +0 -209
- /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
package/src/cloudflare/bucket.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
import { AwsClient } from "aws4fetch";
|
|
1
2
|
import type { Context } from "../context";
|
|
2
3
|
import { Resource } from "../resource";
|
|
3
|
-
import {
|
|
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
|
|
67
|
+
* Time at which the bucket was created
|
|
53
68
|
*/
|
|
54
|
-
creationDate:
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
141
|
+
await updatePublicAccess(
|
|
142
|
+
api,
|
|
143
|
+
bucketName,
|
|
144
|
+
props.allowPublicAccess === true,
|
|
145
|
+
props.jurisdiction
|
|
146
|
+
);
|
|
159
147
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
420
|
+
if (!deleteResponse.ok) {
|
|
421
|
+
throw new CloudflareApiError(
|
|
422
|
+
`Failed to delete objects: ${deleteResponse.statusText}`,
|
|
423
|
+
deleteResponse
|
|
424
|
+
);
|
|
229
425
|
}
|
|
230
|
-
}
|
|
231
426
|
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
259
|
-
|
|
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.
|
|
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
|
}
|