@shipworthy/openapi-parser 0.0.2 → 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 +6 -6
- 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 +48 -46
- 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 -6
|
@@ -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)
|