alchemy 0.29.2 → 0.30.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 (57) hide show
  1. package/bin/alchemy.mjs +16086 -0
  2. package/bin/alchemy.ts +67 -0
  3. package/bin/create-alchemy.ts +1208 -0
  4. package/lib/cloudflare/api-error.d.ts.map +1 -1
  5. package/lib/cloudflare/api-error.js +2 -1
  6. package/lib/cloudflare/api-error.js.map +1 -1
  7. package/lib/cloudflare/bindings.d.ts +16 -3
  8. package/lib/cloudflare/bindings.d.ts.map +1 -1
  9. package/lib/cloudflare/bound.d.ts +3 -7
  10. package/lib/cloudflare/bound.d.ts.map +1 -1
  11. package/lib/cloudflare/bundle/bundle-worker.d.ts +1 -0
  12. package/lib/cloudflare/bundle/bundle-worker.d.ts.map +1 -1
  13. package/lib/cloudflare/bundle/bundle-worker.js +16 -5
  14. package/lib/cloudflare/bundle/bundle-worker.js.map +1 -1
  15. package/lib/cloudflare/nuxt.d.ts.map +1 -1
  16. package/lib/cloudflare/nuxt.js +2 -1
  17. package/lib/cloudflare/nuxt.js.map +1 -1
  18. package/lib/cloudflare/redwood.d.ts.map +1 -1
  19. package/lib/cloudflare/redwood.js +1 -0
  20. package/lib/cloudflare/redwood.js.map +1 -1
  21. package/lib/cloudflare/secret.d.ts +4 -0
  22. package/lib/cloudflare/secret.d.ts.map +1 -1
  23. package/lib/cloudflare/secret.js +46 -7
  24. package/lib/cloudflare/secret.js.map +1 -1
  25. package/lib/cloudflare/secrets-store.d.ts +1 -7
  26. package/lib/cloudflare/secrets-store.d.ts.map +1 -1
  27. package/lib/cloudflare/secrets-store.js +2 -9
  28. package/lib/cloudflare/secrets-store.js.map +1 -1
  29. package/lib/cloudflare/worker-metadata.js +4 -4
  30. package/lib/cloudflare/worker-metadata.js.map +1 -1
  31. package/lib/cloudflare/worker.d.ts +6 -0
  32. package/lib/cloudflare/worker.d.ts.map +1 -1
  33. package/lib/cloudflare/worker.js +1 -0
  34. package/lib/cloudflare/worker.js.map +1 -1
  35. package/lib/cloudflare/wrangler.json.js +6 -10
  36. package/lib/cloudflare/wrangler.json.js.map +1 -1
  37. package/lib/secret.d.ts +5 -0
  38. package/lib/secret.d.ts.map +1 -1
  39. package/lib/secret.js +1 -1
  40. package/lib/secret.js.map +1 -1
  41. package/lib/util/content-type.d.ts.map +1 -1
  42. package/lib/util/content-type.js +24 -20
  43. package/lib/util/content-type.js.map +1 -1
  44. package/package.json +11 -5
  45. package/src/cloudflare/api-error.ts +4 -1
  46. package/src/cloudflare/bindings.ts +17 -2
  47. package/src/cloudflare/bound.ts +15 -23
  48. package/src/cloudflare/bundle/bundle-worker.ts +26 -5
  49. package/src/cloudflare/nuxt.ts +2 -1
  50. package/src/cloudflare/redwood.ts +1 -0
  51. package/src/cloudflare/secret.ts +87 -7
  52. package/src/cloudflare/secrets-store.ts +3 -20
  53. package/src/cloudflare/worker-metadata.ts +4 -4
  54. package/src/cloudflare/worker.ts +8 -0
  55. package/src/cloudflare/wrangler.json.ts +6 -10
  56. package/src/secret.ts +7 -1
  57. package/src/util/content-type.ts +24 -20
@@ -74,6 +74,11 @@ export function isSecret(resource: Resource): resource is Secret {
74
74
  export interface Secret
75
75
  extends Resource<"cloudflare::Secret">,
76
76
  Omit<_SecretProps, "delete"> {
77
+ /**
78
+ * The binding type for Cloudflare Workers
79
+ */
80
+ type: "secrets_store_secret";
81
+
77
82
  /**
78
83
  * The name of the secret
79
84
  */
@@ -190,6 +195,7 @@ const _Secret = Resource(
190
195
  await insertSecret(api, props.store.id, name, props.value);
191
196
 
192
197
  return this({
198
+ type: "secrets_store_secret",
193
199
  name,
194
200
  storeId: props.store.id,
195
201
  store: props.store,
@@ -209,24 +215,98 @@ export async function insertSecret(
209
215
  secretName: string,
210
216
  secretValue: AlchemySecret,
211
217
  ): Promise<void> {
212
- const response = await api.post(
218
+ // First try to create the secret
219
+ const createResponse = await api.post(
213
220
  `/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets`,
214
221
  [
215
222
  {
216
223
  name: secretName,
217
224
  value: secretValue.unencrypted,
218
- scopes: [],
225
+ scopes: ["workers"],
219
226
  },
220
227
  ],
221
228
  );
222
229
 
230
+ if (createResponse.ok) {
231
+ return; // Secret created successfully
232
+ }
233
+
234
+ // If creation failed, check if it's because the secret already exists
235
+ const createErrorData: any = await createResponse.json().catch(() => ({
236
+ errors: [{ message: createResponse.statusText }],
237
+ }));
238
+
239
+ const isAlreadyExists = createErrorData.errors?.some(
240
+ (error: any) =>
241
+ error.code === 10021 ||
242
+ error.message?.includes("secret_name_already_exists"),
243
+ );
244
+
245
+ if (isAlreadyExists) {
246
+ // Secret already exists, find its ID and update it
247
+ const secretId = await getSecretId(api, storeId, secretName);
248
+ if (!secretId) {
249
+ throw new Error(`Secret '${secretName}' not found in store`);
250
+ }
251
+
252
+ const updateResponse = await api.patch(
253
+ `/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets/${secretId}`,
254
+ {
255
+ value: secretValue.unencrypted,
256
+ scopes: ["workers"],
257
+ },
258
+ );
259
+
260
+ if (!updateResponse.ok) {
261
+ const updateErrorData: any = await updateResponse.json().catch(() => ({
262
+ errors: [{ message: updateResponse.statusText }],
263
+ }));
264
+ const updateErrorMessage =
265
+ updateErrorData.errors?.[0]?.message || updateResponse.statusText;
266
+ throw new Error(
267
+ `Error updating secret '${secretName}': ${updateErrorMessage}`,
268
+ );
269
+ }
270
+ } else {
271
+ // Some other error occurred during creation
272
+ const createErrorMessage =
273
+ createErrorData.errors?.[0]?.message || createResponse.statusText;
274
+ throw new Error(
275
+ `Error creating secret '${secretName}': ${createErrorMessage}`,
276
+ );
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Get the ID of a secret by its name
282
+ */
283
+ async function getSecretId(
284
+ api: CloudflareApi,
285
+ storeId: string,
286
+ secretName: string,
287
+ ): Promise<string | null> {
288
+ const response = await api.get(
289
+ `/accounts/${api.accountId}/secrets_store/stores/${storeId}/secrets`,
290
+ );
291
+
223
292
  if (!response.ok) {
224
- const errorData: any = await response.json().catch(() => ({
225
- errors: [{ message: response.statusText }],
226
- }));
227
- const errorMessage = errorData.errors?.[0]?.message || response.statusText;
228
- throw new Error(`Error creating secret '${secretName}': ${errorMessage}`);
293
+ return null;
229
294
  }
295
+
296
+ const data = (await response.json()) as {
297
+ result: Array<{
298
+ id: string;
299
+ name: string;
300
+ created: string;
301
+ modified: string;
302
+ status: string;
303
+ }>;
304
+ success: boolean;
305
+ errors: any[];
306
+ };
307
+
308
+ const secret = data.result.find((s) => s.name === secretName);
309
+ return secret?.id || null;
230
310
  }
231
311
 
232
312
  /**
@@ -1,6 +1,5 @@
1
1
  import type { Context } from "../context.ts";
2
2
  import { Resource, ResourceKind } from "../resource.ts";
3
- import { bind } from "../runtime/bind.ts";
4
3
  import { secret, type Secret } from "../secret.ts";
5
4
  import { handleApiError } from "./api-error.ts";
6
5
  import {
@@ -8,7 +7,6 @@ import {
8
7
  type CloudflareApi,
9
8
  type CloudflareApiOptions,
10
9
  } from "./api.ts";
11
- import type { Bound } from "./bound.ts";
12
10
 
13
11
  /**
14
12
  * Properties for creating or updating a Secrets Store
@@ -66,11 +64,6 @@ export interface SecretsStore<
66
64
  S extends Record<string, Secret> | undefined = undefined,
67
65
  > extends Resource<"cloudflare::SecretsStore">,
68
66
  Omit<SecretsStoreProps<S>, "delete"> {
69
- /**
70
- * The binding type for Cloudflare Workers
71
- */
72
- type: "secrets_store";
73
-
74
67
  /**
75
68
  * The unique identifier of the secrets store
76
69
  */
@@ -98,10 +91,6 @@ export interface SecretsStore<
98
91
  modifiedAt: number;
99
92
  }
100
93
 
101
- export type SecretsStoreWithBinding<
102
- S extends Record<string, Secret> | undefined = undefined,
103
- > = SecretsStore<S> & Bound<SecretsStore<S>>;
104
-
105
94
  /**
106
95
  * A Cloudflare Secrets Store is a secure, centralized location for storing account-level secrets.
107
96
  *
@@ -162,7 +151,7 @@ export async function SecretsStore<
162
151
  >(
163
152
  name: string,
164
153
  props: SecretsStoreProps<S> = {} as SecretsStoreProps<S>,
165
- ): Promise<SecretsStoreWithBinding<S>> {
154
+ ): Promise<SecretsStore<S>> {
166
155
  // Convert string values to Secret instances
167
156
  const normalizedProps: SecretsStoreProps<S> = {
168
157
  ...props,
@@ -176,12 +165,7 @@ export async function SecretsStore<
176
165
  : undefined,
177
166
  };
178
167
 
179
- const store = await _SecretsStore(name, normalizedProps);
180
- const binding = await bind(store);
181
- return {
182
- ...store,
183
- get: binding.get,
184
- } as SecretsStoreWithBinding<S>;
168
+ return _SecretsStore(name, normalizedProps);
185
169
  }
186
170
 
187
171
  const _SecretsStore = Resource("cloudflare::SecretsStore", async function <
@@ -254,7 +238,6 @@ const _SecretsStore = Resource("cloudflare::SecretsStore", async function <
254
238
  }
255
239
 
256
240
  return this({
257
- type: "secrets_store",
258
241
  id: storeId,
259
242
  name: name,
260
243
  secrets: props.secrets as S,
@@ -345,7 +328,7 @@ export async function insertSecrets<
345
328
  const bulkPayload = batch.map(([name, secretValue]) => ({
346
329
  name,
347
330
  value: (secretValue as Secret).unencrypted,
348
- scopes: [],
331
+ scopes: ["workers"],
349
332
  }));
350
333
 
351
334
  const bulkResponse = await api.post(
@@ -387,12 +387,12 @@ export async function prepareWorkerMetadata<B extends Bindings>(
387
387
  name: bindingName,
388
388
  bucket_name: binding.name,
389
389
  });
390
- } else if (binding.type === "secrets_store") {
390
+ } else if (binding.type === "secrets_store_secret") {
391
391
  meta.bindings.push({
392
- type: "secrets_store",
392
+ type: "secrets_store_secret",
393
393
  name: bindingName,
394
- store_id: binding.id,
395
- secret_name: bindingName,
394
+ store_id: binding.storeId,
395
+ secret_name: binding.name,
396
396
  });
397
397
  } else if (binding.type === "assets") {
398
398
  meta.bindings.push({
@@ -288,6 +288,13 @@ export interface EntrypointWorkerProps<
288
288
  */
289
289
  noBundle?: boolean;
290
290
 
291
+ /**
292
+ * Whether to upload source maps for the worker script.
293
+ *
294
+ * @default false
295
+ */
296
+ uploadSourceMaps?: boolean;
297
+
291
298
  /**
292
299
  * Rules for adding additional files to the bundle.
293
300
  *
@@ -847,6 +854,7 @@ export const _Worker = Resource(
847
854
  props.script ??
848
855
  (await bundleWorkerScript({
849
856
  ...props,
857
+ name: workerName,
850
858
  compatibilityDate,
851
859
  compatibilityFlags,
852
860
  }));
@@ -596,16 +596,12 @@ function processBindings(
596
596
  });
597
597
  } else if (binding.type === "json") {
598
598
  // TODO(sam): anything to do here? not sure wrangler.json supports this
599
- } else if (binding.type === "secrets_store") {
600
- if (binding.secrets) {
601
- for (const [secretName, _secret] of Object.entries(binding.secrets)) {
602
- secretsStoreSecrets.push({
603
- binding: bindingName,
604
- store_id: binding.id,
605
- secret_name: secretName,
606
- });
607
- }
608
- }
599
+ } else if (binding.type === "secrets_store_secret") {
600
+ secretsStoreSecrets.push({
601
+ binding: bindingName,
602
+ store_id: binding.storeId,
603
+ secret_name: binding.name,
604
+ });
609
605
  } else if (binding.type === "dispatch_namespace") {
610
606
  dispatchNamespaces.push({
611
607
  binding: bindingName,
package/src/secret.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import { alchemy } from "./alchemy.ts";
2
2
 
3
+ declare global {
4
+ var __ALCHEMY_SECRETS__: {
5
+ [name: string]: Secret;
6
+ };
7
+ }
8
+
3
9
  // a global registry of all secrets that we will use when serializing an application
4
10
  const globalSecrets: {
5
11
  [name: string]: Secret;
6
- } = {};
12
+ } = (globalThis.__ALCHEMY_SECRETS__ ??= {});
7
13
 
8
14
  let i = 0;
9
15
  function nextName() {
@@ -2,34 +2,35 @@ import path from "node:path";
2
2
 
3
3
  // Common MIME types
4
4
  const mimeTypes: Record<string, string> = {
5
- ".html": "text/html",
6
- ".htm": "text/html",
7
5
  ".css": "text/css",
6
+ ".eot": "application/vnd.ms-fontobject",
7
+ ".gif": "image/gif",
8
+ ".htm": "text/html",
9
+ ".html": "text/html",
10
+ ".ico": "image/x-icon",
11
+ ".jpeg": "image/jpeg",
12
+ ".jpg": "image/jpeg",
13
+ ".js.map": "application/source-map",
8
14
  ".js": "application/javascript",
9
- ".mjs": "application/javascript+module",
10
15
  ".json": "application/json",
11
- ".xml": "application/xml",
12
- ".txt": "text/plain",
13
16
  ".md": "text/markdown",
17
+ ".mjs": "application/javascript+module",
18
+ ".mp3": "audio/mpeg",
19
+ ".mp4": "video/mp4",
20
+ ".otf": "font/otf",
21
+ ".pdf": "application/pdf",
14
22
  ".png": "image/png",
15
- ".jpg": "image/jpeg",
16
- ".jpeg": "image/jpeg",
17
- ".gif": "image/gif",
18
- ".webp": "image/webp",
19
23
  ".svg": "image/svg+xml",
20
- ".ico": "image/x-icon",
21
- ".pdf": "application/pdf",
22
- ".zip": "application/zip",
23
- ".woff": "font/woff",
24
- ".woff2": "font/woff2",
25
24
  ".ttf": "font/ttf",
26
- ".otf": "font/otf",
27
- ".eot": "application/vnd.ms-fontobject",
28
- ".mp4": "video/mp4",
29
- ".webm": "video/webm",
30
- ".mp3": "audio/mpeg",
31
- ".wav": "audio/wav",
25
+ ".txt": "text/plain",
32
26
  ".wasm": "application/wasm",
27
+ ".wav": "audio/wav",
28
+ ".webm": "video/webm",
29
+ ".webp": "image/webp",
30
+ ".woff": "font/woff",
31
+ ".woff2": "font/woff2",
32
+ ".xml": "application/xml",
33
+ ".zip": "application/zip",
33
34
  };
34
35
 
35
36
  /**
@@ -39,5 +40,8 @@ const mimeTypes: Record<string, string> = {
39
40
  * @returns The content type for the file
40
41
  */
41
42
  export function getContentType(filePath: string): string | undefined {
43
+ if (filePath.endsWith(".js.map")) {
44
+ return mimeTypes[".js.map"];
45
+ }
42
46
  return mimeTypes[path.extname(filePath).toLowerCase()];
43
47
  }