z-schema 12.2.0 → 12.4.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 +2 -2
- package/bin/z-schema +1 -1
- package/cjs/{index.js → index.cjs} +838 -707
- package/cjs/{index.d.ts → index.d.cts} +47 -26
- package/dist/{errors.d.mts → errors.d.ts} +2 -2
- package/dist/{errors.mjs → errors.js} +1 -2
- package/dist/{format-validators.mjs → format-validators.js} +60 -43
- package/dist/{index.d.mts → index.d.ts} +9 -9
- package/dist/{index.mjs → index.js} +3 -3
- package/dist/{json-schema-versions.d.mts → json-schema-versions.d.ts} +34 -3
- package/dist/{json-schema.d.mts → json-schema.d.ts} +7 -7
- package/dist/{json-schema.mjs → json-schema.js} +6 -5
- package/dist/{json-validation.mjs → json-validation.js} +144 -127
- package/dist/{report.d.mts → report.d.ts} +8 -9
- package/dist/{report.mjs → report.js} +28 -31
- package/dist/{schema-cache.d.mts → schema-cache.d.ts} +5 -5
- package/dist/{schema-cache.mjs → schema-cache.js} +10 -11
- package/dist/{schema-compiler.d.mts → schema-compiler.d.ts} +4 -4
- package/dist/{schema-compiler.mjs → schema-compiler.js} +101 -79
- package/dist/{schema-validator.d.mts → schema-validator.d.ts} +5 -5
- package/dist/{schema-validator.mjs → schema-validator.js} +138 -166
- package/dist/utils/{array.mjs → array.js} +4 -3
- package/dist/utils/{base64.mjs → base64.js} +3 -2
- package/dist/utils/{clone.mjs → clone.js} +18 -20
- package/dist/utils/hostname.js +176 -0
- package/dist/utils/{json.mjs → json.js} +13 -8
- package/dist/utils/{schema-regex.mjs → schema-regex.js} +5 -5
- package/dist/utils/{time.mjs → time.js} +8 -8
- package/dist/utils/unicode.js +22 -0
- package/dist/utils/{what-is.mjs → what-is.js} +1 -2
- package/dist/validation/{array.mjs → array.js} +18 -20
- package/dist/validation/{combinators.mjs → combinators.js} +16 -16
- package/dist/validation/{numeric.mjs → numeric.js} +11 -11
- package/dist/validation/{object.mjs → object.js} +35 -34
- package/dist/validation/{ref.mjs → ref.js} +5 -5
- package/dist/validation/{shared.mjs → shared.js} +15 -12
- package/dist/validation/{string.mjs → string.js} +35 -35
- package/dist/validation/type.js +34 -0
- package/dist/{z-schema-base.d.mts → z-schema-base.d.ts} +11 -12
- package/dist/{z-schema-base.mjs → z-schema-base.js} +45 -40
- package/dist/{z-schema-options.d.mts → z-schema-options.d.ts} +3 -3
- package/dist/{z-schema-options.mjs → z-schema-options.js} +5 -5
- package/dist/{z-schema-reader.d.mts → z-schema-reader.d.ts} +1 -1
- package/dist/{z-schema-versions.mjs → z-schema-versions.js} +21 -21
- package/dist/{z-schema.d.mts → z-schema.d.ts} +5 -13
- package/dist/{z-schema.mjs → z-schema.js} +38 -48
- package/package.json +24 -25
- package/src/errors.ts +1 -2
- package/src/format-validators.ts +173 -67
- package/src/json-schema-versions.ts +56 -2
- package/src/json-schema.ts +10 -9
- package/src/json-validation.ts +189 -146
- package/src/report.ts +37 -49
- package/src/schema-cache.ts +13 -13
- package/src/schema-compiler.ts +170 -117
- package/src/schema-validator.ts +239 -238
- package/src/utils/array.ts +9 -6
- package/src/utils/base64.ts +13 -2
- package/src/utils/clone.ts +28 -30
- package/src/utils/date.ts +6 -3
- package/src/utils/hostname.ts +257 -31
- package/src/utils/json.ts +16 -9
- package/src/utils/properties.ts +2 -2
- package/src/utils/schema-regex.ts +4 -4
- package/src/utils/time.ts +8 -7
- package/src/utils/unicode.ts +12 -5
- package/src/utils/what-is.ts +1 -5
- package/src/validation/array.ts +24 -22
- package/src/validation/combinators.ts +14 -14
- package/src/validation/numeric.ts +14 -28
- package/src/validation/object.ts +32 -36
- package/src/validation/ref.ts +5 -6
- package/src/validation/shared.ts +22 -21
- package/src/validation/string.ts +36 -42
- package/src/validation/type.ts +17 -17
- package/src/z-schema-base.ts +49 -38
- package/src/z-schema-options.ts +4 -3
- package/src/z-schema.ts +35 -45
- package/umd/ZSchema.js +853 -715
- package/umd/ZSchema.min.js +2 -2
- package/umd/package.json +3 -0
- package/dist/utils/hostname.mjs +0 -75
- package/dist/utils/unicode.mjs +0 -12
- package/dist/validation/type.mjs +0 -32
- /package/dist/{format-validators.d.mts → format-validators.d.ts} +0 -0
- /package/dist/{json-schema-versions.mjs → json-schema-versions.js} +0 -0
- /package/dist/schemas/{draft-04-schema.mjs → draft-04-schema.js} +0 -0
- /package/dist/schemas/{draft-06-schema.mjs → draft-06-schema.js} +0 -0
- /package/dist/schemas/{draft-07-schema.mjs → draft-07-schema.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-applicator.mjs → draft-2019-09-meta-applicator.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-content.mjs → draft-2019-09-meta-content.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-core.mjs → draft-2019-09-meta-core.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-format.mjs → draft-2019-09-meta-format.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-meta-data.mjs → draft-2019-09-meta-meta-data.js} +0 -0
- /package/dist/schemas/{draft-2019-09-meta-validation.mjs → draft-2019-09-meta-validation.js} +0 -0
- /package/dist/schemas/{draft-2019-09-schema.mjs → draft-2019-09-schema.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-applicator.mjs → draft-2020-12-meta-applicator.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-content.mjs → draft-2020-12-meta-content.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-core.mjs → draft-2020-12-meta-core.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-format-annotation.mjs → draft-2020-12-meta-format-annotation.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-format-assertion.mjs → draft-2020-12-meta-format-assertion.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-meta-data.mjs → draft-2020-12-meta-meta-data.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-unevaluated.mjs → draft-2020-12-meta-unevaluated.js} +0 -0
- /package/dist/schemas/{draft-2020-12-meta-validation.mjs → draft-2020-12-meta-validation.js} +0 -0
- /package/dist/schemas/{draft-2020-12-schema.mjs → draft-2020-12-schema.js} +0 -0
- /package/dist/utils/{constants.mjs → constants.js} +0 -0
- /package/dist/utils/{date.mjs → date.js} +0 -0
- /package/dist/utils/{properties.mjs → properties.js} +0 -0
- /package/dist/utils/{symbols.mjs → symbols.js} +0 -0
- /package/dist/utils/{uri.mjs → uri.js} +0 -0
- /package/dist/{z-schema-reader.mjs → z-schema-reader.js} +0 -0
package/src/format-validators.ts
CHANGED
|
@@ -14,13 +14,13 @@ const dateValidator: FormatValidatorFn = (date: unknown) => {
|
|
|
14
14
|
return true;
|
|
15
15
|
}
|
|
16
16
|
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
|
|
17
|
-
const matches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(date);
|
|
17
|
+
const matches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(date);
|
|
18
18
|
if (matches === null) {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
-
const year = parseInt(matches[1], 10);
|
|
22
|
-
const month = parseInt(matches[2], 10);
|
|
23
|
-
const day = parseInt(matches[3], 10);
|
|
21
|
+
const year = Number.parseInt(matches[1], 10);
|
|
22
|
+
const month = Number.parseInt(matches[2], 10);
|
|
23
|
+
const day = Number.parseInt(matches[3], 10);
|
|
24
24
|
return isValidRfc3339Date(year, month, day);
|
|
25
25
|
};
|
|
26
26
|
|
|
@@ -29,20 +29,23 @@ const dateTimeValidator: FormatValidatorFn = (dateTime: unknown) => {
|
|
|
29
29
|
return true;
|
|
30
30
|
}
|
|
31
31
|
// date-time from http://tools.ietf.org/html/rfc3339#section-5.6
|
|
32
|
-
|
|
33
|
-
if (
|
|
32
|
+
let tIdx = dateTime.indexOf('T');
|
|
33
|
+
if (tIdx === -1) {
|
|
34
|
+
tIdx = dateTime.indexOf('t');
|
|
35
|
+
}
|
|
36
|
+
if (tIdx === -1) {
|
|
34
37
|
return false;
|
|
35
38
|
}
|
|
36
|
-
const datePart =
|
|
37
|
-
const timePart =
|
|
39
|
+
const datePart = dateTime.slice(0, tIdx);
|
|
40
|
+
const timePart = dateTime.slice(tIdx + 1);
|
|
38
41
|
// Check date
|
|
39
|
-
const dateMatches = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(datePart);
|
|
42
|
+
const dateMatches = /^(?<year>[0-9]{4})-(?<month>[0-9]{2})-(?<day>[0-9]{2})$/.exec(datePart);
|
|
40
43
|
if (dateMatches === null) {
|
|
41
44
|
return false;
|
|
42
45
|
}
|
|
43
|
-
const year = parseInt(dateMatches[1], 10);
|
|
44
|
-
const month = parseInt(dateMatches[2], 10);
|
|
45
|
-
const day = parseInt(dateMatches[3], 10);
|
|
46
|
+
const year = Number.parseInt(dateMatches[1], 10);
|
|
47
|
+
const month = Number.parseInt(dateMatches[2], 10);
|
|
48
|
+
const day = Number.parseInt(dateMatches[3], 10);
|
|
46
49
|
if (!isValidRfc3339Date(year, month, day)) {
|
|
47
50
|
return false;
|
|
48
51
|
}
|
|
@@ -58,7 +61,7 @@ const emailValidator: FormatValidatorFn = (email: unknown) => {
|
|
|
58
61
|
return true;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
const ipv6Literal = /^(
|
|
64
|
+
const ipv6Literal = /^(?<localPart>.+)@\[IPv6:(?<address>[^\]]+)\]$/i.exec(email);
|
|
62
65
|
if (!ipv6Literal) {
|
|
63
66
|
return false;
|
|
64
67
|
}
|
|
@@ -96,12 +99,13 @@ const ipv6Validator: FormatValidatorFn = (ipv6: unknown) => {
|
|
|
96
99
|
return isIPModule.default(ipv6, 6);
|
|
97
100
|
};
|
|
98
101
|
|
|
102
|
+
const INVALID_REGEX_ESCAPES = new Set(['a']);
|
|
103
|
+
|
|
99
104
|
const regexValidator: FormatValidatorFn = (input: unknown) => {
|
|
100
105
|
if (typeof input !== 'string') {
|
|
101
106
|
return true;
|
|
102
107
|
}
|
|
103
108
|
|
|
104
|
-
const invalidEscapes = new Set(['a']);
|
|
105
109
|
for (let idx = 0; idx < input.length; idx++) {
|
|
106
110
|
if (input[idx] !== '\\') {
|
|
107
111
|
continue;
|
|
@@ -113,15 +117,19 @@ const regexValidator: FormatValidatorFn = (input: unknown) => {
|
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
const escaped = input[idx];
|
|
116
|
-
if (
|
|
120
|
+
if (INVALID_REGEX_ESCAPES.has(escaped)) {
|
|
117
121
|
return false;
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
try {
|
|
122
|
-
|
|
126
|
+
// Constructed purely to detect an invalid pattern (throws SyntaxError). The
|
|
127
|
+
// result is intentionally unused; `new` is required by new-for-builtins and
|
|
128
|
+
// `void`/call-form trip no-void/new-for-builtins, so no-new is disabled here.
|
|
129
|
+
// oxlint-disable-next-line no-new
|
|
130
|
+
new RegExp(input);
|
|
123
131
|
return true;
|
|
124
|
-
} catch
|
|
132
|
+
} catch {
|
|
125
133
|
return false;
|
|
126
134
|
}
|
|
127
135
|
};
|
|
@@ -132,7 +140,7 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
|
|
|
132
140
|
}
|
|
133
141
|
|
|
134
142
|
// eslint-disable-next-line no-control-regex
|
|
135
|
-
if (!/^P[\
|
|
143
|
+
if (!/^P[\u0000-\u007F]*$/.test(input)) {
|
|
136
144
|
return false;
|
|
137
145
|
}
|
|
138
146
|
|
|
@@ -163,7 +171,7 @@ const durationValidator: FormatValidatorFn = (input: unknown) => {
|
|
|
163
171
|
return false;
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
const hasDateComponent =
|
|
174
|
+
const hasDateComponent = datePart.length > 0;
|
|
167
175
|
let hasTimeComponent = false;
|
|
168
176
|
|
|
169
177
|
if (timePart !== undefined) {
|
|
@@ -193,43 +201,55 @@ const uuidValidator: FormatValidatorFn = (input: unknown) => {
|
|
|
193
201
|
|
|
194
202
|
const strictUriValidator: FormatValidatorFn = (uri: unknown) => typeof uri !== 'string' || isURLModule.default(uri);
|
|
195
203
|
|
|
204
|
+
const isHexChar = (c: number) => (c >= 48 && c <= 57) || (c >= 65 && c <= 70) || (c >= 97 && c <= 102);
|
|
205
|
+
|
|
196
206
|
const hasValidPercentEncoding = (str: string): boolean => {
|
|
197
207
|
for (let i = 0; i < str.length; i++) {
|
|
198
|
-
if (
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
208
|
+
if (
|
|
209
|
+
str[i] === '%' &&
|
|
210
|
+
(i + 2 >= str.length || !isHexChar(str.charCodeAt(i + 1)) || !isHexChar(str.charCodeAt(i + 2)))
|
|
211
|
+
) {
|
|
212
|
+
return false;
|
|
202
213
|
}
|
|
203
214
|
}
|
|
204
215
|
return true;
|
|
205
216
|
};
|
|
206
217
|
|
|
207
|
-
const uriValidator: FormatValidatorFn =
|
|
208
|
-
if (typeof uri !== 'string')
|
|
218
|
+
const uriValidator: FormatValidatorFn = (uri: unknown) => {
|
|
219
|
+
if (typeof uri !== 'string') {
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
209
222
|
// eslint-disable-next-line no-control-regex
|
|
210
|
-
if (/[^\
|
|
211
|
-
|
|
212
|
-
|
|
223
|
+
if (/[^\u0000-\u007F]/.test(uri)) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
if (!hasValidPercentEncoding(uri)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const match = /^(?<scheme>[a-zA-Z][a-zA-Z0-9+.-]*):\/\/(?<authority>[^/?#]*)/.exec(uri);
|
|
213
230
|
if (match) {
|
|
214
231
|
const authority = match[2];
|
|
215
232
|
const atIndex = authority.indexOf('@');
|
|
216
233
|
if (atIndex > 0) {
|
|
217
|
-
|
|
234
|
+
// userinfo is a string; prefer-set-has misfires here — two String#includes
|
|
235
|
+
// calls are faster than building a Set or a regex for this membership check.
|
|
236
|
+
// oxlint-disable-next-line unicorn/prefer-set-has
|
|
237
|
+
const userinfo = authority.slice(0, atIndex);
|
|
218
238
|
if (userinfo.includes('[') || userinfo.includes(']')) {
|
|
219
239
|
return false;
|
|
220
240
|
}
|
|
221
241
|
}
|
|
222
242
|
// Validate port: must be numeric
|
|
223
|
-
let hostPort = atIndex
|
|
243
|
+
let hostPort = atIndex === -1 ? authority : authority.slice(atIndex + 1);
|
|
224
244
|
if (hostPort.startsWith('[')) {
|
|
225
245
|
const bracketEnd = hostPort.indexOf(']');
|
|
226
|
-
if (bracketEnd
|
|
227
|
-
hostPort = hostPort.
|
|
246
|
+
if (bracketEnd !== -1) {
|
|
247
|
+
hostPort = hostPort.slice(bracketEnd + 1);
|
|
228
248
|
}
|
|
229
249
|
}
|
|
230
250
|
const colonIndex = hostPort.lastIndexOf(':');
|
|
231
|
-
if (colonIndex
|
|
232
|
-
const port = hostPort.
|
|
251
|
+
if (colonIndex !== -1) {
|
|
252
|
+
const port = hostPort.slice(colonIndex + 1);
|
|
233
253
|
if (port.length > 0 && !/^\d+$/.test(port)) {
|
|
234
254
|
return false;
|
|
235
255
|
}
|
|
@@ -239,17 +259,23 @@ const uriValidator: FormatValidatorFn = function (uri: unknown) {
|
|
|
239
259
|
};
|
|
240
260
|
|
|
241
261
|
const uriReferenceValidator: FormatValidatorFn = (uri: unknown) => {
|
|
242
|
-
if (typeof uri !== 'string')
|
|
262
|
+
if (typeof uri !== 'string') {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
243
265
|
// eslint-disable-next-line no-control-regex
|
|
244
|
-
if (/[^\
|
|
266
|
+
if (/[^\u0000-\u007F]/.test(uri)) {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
245
269
|
// URI-reference allows relative URIs
|
|
246
|
-
return /^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
|
|
270
|
+
return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/.test(uri);
|
|
247
271
|
};
|
|
248
272
|
|
|
249
273
|
const uriTemplateValidator: FormatValidatorFn = (uri: unknown) => {
|
|
250
|
-
if (typeof uri !== 'string')
|
|
274
|
+
if (typeof uri !== 'string') {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
251
277
|
// URI template allows braces for expressions.
|
|
252
|
-
if (!/^([a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
|
|
278
|
+
if (!/^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^`| ]*$/.test(uri)) {
|
|
253
279
|
return false;
|
|
254
280
|
}
|
|
255
281
|
|
|
@@ -286,67 +312,127 @@ const hasValidTildeEscapes = (segment: string): boolean => {
|
|
|
286
312
|
};
|
|
287
313
|
|
|
288
314
|
const jsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
|
|
289
|
-
if (typeof pointer !== 'string')
|
|
315
|
+
if (typeof pointer !== 'string') {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
290
318
|
// JSON Pointer: empty, or a sequence of '/'-prefixed reference tokens.
|
|
291
319
|
// In each token, '~' must be escaped as '~0' or '~1'.
|
|
292
|
-
if (pointer === '')
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
320
|
+
if (pointer === '') {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (!/^(?:\/[^/]*)+$/.test(pointer)) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const tokens = pointer.split('/');
|
|
327
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
328
|
+
if (!hasValidTildeEscapes(tokens[i])) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
297
331
|
}
|
|
298
332
|
return true;
|
|
299
333
|
};
|
|
300
334
|
|
|
301
335
|
const relativeJsonPointerValidator: FormatValidatorFn = (pointer: unknown) => {
|
|
302
|
-
if (typeof pointer !== 'string')
|
|
336
|
+
if (typeof pointer !== 'string') {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
303
339
|
// Relative JSON Pointer: non-negative integer prefix (no leading zeros unless zero),
|
|
304
340
|
// followed by either '#', a JSON Pointer, or nothing.
|
|
305
|
-
const match =
|
|
306
|
-
if (!match)
|
|
341
|
+
const match = /^(?<int>0|[1-9]\d*)(?<suffix>.*)$/.exec(pointer);
|
|
342
|
+
if (!match) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
307
345
|
const suffix = match[2];
|
|
308
|
-
if (suffix === '' || suffix === '#')
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
346
|
+
if (suffix === '' || suffix === '#') {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
if (!suffix.startsWith('/')) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
if (!/^(?:\/[^/]*)+$/.test(suffix)) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
const tokens = suffix.split('/');
|
|
356
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
357
|
+
if (!hasValidTildeEscapes(tokens[i])) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
314
360
|
}
|
|
315
361
|
return true;
|
|
316
362
|
};
|
|
317
363
|
|
|
318
364
|
const timeValidator: FormatValidatorFn = (time: unknown) => {
|
|
319
|
-
if (typeof time !== 'string')
|
|
365
|
+
if (typeof time !== 'string') {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
320
368
|
return parseRfc3339Time(time) !== null;
|
|
321
369
|
};
|
|
322
370
|
|
|
371
|
+
// Matches a lone (unpaired) UTF-16 surrogate — either a high surrogate not
|
|
372
|
+
// followed by a low surrogate, or a low surrogate not preceded by a high one.
|
|
373
|
+
const LONE_SURROGATE_REGEX = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:^|[^\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
374
|
+
// A quoted local part: any char except a bare quote/backslash; backslash escapes
|
|
375
|
+
// allowed. Leniently permits raw control chars (e.g. bare CR/LF), which stricter
|
|
376
|
+
// RFC 5321 parsing forbids — an accepted limitation, not exercised by the suite.
|
|
377
|
+
const QUOTED_LOCAL_PART_REGEX = /^"(?:[^"\\]|\\.)*"$/u;
|
|
378
|
+
const UNQUOTED_LOCAL_PART_REGEX = /^[^\s@]+$/u;
|
|
379
|
+
|
|
380
|
+
const isValidIdnEmailLocalPart = (localPart: string): boolean => {
|
|
381
|
+
if (localPart.length === 0 || LONE_SURROGATE_REGEX.test(localPart)) {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
if (localPart.length >= 2 && localPart.startsWith('"') && localPart.endsWith('"')) {
|
|
385
|
+
return QUOTED_LOCAL_PART_REGEX.test(localPart);
|
|
386
|
+
}
|
|
387
|
+
return UNQUOTED_LOCAL_PART_REGEX.test(localPart);
|
|
388
|
+
};
|
|
389
|
+
|
|
323
390
|
const idnEmailValidator: FormatValidatorFn = (email: unknown) => {
|
|
324
|
-
if (typeof email !== 'string')
|
|
325
|
-
|
|
326
|
-
|
|
391
|
+
if (typeof email !== 'string') {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
// Split on the last '@': everything after it is the (idn-)hostname domain,
|
|
395
|
+
// everything before it is the local part (which may itself be quoted).
|
|
396
|
+
// Note: unlike the ASCII `email` validator, IP-literal domains
|
|
397
|
+
// (`user@[192.168.1.1]` / `@[IPv6:...]`) are not supported here.
|
|
398
|
+
const atIdx = email.lastIndexOf('@');
|
|
399
|
+
if (atIdx <= 0 || atIdx === email.length - 1) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
return isValidIdnEmailLocalPart(email.slice(0, atIdx)) && isValidIdnHostname(email.slice(atIdx + 1));
|
|
327
403
|
};
|
|
328
404
|
|
|
329
405
|
const idnHostnameValidator: FormatValidatorFn = (hostname: unknown) => {
|
|
330
|
-
if (typeof hostname !== 'string')
|
|
406
|
+
if (typeof hostname !== 'string') {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
331
409
|
return isValidIdnHostname(hostname);
|
|
332
410
|
};
|
|
333
411
|
|
|
334
412
|
const iriValidator: FormatValidatorFn = (iri: unknown) => {
|
|
335
|
-
if (typeof iri !== 'string')
|
|
413
|
+
if (typeof iri !== 'string') {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
336
416
|
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:[^"\\<>^{}^`| ]*$/u.test(iri)) {
|
|
337
417
|
return false;
|
|
338
418
|
}
|
|
339
419
|
try {
|
|
420
|
+
// Constructed purely to detect an unparseable IRI (throws). Avoids URL.canParse,
|
|
421
|
+
// which is unavailable in older browsers the UMD build still targets; no-new is
|
|
422
|
+
// disabled here as the construction is intentional and the result is unused.
|
|
423
|
+
// oxlint-disable-next-line no-new
|
|
340
424
|
new URL(iri);
|
|
341
425
|
return true;
|
|
342
|
-
} catch
|
|
426
|
+
} catch {
|
|
343
427
|
return false;
|
|
344
428
|
}
|
|
345
429
|
};
|
|
346
430
|
|
|
347
431
|
const iriReferenceValidator: FormatValidatorFn = (iriReference: unknown) => {
|
|
348
|
-
if (typeof iriReference !== 'string')
|
|
349
|
-
|
|
432
|
+
if (typeof iriReference !== 'string') {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
return /^(?:[a-zA-Z][a-zA-Z0-9+.-]*:)?[^"\\<>^{}^`| ]*$/u.test(iriReference);
|
|
350
436
|
};
|
|
351
437
|
|
|
352
438
|
export interface FormatValidatorsOptions {
|
|
@@ -385,10 +471,24 @@ export function getFormatValidators(options?: FormatValidatorsOptions): Record<s
|
|
|
385
471
|
...inbuiltValidators,
|
|
386
472
|
...(options?.strictUris ? { uri: strictUriValidator } : {}),
|
|
387
473
|
...customValidators,
|
|
388
|
-
...
|
|
474
|
+
...options?.customFormats,
|
|
389
475
|
};
|
|
390
476
|
}
|
|
391
477
|
|
|
478
|
+
export function resolveFormatValidator(name: string, options?: FormatValidatorsOptions): FormatValidatorFn | undefined {
|
|
479
|
+
const custom = options?.customFormats;
|
|
480
|
+
if (custom && Object.hasOwn(custom, name)) {
|
|
481
|
+
return custom[name] as FormatValidatorFn | undefined;
|
|
482
|
+
}
|
|
483
|
+
if (Object.hasOwn(customValidators, name)) {
|
|
484
|
+
return customValidators[name];
|
|
485
|
+
}
|
|
486
|
+
if (options?.strictUris && name === 'uri') {
|
|
487
|
+
return strictUriValidator;
|
|
488
|
+
}
|
|
489
|
+
return inbuiltValidators[name];
|
|
490
|
+
}
|
|
491
|
+
|
|
392
492
|
export function registerFormat(name: string, validatorFunction: FormatValidatorFn) {
|
|
393
493
|
customValidators[name] = validatorFunction;
|
|
394
494
|
}
|
|
@@ -411,10 +511,16 @@ export function isFormatSupported(name: string, customFormats?: Record<string, F
|
|
|
411
511
|
if (customFormats) {
|
|
412
512
|
const custom = customFormats[name];
|
|
413
513
|
// Explicitly null means unregistered at instance level
|
|
414
|
-
if (custom === null)
|
|
415
|
-
|
|
514
|
+
if (custom === null) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
if (custom != null) {
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (name in customValidators) {
|
|
522
|
+
return customValidators[name] != null;
|
|
416
523
|
}
|
|
417
|
-
if (name in customValidators) return customValidators[name] != null;
|
|
418
524
|
return name in inbuiltValidators;
|
|
419
525
|
}
|
|
420
526
|
|
|
@@ -75,11 +75,65 @@ export interface JsonSchemaAll extends JsonSchemaCommon {
|
|
|
75
75
|
// Internal types (schema + z-schema runtime properties)
|
|
76
76
|
// ---------------------------------------------------------------------------
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Sub-schema-valued keywords. On the internal type these point at
|
|
80
|
+
* `JsonSchemaInternal` (rather than the public `JsonSchema`) so the validator
|
|
81
|
+
* can traverse nested schemas without asserting `as JsonSchemaInternal` at
|
|
82
|
+
* every step.
|
|
83
|
+
*/
|
|
84
|
+
type InternalSchemaKeys =
|
|
85
|
+
| 'items'
|
|
86
|
+
| 'additionalItems'
|
|
87
|
+
| 'properties'
|
|
88
|
+
| 'patternProperties'
|
|
89
|
+
| 'additionalProperties'
|
|
90
|
+
| 'dependencies'
|
|
91
|
+
| 'allOf'
|
|
92
|
+
| 'anyOf'
|
|
93
|
+
| 'oneOf'
|
|
94
|
+
| 'not'
|
|
95
|
+
| 'definitions'
|
|
96
|
+
| 'contains'
|
|
97
|
+
| 'propertyNames'
|
|
98
|
+
| 'if'
|
|
99
|
+
| 'then'
|
|
100
|
+
| 'else'
|
|
101
|
+
| '$defs'
|
|
102
|
+
| 'dependentSchemas'
|
|
103
|
+
| 'unevaluatedItems'
|
|
104
|
+
| 'unevaluatedProperties'
|
|
105
|
+
| 'prefixItems';
|
|
106
|
+
|
|
78
107
|
/**
|
|
79
108
|
* Internal schema type used throughout the validator. Based on `JsonSchemaAll`
|
|
80
|
-
* so that validators can access any draft-specific property without narrowing
|
|
109
|
+
* so that validators can access any draft-specific property without narrowing,
|
|
110
|
+
* but with every sub-schema keyword re-typed to `JsonSchemaInternal` so nested
|
|
111
|
+
* traversal needs no type assertions.
|
|
81
112
|
*/
|
|
82
|
-
export type JsonSchemaInternal = JsonSchemaAll &
|
|
113
|
+
export type JsonSchemaInternal = Omit<JsonSchemaAll, InternalSchemaKeys> &
|
|
114
|
+
ZSchemaInternalProperties & {
|
|
115
|
+
items?: JsonSchemaInternal | boolean | Array<JsonSchemaInternal | boolean>;
|
|
116
|
+
additionalItems?: boolean | JsonSchemaInternal;
|
|
117
|
+
properties?: Record<string, JsonSchemaInternal | boolean>;
|
|
118
|
+
patternProperties?: Record<string, JsonSchemaInternal>;
|
|
119
|
+
additionalProperties?: boolean | JsonSchemaInternal;
|
|
120
|
+
dependencies?: Record<string, string[] | JsonSchemaInternal>;
|
|
121
|
+
allOf?: JsonSchemaInternal[];
|
|
122
|
+
anyOf?: JsonSchemaInternal[];
|
|
123
|
+
oneOf?: JsonSchemaInternal[];
|
|
124
|
+
not?: JsonSchemaInternal;
|
|
125
|
+
definitions?: Record<string, JsonSchemaInternal>;
|
|
126
|
+
contains?: JsonSchemaInternal;
|
|
127
|
+
propertyNames?: JsonSchemaInternal;
|
|
128
|
+
if?: JsonSchemaInternal | boolean;
|
|
129
|
+
then?: JsonSchemaInternal | boolean;
|
|
130
|
+
else?: JsonSchemaInternal | boolean;
|
|
131
|
+
$defs?: Record<string, JsonSchemaInternal>;
|
|
132
|
+
dependentSchemas?: Record<string, JsonSchemaInternal>;
|
|
133
|
+
unevaluatedItems?: JsonSchemaInternal | boolean;
|
|
134
|
+
unevaluatedProperties?: JsonSchemaInternal | boolean;
|
|
135
|
+
prefixItems?: Array<JsonSchemaInternal | boolean>;
|
|
136
|
+
};
|
|
83
137
|
|
|
84
138
|
/** @deprecated Use `JsonSchemaInternal` — they are now equivalent. */
|
|
85
139
|
export type JsonSchemaInternalAll = JsonSchemaAll & ZSchemaInternalProperties;
|
package/src/json-schema.ts
CHANGED
|
@@ -2,18 +2,19 @@ import type { JsonSchema, JsonSchemaInternal } from './json-schema-versions.js';
|
|
|
2
2
|
import type { Reference } from './schema-compiler.js';
|
|
3
3
|
import type { ZSchemaOptions } from './z-schema-options.js';
|
|
4
4
|
|
|
5
|
+
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
5
6
|
import { getRemotePath, isAbsoluteUri } from './utils/uri.js';
|
|
7
|
+
import { isObject } from './utils/what-is.js';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Keywords whose values are not JSON Schema sub-schemas and must not be
|
|
9
11
|
* traversed during schema walking (id collection, reference collection, etc.).
|
|
10
12
|
*/
|
|
11
13
|
export const NON_SCHEMA_KEYWORDS = ['enum', 'const', 'default', 'examples'] as const;
|
|
14
|
+
export const NON_SCHEMA_KEYWORDS_SET = new Set<string>(NON_SCHEMA_KEYWORDS);
|
|
12
15
|
|
|
13
16
|
/** Returns true if the key is an internal z-schema property (prefixed with `__$`). */
|
|
14
17
|
export const isInternalKey = (key: string): boolean => key.startsWith('__$');
|
|
15
|
-
import { DEFAULT_MAX_RECURSION_DEPTH } from './utils/constants.js';
|
|
16
|
-
import { isObject } from './utils/what-is.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Properties present in ALL JSON Schema drafts (04 through 2020-12) with
|
|
@@ -30,7 +31,7 @@ export interface JsonSchemaCommon {
|
|
|
30
31
|
|
|
31
32
|
// ── Type / enum ─────────────────────────────────────────────────────────
|
|
32
33
|
type?: string | string[];
|
|
33
|
-
enum?:
|
|
34
|
+
enum?: unknown[];
|
|
34
35
|
format?: string;
|
|
35
36
|
|
|
36
37
|
// ── Numeric ─────────────────────────────────────────────────────────────
|
|
@@ -78,9 +79,9 @@ export type JsonSchemaType = 'array' | 'boolean' | 'integer' | 'null' | 'number'
|
|
|
78
79
|
export interface ZSchemaInternalProperties {
|
|
79
80
|
__$compiled?: unknown;
|
|
80
81
|
__$missingReferences?: Reference[];
|
|
81
|
-
__$refResolved?:
|
|
82
|
-
__$dynamicRefResolved?:
|
|
83
|
-
__$recursiveRefResolved?:
|
|
82
|
+
__$refResolved?: JsonSchemaInternal;
|
|
83
|
+
__$dynamicRefResolved?: JsonSchemaInternal;
|
|
84
|
+
__$recursiveRefResolved?: JsonSchemaInternal;
|
|
84
85
|
__$resourceRoot?: JsonSchemaInternal;
|
|
85
86
|
__$schemaResolved?: unknown;
|
|
86
87
|
__$validated?: boolean;
|
|
@@ -142,7 +143,7 @@ export const findId = (
|
|
|
142
143
|
const inTargetBase = !targetBaseUri || nextBaseUri === targetBaseUri;
|
|
143
144
|
|
|
144
145
|
if (inTargetBase) {
|
|
145
|
-
if (schemaId && (schemaId === id || (schemaId
|
|
146
|
+
if (schemaId && (schemaId === id || (schemaId.startsWith('#') && schemaId.slice(1) === id))) {
|
|
146
147
|
return schema;
|
|
147
148
|
}
|
|
148
149
|
if (schema.$anchor === id || schema.$dynamicAnchor === id) {
|
|
@@ -153,7 +154,7 @@ export const findId = (
|
|
|
153
154
|
let result;
|
|
154
155
|
if (Array.isArray(schema)) {
|
|
155
156
|
for (let i = 0; i < schema.length; i++) {
|
|
156
|
-
result = findId(schema[i], id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
|
|
157
|
+
result = findId(schema[i] as JsonSchemaInternal, id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
|
|
157
158
|
if (result) {
|
|
158
159
|
return result;
|
|
159
160
|
}
|
|
@@ -167,7 +168,7 @@ export const findId = (
|
|
|
167
168
|
// intermediate resources - pointer reference across resource boundary".
|
|
168
169
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
169
170
|
const k = keys[i];
|
|
170
|
-
if (isInternalKey(k) ||
|
|
171
|
+
if (isInternalKey(k) || NON_SCHEMA_KEYWORDS_SET.has(k)) {
|
|
171
172
|
continue;
|
|
172
173
|
}
|
|
173
174
|
result = findId(schema[k] as JsonSchemaInternal, id, targetBaseUri, nextBaseUri, maxDepth, _depth + 1);
|