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.
- package/bin/alchemy.js +1 -1
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +15 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/apply.js +1 -1
- package/lib/apply.js.map +1 -1
- package/lib/cloudflare/compatibility-date.gen.d.ts +1 -1
- package/lib/cloudflare/compatibility-date.gen.js +1 -1
- package/lib/cloudflare/index.d.ts +2 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +2 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/rule.d.ts +462 -0
- package/lib/cloudflare/rule.d.ts.map +1 -0
- package/lib/cloudflare/rule.js +2 -0
- package/lib/cloudflare/rule.js.map +1 -0
- package/lib/cloudflare/ruleset.d.ts +169 -0
- package/lib/cloudflare/ruleset.d.ts.map +1 -0
- package/lib/cloudflare/ruleset.js +118 -0
- package/lib/cloudflare/ruleset.js.map +1 -0
- package/lib/state/instrumented-state-store.d.ts.map +1 -1
- package/lib/state/instrumented-state-store.js +14 -5
- package/lib/state/instrumented-state-store.js.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +17 -0
- package/src/apply.ts +1 -1
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/index.ts +2 -0
- package/src/cloudflare/rule.ts +531 -0
- package/src/cloudflare/ruleset.ts +257 -0
- 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
|
+
}
|
|
@@ -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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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() {
|