@webergency-utils/typechecker 0.2.1 → 0.2.3

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.
@@ -18,6 +18,140 @@ function isPlainObject(v) {
18
18
  const proto = Object.getPrototypeOf(v);
19
19
  return proto === Object.prototype || proto === null;
20
20
  }
21
+ function setOwnProperty(target, key, value) {
22
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
23
+ target[key] = value;
24
+ return;
25
+ }
26
+ Object.defineProperty(target, key, {
27
+ value,
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true
31
+ });
32
+ }
33
+ function assignOwnProperties(target, source) {
34
+ for (const key of Object.keys(source)) {
35
+ setOwnProperty(target, key, source[key]);
36
+ }
37
+ }
38
+ function keySetHas(keys, key) {
39
+ return keys instanceof Set ? keys.has(key) : keys.includes(key);
40
+ }
41
+ function commitContainer(target, source) {
42
+ if (target === source) {
43
+ return true;
44
+ }
45
+ if (Array.isArray(target) && Array.isArray(source)) {
46
+ target.length = source.length;
47
+ for (let i = 0; i < source.length; i++) {
48
+ target[i] = source[i];
49
+ }
50
+ return true;
51
+ }
52
+ if (target instanceof Set && source instanceof Set) {
53
+ target.clear();
54
+ for (const value of source) {
55
+ target.add(value);
56
+ }
57
+ return true;
58
+ }
59
+ if (target instanceof Map && source instanceof Map) {
60
+ target.clear();
61
+ for (const [key, value] of source) {
62
+ target.set(key, value);
63
+ }
64
+ return true;
65
+ }
66
+ if (isPlainObject(target) && isPlainObject(source)) {
67
+ for (const key of Object.keys(target)) {
68
+ if (!Object.hasOwn(source, key)) {
69
+ delete target[key];
70
+ }
71
+ }
72
+ for (const key of Object.keys(source)) {
73
+ if (!Object.hasOwn(target, key) || target[key] !== source[key]) {
74
+ setOwnProperty(target, key, source[key]);
75
+ }
76
+ }
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+ const regexSafetyCache = new WeakMap();
82
+ function isSafeRegexSource(source) {
83
+ if (source.length > 1024 || /\\[1-9]/.test(source)) {
84
+ return false;
85
+ }
86
+ const groups = [];
87
+ let inClass = false;
88
+ let escaped = false;
89
+ for (let i = 0; i < source.length; i++) {
90
+ const ch = source[i];
91
+ if (escaped) {
92
+ escaped = false;
93
+ continue;
94
+ }
95
+ if (ch === '\\') {
96
+ escaped = true;
97
+ continue;
98
+ }
99
+ if (ch === '[') {
100
+ inClass = true;
101
+ continue;
102
+ }
103
+ if (ch === ']' && inClass) {
104
+ inClass = false;
105
+ continue;
106
+ }
107
+ if (inClass) {
108
+ continue;
109
+ }
110
+ if (ch === '(') {
111
+ groups.push({ hasRepeat: false, hasAlternation: false });
112
+ continue;
113
+ }
114
+ if (ch === '|' && groups.length > 0) {
115
+ groups[groups.length - 1].hasAlternation = true;
116
+ continue;
117
+ }
118
+ if (ch === '*' || ch === '+' || ch === '{') {
119
+ if (groups.length > 0) {
120
+ groups[groups.length - 1].hasRepeat = true;
121
+ }
122
+ continue;
123
+ }
124
+ if (ch === ')' && groups.length > 0) {
125
+ const group = groups.pop();
126
+ const next = source[i + 1];
127
+ const isRepeated = next === '*' || next === '+' || next === '{';
128
+ if (isRepeated && (group.hasRepeat || group.hasAlternation)) {
129
+ return false;
130
+ }
131
+ if (isRepeated && groups.length > 0) {
132
+ groups[groups.length - 1].hasRepeat = true;
133
+ }
134
+ }
135
+ }
136
+ return true;
137
+ }
138
+ function isRegexSafe(regex) {
139
+ const cached = regexSafetyCache.get(regex);
140
+ if (cached !== undefined) {
141
+ return cached;
142
+ }
143
+ const safe = isSafeRegexSource(regex.source);
144
+ regexSafetyCache.set(regex, safe);
145
+ return safe;
146
+ }
147
+ function createSafeRegex(source, flags) {
148
+ if (!isSafeRegexSource(source)) {
149
+ throw new Error(`Unsafe regular expression: ${source}`);
150
+ }
151
+ const regex = flags === undefined ? new RegExp(source) : new RegExp(source, flags);
152
+ regexSafetyCache.set(regex, true);
153
+ return regex;
154
+ }
21
155
  function testRegex(regex, value) {
22
156
  if (!regex.global && !regex.sticky) {
23
157
  return regex.test(value);
@@ -44,28 +178,38 @@ function wantsQuery(ctx) {
44
178
  function wantsJsonRevive(ctx) {
45
179
  return ctx.from === 'json' || ctx.from === 'query';
46
180
  }
47
- function pathKey(path) {
48
- if (!path) {
49
- return '';
181
+ function isIndexSegment(seg) {
182
+ return seg.startsWith('[') && seg.endsWith(']');
183
+ }
184
+ function pathContext(path, ctx) {
185
+ const pathParts = tokenizePath(path);
186
+ const parentPath = joinPathSegments(pathParts.slice(0, -1));
187
+ const parent = getValueAtPath(ctx.root, parentPath);
188
+ const last = pathParts[pathParts.length - 1];
189
+ let index;
190
+ if (last && isIndexSegment(last)) {
191
+ const parsed = parseInt(last.slice(1, -1), 10);
192
+ if (!Number.isNaN(parsed)) {
193
+ index = parsed;
194
+ }
50
195
  }
51
- const bracket = path.lastIndexOf('[');
52
- const dot = path.lastIndexOf('.');
53
- if (bracket > dot) {
54
- const end = path.lastIndexOf(']');
55
- if (end > bracket) {
56
- return path.slice(bracket + 1, end);
196
+ let key = '';
197
+ for (let i = pathParts.length - 1; i >= 0; i--) {
198
+ if (!isIndexSegment(pathParts[i])) {
199
+ key = pathParts[i];
200
+ break;
57
201
  }
58
202
  }
59
- if (dot >= 0) {
60
- return path.slice(dot + 1);
203
+ if (index === undefined) {
204
+ return { key, path, parent, root: ctx.root };
61
205
  }
62
- return path;
206
+ return { key, path, parent, root: ctx.root, index };
63
207
  }
64
- function fromCustom(ctx, path, value, type) {
208
+ function fromCustom(ctx, path, value, kind) {
65
209
  if (typeof ctx.from !== 'function') {
66
210
  return value;
67
211
  }
68
- return ctx.from(pathKey(path), value, type);
212
+ return ctx.from(value, { ...pathContext(path, ctx), kind });
69
213
  }
70
214
  /** Query-style number coercion — shared by `from: 'query'` and `transform.ToNumber`. */
71
215
  export function coerceQueryNumber(v) {
@@ -73,8 +217,12 @@ export function coerceQueryNumber(v) {
73
217
  return v;
74
218
  }
75
219
  if (typeof v === 'string' && v.trim() !== '') {
76
- const parsed = parseFloat(v);
77
- if (!Number.isNaN(parsed)) {
220
+ const normalized = v.trim();
221
+ if (!/^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i.test(normalized)) {
222
+ return v;
223
+ }
224
+ const parsed = Number(normalized);
225
+ if (Number.isFinite(parsed)) {
78
226
  return parsed;
79
227
  }
80
228
  }
@@ -279,17 +427,32 @@ function isUriTemplate(value) {
279
427
  return URI_TEMPLATE_RE.test(value);
280
428
  }
281
429
  function parseFormatDate(value) {
282
- if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
430
+ if (typeof value !== 'string') {
283
431
  return undefined;
284
432
  }
285
- const parsed = new Date(value);
286
- if (Number.isNaN(parsed.getTime())) {
433
+ const match = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
434
+ if (!match) {
435
+ return undefined;
436
+ }
437
+ const year = Number(match[1]);
438
+ const month = Number(match[2]);
439
+ const day = Number(match[3]);
440
+ const parsed = new Date(0);
441
+ parsed.setUTCHours(0, 0, 0, 0);
442
+ parsed.setUTCFullYear(year, month - 1, day);
443
+ if (parsed.getUTCFullYear() !== year ||
444
+ parsed.getUTCMonth() !== month - 1 ||
445
+ parsed.getUTCDate() !== day) {
287
446
  return undefined;
288
447
  }
289
448
  return parsed;
290
449
  }
291
450
  function parseFormatDateTime(value) {
292
- if (typeof value !== 'string') {
451
+ if (typeof value !== 'string' ||
452
+ !/^\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/i.test(value)) {
453
+ return undefined;
454
+ }
455
+ if (!parseFormatDate(value.slice(0, 10))) {
293
456
  return undefined;
294
457
  }
295
458
  const parsed = new Date(value);
@@ -298,30 +461,236 @@ function parseFormatDateTime(value) {
298
461
  }
299
462
  return parsed;
300
463
  }
301
- function stableStringify(value, seen = new WeakSet()) {
302
- if (value === null || typeof value !== 'object') {
303
- return JSON.stringify(value);
464
+ function deepEqual(left, right, seen = new WeakMap()) {
465
+ if (left === right) {
466
+ return true;
467
+ }
468
+ if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
469
+ return false;
470
+ }
471
+ const seenRight = seen.get(left);
472
+ if (seenRight?.has(right)) {
473
+ return true;
474
+ }
475
+ if (seenRight) {
476
+ seenRight.add(right);
477
+ }
478
+ else {
479
+ seen.set(left, new WeakSet([right]));
480
+ }
481
+ if (Array.isArray(left) || Array.isArray(right)) {
482
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
483
+ return false;
484
+ }
485
+ for (let i = 0; i < left.length; i++) {
486
+ if (!deepEqual(left[i], right[i], seen)) {
487
+ return false;
488
+ }
489
+ }
490
+ return true;
491
+ }
492
+ if (left instanceof Date || right instanceof Date) {
493
+ return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
494
+ }
495
+ if (left instanceof RegExp || right instanceof RegExp) {
496
+ return left instanceof RegExp &&
497
+ right instanceof RegExp &&
498
+ left.source === right.source &&
499
+ left.flags === right.flags;
500
+ }
501
+ if (left instanceof Set || right instanceof Set) {
502
+ if (!(left instanceof Set) || !(right instanceof Set) || left.size !== right.size) {
503
+ return false;
504
+ }
505
+ const unmatched = [...right];
506
+ for (const value of left) {
507
+ const index = unmatched.findIndex(candidate => deepEqual(value, candidate));
508
+ if (index === -1) {
509
+ return false;
510
+ }
511
+ unmatched.splice(index, 1);
512
+ }
513
+ return true;
514
+ }
515
+ if (left instanceof Map || right instanceof Map) {
516
+ if (!(left instanceof Map) || !(right instanceof Map) || left.size !== right.size) {
517
+ return false;
518
+ }
519
+ const unmatched = [...right.entries()];
520
+ for (const [key, value] of left) {
521
+ const index = unmatched.findIndex(([candidateKey, candidateValue]) => deepEqual(key, candidateKey) && deepEqual(value, candidateValue));
522
+ if (index === -1) {
523
+ return false;
524
+ }
525
+ unmatched.splice(index, 1);
526
+ }
527
+ return true;
528
+ }
529
+ const leftKeys = Object.keys(left).sort();
530
+ const rightKeys = Object.keys(right).sort();
531
+ if (leftKeys.length !== rightKeys.length) {
532
+ return false;
533
+ }
534
+ for (let i = 0; i < leftKeys.length; i++) {
535
+ const key = leftKeys[i];
536
+ if (key !== rightKeys[i] || !deepEqual(left[key], right[key], seen)) {
537
+ return false;
538
+ }
539
+ }
540
+ return true;
541
+ }
542
+ function mixHash(h, part) {
543
+ return Math.imul(h ^ (part >>> 0), 16777619);
544
+ }
545
+ function mixStringHash(h, value) {
546
+ const len = value.length;
547
+ h = mixHash(h, len);
548
+ // Fast paths for short property names / small strings.
549
+ if (len === 1) {
550
+ return mixHash(h, value.charCodeAt(0));
551
+ }
552
+ if (len === 2) {
553
+ return mixHash(mixHash(h, value.charCodeAt(0)), value.charCodeAt(1));
554
+ }
555
+ if (len === 3) {
556
+ h = mixHash(h, value.charCodeAt(0));
557
+ h = mixHash(h, value.charCodeAt(1));
558
+ return mixHash(h, value.charCodeAt(2));
559
+ }
560
+ for (let i = 0; i < len; i++) {
561
+ h = mixHash(h, value.charCodeAt(i));
562
+ }
563
+ return h;
564
+ }
565
+ const uniqueFloat64Buf = new ArrayBuffer(8);
566
+ const uniqueFloat64View = new Float64Array(uniqueFloat64Buf);
567
+ const uniqueFloat64Words = new Int32Array(uniqueFloat64Buf);
568
+ function mixNumberHash(h, value) {
569
+ if (Object.is(value, -0)) {
570
+ return mixHash(h, 0x30000001);
571
+ }
572
+ if (Number.isNaN(value)) {
573
+ return mixHash(h, 0x30000002);
304
574
  }
305
- if (seen.has(value)) {
575
+ if (value === Infinity) {
576
+ return mixHash(h, 0x30000003);
577
+ }
578
+ if (value === -Infinity) {
579
+ return mixHash(h, 0x30000004);
580
+ }
581
+ uniqueFloat64View[0] = value;
582
+ return mixHash(mixHash(h, uniqueFloat64Words[0]), uniqueFloat64Words[1]);
583
+ }
584
+ /**
585
+ * Order-independent content hash for plain objects / arrays.
586
+ * Returns `undefined` for cycles and non-plain values (caller uses deepEqual list).
587
+ * Collisions are resolved with `deepEqual`.
588
+ */
589
+ function uniqueContentHash(value, seen) {
590
+ if (value === null) {
591
+ return 0x10000001;
592
+ }
593
+ if (value === undefined) {
594
+ return 0x10000002;
595
+ }
596
+ const type = typeof value;
597
+ if (type === 'string') {
598
+ return mixStringHash(0x20000000, value) >>> 0;
599
+ }
600
+ if (type === 'number') {
601
+ return mixNumberHash(0x30000000, value) >>> 0;
602
+ }
603
+ if (type === 'boolean') {
604
+ return value ? 0x40000001 : 0x40000002;
605
+ }
606
+ if (type === 'bigint') {
607
+ // Split into 32-bit limbs — avoids string alloc for common small bigints.
608
+ let h = 0x50000000;
609
+ let n = value < 0n ? -value : value;
610
+ if (value < 0n) {
611
+ h = mixHash(h, 1);
612
+ }
613
+ while (n > 0n) {
614
+ h = mixHash(h, Number(n & 0xffffffffn));
615
+ n >>= 32n;
616
+ }
617
+ return h >>> 0;
618
+ }
619
+ if (type !== 'object') {
620
+ return undefined;
621
+ }
622
+ if (value instanceof Date) {
623
+ return mixNumberHash(0x60000000, value.getTime()) >>> 0;
624
+ }
625
+ if (value instanceof RegExp) {
626
+ return mixStringHash(0x70000000, `${value.source}/${value.flags}`) >>> 0;
627
+ }
628
+ if (value instanceof Map || value instanceof Set || ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
629
+ return undefined;
630
+ }
631
+ if (seen?.has(value)) {
306
632
  return undefined;
307
633
  }
308
- seen.add(value);
309
634
  if (Array.isArray(value)) {
310
- const parts = value.map(item => {
311
- const s = stableStringify(item, seen);
312
- return s === undefined ? '"[Circular]"' : s;
313
- });
314
- return `[${parts.join(',')}]`;
635
+ const cycleSet = seen ?? new WeakSet();
636
+ cycleSet.add(value);
637
+ let h = mixHash(0x80000000, value.length);
638
+ for (let i = 0; i < value.length; i++) {
639
+ const child = uniqueContentHash(value[i], cycleSet);
640
+ if (child === undefined) {
641
+ return undefined;
642
+ }
643
+ h = mixHash(h, child);
644
+ }
645
+ return h >>> 0;
646
+ }
647
+ const proto = Object.getPrototypeOf(value);
648
+ if (proto !== Object.prototype && proto !== null) {
649
+ return undefined;
315
650
  }
316
- const keys = Object.keys(value).sort();
317
- const parts = keys.map(key => `${JSON.stringify(key)}:${stableStringify(value[key], seen) ?? '"[Circular]"'}`);
318
- return `{${parts.join(',')}}`;
651
+ const keys = Object.keys(value);
652
+ const keyCount = keys.length;
653
+ if (keyCount > 1) {
654
+ // Avoid Array#sort when already ordered (common for same-shape rows).
655
+ let ordered = true;
656
+ for (let i = 1; i < keyCount; i++) {
657
+ if (keys[i] < keys[i - 1]) {
658
+ ordered = false;
659
+ break;
660
+ }
661
+ }
662
+ if (!ordered) {
663
+ keys.sort();
664
+ }
665
+ }
666
+ let cycleSet = seen;
667
+ let h = mixHash(0x90000000, keyCount);
668
+ for (let i = 0; i < keyCount; i++) {
669
+ const key = keys[i];
670
+ const childValue = value[key];
671
+ h = mixStringHash(h, key);
672
+ if (childValue !== null && typeof childValue === 'object') {
673
+ cycleSet ??= new WeakSet();
674
+ cycleSet.add(value);
675
+ }
676
+ const child = uniqueContentHash(childValue, cycleSet);
677
+ if (child === undefined) {
678
+ return undefined;
679
+ }
680
+ h = mixHash(h, child);
681
+ }
682
+ return h >>> 0;
319
683
  }
320
684
  export const validators = {
321
685
  coerceQueryNumber,
322
686
  coerceQueryBoolean,
323
687
  coerceQueryDate,
324
688
  coerceJsonDate,
689
+ safeRegExp: createSafeRegex,
690
+ assign: (target, source) => {
691
+ assignOwnProperties(target, source);
692
+ return target;
693
+ },
325
694
  string: (v, path, ctx) => {
326
695
  if (typeof v === 'string') {
327
696
  return v;
@@ -519,7 +888,7 @@ export const validators = {
519
888
  },
520
889
  array: (v, path, ctx, childValidator) => {
521
890
  if (!Array.isArray(v)) {
522
- if (ctx.wrapArrays && v !== undefined && v !== null) {
891
+ if (wantsQuery(ctx) && v !== undefined && v !== null) {
523
892
  v = [v];
524
893
  }
525
894
  else if (typeof ctx.from === 'function') {
@@ -537,11 +906,15 @@ export const validators = {
537
906
  return v;
538
907
  }
539
908
  }
540
- const mutate = shouldMutate(ctx);
541
- const data = mutate ? v : [];
909
+ if (shouldMutate(ctx)) {
910
+ for (let i = 0; i < v.length; i++) {
911
+ v[i] = childValidator(v[i], path + '[' + i + ']', ctx);
912
+ }
913
+ return v;
914
+ }
915
+ const data = [];
542
916
  for (let i = 0; i < v.length; i++) {
543
- const val = childValidator(v[i], path + '[' + i + ']', ctx);
544
- data[i] = val;
917
+ data[i] = childValidator(v[i], path + '[' + i + ']', ctx);
545
918
  }
546
919
  return data;
547
920
  },
@@ -552,7 +925,7 @@ export const validators = {
552
925
  const wasSuccess = ctx.success;
553
926
  const result = validator(val, path + '.' + key, ctx);
554
927
  if (ctx.success) {
555
- data[key] = result;
928
+ setOwnProperty(data, key, result);
556
929
  }
557
930
  else if (isOptional && val === undefined) {
558
931
  ctx.errors.length = oldErrors;
@@ -577,7 +950,7 @@ export const validators = {
577
950
  return data;
578
951
  }
579
952
  for (const k of Object.keys(data)) {
580
- if (!allowedKeys.includes(k)) {
953
+ if (!keySetHas(allowedKeys, k)) {
581
954
  delete data[k];
582
955
  }
583
956
  }
@@ -588,10 +961,10 @@ export const validators = {
588
961
  return;
589
962
  }
590
963
  for (const key of Object.keys(v)) {
591
- if (knownKeys.includes(key)) {
964
+ if (keySetHas(knownKeys, key)) {
592
965
  continue;
593
966
  }
594
- data[key] = childValidator(v[key], path + '.' + key, ctx);
967
+ setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
595
968
  }
596
969
  },
597
970
  object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
@@ -613,7 +986,7 @@ export const validators = {
613
986
  }
614
987
  if (ctx.mode === 'strict' && allowedKeys) {
615
988
  for (const k of Object.keys(v)) {
616
- if (!allowedKeys.includes(k)) {
989
+ if (!keySetHas(allowedKeys, k)) {
617
990
  report(ctx, path, `PropertyNotAllowed<${k}>`, v[k]);
618
991
  }
619
992
  }
@@ -664,7 +1037,13 @@ export const validators = {
664
1037
  },
665
1038
  multipleOf: (v, path, ctx, n, message) => {
666
1039
  if (typeof v === 'bigint' || typeof n === 'bigint') {
667
- if (BigInt(v) % BigInt(n) !== 0n) {
1040
+ try {
1041
+ const divisor = BigInt(n);
1042
+ if (divisor === 0n || BigInt(v) % divisor !== 0n) {
1043
+ report(ctx, path, `MultipleOf<${n}>`, v, message);
1044
+ }
1045
+ }
1046
+ catch {
668
1047
  report(ctx, path, `MultipleOf<${n}>`, v, message);
669
1048
  }
670
1049
  }
@@ -674,6 +1053,10 @@ export const validators = {
674
1053
  return v;
675
1054
  },
676
1055
  pattern: (v, path, ctx, regex, expected, message) => {
1056
+ if (!isRegexSafe(regex)) {
1057
+ report(ctx, path, 'UnsafePattern', v, message);
1058
+ return v;
1059
+ }
677
1060
  if (!testRegex(regex, v)) {
678
1061
  report(ctx, path, expected, v, message);
679
1062
  }
@@ -730,7 +1113,7 @@ export const validators = {
730
1113
  case 'password': break; // Anything is a password
731
1114
  case 'regex':
732
1115
  try {
733
- new RegExp(v);
1116
+ createSafeRegex(v);
734
1117
  }
735
1118
  catch {
736
1119
  isValid = false;
@@ -792,39 +1175,181 @@ export const validators = {
792
1175
  return v;
793
1176
  },
794
1177
  uniqueItems: (v, path, ctx, message) => {
795
- const seen = new Set();
1178
+ // Typed scalar sets avoid per-item string encoding (SameValueZero for numbers,
1179
+ // with an explicit -0 flag because Set collapses -0 with 0).
1180
+ let seenStrings;
1181
+ let seenNumbers;
1182
+ let seenBigints;
1183
+ let seenDates;
1184
+ let seenRegex;
1185
+ let seenNull = false;
1186
+ let seenUndefined = false;
1187
+ let seenTrue = false;
1188
+ let seenFalse = false;
1189
+ let seenNegZero = false;
1190
+ // First item per hash; collision buckets allocated only when a hash repeats.
1191
+ const firstByHash = new Map();
1192
+ const collisionBuckets = new Map();
1193
+ const complex = [];
796
1194
  for (let i = 0; i < v.length; i++) {
797
1195
  const item = v[i];
798
- const key = typeof item === 'object' && item !== null
799
- ? stableStringify(item) ?? item
800
- : item;
801
- if (seen.has(key)) {
802
- report(ctx, path, 'UniqueItems', v, message);
803
- break;
1196
+ if (item === null) {
1197
+ if (seenNull) {
1198
+ report(ctx, path, 'UniqueItems', v, message);
1199
+ return v;
1200
+ }
1201
+ seenNull = true;
1202
+ continue;
1203
+ }
1204
+ if (item === undefined) {
1205
+ if (seenUndefined) {
1206
+ report(ctx, path, 'UniqueItems', v, message);
1207
+ return v;
1208
+ }
1209
+ seenUndefined = true;
1210
+ continue;
1211
+ }
1212
+ const type = typeof item;
1213
+ if (type === 'string') {
1214
+ seenStrings ??= new Set();
1215
+ if (seenStrings.has(item)) {
1216
+ report(ctx, path, 'UniqueItems', v, message);
1217
+ return v;
1218
+ }
1219
+ seenStrings.add(item);
1220
+ continue;
804
1221
  }
805
- seen.add(key);
1222
+ if (type === 'number') {
1223
+ if (Object.is(item, -0)) {
1224
+ if (seenNegZero) {
1225
+ report(ctx, path, 'UniqueItems', v, message);
1226
+ return v;
1227
+ }
1228
+ seenNegZero = true;
1229
+ continue;
1230
+ }
1231
+ seenNumbers ??= new Set();
1232
+ if (seenNumbers.has(item)) {
1233
+ report(ctx, path, 'UniqueItems', v, message);
1234
+ return v;
1235
+ }
1236
+ seenNumbers.add(item);
1237
+ continue;
1238
+ }
1239
+ if (type === 'boolean') {
1240
+ if (item) {
1241
+ if (seenTrue) {
1242
+ report(ctx, path, 'UniqueItems', v, message);
1243
+ return v;
1244
+ }
1245
+ seenTrue = true;
1246
+ }
1247
+ else {
1248
+ if (seenFalse) {
1249
+ report(ctx, path, 'UniqueItems', v, message);
1250
+ return v;
1251
+ }
1252
+ seenFalse = true;
1253
+ }
1254
+ continue;
1255
+ }
1256
+ if (type === 'bigint') {
1257
+ seenBigints ??= new Set();
1258
+ if (seenBigints.has(item)) {
1259
+ report(ctx, path, 'UniqueItems', v, message);
1260
+ return v;
1261
+ }
1262
+ seenBigints.add(item);
1263
+ continue;
1264
+ }
1265
+ if (type === 'object') {
1266
+ if (item instanceof Date) {
1267
+ const time = item.getTime();
1268
+ seenDates ??= new Set();
1269
+ if (seenDates.has(time)) {
1270
+ report(ctx, path, 'UniqueItems', v, message);
1271
+ return v;
1272
+ }
1273
+ seenDates.add(time);
1274
+ continue;
1275
+ }
1276
+ if (item instanceof RegExp) {
1277
+ const key = `${item.source}/${item.flags}`;
1278
+ seenRegex ??= new Set();
1279
+ if (seenRegex.has(key)) {
1280
+ report(ctx, path, 'UniqueItems', v, message);
1281
+ return v;
1282
+ }
1283
+ seenRegex.add(key);
1284
+ continue;
1285
+ }
1286
+ const hash = uniqueContentHash(item);
1287
+ if (hash !== undefined) {
1288
+ const first = firstByHash.get(hash);
1289
+ if (first === undefined) {
1290
+ firstByHash.set(hash, item);
1291
+ continue;
1292
+ }
1293
+ let bucket = collisionBuckets.get(hash);
1294
+ if (!bucket) {
1295
+ if (deepEqual(item, first)) {
1296
+ report(ctx, path, 'UniqueItems', v, message);
1297
+ return v;
1298
+ }
1299
+ collisionBuckets.set(hash, [first, item]);
1300
+ continue;
1301
+ }
1302
+ for (let j = 0; j < bucket.length; j++) {
1303
+ if (deepEqual(item, bucket[j])) {
1304
+ report(ctx, path, 'UniqueItems', v, message);
1305
+ return v;
1306
+ }
1307
+ }
1308
+ bucket.push(item);
1309
+ continue;
1310
+ }
1311
+ }
1312
+ for (let j = 0; j < complex.length; j++) {
1313
+ if (deepEqual(item, complex[j])) {
1314
+ report(ctx, path, 'UniqueItems', v, message);
1315
+ return v;
1316
+ }
1317
+ }
1318
+ complex.push(item);
806
1319
  }
807
1320
  return v;
808
1321
  },
809
1322
  custom: (v, path, ctx, fn, message) => {
810
- const pathParts = tokenizePath(path);
811
- const parentPath = joinPathSegments(pathParts.slice(0, -1));
812
- const parent = getValueAtPath(ctx.root, parentPath);
813
- const last = pathParts[pathParts.length - 1];
814
- const index = last && last.startsWith('[') ? parseInt(last.slice(1, -1), 10) : undefined;
815
- if (!fn(v, { parent, root: ctx.root, path, index: Number.isNaN(index) ? undefined : index })) {
1323
+ if (!fn(v, pathContext(path, ctx))) {
816
1324
  report(ctx, path, fn.name ? `Custom<${fn.name}>` : 'Custom', v, message);
817
1325
  }
818
1326
  return v;
819
1327
  },
820
1328
  union: (v, path, ctx, checks, expected = 'Type<Union>') => {
821
1329
  const unionErrors = [];
1330
+ // Speculative arms always use a side tree so a failed arm cannot poison the next.
1331
+ const subCtx = {
1332
+ success: true,
1333
+ errors: [],
1334
+ mode: ctx.mode,
1335
+ from: undefined,
1336
+ mutate: false,
1337
+ root: ctx.root
1338
+ };
1339
+ const accept = (val) => {
1340
+ if (shouldMutate(ctx) && commitContainer(v, val)) {
1341
+ return v;
1342
+ }
1343
+ return val;
1344
+ };
822
1345
  // Pass 1: No conversion
823
1346
  for (const check of checks) {
824
- const subCtx = { ...ctx, success: true, errors: [], from: undefined };
1347
+ subCtx.success = true;
1348
+ subCtx.errors.length = 0;
1349
+ subCtx.from = undefined;
825
1350
  const val = check(v, path, subCtx);
826
1351
  if (subCtx.success) {
827
- return val;
1352
+ return accept(val);
828
1353
  }
829
1354
  unionErrors.push(...subCtx.errors);
830
1355
  }
@@ -832,10 +1357,12 @@ export const validators = {
832
1357
  if (ctx.from) {
833
1358
  unionErrors.length = 0;
834
1359
  for (const check of checks) {
835
- const subCtx = { ...ctx, success: true, errors: [] };
1360
+ subCtx.success = true;
1361
+ subCtx.errors.length = 0;
1362
+ subCtx.from = ctx.from;
836
1363
  const val = check(v, path, subCtx);
837
1364
  if (subCtx.success) {
838
- return val;
1365
+ return accept(val);
839
1366
  }
840
1367
  unionErrors.push(...subCtx.errors);
841
1368
  }
@@ -866,8 +1393,13 @@ export const validators = {
866
1393
  return v;
867
1394
  }
868
1395
  }
869
- const mutate = shouldMutate(ctx);
870
- const data = mutate ? v : [];
1396
+ if (shouldMutate(ctx)) {
1397
+ for (let i = 0; i < checks.length; i++) {
1398
+ v[i] = checks[i](v[i], path + '[' + i + ']', ctx);
1399
+ }
1400
+ return v;
1401
+ }
1402
+ const data = [];
871
1403
  for (let i = 0; i < checks.length; i++) {
872
1404
  data[i] = checks[i](v[i], path + '[' + i + ']', ctx);
873
1405
  }
@@ -940,7 +1472,7 @@ export const validators = {
940
1472
  const mutate = shouldMutate(ctx);
941
1473
  const data = mutate ? v : {};
942
1474
  for (const key of Object.keys(v)) {
943
- data[key] = childValidator(v[key], path + '.' + key, ctx);
1475
+ setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
944
1476
  }
945
1477
  return data;
946
1478
  },
@@ -967,13 +1499,17 @@ export const validators = {
967
1499
  return v;
968
1500
  }
969
1501
  }
970
- const mutate = shouldMutate(ctx);
971
1502
  const source = [...v];
972
- if (mutate) {
1503
+ let index = 0;
1504
+ if (shouldMutate(ctx)) {
973
1505
  v.clear();
1506
+ for (const item of source) {
1507
+ v.add(childValidator(item, `${path}[${index}]`, ctx));
1508
+ index++;
1509
+ }
1510
+ return v;
974
1511
  }
975
- const data = mutate ? v : new Set();
976
- let index = 0;
1512
+ const data = new Set();
977
1513
  for (const item of source) {
978
1514
  data.add(childValidator(item, `${path}[${index}]`, ctx));
979
1515
  index++;
@@ -1000,12 +1536,17 @@ export const validators = {
1000
1536
  return v;
1001
1537
  }
1002
1538
  }
1003
- const mutate = shouldMutate(ctx);
1004
1539
  const source = [...v.entries()];
1005
- if (mutate) {
1540
+ if (shouldMutate(ctx)) {
1006
1541
  v.clear();
1542
+ for (const [key, val] of source) {
1543
+ const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
1544
+ const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
1545
+ v.set(validatedKey, validatedVal);
1546
+ }
1547
+ return v;
1007
1548
  }
1008
- const data = mutate ? v : new Map();
1549
+ const data = new Map();
1009
1550
  for (const [key, val] of source) {
1010
1551
  const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
1011
1552
  const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
@@ -1132,8 +1673,11 @@ export class MetadataStoreClass {
1132
1673
  return schema;
1133
1674
  }
1134
1675
  getOrCompileSchema(schema) {
1676
+ if (typeof schema === 'boolean') {
1677
+ return compileSchema(schema);
1678
+ }
1135
1679
  if (typeof schema !== 'object' || schema === null) {
1136
- throw new Error('Invalid JSON Schema: must be a non-null object');
1680
+ throw new Error('Invalid JSON Schema: must be a non-null object or boolean');
1137
1681
  }
1138
1682
  let compiled = this.compiledSchemas.get(schema);
1139
1683
  if (!compiled) {
@@ -1145,20 +1689,24 @@ export class MetadataStoreClass {
1145
1689
  is(validator, value, options) {
1146
1690
  const opt = options;
1147
1691
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1148
- // Type predicates require the value already match T never coerce.
1149
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1150
- const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1151
- const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
1152
- validator(value, '', ctx);
1153
- return ctx.success;
1692
+ const from = typeof opt === 'object' ? opt?.from : undefined;
1693
+ const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
1694
+ const res = validator(value, '', ctx);
1695
+ if (!ctx.success) {
1696
+ return false;
1697
+ }
1698
+ if (res === value) {
1699
+ return true;
1700
+ }
1701
+ // Fallback when a branch returned a side tree (e.g. union) that still needs copying.
1702
+ return commitContainer(value, res);
1154
1703
  }
1155
1704
  assert(validator, value, options) {
1156
1705
  const opt = options;
1157
1706
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1158
1707
  const from = typeof opt === 'object' ? opt?.from : undefined;
1159
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1160
1708
  const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1161
- const ctx = { success: true, errors: [], mode, from, wrapArrays, mutate, root: value };
1709
+ const ctx = { success: true, errors: [], mode, from, mutate, root: value };
1162
1710
  const res = validator(value, '', ctx);
1163
1711
  if (!ctx.success) {
1164
1712
  if (typeof opt === 'object' && opt?.errorFactory) {
@@ -1166,40 +1714,63 @@ export class MetadataStoreClass {
1166
1714
  }
1167
1715
  throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
1168
1716
  }
1717
+ if (mutate) {
1718
+ if (res === value || commitContainer(value, res)) {
1719
+ return value;
1720
+ }
1721
+ return res;
1722
+ }
1169
1723
  return res;
1170
1724
  }
1171
1725
  assertGuard(validator, value, options) {
1172
1726
  const opt = options;
1173
1727
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1174
- // Assertion predicates require the value already match T never coerce.
1175
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1176
- const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1177
- const ctx = { success: true, errors: [], mode, wrapArrays, mutate, root: value };
1178
- validator(value, '', ctx);
1179
- if (!ctx.success) {
1180
- if (typeof opt === 'object' && opt?.errorFactory) {
1181
- throw opt.errorFactory(ctx.errors);
1728
+ const from = typeof opt === 'object' ? opt?.from : undefined;
1729
+ const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
1730
+ const res = validator(value, '', ctx);
1731
+ if (ctx.success && (res === value || commitContainer(value, res))) {
1732
+ return;
1733
+ }
1734
+ // Root was replaced (e.g. primitive coerce) — binding unchanged; report a normal type failure.
1735
+ if (ctx.success && res !== value) {
1736
+ ctx.success = true;
1737
+ ctx.errors.length = 0;
1738
+ ctx.from = undefined;
1739
+ validator(value, '', ctx);
1740
+ if (ctx.success) {
1741
+ report(ctx, '', 'RootNotRewritable', value);
1182
1742
  }
1183
- throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
1184
1743
  }
1744
+ if (typeof opt === 'object' && opt?.errorFactory) {
1745
+ throw opt.errorFactory(ctx.errors);
1746
+ }
1747
+ throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
1185
1748
  }
1186
1749
  validate(validator, value, options) {
1187
1750
  const opt = options;
1188
1751
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1189
1752
  const from = typeof opt === 'object' ? opt?.from : undefined;
1190
- const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
1191
1753
  const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1192
- const ctx = { success: true, errors: [], mode, from, wrapArrays, mutate, root: value };
1754
+ const ctx = { success: true, errors: [], mode, from, mutate, root: value };
1193
1755
  const res = validator(value, '', ctx);
1194
- return { success: ctx.success, errors: ctx.errors, data: res };
1756
+ if (!ctx.success) {
1757
+ return { success: false, errors: ctx.errors };
1758
+ }
1759
+ if (mutate) {
1760
+ if (res === value || commitContainer(value, res)) {
1761
+ return { success: true, errors: [], data: value };
1762
+ }
1763
+ return { success: true, errors: [], data: res };
1764
+ }
1765
+ return { success: true, errors: [], data: res };
1195
1766
  }
1196
1767
  }
1197
1768
  export function groupErrorsByPath(errors) {
1198
- const grouped = {};
1769
+ const grouped = Object.create(null);
1199
1770
  const visit = (list) => {
1200
1771
  for (const err of list) {
1201
- if (!grouped[err.path]) {
1202
- grouped[err.path] = { value: err.value, errors: [] };
1772
+ if (!Object.hasOwn(grouped, err.path)) {
1773
+ setOwnProperty(grouped, err.path, { value: err.value, errors: [] });
1203
1774
  }
1204
1775
  if (!grouped[err.path].errors.includes(err.error)) {
1205
1776
  grouped[err.path].errors.push(err.error);
@@ -1213,13 +1784,53 @@ export function groupErrorsByPath(errors) {
1213
1784
  return grouped;
1214
1785
  }
1215
1786
  export const MetadataStore = new MetadataStoreClass();
1787
+ const UNSUPPORTED_SCHEMA_KEYWORDS = [
1788
+ 'enum',
1789
+ 'oneOf',
1790
+ 'not',
1791
+ 'if',
1792
+ 'then',
1793
+ 'else',
1794
+ 'patternProperties',
1795
+ 'propertyNames',
1796
+ 'dependencies',
1797
+ 'dependentRequired',
1798
+ 'dependentSchemas',
1799
+ 'contains',
1800
+ 'minContains',
1801
+ 'maxContains',
1802
+ 'prefixItems',
1803
+ 'unevaluatedProperties',
1804
+ 'unevaluatedItems'
1805
+ ];
1216
1806
  export function compileSchema(schema) {
1217
1807
  const rootDefs = schema.$defs || schema.definitions || {};
1218
1808
  const compiledDefs = new Map();
1219
1809
  function build(subSchema) {
1220
- if (!subSchema || typeof subSchema !== 'object') {
1810
+ if (subSchema === true || subSchema === undefined) {
1221
1811
  return (v) => v;
1222
1812
  }
1813
+ if (subSchema === false) {
1814
+ return (v, path, ctx) => {
1815
+ report(ctx, path, 'Schema<false>', v);
1816
+ return v;
1817
+ };
1818
+ }
1819
+ if (!subSchema || typeof subSchema !== 'object') {
1820
+ throw new Error('Invalid JSON Schema: subschemas must be objects or booleans');
1821
+ }
1822
+ for (const keyword of UNSUPPORTED_SCHEMA_KEYWORDS) {
1823
+ if (keyword in subSchema) {
1824
+ throw new Error(`Unsupported JSON Schema keyword: ${keyword}`);
1825
+ }
1826
+ }
1827
+ if (Array.isArray(subSchema.type)) {
1828
+ throw new Error('Unsupported JSON Schema keyword: type arrays');
1829
+ }
1830
+ if (typeof subSchema.type === 'string' &&
1831
+ !['string', 'number', 'integer', 'boolean', 'null', 'array', 'object'].includes(subSchema.type)) {
1832
+ throw new Error(`Unsupported JSON Schema type: ${subSchema.type}`);
1833
+ }
1223
1834
  if (subSchema.$ref) {
1224
1835
  const refPath = subSchema.$ref;
1225
1836
  if (compiledDefs.has(refPath)) {
@@ -1281,31 +1892,90 @@ export function compileSchema(schema) {
1281
1892
  return v;
1282
1893
  };
1283
1894
  }
1895
+ if ('x-typescript-type' in subSchema) {
1896
+ throw new Error(`Unsupported x-typescript-type: ${subSchema['x-typescript-type']}`);
1897
+ }
1284
1898
  if (subSchema.allOf) {
1285
1899
  const checks = subSchema.allOf.map((s) => build(s));
1286
- return (v, path, ctx) => {
1287
- const prevMode = ctx.mode;
1288
- if (ctx.mode !== 'strip') {
1289
- ctx.mode = 'relaxed';
1900
+ const mergeKeys = subSchema.allOf.map((s) => {
1901
+ if (s?.type !== 'object' ||
1902
+ ('additionalProperties' in s && s.additionalProperties !== false)) {
1903
+ return undefined;
1290
1904
  }
1291
- let data = validators.objectShell(v, ctx);
1292
- for (const check of checks) {
1293
- const val = check(v, path, ctx);
1905
+ return new Set(Object.keys(s.properties || {}));
1906
+ });
1907
+ const allObjectSchemas = subSchema.allOf.length > 0 &&
1908
+ subSchema.allOf.every((s) => s?.type === 'object');
1909
+ const allowsAdditional = !allObjectSchemas ||
1910
+ subSchema.allOf.some((s) => 'additionalProperties' in s && s.additionalProperties !== false);
1911
+ const combinedKeys = allowsAdditional
1912
+ ? undefined
1913
+ : new Set(subSchema.allOf.flatMap((s) => Object.keys(s.properties || {})));
1914
+ return (v, path, ctx) => {
1915
+ const errors = [];
1916
+ let data = undefined;
1917
+ const subCtx = {
1918
+ success: true,
1919
+ errors: [],
1920
+ mode: 'relaxed',
1921
+ from: ctx.from,
1922
+ mutate: false,
1923
+ root: ctx.root
1924
+ };
1925
+ for (let i = 0; i < checks.length; i++) {
1926
+ const check = checks[i];
1927
+ subCtx.success = true;
1928
+ subCtx.errors.length = 0;
1929
+ subCtx.from = ctx.from;
1930
+ subCtx.root = ctx.root;
1931
+ const val = check(v, path, subCtx);
1932
+ errors.push(...subCtx.errors);
1933
+ if (data === undefined) {
1934
+ data = isPlainObject(val) ? {} : val;
1935
+ }
1294
1936
  if (isPlainObject(val) && isPlainObject(data)) {
1295
- Object.assign(data, val);
1937
+ const keys = mergeKeys[i] || new Set(Object.keys(val));
1938
+ for (const key of keys) {
1939
+ if (Object.hasOwn(val, key)) {
1940
+ setOwnProperty(data, key, val[key]);
1941
+ }
1942
+ }
1296
1943
  }
1297
- else {
1944
+ else if (data !== val) {
1298
1945
  data = val;
1299
1946
  }
1300
1947
  }
1301
- ctx.mode = prevMode;
1302
- return data;
1948
+ if (errors.length > 0) {
1949
+ ctx.success = false;
1950
+ ctx.errors.push(...errors);
1951
+ return v;
1952
+ }
1953
+ if (combinedKeys && isPlainObject(v) && isPlainObject(data)) {
1954
+ for (const key of Object.keys(v)) {
1955
+ if (combinedKeys.has(key)) {
1956
+ continue;
1957
+ }
1958
+ if (ctx.mode === 'strict') {
1959
+ report(ctx, path, `PropertyNotAllowed<${key}>`, v[key]);
1960
+ }
1961
+ else if (ctx.mode === 'strip') {
1962
+ delete data[key];
1963
+ }
1964
+ }
1965
+ }
1966
+ if (!ctx.success) {
1967
+ return v;
1968
+ }
1969
+ if (shouldMutate(ctx) && commitContainer(v, data)) {
1970
+ return v;
1971
+ }
1972
+ return data === undefined ? v : data;
1303
1973
  };
1304
1974
  }
1305
1975
  if (subSchema.type === 'string') {
1306
1976
  const minLength = subSchema.minLength;
1307
1977
  const maxLength = subSchema.maxLength;
1308
- const pattern = subSchema.pattern ? new RegExp(subSchema.pattern) : undefined;
1978
+ const pattern = subSchema.pattern ? createSafeRegex(subSchema.pattern) : undefined;
1309
1979
  const patternStr = subSchema.pattern;
1310
1980
  const format = subSchema.format;
1311
1981
  return (v, path, ctx) => {
@@ -1406,7 +2076,7 @@ export function compileSchema(schema) {
1406
2076
  const check = build(s);
1407
2077
  return [key, isOptional, check];
1408
2078
  });
1409
- const knownKeys = Object.keys(subSchema.properties || {});
2079
+ const knownKeys = new Set(Object.keys(subSchema.properties || {}));
1410
2080
  const additional = 'additionalProperties' in subSchema
1411
2081
  ? subSchema.additionalProperties
1412
2082
  : false;