alchemy 0.4.0 → 0.5.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 (53) hide show
  1. package/README.md +3 -463
  2. package/lib/ai/data.d.ts +5 -0
  3. package/lib/ai/data.js +3 -0
  4. package/lib/alchemy.d.ts +3 -3
  5. package/lib/alchemy.js +82 -85
  6. package/lib/cloudflare/assets.d.ts +74 -0
  7. package/lib/cloudflare/assets.js +75 -0
  8. package/lib/cloudflare/bindings.d.ts +6 -1
  9. package/lib/cloudflare/bindings.js +6 -0
  10. package/lib/cloudflare/bound.d.ts +2 -1
  11. package/lib/cloudflare/custom-domain.d.ts +1 -15
  12. package/lib/cloudflare/custom-domain.js +1 -15
  13. package/lib/cloudflare/index.d.ts +1 -1
  14. package/lib/cloudflare/index.js +1 -1
  15. package/lib/cloudflare/r2-rest-state-store.js +52 -22
  16. package/lib/cloudflare/worker.d.ts +18 -0
  17. package/lib/cloudflare/worker.js +153 -4
  18. package/lib/internal/docs/providers.js +2 -1
  19. package/lib/os/exec.d.ts +103 -0
  20. package/lib/os/exec.js +103 -0
  21. package/lib/os/index.d.ts +1 -0
  22. package/lib/os/index.js +1 -0
  23. package/lib/secret.d.ts +2 -2
  24. package/lib/secret.js +2 -2
  25. package/lib/web/vitepress/vitepress.js +1 -1
  26. package/package.json +2 -1
  27. package/src/ai/data.ts +10 -0
  28. package/src/alchemy.ts +97 -100
  29. package/src/cloudflare/assets.ts +158 -0
  30. package/src/cloudflare/bindings.ts +11 -2
  31. package/src/cloudflare/bound.ts +4 -1
  32. package/src/cloudflare/custom-domain.ts +1 -15
  33. package/src/cloudflare/index.ts +1 -1
  34. package/src/cloudflare/r2-rest-state-store.ts +78 -34
  35. package/src/cloudflare/worker.ts +261 -6
  36. package/src/internal/docs/providers.ts +2 -1
  37. package/src/os/exec.ts +190 -0
  38. package/src/os/index.ts +1 -0
  39. package/src/secret.ts +2 -2
  40. package/src/web/vitepress/vitepress.ts +1 -1
  41. package/lib/cloudflare/generate-asset-manifest.d.ts +0 -2
  42. package/lib/cloudflare/generate-asset-manifest.js +0 -68
  43. package/lib/cloudflare/static-site-router.d.ts +0 -18
  44. package/lib/cloudflare/static-site-router.js +0 -115
  45. package/lib/cloudflare/static-site.d.ts +0 -211
  46. package/lib/cloudflare/static-site.js +0 -213
  47. package/lib/cloudflare/upload-asset-manifest.d.ts +0 -11
  48. package/lib/cloudflare/upload-asset-manifest.js +0 -56
  49. package/src/cloudflare/generate-asset-manifest.ts +0 -93
  50. package/src/cloudflare/static-site-router.ts +0 -157
  51. package/src/cloudflare/static-site.ts +0 -416
  52. package/src/cloudflare/upload-asset-manifest.ts +0 -85
  53. package/src/web/vitepress/index.md +0 -75
package/src/alchemy.ts CHANGED
@@ -22,7 +22,7 @@ export const alchemy: Alchemy = _alchemy as any;
22
22
  *
23
23
  * @example
24
24
  * // Create an application scope with stage and secret handling
25
- * const app = alchemy("github:alchemy", {
25
+ * const app = await alchemy("github:alchemy", {
26
26
  * stage: "prod",
27
27
  * phase: "up",
28
28
  * // Required for encrypting/decrypting secrets
@@ -57,13 +57,13 @@ export interface Alchemy {
57
57
  * Used to create and manage resources with proper secret handling.
58
58
  *
59
59
  * @example
60
- * const app = alchemy("my-app", {
60
+ * const app = await alchemy("my-app", {
61
61
  * stage: "prod",
62
62
  * // Required for encrypting/decrypting secrets
63
63
  * password: process.env.SECRET_PASSPHRASE
64
64
  * });
65
65
  */
66
- (...parameters: Parameters<typeof scope>): ReturnType<typeof scope>;
66
+ (...parameters: Parameters<typeof scope>): Promise<Scope>;
67
67
  /**
68
68
  * Template literal tag that supports file interpolation for documentation.
69
69
  * Automatically formats the content and appends file contents as code blocks.
@@ -88,11 +88,11 @@ export interface Alchemy {
88
88
  * Implementation of the alchemy function that handles both application scoping
89
89
  * and template string interpolation.
90
90
  */
91
- function _alchemy(
91
+ async function _alchemy(
92
92
  ...args:
93
93
  | [template: TemplateStringsArray, ...values: any[]]
94
94
  | [appName: string, options?: Omit<AlchemyOptions, "appName">]
95
- ): any {
95
+ ): Promise<Scope | string | never> {
96
96
  if (typeof args[0] === "string") {
97
97
  const [appName, options] = args as [string, AlchemyOptions?];
98
98
  const root = scope(undefined, {
@@ -101,9 +101,8 @@ function _alchemy(
101
101
  stage: options?.stage,
102
102
  });
103
103
  if (options?.phase === "destroy") {
104
- return destroy(root).finally(() => {
105
- process.exit(0);
106
- });
104
+ await destroy(root);
105
+ return process.exit(0);
107
106
  }
108
107
  return root;
109
108
  } else {
@@ -114,106 +113,104 @@ function _alchemy(
114
113
  : 0;
115
114
  const indent = " ".repeat(leadingSpaces);
116
115
 
117
- return (async () => {
118
- const { isFileCollection, isFileRef } = await import("./fs");
116
+ const { isFileCollection, isFileRef } = await import("./fs");
119
117
 
120
- const appendices: Record<string, string> = {};
118
+ const appendices: Record<string, string> = {};
121
119
 
122
- const stringValues = await Promise.all(
123
- values.map(async function resolve(value): Promise<string> {
124
- if (typeof value === "string") {
125
- return indent + value;
126
- } else if (value === null) {
127
- return "null";
128
- } else if (value === undefined) {
129
- return "undefined";
130
- } else if (
131
- typeof value === "number" ||
132
- typeof value === "boolean" ||
133
- typeof value === "bigint"
134
- ) {
135
- return value.toString();
136
- } else if (value instanceof Promise) {
137
- return resolve(await value);
138
- } else if (isFileRef(value)) {
139
- if (!(value.path in appendices)) {
140
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
141
- }
120
+ const stringValues = await Promise.all(
121
+ values.map(async function resolve(value): Promise<string> {
122
+ if (typeof value === "string") {
123
+ return indent + value;
124
+ } else if (value === null) {
125
+ return "null";
126
+ } else if (value === undefined) {
127
+ return "undefined";
128
+ } else if (
129
+ typeof value === "number" ||
130
+ typeof value === "boolean" ||
131
+ typeof value === "bigint"
132
+ ) {
133
+ return value.toString();
134
+ } else if (value instanceof Promise) {
135
+ return resolve(await value);
136
+ } else if (isFileRef(value)) {
137
+ if (!(value.path in appendices)) {
138
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
139
+ }
140
+ return `[${path.basename(value.path)}](${value.path})`;
141
+ } else if (isFileCollection(value)) {
142
+ return Object.entries(value.files)
143
+ .map(([filePath, content]) => {
144
+ appendices[filePath] = content;
145
+ return `[${path.basename(filePath)}](${filePath})`;
146
+ })
147
+ .join("\n\n");
148
+ } else if (Array.isArray(value)) {
149
+ return (
150
+ await Promise.all(
151
+ value.map(async (value, i) => `${i}. ${await resolve(value)}`)
152
+ )
153
+ ).join("\n");
154
+ } else if (
155
+ typeof value === "object" &&
156
+ typeof value.path === "string"
157
+ ) {
158
+ if (typeof value.content === "string") {
159
+ appendices[value.path] = value.content;
142
160
  return `[${path.basename(value.path)}](${value.path})`;
143
- } else if (isFileCollection(value)) {
144
- return Object.entries(value.files)
145
- .map(([filePath, content]) => {
146
- appendices[filePath] = content;
147
- return `[${path.basename(filePath)}](${filePath})`;
148
- })
149
- .join("\n\n");
150
- } else if (Array.isArray(value)) {
151
- return (
152
- await Promise.all(
153
- value.map(async (value, i) => `${i}. ${await resolve(value)}`)
154
- )
155
- ).join("\n");
156
- } else if (
157
- typeof value === "object" &&
158
- typeof value.path === "string"
159
- ) {
160
- if (typeof value.content === "string") {
161
- appendices[value.path] = value.content;
162
- return `[${path.basename(value.path)}](${value.path})`;
163
- } else {
164
- appendices[value.path] = await fs.readFile(value.path, "utf-8");
165
- return `[${path.basename(value.path)}](${value.path})`;
166
- }
167
- } else if (typeof value === "object") {
168
- return (
169
- await Promise.all(
170
- Object.entries(value).map(async ([key, value]) => {
171
- return `* ${key}: ${await resolve(value)}`;
172
- })
173
- )
174
- ).join("\n");
175
161
  } else {
176
- // TODO: support other types
177
- console.log(value);
178
- throw new Error(`Unsupported value type: ${value}`);
162
+ appendices[value.path] = await fs.readFile(value.path, "utf-8");
163
+ return `[${path.basename(value.path)}](${value.path})`;
179
164
  }
180
- })
181
- );
182
-
183
- // Construct the string template by joining template parts with interpolated values
184
- const lines = template
185
- .map((part) =>
186
- part
187
- .split("\n")
188
- .map((line) =>
189
- line.startsWith(indent) ? line.slice(indent.length) : line
165
+ } else if (typeof value === "object") {
166
+ return (
167
+ await Promise.all(
168
+ Object.entries(value).map(async ([key, value]) => {
169
+ return `* ${key}: ${await resolve(value)}`;
170
+ })
190
171
  )
191
- .join("\n")
192
- )
193
- .flatMap((part, i) =>
194
- i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
195
- )
196
- .join("")
197
- .split("\n");
172
+ ).join("\n");
173
+ } else {
174
+ // TODO: support other types
175
+ console.log(value);
176
+ throw new Error(`Unsupported value type: ${value}`);
177
+ }
178
+ })
179
+ );
198
180
 
199
- // Collect and sort appendices by file path
200
- return [
201
- // format the user prompt and trim the first line if it's empty
202
- lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
203
- ? lines.slice(1).join("\n")
204
- : lines.join("\n"),
181
+ // Construct the string template by joining template parts with interpolated values
182
+ const lines = template
183
+ .map((part) =>
184
+ part
185
+ .split("\n")
186
+ .map((line) =>
187
+ line.startsWith(indent) ? line.slice(indent.length) : line
188
+ )
189
+ .join("\n")
190
+ )
191
+ .flatMap((part, i) =>
192
+ i < stringValues.length ? [part, stringValues[i] ?? ""] : [part]
193
+ )
194
+ .join("")
195
+ .split("\n");
205
196
 
206
- // sort appendices by path and include at the end of the prompt
207
- Object.entries(appendices)
208
- .sort(([a], [b]) => a.localeCompare(b))
209
- .map(([filePath, content]) => {
210
- const extension = path.extname(filePath).slice(1);
211
- const codeTag = extension ? extension : "";
212
- return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
213
- })
214
- .join("\n\n"),
215
- ].join("\n");
216
- })();
197
+ // Collect and sort appendices by file path
198
+ return [
199
+ // format the user prompt and trim the first line if it's empty
200
+ lines.length > 1 && lines[0].replaceAll(" ", "").length === 0
201
+ ? lines.slice(1).join("\n")
202
+ : lines.join("\n"),
203
+
204
+ // sort appendices by path and include at the end of the prompt
205
+ Object.entries(appendices)
206
+ .sort(([a], [b]) => a.localeCompare(b))
207
+ .map(([filePath, content]) => {
208
+ const extension = path.extname(filePath).slice(1);
209
+ const codeTag = extension ? extension : "";
210
+ return `// ${filePath}\n\`\`\`${codeTag}\n${content}\n\`\`\``;
211
+ })
212
+ .join("\n\n"),
213
+ ].join("\n");
217
214
  }
218
215
  }
219
216
  _alchemy.destroy = destroy;
@@ -0,0 +1,158 @@
1
+ import * as fs from "fs/promises";
2
+ import * as path from "path";
3
+ import type { Context } from "../context";
4
+ import { Resource } from "../resource";
5
+ import { getContentType } from "../util/content-type";
6
+
7
+ /**
8
+ * Properties for creating or updating Assets
9
+ */
10
+ export interface AssetsProps {
11
+ /**
12
+ * Path to a directory containing static assets to be uploaded
13
+ * These files will be served by Cloudflare's Workers runtime
14
+ */
15
+ path: string;
16
+ }
17
+
18
+ /**
19
+ * Output returned after Assets creation/update
20
+ */
21
+ export interface Assets extends Resource<"cloudflare::Asset">, AssetsProps {
22
+ /**
23
+ * The type of binding
24
+ */
25
+ type: "assets";
26
+
27
+ /**
28
+ * The ID of the assets bundle
29
+ */
30
+ id: string;
31
+
32
+ /**
33
+ * Asset files that were found
34
+ */
35
+ files: AssetFile[];
36
+
37
+ /**
38
+ * Time at which the assets were created
39
+ */
40
+ createdAt: number;
41
+
42
+ /**
43
+ * Time at which the assets were last updated
44
+ */
45
+ updatedAt: number;
46
+ }
47
+
48
+ /**
49
+ * Represents a single asset file
50
+ */
51
+ export interface AssetFile {
52
+ /**
53
+ * Path relative to the assets directory
54
+ */
55
+ path: string;
56
+
57
+ /**
58
+ * Full filesystem path to the file
59
+ */
60
+ filePath: string;
61
+
62
+ /**
63
+ * Content type of the file
64
+ */
65
+ contentType: string;
66
+ }
67
+
68
+ /**
69
+ * Cloudflare Assets represent a collection of static files that can be uploaded and served
70
+ * by Cloudflare Workers.
71
+ *
72
+ * @example
73
+ * // Create a basic assets bundle from a local directory
74
+ * const staticAssets = await Assets("static", {
75
+ * path: "./src/assets"
76
+ * });
77
+ *
78
+ * // Use these assets with a worker
79
+ * const worker = await Worker("frontend", {
80
+ * name: "frontend-worker",
81
+ * entrypoint: "./src/worker.ts",
82
+ * bindings: {
83
+ * ASSETS: staticAssets
84
+ * }
85
+ * });
86
+ */
87
+ export const Assets = Resource(
88
+ "cloudflare::Asset",
89
+ {
90
+ alwaysUpdate: true,
91
+ },
92
+ async function (
93
+ this: Context<Assets>,
94
+ id: string,
95
+ props: AssetsProps
96
+ ): Promise<Assets> {
97
+ if (this.phase === "delete") {
98
+ return this.destroy();
99
+ }
100
+
101
+ try {
102
+ // Check if the assets directory exists
103
+ const stats = await fs.stat(props.path);
104
+ if (!stats.isDirectory()) {
105
+ throw new Error(`Assets path ${props.path} is not a directory`);
106
+ }
107
+ } catch (error) {
108
+ throw new Error(
109
+ `Assets directory ${props.path} does not exist or is not accessible`
110
+ );
111
+ }
112
+
113
+ // Recursively get all files in the assets directory
114
+ const filesList = await getFilesRecursively(props.path);
115
+
116
+ // Create asset file objects
117
+ const files: AssetFile[] = filesList.map((filePath) => {
118
+ const relativePath = path.relative(props.path, filePath);
119
+ const normalizedPath = relativePath.split(path.sep).join("/"); // Ensure forward slashes for URLs
120
+
121
+ return {
122
+ path: normalizedPath,
123
+ filePath,
124
+ contentType: getContentType(filePath),
125
+ };
126
+ });
127
+
128
+ // Get current timestamp
129
+ const now = Date.now();
130
+
131
+ // Construct the output
132
+ return this({
133
+ id,
134
+ type: "assets",
135
+ path: props.path,
136
+ files,
137
+ createdAt: this.output?.createdAt || now,
138
+ updatedAt: now,
139
+ });
140
+ }
141
+ );
142
+
143
+ // Helper functions for file operations
144
+ async function getFilesRecursively(dir: string): Promise<string[]> {
145
+ const files = await fs.readdir(dir, { withFileTypes: true });
146
+
147
+ const allFiles = await Promise.all(
148
+ files.map(async (file) => {
149
+ const path = `${dir}/${file.name}`;
150
+ if (file.isDirectory()) {
151
+ return getFilesRecursively(path);
152
+ }
153
+ return path;
154
+ })
155
+ );
156
+
157
+ return allFiles.flat();
158
+ }
@@ -4,6 +4,7 @@
4
4
  * https://developers.cloudflare.com/api/resources/workers/subresources/scripts/methods/update/
5
5
  */
6
6
  import type { Secret } from "../secret";
7
+ import type { Assets } from "./assets";
7
8
  import type { R2Bucket } from "./bucket";
8
9
  import type { DurableObjectNamespace } from "./durable-object-namespace";
9
10
  import type { KVNamespace } from "./kv-namespace";
@@ -22,16 +23,24 @@ export type Binding =
22
23
  | Worker
23
24
  | R2Bucket
24
25
  | Secret
25
- | string;
26
+ | string
27
+ | Assets;
26
28
 
27
29
  export function isDurableObjectNamespace(
28
- binding: Binding,
30
+ binding: Binding
29
31
  ): binding is DurableObjectNamespace {
30
32
  return (
31
33
  typeof binding === "object" && binding.type === "durable_object_namespace"
32
34
  );
33
35
  }
34
36
 
37
+ /**
38
+ * Check if a binding is an Assets resource
39
+ */
40
+ export function isAssets(binding: Binding): binding is Assets {
41
+ return typeof binding === "object" && binding.type === "assets";
42
+ }
43
+
35
44
  /**
36
45
  * Union type for all Worker binding types (API spec)
37
46
  */
@@ -1,4 +1,5 @@
1
1
  import type { Secret } from "../secret";
2
+ import type { Assets } from "./assets";
2
3
  import type { Binding } from "./bindings";
3
4
  import type { R2Bucket as _R2Bucket } from "./bucket";
4
5
  import type { DurableObjectNamespace as _DurableObjectNamespace } from "./durable-object-namespace";
@@ -15,4 +16,6 @@ export type Bound<T extends Binding> = T extends _DurableObjectNamespace
15
16
  ? R2Bucket
16
17
  : T extends Secret
17
18
  ? string
18
- : never;
19
+ : T extends Assets
20
+ ? Service
21
+ : Service;
@@ -64,8 +64,7 @@ export interface CustomDomain
64
64
 
65
65
  /**
66
66
  * Configure custom domain for a Cloudflare Worker using the Cloudflare Custom Domains API
67
- * This attaches a worker (either a standard Worker or the one backing a StaticSite)
68
- * to a specific hostname within a zone.
67
+ * This attaches a worker to a specific hostname within a zone.
69
68
  *
70
69
  * @example
71
70
  * // Bind a domain to a standard Cloudflare Worker
@@ -80,19 +79,6 @@ export interface CustomDomain
80
79
  * workerName: apiWorker.name // Use the name from the Worker resource
81
80
  * });
82
81
  *
83
- * @example
84
- * // Bind a domain to a Cloudflare Static Site
85
- * const mySite = await StaticSite("landing-page", {
86
- * name: "my-landing-page-site", // This becomes the underlying worker name
87
- * dir: "./dist/landing-page"
88
- * });
89
- *
90
- * const siteDomain = await CustomDomain("site-domain-binding", {
91
- * name: "www.example.com",
92
- * zoneId: "YOUR_ZONE_ID", // Replace with actual Zone ID
93
- * workerName: mySite.name // Use the name from the StaticSite resource
94
- * });
95
- *
96
82
  * @see https://developers.cloudflare.com/api/resources/workers/subresources/domains/
97
83
  */
98
84
  export const CustomDomain = Resource(
@@ -1,5 +1,6 @@
1
1
  export * from "./account-api-token";
2
2
  export * from "./api";
3
+ export * from "./assets";
3
4
  export * from "./bindings";
4
5
  export * from "./bucket";
5
6
  export * from "./custom-domain";
@@ -8,7 +9,6 @@ export * from "./durable-object-namespace";
8
9
  export * from "./kv-namespace";
9
10
  export * from "./permission-groups";
10
11
  export * from "./r2-rest-state-store";
11
- export * from "./static-site";
12
12
  export * from "./worker";
13
13
  export * from "./wrangler.json";
14
14
  export * from "./zone";
@@ -123,16 +123,30 @@ export class R2RestStateStore implements StateStore {
123
123
  }
124
124
 
125
125
  const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
126
- const response = await this.api.get(listPath);
127
-
128
- if (!response.ok) {
129
- const errorData: any = await response.json().catch(() => ({
130
- errors: [{ message: response.statusText }],
131
- }));
132
- throw new Error(
133
- `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
134
- );
135
- }
126
+
127
+ const response = await withExponentialBackoff(
128
+ async () => {
129
+ const response = await this.api.get(listPath);
130
+
131
+ if (!response.ok) {
132
+ const errorData: any = await response.json().catch(() => ({
133
+ errors: [{ message: response.statusText }],
134
+ }));
135
+ throw new Error(
136
+ `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`
137
+ );
138
+ }
139
+
140
+ return response;
141
+ },
142
+ // Retry on transient errors
143
+ (error) =>
144
+ error.message?.includes("502") ||
145
+ error.message?.includes("503") ||
146
+ error.message?.includes("timeout"),
147
+ 5, // 5 retry attempts
148
+ 1000 // Start with 1 second delay
149
+ );
136
150
 
137
151
  const data = (await response.json()) as any;
138
152
 
@@ -176,21 +190,38 @@ export class R2RestStateStore implements StateStore {
176
190
  await this.ensureInitialized();
177
191
 
178
192
  try {
179
- const response = await this.api.get(
180
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
181
- );
193
+ const response = await withExponentialBackoff(
194
+ async () => {
195
+ const response = await this.api.get(
196
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
197
+ );
182
198
 
183
- if (!response.ok) {
184
- if (response.status === 404) {
185
- return undefined;
186
- }
199
+ if (!response.ok) {
200
+ if (response.status === 404) {
201
+ return response;
202
+ }
203
+
204
+ const errorData: any = await response.json().catch(() => ({
205
+ errors: [{ message: response.statusText }],
206
+ }));
207
+ throw new Error(
208
+ `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
209
+ );
210
+ }
187
211
 
188
- const errorData: any = await response.json().catch(() => ({
189
- errors: [{ message: response.statusText }],
190
- }));
191
- throw new Error(
192
- `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`
193
- );
212
+ return response;
213
+ },
214
+ // Retry on transient errors
215
+ (error) =>
216
+ error.message?.includes("502") ||
217
+ error.message?.includes("503") ||
218
+ error.message?.includes("timeout"),
219
+ 5, // 5 retry attempts
220
+ 1000 // Start with 1 second delay
221
+ );
222
+
223
+ if (response.status === 404) {
224
+ return undefined;
194
225
  }
195
226
 
196
227
  // Parse and deserialize the state data
@@ -298,18 +329,31 @@ export class R2RestStateStore implements StateStore {
298
329
  async delete(key: string): Promise<void> {
299
330
  await this.ensureInitialized();
300
331
 
301
- const response = await this.api.delete(
302
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
303
- );
332
+ await withExponentialBackoff(
333
+ async () => {
334
+ const response = await this.api.delete(
335
+ `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`
336
+ );
304
337
 
305
- if (!response.ok && response.status !== 404) {
306
- const errorData: any = await response.json().catch(() => ({
307
- errors: [{ message: response.statusText }],
308
- }));
309
- throw new Error(
310
- `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
311
- );
312
- }
338
+ if (!response.ok && response.status !== 404) {
339
+ const errorData: any = await response.json().catch(() => ({
340
+ errors: [{ message: response.statusText }],
341
+ }));
342
+ throw new Error(
343
+ `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`
344
+ );
345
+ }
346
+
347
+ return response;
348
+ },
349
+ // Retry on transient errors
350
+ (error) =>
351
+ error.message?.includes("502") ||
352
+ error.message?.includes("503") ||
353
+ error.message?.includes("timeout"),
354
+ 5, // 5 retry attempts
355
+ 1000 // Start with 1 second delay
356
+ );
313
357
  }
314
358
 
315
359
  /**