alchemy 0.5.2 → 0.7.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 (54) 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 +37 -149
  9. package/lib/cloudflare/auth.d.ts +55 -38
  10. package/lib/cloudflare/auth.js +148 -77
  11. package/lib/cloudflare/bindings.d.ts +19 -7
  12. package/lib/cloudflare/bindings.js +0 -9
  13. package/lib/cloudflare/bound.d.ts +2 -1
  14. package/lib/cloudflare/bucket.d.ts +30 -5
  15. package/lib/cloudflare/bucket.js +12 -11
  16. package/lib/cloudflare/custom-domain.d.ts +2 -1
  17. package/lib/cloudflare/custom-domain.js +3 -2
  18. package/lib/cloudflare/dns-records.d.ts +2 -1
  19. package/lib/cloudflare/dns-records.js +2 -2
  20. package/lib/cloudflare/index.d.ts +1 -0
  21. package/lib/cloudflare/index.js +1 -0
  22. package/lib/cloudflare/kv-namespace.d.ts +7 -1
  23. package/lib/cloudflare/kv-namespace.js +72 -71
  24. package/lib/cloudflare/permission-groups.d.ts +4 -0
  25. package/lib/cloudflare/permission-groups.js +5 -1
  26. package/lib/cloudflare/r2-rest-state-store.d.ts +3 -17
  27. package/lib/cloudflare/r2-rest-state-store.js +4 -12
  28. package/lib/cloudflare/worker.d.ts +2 -6
  29. package/lib/cloudflare/worker.js +58 -90
  30. package/lib/cloudflare/workflow.d.ts +46 -0
  31. package/lib/cloudflare/workflow.js +29 -0
  32. package/lib/cloudflare/zone.d.ts +2 -6
  33. package/lib/cloudflare/zone.js +1 -3
  34. package/lib/internal/docs/providers.js +1 -8
  35. package/package.json +5 -1
  36. package/src/cloudflare/account-api-token.ts +7 -3
  37. package/src/cloudflare/account-id.ts +12 -0
  38. package/src/cloudflare/api-error.ts +58 -0
  39. package/src/cloudflare/api.ts +50 -216
  40. package/src/cloudflare/auth.ts +262 -119
  41. package/src/cloudflare/bindings.ts +21 -16
  42. package/src/cloudflare/bound.ts +4 -1
  43. package/src/cloudflare/bucket.ts +52 -29
  44. package/src/cloudflare/custom-domain.ts +8 -3
  45. package/src/cloudflare/dns-records.ts +7 -3
  46. package/src/cloudflare/durable-object-namespace.ts +1 -1
  47. package/src/cloudflare/index.ts +1 -0
  48. package/src/cloudflare/kv-namespace.ts +117 -105
  49. package/src/cloudflare/permission-groups.ts +5 -1
  50. package/src/cloudflare/r2-rest-state-store.ts +8 -31
  51. package/src/cloudflare/worker.ts +71 -141
  52. package/src/cloudflare/workflow.ts +79 -0
  53. package/src/cloudflare/zone.ts +17 -25
  54. package/src/internal/docs/providers.ts +1 -8
@@ -3,28 +3,28 @@ import * as fs from "fs/promises";
3
3
  import type { Context } from "../context";
4
4
  import { Bundle, type BundleProps } from "../esbuild/bundle";
5
5
  import { Resource } from "../resource";
6
- import { isSecret } from "../secret";
7
6
  import { getContentType } from "../util/content-type";
8
7
  import { withExponentialBackoff } from "../util/retry";
9
8
  import { slugify } from "../util/slugify";
10
- import { type CloudflareApi, createCloudflareApi } from "./api";
11
- import type { Assets } from "./assets";
12
9
  import {
13
- type Bindings,
14
- type WorkerBindingSpec,
15
- isAssets,
16
- isDurableObjectNamespace,
17
- } from "./bindings";
10
+ createCloudflareApi,
11
+ type CloudflareApi,
12
+ type CloudflareApiOptions,
13
+ } from "./api";
14
+ import { type Assets } from "./assets";
15
+ import { type Bindings, type WorkerBindingSpec } from "./bindings";
18
16
  import type { Bound } from "./bound";
19
- import type { DurableObjectNamespace } from "./durable-object-namespace";
17
+ import { type DurableObjectNamespace } from "./durable-object-namespace";
20
18
  import { isKVNamespace } from "./kv-namespace";
21
19
  import type { WorkerScriptMetadata } from "./worker-metadata";
22
20
  import type { SingleStepMigration } from "./worker-migration";
21
+ import { upsertWorkflow, type Workflow } from "./workflow";
23
22
 
24
23
  /**
25
24
  * Properties for creating or updating a Worker
26
25
  */
27
- export interface WorkerProps<B extends Bindings = Bindings> {
26
+ export interface WorkerProps<B extends Bindings = Bindings>
27
+ extends CloudflareApiOptions {
28
28
  /**
29
29
  * The worker script content (JavaScript or WASM)
30
30
  * One of script, entryPoint, or bundle must be provided
@@ -58,12 +58,6 @@ export interface WorkerProps<B extends Bindings = Bindings> {
58
58
  */
59
59
  name: string;
60
60
 
61
- /**
62
- * Routes to associate with this worker
63
- * Format: example.com/* or *.example.com/*
64
- */
65
- routes?: string[];
66
-
67
61
  /**
68
62
  * Bindings to attach to the worker
69
63
  */
@@ -236,7 +230,7 @@ export const Worker = Resource(
236
230
  props: WorkerProps<B>
237
231
  ): Promise<Worker<B>> {
238
232
  // Create Cloudflare API client with automatic account discovery
239
- const api = await createCloudflareApi();
233
+ const api = await createCloudflareApi(props);
240
234
 
241
235
  // Use the provided name
242
236
  const workerName = props.name;
@@ -262,10 +256,16 @@ export const Worker = Resource(
262
256
 
263
257
  // Find any assets bindings
264
258
  const assetsBindings: { name: string; assets: Assets }[] = [];
259
+ const workflowsBindings: Workflow[] = [];
260
+
265
261
  if (props.bindings) {
266
262
  for (const [bindingName, binding] of Object.entries(props.bindings)) {
267
- if (isAssets(binding)) {
268
- assetsBindings.push({ name: bindingName, assets: binding });
263
+ if (typeof binding === "object") {
264
+ if (binding.type === "assets") {
265
+ assetsBindings.push({ name: bindingName, assets: binding });
266
+ } else if (binding.type === "workflow") {
267
+ workflowsBindings.push(binding);
268
+ }
269
269
  }
270
270
  }
271
271
  }
@@ -293,15 +293,19 @@ export const Worker = Resource(
293
293
  assetUploadResult
294
294
  );
295
295
 
296
- // Upload the worker script
297
296
  await putWorker(api, workerName, scriptContent, scriptMetadata);
298
297
 
298
+ for (const workflow of workflowsBindings) {
299
+ await upsertWorkflow(api, {
300
+ workflowName: workflow.workflowName,
301
+ className: workflow.className,
302
+ scriptName: workerName,
303
+ });
304
+ }
305
+
299
306
  // TODO: it is less than ideal that this can fail, resulting in state problem
300
307
  await this.set("bindings", props.bindings);
301
308
 
302
- // Handle routes if requested
303
- await setupRoutes(api, workerName, props.routes || []);
304
-
305
309
  // Handle worker URL if requested
306
310
  const workerUrl = await configureURL(
307
311
  this,
@@ -320,7 +324,6 @@ export const Worker = Resource(
320
324
  name: workerName,
321
325
  script: scriptContent,
322
326
  format: props.format || "esm", // Include format in the output
323
- routes: props.routes || [],
324
327
  bindings: props.bindings ?? ({} as B),
325
328
  env: props.env,
326
329
  observability: scriptMetadata.observability,
@@ -354,35 +357,6 @@ async function deleteWorker<B extends Bindings>(
354
357
  );
355
358
  }
356
359
 
357
- // Also remove any associated routes if they exist
358
- if (ctx.output?.routes && ctx.output.routes.length > 0 && api.zoneId) {
359
- // First get existing routes to find their IDs
360
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
361
-
362
- if (!routesResponse.ok) {
363
- throw new Error(
364
- `Could not fetch routes for cleanup: ${routesResponse.status} ${routesResponse.statusText}`
365
- );
366
- }
367
- const routesData: any = await routesResponse.json();
368
- const existingRoutes = routesData.result || [];
369
-
370
- for (const route of existingRoutes) {
371
- if (ctx.output.routes.includes(route.pattern)) {
372
- // Delete the route
373
- const routeDeleteResponse = await api.delete(
374
- `/zones/${api.zoneId}/workers/routes/${route.id}`
375
- );
376
-
377
- if (!routeDeleteResponse.ok) {
378
- console.warn(
379
- `Failed to delete route ${route.pattern}: ${routeDeleteResponse.status} ${routeDeleteResponse.statusText}`
380
- );
381
- }
382
- }
383
- }
384
- }
385
-
386
360
  // Disable the URL if it was enabled
387
361
  if (ctx.output?.url) {
388
362
  try {
@@ -566,60 +540,79 @@ async function prepareWorkerMetadata<B extends Bindings>(
566
540
  service: binding.id,
567
541
  });
568
542
  } else if (binding.type === "durable_object_namespace") {
569
- const stableId = binding.id;
570
- const className = binding.className;
571
-
572
543
  meta.bindings.push({
573
544
  type: "durable_object_namespace",
574
545
  name: bindingName,
575
- class_name: className,
546
+ class_name: binding.className,
576
547
  script_name: binding.scriptName,
577
548
  environment: binding.environment,
578
549
  namespace_id: binding.namespaceId,
579
550
  });
580
-
581
- const oldBinding: DurableObjectNamespace | undefined = Object.values(
582
- oldBindings ?? {}
583
- )
584
- ?.filter(isDurableObjectNamespace)
585
- ?.find((b) => b.id === stableId);
586
-
587
- if (!oldBinding) {
588
- if (binding.sqlite) {
589
- meta.migrations!.new_sqlite_classes!.push(className);
590
- } else {
591
- meta.migrations!.new_classes!.push(className);
592
- }
593
- } else if (oldBinding.className !== className) {
594
- meta.migrations!.renamed_classes!.push({
595
- from: oldBinding.className,
596
- to: className,
597
- });
598
- }
551
+ configureClassMigration(binding, binding.id, binding.className);
599
552
  } else if (binding.type === "r2_bucket") {
600
553
  meta.bindings.push({
601
554
  type: "r2_bucket",
602
555
  name: bindingName,
603
556
  bucket_name: binding.name,
604
557
  });
605
- } else if (isAssets(binding)) {
558
+ } else if (binding.type === "assets") {
606
559
  meta.bindings.push({
607
560
  type: "assets",
608
561
  name: bindingName,
609
562
  });
610
- } else if (isSecret(binding)) {
563
+ } else if (binding.type === "secret") {
611
564
  meta.bindings.push({
612
565
  type: "secret_text",
613
566
  name: bindingName,
614
567
  text: binding.unencrypted,
615
568
  });
569
+ } else if (binding.type === "workflow") {
570
+ meta.bindings.push({
571
+ type: "workflow",
572
+ name: bindingName,
573
+ workflow_name: binding.workflowName,
574
+ class_name: binding.className,
575
+ // this should be set if the Workflow is in another script ...
576
+ // script_name: ??,
577
+ });
578
+ // it's unclear whether this is needed, but it works both ways
579
+ configureClassMigration(binding, binding.id, binding.className);
616
580
  } else {
617
581
  // @ts-expect-error - we should never reach here
618
582
  throw new Error(`Unsupported binding type: ${binding.type}`);
619
583
  }
620
584
  }
621
585
 
586
+ function configureClassMigration(
587
+ binding: DurableObjectNamespace | Workflow,
588
+ stableId: string,
589
+ className: string
590
+ ) {
591
+ const oldBinding: DurableObjectNamespace | Workflow | undefined =
592
+ Object.values(oldBindings ?? {})
593
+ ?.filter(
594
+ (b) =>
595
+ typeof b === "object" &&
596
+ (b.type === "durable_object_namespace" || b.type === "workflow")
597
+ )
598
+ ?.find((b) => b.id === stableId);
599
+
600
+ if (!oldBinding) {
601
+ if (binding.type === "durable_object_namespace" && binding.sqlite) {
602
+ meta.migrations!.new_sqlite_classes!.push(className);
603
+ } else {
604
+ meta.migrations!.new_classes!.push(className);
605
+ }
606
+ } else if (oldBinding.className !== className) {
607
+ meta.migrations!.renamed_classes!.push({
608
+ from: oldBinding.className,
609
+ to: className,
610
+ });
611
+ }
612
+ }
613
+
622
614
  // Convert env variables to plain_text bindings
615
+ // TODO(sam): remove Worker.env in favor of always bindings
623
616
  if (props.env) {
624
617
  for (const [key, value] of Object.entries(props.env)) {
625
618
  meta.bindings.push({
@@ -717,67 +710,6 @@ async function bundleWorkerScript<B extends Bindings>(props: WorkerProps) {
717
710
  }
718
711
  }
719
712
 
720
- async function setupRoutes(
721
- api: CloudflareApi,
722
- workerName: string,
723
- routes: string[]
724
- ) {
725
- // Set up routes if provided
726
- if (routes && routes.length > 0 && api.zoneId) {
727
- // First get existing routes
728
- const routesResponse = await api.get(`/zones/${api.zoneId}/workers/routes`);
729
-
730
- if (!routesResponse.ok) {
731
- throw new Error(
732
- `Could not fetch routes: ${routesResponse.status} ${routesResponse.statusText}`
733
- );
734
- }
735
- const routesData: any = await routesResponse.json();
736
- const existingRoutes = routesData.result || [];
737
-
738
- // For each desired route
739
- for (const pattern of routes) {
740
- const existingRoute = existingRoutes.find(
741
- (r: any) => r.pattern === pattern
742
- );
743
-
744
- if (existingRoute) {
745
- // Update if script name doesn't match
746
- if (existingRoute.script !== workerName) {
747
- const updateRouteResponse = await api.put(
748
- `/zones/${api.zoneId}/workers/routes/${existingRoute.id}`,
749
- {
750
- pattern,
751
- script: workerName,
752
- }
753
- );
754
-
755
- if (!updateRouteResponse.ok) {
756
- console.warn(
757
- `Failed to update route ${pattern}: ${updateRouteResponse.status} ${updateRouteResponse.statusText}`
758
- );
759
- }
760
- }
761
- } else {
762
- // Create new route
763
- const createRouteResponse = await api.post(
764
- `/zones/${api.zoneId}/workers/routes`,
765
- {
766
- pattern,
767
- script: workerName,
768
- }
769
- );
770
-
771
- if (!createRouteResponse.ok) {
772
- throw new Error(
773
- `Failed to create route ${pattern}: ${createRouteResponse.status} ${createRouteResponse.statusText}`
774
- );
775
- }
776
- }
777
- }
778
- }
779
- }
780
-
781
713
  async function configureURL<B extends Bindings>(
782
714
  ctx: Context<Worker<B>>,
783
715
  api: CloudflareApi,
@@ -1006,8 +938,6 @@ async function uploadAssets(
1006
938
  formData.append(fileHash, blob, fileHash);
1007
939
  }
1008
940
 
1009
- console.log(`Uploading ${totalBytes} bytes of assets`);
1010
-
1011
941
  // Upload this batch of files
1012
942
  const uploadResponse = await api.post(
1013
943
  `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
@@ -0,0 +1,79 @@
1
+ import type { CloudflareApi } from "./api";
2
+ import { handleApiError } from "./api-error";
3
+
4
+ export interface WorkflowProps {
5
+ /**
6
+ * Name of the workflow
7
+ *
8
+ * @maxLength 64
9
+ * @minLength 1
10
+ * @default - className if provided, otherwise id
11
+ */
12
+ workflowName?: string;
13
+ /**
14
+ * Name of the class that implements the workflow
15
+ *
16
+ * @maxLength 255
17
+ * @minLength 1
18
+ * @default - workflowName if provided, otherwise id
19
+ */
20
+ className?: string;
21
+ }
22
+
23
+ export class Workflow<PARAMS = unknown> {
24
+ public readonly type: "workflow" = "workflow";
25
+ /**
26
+ * Phantom property to preserve workflow params at the type level.
27
+ *
28
+ * No value exists.
29
+ */
30
+ public readonly _PARAMS: PARAMS = undefined!;
31
+
32
+ public readonly workflowName: string;
33
+ public readonly className: string;
34
+
35
+ constructor(
36
+ public readonly id: string,
37
+ props: WorkflowProps = {}
38
+ ) {
39
+ this.workflowName = props.workflowName ?? props.className ?? id;
40
+ this.className = props.className ?? this.workflowName;
41
+ }
42
+ }
43
+
44
+ export interface WorkflowMetadata {
45
+ id: string; // uuid
46
+ class_name: string;
47
+ created_on: string; // date-time
48
+ modified_on: string; // date-time
49
+ name: string; // maxLength: 64, minLength: 1
50
+ script_name: string;
51
+ triggered_on: string; // date-time
52
+ version_id: string; // uuid
53
+ }
54
+
55
+ export async function upsertWorkflow(
56
+ api: CloudflareApi,
57
+ props: WorkflowProps & {
58
+ workflowName: string;
59
+ scriptName: string;
60
+ }
61
+ ) {
62
+ const response = await api.put(
63
+ `/accounts/${api.accountId}/workflows/${props.workflowName}`,
64
+ {
65
+ class_name: props.className,
66
+ script_name: props.scriptName,
67
+ }
68
+ );
69
+
70
+ if (!response.ok) {
71
+ await handleApiError(response, "create", "workflow", props.workflowName);
72
+ }
73
+
74
+ const body = (await response.json()) as {
75
+ result: WorkflowMetadata;
76
+ };
77
+
78
+ return body.result;
79
+ }
@@ -1,6 +1,6 @@
1
1
  import type { Context } from "../context";
2
2
  import { Resource } from "../resource";
3
- import { createCloudflareApi } from "./api";
3
+ import { createCloudflareApi, type CloudflareApiOptions } from "./api";
4
4
  import type {
5
5
  AlwaysUseHTTPSValue,
6
6
  AutomaticHTTPSRewritesValue,
@@ -24,18 +24,12 @@ import type {
24
24
  /**
25
25
  * Properties for creating or updating a Zone
26
26
  */
27
- export interface ZoneProps {
27
+ export interface ZoneProps extends CloudflareApiOptions {
28
28
  /**
29
29
  * The domain name for the zone
30
30
  */
31
31
  name: string;
32
32
 
33
- /**
34
- * Account ID to use for the zone
35
- * If not provided, will use the default account
36
- */
37
- accountId?: string;
38
-
39
33
  /**
40
34
  * The type of zone to create
41
35
  * "full" - Full zone implies that DNS is hosted with Cloudflare
@@ -305,12 +299,10 @@ export const Zone = Resource(
305
299
  async function (
306
300
  this: Context<Zone>,
307
301
  id: string,
308
- props: ZoneProps,
302
+ props: ZoneProps
309
303
  ): Promise<Zone> {
310
304
  // Create Cloudflare API client with automatic account discovery
311
- const api = await createCloudflareApi({
312
- accountId: props.accountId,
313
- });
305
+ const api = await createCloudflareApi(props);
314
306
 
315
307
  if (this.phase === "delete") {
316
308
  if (this.output?.id) {
@@ -322,7 +314,7 @@ export const Zone = Resource(
322
314
  errors: [{ message: deleteResponse.statusText }],
323
315
  }));
324
316
  throw new Error(
325
- `Error deleting zone '${props.name}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`,
317
+ `Error deleting zone '${props.name}': ${errorData.errors?.[0]?.message || deleteResponse.statusText}`
326
318
  );
327
319
  }
328
320
  } else {
@@ -337,7 +329,7 @@ export const Zone = Resource(
337
329
 
338
330
  if (!response.ok) {
339
331
  throw new Error(
340
- `Error getting zone '${props.name}': ${response.statusText}`,
332
+ `Error getting zone '${props.name}': ${response.statusText}`
341
333
  );
342
334
  }
343
335
 
@@ -385,13 +377,13 @@ export const Zone = Resource(
385
377
  if (response.status === 400 && body.includes("already exists")) {
386
378
  // Zone already exists, fetch it instead
387
379
  console.warn(
388
- `Zone '${props.name}' already exists during Zone create, adopting it...`,
380
+ `Zone '${props.name}' already exists during Zone create, adopting it...`
389
381
  );
390
382
  const getResponse = await api.get(`/zones?name=${props.name}`);
391
383
 
392
384
  if (!getResponse.ok) {
393
385
  throw new Error(
394
- `Error fetching existing zone '${props.name}': ${getResponse.statusText}`,
386
+ `Error fetching existing zone '${props.name}': ${getResponse.statusText}`
395
387
  );
396
388
  }
397
389
 
@@ -400,13 +392,13 @@ export const Zone = Resource(
400
392
  ).result;
401
393
  if (zones.length === 0) {
402
394
  throw new Error(
403
- `Zone '${props.name}' does not exist, but the name is reserved for another user.`,
395
+ `Zone '${props.name}' does not exist, but the name is reserved for another user.`
404
396
  );
405
397
  }
406
398
  zoneData = zones[0];
407
399
  } else {
408
400
  throw new Error(
409
- `Error creating zone '${props.name}': ${response.statusText}\n${body}`,
401
+ `Error creating zone '${props.name}': ${response.statusText}\n${body}`
410
402
  );
411
403
  }
412
404
  } else {
@@ -437,7 +429,7 @@ export const Zone = Resource(
437
429
  settings: await getZoneSettings(api, zoneData.id),
438
430
  });
439
431
  }
440
- },
432
+ }
441
433
  );
442
434
 
443
435
  /**
@@ -446,7 +438,7 @@ export const Zone = Resource(
446
438
  async function updateZoneSettings(
447
439
  api: any,
448
440
  zoneId: string,
449
- settings: ZoneProps["settings"],
441
+ settings: ZoneProps["settings"]
450
442
  ): Promise<void> {
451
443
  if (!settings) return;
452
444
 
@@ -480,7 +472,7 @@ async function updateZoneSettings(
480
472
  `/zones/${zoneId}/settings/${settingId}`,
481
473
  {
482
474
  value,
483
- } as UpdateZoneSettingParams,
475
+ } as UpdateZoneSettingParams
484
476
  );
485
477
 
486
478
  if (!response.ok) {
@@ -490,10 +482,10 @@ async function updateZoneSettings(
490
482
  return;
491
483
  }
492
484
  throw new Error(
493
- `Failed to update zone setting ${key}: ${response.statusText}`,
485
+ `Failed to update zone setting ${key}: ${response.statusText}`
494
486
  );
495
487
  }
496
- }),
488
+ })
497
489
  );
498
490
  }
499
491
 
@@ -502,13 +494,13 @@ async function updateZoneSettings(
502
494
  */
503
495
  async function getZoneSettings(
504
496
  api: any,
505
- zoneId: string,
497
+ zoneId: string
506
498
  ): Promise<Zone["settings"]> {
507
499
  const settingsResponse = await api.get(`/zones/${zoneId}/settings`);
508
500
 
509
501
  if (!settingsResponse.ok) {
510
502
  throw new Error(
511
- `Failed to fetch zone settings: ${settingsResponse.status} ${settingsResponse.statusText}`,
503
+ `Failed to fetch zone settings: ${settingsResponse.status} ${settingsResponse.statusText}`
512
504
  );
513
505
  }
514
506
 
@@ -67,13 +67,6 @@ export async function Providers({
67
67
  .filter((dirent) => dirent.isDirectory() && !exclude.includes(dirent.name))
68
68
  .map((dirent) => path.join(dirent.parentPath, dirent.name));
69
69
 
70
- // For each provider, list all files
71
- if (filter === false) {
72
- return [];
73
- } else if (typeof filter === "number") {
74
- providers = providers.slice(0, filter);
75
- }
76
-
77
70
  if (parallel) {
78
71
  return await Promise.all(
79
72
  providers.map((provider) =>
@@ -152,7 +145,7 @@ async function generateProviderDocs({
152
145
  The ${provider} Service has the following resources:
153
146
  ${alchemy.files(files)}
154
147
 
155
- A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...).
148
+ A file is considered a "Resource" if it contains a const <ResourceName> = Resource(...) call or if it is a function that calls a Resource function, e.g. const TypeScriptFile = () => File(...), or it is the Workflow or DurableObject classes.
156
149
  A file is considered a "Client" if it exposes a wrapper around creating a SDK client or fetch.
157
150
  A file is considered a "Utility" if it contains utility functions that are not resources or clients.
158
151
  A file is considered a "Types" if it contains just type definitions and maybe helpers around working with those types.