@shipworthy/openapi-parser 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +364 -2
- package/dist/bundle.d.ts +973 -0
- package/dist/bundle.js +11 -0
- package/dist/codec.d.ts +2188 -0
- package/dist/codec.js +42 -0
- package/dist/document.d.ts +9 -0
- package/dist/document.js +28 -0
- package/dist/errors.d.ts +105 -0
- package/dist/errors.js +162 -0
- package/dist/hoist.d.ts +10 -0
- package/dist/hoist.js +444 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +163 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/load.d.ts +17 -0
- package/dist/load.js +128 -0
- package/dist/loader.d.ts +11 -0
- package/dist/loader.js +89 -0
- package/dist/parsers.d.ts +5 -0
- package/dist/parsers.js +90 -0
- package/dist/pipeline.d.ts +3 -0
- package/dist/pipeline.js +433 -0
- package/dist/plugins.d.ts +95 -0
- package/dist/plugins.js +61 -0
- package/dist/pointer.d.ts +7 -0
- package/dist/pointer.js +80 -0
- package/dist/run.d.ts +17 -0
- package/dist/run.js +313 -0
- package/dist/schemas/3.1/callback.d.ts +205 -0
- package/dist/schemas/3.1/callback.js +4 -0
- package/dist/schemas/3.1/components.d.ts +801 -0
- package/dist/schemas/3.1/components.js +25 -0
- package/dist/schemas/3.1/contact.d.ts +7 -0
- package/dist/schemas/3.1/contact.js +7 -0
- package/dist/schemas/3.1/document.d.ts +1093 -0
- package/dist/schemas/3.1/document.js +25 -0
- package/dist/schemas/3.1/encoding.d.ts +14 -0
- package/dist/schemas/3.1/encoding.js +11 -0
- package/dist/schemas/3.1/example.d.ts +8 -0
- package/dist/schemas/3.1/example.js +10 -0
- package/dist/schemas/3.1/external-docs.d.ts +6 -0
- package/dist/schemas/3.1/external-docs.js +6 -0
- package/dist/schemas/3.1/header.d.ts +194 -0
- package/dist/schemas/3.1/header.js +29 -0
- package/dist/schemas/3.1/http-method.d.ts +4 -0
- package/dist/schemas/3.1/http-method.js +12 -0
- package/dist/schemas/3.1/index.d.ts +29 -0
- package/dist/schemas/3.1/index.js +29 -0
- package/dist/schemas/3.1/info.d.ts +19 -0
- package/dist/schemas/3.1/info.js +13 -0
- package/dist/schemas/3.1/license.d.ts +7 -0
- package/dist/schemas/3.1/license.js +9 -0
- package/dist/schemas/3.1/link.d.ts +18 -0
- package/dist/schemas/3.1/link.js +13 -0
- package/dist/schemas/3.1/media-type.d.ts +27 -0
- package/dist/schemas/3.1/media-type.js +14 -0
- package/dist/schemas/3.1/oauth-flow.d.ts +8 -0
- package/dist/schemas/3.1/oauth-flow.js +8 -0
- package/dist/schemas/3.1/oauth-flows.d.ts +28 -0
- package/dist/schemas/3.1/oauth-flows.js +13 -0
- package/dist/schemas/3.1/object.d.ts +5 -0
- package/dist/schemas/3.1/object.js +17 -0
- package/dist/schemas/3.1/operation.d.ts +1040 -0
- package/dist/schemas/3.1/operation.js +25 -0
- package/dist/schemas/3.1/parameter.d.ts +101 -0
- package/dist/schemas/3.1/parameter.js +33 -0
- package/dist/schemas/3.1/path-item.d.ts +127 -0
- package/dist/schemas/3.1/path-item.js +22 -0
- package/dist/schemas/3.1/paths.d.ts +126 -0
- package/dist/schemas/3.1/paths.js +4 -0
- package/dist/schemas/3.1/reference.d.ts +13 -0
- package/dist/schemas/3.1/reference.js +11 -0
- package/dist/schemas/3.1/request-body.d.ts +31 -0
- package/dist/schemas/3.1/request-body.js +8 -0
- package/dist/schemas/3.1/response.d.ts +149 -0
- package/dist/schemas/3.1/response.js +12 -0
- package/dist/schemas/3.1/responses.d.ts +152 -0
- package/dist/schemas/3.1/responses.js +7 -0
- package/dist/schemas/3.1/schema.d.ts +4 -0
- package/dist/schemas/3.1/schema.js +6 -0
- package/dist/schemas/3.1/security-requirement.d.ts +3 -0
- package/dist/schemas/3.1/security-requirement.js +2 -0
- package/dist/schemas/3.1/security-scheme.d.ts +49 -0
- package/dist/schemas/3.1/security-scheme.js +25 -0
- package/dist/schemas/3.1/server-variable.d.ts +7 -0
- package/dist/schemas/3.1/server-variable.js +7 -0
- package/dist/schemas/3.1/server.d.ts +11 -0
- package/dist/schemas/3.1/server.js +8 -0
- package/dist/schemas/3.1/tag.d.ts +10 -0
- package/dist/schemas/3.1/tag.js +8 -0
- package/dist/upgraders/3.0-to-3.1.d.ts +9 -0
- package/dist/upgraders/3.0-to-3.1.js +310 -0
- package/dist/url.d.ts +29 -0
- package/dist/url.js +255 -0
- package/dist/yaml.d.ts +6 -0
- package/dist/yaml.js +235 -0
- package/package.json +47 -4
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { JsonSchema, Predicate } from "effect";
|
|
2
|
+
import { OpenApiHttpMethod } from "../schemas/3.1/http-method.js";
|
|
3
|
+
function asRecord(value) {
|
|
4
|
+
return Predicate.isObject(value) ? value : undefined;
|
|
5
|
+
}
|
|
6
|
+
const METHODS = new Set(OpenApiHttpMethod.literals);
|
|
7
|
+
const SINGLE_KEYS = [
|
|
8
|
+
"not",
|
|
9
|
+
"additionalProperties",
|
|
10
|
+
"unevaluatedProperties",
|
|
11
|
+
"items",
|
|
12
|
+
"additionalItems",
|
|
13
|
+
"contains",
|
|
14
|
+
"if",
|
|
15
|
+
"then",
|
|
16
|
+
"else",
|
|
17
|
+
"propertyNames",
|
|
18
|
+
"unevaluatedItems",
|
|
19
|
+
"contentSchema",
|
|
20
|
+
];
|
|
21
|
+
const ARRAY_KEYS = ["allOf", "anyOf", "oneOf", "prefixItems"];
|
|
22
|
+
const MAP_KEYS = ["properties", "patternProperties", "$defs", "definitions", "dependentSchemas"];
|
|
23
|
+
export function isOpenApi3(document) {
|
|
24
|
+
const record = asRecord(document);
|
|
25
|
+
return typeof record?.openapi === "string" && record.openapi.startsWith("3.");
|
|
26
|
+
}
|
|
27
|
+
export function isOpenApi3_0(document) {
|
|
28
|
+
const record = asRecord(document);
|
|
29
|
+
return typeof record?.openapi === "string" && record.openapi.startsWith("3.0");
|
|
30
|
+
}
|
|
31
|
+
export function upgradeOpenApi3_0(document) {
|
|
32
|
+
if (!isOpenApi3_0(document))
|
|
33
|
+
return;
|
|
34
|
+
for (const slot of schemaSlots(document)) {
|
|
35
|
+
slot.parent[slot.key] = upgradeSchema(slot.parent[slot.key]);
|
|
36
|
+
}
|
|
37
|
+
document.openapi = "3.1.0";
|
|
38
|
+
}
|
|
39
|
+
export function schemaSlots(document, includeComponentSchemas = true) {
|
|
40
|
+
const slots = [];
|
|
41
|
+
const components = asRecord(document.components);
|
|
42
|
+
if (components) {
|
|
43
|
+
if (includeComponentSchemas)
|
|
44
|
+
collectSchemaMap(components.schemas, slots);
|
|
45
|
+
collectParameterMap(components.parameters, slots);
|
|
46
|
+
collectResponseMap(components.responses, slots);
|
|
47
|
+
collectRequestBodyMap(components.requestBodies, slots);
|
|
48
|
+
collectHeaderMap(components.headers, slots);
|
|
49
|
+
const pathItems = asRecord(components.pathItems);
|
|
50
|
+
if (pathItems) {
|
|
51
|
+
for (const value of Object.values(pathItems))
|
|
52
|
+
collectPathItem(value, slots);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const paths = asRecord(document.paths);
|
|
56
|
+
if (paths) {
|
|
57
|
+
for (const value of Object.values(paths))
|
|
58
|
+
collectPathItem(value, slots);
|
|
59
|
+
}
|
|
60
|
+
const webhooks = asRecord(document.webhooks);
|
|
61
|
+
if (webhooks) {
|
|
62
|
+
for (const value of Object.values(webhooks))
|
|
63
|
+
collectPathItem(value, slots);
|
|
64
|
+
}
|
|
65
|
+
return slots;
|
|
66
|
+
}
|
|
67
|
+
export function componentSchemas(document) {
|
|
68
|
+
const slots = [];
|
|
69
|
+
collectSchemaMap(asRecord(document.components)?.schemas, slots);
|
|
70
|
+
return slots;
|
|
71
|
+
}
|
|
72
|
+
function collectSchemaMap(value, slots) {
|
|
73
|
+
const map = asRecord(value);
|
|
74
|
+
if (!map)
|
|
75
|
+
return;
|
|
76
|
+
for (const key of Object.keys(map))
|
|
77
|
+
slots.push({ parent: map, key });
|
|
78
|
+
}
|
|
79
|
+
function collectParameterMap(value, slots) {
|
|
80
|
+
const map = asRecord(value);
|
|
81
|
+
if (!map)
|
|
82
|
+
return;
|
|
83
|
+
for (const parameter of Object.values(map))
|
|
84
|
+
collectParameter(parameter, slots);
|
|
85
|
+
}
|
|
86
|
+
function collectResponseMap(value, slots) {
|
|
87
|
+
const map = asRecord(value);
|
|
88
|
+
if (!map)
|
|
89
|
+
return;
|
|
90
|
+
for (const response of Object.values(map))
|
|
91
|
+
collectResponse(response, slots);
|
|
92
|
+
}
|
|
93
|
+
function collectRequestBodyMap(value, slots) {
|
|
94
|
+
const map = asRecord(value);
|
|
95
|
+
if (!map)
|
|
96
|
+
return;
|
|
97
|
+
for (const body of Object.values(map))
|
|
98
|
+
collectRequestBody(body, slots);
|
|
99
|
+
}
|
|
100
|
+
function collectHeaderMap(value, slots) {
|
|
101
|
+
const map = asRecord(value);
|
|
102
|
+
if (!map)
|
|
103
|
+
return;
|
|
104
|
+
for (const header of Object.values(map))
|
|
105
|
+
collectHeader(header, slots);
|
|
106
|
+
}
|
|
107
|
+
function collectPathItem(value, slots) {
|
|
108
|
+
const record = asRecord(value);
|
|
109
|
+
if (!record || typeof record.$ref === "string")
|
|
110
|
+
return;
|
|
111
|
+
collectParameterList(record.parameters, slots);
|
|
112
|
+
for (const [key, child] of Object.entries(record)) {
|
|
113
|
+
if (METHODS.has(key))
|
|
114
|
+
collectOperation(child, slots);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function collectOperation(value, slots) {
|
|
118
|
+
const record = asRecord(value);
|
|
119
|
+
if (!record)
|
|
120
|
+
return;
|
|
121
|
+
collectParameterList(record.parameters, slots);
|
|
122
|
+
collectRequestBody(record.requestBody, slots);
|
|
123
|
+
const responses = asRecord(record.responses);
|
|
124
|
+
if (responses) {
|
|
125
|
+
for (const response of Object.values(responses))
|
|
126
|
+
collectResponse(response, slots);
|
|
127
|
+
}
|
|
128
|
+
const callbacks = asRecord(record.callbacks);
|
|
129
|
+
if (callbacks) {
|
|
130
|
+
for (const callback of Object.values(callbacks)) {
|
|
131
|
+
const paths = asRecord(callback);
|
|
132
|
+
if (!paths || typeof paths.$ref === "string")
|
|
133
|
+
continue;
|
|
134
|
+
for (const pathItem of Object.values(paths))
|
|
135
|
+
collectPathItem(pathItem, slots);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function collectParameterList(value, slots) {
|
|
140
|
+
if (!Array.isArray(value))
|
|
141
|
+
return;
|
|
142
|
+
for (const parameter of value)
|
|
143
|
+
collectParameter(parameter, slots);
|
|
144
|
+
}
|
|
145
|
+
function collectParameter(value, slots) {
|
|
146
|
+
const record = asRecord(value);
|
|
147
|
+
if (!record || typeof record.$ref === "string")
|
|
148
|
+
return;
|
|
149
|
+
if ("schema" in record)
|
|
150
|
+
slots.push({ parent: record, key: "schema" });
|
|
151
|
+
collectContent(record.content, slots);
|
|
152
|
+
}
|
|
153
|
+
function collectRequestBody(value, slots) {
|
|
154
|
+
const record = asRecord(value);
|
|
155
|
+
if (!record || typeof record.$ref === "string")
|
|
156
|
+
return;
|
|
157
|
+
collectContent(record.content, slots);
|
|
158
|
+
}
|
|
159
|
+
function collectResponse(value, slots) {
|
|
160
|
+
const record = asRecord(value);
|
|
161
|
+
if (!record || typeof record.$ref === "string")
|
|
162
|
+
return;
|
|
163
|
+
collectContent(record.content, slots);
|
|
164
|
+
collectHeaderMap(record.headers, slots);
|
|
165
|
+
}
|
|
166
|
+
function collectHeader(value, slots) {
|
|
167
|
+
const record = asRecord(value);
|
|
168
|
+
if (!record || typeof record.$ref === "string")
|
|
169
|
+
return;
|
|
170
|
+
if ("schema" in record)
|
|
171
|
+
slots.push({ parent: record, key: "schema" });
|
|
172
|
+
collectContent(record.content, slots);
|
|
173
|
+
}
|
|
174
|
+
function collectContent(value, slots) {
|
|
175
|
+
const content = asRecord(value);
|
|
176
|
+
if (!content)
|
|
177
|
+
return;
|
|
178
|
+
for (const media of Object.values(content)) {
|
|
179
|
+
const mediaRecord = asRecord(media);
|
|
180
|
+
if (mediaRecord && "schema" in mediaRecord)
|
|
181
|
+
slots.push({ parent: mediaRecord, key: "schema" });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function upgradeSchema(value) {
|
|
185
|
+
const record = asRecord(value);
|
|
186
|
+
if (!record)
|
|
187
|
+
return value;
|
|
188
|
+
for (const key of SINGLE_KEYS) {
|
|
189
|
+
if (!(key in record))
|
|
190
|
+
continue;
|
|
191
|
+
if (key === "items" && Array.isArray(record.items)) {
|
|
192
|
+
record.items = record.items.map((item) => upgradeSchema(item));
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
record[key] = upgradeSchema(record[key]);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const key of ARRAY_KEYS) {
|
|
199
|
+
if (Array.isArray(record[key]))
|
|
200
|
+
record[key] = record[key].map((item) => upgradeSchema(item));
|
|
201
|
+
}
|
|
202
|
+
for (const key of MAP_KEYS) {
|
|
203
|
+
const map = asRecord(record[key]);
|
|
204
|
+
if (!map)
|
|
205
|
+
continue;
|
|
206
|
+
for (const child of Object.keys(map))
|
|
207
|
+
map[child] = upgradeSchema(map[child]);
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(record.items)) {
|
|
210
|
+
record.prefixItems = record.items;
|
|
211
|
+
if ("additionalItems" in record) {
|
|
212
|
+
record.items = record.additionalItems;
|
|
213
|
+
delete record.additionalItems;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
delete record.items;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (typeof record.$ref === "string" && Object.keys(record).some((key) => key !== "$ref")) {
|
|
220
|
+
const nullable = record.nullable === true;
|
|
221
|
+
const readOnly = record.readOnly === true;
|
|
222
|
+
const writeOnly = record.writeOnly === true;
|
|
223
|
+
const { $ref, ...siblings } = record;
|
|
224
|
+
delete siblings.nullable;
|
|
225
|
+
if (readOnly)
|
|
226
|
+
delete siblings.readOnly;
|
|
227
|
+
if (writeOnly)
|
|
228
|
+
delete siblings.writeOnly;
|
|
229
|
+
// OpenAPI 3.0 ignores the siblings of a $ref; 3.1 applies them, so they move into allOf
|
|
230
|
+
const target = Object.keys(siblings).length > 0 ? { allOf: [{ $ref }, siblings] } : { $ref };
|
|
231
|
+
const lifted = nullable ? orNull(target) : target;
|
|
232
|
+
if (readOnly)
|
|
233
|
+
lifted.readOnly = true;
|
|
234
|
+
if (writeOnly)
|
|
235
|
+
lifted.writeOnly = true;
|
|
236
|
+
return lifted;
|
|
237
|
+
}
|
|
238
|
+
return applyOpenApi30Keywords(record);
|
|
239
|
+
}
|
|
240
|
+
/** Keywords that describe a schema without constraining it. They stay outside `orNull`'s `anyOf`. */
|
|
241
|
+
const ANNOTATION_KEYS = new Set([
|
|
242
|
+
"title",
|
|
243
|
+
"description",
|
|
244
|
+
"default",
|
|
245
|
+
"deprecated",
|
|
246
|
+
"readOnly",
|
|
247
|
+
"writeOnly",
|
|
248
|
+
"example",
|
|
249
|
+
"examples",
|
|
250
|
+
"externalDocs",
|
|
251
|
+
"xml",
|
|
252
|
+
"$comment",
|
|
253
|
+
]);
|
|
254
|
+
/** `schema` or null, as `anyOf: [schema, { type: "null" }]`. Annotations and extensions stay outside. */
|
|
255
|
+
function orNull(schema) {
|
|
256
|
+
const outer = {};
|
|
257
|
+
const inner = {};
|
|
258
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
259
|
+
if (ANNOTATION_KEYS.has(key) || key.startsWith("x-"))
|
|
260
|
+
outer[key] = value;
|
|
261
|
+
else
|
|
262
|
+
inner[key] = value;
|
|
263
|
+
}
|
|
264
|
+
return Object.keys(inner).length === 0 ? schema : { ...outer, anyOf: [inner, { type: "null" }] };
|
|
265
|
+
}
|
|
266
|
+
function applyOpenApi30Keywords(schema) {
|
|
267
|
+
const nullable = schema.nullable === true;
|
|
268
|
+
delete schema.nullable;
|
|
269
|
+
if (typeof schema.exclusiveMinimum === "boolean" ||
|
|
270
|
+
typeof schema.exclusiveMaximum === "boolean") {
|
|
271
|
+
const projection = {};
|
|
272
|
+
if ("minimum" in schema)
|
|
273
|
+
projection.minimum = schema.minimum;
|
|
274
|
+
if ("maximum" in schema)
|
|
275
|
+
projection.maximum = schema.maximum;
|
|
276
|
+
if ("exclusiveMinimum" in schema)
|
|
277
|
+
projection.exclusiveMinimum = schema.exclusiveMinimum;
|
|
278
|
+
if ("exclusiveMaximum" in schema)
|
|
279
|
+
projection.exclusiveMaximum = schema.exclusiveMaximum;
|
|
280
|
+
const converted = JsonSchema.fromSchemaOpenApi3_0(projection).schema;
|
|
281
|
+
delete schema.minimum;
|
|
282
|
+
delete schema.maximum;
|
|
283
|
+
delete schema.exclusiveMinimum;
|
|
284
|
+
delete schema.exclusiveMaximum;
|
|
285
|
+
for (const key of ["minimum", "maximum", "exclusiveMinimum", "exclusiveMaximum"]) {
|
|
286
|
+
if (key in converted)
|
|
287
|
+
schema[key] = converted[key];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if ("example" in schema) {
|
|
291
|
+
const converted = JsonSchema.fromSchemaOpenApi3_0({ example: schema.example }).schema;
|
|
292
|
+
const existing = Array.isArray(schema.examples) ? schema.examples : [];
|
|
293
|
+
const convertedExamples = Array.isArray(converted.examples) ? converted.examples : [];
|
|
294
|
+
schema.examples = [...convertedExamples, ...existing];
|
|
295
|
+
delete schema.example;
|
|
296
|
+
}
|
|
297
|
+
if (!nullable)
|
|
298
|
+
return schema;
|
|
299
|
+
// A "null" type can only join a schema's own type. Compositions have to hold for null too,
|
|
300
|
+
// so a schema with allOf/anyOf/oneOf, or without a type, becomes a disjunction instead.
|
|
301
|
+
const composed = "allOf" in schema || "anyOf" in schema || "oneOf" in schema;
|
|
302
|
+
if (typeof schema.type === "string" && !composed) {
|
|
303
|
+
const converted = JsonSchema.fromSchemaOpenApi3_0({ type: schema.type, nullable: true }).schema;
|
|
304
|
+
schema.type = converted.type;
|
|
305
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(null))
|
|
306
|
+
schema.enum = [...schema.enum, null];
|
|
307
|
+
return schema;
|
|
308
|
+
}
|
|
309
|
+
return orNull(schema);
|
|
310
|
+
}
|
package/dist/url.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { ResolverFailure } from "./errors.ts";
|
|
3
|
+
export interface ResolvedUrlAddress {
|
|
4
|
+
address: string;
|
|
5
|
+
family: number;
|
|
6
|
+
}
|
|
7
|
+
export interface UrlSafety {
|
|
8
|
+
unsafe: boolean;
|
|
9
|
+
addresses?: ResolvedUrlAddress[];
|
|
10
|
+
}
|
|
11
|
+
export declare function stripHash(value: string): string;
|
|
12
|
+
export declare function getHash(value: string): string;
|
|
13
|
+
export declare function isHttp(value: string): boolean;
|
|
14
|
+
export declare function isFileSystemPath(value: string | undefined): boolean;
|
|
15
|
+
export declare function httpOrigin(value: string): string | undefined;
|
|
16
|
+
export declare const normalizeRef: (input: string | undefined) => Effect.Effect<string, ResolverFailure, never>;
|
|
17
|
+
export declare function resolveAgainst(from: string, to: string): string;
|
|
18
|
+
export declare function toFileSystemPath(ref: string): string;
|
|
19
|
+
export declare function extensionOf(ref: string): string;
|
|
20
|
+
export declare function isUnsafeUrl(value: string): boolean;
|
|
21
|
+
export declare function isAllowedOrigin(href: string, allowedOrigins: readonly string[] | undefined): boolean;
|
|
22
|
+
export declare const resolveUrlSafety: (href: string) => Effect.Effect<{
|
|
23
|
+
unsafe: boolean;
|
|
24
|
+
addresses?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
unsafe: boolean;
|
|
27
|
+
addresses: import("dns").LookupAddress[];
|
|
28
|
+
}, ResolverFailure, never>;
|
|
29
|
+
export declare function isUnsafeHostname(hostname: string): boolean;
|
package/dist/url.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import * as NodePath from "@effect/platform-node/NodePath";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { Effect, Path } from "effect";
|
|
4
|
+
import { ResolverFailure, nodeErrorCode } from "./errors.js";
|
|
5
|
+
const unsafeDomainSuffixes = [
|
|
6
|
+
".localhost",
|
|
7
|
+
".local",
|
|
8
|
+
".internal",
|
|
9
|
+
".intranet",
|
|
10
|
+
".corp",
|
|
11
|
+
".home",
|
|
12
|
+
".lan",
|
|
13
|
+
];
|
|
14
|
+
export function stripHash(value) {
|
|
15
|
+
const index = value.indexOf("#");
|
|
16
|
+
return index === -1 ? value : value.slice(0, index);
|
|
17
|
+
}
|
|
18
|
+
export function getHash(value) {
|
|
19
|
+
const index = value.indexOf("#");
|
|
20
|
+
if (index === -1)
|
|
21
|
+
return "#";
|
|
22
|
+
return value.slice(index) || "#";
|
|
23
|
+
}
|
|
24
|
+
export function isHttp(value) {
|
|
25
|
+
return /^https?:\/\//i.test(value);
|
|
26
|
+
}
|
|
27
|
+
export function isFileSystemPath(value) {
|
|
28
|
+
if (!value)
|
|
29
|
+
return false;
|
|
30
|
+
if (value.startsWith("file:"))
|
|
31
|
+
return true;
|
|
32
|
+
return !/^[a-z][a-z0-9+.-]*:/i.test(value);
|
|
33
|
+
}
|
|
34
|
+
export function httpOrigin(value) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = new URL(value);
|
|
37
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
38
|
+
return undefined;
|
|
39
|
+
return parsed.origin;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const normalizeRef = Effect.fnUntraced(function* (input) {
|
|
46
|
+
if (input && /^[a-z][a-z0-9+.-]*:/i.test(input))
|
|
47
|
+
return input;
|
|
48
|
+
const path = yield* Path.Path;
|
|
49
|
+
const absolute = !input || input === "/" ? `${path.resolve(".")}${path.sep}` : path.resolve(input);
|
|
50
|
+
return yield* path.toFileUrl(absolute).pipe(Effect.map((url) => url.href), Effect.mapError((cause) => new ResolverFailure({
|
|
51
|
+
message: cause.message,
|
|
52
|
+
source: input ?? "",
|
|
53
|
+
cause,
|
|
54
|
+
})));
|
|
55
|
+
}, Effect.provide(NodePath.layer));
|
|
56
|
+
export function resolveAgainst(from, to) {
|
|
57
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(to))
|
|
58
|
+
return to;
|
|
59
|
+
return new URL(to, stripHash(from)).href;
|
|
60
|
+
}
|
|
61
|
+
export function toFileSystemPath(ref) {
|
|
62
|
+
const withoutHash = stripHash(ref);
|
|
63
|
+
if (withoutHash.startsWith("file:"))
|
|
64
|
+
return fileURLToPath(withoutHash);
|
|
65
|
+
return withoutHash;
|
|
66
|
+
}
|
|
67
|
+
export function extensionOf(ref) {
|
|
68
|
+
const pathname = stripHash(ref).split("?")[0] ?? "";
|
|
69
|
+
const base = pathname.slice(Math.max(pathname.lastIndexOf("/"), pathname.lastIndexOf("\\")) + 1);
|
|
70
|
+
const dot = base.lastIndexOf(".");
|
|
71
|
+
return dot >= 0 ? base.slice(dot).toLowerCase() : "";
|
|
72
|
+
}
|
|
73
|
+
export function isUnsafeUrl(value) {
|
|
74
|
+
const normalized = value.trim().toLowerCase();
|
|
75
|
+
if (!normalized)
|
|
76
|
+
return true;
|
|
77
|
+
if (normalized.startsWith("javascript:") ||
|
|
78
|
+
normalized.startsWith("vbscript:") ||
|
|
79
|
+
normalized.startsWith("data:")) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (normalized.startsWith("file:"))
|
|
83
|
+
return true;
|
|
84
|
+
try {
|
|
85
|
+
const url = new URL(normalized.startsWith("//") ? `http:${normalized}` : normalized);
|
|
86
|
+
if (isUnsafeHostname(url.hostname))
|
|
87
|
+
return true;
|
|
88
|
+
if (url.port && isInternalPort(Number(url.port)))
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
export function isAllowedOrigin(href, allowedOrigins) {
|
|
97
|
+
const origin = httpOrigin(href);
|
|
98
|
+
if (!origin || !allowedOrigins?.length)
|
|
99
|
+
return false;
|
|
100
|
+
return allowedOrigins.some((allowed) => httpOrigin(allowed) === origin);
|
|
101
|
+
}
|
|
102
|
+
export const resolveUrlSafety = Effect.fnUntraced(function* (href) {
|
|
103
|
+
if (isUnsafeUrl(href))
|
|
104
|
+
return { unsafe: true };
|
|
105
|
+
if (typeof process === "undefined" || !process.versions?.node)
|
|
106
|
+
return { unsafe: false };
|
|
107
|
+
const parsed = URL.parse(href.startsWith("//") ? `http:${href}` : href);
|
|
108
|
+
if (parsed === null)
|
|
109
|
+
return { unsafe: true };
|
|
110
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
111
|
+
return { unsafe: false };
|
|
112
|
+
const onError = (cause) => new ResolverFailure({
|
|
113
|
+
message: `Error downloading ${href}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
|
114
|
+
source: href,
|
|
115
|
+
cause,
|
|
116
|
+
...ioCode(cause),
|
|
117
|
+
});
|
|
118
|
+
const dns = yield* Effect.tryPromise({
|
|
119
|
+
try: () => import("node:dns/promises"),
|
|
120
|
+
catch: onError,
|
|
121
|
+
});
|
|
122
|
+
const addresses = yield* Effect.tryPromise({
|
|
123
|
+
try: () => dns.lookup(normalizeHostname(parsed.hostname), { all: true, verbatim: true }),
|
|
124
|
+
catch: onError,
|
|
125
|
+
});
|
|
126
|
+
if (addresses.length === 0 || addresses.some(({ address }) => isUnsafeHostname(address))) {
|
|
127
|
+
return { unsafe: true };
|
|
128
|
+
}
|
|
129
|
+
return { unsafe: false, addresses };
|
|
130
|
+
});
|
|
131
|
+
export function isUnsafeHostname(hostname) {
|
|
132
|
+
const normalized = normalizeHostname(hostname);
|
|
133
|
+
if (!normalized)
|
|
134
|
+
return true;
|
|
135
|
+
if (normalized === "localhost" ||
|
|
136
|
+
unsafeDomainSuffixes.some((suffix) => normalized.endsWith(suffix)))
|
|
137
|
+
return true;
|
|
138
|
+
const ipv4 = parseIPv4Address(normalized);
|
|
139
|
+
if (ipv4)
|
|
140
|
+
return isUnsafeIPv4Address(ipv4);
|
|
141
|
+
const ipv6 = parseIPv6Address(normalized);
|
|
142
|
+
if (ipv6)
|
|
143
|
+
return isUnsafeIPv6Address(ipv6);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function ioCode(cause) {
|
|
147
|
+
const ioErrorCode = nodeErrorCode(cause);
|
|
148
|
+
return ioErrorCode ? { ioErrorCode } : undefined;
|
|
149
|
+
}
|
|
150
|
+
function normalizeHostname(hostname) {
|
|
151
|
+
let normalized = hostname.trim().toLowerCase();
|
|
152
|
+
if (normalized.startsWith("[") && normalized.endsWith("]"))
|
|
153
|
+
normalized = normalized.slice(1, -1);
|
|
154
|
+
while (normalized.endsWith("."))
|
|
155
|
+
normalized = normalized.slice(0, -1);
|
|
156
|
+
return normalized;
|
|
157
|
+
}
|
|
158
|
+
function parseIPv4Address(ip) {
|
|
159
|
+
const parts = ip.split(".");
|
|
160
|
+
if (parts.length !== 4)
|
|
161
|
+
return undefined;
|
|
162
|
+
const octets = parts.map((part) => (/^\d+$/.test(part) ? Number(part) : Number.NaN));
|
|
163
|
+
if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
|
|
164
|
+
return undefined;
|
|
165
|
+
return octets;
|
|
166
|
+
}
|
|
167
|
+
function isUnsafeIPv4Address(octets) {
|
|
168
|
+
const a = octets[0];
|
|
169
|
+
const b = octets[1];
|
|
170
|
+
const c = octets[2];
|
|
171
|
+
const d = octets[3];
|
|
172
|
+
if (a === undefined || b === undefined || c === undefined || d === undefined)
|
|
173
|
+
return true;
|
|
174
|
+
return (a === 0 ||
|
|
175
|
+
a === 10 ||
|
|
176
|
+
a === 127 ||
|
|
177
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
178
|
+
(a === 169 && b === 254) ||
|
|
179
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
180
|
+
(a === 192 && b === 0 && c === 0) ||
|
|
181
|
+
(a === 192 && b === 168) ||
|
|
182
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
183
|
+
a >= 224 ||
|
|
184
|
+
(a === 255 && b === 255 && c === 255 && d === 255));
|
|
185
|
+
}
|
|
186
|
+
function parseIPv6Address(ip) {
|
|
187
|
+
if (!ip.includes(":"))
|
|
188
|
+
return undefined;
|
|
189
|
+
let normalized = ip;
|
|
190
|
+
const lastSeparator = normalized.lastIndexOf(":");
|
|
191
|
+
const possibleIPv4 = normalized.slice(lastSeparator + 1);
|
|
192
|
+
if (possibleIPv4.includes(".")) {
|
|
193
|
+
const ipv4 = parseIPv4Address(possibleIPv4);
|
|
194
|
+
if (!ipv4)
|
|
195
|
+
return undefined;
|
|
196
|
+
const firstGroup = ipv4[0] * 256 + ipv4[1];
|
|
197
|
+
const secondGroup = ipv4[2] * 256 + ipv4[3];
|
|
198
|
+
normalized = `${normalized.slice(0, lastSeparator + 1)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
|
|
199
|
+
}
|
|
200
|
+
const halves = normalized.split("::");
|
|
201
|
+
if (halves.length > 2)
|
|
202
|
+
return undefined;
|
|
203
|
+
const head = parseIPv6Groups(halves[0] ?? "");
|
|
204
|
+
const tail = halves.length === 2 ? parseIPv6Groups(halves[1] ?? "") : [];
|
|
205
|
+
if (!head || !tail)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (halves.length === 1)
|
|
208
|
+
return head.length === 8 ? head : undefined;
|
|
209
|
+
const missing = 8 - head.length - tail.length;
|
|
210
|
+
if (missing < 1)
|
|
211
|
+
return undefined;
|
|
212
|
+
return [...head, ...Array(missing).fill(0), ...tail];
|
|
213
|
+
}
|
|
214
|
+
function parseIPv6Groups(groups) {
|
|
215
|
+
if (!groups)
|
|
216
|
+
return [];
|
|
217
|
+
const parsed = groups
|
|
218
|
+
.split(":")
|
|
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))
|
|
221
|
+
return undefined;
|
|
222
|
+
return parsed;
|
|
223
|
+
}
|
|
224
|
+
function isUnsafeIPv6Address(groups) {
|
|
225
|
+
if (groups.length !== 8)
|
|
226
|
+
return false;
|
|
227
|
+
const unspecified = groups.every((group) => group === 0);
|
|
228
|
+
const loopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
|
|
229
|
+
const uniqueLocal = (groups[0] & 0xfe00) === 0xfc00;
|
|
230
|
+
const linkLocal = (groups[0] & 0xffc0) === 0xfe80;
|
|
231
|
+
const multicast = (groups[0] & 0xff00) === 0xff00;
|
|
232
|
+
if (unspecified || loopback || uniqueLocal || linkLocal || multicast)
|
|
233
|
+
return true;
|
|
234
|
+
const mapped = mappedIPv4(groups);
|
|
235
|
+
return mapped ? isUnsafeIPv4Address(mapped) : false;
|
|
236
|
+
}
|
|
237
|
+
function mappedIPv4(groups) {
|
|
238
|
+
const firstFive = groups.slice(0, 5).every((group) => group === 0);
|
|
239
|
+
const firstSix = firstFive && groups[5] === 0;
|
|
240
|
+
const v4Mapped = firstFive && groups[5] === 0xffff;
|
|
241
|
+
if (!firstSix && !v4Mapped)
|
|
242
|
+
return undefined;
|
|
243
|
+
return [
|
|
244
|
+
Math.floor(groups[6] / 256),
|
|
245
|
+
groups[6] % 256,
|
|
246
|
+
Math.floor(groups[7] / 256),
|
|
247
|
+
groups[7] % 256,
|
|
248
|
+
];
|
|
249
|
+
}
|
|
250
|
+
function isInternalPort(port) {
|
|
251
|
+
return [
|
|
252
|
+
22, 23, 25, 53, 135, 139, 445, 993, 995, 1433, 1521, 3306, 3389, 5432, 5900, 6379, 8080, 8443,
|
|
253
|
+
9200, 27017,
|
|
254
|
+
].includes(port);
|
|
255
|
+
}
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect's YAML decoder rejects `- - value` and treats `<<` as a normal key.
|
|
3
|
+
* OpenAPI fixtures in this repo use both, so expand those forms before parsing.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseYamlDocument(source: string): unknown;
|
|
6
|
+
export declare function expandCompactNestedSequences(source: string): string;
|