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,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
+ }