@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/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 Object.keys(record).every((key) => referenceKeys.has(key));
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
- !record.$ref.startsWith("#") &&
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(...Object.values(record));
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" && !record.$ref.startsWith("#")) {
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 Object.entries(mapping)) {
168
- if (typeof value !== "string" || value.startsWith("#"))
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 Object.entries(record)) {
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" || record.$ref.startsWith("#"))
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 Object.entries(mapping)) {
229
- if (typeof value !== "string" || value.startsWith("#"))
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 (typeof root !== "object" || root === null || seen.has(root))
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 Object.entries(root)) {
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 Object.values(record))
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
- if (typeof current !== "object" || current === null || !Object.hasOwn(current, token)) {
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) => !key.startsWith("x-"))), Schema.suspend(() => OpenApiPathItem));
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) => Object.hasOwn(value, "value") && value.externalValue !== undefined
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 && Object.keys(value.content).length !== 1) {
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 (Object.hasOwn(value, "example") && value.examples !== undefined) {
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) => Object.hasOwn(value, "example") && value.examples !== undefined
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 && Object.keys(value.content).length !== 1) {
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 (Object.hasOwn(value, "example") && value.examples !== undefined) {
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) => Object.keys(value).some((key) => !key.startsWith("x-"))
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" && record.openapi.startsWith("3.");
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" && record.openapi.startsWith("3.0");
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 Object.values(pathItems))
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 Object.values(paths))
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 Object.values(webhooks))
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 Object.keys(map))
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 Object.values(map))
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 Object.values(map))
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 Object.values(map))
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 Object.values(map))
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 Object.entries(record)) {
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 Object.values(responses))
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 Object.values(callbacks)) {
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 Object.values(paths))
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 (!Array.isArray(value))
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 Object.values(content)) {
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" && Array.isArray(record.items)) {
192
- record.items = record.items.map((item) => upgradeSchema(item));
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
- if (Array.isArray(record[key]))
200
- record[key] = record[key].map((item) => upgradeSchema(item));
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 Object.keys(map))
207
+ for (const child of Rec.keys(map))
207
208
  map[child] = upgradeSchema(map[child]);
208
209
  }
209
- if (Array.isArray(record.items)) {
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" && Object.keys(record).some((key) => key !== "$ref")) {
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 = Object.keys(siblings).length > 0 ? { allOf: [{ $ref }, siblings] } : { $ref };
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 Object.entries(schema)) {
259
- if (ANNOTATION_KEYS.has(key) || key.startsWith("x-"))
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 Object.keys(inner).length === 0 ? schema : { ...outer, anyOf: [inner, { type: "null" }] };
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 = Array.isArray(schema.examples) ? schema.examples : [];
293
- const convertedExamples = Array.isArray(converted.examples) ? converted.examples : [];
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 (Array.isArray(schema.enum) && !schema.enum.includes(null))
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
- const index = value.indexOf("#");
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
- const index = value.indexOf("#");
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 (value.startsWith("file:"))
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 (withoutHash.startsWith("file:"))
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("?")[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() : "";
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().toLowerCase();
72
+ const normalized = pipe(value, Str.trim, Str.toLowerCase);
75
73
  if (!normalized)
76
74
  return true;
77
- if (normalized.startsWith("javascript:") ||
78
- normalized.startsWith("vbscript:") ||
79
- normalized.startsWith("data:")) {
75
+ if (Str.startsWith("javascript:")(normalized) ||
76
+ Str.startsWith("vbscript:")(normalized) ||
77
+ Str.startsWith("data:")(normalized)) {
80
78
  return true;
81
79
  }
82
- if (normalized.startsWith("file:"))
80
+ if (Str.startsWith("file:")(normalized))
83
81
  return true;
84
82
  try {
85
- const url = new URL(normalized.startsWith("//") ? `http:${normalized}` : normalized);
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 allowedOrigins.some((allowed) => httpOrigin(allowed) === origin);
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(href.startsWith("//") ? `http:${href}` : href);
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 || addresses.some(({ address }) => isUnsafeHostname(address))) {
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
- unsafeDomainSuffixes.some((suffix) => normalized.endsWith(suffix)))
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().toLowerCase();
152
- if (normalized.startsWith("[") && normalized.endsWith("]"))
153
- normalized = normalized.slice(1, -1);
154
- while (normalized.endsWith("."))
155
- normalized = normalized.slice(0, -1);
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 = ip.split(".");
157
+ const parts = Str.split(ip, ".");
160
158
  if (parts.length !== 4)
161
159
  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))
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
- if (!ip.includes(":"))
185
+ const separator = Str.lastIndexOf(":")(ip);
186
+ if (Option.isNone(separator))
188
187
  return undefined;
189
188
  let normalized = ip;
190
- const lastSeparator = normalized.lastIndexOf(":");
191
- const possibleIPv4 = normalized.slice(lastSeparator + 1);
192
- if (possibleIPv4.includes(".")) {
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 = `${normalized.slice(0, lastSeparator + 1)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
197
+ normalized = `${Str.slice(0, lastSeparator + 1)(normalized)}${firstGroup.toString(16)}:${secondGroup.toString(16)}`;
199
198
  }
200
- const halves = normalized.split("::");
199
+ const halves = Str.split(normalized, "::");
201
200
  if (halves.length > 2)
202
201
  return undefined;
203
- const head = parseIPv6Groups(halves[0] ?? "");
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
- .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))
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 = groups.every((group) => group === 0);
228
- const loopback = groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
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).every((group) => group === 0);
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)