@shipworthy/openapi-parser 0.0.1 → 1.0.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/README.md +10 -7
- package/dist/errors.d.ts +42 -14
- package/dist/errors.js +3 -3
- package/dist/hoist.js +48 -43
- package/dist/http.js +6 -9
- package/dist/load.js +18 -18
- package/dist/loader.js +7 -7
- package/dist/parsers.js +7 -18
- package/dist/pipeline.js +78 -89
- package/dist/pointer.d.ts +3 -0
- package/dist/pointer.js +26 -22
- package/dist/run.js +18 -18
- package/dist/schemas/3.1/callback.js +2 -2
- package/dist/schemas/3.1/example.js +2 -2
- package/dist/schemas/3.1/header.js +3 -3
- package/dist/schemas/3.1/media-type.js +2 -2
- package/dist/schemas/3.1/object.js +2 -5
- package/dist/schemas/3.1/parameter.js +3 -3
- package/dist/schemas/3.1/responses.js +2 -2
- package/dist/upgraders/3.0-to-3.1.js +34 -31
- package/dist/url.d.ts +1 -1
- package/dist/url.js +42 -45
- package/dist/yaml.js +50 -46
- package/package.json +7 -9
package/dist/run.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Array, Cache, Effect, Predicate, Ref, Schema } from "effect";
|
|
1
|
+
import { Array, Cache, Effect, Option, Predicate, Record as Rec, Ref, Schema, String as Str, } from "effect";
|
|
2
2
|
import { ParserFailure, groupFailure, isLeafFailure, missingPointerFailure, syncFailure, } from "./errors.js";
|
|
3
3
|
import { hoistExternalRefs } from "./hoist.js";
|
|
4
4
|
import { loadDocument, makeContext } from "./loader.js";
|
|
5
5
|
import { Options } from "./plugins.js";
|
|
6
|
-
import { cloneData, joinPointer, parsePointer } from "./pointer.js";
|
|
6
|
+
import { childAt, cloneData, joinPointer, parsePointer } from "./pointer.js";
|
|
7
7
|
import { inlineRemaining, runSchemaPipeline } from "./pipeline.js";
|
|
8
8
|
import { isOpenApi3, upgradeOpenApi3_0 } from "./upgraders/3.0-to-3.1.js";
|
|
9
9
|
import { getHash, isHttp, normalizeRef, resolveAgainst, stripHash } from "./url.js";
|
|
@@ -112,7 +112,7 @@ function isBundledRef(record) {
|
|
|
112
112
|
const ref = record.$ref;
|
|
113
113
|
if (typeof ref !== "string" || !isHttp(ref))
|
|
114
114
|
return true;
|
|
115
|
-
return
|
|
115
|
+
return Array.every(Rec.keys(record), (key) => referenceKeys.has(key));
|
|
116
116
|
}
|
|
117
117
|
function* externalRefs(node, kept) {
|
|
118
118
|
const seen = new Set();
|
|
@@ -128,14 +128,14 @@ function* externalRefs(node, kept) {
|
|
|
128
128
|
}
|
|
129
129
|
const record = current;
|
|
130
130
|
if (typeof record.$ref === "string" &&
|
|
131
|
-
!
|
|
131
|
+
!Str.startsWith("#")(record.$ref) &&
|
|
132
132
|
!kept.has(record.$ref) &&
|
|
133
133
|
isBundledRef(record)) {
|
|
134
134
|
yield record.$ref;
|
|
135
135
|
}
|
|
136
136
|
if (typeof record.externalValue === "string" && !kept.has(record.externalValue))
|
|
137
137
|
yield record.externalValue;
|
|
138
|
-
stack.push(...
|
|
138
|
+
stack.push(...Rec.values(record));
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
@@ -153,7 +153,7 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
153
153
|
next = resolveAgainst(base, record.$id);
|
|
154
154
|
ids.set(stripHash(next), record);
|
|
155
155
|
}
|
|
156
|
-
if (typeof record.$ref === "string" && !
|
|
156
|
+
if (typeof record.$ref === "string" && !Str.startsWith("#")(record.$ref)) {
|
|
157
157
|
if (keep(record.$ref))
|
|
158
158
|
kept.add(record.$ref);
|
|
159
159
|
else
|
|
@@ -164,8 +164,8 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
164
164
|
? discriminator.mapping
|
|
165
165
|
: undefined;
|
|
166
166
|
if (mapping) {
|
|
167
|
-
for (const [key, value] of
|
|
168
|
-
if (typeof value !== "string" ||
|
|
167
|
+
for (const [key, value] of Rec.toEntries(mapping)) {
|
|
168
|
+
if (typeof value !== "string" || Str.startsWith("#")(value))
|
|
169
169
|
continue;
|
|
170
170
|
if (keep(value))
|
|
171
171
|
kept.add(value);
|
|
@@ -179,7 +179,7 @@ function absolutize(node, base, ids, keep, kept, seen = new Set()) {
|
|
|
179
179
|
else
|
|
180
180
|
record.externalValue = resolveAgainst(base, record.externalValue);
|
|
181
181
|
}
|
|
182
|
-
for (const [key, value] of
|
|
182
|
+
for (const [key, value] of Rec.toEntries(record)) {
|
|
183
183
|
if (key === "$ref" || key === "$id")
|
|
184
184
|
continue;
|
|
185
185
|
absolutize(value, next, ids, keep, kept, seen);
|
|
@@ -193,7 +193,7 @@ function rewriteIdRefs(root, ids) {
|
|
|
193
193
|
pointers.set(url, pointer);
|
|
194
194
|
}
|
|
195
195
|
walkRecords(root, (record) => {
|
|
196
|
-
if (typeof record.$ref !== "string" ||
|
|
196
|
+
if (typeof record.$ref !== "string" || Str.startsWith("#")(record.$ref))
|
|
197
197
|
return;
|
|
198
198
|
const url = stripHash(record.$ref);
|
|
199
199
|
const pointer = pointers.get(url);
|
|
@@ -225,8 +225,8 @@ function rewriteMappings(root, placed, ids) {
|
|
|
225
225
|
: undefined;
|
|
226
226
|
if (!mapping)
|
|
227
227
|
return;
|
|
228
|
-
for (const [key, value] of
|
|
229
|
-
if (typeof value !== "string" ||
|
|
228
|
+
for (const [key, value] of Rec.toEntries(mapping)) {
|
|
229
|
+
if (typeof value !== "string" || Str.startsWith("#")(value))
|
|
230
230
|
continue;
|
|
231
231
|
const url = stripHash(value);
|
|
232
232
|
const hash = getHash(value);
|
|
@@ -242,7 +242,7 @@ function rewriteMappings(root, placed, ids) {
|
|
|
242
242
|
function findPointer(root, target, pointer = "#", seen = new Set()) {
|
|
243
243
|
if (root === target)
|
|
244
244
|
return pointer;
|
|
245
|
-
if (
|
|
245
|
+
if (!Predicate.isObjectOrArray(root) || seen.has(root))
|
|
246
246
|
return undefined;
|
|
247
247
|
seen.add(root);
|
|
248
248
|
if (Array.isArray(root)) {
|
|
@@ -253,7 +253,7 @@ function findPointer(root, target, pointer = "#", seen = new Set()) {
|
|
|
253
253
|
}
|
|
254
254
|
return undefined;
|
|
255
255
|
}
|
|
256
|
-
for (const [key, value] of
|
|
256
|
+
for (const [key, value] of Rec.toEntries(root)) {
|
|
257
257
|
const found = findPointer(value, target, joinPointer(pointer, key), seen);
|
|
258
258
|
if (found)
|
|
259
259
|
return found;
|
|
@@ -271,7 +271,7 @@ function walkRecords(node, visit, seen = new Set()) {
|
|
|
271
271
|
}
|
|
272
272
|
const record = node;
|
|
273
273
|
visit(record);
|
|
274
|
-
for (const value of
|
|
274
|
+
for (const value of Rec.values(record))
|
|
275
275
|
walkRecords(value, visit, seen);
|
|
276
276
|
}
|
|
277
277
|
const inlineExternalValues = Effect.fnUntraced(function* (root, cache, keep, kept, options, failures, recorded) {
|
|
@@ -304,10 +304,10 @@ function valueAt(document, hash) {
|
|
|
304
304
|
const tokens = parsePointer(hash);
|
|
305
305
|
let current = document;
|
|
306
306
|
for (const token of tokens) {
|
|
307
|
-
|
|
307
|
+
const child = childAt(current, token);
|
|
308
|
+
if (Option.isNone(child))
|
|
308
309
|
throw missingPointerFailure(token, hash);
|
|
309
|
-
|
|
310
|
-
current = current[token];
|
|
310
|
+
current = child.value;
|
|
311
311
|
}
|
|
312
312
|
return current;
|
|
313
313
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Schema, String as Str } from "effect";
|
|
2
2
|
import { openApiMap } from "./object.js";
|
|
3
3
|
import { OpenApiPathItem } from "./path-item.js";
|
|
4
|
-
export const OpenApiCallback = openApiMap(Schema.String.check(Schema.makeFilter((key) => !
|
|
4
|
+
export const OpenApiCallback = openApiMap(Schema.String.check(Schema.makeFilter((key) => !Str.startsWith("x-")(key))), Schema.suspend(() => OpenApiPathItem));
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { openApiObject } from "./object.js";
|
|
3
3
|
export const OpenApiExample = openApiObject({
|
|
4
4
|
summary: Schema.optionalKey(Schema.String),
|
|
5
5
|
description: Schema.optionalKey(Schema.String),
|
|
6
6
|
value: Schema.optionalKey(Schema.Unknown),
|
|
7
7
|
externalValue: Schema.optionalKey(Schema.String),
|
|
8
|
-
}).check(Schema.makeFilter((value) =>
|
|
8
|
+
}).check(Schema.makeFilter((value) => Rec.has(value, "value") && value.externalValue !== undefined
|
|
9
9
|
? "Example value and externalValue are mutually exclusive"
|
|
10
10
|
: undefined));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { OpenApiExample } from "./example.js";
|
|
3
3
|
import { OpenApiMediaType } from "./media-type.js";
|
|
4
4
|
import { openApiObject } from "./object.js";
|
|
@@ -19,10 +19,10 @@ export const OpenApiHeader = openApiObject(fields).check(Schema.makeFilter((valu
|
|
|
19
19
|
if ((value.schema === undefined) === (value.content === undefined)) {
|
|
20
20
|
return "A header must have exactly one of schema or content";
|
|
21
21
|
}
|
|
22
|
-
if (value.content !== undefined &&
|
|
22
|
+
if (value.content !== undefined && Rec.size(value.content) !== 1) {
|
|
23
23
|
return "Header content must contain exactly one media type";
|
|
24
24
|
}
|
|
25
|
-
if (
|
|
25
|
+
if (Rec.has(value, "example") && value.examples !== undefined) {
|
|
26
26
|
return "Header example and examples are mutually exclusive";
|
|
27
27
|
}
|
|
28
28
|
return undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { OpenApiEncoding } from "./encoding.js";
|
|
3
3
|
import { OpenApiExample } from "./example.js";
|
|
4
4
|
import { openApiObject } from "./object.js";
|
|
@@ -9,6 +9,6 @@ export const OpenApiMediaType = openApiObject({
|
|
|
9
9
|
example: Schema.optionalKey(Schema.Unknown),
|
|
10
10
|
examples: Schema.optionalKey(Schema.Record(Schema.String, withReference(OpenApiExample))),
|
|
11
11
|
encoding: Schema.optionalKey(Schema.Record(Schema.String, OpenApiEncoding)),
|
|
12
|
-
}).check(Schema.makeFilter((value) =>
|
|
12
|
+
}).check(Schema.makeFilter((value) => Rec.has(value, "example") && value.examples !== undefined
|
|
13
13
|
? "Media type example and examples are mutually exclusive"
|
|
14
14
|
: undefined));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Array as Arr, Formatter, Option, pipe, Record as Rec, Schema, String as Str, } from "effect";
|
|
2
2
|
/** Preserve extensions and unrecognized fields while validating every declared field. */
|
|
3
3
|
export function openApiObject(fields) {
|
|
4
4
|
return Schema.StructWithRest(Schema.Struct(fields), [
|
|
@@ -10,8 +10,5 @@ export function openApiMap(key, value) {
|
|
|
10
10
|
const isKey = Schema.is(key);
|
|
11
11
|
return Schema.StructWithRest(Schema.Record(key, value), [
|
|
12
12
|
Schema.Record(Schema.String, Schema.Unknown),
|
|
13
|
-
]).check(Schema.makeFilter((entries) => {
|
|
14
|
-
const invalid = Object.keys(entries).find((name) => !name.startsWith("x-") && !isKey(name));
|
|
15
|
-
return invalid === undefined ? undefined : `Unexpected key ${JSON.stringify(invalid)}`;
|
|
16
|
-
}));
|
|
13
|
+
]).check(Schema.makeFilter((entries) => pipe(Rec.keys(entries), Arr.findFirst((name) => !Str.startsWith("x-")(name) && !isKey(name)), Option.map((invalid) => `Unexpected key ${Formatter.formatJson(invalid)}`), Option.getOrUndefined)));
|
|
17
14
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Record as Rec, Schema } from "effect";
|
|
2
2
|
import { OpenApiHeader } from "./header.js";
|
|
3
3
|
import { openApiObject } from "./object.js";
|
|
4
4
|
export const OpenApiParameter = openApiObject({
|
|
@@ -23,10 +23,10 @@ export const OpenApiParameter = openApiObject({
|
|
|
23
23
|
if ((value.schema === undefined) === (value.content === undefined)) {
|
|
24
24
|
return "A parameter must have exactly one of schema or content";
|
|
25
25
|
}
|
|
26
|
-
if (value.content !== undefined &&
|
|
26
|
+
if (value.content !== undefined && Rec.size(value.content) !== 1) {
|
|
27
27
|
return "Parameter content must contain exactly one media type";
|
|
28
28
|
}
|
|
29
|
-
if (
|
|
29
|
+
if (Rec.has(value, "example") && value.examples !== undefined) {
|
|
30
30
|
return "Parameter example and examples are mutually exclusive";
|
|
31
31
|
}
|
|
32
32
|
return undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Schema } from "effect";
|
|
1
|
+
import { Array as Arr, Record as Rec, Schema, String as Str } from "effect";
|
|
2
2
|
import { openApiMap } from "./object.js";
|
|
3
3
|
import { withReference } from "./reference.js";
|
|
4
4
|
import { OpenApiResponse } from "./response.js";
|
|
5
|
-
export const OpenApiResponses = openApiMap(Schema.String.check(Schema.isPattern(/^(?:default|[1-5](?:\d{2}|XX))$/)), withReference(OpenApiResponse)).check(Schema.makeFilter((value) =>
|
|
5
|
+
export const OpenApiResponses = openApiMap(Schema.String.check(Schema.isPattern(/^(?:default|[1-5](?:\d{2}|XX))$/)), withReference(OpenApiResponse)).check(Schema.makeFilter((value) => Arr.some(Rec.keys(value), (key) => !Str.startsWith("x-")(key))
|
|
6
6
|
? undefined
|
|
7
7
|
: "Responses must contain at least one response"));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JsonSchema, Predicate } from "effect";
|
|
1
|
+
import { Array as Arr, JsonSchema, Predicate, Record as Rec, String as Str } from "effect";
|
|
2
2
|
import { OpenApiHttpMethod } from "../schemas/3.1/http-method.js";
|
|
3
3
|
function asRecord(value) {
|
|
4
4
|
return Predicate.isObject(value) ? value : undefined;
|
|
@@ -22,11 +22,11 @@ const ARRAY_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
|
22
22
|
const MAP_KEYS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
|
|
23
23
|
export function isOpenApi3(document) {
|
|
24
24
|
const record = asRecord(document);
|
|
25
|
-
return typeof record?.openapi === "string" &&
|
|
25
|
+
return typeof record?.openapi === "string" && Str.startsWith("3.")(record.openapi);
|
|
26
26
|
}
|
|
27
27
|
export function isOpenApi3_0(document) {
|
|
28
28
|
const record = asRecord(document);
|
|
29
|
-
return typeof record?.openapi === "string" &&
|
|
29
|
+
return typeof record?.openapi === "string" && Str.startsWith("3.0")(record.openapi);
|
|
30
30
|
}
|
|
31
31
|
export function upgradeOpenApi3_0(document) {
|
|
32
32
|
if (!isOpenApi3_0(document))
|
|
@@ -48,18 +48,18 @@ export function schemaSlots(document, includeComponentSchemas = true) {
|
|
|
48
48
|
collectHeaderMap(components.headers, slots);
|
|
49
49
|
const pathItems = asRecord(components.pathItems);
|
|
50
50
|
if (pathItems) {
|
|
51
|
-
for (const value of
|
|
51
|
+
for (const value of Rec.values(pathItems))
|
|
52
52
|
collectPathItem(value, slots);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
const paths = asRecord(document.paths);
|
|
56
56
|
if (paths) {
|
|
57
|
-
for (const value of
|
|
57
|
+
for (const value of Rec.values(paths))
|
|
58
58
|
collectPathItem(value, slots);
|
|
59
59
|
}
|
|
60
60
|
const webhooks = asRecord(document.webhooks);
|
|
61
61
|
if (webhooks) {
|
|
62
|
-
for (const value of
|
|
62
|
+
for (const value of Rec.values(webhooks))
|
|
63
63
|
collectPathItem(value, slots);
|
|
64
64
|
}
|
|
65
65
|
return slots;
|
|
@@ -73,35 +73,35 @@ function collectSchemaMap(value, slots) {
|
|
|
73
73
|
const map = asRecord(value);
|
|
74
74
|
if (!map)
|
|
75
75
|
return;
|
|
76
|
-
for (const key of
|
|
76
|
+
for (const key of Rec.keys(map))
|
|
77
77
|
slots.push({ parent: map, key });
|
|
78
78
|
}
|
|
79
79
|
function collectParameterMap(value, slots) {
|
|
80
80
|
const map = asRecord(value);
|
|
81
81
|
if (!map)
|
|
82
82
|
return;
|
|
83
|
-
for (const parameter of
|
|
83
|
+
for (const parameter of Rec.values(map))
|
|
84
84
|
collectParameter(parameter, slots);
|
|
85
85
|
}
|
|
86
86
|
function collectResponseMap(value, slots) {
|
|
87
87
|
const map = asRecord(value);
|
|
88
88
|
if (!map)
|
|
89
89
|
return;
|
|
90
|
-
for (const response of
|
|
90
|
+
for (const response of Rec.values(map))
|
|
91
91
|
collectResponse(response, slots);
|
|
92
92
|
}
|
|
93
93
|
function collectRequestBodyMap(value, slots) {
|
|
94
94
|
const map = asRecord(value);
|
|
95
95
|
if (!map)
|
|
96
96
|
return;
|
|
97
|
-
for (const body of
|
|
97
|
+
for (const body of Rec.values(map))
|
|
98
98
|
collectRequestBody(body, slots);
|
|
99
99
|
}
|
|
100
100
|
function collectHeaderMap(value, slots) {
|
|
101
101
|
const map = asRecord(value);
|
|
102
102
|
if (!map)
|
|
103
103
|
return;
|
|
104
|
-
for (const header of
|
|
104
|
+
for (const header of Rec.values(map))
|
|
105
105
|
collectHeader(header, slots);
|
|
106
106
|
}
|
|
107
107
|
function collectPathItem(value, slots) {
|
|
@@ -109,7 +109,7 @@ function collectPathItem(value, slots) {
|
|
|
109
109
|
if (!record || typeof record.$ref === "string")
|
|
110
110
|
return;
|
|
111
111
|
collectParameterList(record.parameters, slots);
|
|
112
|
-
for (const [key, child] of
|
|
112
|
+
for (const [key, child] of Rec.toEntries(record)) {
|
|
113
113
|
if (METHODS.has(key))
|
|
114
114
|
collectOperation(child, slots);
|
|
115
115
|
}
|
|
@@ -122,22 +122,22 @@ function collectOperation(value, slots) {
|
|
|
122
122
|
collectRequestBody(record.requestBody, slots);
|
|
123
123
|
const responses = asRecord(record.responses);
|
|
124
124
|
if (responses) {
|
|
125
|
-
for (const response of
|
|
125
|
+
for (const response of Rec.values(responses))
|
|
126
126
|
collectResponse(response, slots);
|
|
127
127
|
}
|
|
128
128
|
const callbacks = asRecord(record.callbacks);
|
|
129
129
|
if (callbacks) {
|
|
130
|
-
for (const callback of
|
|
130
|
+
for (const callback of Rec.values(callbacks)) {
|
|
131
131
|
const paths = asRecord(callback);
|
|
132
132
|
if (!paths || typeof paths.$ref === "string")
|
|
133
133
|
continue;
|
|
134
|
-
for (const pathItem of
|
|
134
|
+
for (const pathItem of Rec.values(paths))
|
|
135
135
|
collectPathItem(pathItem, slots);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
function collectParameterList(value, slots) {
|
|
140
|
-
if (!
|
|
140
|
+
if (!Arr.isArray(value))
|
|
141
141
|
return;
|
|
142
142
|
for (const parameter of value)
|
|
143
143
|
collectParameter(parameter, slots);
|
|
@@ -175,7 +175,7 @@ function collectContent(value, slots) {
|
|
|
175
175
|
const content = asRecord(value);
|
|
176
176
|
if (!content)
|
|
177
177
|
return;
|
|
178
|
-
for (const media of
|
|
178
|
+
for (const media of Rec.values(content)) {
|
|
179
179
|
const mediaRecord = asRecord(media);
|
|
180
180
|
if (mediaRecord && "schema" in mediaRecord)
|
|
181
181
|
slots.push({ parent: mediaRecord, key: "schema" });
|
|
@@ -188,25 +188,26 @@ function upgradeSchema(value) {
|
|
|
188
188
|
for (const key of SINGLE_KEYS) {
|
|
189
189
|
if (!(key in record))
|
|
190
190
|
continue;
|
|
191
|
-
if (key === "items" &&
|
|
192
|
-
record.items = record.items
|
|
191
|
+
if (key === "items" && Arr.isArray(record.items)) {
|
|
192
|
+
record.items = Arr.map(record.items, (item) => upgradeSchema(item));
|
|
193
193
|
}
|
|
194
194
|
else {
|
|
195
195
|
record[key] = upgradeSchema(record[key]);
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
198
|
for (const key of ARRAY_KEYS) {
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const items = record[key];
|
|
200
|
+
if (Arr.isArray(items))
|
|
201
|
+
record[key] = Arr.map(items, (item) => upgradeSchema(item));
|
|
201
202
|
}
|
|
202
203
|
for (const key of MAP_KEYS) {
|
|
203
204
|
const map = asRecord(record[key]);
|
|
204
205
|
if (!map)
|
|
205
206
|
continue;
|
|
206
|
-
for (const child of
|
|
207
|
+
for (const child of Rec.keys(map))
|
|
207
208
|
map[child] = upgradeSchema(map[child]);
|
|
208
209
|
}
|
|
209
|
-
if (
|
|
210
|
+
if (Arr.isArray(record.items)) {
|
|
210
211
|
record.prefixItems = record.items;
|
|
211
212
|
if ("additionalItems" in record) {
|
|
212
213
|
record.items = record.additionalItems;
|
|
@@ -216,7 +217,7 @@ function upgradeSchema(value) {
|
|
|
216
217
|
delete record.items;
|
|
217
218
|
}
|
|
218
219
|
}
|
|
219
|
-
if (typeof record.$ref === "string" &&
|
|
220
|
+
if (typeof record.$ref === "string" && Arr.some(Rec.keys(record), (key) => key !== "$ref")) {
|
|
220
221
|
const nullable = record.nullable === true;
|
|
221
222
|
const readOnly = record.readOnly === true;
|
|
222
223
|
const writeOnly = record.writeOnly === true;
|
|
@@ -227,7 +228,9 @@ function upgradeSchema(value) {
|
|
|
227
228
|
if (writeOnly)
|
|
228
229
|
delete siblings.writeOnly;
|
|
229
230
|
// OpenAPI 3.0 ignores the siblings of a $ref; 3.1 applies them, so they move into allOf
|
|
230
|
-
const target =
|
|
231
|
+
const target = !Rec.isEmptyRecord(siblings)
|
|
232
|
+
? { allOf: [{ $ref }, siblings] }
|
|
233
|
+
: { $ref };
|
|
231
234
|
const lifted = nullable ? orNull(target) : target;
|
|
232
235
|
if (readOnly)
|
|
233
236
|
lifted.readOnly = true;
|
|
@@ -255,13 +258,13 @@ const ANNOTATION_KEYS = new Set([
|
|
|
255
258
|
function orNull(schema) {
|
|
256
259
|
const outer = {};
|
|
257
260
|
const inner = {};
|
|
258
|
-
for (const [key, value] of
|
|
259
|
-
if (ANNOTATION_KEYS.has(key) ||
|
|
261
|
+
for (const [key, value] of Rec.toEntries(schema)) {
|
|
262
|
+
if (ANNOTATION_KEYS.has(key) || Str.startsWith("x-")(key))
|
|
260
263
|
outer[key] = value;
|
|
261
264
|
else
|
|
262
265
|
inner[key] = value;
|
|
263
266
|
}
|
|
264
|
-
return
|
|
267
|
+
return Rec.isEmptyRecord(inner) ? schema : { ...outer, anyOf: [inner, { type: "null" }] };
|
|
265
268
|
}
|
|
266
269
|
function applyOpenApi30Keywords(schema) {
|
|
267
270
|
const nullable = schema.nullable === true;
|
|
@@ -289,8 +292,8 @@ function applyOpenApi30Keywords(schema) {
|
|
|
289
292
|
}
|
|
290
293
|
if ("example" in schema) {
|
|
291
294
|
const converted = JsonSchema.fromSchemaOpenApi3_0({ example: schema.example }).schema;
|
|
292
|
-
const existing =
|
|
293
|
-
const convertedExamples =
|
|
295
|
+
const existing = Arr.isArray(schema.examples) ? schema.examples : [];
|
|
296
|
+
const convertedExamples = Arr.isArray(converted.examples) ? converted.examples : [];
|
|
294
297
|
schema.examples = [...convertedExamples, ...existing];
|
|
295
298
|
delete schema.example;
|
|
296
299
|
}
|
|
@@ -302,7 +305,7 @@ function applyOpenApi30Keywords(schema) {
|
|
|
302
305
|
if (typeof schema.type === "string" && !composed) {
|
|
303
306
|
const converted = JsonSchema.fromSchemaOpenApi3_0({ type: schema.type, nullable: true }).schema;
|
|
304
307
|
schema.type = converted.type;
|
|
305
|
-
if (
|
|
308
|
+
if (Arr.isArray(schema.enum) && !schema.enum.includes(null))
|
|
306
309
|
schema.enum = [...schema.enum, null];
|
|
307
310
|
return schema;
|
|
308
311
|
}
|
package/dist/url.d.ts
CHANGED
|
@@ -24,6 +24,6 @@ export declare const resolveUrlSafety: (href: string) => Effect.Effect<{
|
|
|
24
24
|
addresses?: undefined;
|
|
25
25
|
} | {
|
|
26
26
|
unsafe: boolean;
|
|
27
|
-
addresses: import("dns").LookupAddress[];
|
|
27
|
+
addresses: import("node:dns").LookupAddress[];
|
|
28
28
|
}, ResolverFailure, never>;
|
|
29
29
|
export declare function isUnsafeHostname(hostname: string): boolean;
|
package/dist/url.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as NodePath from "@effect/platform-node/NodePath";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { Effect, Path } from "effect";
|
|
3
|
+
import { Array as Arr, Effect, Option, Path, pipe, String as Str } from "effect";
|
|
4
4
|
import { ResolverFailure, nodeErrorCode } from "./errors.js";
|
|
5
5
|
const unsafeDomainSuffixes = [
|
|
6
6
|
".localhost",
|
|
@@ -12,14 +12,10 @@ const unsafeDomainSuffixes = [
|
|
|
12
12
|
".lan",
|
|
13
13
|
];
|
|
14
14
|
export function stripHash(value) {
|
|
15
|
-
|
|
16
|
-
return index === -1 ? value : value.slice(0, index);
|
|
15
|
+
return pipe(value, Str.indexOf("#"), Option.match({ onNone: () => value, onSome: (index) => Str.slice(0, index)(value) }));
|
|
17
16
|
}
|
|
18
17
|
export function getHash(value) {
|
|
19
|
-
|
|
20
|
-
if (index === -1)
|
|
21
|
-
return "#";
|
|
22
|
-
return value.slice(index) || "#";
|
|
18
|
+
return pipe(value, Str.indexOf("#"), Option.match({ onNone: () => "#", onSome: (index) => Str.slice(index)(value) || "#" }));
|
|
23
19
|
}
|
|
24
20
|
export function isHttp(value) {
|
|
25
21
|
return /^https?:\/\//i.test(value);
|
|
@@ -27,7 +23,7 @@ export function isHttp(value) {
|
|
|
27
23
|
export function isFileSystemPath(value) {
|
|
28
24
|
if (!value)
|
|
29
25
|
return false;
|
|
30
|
-
if (
|
|
26
|
+
if (Str.startsWith("file:")(value))
|
|
31
27
|
return true;
|
|
32
28
|
return !/^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
33
29
|
}
|
|
@@ -60,29 +56,31 @@ export function resolveAgainst(from, to) {
|
|
|
60
56
|
}
|
|
61
57
|
export function toFileSystemPath(ref) {
|
|
62
58
|
const withoutHash = stripHash(ref);
|
|
63
|
-
if (
|
|
59
|
+
if (Str.startsWith("file:")(withoutHash))
|
|
64
60
|
return fileURLToPath(withoutHash);
|
|
65
61
|
return withoutHash;
|
|
66
62
|
}
|
|
67
63
|
export function extensionOf(ref) {
|
|
68
|
-
const pathname = stripHash(ref).split("?")
|
|
69
|
-
const base =
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
const pathname = pipe(stripHash(ref), Str.split("?"), Arr.headNonEmpty);
|
|
65
|
+
const base = pipe(pathname, Str.split(/[/\\]/), Arr.lastNonEmpty);
|
|
66
|
+
return pipe(base, Str.lastIndexOf("."), Option.match({
|
|
67
|
+
onNone: () => "",
|
|
68
|
+
onSome: (dot) => pipe(base, Str.slice(dot), Str.toLowerCase),
|
|
69
|
+
}));
|
|
72
70
|
}
|
|
73
71
|
export function isUnsafeUrl(value) {
|
|
74
|
-
const normalized = value.trim
|
|
72
|
+
const normalized = pipe(value, Str.trim, Str.toLowerCase);
|
|
75
73
|
if (!normalized)
|
|
76
74
|
return true;
|
|
77
|
-
if (
|
|
78
|
-
|
|
79
|
-
|
|
75
|
+
if (Str.startsWith("javascript:")(normalized) ||
|
|
76
|
+
Str.startsWith("vbscript:")(normalized) ||
|
|
77
|
+
Str.startsWith("data:")(normalized)) {
|
|
80
78
|
return true;
|
|
81
79
|
}
|
|
82
|
-
if (
|
|
80
|
+
if (Str.startsWith("file:")(normalized))
|
|
83
81
|
return true;
|
|
84
82
|
try {
|
|
85
|
-
const url = new URL(
|
|
83
|
+
const url = new URL(Str.startsWith("//")(normalized) ? `http:${normalized}` : normalized);
|
|
86
84
|
if (isUnsafeHostname(url.hostname))
|
|
87
85
|
return true;
|
|
88
86
|
if (url.port && isInternalPort(Number(url.port)))
|
|
@@ -97,14 +95,14 @@ export function isAllowedOrigin(href, allowedOrigins) {
|
|
|
97
95
|
const origin = httpOrigin(href);
|
|
98
96
|
if (!origin || !allowedOrigins?.length)
|
|
99
97
|
return false;
|
|
100
|
-
return
|
|
98
|
+
return Arr.some(allowedOrigins, (allowed) => httpOrigin(allowed) === origin);
|
|
101
99
|
}
|
|
102
100
|
export const resolveUrlSafety = Effect.fnUntraced(function* (href) {
|
|
103
101
|
if (isUnsafeUrl(href))
|
|
104
102
|
return { unsafe: true };
|
|
105
103
|
if (typeof process === "undefined" || !process.versions?.node)
|
|
106
104
|
return { unsafe: false };
|
|
107
|
-
const parsed = URL.parse(
|
|
105
|
+
const parsed = URL.parse(Str.startsWith("//")(href) ? `http:${href}` : href);
|
|
108
106
|
if (parsed === null)
|
|
109
107
|
return { unsafe: true };
|
|
110
108
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
@@ -123,7 +121,7 @@ export const resolveUrlSafety = Effect.fnUntraced(function* (href) {
|
|
|
123
121
|
try: () => dns.lookup(normalizeHostname(parsed.hostname), { all: true, verbatim: true }),
|
|
124
122
|
catch: onError,
|
|
125
123
|
});
|
|
126
|
-
if (addresses.length === 0 ||
|
|
124
|
+
if (addresses.length === 0 || Arr.some(addresses, ({ address }) => isUnsafeHostname(address))) {
|
|
127
125
|
return { unsafe: true };
|
|
128
126
|
}
|
|
129
127
|
return { unsafe: false, addresses };
|
|
@@ -133,7 +131,7 @@ export function isUnsafeHostname(hostname) {
|
|
|
133
131
|
if (!normalized)
|
|
134
132
|
return true;
|
|
135
133
|
if (normalized === "localhost" ||
|
|
136
|
-
|
|
134
|
+
Arr.some(unsafeDomainSuffixes, (suffix) => Str.endsWith(suffix)(normalized)))
|
|
137
135
|
return true;
|
|
138
136
|
const ipv4 = parseIPv4Address(normalized);
|
|
139
137
|
if (ipv4)
|
|
@@ -148,19 +146,19 @@ function ioCode(cause) {
|
|
|
148
146
|
return ioErrorCode ? { ioErrorCode } : undefined;
|
|
149
147
|
}
|
|
150
148
|
function normalizeHostname(hostname) {
|
|
151
|
-
let normalized = hostname.trim
|
|
152
|
-
if (
|
|
153
|
-
normalized =
|
|
154
|
-
while (
|
|
155
|
-
normalized =
|
|
149
|
+
let normalized = pipe(hostname, Str.trim, Str.toLowerCase);
|
|
150
|
+
if (Str.startsWith("[")(normalized) && Str.endsWith("]")(normalized))
|
|
151
|
+
normalized = Str.slice(1, -1)(normalized);
|
|
152
|
+
while (Str.endsWith(".")(normalized))
|
|
153
|
+
normalized = Str.slice(0, -1)(normalized);
|
|
156
154
|
return normalized;
|
|
157
155
|
}
|
|
158
156
|
function parseIPv4Address(ip) {
|
|
159
|
-
const parts =
|
|
157
|
+
const parts = Str.split(ip, ".");
|
|
160
158
|
if (parts.length !== 4)
|
|
161
159
|
return undefined;
|
|
162
|
-
const octets =
|
|
163
|
-
if (
|
|
160
|
+
const octets = Arr.map(parts, (part) => (/^\d+$/.test(part) ? Number(part) : Number.NaN));
|
|
161
|
+
if (Arr.some(octets, (octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
|
|
164
162
|
return undefined;
|
|
165
163
|
return octets;
|
|
166
164
|
}
|
|
@@ -184,23 +182,24 @@ function isUnsafeIPv4Address(octets) {
|
|
|
184
182
|
(a === 255 && b === 255 && c === 255 && d === 255));
|
|
185
183
|
}
|
|
186
184
|
function parseIPv6Address(ip) {
|
|
187
|
-
|
|
185
|
+
const separator = Str.lastIndexOf(":")(ip);
|
|
186
|
+
if (Option.isNone(separator))
|
|
188
187
|
return undefined;
|
|
189
188
|
let normalized = ip;
|
|
190
|
-
const lastSeparator =
|
|
191
|
-
const possibleIPv4 =
|
|
192
|
-
if (
|
|
189
|
+
const lastSeparator = separator.value;
|
|
190
|
+
const possibleIPv4 = Str.slice(lastSeparator + 1)(normalized);
|
|
191
|
+
if (Str.includes(".")(possibleIPv4)) {
|
|
193
192
|
const ipv4 = parseIPv4Address(possibleIPv4);
|
|
194
193
|
if (!ipv4)
|
|
195
194
|
return undefined;
|
|
196
195
|
const firstGroup = ipv4[0] * 256 + ipv4[1];
|
|
197
196
|
const secondGroup = ipv4[2] * 256 + ipv4[3];
|
|
198
|
-
normalized = `${
|
|
197
|
+
normalized = `${Str.slice(0, lastSeparator + 1)(normalized)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
|
|
199
198
|
}
|
|
200
|
-
const halves =
|
|
199
|
+
const halves = Str.split(normalized, "::");
|
|
201
200
|
if (halves.length > 2)
|
|
202
201
|
return undefined;
|
|
203
|
-
const head = parseIPv6Groups(halves
|
|
202
|
+
const head = parseIPv6Groups(Arr.headNonEmpty(halves));
|
|
204
203
|
const tail = halves.length === 2 ? parseIPv6Groups(halves[1] ?? "") : [];
|
|
205
204
|
if (!head || !tail)
|
|
206
205
|
return undefined;
|
|
@@ -214,18 +213,16 @@ function parseIPv6Address(ip) {
|
|
|
214
213
|
function parseIPv6Groups(groups) {
|
|
215
214
|
if (!groups)
|
|
216
215
|
return [];
|
|
217
|
-
const parsed = groups
|
|
218
|
-
|
|
219
|
-
.map((group) => (/^[\da-f]{1,4}$/i.test(group) ? Number.parseInt(group, 16) : Number.NaN));
|
|
220
|
-
if (parsed.some((group) => !Number.isInteger(group) || group < 0 || group > 0xffff))
|
|
216
|
+
const parsed = pipe(groups, Str.split(":"), Arr.map((group) => (/^[\da-f]{1,4}$/i.test(group) ? Number.parseInt(group, 16) : Number.NaN)));
|
|
217
|
+
if (Arr.some(parsed, (group) => !Number.isInteger(group) || group < 0 || group > 0xffff))
|
|
221
218
|
return undefined;
|
|
222
219
|
return parsed;
|
|
223
220
|
}
|
|
224
221
|
function isUnsafeIPv6Address(groups) {
|
|
225
222
|
if (groups.length !== 8)
|
|
226
223
|
return false;
|
|
227
|
-
const unspecified =
|
|
228
|
-
const loopback = groups.slice(0, 7)
|
|
224
|
+
const unspecified = Arr.every(groups, (group) => group === 0);
|
|
225
|
+
const loopback = Arr.every(groups.slice(0, 7), (group) => group === 0) && groups[7] === 1;
|
|
229
226
|
const uniqueLocal = (groups[0] & 0xfe00) === 0xfc00;
|
|
230
227
|
const linkLocal = (groups[0] & 0xffc0) === 0xfe80;
|
|
231
228
|
const multicast = (groups[0] & 0xff00) === 0xff00;
|
|
@@ -235,7 +232,7 @@ function isUnsafeIPv6Address(groups) {
|
|
|
235
232
|
return mapped ? isUnsafeIPv4Address(mapped) : false;
|
|
236
233
|
}
|
|
237
234
|
function mappedIPv4(groups) {
|
|
238
|
-
const firstFive = groups.slice(0, 5)
|
|
235
|
+
const firstFive = Arr.every(groups.slice(0, 5), (group) => group === 0);
|
|
239
236
|
const firstSix = firstFive && groups[5] === 0;
|
|
240
237
|
const v4Mapped = firstFive && groups[5] === 0xffff;
|
|
241
238
|
if (!firstSix && !v4Mapped)
|