alchemy 0.50.0 → 0.51.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/apply.js +10 -9
  2. package/lib/apply.js.map +1 -1
  3. package/lib/aws/control/types.d.ts +4 -4
  4. package/lib/cloudflare/api-gateway-operation.d.ts +141 -0
  5. package/lib/cloudflare/api-gateway-operation.d.ts.map +1 -0
  6. package/lib/cloudflare/api-gateway-operation.js +153 -0
  7. package/lib/cloudflare/api-gateway-operation.js.map +1 -0
  8. package/lib/cloudflare/api-mitigation.d.ts +19 -0
  9. package/lib/cloudflare/api-mitigation.d.ts.map +1 -0
  10. package/lib/cloudflare/api-mitigation.js +1 -0
  11. package/lib/cloudflare/api-mitigation.js.map +1 -0
  12. package/lib/cloudflare/api-schema.d.ts +137 -0
  13. package/lib/cloudflare/api-schema.d.ts.map +1 -0
  14. package/lib/cloudflare/api-schema.js +197 -0
  15. package/lib/cloudflare/api-schema.js.map +1 -0
  16. package/lib/cloudflare/api-shield.d.ts +366 -0
  17. package/lib/cloudflare/api-shield.d.ts.map +1 -0
  18. package/lib/cloudflare/api-shield.js +427 -0
  19. package/lib/cloudflare/api-shield.js.map +1 -0
  20. package/lib/cloudflare/certificate-pack.d.ts +1 -1
  21. package/lib/cloudflare/certificate-pack.d.ts.map +1 -1
  22. package/lib/cloudflare/certificate-pack.js +18 -88
  23. package/lib/cloudflare/certificate-pack.js.map +1 -1
  24. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  25. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  26. package/lib/cloudflare/index.d.ts +3 -0
  27. package/lib/cloudflare/index.d.ts.map +1 -1
  28. package/lib/cloudflare/index.js +3 -0
  29. package/lib/cloudflare/index.js.map +1 -1
  30. package/lib/cloudflare/zone.d.ts +12 -0
  31. package/lib/cloudflare/zone.d.ts.map +1 -1
  32. package/lib/cloudflare/zone.js +35 -0
  33. package/lib/cloudflare/zone.js.map +1 -1
  34. package/lib/destroy.d.ts.map +1 -1
  35. package/lib/destroy.js +26 -18
  36. package/lib/destroy.js.map +1 -1
  37. package/lib/scope.d.ts.map +1 -1
  38. package/lib/scope.js +2 -2
  39. package/lib/scope.js.map +1 -1
  40. package/package.json +2 -1
  41. package/src/apply.ts +11 -12
  42. package/src/aws/control/types.ts +4 -4
  43. package/src/cloudflare/api-gateway-operation.ts +340 -0
  44. package/src/cloudflare/api-mitigation.ts +22 -0
  45. package/src/cloudflare/api-schema.ts +364 -0
  46. package/src/cloudflare/api-shield.ts +676 -0
  47. package/src/cloudflare/certificate-pack.ts +28 -152
  48. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  49. package/src/cloudflare/index.ts +3 -0
  50. package/src/cloudflare/zone.ts +52 -0
  51. package/src/destroy.ts +26 -20
  52. package/src/scope.ts +3 -3
  53. package/templates/tanstack-start/package.json +3 -3
  54. package/templates/tanstack-start/vite.config.ts +2 -6
@@ -7,7 +7,7 @@ import {
7
7
  type CloudflareApi,
8
8
  type CloudflareApiOptions,
9
9
  } from "./api.ts";
10
- import type { Zone } from "./zone.ts";
10
+ import { findZoneForHostname, type Zone } from "./zone.ts";
11
11
 
12
12
  /**
13
13
  * Certificate Authority options for Advanced Certificate Packs
@@ -298,8 +298,18 @@ export const CertificatePack = Resource(
298
298
  const deleteResponse = await api.delete(
299
299
  `/zones/${zoneId}/ssl/certificate_packs/${this.output.id}`,
300
300
  );
301
-
302
- if (!deleteResponse.ok && deleteResponse.status !== 404) {
301
+ const deleteResponseJson = (await deleteResponse.json()) as {
302
+ errors: Array<{ code: number }>;
303
+ };
304
+ const isCurrentlyDeleting =
305
+ deleteResponse.status === 400 &&
306
+ deleteResponseJson.errors[0].code === 1406;
307
+
308
+ if (
309
+ !deleteResponse.ok &&
310
+ deleteResponse.status !== 404 &&
311
+ !isCurrentlyDeleting
312
+ ) {
303
313
  await handleApiError(
304
314
  deleteResponse,
305
315
  "delete",
@@ -314,96 +324,7 @@ export const CertificatePack = Resource(
314
324
  }
315
325
 
316
326
  if (this.phase === "update" && this.output?.id) {
317
- // Validate immutable properties
318
- const currentPack = this.output;
319
-
320
- if (props.certificateAuthority !== currentPack.certificateAuthority) {
321
- throw new Error(
322
- `Cannot change certificateAuthority from '${currentPack.certificateAuthority}' to '${props.certificateAuthority}'. Certificate Authority is immutable after creation. You must delete and recreate the certificate pack.`,
323
- );
324
- }
325
-
326
- if (
327
- JSON.stringify(props.hosts.sort()) !==
328
- JSON.stringify(currentPack.hosts.sort())
329
- ) {
330
- throw new Error(
331
- `Cannot change hosts from [${currentPack.hosts.join(", ")}] to [${props.hosts.join(", ")}]. Hosts are immutable after creation. You must delete and recreate the certificate pack.`,
332
- );
333
- }
334
-
335
- if (props.validationMethod !== currentPack.validationMethod) {
336
- throw new Error(
337
- `Cannot change validationMethod from '${currentPack.validationMethod}' to '${props.validationMethod}'. Validation method is immutable after creation. You must delete and recreate the certificate pack.`,
338
- );
339
- }
340
-
341
- if (props.validityDays !== currentPack.validityDays) {
342
- throw new Error(
343
- `Cannot change validityDays from ${currentPack.validityDays} to ${props.validityDays}. Validity period is immutable after creation. You must delete and recreate the certificate pack.`,
344
- );
345
- }
346
-
347
- const type = props.type || "advanced";
348
- if (type !== currentPack.type) {
349
- throw new Error(
350
- `Cannot change type from '${currentPack.type}' to '${type}'. Type is immutable after creation. You must delete and recreate the certificate pack.`,
351
- );
352
- }
353
-
354
- // Only cloudflareBranding can be updated
355
- if (props.cloudflareBranding !== currentPack.cloudflareBranding) {
356
- logger.log(
357
- `Updating certificate pack cloudflare branding from ${currentPack.cloudflareBranding} to ${props.cloudflareBranding}`,
358
- );
359
-
360
- const updateResponse = await api.patch(
361
- `/zones/${zoneId}/ssl/certificate_packs/${this.output.id}`,
362
- {
363
- cloudflare_branding: props.cloudflareBranding || false,
364
- },
365
- );
366
-
367
- if (!updateResponse.ok) {
368
- await handleApiError(
369
- updateResponse,
370
- "update",
371
- "certificate pack",
372
- this.output.id,
373
- );
374
- }
375
- }
376
-
377
- // Get updated certificate pack details
378
- const response = await api.get(
379
- `/zones/${zoneId}/ssl/certificate_packs/${this.output.id}`,
380
- );
381
-
382
- if (!response.ok) {
383
- await handleApiError(
384
- response,
385
- "get",
386
- "certificate pack",
387
- this.output.id,
388
- );
389
- }
390
-
391
- const updatedPack = (
392
- (await response.json()) as { result: CloudflareCertificatePack }
393
- ).result;
394
-
395
- return this({
396
- id: updatedPack.id,
397
- certificateAuthority: updatedPack.certificate_authority,
398
- cloudflareBranding: updatedPack.cloudflare_branding,
399
- hosts: updatedPack.hosts,
400
- status: updatedPack.status,
401
- type: updatedPack.type,
402
- validationMethod: updatedPack.validation_method,
403
- validityDays: updatedPack.validity_days,
404
- zoneId: updatedPack.zone_id,
405
- zoneName: zoneName,
406
- });
327
+ this.replace();
407
328
  }
408
329
 
409
330
  // Create new certificate pack
@@ -438,13 +359,13 @@ export const CertificatePack = Resource(
438
359
  return this({
439
360
  id: existingPack.id,
440
361
  certificateAuthority: existingPack.certificate_authority,
441
- cloudflareBranding: existingPack.cloudflare_branding,
362
+ cloudflareBranding: props.cloudflareBranding ?? false,
442
363
  hosts: existingPack.hosts,
443
364
  status: existingPack.status,
444
365
  type: existingPack.type,
445
366
  validationMethod: existingPack.validation_method,
446
367
  validityDays: existingPack.validity_days,
447
- zoneId: existingPack.zone_id,
368
+ zoneId,
448
369
  zoneName: zoneName,
449
370
  });
450
371
  }
@@ -498,13 +419,13 @@ export const CertificatePack = Resource(
498
419
  return this({
499
420
  id: createdPack.id,
500
421
  certificateAuthority: createdPack.certificate_authority,
501
- cloudflareBranding: createdPack.cloudflare_branding,
422
+ cloudflareBranding: props.cloudflareBranding ?? false,
502
423
  hosts: createdPack.hosts,
503
424
  status: createdPack.status,
504
425
  type: createdPack.type,
505
426
  validationMethod: createdPack.validation_method,
506
427
  validityDays: createdPack.validity_days,
507
- zoneId: createdPack.zone_id,
428
+ zoneId,
508
429
  zoneName: zoneName,
509
430
  });
510
431
  },
@@ -516,13 +437,11 @@ export const CertificatePack = Resource(
516
437
  interface CloudflareCertificatePack {
517
438
  id: string;
518
439
  certificate_authority: CertificateAuthority;
519
- cloudflare_branding: boolean;
520
440
  hosts: string[];
521
441
  status: CertificatePackStatus;
522
442
  type: "advanced";
523
443
  validation_method: ValidationMethod;
524
444
  validity_days: ValidityDays;
525
- zone_id: string;
526
445
  }
527
446
 
528
447
  /**
@@ -591,58 +510,6 @@ export async function waitForCertificatePackActive(
591
510
  );
592
511
  }
593
512
 
594
- /**
595
- * Helper function to find zone ID from a hostname
596
- * Searches for the zone that matches the hostname or its parent domains
597
- *
598
- * @param api CloudflareApi instance
599
- * @param hostname The hostname to find the zone for
600
- * @returns Promise resolving to the zone ID and zone name
601
- */
602
- async function findZoneForHostname(
603
- api: CloudflareApi,
604
- hostname: string,
605
- ): Promise<{ zoneId: string; zoneName: string }> {
606
- // Remove wildcard prefix if present
607
- const cleanHostname = hostname.replace(/^\*\./, "");
608
-
609
- // Get all zones and find the best match
610
- const response = await api.get("/zones");
611
-
612
- if (!response.ok) {
613
- throw new Error(`Failed to list zones: ${response.statusText}`);
614
- }
615
-
616
- const zonesData = (await response.json()) as {
617
- result: Array<{ id: string; name: string }>;
618
- };
619
-
620
- // Find the zone that best matches the hostname
621
- // We look for the longest matching zone name (most specific)
622
- let bestMatch: { zoneId: string; zoneName: string } | null = null;
623
- let longestMatch = 0;
624
-
625
- for (const zone of zonesData.result) {
626
- if (
627
- cleanHostname === zone.name ||
628
- cleanHostname.endsWith(`.${zone.name}`)
629
- ) {
630
- if (zone.name.length > longestMatch) {
631
- longestMatch = zone.name.length;
632
- bestMatch = { zoneId: zone.id, zoneName: zone.name };
633
- }
634
- }
635
- }
636
-
637
- if (!bestMatch) {
638
- throw new Error(
639
- `Could not find zone for hostname '${hostname}'. Available zones: ${zonesData.result.map((z) => z.name).join(", ")}`,
640
- );
641
- }
642
-
643
- return bestMatch;
644
- }
645
-
646
513
  /**
647
514
  * Helper function to find existing certificate packs that match the given configuration
648
515
  * Used to adopt existing certificates instead of creating duplicates
@@ -657,7 +524,9 @@ async function findMatchingCertificatePack(
657
524
  zoneId: string,
658
525
  props: CertificatePackProps,
659
526
  ): Promise<CloudflareCertificatePack | null> {
660
- const response = await api.get(`/zones/${zoneId}/ssl/certificate_packs`);
527
+ const response = await api.get(
528
+ `/zones/${zoneId}/ssl/certificate_packs?status=all`,
529
+ );
661
530
 
662
531
  if (!response.ok) {
663
532
  throw new Error(`Failed to list certificate packs: ${response.statusText}`);
@@ -686,6 +555,13 @@ async function findMatchingCertificatePack(
686
555
  const allHostsCovered = props.hosts.every((host) => packHosts.has(host));
687
556
 
688
557
  if (allHostsCovered) {
558
+ // If Cloudflare branding is enabled, ensure that the cert pack is cloudflare branded`
559
+ if (
560
+ props.cloudflareBranding &&
561
+ !pack.hosts.some((host) => host.endsWith("sni.cloudflaressl.com"))
562
+ ) {
563
+ continue;
564
+ }
689
565
  logger.log(
690
566
  `Found existing certificate pack ${pack.id} that covers all requested hosts`,
691
567
  );
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * The default Cloudflare Workers compatibility date, set based on the latest workerd release at the time of build.
6
6
  */
7
- export const DEFAULT_COMPATIBILITY_DATE = "2025-07-19";
7
+ export const DEFAULT_COMPATIBILITY_DATE = "2025-07-22";
@@ -4,6 +4,9 @@ export * from "./ai-gateway.ts";
4
4
  export * from "./ai.ts";
5
5
  export * from "./analytics-engine.ts";
6
6
  export * from "./api-error.ts";
7
+ export * from "./api-gateway-operation.ts";
8
+ export * from "./api-schema.ts";
9
+ export * from "./api-shield.ts";
7
10
  export * from "./api.ts";
8
11
  export * from "./assets.ts";
9
12
  export * from "./astro.ts";
@@ -610,3 +610,55 @@ export interface CloudflareZone {
610
610
  modified_on: string;
611
611
  activated_on: string | null;
612
612
  }
613
+
614
+ /**
615
+ * Helper function to find zone ID from a hostname
616
+ * Searches for the zone that matches the hostname or its parent domains
617
+ *
618
+ * @param api CloudflareApi instance
619
+ * @param hostname The hostname to find the zone for
620
+ * @returns Promise resolving to the zone ID and zone name
621
+ */
622
+ export async function findZoneForHostname(
623
+ api: CloudflareApi,
624
+ hostname: string,
625
+ ): Promise<{ zoneId: string; zoneName: string }> {
626
+ // Remove wildcard prefix if present
627
+ const cleanHostname = hostname.replace(/^\*\./, "");
628
+
629
+ // Get all zones and find the best match
630
+ const response = await api.get("/zones");
631
+
632
+ if (!response.ok) {
633
+ throw new Error(`Failed to list zones: ${response.statusText}`);
634
+ }
635
+
636
+ const zonesData = (await response.json()) as {
637
+ result: Array<{ id: string; name: string }>;
638
+ };
639
+
640
+ // Find the zone that best matches the hostname
641
+ // We look for the longest matching zone name (most specific)
642
+ let bestMatch: { zoneId: string; zoneName: string } | null = null;
643
+ let longestMatch = 0;
644
+
645
+ for (const zone of zonesData.result) {
646
+ if (
647
+ cleanHostname === zone.name ||
648
+ cleanHostname.endsWith(`.${zone.name}`)
649
+ ) {
650
+ if (zone.name.length > longestMatch) {
651
+ longestMatch = zone.name.length;
652
+ bestMatch = { zoneId: zone.id, zoneName: zone.name };
653
+ }
654
+ }
655
+ }
656
+
657
+ if (!bestMatch) {
658
+ throw new Error(
659
+ `Could not find zone for hostname '${hostname}'. Available zones: ${zonesData.result.map((z) => z.name).join(", ")}`,
660
+ );
661
+ }
662
+
663
+ return bestMatch;
664
+ }
package/src/destroy.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  ResourceSeq,
12
12
  } from "./resource.ts";
13
13
  import { isScope, type PendingDeletions, Scope } from "./scope.ts";
14
+ import type { State } from "./state.ts";
14
15
  import { formatFQN } from "./util/cli.ts";
15
16
  import { logger } from "./util/logger.ts";
16
17
 
@@ -108,12 +109,29 @@ export async function destroy<Type extends string>(
108
109
  });
109
110
  }
110
111
 
111
- const state = await scope.state.get(instance[ResourceID]);
112
-
113
- if (state === undefined) {
114
- return;
112
+ let state: State;
113
+ let props: ResourceProps | undefined;
114
+ if (options?.replace) {
115
+ props = options.replace.props;
116
+ state = {
117
+ output: options.replace.output!,
118
+ status: "deleting",
119
+ oldProps: options.replace.props,
120
+ data: {},
121
+ kind: instance[ResourceKind],
122
+ id: instance[ResourceID],
123
+ fqn: instance[ResourceFQN],
124
+ seq: instance[ResourceSeq],
125
+ props,
126
+ };
127
+ } else {
128
+ const _state = await scope.state.get(instance[ResourceID]);
129
+ if (_state === undefined) {
130
+ return;
131
+ }
132
+ state = _state;
133
+ props = state.props;
115
134
  }
116
-
117
135
  const ctx = context({
118
136
  scope,
119
137
  phase: "delete",
@@ -121,20 +139,8 @@ export async function destroy<Type extends string>(
121
139
  id: instance[ResourceID],
122
140
  fqn: instance[ResourceFQN],
123
141
  seq: instance[ResourceSeq],
124
- props: options?.replace?.props ?? state.props,
125
- state: options?.replace?.output
126
- ? {
127
- output: options.replace.output,
128
- status: "deleting",
129
- oldProps: options.replace.props,
130
- data: {},
131
- kind: instance[ResourceKind],
132
- id: instance[ResourceID],
133
- fqn: instance[ResourceFQN],
134
- seq: instance[ResourceSeq],
135
- props: options.replace.props,
136
- }
137
- : state,
142
+ props,
143
+ state,
138
144
  // TODO(sam|michael): should this always be false or !!options?.replace
139
145
  isReplacement: false,
140
146
  replace: () => {
@@ -157,7 +163,7 @@ export async function destroy<Type extends string>(
157
163
  nestedScope = options?.replace?.props == null ? scope : undefined;
158
164
  return await Provider.handler.bind(ctx)(
159
165
  instance[ResourceID],
160
- options?.replace?.props ?? state.props!,
166
+ ctx.props,
161
167
  );
162
168
  },
163
169
  );
package/src/scope.ts CHANGED
@@ -142,7 +142,6 @@ export class Scope {
142
142
  private isSkipped = false;
143
143
  private finalized = false;
144
144
  private startedAt = performance.now();
145
-
146
145
  private deferred: (() => Promise<any>)[] = [];
147
146
 
148
147
  public get appName(): string {
@@ -345,13 +344,13 @@ export class Scope {
345
344
  }
346
345
 
347
346
  public async set<T>(key: string, value: T): Promise<void> {
348
- return this.withScopeState<void>(async (state, persist) => {
347
+ await this.withScopeState<void>(async (state, persist) => {
349
348
  state.data[key] = value;
350
349
  await persist(state); // only one line to save!
351
350
  });
352
351
  }
353
352
 
354
- public async get<T>(key: string): Promise<T> {
353
+ public get<T>(key: string): Promise<T> {
355
354
  return this.withScopeState<T>(async (state) => state.data[key]);
356
355
  }
357
356
 
@@ -475,6 +474,7 @@ export class Scope {
475
474
  }
476
475
  throw e;
477
476
  })) ?? [];
477
+
478
478
  //todo(michael): remove once we deprecate doss; see: https://github.com/sam-goodwin/alchemy/issues/585
479
479
  let hasCorruptedResources = false;
480
480
  if (pendingDeletions) {
@@ -11,9 +11,9 @@
11
11
  "start": "node .output/server/index.mjs"
12
12
  },
13
13
  "dependencies": {
14
- "@tanstack/react-router": "^1.128.3",
15
- "@tanstack/react-router-devtools": "^1.128.3",
16
- "@tanstack/react-start": "^1.128.3",
14
+ "@tanstack/react-router": "^1.129.2",
15
+ "@tanstack/react-router-devtools": "^1.129.2",
16
+ "@tanstack/react-start": "^1.129.2",
17
17
  "react": "^19.0.0",
18
18
  "react-dom": "^19.0.0",
19
19
  "tailwind-merge": "3",
@@ -1,5 +1,6 @@
1
1
  import tailwindcss from "@tailwindcss/vite";
2
2
  import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3
+ import viteReact from "@vitejs/plugin-react";
3
4
  import { cloudflareWorkersDevEnvironmentShim } from "alchemy/cloudflare";
4
5
  import { defineConfig, PluginOption } from "vite";
5
6
  import tsConfigPaths from "vite-tsconfig-paths";
@@ -23,12 +24,7 @@ export default defineConfig({
23
24
  tanstackStart({
24
25
  target: "cloudflare-module",
25
26
  customViteReactPlugin: true,
26
- tsr: {
27
- routeTreeFileHeader: [
28
- "/** biome-ignore-all lint/suspicious/noExplicitAny: code generated by @tanstack/react-start */",
29
- ],
30
- quoteStyle: "double",
31
- },
32
27
  }),
28
+ viteReact(),
33
29
  ],
34
30
  });