alchemy 0.5.1 → 0.6.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 (44) hide show
  1. package/lib/cloudflare/account-api-token.d.ts +6 -1
  2. package/lib/cloudflare/account-api-token.js +5 -1
  3. package/lib/cloudflare/account-id.d.ts +5 -1
  4. package/lib/cloudflare/account-id.js +5 -0
  5. package/lib/cloudflare/api-error.d.ts +32 -0
  6. package/lib/cloudflare/api-error.js +47 -0
  7. package/lib/cloudflare/api.d.ts +16 -49
  8. package/lib/cloudflare/api.js +50 -151
  9. package/lib/cloudflare/auth.d.ts +55 -38
  10. package/lib/cloudflare/auth.js +148 -77
  11. package/lib/cloudflare/bucket.d.ts +30 -5
  12. package/lib/cloudflare/bucket.js +12 -11
  13. package/lib/cloudflare/custom-domain.d.ts +2 -1
  14. package/lib/cloudflare/custom-domain.js +3 -2
  15. package/lib/cloudflare/dns-records.d.ts +2 -1
  16. package/lib/cloudflare/dns-records.js +2 -2
  17. package/lib/cloudflare/kv-namespace.d.ts +7 -1
  18. package/lib/cloudflare/kv-namespace.js +72 -71
  19. package/lib/cloudflare/permission-groups.d.ts +4 -0
  20. package/lib/cloudflare/permission-groups.js +5 -1
  21. package/lib/cloudflare/r2-rest-state-store.d.ts +3 -17
  22. package/lib/cloudflare/r2-rest-state-store.js +4 -12
  23. package/lib/cloudflare/worker.d.ts +2 -6
  24. package/lib/cloudflare/worker.js +4 -62
  25. package/lib/cloudflare/zone.d.ts +2 -6
  26. package/lib/cloudflare/zone.js +1 -3
  27. package/lib/index.d.ts +1 -0
  28. package/lib/internal/docs/providers.js +0 -7
  29. package/package.json +5 -1
  30. package/src/cloudflare/account-api-token.ts +7 -3
  31. package/src/cloudflare/account-id.ts +12 -0
  32. package/src/cloudflare/api-error.ts +58 -0
  33. package/src/cloudflare/api.ts +70 -219
  34. package/src/cloudflare/auth.ts +262 -119
  35. package/src/cloudflare/bucket.ts +52 -29
  36. package/src/cloudflare/custom-domain.ts +8 -3
  37. package/src/cloudflare/dns-records.ts +7 -3
  38. package/src/cloudflare/kv-namespace.ts +117 -105
  39. package/src/cloudflare/permission-groups.ts +5 -1
  40. package/src/cloudflare/r2-rest-state-store.ts +8 -31
  41. package/src/cloudflare/worker.ts +11 -103
  42. package/src/cloudflare/zone.ts +17 -25
  43. package/src/index.ts +1 -0
  44. package/src/internal/docs/providers.ts +0 -7
@@ -1,15 +1,28 @@
1
1
  import type { Secret } from "../secret";
2
- import { type CloudflareAuthOptions, getCloudflareHeaders } from "./auth";
2
+ import { withExponentialBackoff } from "../util/retry";
3
+ import { getCloudflareAuthHeaders, getCloudflareUserInfo } from "./auth";
3
4
 
4
5
  /**
5
6
  * Options for Cloudflare API requests
6
7
  */
7
8
  export interface CloudflareApiOptions {
9
+ /**
10
+ * Base URL for Cloudflare API
11
+ *
12
+ * @default https://api.cloudflare.com/client/v4
13
+ */
14
+ baseUrl?: string;
15
+
8
16
  /**
9
17
  * API Key to use (overrides CLOUDFLARE_API_KEY env var)
10
18
  */
11
19
  apiKey?: Secret;
12
20
 
21
+ /**
22
+ * API Token to use (overrides CLOUDFLARE_API_TOKEN env var)
23
+ */
24
+ apiToken?: Secret;
25
+
13
26
  /**
14
27
  * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
15
28
  * If not provided, will be automatically retrieved from the Cloudflare API
@@ -28,73 +41,6 @@ export interface CloudflareApiOptions {
28
41
  email?: string;
29
42
  }
30
43
 
31
- /**
32
- * Account information returned from Cloudflare API
33
- */
34
- interface CloudflareAccount {
35
- id: string;
36
- name: string;
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
-
98
44
  /**
99
45
  * Creates a CloudflareApi instance with automatic account ID discovery if not provided
100
46
  *
@@ -102,55 +48,25 @@ export async function handleApiError(
102
48
  * @returns Promise resolving to a CloudflareApi instance
103
49
  */
104
50
  export async function createCloudflareApi(
105
- options: CloudflareApiOptions = {}
51
+ options: Partial<CloudflareApiOptions> = {}
106
52
  ): Promise<CloudflareApi> {
107
- try {
108
- return new CloudflareApi({
109
- ...options,
110
- accountId: await CloudflareAccountId(options),
111
- });
112
- } catch (error) {
113
- console.error("Error during Cloudflare account ID discovery:", error);
114
- throw new Error(
115
- "Failed to automatically discover Cloudflare account ID. Please provide an account ID explicitly or ensure your API token/key has sufficient permissions."
116
- );
117
- }
118
- }
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;
53
+ const userInfo = await getCloudflareUserInfo(options);
54
+ return new CloudflareApi({
55
+ baseUrl: options.baseUrl,
56
+ accountId: options.accountId ?? userInfo.accounts[0].id!,
57
+ email: userInfo.email!,
58
+ apiKey: userInfo.apiKey,
59
+ apiToken: userInfo.apiToken,
60
+ zoneId: options.zoneId,
61
+ });
130
62
  }
131
63
 
132
64
  /**
133
65
  * Cloudflare API client using raw fetch
134
66
  */
135
67
  export class CloudflareApi {
136
- /** Base URL for Cloudflare API */
137
- readonly baseUrl: string;
138
-
139
- /** Cloudflare API Key */
140
- readonly apiKey: string;
141
-
142
- /** Cloudflare account ID */
143
- readonly accountId: string;
144
-
145
- /** Cloudflare zone ID (if provided) */
146
- readonly zoneId: string | null;
147
-
148
- /** User email (when using API Key auth) */
149
- readonly email?: string;
150
-
151
- /** Auth options for making requests */
152
- private readonly authOptions: CloudflareAuthOptions;
153
-
68
+ public readonly accountId: string;
69
+ public readonly baseUrl: string;
154
70
  /**
155
71
  * Create a new Cloudflare API client
156
72
  * Use createCloudflareApi factory function instead of direct constructor
@@ -158,43 +74,13 @@ export class CloudflareApi {
158
74
  *
159
75
  * @param options API options
160
76
  */
161
- constructor(options: CloudflareApiOptions = {}) {
162
- this.baseUrl = "https://api.cloudflare.com/client/v4";
163
-
164
- const apiKey =
165
- options.apiKey?.unencrypted ||
166
- process.env.CLOUDFLARE_API_KEY ||
167
- undefined;
168
- if (!apiKey) {
169
- throw new Error(
170
- "No API key provided. Use createCloudflareApi() instead for automatic account discovery."
171
- );
172
- }
173
- this.apiKey = apiKey;
174
-
175
- this.email = options.email || process.env.CLOUDFLARE_EMAIL;
176
- if (!this.email) {
177
- throw new Error(
178
- "No email provided. Use createCloudflareApi() instead for automatic account discovery."
179
- );
180
- }
181
-
182
- // Get account ID from options or environment
183
- this.accountId =
184
- options.accountId || process.env.CLOUDFLARE_ACCOUNT_ID || "";
185
- if (!this.accountId) {
186
- throw new Error(
187
- "No account ID provided. Use createCloudflareApi() instead for automatic account discovery."
188
- );
77
+ constructor(
78
+ private readonly options: CloudflareApiOptions & {
79
+ accountId: string;
189
80
  }
190
-
191
- // Zone ID is optional for some operations
192
- this.zoneId = options.zoneId || process.env.CLOUDFLARE_ZONE_ID || null;
193
-
194
- // Store auth options for later use
195
- this.authOptions = {
196
- email: this.email,
197
- };
81
+ ) {
82
+ this.accountId = options.accountId;
83
+ this.baseUrl = options.baseUrl ?? "https://api.cloudflare.com/client/v4";
198
84
  }
199
85
 
200
86
  /**
@@ -205,46 +91,51 @@ export class CloudflareApi {
205
91
  * @returns Raw Response object from fetch
206
92
  */
207
93
  async fetch(path: string, init: RequestInit = {}): Promise<Response> {
208
- // Get content type from init headers or default to application/json
209
- let contentType = "application/json";
210
- if (init.headers) {
211
- const initHeaders = init.headers as Record<string, string>;
212
- if (initHeaders["Content-Type"]) {
213
- contentType = initHeaders["Content-Type"];
214
- }
215
- }
216
-
217
- // Get auth headers (async now)
218
- const authHeaders = await getCloudflareHeaders(
219
- contentType,
220
- this.authOptions
221
- );
222
-
223
- // Combine all headers
224
- const combinedHeaders: Record<string, string> = {
225
- ...authHeaders,
94
+ let headers: Record<string, string> = {
95
+ "Content-Type": "application/json",
226
96
  };
227
-
228
- // Add headers from init if provided
229
- if (init.headers) {
230
- const initHeadersObj = init.headers as Record<string, string>;
231
- Object.keys(initHeadersObj).forEach((key) => {
232
- combinedHeaders[key] = initHeadersObj[key];
97
+ if (Array.isArray(init.headers)) {
98
+ init.headers.forEach(([key, value]) => {
99
+ headers[key] = value;
233
100
  });
101
+ } else if (init.headers instanceof Headers) {
102
+ init.headers.forEach((value, key) => {
103
+ headers[key] = value;
104
+ });
105
+ } else if (init.headers) {
106
+ headers = init.headers;
234
107
  }
108
+ headers = {
109
+ ...(await getCloudflareAuthHeaders(this.options)),
110
+ ...headers,
111
+ };
235
112
 
236
- // If using FormData, remove Content-Type to let browser set it with boundary
113
+ // TODO(sam): is this necessary?
237
114
  if (init.body instanceof FormData) {
238
- delete combinedHeaders["Content-Type"];
115
+ delete headers["Content-Type"];
239
116
  }
240
117
 
241
- const url = `${this.baseUrl}${path}`;
242
-
243
- // Make the request
244
- return fetch(url, {
245
- ...init,
246
- headers: combinedHeaders,
247
- });
118
+ // Use withExponentialBackoff for automatic retry on network errors
119
+ return withExponentialBackoff(
120
+ () =>
121
+ fetch(`${this.baseUrl}${path}`, {
122
+ ...init,
123
+ headers,
124
+ }),
125
+ (error) => {
126
+ // Only retry on network-related errors
127
+ const errorMsg = (error as Error).message || "";
128
+ const isNetworkError =
129
+ errorMsg.includes("socket connection was closed") ||
130
+ errorMsg.includes("ECONNRESET") ||
131
+ errorMsg.includes("ETIMEDOUT") ||
132
+ errorMsg.includes("ECONNREFUSED");
133
+
134
+ return isNetworkError;
135
+ },
136
+ 5, // Maximum 5 attempts (1 initial + 4 retries)
137
+ 1000 // Start with 1s delay, will exponentially increase
138
+ );
248
139
  }
249
140
 
250
141
  /**
@@ -311,43 +202,3 @@ export class CloudflareApi {
311
202
  return this.fetch(path, { ...init, method: "DELETE" });
312
203
  }
313
204
  }
314
-
315
- /**
316
- * Create a temporary API client for bootstrapping
317
- * This client can only be used to fetch the account ID
318
- */
319
- async function fetchAccountId(): Promise<string> {
320
- // Create a minimal API client for bootstrapping
321
- const baseUrl = "https://api.cloudflare.com/client/v4";
322
-
323
- // Get content type and auth headers
324
- const authHeaders = await getCloudflareHeaders("application/json");
325
-
326
- // Make the request to get accounts
327
- const response = await fetch(`${baseUrl}/accounts`, {
328
- method: "GET",
329
- headers: authHeaders,
330
- });
331
-
332
- if (!response.ok) {
333
- const errorData: any = await response.json().catch(() => ({
334
- errors: [{ message: response.statusText }],
335
- }));
336
-
337
- throw new Error(
338
- `Error fetching Cloudflare accounts: ${errorData.errors?.[0]?.message || response.statusText}`
339
- );
340
- }
341
-
342
- const data: any = await response.json();
343
- const accounts = data.result as CloudflareAccount[];
344
-
345
- if (!accounts || accounts.length === 0) {
346
- throw new Error(
347
- "No Cloudflare accounts found. Check your API token/key permissions."
348
- );
349
- }
350
-
351
- // Return the first account ID
352
- return accounts[0].id;
353
- }