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,10 +1,10 @@
1
1
  import type { Scope } from "../scope";
2
- import type { Secret } from "../secret";
3
2
  import type { State, StateStore } from "../state";
3
+ import { type CloudflareApiOptions } from "./api";
4
4
  /**
5
5
  * Options for CloudflareR2StateStore
6
6
  */
7
- export interface CloudflareR2StateStoreOptions {
7
+ export interface CloudflareR2StateStoreOptions extends CloudflareApiOptions {
8
8
  /**
9
9
  * The prefix to use for object keys in the R2 bucket
10
10
  * This allows multiple state stores to use the same R2 bucket
@@ -15,18 +15,6 @@ export interface CloudflareR2StateStoreOptions {
15
15
  * Required - the bucket must already exist
16
16
  */
17
17
  bucketName: string;
18
- /**
19
- * API key to use (overrides CLOUDFLARE_API_KEY env var)
20
- */
21
- apiKey?: Secret;
22
- /**
23
- * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
24
- */
25
- accountId?: string;
26
- /**
27
- * Email to use with API Key authentication (overrides CLOUDFLARE_EMAIL env var)
28
- */
29
- email?: string;
30
18
  }
31
19
  /**
32
20
  * State store implementation using Cloudflare R2 API
@@ -34,12 +22,10 @@ export interface CloudflareR2StateStoreOptions {
34
22
  */
35
23
  export declare class R2RestStateStore implements StateStore {
36
24
  readonly scope: Scope;
25
+ private readonly options;
37
26
  private api;
38
27
  private prefix;
39
28
  private bucketName;
40
- private apiKey;
41
- private accountId;
42
- private email;
43
29
  private initialized;
44
30
  /**
45
31
  * Create a new CloudflareR2StateStore
@@ -1,18 +1,16 @@
1
1
  import { deserialize, serialize } from "../serde";
2
2
  import { withExponentialBackoff } from "../util/retry";
3
- import { createCloudflareApi } from "./api";
3
+ import { createCloudflareApi, } from "./api";
4
4
  /**
5
5
  * State store implementation using Cloudflare R2 API
6
6
  * Uses R2 for immediate consistency compared to KV's eventual consistency
7
7
  */
8
8
  export class R2RestStateStore {
9
9
  scope;
10
+ options;
10
11
  api;
11
12
  prefix;
12
13
  bucketName;
13
- apiKey;
14
- accountId;
15
- email;
16
14
  initialized = false;
17
15
  /**
18
16
  * Create a new CloudflareR2StateStore
@@ -22,6 +20,7 @@ export class R2RestStateStore {
22
20
  */
23
21
  constructor(scope, options) {
24
22
  this.scope = scope;
23
+ this.options = options;
25
24
  // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
26
25
  const scopePath = scope.chain.join("/");
27
26
  this.prefix = options.prefix
@@ -31,9 +30,6 @@ export class R2RestStateStore {
31
30
  throw new Error("bucketName is required for CloudflareR2StateStore");
32
31
  }
33
32
  this.bucketName = options.bucketName;
34
- this.apiKey = options.apiKey;
35
- this.accountId = options.accountId;
36
- this.email = options.email;
37
33
  // We'll initialize the API in init() to allow for async creation
38
34
  this.api = null;
39
35
  }
@@ -44,11 +40,7 @@ export class R2RestStateStore {
44
40
  if (this.initialized)
45
41
  return;
46
42
  // Create Cloudflare API client with automatic account discovery
47
- this.api = await createCloudflareApi({
48
- apiKey: this.apiKey,
49
- accountId: this.accountId,
50
- email: this.email,
51
- });
43
+ this.api = await createCloudflareApi(this.options);
52
44
  this.initialized = true;
53
45
  }
54
46
  /**
@@ -1,13 +1,14 @@
1
1
  import type { Context } from "../context";
2
2
  import { type BundleProps } from "../esbuild/bundle";
3
3
  import { Resource } from "../resource";
4
+ import { type CloudflareApiOptions } from "./api";
4
5
  import { type Bindings } from "./bindings";
5
6
  import type { Bound } from "./bound";
6
7
  import type { SingleStepMigration } from "./worker-migration";
7
8
  /**
8
9
  * Properties for creating or updating a Worker
9
10
  */
10
- export interface WorkerProps<B extends Bindings = Bindings> {
11
+ export interface WorkerProps<B extends Bindings = Bindings> extends CloudflareApiOptions {
11
12
  /**
12
13
  * The worker script content (JavaScript or WASM)
13
14
  * One of script, entryPoint, or bundle must be provided
@@ -36,11 +37,6 @@ export interface WorkerProps<B extends Bindings = Bindings> {
36
37
  * This is mandatory - must be explicitly specified
37
38
  */
38
39
  name: string;
39
- /**
40
- * Routes to associate with this worker
41
- * Format: example.com/* or *.example.com/*
42
- */
43
- routes?: string[];
44
40
  /**
45
41
  * Bindings to attach to the worker
46
42
  */
@@ -6,7 +6,7 @@ import { isSecret } from "../secret";
6
6
  import { getContentType } from "../util/content-type";
7
7
  import { withExponentialBackoff } from "../util/retry";
8
8
  import { slugify } from "../util/slugify";
9
- import { createCloudflareApi } from "./api";
9
+ import { createCloudflareApi, } from "./api";
10
10
  import { isAssets, isDurableObjectNamespace, } from "./bindings";
11
11
  import { isKVNamespace } from "./kv-namespace";
12
12
  /**
@@ -90,7 +90,7 @@ export const Worker = Resource("cloudflare::Worker", {
90
90
  alwaysUpdate: true,
91
91
  }, async function (id, props) {
92
92
  // Create Cloudflare API client with automatic account discovery
93
- const api = await createCloudflareApi();
93
+ const api = await createCloudflareApi(props);
94
94
  // Use the provided name
95
95
  const workerName = props.name;
96
96
  // Validate input - we need either script, entryPoint, or bundle
@@ -133,8 +133,6 @@ export const Worker = Resource("cloudflare::Worker", {
133
133
  await putWorker(api, workerName, scriptContent, scriptMetadata);
134
134
  // TODO: it is less than ideal that this can fail, resulting in state problem
135
135
  await this.set("bindings", props.bindings);
136
- // Handle routes if requested
137
- await setupRoutes(api, workerName, props.routes || []);
138
136
  // Handle worker URL if requested
139
137
  const workerUrl = await configureURL(this, api, workerName, props.url ?? false);
140
138
  // Get current timestamp
@@ -146,7 +144,6 @@ export const Worker = Resource("cloudflare::Worker", {
146
144
  name: workerName,
147
145
  script: scriptContent,
148
146
  format: props.format || "esm", // Include format in the output
149
- routes: props.routes || [],
150
147
  bindings: props.bindings ?? {},
151
148
  env: props.env,
152
149
  observability: scriptMetadata.observability,
@@ -167,25 +164,6 @@ async function deleteWorker(ctx, api, workerName) {
167
164
  .catch(() => ({ errors: [{ message: deleteResponse.statusText }] }));
168
165
  console.error("Error deleting worker:", errorData.errors?.[0]?.message || deleteResponse.statusText);
169
166
  }
170
- // Also remove any associated routes if they exist
171
- if (ctx.output?.routes && ctx.output.routes.length > 0 && api.zoneId) {
172
- // First get existing routes to find their IDs
173
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
174
- if (!routesResponse.ok) {
175
- throw new Error(`Could not fetch routes for cleanup: ${routesResponse.status} ${routesResponse.statusText}`);
176
- }
177
- const routesData = await routesResponse.json();
178
- const existingRoutes = routesData.result || [];
179
- for (const route of existingRoutes) {
180
- if (ctx.output.routes.includes(route.pattern)) {
181
- // Delete the route
182
- const routeDeleteResponse = await api.delete(`/zones/${api.zoneId}/workers/routes/${route.id}`);
183
- if (!routeDeleteResponse.ok) {
184
- console.warn(`Failed to delete route ${route.pattern}: ${routeDeleteResponse.status} ${routeDeleteResponse.statusText}`);
185
- }
186
- }
187
- }
188
- }
189
167
  // Disable the URL if it was enabled
190
168
  if (ctx.output?.url) {
191
169
  try {
@@ -421,44 +399,6 @@ async function bundleWorkerScript(props) {
421
399
  throw new Error("Error reading bundle");
422
400
  }
423
401
  }
424
- async function setupRoutes(api, workerName, routes) {
425
- // Set up routes if provided
426
- if (routes && routes.length > 0 && api.zoneId) {
427
- // First get existing routes
428
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
429
- if (!routesResponse.ok) {
430
- throw new Error(`Could not fetch routes: ${routesResponse.status} ${routesResponse.statusText}`);
431
- }
432
- const routesData = await routesResponse.json();
433
- const existingRoutes = routesData.result || [];
434
- // For each desired route
435
- for (const pattern of routes) {
436
- const existingRoute = existingRoutes.find((r) => r.pattern === pattern);
437
- if (existingRoute) {
438
- // Update if script name doesn't match
439
- if (existingRoute.script !== workerName) {
440
- const updateRouteResponse = await api.put(`/zones/${api.zoneId}/workers/routes/${existingRoute.id}`, {
441
- pattern,
442
- script: workerName,
443
- });
444
- if (!updateRouteResponse.ok) {
445
- console.warn(`Failed to update route ${pattern}: ${updateRouteResponse.status} ${updateRouteResponse.statusText}`);
446
- }
447
- }
448
- }
449
- else {
450
- // Create new route
451
- const createRouteResponse = await api.post(`/zones/${api.zoneId}/workers/routes`, {
452
- pattern,
453
- script: workerName,
454
- });
455
- if (!createRouteResponse.ok) {
456
- throw new Error(`Failed to create route ${pattern}: ${createRouteResponse.status} ${createRouteResponse.statusText}`);
457
- }
458
- }
459
- }
460
- }
461
- }
462
402
  async function configureURL(ctx, api, workerName, url) {
463
403
  let workerUrl;
464
404
  if (url) {
@@ -557,6 +497,7 @@ async function uploadAssets(api, workerName, assets) {
557
497
  // Process each bucket of files
558
498
  for (const bucket of buckets) {
559
499
  const formData = new FormData();
500
+ let totalBytes = 0;
560
501
  // Add each file in the bucket to the form
561
502
  for (const fileHash of bucket) {
562
503
  // Find the file with this hash
@@ -575,6 +516,7 @@ async function uploadAssets(api, workerName, assets) {
575
516
  const blob = new Blob([base64Content], {
576
517
  type: getContentType(file.filePath),
577
518
  });
519
+ totalBytes += blob.size;
578
520
  formData.append(fileHash, blob, fileHash);
579
521
  }
580
522
  // Upload this batch of files
@@ -1,19 +1,15 @@
1
1
  import type { Context } from "../context";
2
2
  import { Resource } from "../resource";
3
+ import { type CloudflareApiOptions } from "./api";
3
4
  import type { AlwaysUseHTTPSValue, AutomaticHTTPSRewritesValue, BrotliValue, DevelopmentModeValue, EarlyHintsValue, EmailObfuscationValue, HTTP2Value, HTTP3Value, HotlinkProtectionValue, IPv6Value, MinTLSVersionValue, SSLValue, TLS13Value, WebSocketsValue, ZeroRTTValue } from "./zone-settings";
4
5
  /**
5
6
  * Properties for creating or updating a Zone
6
7
  */
7
- export interface ZoneProps {
8
+ export interface ZoneProps extends CloudflareApiOptions {
8
9
  /**
9
10
  * The domain name for the zone
10
11
  */
11
12
  name: string;
12
- /**
13
- * Account ID to use for the zone
14
- * If not provided, will use the default account
15
- */
16
- accountId?: string;
17
13
  /**
18
14
  * The type of zone to create
19
15
  * "full" - Full zone implies that DNS is hosted with Cloudflare
@@ -57,9 +57,7 @@ import { createCloudflareApi } from "./api";
57
57
  */
58
58
  export const Zone = Resource("cloudflare::Zone", async function (id, props) {
59
59
  // Create Cloudflare API client with automatic account discovery
60
- const api = await createCloudflareApi({
61
- accountId: props.accountId,
62
- });
60
+ const api = await createCloudflareApi(props);
63
61
  if (this.phase === "delete") {
64
62
  if (this.output?.id) {
65
63
  // Delete zone
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type * from "./context";
2
2
  export * from "./resource";
3
+ export type * from "./scope";
3
4
  export * from "./secret";
4
5
  export * from "./state";
5
6
  export * from "./util/ignore";
@@ -21,13 +21,6 @@ export async function Providers({ srcDir, outDir, filter, parallel = true, }) {
21
21
  }))
22
22
  .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
23
23
  .map((dirent) => path.join(dirent.parentPath, dirent.name));
24
- // For each provider, list all files
25
- if (filter === false) {
26
- return [];
27
- }
28
- else if (typeof filter === "number") {
29
- providers = providers.slice(0, filter);
30
- }
31
24
  if (parallel) {
32
25
  return await Promise.all(providers.map((provider) => generateProviderDocs({ provider, outDir, parallel })));
33
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "module": "./lib/index.js",
6
6
  "scripts": {
@@ -39,6 +39,7 @@
39
39
  "@aws-sdk/client-sesv2": "^3.0.0",
40
40
  "@aws-sdk/client-sqs": "^3.0.0",
41
41
  "@aws-sdk/client-sts": "^3.0.0",
42
+ "@iarna/toml": "^2.2.5",
42
43
  "@octokit/rest": "^21.1.1",
43
44
  "ai": "^4.0.0",
44
45
  "arktype": "^2.0.0",
@@ -53,6 +54,7 @@
53
54
  "prettier": "^3.0.0",
54
55
  "stripe": "^17.0.0",
55
56
  "turndown": "^7.0.0",
57
+ "xdg-app-paths": "^8.0.0",
56
58
  "yaml": "^2.0.0",
57
59
  "zod": "^3.0.0"
58
60
  },
@@ -62,6 +64,7 @@
62
64
  "@aws-sdk/client-s3": "3.726.1",
63
65
  "@biomejs/biome": "^1.9.4",
64
66
  "@cloudflare/workers-types": "^4.20250303.0",
67
+ "@iarna/toml": "^2.2.5",
65
68
  "@octokit/rest": "^21.1.1",
66
69
  "@types/bun": "latest",
67
70
  "@types/diff": "^5.0.0",
@@ -82,6 +85,7 @@
82
85
  "vite": "^6.0.7",
83
86
  "vitepress": "^1.6.3",
84
87
  "wrangler": "^3.114.0",
88
+ "xdg-app-paths": "^8.3.0",
85
89
  "yaml": "^2.7.1"
86
90
  }
87
91
  }
@@ -3,7 +3,7 @@ import type { Context } from "../context";
3
3
  import { Resource } from "../resource";
4
4
  import { Secret } from "../secret";
5
5
  import { sha256 } from "../util/sha256";
6
- import { createCloudflareApi } from "./api";
6
+ import { createCloudflareApi, type CloudflareApiOptions } from "./api";
7
7
 
8
8
  /**
9
9
  * Permission group for a token policy
@@ -63,7 +63,7 @@ export interface TokenCondition {
63
63
  /**
64
64
  * Properties for creating or updating an Account API Token
65
65
  */
66
- export interface AccountApiTokenProps {
66
+ export interface AccountApiTokenProps extends CloudflareApiOptions {
67
67
  /**
68
68
  * Name of the token
69
69
  */
@@ -162,6 +162,10 @@ export interface AccountApiToken
162
162
  /**
163
163
  * Creates a Cloudflare Account API Token with specified permissions.
164
164
  *
165
+ * Note: Requires a Cloudflare API Key or Token with admin-level account access.
166
+ * The OAuth token from `wrangler login` is NOT sufficient for this operation.
167
+ * You must use an API token with permission to manage account API tokens.
168
+ *
165
169
  * @see https://developers.cloudflare.com/api/resources/accounts/subresources/tokens/methods/create/
166
170
  *
167
171
  * @example
@@ -222,7 +226,7 @@ export const AccountApiToken = Resource(
222
226
  props: AccountApiTokenProps
223
227
  ): Promise<AccountApiToken> {
224
228
  // Create Cloudflare API client with automatic account discovery
225
- const api = await createCloudflareApi();
229
+ const api = await createCloudflareApi(props);
226
230
 
227
231
  if (this.phase === "delete") {
228
232
  // Delete token if we have an ID
@@ -0,0 +1,12 @@
1
+ import { getCloudflareUserInfo, type CloudflareAuthOptions } from "./auth";
2
+
3
+ export type CloudflareAccountId = string & {
4
+ readonly __brand: "CloudflareAccountId";
5
+ };
6
+
7
+ export async function CloudflareAccountId(
8
+ options: CloudflareAuthOptions
9
+ ): Promise<CloudflareAccountId> {
10
+ const userInfo = await getCloudflareUserInfo(options);
11
+ return userInfo.accounts[0].id as CloudflareAccountId;
12
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Custom error class for Cloudflare API errors
3
+ * Includes HTTP status information from the Response
4
+ */
5
+ export class CloudflareApiError extends Error {
6
+ /**
7
+ * HTTP status code
8
+ */
9
+ status: number;
10
+
11
+ /**
12
+ * HTTP status text
13
+ */
14
+ statusText: string;
15
+
16
+ /**
17
+ * Raw error data from the API
18
+ */
19
+ errorData?: any;
20
+
21
+ /**
22
+ * Create a new CloudflareApiError
23
+ */
24
+ constructor(message: string, response: Response, errorData?: any) {
25
+ super(message);
26
+ this.name = "CloudflareApiError";
27
+ this.status = response.status;
28
+ this.statusText = response.statusText;
29
+ this.errorData = errorData;
30
+
31
+ // Ensure instanceof works correctly
32
+ Object.setPrototypeOf(this, CloudflareApiError.prototype);
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Helper function to handle API errors
38
+ *
39
+ * @param response The fetch Response object
40
+ * @param action The action being performed (e.g., "creating", "deleting")
41
+ * @param resourceType The type of resource being acted upon (e.g., "R2 bucket", "Worker")
42
+ * @param resourceName The name/identifier of the specific resource
43
+ * @returns Never returns - always throws an error
44
+ */
45
+ export async function handleApiError(
46
+ response: Response,
47
+ action: string,
48
+ resourceType: string,
49
+ resourceName: string
50
+ ): Promise<never> {
51
+ const json: any = await response.json();
52
+ const errors: { message: string }[] = json.errors || [
53
+ { message: response.statusText },
54
+ ];
55
+ const errorMessage = `Error ${action} ${resourceType} '${resourceName}': ${errors[0]?.message || response.statusText}`;
56
+
57
+ throw new CloudflareApiError(errorMessage, response, errors);
58
+ }