alchemy 0.59.2 → 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.
@@ -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
+ }
@@ -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() {