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
@@ -0,0 +1,340 @@
1
+ import type { Context } from "../context.ts";
2
+ import { Resource } from "../resource.ts";
3
+ import { handleApiError } from "./api-error.ts";
4
+ import type { APIMitigation } from "./api-mitigation.ts";
5
+ import {
6
+ createCloudflareApi,
7
+ type CloudflareApi,
8
+ type CloudflareApiOptions,
9
+ } from "./api.ts";
10
+ import type { Zone } from "./zone.ts";
11
+
12
+ /**
13
+ * HTTP methods supported by API Gateway
14
+ */
15
+ export type HTTPMethod =
16
+ | "get"
17
+ | "post"
18
+ | "put"
19
+ | "patch"
20
+ | "delete"
21
+ | "head"
22
+ | "options"
23
+ | "trace";
24
+
25
+ /**
26
+ * Properties for creating or updating an API Operation
27
+ */
28
+ export interface APIGatewayOperationProps extends CloudflareApiOptions {
29
+ /**
30
+ * The zone this operation belongs to
31
+ */
32
+ zone: string | Zone;
33
+
34
+ /**
35
+ * The API endpoint path (can contain path variables like /users/{id})
36
+ */
37
+ endpoint: string;
38
+
39
+ /**
40
+ * The host for this operation
41
+ */
42
+ host: string;
43
+
44
+ /**
45
+ * The HTTP method (GET, POST, PUT, DELETE, etc.)
46
+ */
47
+ method: string;
48
+
49
+ /**
50
+ * Mitigation action for this operation
51
+ * @default "none"
52
+ */
53
+ mitigation?: APIMitigation;
54
+ }
55
+
56
+ /**
57
+ * API Operation output
58
+ */
59
+ export interface APIGatewayOperation
60
+ extends Resource<"cloudflare::APIGatewayOperation"> {
61
+ /**
62
+ * Zone ID
63
+ */
64
+ zoneId: string;
65
+
66
+ /**
67
+ * Zone name
68
+ */
69
+ zoneName: string;
70
+
71
+ /**
72
+ * Operation ID assigned by Cloudflare
73
+ */
74
+ operationId: string;
75
+
76
+ /**
77
+ * The API endpoint path
78
+ */
79
+ endpoint: string;
80
+
81
+ /**
82
+ * The host for this operation
83
+ */
84
+ host: string;
85
+
86
+ /**
87
+ * The HTTP method
88
+ */
89
+ method: string;
90
+
91
+ /**
92
+ * The mitigation action for this operation
93
+ */
94
+ action: APIMitigation;
95
+ }
96
+
97
+ /**
98
+ * Cloudflare API Gateway Operation manages individual API endpoints that can be
99
+ * monitored, secured, and configured through Cloudflare's API Shield.
100
+ *
101
+ * Operations are the building blocks for API management, representing specific
102
+ * HTTP method + endpoint + host combinations that your API exposes.
103
+ *
104
+ * @example
105
+ * ## Basic API operation
106
+ *
107
+ * Create a simple GET endpoint for user retrieval
108
+ *
109
+ * const getUserOp = await APIGatewayOperation("get-users", {
110
+ * zone: myZone,
111
+ * endpoint: "/users",
112
+ * host: "api.example.com",
113
+ * method: "GET"
114
+ * });
115
+ *
116
+ * @example
117
+ * ## API operation with path parameters
118
+ *
119
+ * Create an operation that includes path variables
120
+ *
121
+ * const getUserByIdOp = await APIGatewayOperation("get-user-by-id", {
122
+ * zone: "api.example.com",
123
+ * endpoint: "/users/{id}",
124
+ * host: "api.example.com",
125
+ * method: "GET"
126
+ * });
127
+ *
128
+ * @example
129
+ * ## RESTful CRUD operations
130
+ *
131
+ * Create a complete set of CRUD operations for a resource
132
+ *
133
+ * const createUserOp = await APIGatewayOperation("create-user", {
134
+ * zone: myZone,
135
+ * endpoint: "/users",
136
+ * host: "api.example.com",
137
+ * method: "POST"
138
+ * });
139
+ *
140
+ * const updateUserOp = await APIGatewayOperation("update-user", {
141
+ * zone: myZone,
142
+ * endpoint: "/users/{id}",
143
+ * host: "api.example.com",
144
+ * method: "PUT"
145
+ * });
146
+ *
147
+ * const deleteUserOp = await APIGatewayOperation("delete-user", {
148
+ * zone: myZone,
149
+ * endpoint: "/users/{id}",
150
+ * host: "api.example.com",
151
+ * method: "DELETE"
152
+ * });
153
+ *
154
+ * @see https://developers.cloudflare.com/api/resources/api_gateway/subresources/operations/
155
+ * @see https://developers.cloudflare.com/api-shield/management-and-monitoring/endpoint-management/
156
+ */
157
+ export const APIGatewayOperation = Resource(
158
+ "cloudflare::APIGatewayOperation",
159
+ async function (
160
+ this: Context<APIGatewayOperation>,
161
+ _id: string,
162
+ props: APIGatewayOperationProps,
163
+ ): Promise<APIGatewayOperation> {
164
+ const api = await createCloudflareApi(props);
165
+
166
+ // Resolve zone ID and name
167
+ const zoneId = typeof props.zone === "string" ? props.zone : props.zone.id;
168
+ const zoneName =
169
+ typeof props.zone === "string" ? props.zone : props.zone.name;
170
+
171
+ if (this.phase === "delete") {
172
+ if (this.output?.operationId) {
173
+ await deleteOperation(api, zoneId, this.output.operationId);
174
+ }
175
+ return this.destroy();
176
+ }
177
+ let operationId: string;
178
+ if (this.phase === "update") {
179
+ if (
180
+ props.endpoint !== this.output?.endpoint ||
181
+ props.host !== this.output?.host ||
182
+ props.method !== this.output?.method
183
+ ) {
184
+ // delete and then re-create (cloudflare doesn't allow updating these properties)
185
+ this.replace(true);
186
+ }
187
+ if (props.mitigation !== this.output?.action) {
188
+ // we can update the mitigation action
189
+ await updateOperationSettings(api, zoneId, this.output.operationId, {
190
+ mitigation_action: props.mitigation ?? null,
191
+ });
192
+ }
193
+ operationId = this.output.operationId;
194
+ } else {
195
+ const operation = await createOperation(api, zoneId, props);
196
+ operationId = operation.operation_id;
197
+
198
+ if (props.mitigation) {
199
+ await updateOperationSettings(api, zoneId, operationId, {
200
+ mitigation_action: props.mitigation ?? "none",
201
+ });
202
+ }
203
+ }
204
+
205
+ return this({
206
+ zoneId,
207
+ zoneName,
208
+ operationId,
209
+ endpoint: props.endpoint,
210
+ host: props.host,
211
+ method: props.method.toUpperCase(),
212
+ action: props.mitigation ?? null,
213
+ });
214
+ },
215
+ );
216
+
217
+ async function createOperation(
218
+ api: CloudflareApi,
219
+ zoneId: string,
220
+ props: APIGatewayOperationProps,
221
+ ) {
222
+ // Create the operation
223
+ const response = await api.post(
224
+ `/zones/${zoneId}/api_gateway/operations/item`,
225
+ {
226
+ endpoint: props.endpoint,
227
+ host: props.host,
228
+ method: props.method.toUpperCase(),
229
+ },
230
+ );
231
+
232
+ if (!response.ok) {
233
+ await handleApiError(
234
+ response,
235
+ "creating",
236
+ "operation",
237
+ `${props.method} ${props.endpoint}`,
238
+ );
239
+ }
240
+
241
+ const data = (await response.json()) as {
242
+ result: {
243
+ operation_id: string;
244
+ endpoint: string;
245
+ host: string;
246
+ method: string;
247
+ last_updated: string;
248
+ };
249
+ };
250
+
251
+ return data.result;
252
+ }
253
+
254
+ async function deleteOperation(
255
+ api: CloudflareApi,
256
+ zoneId: string,
257
+ operationId: string,
258
+ ) {
259
+ const deleteResponse = await api.delete(
260
+ `/zones/${zoneId}/api_gateway/operations/${operationId}`,
261
+ );
262
+
263
+ if (!deleteResponse.ok && deleteResponse.status !== 404) {
264
+ await handleApiError(deleteResponse, "deleting", "operation", operationId);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Update validation settings for a specific operation
270
+ */
271
+ async function updateOperationSettings(
272
+ api: CloudflareApi,
273
+ zoneId: string,
274
+ operationId: string,
275
+ params: {
276
+ mitigation_action: APIMitigation;
277
+ },
278
+ ): Promise<void> {
279
+ const response = await api.put(
280
+ `/zones/${zoneId}/schema_validation/settings/operations/${operationId}`,
281
+ params,
282
+ );
283
+
284
+ if (!response.ok) {
285
+ await handleApiError(
286
+ response,
287
+ "updating",
288
+ "operation settings",
289
+ operationId,
290
+ );
291
+ }
292
+ }
293
+
294
+ export interface CloudflareOperation {
295
+ operation_id: string;
296
+ method: string;
297
+ host: string;
298
+ endpoint: string;
299
+ }
300
+
301
+ export async function getOperations(
302
+ api: CloudflareApi,
303
+ zoneId: string,
304
+ ): Promise<CloudflareOperation[]> {
305
+ const response = await api.get(`/zones/${zoneId}/api_gateway/operations`);
306
+
307
+ if (!response.ok) {
308
+ await handleApiError(response, "getting", "operations");
309
+ }
310
+
311
+ return ((await response.json()) as { result: CloudflareOperation[] }).result;
312
+ }
313
+
314
+ export async function getOperationSchemaValidationSetting(
315
+ api: CloudflareApi,
316
+ zoneId: string,
317
+ operationId: string,
318
+ ) {
319
+ const response = await api.get(
320
+ `/zones/${zoneId}/schema_validation/settings/operations/${operationId}`,
321
+ );
322
+
323
+ if (!response.ok) {
324
+ await handleApiError(
325
+ response,
326
+ "getting",
327
+ "operation mitigation status",
328
+ operationId,
329
+ );
330
+ }
331
+
332
+ const data = (await response.json()) as {
333
+ result: {
334
+ mitigation_action: APIMitigation;
335
+ operation_id: string;
336
+ };
337
+ };
338
+
339
+ return data.result;
340
+ }
@@ -0,0 +1,22 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+
3
+ /**
4
+ * Specifies the mitigation action to apply when a request does not conform to
5
+ * the schema for this operation:
6
+ * - `"log"`: Log the request.
7
+ * - `"block"`: Deny access to the site.
8
+ * - `"none"`: Skip mitigation for this operation.
9
+ * - `null`: Clear any mitigation action.
10
+ */
11
+ export type APIMitigation = "none" | "log" | "block" | null;
12
+
13
+ /**
14
+ * Mitigation actions for a given API schema.
15
+ */
16
+ export type APIMitigations<S extends OpenAPIV3.Document> = {
17
+ [path in keyof S["paths"]]?:
18
+ | APIMitigation
19
+ | {
20
+ [method in keyof S["paths"][path]]?: APIMitigation;
21
+ };
22
+ };
@@ -0,0 +1,364 @@
1
+ import type { OpenAPIV3 } from "openapi-types";
2
+ import * as yaml from "yaml";
3
+ import type { Context } from "../context.ts";
4
+ import { Resource } from "../resource.ts";
5
+ import { CloudflareApiError, handleApiError } from "./api-error.ts";
6
+ import {
7
+ createCloudflareApi,
8
+ type CloudflareApi,
9
+ type CloudflareApiOptions,
10
+ } from "./api.ts";
11
+ import type { Zone } from "./zone.ts";
12
+ import { findZoneForHostname } from "./zone.ts";
13
+
14
+ /**
15
+ * Properties for creating or updating a Schema
16
+ */
17
+ export interface APISchemaProps<S extends OpenAPIV3.Document>
18
+ extends CloudflareApiOptions {
19
+ /**
20
+ * The zone to upload the schema to
21
+ */
22
+ zone: string | Zone;
23
+
24
+ /**
25
+ * OpenAPI v3.0.x schema content (YAML string, JSON string, or OpenAPI object)
26
+ * Provide either this or schemaFile
27
+ *
28
+ * Note: Cloudflare only supports OpenAPI v3.0.x, not v3.1
29
+ */
30
+ schema: S;
31
+
32
+ /**
33
+ * Name for the schema
34
+ * @default resource id
35
+ */
36
+ name?: string;
37
+
38
+ /**
39
+ * Enable validation immediately after upload
40
+ *
41
+ * Warning: will trigger a replace when disabling validation.
42
+ *
43
+ * @default true
44
+ */
45
+ enabled?: boolean;
46
+ }
47
+
48
+ /**
49
+ * APISchema resource attributes.
50
+ */
51
+ export interface APISchema<S extends OpenAPIV3.Document = OpenAPIV3.Document>
52
+ extends Resource<"cloudflare::APISchema"> {
53
+ /**
54
+ * Schema ID
55
+ */
56
+ id: string;
57
+
58
+ /**
59
+ * Name for the schema
60
+ */
61
+ name: string;
62
+
63
+ /**
64
+ * The API Schema
65
+ */
66
+ schema: S;
67
+
68
+ /**
69
+ * Source of the schema
70
+ */
71
+ source: string;
72
+
73
+ /**
74
+ * Whether validation is enabled
75
+ */
76
+ enabled: boolean;
77
+ }
78
+
79
+ /**
80
+ * Cloudflare API Gateway Schema manages OpenAPI v3 schemas for API validation.
81
+ *
82
+ * @example
83
+ * ## Basic schema upload with inline YAML
84
+ *
85
+ * const apiSchema = await APISchema("my-api-schema", {
86
+ * zone: myZone,
87
+ * name: "my-api-v1"
88
+ * schema: `
89
+ * openapi: 3.0.0
90
+ * info:
91
+ * title: My API
92
+ * version: 1.0.0
93
+ * servers:
94
+ * - url: https://api.example.com
95
+ * paths:
96
+ * /users:
97
+ * get:
98
+ * operationId: getUsers
99
+ * responses:
100
+ * '200':
101
+ * description: Success
102
+ * `,
103
+ * });
104
+ *
105
+ * @example
106
+ * ## Schema upload from file
107
+ *
108
+ * const fileSchema = await APISchema("api-schema-from-file", {
109
+ * zone: "example.com",
110
+ * schemaFile: "./openapi.yaml",
111
+ * name: "production-api-v2",
112
+ * enabled: false // Upload but don't enable validation yet
113
+ * });
114
+ *
115
+ * @example
116
+ * ## Schema with typed OpenAPI object
117
+ *
118
+ * import type { OpenAPIV3 } from "openapi-types";
119
+ *
120
+ * const typedSchema: OpenAPIV3.Document = {
121
+ * openapi: "3.0.0",
122
+ * info: { title: "Typed API", version: "1.0.0" },
123
+ * paths: {
124
+ * "/health": {
125
+ * get: {
126
+ * operationId: "healthCheck",
127
+ * responses: { "200": { description: "OK" } }
128
+ * }
129
+ * }
130
+ * }
131
+ * };
132
+ *
133
+ * const schema = await APISchema("typed-schema", {
134
+ * zone: myZone,
135
+ * schema: typedSchema
136
+ * });
137
+ */
138
+ export const APISchema = Resource("cloudflare::APISchema", async function <
139
+ S extends OpenAPIV3.Document,
140
+ >(this: Context<APISchema<S>>, id: string, props: APISchemaProps<S>): Promise<
141
+ APISchema<S>
142
+ > {
143
+ const api = await createCloudflareApi(props);
144
+
145
+ // Resolve zone ID and name
146
+ const zoneId =
147
+ typeof props.zone === "string"
148
+ ? (await findZoneForHostname(api, props.zone)).zoneId
149
+ : props.zone.id;
150
+
151
+ if (this.phase === "delete") {
152
+ if (this.output?.id) {
153
+ await deleteSchema(api, zoneId, this.output.id);
154
+ }
155
+ return this.destroy();
156
+ }
157
+
158
+ // Load schema content
159
+ const parsedSchema = props.schema;
160
+
161
+ let schemaDetails: CloudflareSchemaDetails;
162
+
163
+ if (this.phase === "update" && this.output?.id) {
164
+ // Check if we need to replace due to name, schema content change, or disabling validation
165
+ if (
166
+ props.name !== this.output.name ||
167
+ JSON.stringify(parsedSchema) !== JSON.stringify(this.output.schema) ||
168
+ (this.output.enabled === true && props.enabled === false)
169
+ ) {
170
+ // Name, schema content changed, or trying to disable validation - need to replace
171
+ this.replace();
172
+ }
173
+
174
+ // Update existing schema (can only update validation_enabled)
175
+ schemaDetails = await updateSchema(api, zoneId, this.output.id, {
176
+ validation_enabled: props.enabled !== false,
177
+ });
178
+ } else {
179
+ // Create new schema
180
+ schemaDetails = await uploadSchema(api, zoneId, {
181
+ file: yaml.stringify(parsedSchema),
182
+ name: props.name || id,
183
+ validation_enabled: props.enabled !== false,
184
+ });
185
+ }
186
+
187
+ return this({
188
+ id: schemaDetails.id,
189
+ name: schemaDetails.name,
190
+ schema: parsedSchema as any,
191
+ source: schemaDetails.source,
192
+ enabled: schemaDetails.validationEnabled,
193
+ });
194
+ });
195
+
196
+ // API helper functions
197
+
198
+ interface CloudflareSchemaDetails {
199
+ id: string;
200
+ name: string;
201
+ source: string;
202
+ validationEnabled: boolean;
203
+ createdAt: string;
204
+ }
205
+
206
+ interface CloudflareSchema {
207
+ schema_id: string;
208
+ name: string;
209
+ kind: string;
210
+ source: string;
211
+ validation_enabled: boolean;
212
+ created_at: string;
213
+ size?: number;
214
+ is_learned?: boolean;
215
+ }
216
+
217
+ async function uploadSchema(
218
+ api: CloudflareApi,
219
+ zoneId: string,
220
+ params: {
221
+ file: string;
222
+ name: string;
223
+ validation_enabled?: boolean;
224
+ },
225
+ ): Promise<CloudflareSchemaDetails> {
226
+ const body = {
227
+ source: params.file,
228
+ name: params.name,
229
+ kind: "openapi_v3",
230
+ validation_enabled: params.validation_enabled ?? true,
231
+ };
232
+
233
+ const response = await api.post(
234
+ `/zones/${zoneId}/schema_validation/schemas`,
235
+ body,
236
+ );
237
+
238
+ if (!response.ok) {
239
+ await handleApiError(response, "uploading", "schema", params.name);
240
+ }
241
+
242
+ const data = (await response.json()) as {
243
+ result: CloudflareSchema;
244
+ };
245
+ return {
246
+ id: data.result.schema_id,
247
+ name: data.result.name,
248
+ source: data.result.source,
249
+ validationEnabled: data.result.validation_enabled,
250
+ createdAt: data.result.created_at,
251
+ };
252
+ }
253
+
254
+ async function updateSchema(
255
+ api: CloudflareApi,
256
+ zoneId: string,
257
+ schemaId: string,
258
+ params: {
259
+ validation_enabled: boolean;
260
+ },
261
+ ): Promise<CloudflareSchemaDetails> {
262
+ const response = await api.patch(
263
+ `/zones/${zoneId}/schema_validation/schemas/${schemaId}`,
264
+ params,
265
+ );
266
+
267
+ if (!response.ok) {
268
+ await handleApiError(response, "updating", "schema", schemaId);
269
+ }
270
+
271
+ const data = (await response.json()) as { result: CloudflareSchema };
272
+ return {
273
+ id: data.result.schema_id,
274
+ name: data.result.name,
275
+ source: data.result.source,
276
+ validationEnabled: data.result.validation_enabled,
277
+ createdAt: data.result.created_at,
278
+ };
279
+ }
280
+
281
+ export async function deleteSchema(
282
+ api: CloudflareApi,
283
+ zoneId: string,
284
+ schemaId: string,
285
+ ): Promise<void> {
286
+ const response = await api.delete(
287
+ `/zones/${zoneId}/schema_validation/schemas/${schemaId}`,
288
+ );
289
+ const data = (await response.json()) as {
290
+ success: boolean;
291
+ errors: {
292
+ code: number;
293
+ message: string;
294
+ }[];
295
+ };
296
+ if (response.status === 404) {
297
+ return;
298
+ } else if (response.status === 400 && data.errors[0].code === 19400) {
299
+ // Bad request: schema with this ID does not exist
300
+ return;
301
+ }
302
+
303
+ if (!response.ok) {
304
+ await handleApiError(response, "deleting", "schema", schemaId);
305
+ } else if (!data.success) {
306
+ throw new CloudflareApiError(data.errors[0].message, response);
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Get schema details
312
+ */
313
+ export async function getSchema(
314
+ api: CloudflareApi,
315
+ zoneId: string,
316
+ schemaId: string,
317
+ ): Promise<CloudflareSchemaDetails | null> {
318
+ const response = await api.get(
319
+ `/zones/${zoneId}/schema_validation/schemas/${schemaId}`,
320
+ );
321
+
322
+ if (response.status === 404) {
323
+ return null;
324
+ }
325
+
326
+ if (!response.ok) {
327
+ await handleApiError(response, "getting", "schema", schemaId);
328
+ }
329
+
330
+ const data = (await response.json()) as { result: CloudflareSchema };
331
+ return {
332
+ id: data.result.schema_id,
333
+ name: data.result.name,
334
+ source: data.result.source,
335
+ validationEnabled: data.result.validation_enabled,
336
+ createdAt: data.result.created_at,
337
+ };
338
+ }
339
+
340
+ /**
341
+ * List all schemas in a zone
342
+ */
343
+ export async function listSchemas(
344
+ api: CloudflareApi,
345
+ zoneId: string,
346
+ ): Promise<CloudflareSchemaDetails[]> {
347
+ const response = await api.get(`/zones/${zoneId}/schema_validation/schemas`);
348
+
349
+ if (!response.ok) {
350
+ await handleApiError(response, "listing", "schemas", zoneId);
351
+ }
352
+
353
+ const data = (await response.json()) as {
354
+ result: CloudflareSchema[];
355
+ };
356
+
357
+ return data.result.map((schema) => ({
358
+ id: schema.schema_id,
359
+ name: schema.name,
360
+ source: schema.source,
361
+ validationEnabled: schema.validation_enabled,
362
+ createdAt: schema.created_at,
363
+ }));
364
+ }