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.
Files changed (111) hide show
  1. package/README.md +2 -2
  2. package/bin/z-schema +1 -1
  3. package/cjs/{index.js → index.cjs} +838 -707
  4. package/cjs/{index.d.ts → index.d.cts} +47 -26
  5. package/dist/{errors.d.mts → errors.d.ts} +2 -2
  6. package/dist/{errors.mjs → errors.js} +1 -2
  7. package/dist/{format-validators.mjs → format-validators.js} +60 -43
  8. package/dist/{index.d.mts → index.d.ts} +9 -9
  9. package/dist/{index.mjs → index.js} +3 -3
  10. package/dist/{json-schema-versions.d.mts → json-schema-versions.d.ts} +34 -3
  11. package/dist/{json-schema.d.mts → json-schema.d.ts} +7 -7
  12. package/dist/{json-schema.mjs → json-schema.js} +6 -5
  13. package/dist/{json-validation.mjs → json-validation.js} +144 -127
  14. package/dist/{report.d.mts → report.d.ts} +8 -9
  15. package/dist/{report.mjs → report.js} +28 -31
  16. package/dist/{schema-cache.d.mts → schema-cache.d.ts} +5 -5
  17. package/dist/{schema-cache.mjs → schema-cache.js} +10 -11
  18. package/dist/{schema-compiler.d.mts → schema-compiler.d.ts} +4 -4
  19. package/dist/{schema-compiler.mjs → schema-compiler.js} +101 -79
  20. package/dist/{schema-validator.d.mts → schema-validator.d.ts} +5 -5
  21. package/dist/{schema-validator.mjs → schema-validator.js} +138 -166
  22. package/dist/utils/{array.mjs → array.js} +4 -3
  23. package/dist/utils/{base64.mjs → base64.js} +3 -2
  24. package/dist/utils/{clone.mjs → clone.js} +18 -20
  25. package/dist/utils/hostname.js +176 -0
  26. package/dist/utils/{json.mjs → json.js} +13 -8
  27. package/dist/utils/{schema-regex.mjs → schema-regex.js} +5 -5
  28. package/dist/utils/{time.mjs → time.js} +8 -8
  29. package/dist/utils/unicode.js +22 -0
  30. package/dist/utils/{what-is.mjs → what-is.js} +1 -2
  31. package/dist/validation/{array.mjs → array.js} +18 -20
  32. package/dist/validation/{combinators.mjs → combinators.js} +16 -16
  33. package/dist/validation/{numeric.mjs → numeric.js} +11 -11
  34. package/dist/validation/{object.mjs → object.js} +35 -34
  35. package/dist/validation/{ref.mjs → ref.js} +5 -5
  36. package/dist/validation/{shared.mjs → shared.js} +15 -12
  37. package/dist/validation/{string.mjs → string.js} +35 -35
  38. package/dist/validation/type.js +34 -0
  39. package/dist/{z-schema-base.d.mts → z-schema-base.d.ts} +11 -12
  40. package/dist/{z-schema-base.mjs → z-schema-base.js} +45 -40
  41. package/dist/{z-schema-options.d.mts → z-schema-options.d.ts} +3 -3
  42. package/dist/{z-schema-options.mjs → z-schema-options.js} +5 -5
  43. package/dist/{z-schema-reader.d.mts → z-schema-reader.d.ts} +1 -1
  44. package/dist/{z-schema-versions.mjs → z-schema-versions.js} +21 -21
  45. package/dist/{z-schema.d.mts → z-schema.d.ts} +5 -13
  46. package/dist/{z-schema.mjs → z-schema.js} +38 -48
  47. package/package.json +24 -25
  48. package/src/errors.ts +1 -2
  49. package/src/format-validators.ts +173 -67
  50. package/src/json-schema-versions.ts +56 -2
  51. package/src/json-schema.ts +10 -9
  52. package/src/json-validation.ts +189 -146
  53. package/src/report.ts +37 -49
  54. package/src/schema-cache.ts +13 -13
  55. package/src/schema-compiler.ts +170 -117
  56. package/src/schema-validator.ts +239 -238
  57. package/src/utils/array.ts +9 -6
  58. package/src/utils/base64.ts +13 -2
  59. package/src/utils/clone.ts +28 -30
  60. package/src/utils/date.ts +6 -3
  61. package/src/utils/hostname.ts +257 -31
  62. package/src/utils/json.ts +16 -9
  63. package/src/utils/properties.ts +2 -2
  64. package/src/utils/schema-regex.ts +4 -4
  65. package/src/utils/time.ts +8 -7
  66. package/src/utils/unicode.ts +12 -5
  67. package/src/utils/what-is.ts +1 -5
  68. package/src/validation/array.ts +24 -22
  69. package/src/validation/combinators.ts +14 -14
  70. package/src/validation/numeric.ts +14 -28
  71. package/src/validation/object.ts +32 -36
  72. package/src/validation/ref.ts +5 -6
  73. package/src/validation/shared.ts +22 -21
  74. package/src/validation/string.ts +36 -42
  75. package/src/validation/type.ts +17 -17
  76. package/src/z-schema-base.ts +49 -38
  77. package/src/z-schema-options.ts +4 -3
  78. package/src/z-schema.ts +35 -45
  79. package/umd/ZSchema.js +853 -715
  80. package/umd/ZSchema.min.js +2 -2
  81. package/umd/package.json +3 -0
  82. package/dist/utils/hostname.mjs +0 -75
  83. package/dist/utils/unicode.mjs +0 -12
  84. package/dist/validation/type.mjs +0 -32
  85. /package/dist/{format-validators.d.mts → format-validators.d.ts} +0 -0
  86. /package/dist/{json-schema-versions.mjs → json-schema-versions.js} +0 -0
  87. /package/dist/schemas/{draft-04-schema.mjs → draft-04-schema.js} +0 -0
  88. /package/dist/schemas/{draft-06-schema.mjs → draft-06-schema.js} +0 -0
  89. /package/dist/schemas/{draft-07-schema.mjs → draft-07-schema.js} +0 -0
  90. /package/dist/schemas/{draft-2019-09-meta-applicator.mjs → draft-2019-09-meta-applicator.js} +0 -0
  91. /package/dist/schemas/{draft-2019-09-meta-content.mjs → draft-2019-09-meta-content.js} +0 -0
  92. /package/dist/schemas/{draft-2019-09-meta-core.mjs → draft-2019-09-meta-core.js} +0 -0
  93. /package/dist/schemas/{draft-2019-09-meta-format.mjs → draft-2019-09-meta-format.js} +0 -0
  94. /package/dist/schemas/{draft-2019-09-meta-meta-data.mjs → draft-2019-09-meta-meta-data.js} +0 -0
  95. /package/dist/schemas/{draft-2019-09-meta-validation.mjs → draft-2019-09-meta-validation.js} +0 -0
  96. /package/dist/schemas/{draft-2019-09-schema.mjs → draft-2019-09-schema.js} +0 -0
  97. /package/dist/schemas/{draft-2020-12-meta-applicator.mjs → draft-2020-12-meta-applicator.js} +0 -0
  98. /package/dist/schemas/{draft-2020-12-meta-content.mjs → draft-2020-12-meta-content.js} +0 -0
  99. /package/dist/schemas/{draft-2020-12-meta-core.mjs → draft-2020-12-meta-core.js} +0 -0
  100. /package/dist/schemas/{draft-2020-12-meta-format-annotation.mjs → draft-2020-12-meta-format-annotation.js} +0 -0
  101. /package/dist/schemas/{draft-2020-12-meta-format-assertion.mjs → draft-2020-12-meta-format-assertion.js} +0 -0
  102. /package/dist/schemas/{draft-2020-12-meta-meta-data.mjs → draft-2020-12-meta-meta-data.js} +0 -0
  103. /package/dist/schemas/{draft-2020-12-meta-unevaluated.mjs → draft-2020-12-meta-unevaluated.js} +0 -0
  104. /package/dist/schemas/{draft-2020-12-meta-validation.mjs → draft-2020-12-meta-validation.js} +0 -0
  105. /package/dist/schemas/{draft-2020-12-schema.mjs → draft-2020-12-schema.js} +0 -0
  106. /package/dist/utils/{constants.mjs → constants.js} +0 -0
  107. /package/dist/utils/{date.mjs → date.js} +0 -0
  108. /package/dist/utils/{properties.mjs → properties.js} +0 -0
  109. /package/dist/utils/{symbols.mjs → symbols.js} +0 -0
  110. /package/dist/utils/{uri.mjs → uri.js} +0 -0
  111. /package/dist/{z-schema-reader.mjs → z-schema-reader.js} +0 -0
@@ -7,9 +7,11 @@ import { areEqual } from './json.js';
7
7
  * Falls back to pairwise deep comparison (O(n²)) when the array contains
8
8
  * objects or arrays that need structural equality checks.
9
9
  */
10
- export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number): boolean => {
10
+ export const isUniqueArray = (arr: unknown[], indexes?: number[], maxDepth?: number): boolean => {
11
11
  const l = arr.length;
12
- if (l <= 1) return true;
12
+ if (l <= 1) {
13
+ return true;
14
+ }
13
15
 
14
16
  // Fast path: if every element is a primitive, use a Set.
15
17
  // We distinguish types so that e.g. 1 !== '1' and 0 !== false.
@@ -27,7 +29,7 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
27
29
  const seen = new Set<string>();
28
30
  for (let i = 0; i < l; i++) {
29
31
  const v = arr[i];
30
- const key = typeof v + ':' + String(v);
32
+ const key = `${typeof v}:${String(v)}`;
31
33
  if (seen.has(key)) {
32
34
  // Find the first occurrence for the indexes report.
33
35
  if (indexes) {
@@ -47,9 +49,10 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
47
49
  }
48
50
 
49
51
  // Slow path: at least one element is an object/array — need deep comparison.
52
+ const eqOpts = { maxDepth };
50
53
  for (let i = 0; i < l; i++) {
51
54
  for (let j = i + 1; j < l; j++) {
52
- if (areEqual(arr[i], arr[j], { maxDepth })) {
55
+ if (areEqual(arr[i], arr[j], eqOpts)) {
53
56
  if (indexes) {
54
57
  indexes.push(i, j);
55
58
  }
@@ -60,9 +63,9 @@ export const isUniqueArray = <T>(arr: T[], indexes?: number[], maxDepth?: number
60
63
  return true;
61
64
  };
62
65
 
63
- export const difference = (bigSet: any[], subSet: any[]) => {
66
+ export const difference = <T>(bigSet: readonly T[], subSet: readonly T[]): T[] => {
64
67
  const exclusions = new Set(subSet);
65
- const arr = [];
68
+ const arr: T[] = [];
66
69
  let idx = bigSet.length;
67
70
  while (idx--) {
68
71
  if (!exclusions.has(bigSet[idx])) {
@@ -20,9 +20,20 @@ export const decodeBase64 = (value: string): string | undefined => {
20
20
  }
21
21
  }
22
22
 
23
- if (typeof Buffer !== 'undefined') {
23
+ // Node fallback (reached only when `atob` is unavailable, i.e. not a browser).
24
+ // Read `Buffer` off `globalThis` rather than the bare global so it is typed
25
+ // without an untyped global reference; on Node >=22 `Buffer` is always present
26
+ // on `globalThis`. (A bundler polyfill injected as a module-scoped variable but
27
+ // not onto `globalThis` would be missed, but that path is unreachable in browsers
28
+ // where `atob` above is used instead.)
29
+ const bufferCtor = (
30
+ globalThis as {
31
+ Buffer?: { from: (data: string, encoding: string) => { toString: (encoding: string) => string } };
32
+ }
33
+ ).Buffer;
34
+ if (bufferCtor !== undefined) {
24
35
  try {
25
- return Buffer.from(value, 'base64').toString('utf8');
36
+ return bufferCtor.from(value, 'base64').toString('utf-8');
26
37
  } catch {
27
38
  return undefined;
28
39
  }
@@ -5,29 +5,28 @@ export const shallowClone = <T>(src: T): T => {
5
5
  if (src == null || typeof src !== 'object') {
6
6
  return src;
7
7
  }
8
- let res: any;
9
8
  if (Array.isArray(src)) {
10
- res = [];
9
+ const res: unknown[] = [];
11
10
  for (let i = 0; i < src.length; i++) {
12
11
  res[i] = src[i];
13
12
  }
14
- } else {
15
- res = {};
16
- const keys = Object.keys(src).sort() as Array<keyof T>;
17
- for (const key of keys) {
18
- copyProp(src, res, key);
19
- }
13
+ return res as T;
14
+ }
15
+ const res: Record<string, unknown> = {};
16
+ const keys = Object.keys(src).sort() as Array<keyof T>;
17
+ for (let i = 0; i < keys.length; i++) {
18
+ copyProp(src, res, keys[i]);
20
19
  }
21
- return res;
20
+ return res as T;
22
21
  };
23
22
 
24
23
  export const deepClone = <T>(src: T, maxDepth = DEFAULT_MAX_RECURSION_DEPTH): T => {
25
24
  let vidx = 0;
26
- const visited = new Map();
27
- const cloned: any[] = [];
28
- const cloneDeepInner = <T>(src: T, _depth: number): T => {
29
- if (typeof src !== 'object' || src === null) {
30
- return src;
25
+ const visited = new Map<unknown, number>();
26
+ const cloned: unknown[] = [];
27
+ const cloneDeepInner = <U>(node: U, _depth: number): U => {
28
+ if (typeof node !== 'object' || node === null) {
29
+ return node;
31
30
  }
32
31
 
33
32
  if (_depth >= maxDepth) {
@@ -37,29 +36,28 @@ export const deepClone = <T>(src: T, maxDepth = DEFAULT_MAX_RECURSION_DEPTH): T
37
36
  );
38
37
  }
39
38
 
40
- let res: any;
41
- const cidx = visited.get(src);
39
+ const cidx = visited.get(node);
42
40
 
43
41
  if (cidx !== undefined) {
44
- return cloned[cidx];
42
+ return cloned[cidx] as U;
45
43
  }
46
44
 
47
- visited.set(src, vidx++);
48
- if (Array.isArray(src)) {
49
- res = [];
45
+ visited.set(node, vidx++);
46
+ if (Array.isArray(node)) {
47
+ const res: unknown[] = [];
50
48
  cloned.push(res);
51
- for (let i = 0; i < src.length; i++) {
52
- res[i] = cloneDeepInner(src[i], _depth + 1);
53
- }
54
- } else {
55
- res = {};
56
- cloned.push(res);
57
- const keys = Object.keys(src).sort() as Array<keyof T>;
58
- for (const key of keys) {
59
- copyProp(src, res, key, (v: any) => cloneDeepInner(v, _depth + 1));
49
+ for (let i = 0; i < node.length; i++) {
50
+ res[i] = cloneDeepInner(node[i], _depth + 1);
60
51
  }
52
+ return res as U;
53
+ }
54
+ const res: Record<string, unknown> = {};
55
+ cloned.push(res);
56
+ const keys = Object.keys(node).sort() as Array<keyof U>;
57
+ for (let i = 0; i < keys.length; i++) {
58
+ copyProp(node, res, keys[i], (v: unknown) => cloneDeepInner(v, _depth + 1));
61
59
  }
62
- return res;
60
+ return res as U;
63
61
  };
64
62
  return cloneDeepInner(src, 0);
65
63
  };
package/src/utils/date.ts CHANGED
@@ -2,15 +2,18 @@ const isLeapYear = (year: number): boolean => year % 4 === 0 && (year % 100 !==
2
2
 
3
3
  const getDaysInMonth = (year: number, month: number): number => {
4
4
  switch (month) {
5
- case 2:
5
+ case 2: {
6
6
  return isLeapYear(year) ? 29 : 28;
7
+ }
7
8
  case 4:
8
9
  case 6:
9
10
  case 9:
10
- case 11:
11
+ case 11: {
11
12
  return 30;
12
- default:
13
+ }
14
+ default: {
13
15
  return 31;
16
+ }
14
17
  }
15
18
  };
16
19
 
@@ -1,8 +1,188 @@
1
1
  import punycode from 'punycode/punycode.js';
2
2
  import isIPModule from 'validator/lib/isIP.js';
3
3
 
4
- const IDN_SEPARATOR_REGEX = /[\u3002\uff0e\uff61]/g;
5
- const IDN_SEPARATOR_TEST_REGEX = /[\u3002\uff0e\uff61]/;
4
+ const IDN_SEPARATOR_REGEX = /[\u3002\uFF0E\uFF61]/g;
5
+ const IDN_SEPARATOR_TEST_REGEX = /[\u3002\uFF0E\uFF61]/;
6
+
7
+ // IDNA2008 (RFC 5890-5893) support below is a hand-rolled approximation built on
8
+ // General_Category / Script Unicode-property regexes (JS regex has no
9
+ // \p{Bidi_Class} or \p{Joining_Type}). It covers the JSON-Schema-Test-Suite
10
+ // corpus, not the full IDNA table. Known limitations: joining-context (CONTEXTJ)
11
+ // only models Arabic script; the ZWJ/ZWNJ Virama context recognizes only the
12
+ // Devanagari virama U+094D (not other scripts' viramas); Bidi class detection
13
+ // covers L/R/AL/AN/EN/NSM and treats everything else as ON; and U-labels are not
14
+ // required to be in Unicode NFC form. All regexes/Sets are hoisted to module scope
15
+ // so they are compiled once (format validation runs on hot paths).
16
+
17
+ // Matches an A-label (ACE) prefix; hoisted so it is compiled once, not per label.
18
+ const XN_LABEL_REGEX = /^xn--/i;
19
+
20
+ // RFC 5892 section 2.6 lists a handful of code points that are PVALID (or governed
21
+ // by a contextual rule) despite being Punctuation/Symbol. A blanket "reject
22
+ // P/S/Z/C" DISALLOWED heuristic must whitelist exactly these so valid labels are
23
+ // not rejected. Letters (e.g. U+00DF sharp s, U+03C2 final sigma) and Numbers
24
+ // (e.g. U+3007) are unaffected by the heuristic and need no entry here.
25
+ const DISALLOWED_CATEGORY_REGEX = /[\p{P}\p{S}\p{Z}\p{C}]/u;
26
+ const DISALLOWED_CATEGORY_WHITELIST: ReadonlySet<string> = new Set([
27
+ '-', // HYPHEN-MINUS (medial; leading/trailing rejected separately)
28
+ '\u00B7', // MIDDLE DOT (CONTEXTO)
29
+ '\u0375', // GREEK LOWER NUMERAL SIGN / KERAIA (CONTEXTO)
30
+ '\u05F3', // HEBREW PUNCTUATION GERESH (CONTEXTO)
31
+ '\u05F4', // HEBREW PUNCTUATION GERSHAYIM (CONTEXTO)
32
+ '\u30FB', // KATAKANA MIDDLE DOT (CONTEXTO)
33
+ '\u06FD', // ARABIC SIGN SINDHI AMPERSAND (RFC 5892 section 2.6 PVALID)
34
+ '\u06FE', // ARABIC SIGN SINDHI POSTPOSITION MEN (RFC 5892 section 2.6 PVALID)
35
+ '\u0F0B', // TIBETAN MARK INTERSYLLABIC TSHEG (RFC 5892 section 2.6 PVALID)
36
+ '\u200C', // ZERO WIDTH NON-JOINER (CONTEXTJ, checked separately)
37
+ '\u200D', // ZERO WIDTH JOINER (CONTEXTJ, checked separately)
38
+ ]);
39
+
40
+ const isDisallowedCodePoint = (char: string): boolean =>
41
+ DISALLOWED_CATEGORY_REGEX.test(char) && !DISALLOWED_CATEGORY_WHITELIST.has(char);
42
+
43
+ // CONTEXTJ (RFC 5892 Appendix A.1) support for ZERO WIDTH NON-JOINER.
44
+ const TRANSPARENT_MARK_REGEX = /\p{Mn}/u;
45
+ const ARABIC_SCRIPT_REGEX = /\p{Script=Arabic}/u;
46
+ const ARABIC_INDIC_DIGITS_REGEX = /[\u0660-\u0669\u06F0-\u06F9]/;
47
+
48
+ // Approximates Joining_Type {L, D, R} as "an Arabic-script letter" (excluding the
49
+ // Arabic-Indic digit ranges, which are not letters).
50
+ const isArabicJoiningLetter = (char: string | undefined): boolean =>
51
+ char !== undefined && ARABIC_SCRIPT_REGEX.test(char) && !ARABIC_INDIC_DIGITS_REGEX.test(char);
52
+
53
+ // ZWNJ at `idx` is valid if it sits between two joining letters, skipping over any
54
+ // Transparent (combining-mark) code points on either side.
55
+ const hasZwnjJoiningContext = (label: string, idx: number): boolean => {
56
+ let before = idx - 1;
57
+ while (before >= 0 && TRANSPARENT_MARK_REGEX.test(label[before])) {
58
+ before--;
59
+ }
60
+ let after = idx + 1;
61
+ while (after < label.length && TRANSPARENT_MARK_REGEX.test(label[after])) {
62
+ after++;
63
+ }
64
+ return isArabicJoiningLetter(label[before]) && isArabicJoiningLetter(label[after]);
65
+ };
66
+
67
+ // RFC 5893 Bidi rule. Bidi_Class is approximated from Script / General_Category.
68
+ type BidiClass = 'AL' | 'AN' | 'EN' | 'L' | 'NSM' | 'ON' | 'R';
69
+
70
+ const BIDI_NSM_REGEX = /[\p{Mn}\p{Me}]/u;
71
+ const BIDI_ARABIC_INDIC_DIGIT_REGEX = /[\u0660-\u0669]/; // Bidi_Class AN
72
+ const BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX = /[\u06F0-\u06F9]/; // Bidi_Class EN (NOT AN)
73
+ const BIDI_HEBREW_SCRIPT_REGEX = /\p{Script=Hebrew}/u; // Bidi_Class R
74
+ const BIDI_AL_SCRIPT_REGEX = /[\p{Script=Arabic}\p{Script=Syriac}\p{Script=Thaana}]/u; // Bidi_Class AL
75
+ const BIDI_ASCII_DIGIT_REGEX = /[0-9]/; // Bidi_Class EN
76
+ const BIDI_LETTER_REGEX = /\p{L}/u;
77
+
78
+ const classifyBidi = (char: string): BidiClass => {
79
+ if (BIDI_NSM_REGEX.test(char)) {
80
+ return 'NSM';
81
+ }
82
+ if (BIDI_ARABIC_INDIC_DIGIT_REGEX.test(char)) {
83
+ return 'AN';
84
+ }
85
+ if (BIDI_EXTENDED_ARABIC_INDIC_DIGIT_REGEX.test(char)) {
86
+ return 'EN';
87
+ }
88
+ if (BIDI_HEBREW_SCRIPT_REGEX.test(char)) {
89
+ return 'R';
90
+ }
91
+ if (BIDI_AL_SCRIPT_REGEX.test(char)) {
92
+ return 'AL';
93
+ }
94
+ if (BIDI_ASCII_DIGIT_REGEX.test(char)) {
95
+ return 'EN';
96
+ }
97
+ if (BIDI_LETTER_REGEX.test(char)) {
98
+ return 'L';
99
+ }
100
+ return 'ON';
101
+ };
102
+
103
+ // A domain is "Bidi" (and thus subject to the Bidi rule) if any label contains an
104
+ // R, AL, or AN character. Iterated by code point so surrogate pairs stay intact.
105
+ const isLabelBidiTriggering = (label: string): boolean => {
106
+ for (const char of label) {
107
+ const bidiClass = classifyBidi(char);
108
+ if (bidiClass === 'R' || bidiClass === 'AL' || bidiClass === 'AN') {
109
+ return true;
110
+ }
111
+ }
112
+ return false;
113
+ };
114
+
115
+ const isLabelBidiRuleValid = (label: string): boolean => {
116
+ const classes: BidiClass[] = [];
117
+ for (const char of label) {
118
+ classes.push(classifyBidi(char));
119
+ }
120
+ if (classes.length === 0) {
121
+ return false;
122
+ }
123
+
124
+ const first = classes[0];
125
+ // Rule 1: the first character must be L, R, or AL.
126
+ if (first !== 'L' && first !== 'R' && first !== 'AL') {
127
+ return false;
128
+ }
129
+
130
+ // The "last" character check ignores trailing NSM marks.
131
+ let lastIdx = classes.length - 1;
132
+ while (lastIdx > 0 && classes[lastIdx] === 'NSM') {
133
+ lastIdx--;
134
+ }
135
+ const last = classes[lastIdx];
136
+
137
+ if (first === 'R' || first === 'AL') {
138
+ // Rule 2: RTL label.
139
+ let hasEN = false;
140
+ let hasAN = false;
141
+ for (let i = 0; i < classes.length; i++) {
142
+ const c = classes[i];
143
+ if (c !== 'R' && c !== 'AL' && c !== 'AN' && c !== 'EN' && c !== 'NSM' && c !== 'ON') {
144
+ return false;
145
+ }
146
+ if (c === 'EN') {
147
+ hasEN = true;
148
+ } else if (c === 'AN') {
149
+ hasAN = true;
150
+ }
151
+ }
152
+ if (hasEN && hasAN) {
153
+ return false;
154
+ }
155
+ return last === 'R' || last === 'AL' || last === 'EN' || last === 'AN';
156
+ }
157
+
158
+ // Rule 3: LTR label.
159
+ for (let i = 0; i < classes.length; i++) {
160
+ const c = classes[i];
161
+ if (c !== 'L' && c !== 'EN' && c !== 'NSM' && c !== 'ON') {
162
+ return false;
163
+ }
164
+ }
165
+ return last === 'L' || last === 'EN';
166
+ };
167
+
168
+ const isDomainBidiValid = (unicodeLabels: string[]): boolean => {
169
+ let isBidiDomain = false;
170
+ for (let i = 0; i < unicodeLabels.length; i++) {
171
+ if (isLabelBidiTriggering(unicodeLabels[i])) {
172
+ isBidiDomain = true;
173
+ break;
174
+ }
175
+ }
176
+ if (!isBidiDomain) {
177
+ return true;
178
+ }
179
+ for (let i = 0; i < unicodeLabels.length; i++) {
180
+ if (!isLabelBidiRuleValid(unicodeLabels[i])) {
181
+ return false;
182
+ }
183
+ }
184
+ return true;
185
+ };
6
186
 
7
187
  const splitHostnameLabels = (hostname: string): string[] | null => {
8
188
  if (hostname.length === 0 || hostname.length > 255) {
@@ -12,8 +192,11 @@ const splitHostnameLabels = (hostname: string): string[] | null => {
12
192
  return null;
13
193
  }
14
194
  const labels = hostname.split('.');
15
- if (labels.some((label) => label.length === 0 || label.length > 63)) {
16
- return null;
195
+ for (let i = 0; i < labels.length; i++) {
196
+ const len = labels[i].length;
197
+ if (len === 0 || len > 63) {
198
+ return null;
199
+ }
17
200
  }
18
201
  return labels;
19
202
  };
@@ -28,12 +211,12 @@ const hasCjkKanaOrHan = (input: string): boolean =>
28
211
  /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/u.test(input);
29
212
 
30
213
  const toUnicodeLabel = (label: string): string | null => {
31
- if (!/^xn--/i.test(label)) {
214
+ if (!XN_LABEL_REGEX.test(label)) {
32
215
  return label;
33
216
  }
34
217
  try {
35
218
  return punycode.toUnicode(label.toLowerCase());
36
- } catch (_e) {
219
+ } catch {
37
220
  return null;
38
221
  }
39
222
  };
@@ -43,7 +226,7 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
43
226
  return false;
44
227
  }
45
228
 
46
- if (label.length >= 4 && label[2] === '-' && label[3] === '-' && !/^xn--/i.test(label)) {
229
+ if (label.length >= 4 && label[2] === '-' && label[3] === '-' && !XN_LABEL_REGEX.test(label)) {
47
230
  return false;
48
231
  }
49
232
 
@@ -51,44 +234,54 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
51
234
  return false;
52
235
  }
53
236
 
54
- if (/[\u302e\u302f\u0640\u07fa]/u.test(label)) {
237
+ if (/[\u302E\u302F\u0640\u07FA]/u.test(label)) {
55
238
  return false;
56
239
  }
57
240
 
58
241
  for (let idx = 0; idx < label.length; idx++) {
59
242
  const char = label[idx];
60
243
 
61
- if (char === '\u00b7') {
62
- if (idx === 0 || idx === label.length - 1 || label[idx - 1] !== 'l' || label[idx + 1] !== 'l') {
63
- return false;
64
- }
244
+ if (
245
+ char === '\u00B7' &&
246
+ (idx === 0 || idx === label.length - 1 || label[idx - 1] !== 'l' || label[idx + 1] !== 'l')
247
+ ) {
248
+ return false;
65
249
  }
66
250
 
67
- if (char === '\u0375') {
68
- if (idx === label.length - 1 || !isGreek(label[idx + 1])) {
69
- return false;
70
- }
251
+ if (char === '\u0375' && (idx === label.length - 1 || !isGreek(label[idx + 1]))) {
252
+ return false;
71
253
  }
72
254
 
73
- if (char === '\u05f3' || char === '\u05f4') {
74
- if (idx === 0 || !isHebrew(label[idx - 1])) {
75
- return false;
76
- }
255
+ if ((char === '\u05F3' || char === '\u05F4') && (idx === 0 || !isHebrew(label[idx - 1]))) {
256
+ return false;
77
257
  }
78
258
 
79
- if (char === '\u200d') {
80
- if (idx === 0 || label[idx - 1] !== '\u094d') {
81
- return false;
82
- }
259
+ if (char === '\u200D' && (idx === 0 || label[idx - 1] !== '\u094D')) {
260
+ return false;
261
+ }
262
+
263
+ // ZERO WIDTH NON-JOINER (CONTEXTJ, RFC 5892 Appendix A.1): valid only when
264
+ // preceded by a Virama, or when it sits in a joining context.
265
+ if (char === '\u200C' && (idx === 0 || (label[idx - 1] !== '\u094D' && !hasZwnjJoiningContext(label, idx)))) {
266
+ return false;
267
+ }
268
+ }
269
+
270
+ // Reject IDNA2008-DISALLOWED code points (Punctuation/Symbol/Separator/Other,
271
+ // minus the RFC 5892 section 2.6 / contextual exceptions). Iterated by code point so a
272
+ // surrogate pair is not mistaken for a lone \p{Cs} half.
273
+ for (const char of label) {
274
+ if (isDisallowedCodePoint(char)) {
275
+ return false;
83
276
  }
84
277
  }
85
278
 
86
- if (label.includes('\u30fb') && !hasCjkKanaOrHan(label.replace(/\u30fb/g, ''))) {
279
+ if (label.includes('\u30FB') && !hasCjkKanaOrHan(label.replaceAll('・', ''))) {
87
280
  return false;
88
281
  }
89
282
 
90
283
  const hasArabicIndic = /[\u0660-\u0669]/.test(label);
91
- const hasExtendedArabicIndic = /[\u06f0-\u06f9]/.test(label);
284
+ const hasExtendedArabicIndic = /[\u06F0-\u06F9]/.test(label);
92
285
  if (hasArabicIndic && hasExtendedArabicIndic) {
93
286
  return false;
94
287
  }
@@ -129,7 +322,7 @@ const isValidIdnUnicodeLabel = (label: string): boolean => {
129
322
  */
130
323
  export const isValidHostname = (hostname: string): boolean => {
131
324
  // eslint-disable-next-line no-control-regex
132
- if (IDN_SEPARATOR_TEST_REGEX.test(hostname) || /[^\x00-\x7F]/.test(hostname)) {
325
+ if (IDN_SEPARATOR_TEST_REGEX.test(hostname) || /[^\u0000-\u007F]/.test(hostname)) {
133
326
  return false;
134
327
  }
135
328
 
@@ -142,13 +335,14 @@ export const isValidHostname = (hostname: string): boolean => {
142
335
  return false;
143
336
  }
144
337
 
145
- for (const label of labels) {
338
+ for (let i = 0; i < labels.length; i++) {
339
+ const label = labels[i];
146
340
  if (!isAsciiHostnameLabel(label)) {
147
341
  return false;
148
342
  }
149
343
 
150
344
  // Punycode (A-label) hostnames encode IDN labels, so validate the decoded Unicode form
151
- if (/^xn--/i.test(label)) {
345
+ if (XN_LABEL_REGEX.test(label)) {
152
346
  const unicodeLabel = toUnicodeLabel(label);
153
347
  if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) {
154
348
  return false;
@@ -166,12 +360,44 @@ export const isValidIdnHostname = (hostname: string): boolean => {
166
360
  return false;
167
361
  }
168
362
 
169
- for (const label of labels) {
363
+ const unicodeLabels: string[] = [];
364
+ let totalLength = labels.length - 1; // separator dots between labels
365
+
366
+ for (let i = 0; i < labels.length; i++) {
367
+ const label = labels[i];
170
368
  const unicodeLabel = toUnicodeLabel(label);
171
369
  if (unicodeLabel === null || !isValidIdnUnicodeLabel(unicodeLabel)) {
172
370
  return false;
173
371
  }
372
+
373
+ let aLabel: string;
374
+ try {
375
+ aLabel = punycode.toASCII(unicodeLabel);
376
+ } catch {
377
+ return false;
378
+ }
379
+
380
+ // An A-label (xn-- input) must be canonical Punycode: re-encoding its decoded
381
+ // U-label must reproduce the original A-label. This also rejects an A-label
382
+ // that decodes to pure ASCII, since such a label never re-encodes to an "xn--"
383
+ // form (RFC 5890 §2.3.2.1, RFC 5891 §5.4).
384
+ if (XN_LABEL_REGEX.test(label) && aLabel.toLowerCase() !== label.toLowerCase()) {
385
+ return false;
386
+ }
387
+
388
+ // RFC 5890 §2.3.2.1 — the A-label form of any label must not exceed 63 octets.
389
+ if (aLabel.length > 63) {
390
+ return false;
391
+ }
392
+
393
+ totalLength += aLabel.length;
394
+ unicodeLabels.push(unicodeLabel);
174
395
  }
175
396
 
176
- return true;
397
+ // RFC 1035 §3.1 — the domain name (in A-label form) must not exceed 253 octets.
398
+ if (totalLength > 253) {
399
+ return false;
400
+ }
401
+
402
+ return isDomainBidiValid(unicodeLabels);
177
403
  };
package/src/utils/json.ts CHANGED
@@ -6,6 +6,8 @@ interface AreEqualOptions {
6
6
  maxDepth?: number;
7
7
  }
8
8
 
9
+ export const sortedKeys = (obj: Record<string, unknown>): string[] => Object.keys(obj).sort();
10
+
9
11
  export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptions, _depth = 0): boolean => {
10
12
  const caseInsensitiveComparison = options?.caseInsensitiveComparison || false;
11
13
  const maxDepth = options?.maxDepth ?? DEFAULT_MAX_RECURSION_DEPTH;
@@ -21,7 +23,7 @@ export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptio
21
23
  return true;
22
24
  }
23
25
  if (
24
- caseInsensitiveComparison === true &&
26
+ caseInsensitiveComparison &&
25
27
  typeof json1 === 'string' &&
26
28
  typeof json2 === 'string' &&
27
29
  json1.toUpperCase() === json2.toUpperCase()
@@ -75,16 +77,21 @@ export const areEqual = (json1: unknown, json2: unknown, options?: AreEqualOptio
75
77
  return false;
76
78
  };
77
79
 
78
- export const decodeJSONPointer = (str: string) => {
79
- // http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07#section-3
80
- return decodeURIComponent(str).replace(/~[0-1]/g, (x) => (x === '~1' ? '/' : '~'));
81
- };
80
+ export const decodeJSONPointer = (str: string) =>
81
+ decodeURIComponent(str).replaceAll(/~[0-1]/g, (x) => (x === '~1' ? '/' : '~'));
82
82
 
83
- export const sortedKeys = (obj: Record<string, unknown>): string[] => Object.keys(obj).sort();
84
-
85
- export const get = (obj: any, path: string | Array<string | number>): any => {
83
+ export const get = (obj: unknown, path: string | Array<string | number>): unknown => {
86
84
  if (typeof path === 'string') {
87
85
  path = path.split('.');
88
86
  }
89
- return path.reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), obj);
87
+ let acc: unknown = obj;
88
+ for (let i = 0; i < path.length; i++) {
89
+ const key = path[i];
90
+ if (acc && (acc as Record<string | number, unknown>)[key] !== undefined) {
91
+ acc = (acc as Record<string | number, unknown>)[key];
92
+ } else {
93
+ return undefined;
94
+ }
95
+ }
96
+ return acc;
90
97
  };
@@ -1,7 +1,7 @@
1
- export function copyProp(from: object, to: object, key: string | number | symbol, fn?: <T>(obj: T) => T) {
1
+ export function copyProp(from: object, to: object, key: string | number | symbol, fn?: (value: unknown) => unknown) {
2
2
  if (Object.hasOwn(from, key)) {
3
3
  Object.defineProperty(to, key, {
4
- value: fn ? fn((from as any)[key]) : (from as any)[key],
4
+ value: fn ? fn((from as Record<PropertyKey, unknown>)[key]) : (from as Record<PropertyKey, unknown>)[key],
5
5
  enumerable: true,
6
6
  writable: true,
7
7
  configurable: true,
@@ -40,12 +40,12 @@ export function compileSchemaRegex(
40
40
  try {
41
41
  const re = new RegExp(pattern, 'u');
42
42
  return { ok: true, value: re };
43
- } catch (e: any) {
43
+ } catch (error: unknown) {
44
44
  return {
45
45
  ok: false,
46
46
  error: {
47
47
  pattern,
48
- message: e && e.message ? e.message : 'Invalid regular expression',
48
+ message: error instanceof Error ? error.message : 'Invalid regular expression',
49
49
  },
50
50
  };
51
51
  }
@@ -53,12 +53,12 @@ export function compileSchemaRegex(
53
53
  try {
54
54
  const re = new RegExp(pattern);
55
55
  return { ok: true, value: re };
56
- } catch (e: any) {
56
+ } catch (error: unknown) {
57
57
  return {
58
58
  ok: false,
59
59
  error: {
60
60
  pattern,
61
- message: e && e.message ? e.message : 'Invalid regular expression',
61
+ message: error instanceof Error ? error.message : 'Invalid regular expression',
62
62
  },
63
63
  };
64
64
  }