alchemy 0.59.1 → 0.60.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 (46) hide show
  1. package/bin/alchemy.js +1 -1
  2. package/lib/alchemy.d.ts.map +1 -1
  3. package/lib/alchemy.js +15 -0
  4. package/lib/alchemy.js.map +1 -1
  5. package/lib/apply.js +2 -1
  6. package/lib/apply.js.map +1 -1
  7. package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
  8. package/lib/cloudflare/compatibility-date.gen.js +1 -1
  9. package/lib/cloudflare/index.d.ts +2 -0
  10. package/lib/cloudflare/index.d.ts.map +1 -1
  11. package/lib/cloudflare/index.js +2 -0
  12. package/lib/cloudflare/index.js.map +1 -1
  13. package/lib/cloudflare/rule.d.ts +462 -0
  14. package/lib/cloudflare/rule.d.ts.map +1 -0
  15. package/lib/cloudflare/rule.js +2 -0
  16. package/lib/cloudflare/rule.js.map +1 -0
  17. package/lib/cloudflare/ruleset.d.ts +169 -0
  18. package/lib/cloudflare/ruleset.d.ts.map +1 -0
  19. package/lib/cloudflare/ruleset.js +118 -0
  20. package/lib/cloudflare/ruleset.js.map +1 -0
  21. package/lib/cloudflare/wrangler.json.d.ts +3 -0
  22. package/lib/cloudflare/wrangler.json.d.ts.map +1 -1
  23. package/lib/cloudflare/wrangler.json.js +3 -0
  24. package/lib/cloudflare/wrangler.json.js.map +1 -1
  25. package/lib/cloudflare/zone-bot-management.d.ts +85 -0
  26. package/lib/cloudflare/zone-bot-management.d.ts.map +1 -0
  27. package/lib/cloudflare/zone-bot-management.js +39 -0
  28. package/lib/cloudflare/zone-bot-management.js.map +1 -0
  29. package/lib/cloudflare/zone.d.ts +6 -1
  30. package/lib/cloudflare/zone.d.ts.map +1 -1
  31. package/lib/cloudflare/zone.js +3 -0
  32. package/lib/cloudflare/zone.js.map +1 -1
  33. package/lib/state/instrumented-state-store.d.ts.map +1 -1
  34. package/lib/state/instrumented-state-store.js +14 -5
  35. package/lib/state/instrumented-state-store.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/alchemy.ts +17 -0
  38. package/src/apply.ts +2 -1
  39. package/src/cloudflare/compatibility-date.gen.ts +1 -1
  40. package/src/cloudflare/index.ts +2 -0
  41. package/src/cloudflare/rule.ts +531 -0
  42. package/src/cloudflare/ruleset.ts +257 -0
  43. package/src/cloudflare/wrangler.json.ts +10 -0
  44. package/src/cloudflare/zone-bot-management.ts +147 -0
  45. package/src/cloudflare/zone.ts +18 -1
  46. package/src/state/instrumented-state-store.ts +17 -9
@@ -0,0 +1,257 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { handleApiError } from "./api-error.ts";
4
+ import {
5
+ createCloudflareApi,
6
+ type CloudflareApi,
7
+ type CloudflareApiOptions,
8
+ } from "./api.ts";
9
+ import type { Kind, Rule, RulePhase } from "./rule.ts";
10
+ import { findZoneForHostname, type Zone } from "./zone.ts";
11
+
12
+ /**
13
+ * Properties for creating or updating a Ruleset
14
+ */
15
+ export interface RulesetProps<Phase extends RulePhase>
16
+ extends Partial<CloudflareApiOptions> {
17
+ /**
18
+ * The zone to apply the ruleset to
19
+ */
20
+ zone: string | Zone;
21
+
22
+ /**
23
+ * The phase of the ruleset
24
+ * @default "http_ratelimit"
25
+ */
26
+ phase: Phase;
27
+
28
+ /**
29
+ * Rules to apply to the ruleset
30
+ */
31
+ rules: Array<Rule>;
32
+
33
+ /**
34
+ * Human-readable name for the ruleset
35
+ */
36
+ name?: string;
37
+
38
+ /**
39
+ * Description of the ruleset
40
+ */
41
+ description?: string;
42
+ }
43
+
44
+ /**
45
+ * Output returned after Ruleset creation/update
46
+ */
47
+ export interface Ruleset<Phase extends RulePhase>
48
+ extends Resource<"cloudflare::Ruleset"> {
49
+ /**
50
+ * The ID of the ruleset
51
+ */
52
+ id: string;
53
+
54
+ /**
55
+ * The zone ID
56
+ */
57
+ zoneId: string;
58
+
59
+ /**
60
+ * The phase of the ruleset
61
+ */
62
+ phase: Phase;
63
+
64
+ /**
65
+ * Human-readable name of the ruleset
66
+ */
67
+ name: string;
68
+
69
+ /**
70
+ * Description of the ruleset
71
+ */
72
+ description?: string;
73
+
74
+ /**
75
+ * The rules in the ruleset
76
+ */
77
+ rules: Array<Rule>;
78
+
79
+ /**
80
+ * ISO datetime the ruleset was last updated
81
+ */
82
+ lastUpdated: string;
83
+
84
+ /**
85
+ * Version string of the ruleset
86
+ */
87
+ version: string;
88
+ }
89
+
90
+ /**
91
+ * Cloudflare Ruleset manages rules within a specific phase's entrypoint ruleset.
92
+ * This resource allows you to configure rules for various phases like rate limiting,
93
+ * firewall, transforms, and more.
94
+ *
95
+ * @example
96
+ * // Create a rate limiting ruleset
97
+ * const rateLimits = await Ruleset("api-rate-limits", {
98
+ * zone: "example.com",
99
+ * phase: "http_ratelimit",
100
+ * rules: [
101
+ * {
102
+ * description: "API rate limit",
103
+ * expression: "(http.request.uri.path wildcard r\"/api/*\")",
104
+ * action: "block",
105
+ * ratelimit: {
106
+ * characteristics: ["ip.src"],
107
+ * period: 60,
108
+ * requests_per_period: 100,
109
+ * mitigation_timeout: 600
110
+ * }
111
+ * }
112
+ * ]
113
+ * });
114
+ *
115
+ * @example
116
+ * // Create a custom firewall ruleset
117
+ * const firewall = await Ruleset("custom-firewall", {
118
+ * zone: myZone,
119
+ * phase: "http_request_firewall_custom",
120
+ * rules: [
121
+ * {
122
+ * description: "Block bad IPs",
123
+ * expression: "ip.src in {1.2.3.4 1.2.3.5}",
124
+ * action: "block"
125
+ * },
126
+ * {
127
+ * description: "Challenge suspicious requests",
128
+ * expression: "cf.threat_score > 50",
129
+ * action: "challenge"
130
+ * }
131
+ * ]
132
+ * });
133
+ *
134
+ * @example
135
+ * // Create a transform ruleset for request headers
136
+ * const transforms = await Ruleset("header-transforms", {
137
+ * zone: "example.com",
138
+ * phase: "http_request_transform",
139
+ * rules: [
140
+ * {
141
+ * description: "Add custom header",
142
+ * expression: "true",
143
+ * action: "rewrite",
144
+ * action_parameters: {
145
+ * headers: {
146
+ * "X-Custom-Header": { value: "my-value" }
147
+ * }
148
+ * }
149
+ * }
150
+ * ]
151
+ * });
152
+ */
153
+ export const Ruleset = Resource("cloudflare::Ruleset", async function <
154
+ Phase extends RulePhase,
155
+ >(this: Context<Ruleset<Phase>>, _id: string, props: RulesetProps<Phase>): Promise<
156
+ Ruleset<Phase>
157
+ > {
158
+ const api = await createCloudflareApi(props);
159
+
160
+ // Default phase to http_ratelimit if not specified
161
+ const phase = props.phase || "http_ratelimit";
162
+
163
+ // Get zone ID from zone name
164
+ const zoneId =
165
+ typeof props.zone === "string"
166
+ ? (await findZoneForHostname(api, props.zone)).zoneId
167
+ : props.zone.id;
168
+
169
+ if (this.phase === "delete") {
170
+ // Overwrite entire entrypoint with empty rules
171
+ await updateRuleset(api, zoneId, phase, { rules: [] });
172
+ return this.destroy();
173
+ }
174
+
175
+ // Overwrite entire entrypoint with only the provided rules
176
+ const result = await updateRuleset(api, zoneId, phase, {
177
+ rules: props.rules,
178
+ ...(props.name && { name: props.name }),
179
+ ...(props.description && { description: props.description }),
180
+ });
181
+
182
+ // Transform response back to our format
183
+ return this({
184
+ id: result.id,
185
+ zoneId,
186
+ phase: props.phase,
187
+ name: result.name || props.name || `${phase} ruleset`,
188
+ description: result.description || props.description,
189
+ rules: (result.rules as Array<Rule> | undefined) ?? [],
190
+ lastUpdated: result.last_updated,
191
+ version: result.version,
192
+ });
193
+ });
194
+
195
+ interface CloudflareApiRuleset {
196
+ /** Unique ID of the ruleset (versioned under the hood). */
197
+ id: string;
198
+ /** ISO datetime last updated. */
199
+ last_updated: string; // date-time
200
+ /** Version string of the ruleset. */
201
+ version: string;
202
+ /** Kind of ruleset. */
203
+ kind: Kind;
204
+ /** Human-readable name. */
205
+ name: string; // minLength: 1
206
+ /** Phase this ruleset runs in. */
207
+ phase: RulePhase;
208
+ /** Optional description. */
209
+ description?: string;
210
+ /** The list of rules in the ruleset. */
211
+ rules?: Rule[]; // or (RulesetRule[]) if you prefer loose typing
212
+ }
213
+
214
+ /**
215
+ * Update a ruleset for a specific zone and phase
216
+ * Ensures consistent error handling on non-2xx responses
217
+ */
218
+ export async function updateRuleset(
219
+ api: CloudflareApi,
220
+ zoneId: string,
221
+ /** Phase of the ruleset */
222
+ phase: RulePhase,
223
+ body: {
224
+ /** Human-readable name of the ruleset */
225
+ name?: string; // minLength: 1
226
+ /** Informative description of the ruleset */
227
+ description?: string;
228
+ /** Kind of the ruleset (managed, custom, root, etc.) */
229
+ kind?: Kind;
230
+ /** The list of rules in the ruleset */
231
+ rules: Array<Rule>;
232
+ },
233
+ ): Promise<CloudflareApiRuleset> {
234
+ const response = await api.put(
235
+ `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`,
236
+ body,
237
+ );
238
+
239
+ if (!response.ok) {
240
+ await handleApiError(response, "updating", "ruleset", `${zoneId}/${phase}`);
241
+ }
242
+
243
+ const data = (await response.json()) as { result: CloudflareApiRuleset };
244
+ return data.result;
245
+ }
246
+
247
+ export async function getRuleset(
248
+ api: CloudflareApi,
249
+ zoneId: string,
250
+ phase: RulePhase,
251
+ ): Promise<CloudflareApiRuleset> {
252
+ const response = await api.get(
253
+ `/zones/${zoneId}/rulesets/phases/${phase}/entrypoint`,
254
+ );
255
+ const data = (await response.json()) as { result: CloudflareApiRuleset };
256
+ return data.result;
257
+ }
@@ -163,6 +163,9 @@ export const WranglerJson = Resource(
163
163
  ? {
164
164
  directory: toAbsolute(props.assets.directory),
165
165
  binding: props.assets.binding,
166
+ not_found_handling: props.worker.assets?.not_found_handling,
167
+ html_handling: props.worker.assets?.html_handling,
168
+ run_worker_first: props.worker.assets?.run_worker_first,
166
169
  }
167
170
  : undefined,
168
171
  placement: worker.placement,
@@ -417,6 +420,13 @@ export interface WranglerJsonSpec {
417
420
  assets?: {
418
421
  directory: string;
419
422
  binding: string;
423
+ not_found_handling?: "none" | "404-page" | "single-page-application";
424
+ html_handling?:
425
+ | "auto-trailing-slash"
426
+ | "drop-trailing-slash"
427
+ | "force-trailing-slash"
428
+ | "none";
429
+ run_worker_first?: boolean | string[];
420
430
  };
421
431
 
422
432
  /**
@@ -0,0 +1,147 @@
1
+ import type { CloudflareApi } from "./api.ts";
2
+
3
+ export type BotManagement =
4
+ | BotFightModeConfiguration
5
+ | SuperBotFightModeDefinitelyConfiguration
6
+ | SuperBotFightModeLikelyConfiguration
7
+ | SubscriptionConfiguration;
8
+
9
+ /**
10
+ * Bot Management configuration types
11
+ * See Cloudflare API: https://developers.cloudflare.com/api/resources/bot_management/methods/update/
12
+ */
13
+ /**
14
+ * Action to apply to traffic classified by Super Bot Fight Mode.
15
+ * Maps to Cloudflare API values for SBFM: sbfm_* fields.
16
+ */
17
+ export type SbfmAction = "allow" | "block" | "challenge";
18
+
19
+ /**
20
+ * Bot Fight Mode plan configuration.
21
+ * Maps to Cloudflare API: fight_mode, ai_bots_protection, crawler_protection,
22
+ * enable_js, is_robots_txt_managed, optimize_wordpress, suppress_session_score
23
+ */
24
+ export interface BotFightModeConfiguration {
25
+ /** Enable or disable Bot Fight Mode (fight_mode) */
26
+ fightMode?: boolean;
27
+ /** Configure AI bot protection behavior (ai_bots_protection): "block" or "none" */
28
+ aiBotsProtection?: "block" | "none";
29
+ /** Enable or disable crawler protection (crawler_protection): "enabled" or "disabled" */
30
+ crawlerProtection?: "enabled" | "disabled";
31
+ /** Require JavaScript challenges for suspicious traffic (enable_js) */
32
+ enableJs?: boolean;
33
+ /** Manage robots.txt via Cloudflare (is_robots_txt_managed) */
34
+ isRobotsTxtManaged?: boolean;
35
+ /** Optimize protections for WordPress (optimize_wordpress) */
36
+ optimizeWordpress?: boolean;
37
+ /** Suppress session score collection (suppress_session_score) */
38
+ suppressSessionScore?: boolean;
39
+ }
40
+
41
+ /**
42
+ * Super Bot Fight Mode Likely plan configuration.
43
+ * Maps to Cloudflare API: sbfm_likely_automated, sbfm_verified_bots,
44
+ * sbfm_static_resource_protection, optimize_wordpress, suppress_session_score, fight_mode
45
+ */
46
+ export interface SuperBotFightModeLikelyConfiguration {
47
+ /** Action for likely automated traffic (sbfm_likely_automated) */
48
+ sbfmLikelyAutomated?: SbfmAction;
49
+ /** Handling for verified bots (sbfm_verified_bots): "allow" or "challenge" */
50
+ sbfmVerifiedBots?: "allow" | "challenge";
51
+ /** Protect static resources from automated requests (sbfm_static_resource_protection) */
52
+ sbfmStaticResourceProtection?: boolean;
53
+ /** Optimize protections for WordPress (optimize_wordpress) */
54
+ optimizeWordpress?: boolean;
55
+ /** Suppress session score collection (suppress_session_score) */
56
+ suppressSessionScore?: boolean;
57
+ /** Enable or disable legacy Bot Fight Mode setting if applicable (fight_mode) */
58
+ fightMode?: boolean;
59
+ }
60
+
61
+ /**
62
+ * Super Bot Fight Mode Definitely plan configuration.
63
+ * Maps to Cloudflare API: sbfm_definitely_automated, sbfm_verified_bots,
64
+ * sbfm_static_resource_protection, optimize_wordpress, suppress_session_score, fight_mode
65
+ */
66
+ export interface SuperBotFightModeDefinitelyConfiguration {
67
+ /** Action for definitely automated traffic (sbfm_definitely_automated) */
68
+ sbfmDefinitelyAutomated?: SbfmAction;
69
+ /** Handling for verified bots (sbfm_verified_bots): "allow" or "challenge" */
70
+ sbfmVerifiedBots?: "allow" | "challenge";
71
+ /** Protect static resources from automated requests (sbfm_static_resource_protection) */
72
+ sbfmStaticResourceProtection?: boolean;
73
+ /** Optimize protections for WordPress (optimize_wordpress) */
74
+ optimizeWordpress?: boolean;
75
+ /** Suppress session score collection (suppress_session_score) */
76
+ suppressSessionScore?: boolean;
77
+ /** Enable or disable legacy Bot Fight Mode setting if applicable (fight_mode) */
78
+ fightMode?: boolean;
79
+ }
80
+
81
+ /**
82
+ * Bot Management for Enterprise subscription configuration.
83
+ * Extends base Bot Fight Mode options, adds model auto-update control.
84
+ * Maps to Cloudflare API: auto_update_model
85
+ */
86
+ export interface SubscriptionConfiguration extends BotFightModeConfiguration {
87
+ /** Automatically update Bot Management model (auto_update_model) */
88
+ autoUpdateModel?: boolean;
89
+ }
90
+
91
+ /**
92
+ * Helper function to update Bot Management configuration for a zone
93
+ * Maps high-level options to Cloudflare API fields
94
+ */
95
+ export async function updateBotManagement(
96
+ api: CloudflareApi,
97
+ zoneId: string,
98
+ botManagement: BotManagement | undefined,
99
+ oldBotManagement: BotManagement | undefined,
100
+ ): Promise<void> {
101
+ const payload = createUpdateBotManagementPayload(botManagement);
102
+ const oldPayload = createUpdateBotManagementPayload(oldBotManagement);
103
+ if (JSON.stringify(payload) === JSON.stringify(oldPayload)) {
104
+ return;
105
+ }
106
+ const response = await api.put(`/zones/${zoneId}/bot_management`, payload);
107
+ if (!response.ok) {
108
+ const body = await response.text();
109
+ throw new Error(
110
+ `Failed to update bot management settings: ${response.status} ${response.statusText}\n${body}`,
111
+ );
112
+ }
113
+ }
114
+
115
+ const createUpdateBotManagementPayload = (props: BotManagement | undefined) =>
116
+ props === undefined
117
+ ? {}
118
+ : {
119
+ fight_mode: (props as BotFightModeConfiguration).fightMode,
120
+ ai_bots_protection: (props as BotFightModeConfiguration)
121
+ .aiBotsProtection,
122
+ crawler_protection: (props as BotFightModeConfiguration)
123
+ .crawlerProtection,
124
+ enable_js: (props as BotFightModeConfiguration).enableJs,
125
+ is_robots_txt_managed: (props as BotFightModeConfiguration)
126
+ .isRobotsTxtManaged,
127
+ auto_update_model: (props as SubscriptionConfiguration).autoUpdateModel,
128
+ optimize_wordpress: (props as BotFightModeConfiguration)
129
+ .optimizeWordpress,
130
+ suppress_session_score: (props as BotFightModeConfiguration)
131
+ .suppressSessionScore,
132
+ sbfm_likely_automated: (props as SuperBotFightModeLikelyConfiguration)
133
+ .sbfmLikelyAutomated,
134
+ sbfm_definitely_automated: (
135
+ props as SuperBotFightModeDefinitelyConfiguration
136
+ ).sbfmDefinitelyAutomated,
137
+ sbfm_verified_bots: (
138
+ props as
139
+ | SuperBotFightModeLikelyConfiguration
140
+ | SuperBotFightModeDefinitelyConfiguration
141
+ ).sbfmVerifiedBots,
142
+ sbfm_static_resource_protection: (
143
+ props as
144
+ | SuperBotFightModeLikelyConfiguration
145
+ | SuperBotFightModeDefinitelyConfiguration
146
+ ).sbfmStaticResourceProtection,
147
+ };
@@ -7,6 +7,10 @@ import {
7
7
  type CloudflareApi,
8
8
  type CloudflareApiOptions,
9
9
  } from "./api.ts";
10
+ import {
11
+ updateBotManagement,
12
+ type BotManagement,
13
+ } from "./zone-bot-management.ts";
10
14
  import type {
11
15
  AlwaysUseHTTPSValue,
12
16
  AutomaticHTTPSRewritesValue,
@@ -167,6 +171,11 @@ export interface ZoneProps extends CloudflareApiOptions {
167
171
  */
168
172
  minTlsVersion?: MinTLSVersionValue;
169
173
  };
174
+
175
+ /**
176
+ * Cloudflare Bot Management settings
177
+ */
178
+ botManagement?: BotManagement;
170
179
  }
171
180
 
172
181
  /**
@@ -314,7 +323,7 @@ export interface Zone extends Resource<"cloudflare::Zone">, ZoneData {}
314
323
  export const Zone = Resource(
315
324
  "cloudflare::Zone",
316
325
  async function (
317
- this: Context<Zone>,
326
+ this: Context<Zone, ZoneProps>,
318
327
  _id: string,
319
328
  props: ZoneProps,
320
329
  ): Promise<Zone> {
@@ -408,6 +417,14 @@ export const Zone = Resource(
408
417
  // https://github.com/sam-goodwin/alchemy/issues/681
409
418
  await new Promise((resolve) => setTimeout(resolve, 2000));
410
419
 
420
+ // Update Bot Management configuration if provided
421
+ await updateBotManagement(
422
+ api,
423
+ zoneData.id,
424
+ props.botManagement,
425
+ this.props?.botManagement,
426
+ );
427
+
411
428
  return this({
412
429
  id: zoneData.id,
413
430
  name: zoneData.name,
@@ -24,16 +24,24 @@ export class InstrumentedStateStore<T extends StateStore>
24
24
  ): Promise<T> {
25
25
  const start = performance.now();
26
26
  let error: unknown;
27
- return await fn()
28
- .catch((err) => (error = err))
29
- .finally(() => {
30
- this.telemetryClient.record({
31
- event,
32
- stateStoreClass: this.stateStoreClass,
33
- elapsed: performance.now() - start,
34
- error: error instanceof Error ? error : new Error(String(error)),
35
- });
27
+ try {
28
+ return await fn();
29
+ } catch (err) {
30
+ error = err;
31
+ throw err;
32
+ } finally {
33
+ this.telemetryClient.record({
34
+ event,
35
+ stateStoreClass: this.stateStoreClass,
36
+ elapsed: performance.now() - start,
37
+ error:
38
+ error instanceof Error
39
+ ? error
40
+ : error
41
+ ? new Error(String(error))
42
+ : undefined,
36
43
  });
44
+ }
37
45
  }
38
46
 
39
47
  async init() {