alchemy 0.3.1 → 0.4.1

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 (113) hide show
  1. package/README.md +8 -3
  2. package/lib/ai/document.d.ts +12 -1
  3. package/lib/ai/document.js +10 -1
  4. package/lib/ai/index.d.ts +0 -2
  5. package/lib/ai/index.js +0 -2
  6. package/lib/alchemy.d.ts +5 -0
  7. package/lib/alchemy.js +44 -1
  8. package/lib/apply.js +17 -10
  9. package/lib/aws/account-id.d.ts +4 -1
  10. package/lib/aws/account-id.js +1 -1
  11. package/lib/aws/credentials.d.ts +5 -0
  12. package/lib/aws/credentials.js +0 -0
  13. package/lib/aws/oidc/oidc-provider.js +2 -2
  14. package/lib/aws/role.js +35 -23
  15. package/lib/cloudflare/account-api-token.d.ts +168 -0
  16. package/lib/cloudflare/account-api-token.js +179 -0
  17. package/lib/cloudflare/account-id.d.ts +1 -0
  18. package/lib/cloudflare/account-id.js +0 -0
  19. package/lib/cloudflare/api.d.ts +38 -1
  20. package/lib/cloudflare/api.js +55 -4
  21. package/lib/cloudflare/assets.d.ts +74 -0
  22. package/lib/cloudflare/assets.js +73 -0
  23. package/lib/cloudflare/bindings.d.ts +6 -1
  24. package/lib/cloudflare/bindings.js +6 -0
  25. package/lib/cloudflare/bucket.d.ts +118 -3
  26. package/lib/cloudflare/bucket.js +269 -89
  27. package/lib/cloudflare/custom-domain.d.ts +75 -0
  28. package/lib/cloudflare/custom-domain.js +154 -0
  29. package/lib/cloudflare/{dns.js → dns-records.js} +14 -1
  30. package/lib/cloudflare/index.d.ts +6 -1
  31. package/lib/cloudflare/index.js +6 -1
  32. package/lib/cloudflare/permission-groups.d.ts +78 -0
  33. package/lib/cloudflare/permission-groups.js +48 -0
  34. package/lib/cloudflare/r2-rest-state-store.d.ts +2 -1
  35. package/lib/cloudflare/r2-rest-state-store.js +55 -24
  36. package/lib/cloudflare/static-site.d.ts +17 -18
  37. package/lib/cloudflare/static-site.js +19 -10
  38. package/lib/cloudflare/worker.d.ts +14 -0
  39. package/lib/cloudflare/worker.js +150 -3
  40. package/lib/{util/encrypt.d.ts → encrypt.d.ts} +1 -1
  41. package/lib/{util/encrypt.js → encrypt.js} +1 -1
  42. package/lib/fs/file-system-state-store.js +1 -1
  43. package/lib/fs/file.d.ts +18 -0
  44. package/lib/fs/file.js +6 -0
  45. package/lib/internal/{providers.d.ts → docs/providers.d.ts} +10 -4
  46. package/lib/internal/docs/providers.js +196 -0
  47. package/lib/secret.d.ts +3 -0
  48. package/lib/secret.js +13 -0
  49. package/lib/{util/serde.d.ts → serde.d.ts} +1 -1
  50. package/lib/{util/serde.js → serde.js} +12 -4
  51. package/lib/test/bun.js +3 -2
  52. package/lib/util/sha256.d.ts +1 -0
  53. package/lib/util/sha256.js +4 -0
  54. package/lib/web/vitepress/config.d.ts +2 -1
  55. package/lib/web/vitepress/config.js +1 -0
  56. package/lib/web/vitepress/index.d.ts +1 -0
  57. package/lib/web/vitepress/index.js +1 -0
  58. package/lib/web/vitepress/process-front-matter-files.d.ts +18 -0
  59. package/lib/web/vitepress/process-front-matter-files.js +68 -0
  60. package/lib/web/vitepress/vitepress.js +3 -2
  61. package/package.json +8 -4
  62. package/src/ai/document.ts +26 -2
  63. package/src/ai/index.ts +0 -2
  64. package/src/alchemy.ts +56 -2
  65. package/src/apply.ts +21 -20
  66. package/src/aws/account-id.ts +6 -2
  67. package/src/aws/credentials.ts +6 -0
  68. package/src/aws/oidc/oidc-provider.ts +17 -17
  69. package/src/aws/role.ts +68 -54
  70. package/src/cloudflare/account-api-token.ts +365 -0
  71. package/src/cloudflare/account-id.ts +0 -0
  72. package/src/cloudflare/api.ts +88 -17
  73. package/src/cloudflare/assets.ts +155 -0
  74. package/src/cloudflare/bindings.ts +11 -2
  75. package/src/cloudflare/bucket.ts +493 -133
  76. package/src/cloudflare/custom-domain.ts +318 -0
  77. package/src/cloudflare/{dns.ts → dns-records.ts} +36 -22
  78. package/src/cloudflare/index.ts +6 -1
  79. package/src/cloudflare/permission-groups.ts +137 -0
  80. package/src/cloudflare/r2-rest-state-store.ts +87 -42
  81. package/src/cloudflare/static-site.ts +20 -32
  82. package/src/cloudflare/worker.ts +253 -5
  83. package/src/dns/import-dns.ts +4 -4
  84. package/src/{util/encrypt.ts → encrypt.ts} +7 -10
  85. package/src/fs/file-system-state-store.ts +5 -5
  86. package/src/fs/file.ts +29 -0
  87. package/src/internal/docs/providers.ts +281 -0
  88. package/src/secret.ts +16 -0
  89. package/src/{util/serde.ts → serde.ts} +16 -10
  90. package/src/test/bun.ts +8 -7
  91. package/src/util/sha256.ts +5 -0
  92. package/src/web/vitepress/config.ts +3 -1
  93. package/src/web/vitepress/index.ts +1 -0
  94. package/src/web/vitepress/process-front-matter-files.ts +98 -0
  95. package/src/web/vitepress/vitepress.ts +3 -2
  96. package/lib/ai/approve.d.ts +0 -99
  97. package/lib/ai/approve.js +0 -76
  98. package/lib/ai/review.d.ts +0 -122
  99. package/lib/ai/review.js +0 -101
  100. package/lib/internal/getting-started.d.ts +0 -21
  101. package/lib/internal/getting-started.js +0 -87
  102. package/lib/internal/index.d.ts +0 -3
  103. package/lib/internal/index.js +0 -3
  104. package/lib/internal/providers.js +0 -172
  105. package/lib/internal/tutorial.d.ts +0 -104
  106. package/lib/internal/tutorial.js +0 -251
  107. package/src/ai/approve.ts +0 -163
  108. package/src/ai/review.ts +0 -213
  109. package/src/internal/getting-started.ts +0 -115
  110. package/src/internal/index.ts +0 -3
  111. package/src/internal/providers.ts +0 -241
  112. package/src/internal/tutorial.ts +0 -392
  113. /package/lib/cloudflare/{dns.d.ts → dns-records.d.ts} +0 -0
@@ -1,7 +1,8 @@
1
1
  import type { Scope } from "../scope";
2
+ import type { Secret } from "../secret";
3
+ import { deserialize, serialize } from "../serde";
2
4
  import type { State, StateStore } from "../state";
3
5
  import { withExponentialBackoff } from "../util/retry";
4
- import { deserialize, serialize } from "../util/serde";
5
6
  import { type CloudflareApi, createCloudflareApi } from "./api";
6
7
 
7
8
  /**
@@ -23,7 +24,7 @@ export interface CloudflareR2StateStoreOptions {
23
24
  /**
24
25
  * API key to use (overrides CLOUDFLARE_API_KEY env var)
25
26
  */
26
- apiKey?: string;
27
+ apiKey?: Secret;
27
28
 
28
29
  /**
29
30
  * Account ID to use (overrides CLOUDFLARE_ACCOUNT_ID env var)
@@ -44,7 +45,7 @@ export class R2RestStateStore implements StateStore {
44
45
  private api: CloudflareApi;
45
46
  private prefix: string;
46
47
  private bucketName: string;
47
- private apiKey: string | undefined;
48
+ private apiKey: Secret | undefined;
48
49
  private accountId: string | undefined;
49
50
  private email: string | undefined;
50
51
  private initialized = false;
@@ -57,7 +58,7 @@ export class R2RestStateStore implements StateStore {
57
58
  */
58
59
  constructor(
59
60
  public readonly scope: Scope,
60
- options: CloudflareR2StateStoreOptions,
61
+ options: CloudflareR2StateStoreOptions
61
62
  ) {
62
63
  // Use the scope's chain to build the prefix, similar to how FileSystemStateStore builds its directory
63
64
  const scopePath = scope.chain.join("/");
@@ -122,16 +123,30 @@ export class R2RestStateStore implements StateStore {
122
123
  }
123
124
 
124
125
  const listPath = `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects?${params.toString()}`;
125
- const response = await this.api.get(listPath);
126
-
127
- if (!response.ok) {
128
- const errorData: any = await response.json().catch(() => ({
129
- errors: [{ message: response.statusText }],
130
- }));
131
- throw new Error(
132
- `Error listing R2 objects: ${errorData.errors?.[0]?.message || response.statusText}`,
133
- );
134
- }
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
+ );
135
150
 
136
151
  const data = (await response.json()) as any;
137
152
 
@@ -144,7 +159,7 @@ export class R2RestStateStore implements StateStore {
144
159
  objects.map((obj: any) => {
145
160
  const keyName = obj.key || obj.name;
146
161
  return this.convertKeyFromStorage(keyName.slice(this.prefix.length));
147
- }),
162
+ })
148
163
  );
149
164
 
150
165
  // Update cursor for next page if available
@@ -175,21 +190,38 @@ export class R2RestStateStore implements StateStore {
175
190
  await this.ensureInitialized();
176
191
 
177
192
  try {
178
- const response = await this.api.get(
179
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
180
- );
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
+ );
181
198
 
182
- if (!response.ok) {
183
- if (response.status === 404) {
184
- return undefined;
185
- }
199
+ if (!response.ok) {
200
+ if (response.status === 404) {
201
+ return response;
202
+ }
186
203
 
187
- const errorData: any = await response.json().catch(() => ({
188
- errors: [{ message: response.statusText }],
189
- }));
190
- throw new Error(
191
- `Error getting R2 object: ${errorData.errors?.[0]?.message || response.statusText}`,
192
- );
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
+ }
211
+
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;
193
225
  }
194
226
 
195
227
  // Parse and deserialize the state data
@@ -267,7 +299,7 @@ export class R2RestStateStore implements StateStore {
267
299
  headers: {
268
300
  "Content-Type": "application/json",
269
301
  },
270
- },
302
+ }
271
303
  );
272
304
 
273
305
  if (!response.ok) {
@@ -275,7 +307,7 @@ export class R2RestStateStore implements StateStore {
275
307
  errors: [{ message: response.statusText }],
276
308
  }));
277
309
  throw new Error(
278
- `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`,
310
+ `Error writing to R2: ${errorData.errors?.[0]?.message || response.statusText}`
279
311
  );
280
312
  }
281
313
 
@@ -285,7 +317,7 @@ export class R2RestStateStore implements StateStore {
285
317
  (error) =>
286
318
  error.message?.includes("503") || error.message?.includes("timeout"),
287
319
  5, // 5 retry attempts
288
- 1000, // Start with 1 second delay
320
+ 1000 // Start with 1 second delay
289
321
  );
290
322
  }
291
323
 
@@ -297,18 +329,31 @@ export class R2RestStateStore implements StateStore {
297
329
  async delete(key: string): Promise<void> {
298
330
  await this.ensureInitialized();
299
331
 
300
- const response = await this.api.delete(
301
- `/accounts/${this.api.accountId}/r2/buckets/${this.bucketName}/objects/${this.getObjectKey(key)}`,
302
- );
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
+ );
303
337
 
304
- if (!response.ok && response.status !== 404) {
305
- const errorData: any = await response.json().catch(() => ({
306
- errors: [{ message: response.statusText }],
307
- }));
308
- throw new Error(
309
- `Error deleting from R2: ${errorData.errors?.[0]?.message || response.statusText}`,
310
- );
311
- }
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
+ );
312
357
  }
313
358
 
314
359
  /**
@@ -96,16 +96,6 @@ export interface StaticSiteProps {
96
96
  */
97
97
  production?: boolean;
98
98
 
99
- /**
100
- * Custom domain for the site
101
- */
102
- domain?:
103
- | string
104
- | {
105
- name: string;
106
- redirects?: string[];
107
- };
108
-
109
99
  build?: {
110
100
  /**
111
101
  * Command to run before deploying the site
@@ -176,16 +166,6 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
176
166
  */
177
167
  assets: string[];
178
168
 
179
- /**
180
- * Custom domain for the site
181
- */
182
- domain?:
183
- | string
184
- | {
185
- name: string;
186
- redirects?: string[];
187
- };
188
-
189
169
  /**
190
170
  * Whether the site is deployed to production
191
171
  */
@@ -243,13 +223,26 @@ export interface StaticSite extends Resource<"cloudflare::StaticSite"> {
243
223
  * });
244
224
  *
245
225
  * @example
246
- * // Create a static site with custom error page and index
247
- * const customSite = await StaticSite("custom-site", {
226
+ * // Create a static site with a custom domain using the CustomDomain resource
227
+ * const site = await StaticSite("custom-site", {
248
228
  * name: "custom-site",
249
229
  * dir: "./www",
250
230
  * errorPage: "404.html",
251
- * indexPage: "home.html",
252
- * domain: "www.example.com"
231
+ * indexPage: "home.html"
232
+ * });
233
+ *
234
+ * // Then configure the custom domain separately
235
+ * const domain = await CustomDomain("custom-domain", {
236
+ * name: "www.example.com",
237
+ * zoneId: "abcdef123456789",
238
+ * workerName: site.name
239
+ * });
240
+ *
241
+ * @example
242
+ * // Create a static site with multiple domains (primary + redirects)
243
+ * const site = await StaticSite("multi-domain-site", {
244
+ * name: "multi-domain-site",
245
+ * dir: "./public"
253
246
  * });
254
247
  *
255
248
  * @see https://developers.cloudflare.com/workers/platform/sites
@@ -264,6 +257,9 @@ export const StaticSite = Resource(
264
257
  id: string,
265
258
  props: StaticSiteProps
266
259
  ) {
260
+ // Create Cloudflare API client with automatic account discovery
261
+ const api = await createCloudflareApi();
262
+
267
263
  if (this.phase === "delete") {
268
264
  // For delete operations, we'll rely on the Worker delete to clean up
269
265
  // Return empty output for deleted state
@@ -322,9 +318,6 @@ export const StaticSite = Resource(
322
318
  const siteName = props.name;
323
319
  const indexPage = props.indexPage || "index.html";
324
320
 
325
- // Create Cloudflare API client with automatic account discovery
326
- const api = await createCloudflareApi();
327
-
328
321
  // Step 1: Create or get the KV namespace for assets
329
322
  const [kv, assetManifest] = await Promise.all([
330
323
  KVNamespace("assets", {
@@ -333,10 +326,6 @@ export const StaticSite = Resource(
333
326
  generateAssetManifest(props.dir),
334
327
  ]);
335
328
 
336
- console.log({
337
- assetManifest,
338
- });
339
-
340
329
  // Step 3: Upload assets to KV
341
330
  await uploadAssetManifest(api, kv.namespaceId, assetManifest);
342
331
 
@@ -419,7 +408,6 @@ export const StaticSite = Resource(
419
408
  assets: assetManifest.map((item) => item.key),
420
409
  createdAt: this.output?.createdAt || now,
421
410
  updatedAt: now,
422
- domain: props.domain,
423
411
  production: props.production !== false,
424
412
  url: worker.url,
425
413
  routes,
@@ -1,14 +1,18 @@
1
+ import * as crypto from "crypto";
1
2
  import * as fs from "fs/promises";
2
3
  import type { Context } from "../context";
3
4
  import { Bundle, type BundleProps } from "../esbuild/bundle";
4
5
  import { Resource } from "../resource";
5
6
  import { isSecret } from "../secret";
7
+ import { getContentType } from "../util/content-type";
6
8
  import { withExponentialBackoff } from "../util/retry";
7
9
  import { slugify } from "../util/slugify";
8
10
  import { type CloudflareApi, createCloudflareApi } from "./api";
11
+ import type { Assets } from "./assets";
9
12
  import {
10
13
  type Bindings,
11
14
  type WorkerBindingSpec,
15
+ isAssets,
12
16
  isDurableObjectNamespace,
13
17
  } from "./bindings";
14
18
  import type { Bound } from "./bound";
@@ -200,6 +204,20 @@ export interface Worker<B extends Bindings = Bindings>
200
204
  * }
201
205
  * });
202
206
  *
207
+ * @example
208
+ * // Create a worker with static assets:
209
+ * const staticAssets = await Assets("static", {
210
+ * path: "./src/assets"
211
+ * });
212
+ *
213
+ * const frontendWorker = await Worker("frontend", {
214
+ * name: "frontend-worker",
215
+ * entrypoint: "./src/worker.ts",
216
+ * bindings: {
217
+ * ASSETS: staticAssets
218
+ * }
219
+ * });
220
+ *
203
221
  * @see https://developers.cloudflare.com/workers/
204
222
  */
205
223
  export const Worker = Resource(
@@ -232,15 +250,42 @@ export const Worker = Resource(
232
250
 
233
251
  const oldBindings = await this.get<Bindings>("bindings");
234
252
 
253
+ // Get the script content - either from props.script, or by bundling
254
+ const scriptContent = props.script ?? (await bundleWorkerScript(props));
255
+
256
+ // Find any assets bindings
257
+ const assetsBindings: { name: string; assets: Assets }[] = [];
258
+ if (props.bindings) {
259
+ for (const [bindingName, binding] of Object.entries(props.bindings)) {
260
+ if (isAssets(binding)) {
261
+ assetsBindings.push({ name: bindingName, assets: binding });
262
+ }
263
+ }
264
+ }
265
+
266
+ // Upload any assets and get completion tokens
267
+ let assetUploadResult: AssetUploadResult | undefined;
268
+ if (assetsBindings.length > 0) {
269
+ // We'll use the first asset binding for now
270
+ // In the future, we might want to support multiple asset bindings
271
+ const assetBinding = assetsBindings[0];
272
+
273
+ // Upload the assets and get the completion token
274
+ assetUploadResult = await uploadAssets(
275
+ api,
276
+ workerName,
277
+ assetBinding.assets
278
+ );
279
+ }
280
+
281
+ // Prepare metadata with bindings
235
282
  const scriptMetadata = await prepareWorkerMetadata(
236
283
  this,
237
284
  oldBindings,
238
- props
285
+ props,
286
+ assetUploadResult
239
287
  );
240
288
 
241
- // Get the script content - either from props.script, or by bundling
242
- const scriptContent = props.script ?? (await bundleWorkerScript(props));
243
-
244
289
  // Upload the worker script
245
290
  await putWorker(api, workerName, scriptContent, scriptMetadata);
246
291
 
@@ -437,12 +482,29 @@ interface WorkerMetadata {
437
482
  main_module?: string;
438
483
  body_part?: string;
439
484
  tags?: string[];
485
+ assets?: {
486
+ jwt?: string;
487
+ keep_assets?: boolean;
488
+ config?: {
489
+ html_handling?: "auto-trailing-slash" | "none";
490
+ not_found_handling?: "none" | "fall-through";
491
+ };
492
+ };
493
+ }
494
+
495
+ interface AssetUploadResult {
496
+ completionToken: string;
497
+ assetConfig?: {
498
+ html_handling?: "auto-trailing-slash" | "none";
499
+ not_found_handling?: "none" | "fall-through";
500
+ };
440
501
  }
441
502
 
442
503
  async function prepareWorkerMetadata<B extends Bindings>(
443
504
  ctx: Context<Worker<B>>,
444
505
  oldBindings: Bindings | undefined,
445
- props: WorkerProps
506
+ props: WorkerProps,
507
+ assetUploadResult?: AssetUploadResult
446
508
  ): Promise<WorkerMetadata> {
447
509
  // Prepare metadata with bindings
448
510
  const meta: WorkerMetadata = {
@@ -461,6 +523,17 @@ async function prepareWorkerMetadata<B extends Bindings>(
461
523
  },
462
524
  };
463
525
 
526
+ // If we have asset upload results, add them to the metadata
527
+ if (assetUploadResult) {
528
+ meta.assets = {
529
+ jwt: assetUploadResult.completionToken,
530
+ };
531
+
532
+ if (assetUploadResult.assetConfig) {
533
+ meta.assets.config = assetUploadResult.assetConfig;
534
+ }
535
+ }
536
+
464
537
  const bindings = (props.bindings ?? {}) as Bindings;
465
538
 
466
539
  // Convert bindings to the format expected by the API
@@ -522,6 +595,11 @@ async function prepareWorkerMetadata<B extends Bindings>(
522
595
  name: bindingName,
523
596
  bucket_name: binding.name,
524
597
  });
598
+ } else if (isAssets(binding)) {
599
+ meta.bindings.push({
600
+ type: "assets",
601
+ name: bindingName,
602
+ });
525
603
  } else if (isSecret(binding)) {
526
604
  meta.bindings.push({
527
605
  type: "secret_text",
@@ -802,3 +880,173 @@ async function getWorkerBindings(
802
880
 
803
881
  return data.result;
804
882
  }
883
+
884
+ /**
885
+ * Interface for a file's metadata to be uploaded
886
+ */
887
+ interface FileMetadata {
888
+ hash: string;
889
+ size: number;
890
+ }
891
+
892
+ /**
893
+ * Response from the assets upload session API
894
+ */
895
+ interface UploadSessionResponse {
896
+ result: {
897
+ jwt: string;
898
+ buckets: string[][];
899
+ };
900
+ success: boolean;
901
+ errors: any[];
902
+ messages: any[];
903
+ }
904
+
905
+ /**
906
+ * Response from the file upload API
907
+ */
908
+ interface UploadResponse {
909
+ result: {
910
+ jwt: string;
911
+ buckets?: string[][];
912
+ };
913
+ success: boolean;
914
+ errors: any[];
915
+ messages: any[];
916
+ }
917
+
918
+ /**
919
+ * Uploads assets to Cloudflare and returns a completion token
920
+ *
921
+ * @param api CloudflareApi instance
922
+ * @param workerName Name of the worker
923
+ * @param assets Assets resource containing files to upload
924
+ * @returns Completion token for the assets upload
925
+ */
926
+ async function uploadAssets(
927
+ api: CloudflareApi,
928
+ workerName: string,
929
+ assets: Assets
930
+ ): Promise<AssetUploadResult> {
931
+ // Generate the file manifest
932
+ const fileMetadata: Record<string, FileMetadata> = {};
933
+
934
+ // Process each file in the assets
935
+ for (const file of assets.files) {
936
+ const { hash, size } = await calculateFileMetadata(file.filePath);
937
+ // Use the relative path as the key, ensuring it starts with a slash
938
+ const key = file.path.startsWith("/") ? file.path : `/${file.path}`;
939
+ fileMetadata[key] = { hash, size };
940
+ }
941
+
942
+ // Start the upload session
943
+ const uploadSessionUrl = `/accounts/${api.accountId}/workers/scripts/${workerName}/assets-upload-session`;
944
+ const uploadSessionResponse = await api.post(
945
+ uploadSessionUrl,
946
+ JSON.stringify({ manifest: fileMetadata }),
947
+ {
948
+ headers: { "Content-Type": "application/json" },
949
+ }
950
+ );
951
+
952
+ if (!uploadSessionResponse.ok) {
953
+ throw new Error(
954
+ `Failed to start assets upload session: ${uploadSessionResponse.status} ${uploadSessionResponse.statusText}`
955
+ );
956
+ }
957
+
958
+ const sessionData =
959
+ (await uploadSessionResponse.json()) as UploadSessionResponse;
960
+
961
+ // If there are no buckets, assets are already uploaded or empty
962
+ if (!sessionData.result.buckets || sessionData.result.buckets.length === 0) {
963
+ return { completionToken: sessionData.result.jwt };
964
+ }
965
+
966
+ // Upload the files in batches as specified by the API
967
+ let completionToken = sessionData.result.jwt;
968
+ const buckets = sessionData.result.buckets;
969
+
970
+ // Process each bucket of files
971
+ for (const bucket of buckets) {
972
+ const formData = new FormData();
973
+
974
+ // Add each file in the bucket to the form
975
+ for (const fileHash of bucket) {
976
+ // Find the file with this hash
977
+ const file = assets.files.find((f) => {
978
+ const filePath = f.path.startsWith("/") ? f.path : `/${f.path}`;
979
+ return fileMetadata[filePath]?.hash === fileHash;
980
+ });
981
+
982
+ if (!file) {
983
+ throw new Error(`Could not find file with hash ${fileHash}`);
984
+ }
985
+
986
+ // Read the file content
987
+ const fileContent = await fs.readFile(file.filePath);
988
+
989
+ // Convert to base64 as required by the API when using base64=true
990
+ const base64Content = fileContent.toString("base64");
991
+
992
+ // Add the file to the form with the hash as the key and set the correct content type
993
+ const blob = new Blob([base64Content], {
994
+ type: getContentType(file.filePath),
995
+ });
996
+ formData.append(fileHash, blob, fileHash);
997
+ }
998
+
999
+ // Upload this batch of files
1000
+ const uploadResponse = await api.post(
1001
+ `/accounts/${api.accountId}/workers/assets/upload?base64=true`,
1002
+ formData,
1003
+ {
1004
+ headers: {
1005
+ Authorization: `Bearer ${completionToken}`,
1006
+ "Content-Type": "multipart/form-data",
1007
+ },
1008
+ }
1009
+ );
1010
+
1011
+ if (!uploadResponse.ok) {
1012
+ throw new Error(
1013
+ `Failed to upload asset files: ${uploadResponse.status} ${uploadResponse.statusText}`
1014
+ );
1015
+ }
1016
+
1017
+ const uploadData = (await uploadResponse.json()) as UploadResponse;
1018
+ // Update the completion token for the next batch
1019
+ if (uploadData.result.jwt) {
1020
+ completionToken = uploadData.result.jwt;
1021
+ }
1022
+ }
1023
+
1024
+ // Return the final completion token
1025
+ return {
1026
+ completionToken,
1027
+ assetConfig: {
1028
+ html_handling: "auto-trailing-slash",
1029
+ },
1030
+ };
1031
+ }
1032
+
1033
+ /**
1034
+ * Calculate the SHA-256 hash and size of a file
1035
+ *
1036
+ * @param filePath Path to the file
1037
+ * @returns Hash (first 32 chars of SHA-256) and size of the file
1038
+ */
1039
+ async function calculateFileMetadata(
1040
+ filePath: string
1041
+ ): Promise<{ hash: string; size: number }> {
1042
+ const hash = crypto.createHash("sha256");
1043
+ const fileContent = await fs.readFile(filePath);
1044
+
1045
+ hash.update(fileContent);
1046
+ const fileHash = hash.digest("hex").substring(0, 32); // First 32 chars of hash
1047
+
1048
+ return {
1049
+ hash: fileHash,
1050
+ size: fileContent.length,
1051
+ };
1052
+ }
@@ -134,7 +134,7 @@ export const ImportDnsRecords = Resource(
134
134
  async function (
135
135
  this: Context<ImportDnsRecords>,
136
136
  id: string,
137
- props: ImportDnsRecordsProps,
137
+ props: ImportDnsRecordsProps
138
138
  ): Promise<ImportDnsRecords> {
139
139
  // For delete phase, just return destroyed state since this is a read-only resource
140
140
  if (this.phase === "delete") {
@@ -152,7 +152,7 @@ export const ImportDnsRecords = Resource(
152
152
  headers: {
153
153
  accept: "application/dns-json",
154
154
  },
155
- },
155
+ }
156
156
  );
157
157
 
158
158
  if (!res.ok) {
@@ -197,7 +197,7 @@ export const ImportDnsRecords = Resource(
197
197
  } catch (error) {
198
198
  console.warn(
199
199
  `Failed to fetch ${type} records for ${props.domain}:`,
200
- error,
200
+ error
201
201
  );
202
202
  }
203
203
  }
@@ -209,5 +209,5 @@ export const ImportDnsRecords = Resource(
209
209
  records: allRecords,
210
210
  importedAt: Date.now(),
211
211
  });
212
- },
212
+ }
213
213
  );