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.
- package/lib/apply.js +10 -9
- package/lib/apply.js.map +1 -1
- package/lib/aws/control/types.d.ts +4 -4
- package/lib/cloudflare/api-gateway-operation.d.ts +141 -0
- package/lib/cloudflare/api-gateway-operation.d.ts.map +1 -0
- package/lib/cloudflare/api-gateway-operation.js +153 -0
- package/lib/cloudflare/api-gateway-operation.js.map +1 -0
- package/lib/cloudflare/api-mitigation.d.ts +19 -0
- package/lib/cloudflare/api-mitigation.d.ts.map +1 -0
- package/lib/cloudflare/api-mitigation.js +1 -0
- package/lib/cloudflare/api-mitigation.js.map +1 -0
- package/lib/cloudflare/api-schema.d.ts +137 -0
- package/lib/cloudflare/api-schema.d.ts.map +1 -0
- package/lib/cloudflare/api-schema.js +197 -0
- package/lib/cloudflare/api-schema.js.map +1 -0
- package/lib/cloudflare/api-shield.d.ts +366 -0
- package/lib/cloudflare/api-shield.d.ts.map +1 -0
- package/lib/cloudflare/api-shield.js +427 -0
- package/lib/cloudflare/api-shield.js.map +1 -0
- package/lib/cloudflare/certificate-pack.d.ts +1 -1
- package/lib/cloudflare/certificate-pack.d.ts.map +1 -1
- package/lib/cloudflare/certificate-pack.js +18 -88
- package/lib/cloudflare/certificate-pack.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 +3 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +3 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/zone.d.ts +12 -0
- package/lib/cloudflare/zone.d.ts.map +1 -1
- package/lib/cloudflare/zone.js +35 -0
- package/lib/cloudflare/zone.js.map +1 -1
- package/lib/destroy.d.ts.map +1 -1
- package/lib/destroy.js +26 -18
- package/lib/destroy.js.map +1 -1
- package/lib/scope.d.ts.map +1 -1
- package/lib/scope.js +2 -2
- package/lib/scope.js.map +1 -1
- package/package.json +2 -1
- package/src/apply.ts +11 -12
- package/src/aws/control/types.ts +4 -4
- package/src/cloudflare/api-gateway-operation.ts +340 -0
- package/src/cloudflare/api-mitigation.ts +22 -0
- package/src/cloudflare/api-schema.ts +364 -0
- package/src/cloudflare/api-shield.ts +676 -0
- package/src/cloudflare/certificate-pack.ts +28 -152
- package/src/cloudflare/compatibility-date.gen.ts +1 -1
- package/src/cloudflare/index.ts +3 -0
- package/src/cloudflare/zone.ts +52 -0
- package/src/destroy.ts +26 -20
- package/src/scope.ts +3 -3
- package/templates/tanstack-start/package.json +3 -3
- package/templates/tanstack-start/vite.config.ts +2 -6
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
3
|
+
import * as yaml from "yaml";
|
|
4
|
+
import type { Context } from "../context.ts";
|
|
5
|
+
import { Resource } from "../resource.ts";
|
|
6
|
+
import { handleApiError } from "./api-error.ts";
|
|
7
|
+
import { APIGatewayOperation } from "./api-gateway-operation.ts";
|
|
8
|
+
import type { APIMitigation, APIMitigations } from "./api-mitigation.ts";
|
|
9
|
+
import { APISchema } from "./api-schema.ts";
|
|
10
|
+
import {
|
|
11
|
+
createCloudflareApi,
|
|
12
|
+
type CloudflareApi,
|
|
13
|
+
type CloudflareApiOptions,
|
|
14
|
+
} from "./api.ts";
|
|
15
|
+
import type { Zone } from "./zone.ts";
|
|
16
|
+
import { findZoneForHostname } from "./zone.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Properties for creating or updating Schema Validation
|
|
20
|
+
*/
|
|
21
|
+
export interface APIShieldProps<S extends string | URL | OpenAPIV3.Document>
|
|
22
|
+
extends CloudflareApiOptions {
|
|
23
|
+
/**
|
|
24
|
+
* The zone to configure schema validation for
|
|
25
|
+
*/
|
|
26
|
+
zone: string | Zone;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The name of the schema validation
|
|
30
|
+
*
|
|
31
|
+
* @default id
|
|
32
|
+
*/
|
|
33
|
+
name?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The schema resource to use for validation
|
|
37
|
+
*
|
|
38
|
+
* Can be one of:
|
|
39
|
+
* 1. a string containing OpenAPI v3 schema
|
|
40
|
+
* 2. a string path to a file containing OpenAPI v3 schema
|
|
41
|
+
* 3. a file://, http:// or https:// URL pointing to an OpenAPI v3 schema
|
|
42
|
+
* 4. a literal OpenAPI v3 schema object
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* await APIShield("my-validation", {
|
|
46
|
+
* zone: myZone,
|
|
47
|
+
* schema: "path/to/openapi.yaml",
|
|
48
|
+
* });
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* await APIShield("my-validation", {
|
|
52
|
+
* zone: myZone,
|
|
53
|
+
* schema: new URL("file:///path/to/openapi.yaml"),
|
|
54
|
+
* });
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* await APIShield("my-validation", {
|
|
58
|
+
* zone: myZone,
|
|
59
|
+
* schema: new URL("https://api.example.com/openapi.yaml"),
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* await APIShield("my-validation", {
|
|
64
|
+
* zone: myZone,
|
|
65
|
+
* schema: `
|
|
66
|
+
* openapi: 3.0.0
|
|
67
|
+
* info:
|
|
68
|
+
* title: My API
|
|
69
|
+
* version: 1.0.0
|
|
70
|
+
* paths:
|
|
71
|
+
* /users:
|
|
72
|
+
* get:
|
|
73
|
+
* operationId: getUsers
|
|
74
|
+
* responses:
|
|
75
|
+
* '200':
|
|
76
|
+
* description: Success
|
|
77
|
+
* `,
|
|
78
|
+
* });
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* await APIShield("my-validation", {
|
|
82
|
+
* zone: myZone,
|
|
83
|
+
* schema: `
|
|
84
|
+
* openapi: 3.0.0
|
|
85
|
+
* info:
|
|
86
|
+
* title: My API
|
|
87
|
+
* version: 1.0.0
|
|
88
|
+
* paths:
|
|
89
|
+
* /users:
|
|
90
|
+
* get:
|
|
91
|
+
* operationId: getUsers
|
|
92
|
+
* responses:
|
|
93
|
+
* '200':
|
|
94
|
+
* description: Success
|
|
95
|
+
* `
|
|
96
|
+
* });
|
|
97
|
+
*/
|
|
98
|
+
schema: S;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether to enable the schema validation
|
|
102
|
+
*
|
|
103
|
+
* @default true
|
|
104
|
+
*/
|
|
105
|
+
enabled?: boolean;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Per-operation validation overrides using OpenAPI-style path structure
|
|
109
|
+
*
|
|
110
|
+
* Can specify mitigations per HTTP method or a blanket action for all methods on a path:
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* // Per-method configuration
|
|
114
|
+
* {
|
|
115
|
+
* "/users": {
|
|
116
|
+
* get: "none",
|
|
117
|
+
* post: "block"
|
|
118
|
+
* },
|
|
119
|
+
* "/users/{id}": {
|
|
120
|
+
* delete: "block"
|
|
121
|
+
* }
|
|
122
|
+
* }
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* // Blanket action for all methods on a path
|
|
126
|
+
* {
|
|
127
|
+
* "/users": "none",
|
|
128
|
+
* "/users/{id}": "block",
|
|
129
|
+
* "/admin": "block"
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
mitigations?: APIMitigations<S extends string | URL ? OpenAPIV3.Document : S>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Default validation action for all operations
|
|
136
|
+
*
|
|
137
|
+
* @default "none"
|
|
138
|
+
*/
|
|
139
|
+
defaultMitigation?: APIMitigation;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Action for requests that don't match any operation
|
|
143
|
+
* @default "none"
|
|
144
|
+
*/
|
|
145
|
+
unknownOperationMitigation?: APIMitigation;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Global validation settings
|
|
150
|
+
*/
|
|
151
|
+
export interface ValidationSettings {
|
|
152
|
+
/**
|
|
153
|
+
* Default mitigation action
|
|
154
|
+
*/
|
|
155
|
+
defaultMitigation: APIMitigation;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Override mitigation action for specific operations
|
|
159
|
+
*/
|
|
160
|
+
overrideMitigation?: APIMitigation;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Schema Validation output
|
|
165
|
+
*/
|
|
166
|
+
export interface APIShield<S extends OpenAPIV3.Document = OpenAPIV3.Document>
|
|
167
|
+
extends Resource<"cloudflare::APIShield"> {
|
|
168
|
+
/**
|
|
169
|
+
* The schema resource
|
|
170
|
+
*/
|
|
171
|
+
schema: APISchema<S>;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Zone ID
|
|
175
|
+
*/
|
|
176
|
+
zoneId: string;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The API Schema's API Gateway Operations (and their respective mitigation actions)
|
|
180
|
+
*/
|
|
181
|
+
operations: APIGatewayOperation[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Cloudflare Schema Validation protects your API endpoints by validating incoming requests
|
|
186
|
+
* against an OpenAPI v3 schema. It can log or block requests that don't match your schema,
|
|
187
|
+
* helping prevent malformed requests and potential security issues.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ## Basic schema validation with inline YAML
|
|
191
|
+
*
|
|
192
|
+
* Enable schema validation with a simple OpenAPI schema as YAML string
|
|
193
|
+
*
|
|
194
|
+
* const apiSchema = await APISchema("my-schema", {
|
|
195
|
+
* zone: myZone,
|
|
196
|
+
* schema: `
|
|
197
|
+
* openapi: 3.0.0
|
|
198
|
+
* info:
|
|
199
|
+
* title: My API
|
|
200
|
+
* version: 1.0.0
|
|
201
|
+
* servers:
|
|
202
|
+
* - url: https://api.example.com
|
|
203
|
+
* paths:
|
|
204
|
+
* /users:
|
|
205
|
+
* get:
|
|
206
|
+
* operationId: getUsers
|
|
207
|
+
* responses:
|
|
208
|
+
* '200':
|
|
209
|
+
* description: Success
|
|
210
|
+
* /users/{id}:
|
|
211
|
+
* get:
|
|
212
|
+
* operationId: getUser
|
|
213
|
+
* parameters:
|
|
214
|
+
* - name: id
|
|
215
|
+
* in: path
|
|
216
|
+
* required: true
|
|
217
|
+
* schema:
|
|
218
|
+
* type: string
|
|
219
|
+
* `,
|
|
220
|
+
* });
|
|
221
|
+
*
|
|
222
|
+
* const shield = await APIShield("api-validation", {
|
|
223
|
+
* zone: myZone,
|
|
224
|
+
* schema: apiSchema,
|
|
225
|
+
* defaultAction: "none"
|
|
226
|
+
* });
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ## API Shield with typed OpenAPI object
|
|
230
|
+
*
|
|
231
|
+
* Use strongly-typed OpenAPI v3 objects for better IDE support
|
|
232
|
+
*
|
|
233
|
+
* import type { OpenAPIV3 } from "openapi-types";
|
|
234
|
+
*
|
|
235
|
+
* const apiSchema: OpenAPIV3.Document = {
|
|
236
|
+
* openapi: "3.0.0",
|
|
237
|
+
* info: {
|
|
238
|
+
* title: "My API",
|
|
239
|
+
* version: "1.0.0",
|
|
240
|
+
* },
|
|
241
|
+
* servers: [
|
|
242
|
+
* { url: "https://api.example.com" }
|
|
243
|
+
* ],
|
|
244
|
+
* paths: {
|
|
245
|
+
* "/users": {
|
|
246
|
+
* get: {
|
|
247
|
+
* operationId: "getUsers",
|
|
248
|
+
* responses: {
|
|
249
|
+
* "200": {
|
|
250
|
+
* description: "Success",
|
|
251
|
+
* content: {
|
|
252
|
+
* "application/json": {
|
|
253
|
+
* schema: {
|
|
254
|
+
* type: "array",
|
|
255
|
+
* items: {
|
|
256
|
+
* type: "object",
|
|
257
|
+
* properties: {
|
|
258
|
+
* id: { type: "string" },
|
|
259
|
+
* name: { type: "string" }
|
|
260
|
+
* }
|
|
261
|
+
* }
|
|
262
|
+
* }
|
|
263
|
+
* }
|
|
264
|
+
* }
|
|
265
|
+
* }
|
|
266
|
+
* }
|
|
267
|
+
* }
|
|
268
|
+
* }
|
|
269
|
+
* }
|
|
270
|
+
* };
|
|
271
|
+
*
|
|
272
|
+
* const schema = await APISchema("my-schema", {
|
|
273
|
+
* zone: myZone,
|
|
274
|
+
* schema: apiSchema,
|
|
275
|
+
* });
|
|
276
|
+
*
|
|
277
|
+
* const shield = await APIShield("api-validation", {
|
|
278
|
+
* zone: myZone,
|
|
279
|
+
* schema: schema,
|
|
280
|
+
* defaultAction: "none"
|
|
281
|
+
* });
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* ## API Shield with file
|
|
285
|
+
*
|
|
286
|
+
* Load schema from an external file with custom settings
|
|
287
|
+
*
|
|
288
|
+
* const schema = await APISchema("my-schema", {
|
|
289
|
+
* zone: "example.com",
|
|
290
|
+
* schema: new URL("file:///path/to/openapi.yaml"),
|
|
291
|
+
* name: "production-api-v2",
|
|
292
|
+
* });
|
|
293
|
+
*
|
|
294
|
+
* const shield = await APIShield("api-validation", {
|
|
295
|
+
* zone: "example.com",
|
|
296
|
+
* schema: schema,
|
|
297
|
+
* defaultAction: "none",
|
|
298
|
+
* mitigations: {
|
|
299
|
+
* "/users": {
|
|
300
|
+
* get: "none", // No mitigation for read operations
|
|
301
|
+
* post: "log", // Log violations for writes (requires paid plan)
|
|
302
|
+
* },
|
|
303
|
+
* "/users/{id}": {
|
|
304
|
+
* delete: "block" // Block destructive operations (requires paid plan)
|
|
305
|
+
* }
|
|
306
|
+
* },
|
|
307
|
+
* unknownOperationAction: "none"
|
|
308
|
+
* });
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ## Monitor API traffic without impact
|
|
312
|
+
*
|
|
313
|
+
* Use validation in monitoring mode to understand traffic patterns
|
|
314
|
+
*
|
|
315
|
+
* const schema = await APISchema("my-schema", {
|
|
316
|
+
* zone: myZone,
|
|
317
|
+
* schema: new URL("file:///path/to/api-schema.json"),
|
|
318
|
+
* });
|
|
319
|
+
*
|
|
320
|
+
* const monitoring = await APIShield("api-monitoring", {
|
|
321
|
+
* zone: myZone,
|
|
322
|
+
* schema: schema,
|
|
323
|
+
* defaultAction: "none"
|
|
324
|
+
* });
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* ## Log schema violations
|
|
328
|
+
*
|
|
329
|
+
* Track non-compliant requests without blocking (requires paid plan)
|
|
330
|
+
*
|
|
331
|
+
* const schema = await APISchema("my-schema", {
|
|
332
|
+
* zone: myZone,
|
|
333
|
+
* schema: new URL("file:///path/to/api-schema.json"),
|
|
334
|
+
* });
|
|
335
|
+
*
|
|
336
|
+
* const withLogging = await APIShield("api-logging", {
|
|
337
|
+
* zone: myZone,
|
|
338
|
+
* schema: schema,
|
|
339
|
+
* defaultAction: "log"
|
|
340
|
+
* });
|
|
341
|
+
*
|
|
342
|
+
* @example
|
|
343
|
+
* ## Protect critical endpoints with blanket mitigations
|
|
344
|
+
*
|
|
345
|
+
* Apply mitigations to entire paths or specific methods (requires paid plan)
|
|
346
|
+
*
|
|
347
|
+
* const schema = await APISchema("my-schema", {
|
|
348
|
+
* zone: myZone,
|
|
349
|
+
* schema: new URL("file:///path/to/api-schema.json"),
|
|
350
|
+
* });
|
|
351
|
+
*
|
|
352
|
+
* const protection = await APIShield("api-protection", {
|
|
353
|
+
* zone: myZone,
|
|
354
|
+
* schema: schema,
|
|
355
|
+
* defaultAction: "log",
|
|
356
|
+
* mitigations: {
|
|
357
|
+
* "/admin": "block", // Block all methods on admin endpoints
|
|
358
|
+
* "/payments": {
|
|
359
|
+
* post: "block", // Block payment creation
|
|
360
|
+
* put: "block" // Block payment updates
|
|
361
|
+
* },
|
|
362
|
+
* "/users/{id}": {
|
|
363
|
+
* delete: "block" // Block user deletion
|
|
364
|
+
* },
|
|
365
|
+
* "/public": "none", // Allow all methods on public endpoints
|
|
366
|
+
* "/products": "none" // Allow all methods on products
|
|
367
|
+
* }
|
|
368
|
+
* });
|
|
369
|
+
*
|
|
370
|
+
* @see https://developers.cloudflare.com/api-shield/security/schema-validation/
|
|
371
|
+
*/
|
|
372
|
+
export async function APIShield<S extends string | URL | OpenAPIV3.Document>(
|
|
373
|
+
id: string,
|
|
374
|
+
props: APIShieldProps<S>,
|
|
375
|
+
): Promise<APIShield<S extends string | URL ? OpenAPIV3.Document : S>> {
|
|
376
|
+
return (await _APIShield(id, {
|
|
377
|
+
...props,
|
|
378
|
+
// resolve file URLs to documents prior to passing input to the resource
|
|
379
|
+
// so that updates to the schema trigger changes to the resource
|
|
380
|
+
schema: await loadSchemaContent(props.schema),
|
|
381
|
+
})) as APIShield<S extends string | URL ? OpenAPIV3.Document : S>;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const _APIShield = Resource(
|
|
385
|
+
"cloudflare::APIShield",
|
|
386
|
+
{
|
|
387
|
+
// delete the api gateway operations in parallel
|
|
388
|
+
destroyStrategy: "parallel",
|
|
389
|
+
},
|
|
390
|
+
async function <const S extends OpenAPIV3.Document>(
|
|
391
|
+
this: Context<APIShield<S>>,
|
|
392
|
+
id: string,
|
|
393
|
+
props: Omit<APIShieldProps<S>, "schema"> & { schema: S },
|
|
394
|
+
): Promise<APIShield<S>> {
|
|
395
|
+
const api = await createCloudflareApi(props);
|
|
396
|
+
|
|
397
|
+
// Resolve zone ID and name
|
|
398
|
+
const zoneId =
|
|
399
|
+
typeof props.zone === "string"
|
|
400
|
+
? (await findZoneForHostname(api, props.zone)).zoneId
|
|
401
|
+
: props.zone.id;
|
|
402
|
+
|
|
403
|
+
if (this.phase === "delete") {
|
|
404
|
+
// Reset settings to default
|
|
405
|
+
await updateGlobalSettings(api, zoneId, {
|
|
406
|
+
validation_default_mitigation_action: "none",
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return this.destroy();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Update global settings
|
|
413
|
+
const defaultAction = props.defaultMitigation || "none";
|
|
414
|
+
await updateGlobalSettings(api, zoneId, {
|
|
415
|
+
validation_default_mitigation_action: defaultAction,
|
|
416
|
+
validation_override_mitigation_action: props.unknownOperationMitigation,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
const schema = await APISchema("schema", {
|
|
420
|
+
schema: props.schema,
|
|
421
|
+
zone: props.zone,
|
|
422
|
+
name: props.name ?? id,
|
|
423
|
+
enabled: props.enabled,
|
|
424
|
+
accountId: props.accountId,
|
|
425
|
+
apiKey: props.apiKey,
|
|
426
|
+
apiToken: props.apiToken,
|
|
427
|
+
baseUrl: props.baseUrl,
|
|
428
|
+
email: props.email,
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return this({
|
|
432
|
+
zoneId,
|
|
433
|
+
schema,
|
|
434
|
+
operations: await Promise.all(
|
|
435
|
+
parseSchemaOperations(schema.schema).map(async (parsedOp) => {
|
|
436
|
+
let operationAction = defaultAction;
|
|
437
|
+
const method = parsedOp.method.toLowerCase();
|
|
438
|
+
if (props.mitigations) {
|
|
439
|
+
const pathActions = props.mitigations[parsedOp.endpoint];
|
|
440
|
+
if (typeof pathActions === "string") {
|
|
441
|
+
// Blanket action for all methods on this path
|
|
442
|
+
operationAction = pathActions;
|
|
443
|
+
} else if (pathActions && typeof pathActions === "object") {
|
|
444
|
+
// Per-method configuration
|
|
445
|
+
const methodAction =
|
|
446
|
+
pathActions[method as keyof typeof pathActions];
|
|
447
|
+
if (methodAction) {
|
|
448
|
+
operationAction = methodAction;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return APIGatewayOperation(
|
|
453
|
+
// Create a deterministic ID for the operation
|
|
454
|
+
`${id}-${method}-${parsedOp.endpoint.replace(/[^a-z0-9]/gi, "-")}`,
|
|
455
|
+
{
|
|
456
|
+
zone: zoneId,
|
|
457
|
+
endpoint: parsedOp.endpoint,
|
|
458
|
+
host: parsedOp.host,
|
|
459
|
+
method: parsedOp.method,
|
|
460
|
+
mitigation: operationAction,
|
|
461
|
+
},
|
|
462
|
+
);
|
|
463
|
+
}),
|
|
464
|
+
),
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Helper functions for API calls
|
|
470
|
+
|
|
471
|
+
async function getGlobalSettings(
|
|
472
|
+
api: CloudflareApi,
|
|
473
|
+
zoneId: string,
|
|
474
|
+
): Promise<CloudflareGlobalSettings> {
|
|
475
|
+
const response = await api.get(
|
|
476
|
+
`/zones/${zoneId}/api_gateway/settings/schema_validation`,
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
if (!response.ok) {
|
|
480
|
+
await handleApiError(response, "getting", "global settings");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const data = (await response.json()) as { result: CloudflareGlobalSettings };
|
|
484
|
+
return data.result;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
async function updateGlobalSettings(
|
|
488
|
+
api: CloudflareApi,
|
|
489
|
+
zoneId: string,
|
|
490
|
+
params: Partial<CloudflareGlobalSettings>,
|
|
491
|
+
): Promise<void> {
|
|
492
|
+
const response = await api.put(
|
|
493
|
+
`/zones/${zoneId}/api_gateway/settings/schema_validation`,
|
|
494
|
+
params,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
if (!response.ok) {
|
|
498
|
+
await handleApiError(response, "updating", "global settings");
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Get global schema validation settings for a zone
|
|
504
|
+
*/
|
|
505
|
+
export async function getGlobalSettingsForZone(
|
|
506
|
+
api: CloudflareApi,
|
|
507
|
+
zoneId: string,
|
|
508
|
+
): Promise<CloudflareGlobalSettings> {
|
|
509
|
+
return getGlobalSettings(api, zoneId);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Cloudflare API response types
|
|
513
|
+
|
|
514
|
+
export interface CloudflareGlobalSettings {
|
|
515
|
+
validation_default_mitigation_action: APIMitigation;
|
|
516
|
+
validation_override_mitigation_action?: APIMitigation;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Extract operations from an OpenAPI schema
|
|
521
|
+
*/
|
|
522
|
+
export function parseSchemaOperations(schema: OpenAPIV3.Document): Array<{
|
|
523
|
+
method: string;
|
|
524
|
+
endpoint: string;
|
|
525
|
+
operationId?: string;
|
|
526
|
+
host: string;
|
|
527
|
+
}> {
|
|
528
|
+
const operations: Array<{
|
|
529
|
+
method: string;
|
|
530
|
+
endpoint: string;
|
|
531
|
+
operationId?: string;
|
|
532
|
+
host: string;
|
|
533
|
+
}> = [];
|
|
534
|
+
|
|
535
|
+
if (!schema?.paths) {
|
|
536
|
+
return operations;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Determine the host from servers
|
|
540
|
+
const defaultHost = extractHostFromSchema(schema);
|
|
541
|
+
|
|
542
|
+
// Extract operations from each path
|
|
543
|
+
for (const [path, pathItem] of Object.entries(schema.paths)) {
|
|
544
|
+
if (!pathItem || typeof pathItem !== "object") continue;
|
|
545
|
+
|
|
546
|
+
// Check each HTTP method
|
|
547
|
+
for (const method of [
|
|
548
|
+
"get",
|
|
549
|
+
"post",
|
|
550
|
+
"put",
|
|
551
|
+
"patch",
|
|
552
|
+
"delete",
|
|
553
|
+
"head",
|
|
554
|
+
"options",
|
|
555
|
+
"trace",
|
|
556
|
+
]) {
|
|
557
|
+
const operation = pathItem[method as keyof typeof pathItem];
|
|
558
|
+
if (
|
|
559
|
+
operation &&
|
|
560
|
+
typeof operation === "object" &&
|
|
561
|
+
!Array.isArray(operation)
|
|
562
|
+
) {
|
|
563
|
+
// Determine host for this operation (operation-level servers override global)
|
|
564
|
+
const operationHost = (operation as any).servers?.[0]?.url
|
|
565
|
+
? extractHostFromUrl((operation as any).servers[0].url)
|
|
566
|
+
: defaultHost;
|
|
567
|
+
|
|
568
|
+
operations.push({
|
|
569
|
+
method: method.toUpperCase(),
|
|
570
|
+
endpoint: path,
|
|
571
|
+
operationId: (operation as any).operationId,
|
|
572
|
+
host: operationHost,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return operations;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Extract host from OpenAPI schema servers
|
|
582
|
+
*/
|
|
583
|
+
function extractHostFromSchema(schema: OpenAPIV3.Document): string {
|
|
584
|
+
if (schema.servers && schema.servers.length > 0) {
|
|
585
|
+
return extractHostFromUrl(schema.servers[0].url);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Fallback to a default host
|
|
589
|
+
return "api.example.com";
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Extract hostname from a URL
|
|
594
|
+
*/
|
|
595
|
+
function extractHostFromUrl(url: string): string {
|
|
596
|
+
try {
|
|
597
|
+
const urlObj = new URL(url);
|
|
598
|
+
return urlObj.hostname;
|
|
599
|
+
} catch {
|
|
600
|
+
// If URL parsing fails, return the URL as-is (might be relative)
|
|
601
|
+
return url;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Helper function to load schema content from various sources
|
|
607
|
+
*/
|
|
608
|
+
async function loadSchemaContent(
|
|
609
|
+
schema: string | URL | OpenAPIV3.Document,
|
|
610
|
+
): Promise<OpenAPIV3.Document> {
|
|
611
|
+
// Handle string content (YAML/JSON)
|
|
612
|
+
try {
|
|
613
|
+
if (typeof schema === "string") {
|
|
614
|
+
if (schema.startsWith("http://") || schema.startsWith("https://")) {
|
|
615
|
+
return loadSchemaContent(new URL(schema));
|
|
616
|
+
} else if (schema.includes("\n")) {
|
|
617
|
+
// json or YAML
|
|
618
|
+
return tryParse(schema);
|
|
619
|
+
} else {
|
|
620
|
+
try {
|
|
621
|
+
return tryParse(await fs.readFile(schema, "utf-8"));
|
|
622
|
+
} catch {
|
|
623
|
+
// maybe minified JSON
|
|
624
|
+
return tryParse(schema);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
} else if (schema instanceof URL) {
|
|
628
|
+
return yaml.parse(await fetchUrl(schema));
|
|
629
|
+
} else if (typeof schema === "object") {
|
|
630
|
+
return schema as OpenAPIV3.Document;
|
|
631
|
+
} else {
|
|
632
|
+
// should be unreachable
|
|
633
|
+
throw SchemaError(new Error(`Unsupported schema type: ${typeof schema}`));
|
|
634
|
+
}
|
|
635
|
+
} catch (err) {
|
|
636
|
+
throw SchemaError(err);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const SchemaError = (cause?: any) =>
|
|
641
|
+
new Error(
|
|
642
|
+
`Invalid OpenAPI schema. Please provide a:
|
|
643
|
+
1. a string containing OpenAPI v3.0 schema (in YAML or JSON format)
|
|
644
|
+
2. a string path to a file containing OpenAPI v3.0 schema (in YAML or JSON format)
|
|
645
|
+
3. a file://, http:// or https:// URL pointing to an OpenAPI v3.0 schema (in YAML or JSON format)
|
|
646
|
+
4. a literal OpenAPI v3.0 schema object`,
|
|
647
|
+
{ cause },
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
function tryParse(schema: string): OpenAPIV3.Document {
|
|
651
|
+
try {
|
|
652
|
+
return yaml.parse(schema);
|
|
653
|
+
} catch {
|
|
654
|
+
return JSON.parse(schema);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function fetchUrl(url: URL): Promise<string> {
|
|
659
|
+
if (url.protocol === "file:") {
|
|
660
|
+
// Read from local filesystem for file:// URLs
|
|
661
|
+
return await fs.readFile(url.pathname, "utf-8");
|
|
662
|
+
} else if (url.protocol === "http:" || url.protocol === "https:") {
|
|
663
|
+
// Fetch from remote for http/https URLs
|
|
664
|
+
const response = await fetch(url.toString());
|
|
665
|
+
if (!response.ok) {
|
|
666
|
+
throw new Error(
|
|
667
|
+
`Failed to fetch schema from URL: ${response.statusText}`,
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
return await response.text();
|
|
671
|
+
} else {
|
|
672
|
+
throw new Error(
|
|
673
|
+
`Unsupported URL protocol: ${url.protocol}. Only http:, https:, and file: are supported.`,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
}
|