@webergency-utils/typechecker 0.2.2 → 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);
@@ -83,8 +217,12 @@ export function coerceQueryNumber(v) {
83
217
  return v;
84
218
  }
85
219
  if (typeof v === 'string' && v.trim() !== '') {
86
- const parsed = parseFloat(v);
87
- 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)) {
88
226
  return parsed;
89
227
  }
90
228
  }
@@ -289,17 +427,32 @@ function isUriTemplate(value) {
289
427
  return URI_TEMPLATE_RE.test(value);
290
428
  }
291
429
  function parseFormatDate(value) {
292
- if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
430
+ if (typeof value !== 'string') {
293
431
  return undefined;
294
432
  }
295
- const parsed = new Date(value);
296
- 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) {
297
446
  return undefined;
298
447
  }
299
448
  return parsed;
300
449
  }
301
450
  function parseFormatDateTime(value) {
302
- 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))) {
303
456
  return undefined;
304
457
  }
305
458
  const parsed = new Date(value);
@@ -308,30 +461,236 @@ function parseFormatDateTime(value) {
308
461
  }
309
462
  return parsed;
310
463
  }
311
- function stableStringify(value, seen = new WeakSet()) {
312
- if (value === null || typeof value !== 'object') {
313
- 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;
314
474
  }
315
- if (seen.has(value)) {
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);
574
+ }
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)) {
316
632
  return undefined;
317
633
  }
318
- seen.add(value);
319
634
  if (Array.isArray(value)) {
320
- const parts = value.map(item => {
321
- const s = stableStringify(item, seen);
322
- return s === undefined ? '"[Circular]"' : s;
323
- });
324
- 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;
650
+ }
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
+ }
325
665
  }
326
- const keys = Object.keys(value).sort();
327
- const parts = keys.map(key => `${JSON.stringify(key)}:${stableStringify(value[key], seen) ?? '"[Circular]"'}`);
328
- return `{${parts.join(',')}}`;
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;
329
683
  }
330
684
  export const validators = {
331
685
  coerceQueryNumber,
332
686
  coerceQueryBoolean,
333
687
  coerceQueryDate,
334
688
  coerceJsonDate,
689
+ safeRegExp: createSafeRegex,
690
+ assign: (target, source) => {
691
+ assignOwnProperties(target, source);
692
+ return target;
693
+ },
335
694
  string: (v, path, ctx) => {
336
695
  if (typeof v === 'string') {
337
696
  return v;
@@ -547,11 +906,15 @@ export const validators = {
547
906
  return v;
548
907
  }
549
908
  }
550
- const mutate = shouldMutate(ctx);
551
- 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 = [];
552
916
  for (let i = 0; i < v.length; i++) {
553
- const val = childValidator(v[i], path + '[' + i + ']', ctx);
554
- data[i] = val;
917
+ data[i] = childValidator(v[i], path + '[' + i + ']', ctx);
555
918
  }
556
919
  return data;
557
920
  },
@@ -562,7 +925,7 @@ export const validators = {
562
925
  const wasSuccess = ctx.success;
563
926
  const result = validator(val, path + '.' + key, ctx);
564
927
  if (ctx.success) {
565
- data[key] = result;
928
+ setOwnProperty(data, key, result);
566
929
  }
567
930
  else if (isOptional && val === undefined) {
568
931
  ctx.errors.length = oldErrors;
@@ -587,7 +950,7 @@ export const validators = {
587
950
  return data;
588
951
  }
589
952
  for (const k of Object.keys(data)) {
590
- if (!allowedKeys.includes(k)) {
953
+ if (!keySetHas(allowedKeys, k)) {
591
954
  delete data[k];
592
955
  }
593
956
  }
@@ -598,10 +961,10 @@ export const validators = {
598
961
  return;
599
962
  }
600
963
  for (const key of Object.keys(v)) {
601
- if (knownKeys.includes(key)) {
964
+ if (keySetHas(knownKeys, key)) {
602
965
  continue;
603
966
  }
604
- data[key] = childValidator(v[key], path + '.' + key, ctx);
967
+ setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
605
968
  }
606
969
  },
607
970
  object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
@@ -623,7 +986,7 @@ export const validators = {
623
986
  }
624
987
  if (ctx.mode === 'strict' && allowedKeys) {
625
988
  for (const k of Object.keys(v)) {
626
- if (!allowedKeys.includes(k)) {
989
+ if (!keySetHas(allowedKeys, k)) {
627
990
  report(ctx, path, `PropertyNotAllowed<${k}>`, v[k]);
628
991
  }
629
992
  }
@@ -674,7 +1037,13 @@ export const validators = {
674
1037
  },
675
1038
  multipleOf: (v, path, ctx, n, message) => {
676
1039
  if (typeof v === 'bigint' || typeof n === 'bigint') {
677
- 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 {
678
1047
  report(ctx, path, `MultipleOf<${n}>`, v, message);
679
1048
  }
680
1049
  }
@@ -684,6 +1053,10 @@ export const validators = {
684
1053
  return v;
685
1054
  },
686
1055
  pattern: (v, path, ctx, regex, expected, message) => {
1056
+ if (!isRegexSafe(regex)) {
1057
+ report(ctx, path, 'UnsafePattern', v, message);
1058
+ return v;
1059
+ }
687
1060
  if (!testRegex(regex, v)) {
688
1061
  report(ctx, path, expected, v, message);
689
1062
  }
@@ -740,7 +1113,7 @@ export const validators = {
740
1113
  case 'password': break; // Anything is a password
741
1114
  case 'regex':
742
1115
  try {
743
- new RegExp(v);
1116
+ createSafeRegex(v);
744
1117
  }
745
1118
  catch {
746
1119
  isValid = false;
@@ -802,17 +1175,147 @@ export const validators = {
802
1175
  return v;
803
1176
  },
804
1177
  uniqueItems: (v, path, ctx, message) => {
805
- 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 = [];
806
1194
  for (let i = 0; i < v.length; i++) {
807
1195
  const item = v[i];
808
- const key = typeof item === 'object' && item !== null
809
- ? stableStringify(item) ?? item
810
- : item;
811
- if (seen.has(key)) {
812
- report(ctx, path, 'UniqueItems', v, message);
813
- 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;
1221
+ }
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;
814
1255
  }
815
- seen.add(key);
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);
816
1319
  }
817
1320
  return v;
818
1321
  },
@@ -824,12 +1327,29 @@ export const validators = {
824
1327
  },
825
1328
  union: (v, path, ctx, checks, expected = 'Type<Union>') => {
826
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
+ };
827
1345
  // Pass 1: No conversion
828
1346
  for (const check of checks) {
829
- const subCtx = { ...ctx, success: true, errors: [], from: undefined };
1347
+ subCtx.success = true;
1348
+ subCtx.errors.length = 0;
1349
+ subCtx.from = undefined;
830
1350
  const val = check(v, path, subCtx);
831
1351
  if (subCtx.success) {
832
- return val;
1352
+ return accept(val);
833
1353
  }
834
1354
  unionErrors.push(...subCtx.errors);
835
1355
  }
@@ -837,10 +1357,12 @@ export const validators = {
837
1357
  if (ctx.from) {
838
1358
  unionErrors.length = 0;
839
1359
  for (const check of checks) {
840
- const subCtx = { ...ctx, success: true, errors: [] };
1360
+ subCtx.success = true;
1361
+ subCtx.errors.length = 0;
1362
+ subCtx.from = ctx.from;
841
1363
  const val = check(v, path, subCtx);
842
1364
  if (subCtx.success) {
843
- return val;
1365
+ return accept(val);
844
1366
  }
845
1367
  unionErrors.push(...subCtx.errors);
846
1368
  }
@@ -871,8 +1393,13 @@ export const validators = {
871
1393
  return v;
872
1394
  }
873
1395
  }
874
- const mutate = shouldMutate(ctx);
875
- 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 = [];
876
1403
  for (let i = 0; i < checks.length; i++) {
877
1404
  data[i] = checks[i](v[i], path + '[' + i + ']', ctx);
878
1405
  }
@@ -945,7 +1472,7 @@ export const validators = {
945
1472
  const mutate = shouldMutate(ctx);
946
1473
  const data = mutate ? v : {};
947
1474
  for (const key of Object.keys(v)) {
948
- data[key] = childValidator(v[key], path + '.' + key, ctx);
1475
+ setOwnProperty(data, key, childValidator(v[key], path + '.' + key, ctx));
949
1476
  }
950
1477
  return data;
951
1478
  },
@@ -972,13 +1499,17 @@ export const validators = {
972
1499
  return v;
973
1500
  }
974
1501
  }
975
- const mutate = shouldMutate(ctx);
976
1502
  const source = [...v];
977
- if (mutate) {
1503
+ let index = 0;
1504
+ if (shouldMutate(ctx)) {
978
1505
  v.clear();
1506
+ for (const item of source) {
1507
+ v.add(childValidator(item, `${path}[${index}]`, ctx));
1508
+ index++;
1509
+ }
1510
+ return v;
979
1511
  }
980
- const data = mutate ? v : new Set();
981
- let index = 0;
1512
+ const data = new Set();
982
1513
  for (const item of source) {
983
1514
  data.add(childValidator(item, `${path}[${index}]`, ctx));
984
1515
  index++;
@@ -1005,12 +1536,17 @@ export const validators = {
1005
1536
  return v;
1006
1537
  }
1007
1538
  }
1008
- const mutate = shouldMutate(ctx);
1009
1539
  const source = [...v.entries()];
1010
- if (mutate) {
1540
+ if (shouldMutate(ctx)) {
1011
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;
1012
1548
  }
1013
- const data = mutate ? v : new Map();
1549
+ const data = new Map();
1014
1550
  for (const [key, val] of source) {
1015
1551
  const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
1016
1552
  const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
@@ -1137,8 +1673,11 @@ export class MetadataStoreClass {
1137
1673
  return schema;
1138
1674
  }
1139
1675
  getOrCompileSchema(schema) {
1676
+ if (typeof schema === 'boolean') {
1677
+ return compileSchema(schema);
1678
+ }
1140
1679
  if (typeof schema !== 'object' || schema === null) {
1141
- 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');
1142
1681
  }
1143
1682
  let compiled = this.compiledSchemas.get(schema);
1144
1683
  if (!compiled) {
@@ -1151,14 +1690,16 @@ export class MetadataStoreClass {
1151
1690
  const opt = options;
1152
1691
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1153
1692
  const from = typeof opt === 'object' ? opt?.from : undefined;
1154
- // Always mutate — no returned data. Root must stay the same binding (res === value).
1155
1693
  const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
1156
1694
  const res = validator(value, '', ctx);
1157
1695
  if (!ctx.success) {
1158
1696
  return false;
1159
1697
  }
1160
- // Primitive / replaced roots cannot update the caller's binding — treat as not matching.
1161
- return res === value;
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);
1162
1703
  }
1163
1704
  assert(validator, value, options) {
1164
1705
  const opt = options;
@@ -1173,16 +1714,21 @@ export class MetadataStoreClass {
1173
1714
  }
1174
1715
  throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
1175
1716
  }
1717
+ if (mutate) {
1718
+ if (res === value || commitContainer(value, res)) {
1719
+ return value;
1720
+ }
1721
+ return res;
1722
+ }
1176
1723
  return res;
1177
1724
  }
1178
1725
  assertGuard(validator, value, options) {
1179
1726
  const opt = options;
1180
1727
  const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
1181
1728
  const from = typeof opt === 'object' ? opt?.from : undefined;
1182
- // Always mutate — no returned data. Root must stay the same binding (res === value).
1183
1729
  const ctx = { success: true, errors: [], mode, from, mutate: true, root: value };
1184
1730
  const res = validator(value, '', ctx);
1185
- if (ctx.success && res === value) {
1731
+ if (ctx.success && (res === value || commitContainer(value, res))) {
1186
1732
  return;
1187
1733
  }
1188
1734
  // Root was replaced (e.g. primitive coerce) — binding unchanged; report a normal type failure.
@@ -1191,6 +1737,9 @@ export class MetadataStoreClass {
1191
1737
  ctx.errors.length = 0;
1192
1738
  ctx.from = undefined;
1193
1739
  validator(value, '', ctx);
1740
+ if (ctx.success) {
1741
+ report(ctx, '', 'RootNotRewritable', value);
1742
+ }
1194
1743
  }
1195
1744
  if (typeof opt === 'object' && opt?.errorFactory) {
1196
1745
  throw opt.errorFactory(ctx.errors);
@@ -1204,15 +1753,24 @@ export class MetadataStoreClass {
1204
1753
  const mutate = typeof opt === 'object' ? opt?.mutate === true : false;
1205
1754
  const ctx = { success: true, errors: [], mode, from, mutate, root: value };
1206
1755
  const res = validator(value, '', ctx);
1207
- 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 };
1208
1766
  }
1209
1767
  }
1210
1768
  export function groupErrorsByPath(errors) {
1211
- const grouped = {};
1769
+ const grouped = Object.create(null);
1212
1770
  const visit = (list) => {
1213
1771
  for (const err of list) {
1214
- if (!grouped[err.path]) {
1215
- grouped[err.path] = { value: err.value, errors: [] };
1772
+ if (!Object.hasOwn(grouped, err.path)) {
1773
+ setOwnProperty(grouped, err.path, { value: err.value, errors: [] });
1216
1774
  }
1217
1775
  if (!grouped[err.path].errors.includes(err.error)) {
1218
1776
  grouped[err.path].errors.push(err.error);
@@ -1226,13 +1784,53 @@ export function groupErrorsByPath(errors) {
1226
1784
  return grouped;
1227
1785
  }
1228
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
+ ];
1229
1806
  export function compileSchema(schema) {
1230
1807
  const rootDefs = schema.$defs || schema.definitions || {};
1231
1808
  const compiledDefs = new Map();
1232
1809
  function build(subSchema) {
1233
- if (!subSchema || typeof subSchema !== 'object') {
1810
+ if (subSchema === true || subSchema === undefined) {
1234
1811
  return (v) => v;
1235
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
+ }
1236
1834
  if (subSchema.$ref) {
1237
1835
  const refPath = subSchema.$ref;
1238
1836
  if (compiledDefs.has(refPath)) {
@@ -1294,31 +1892,90 @@ export function compileSchema(schema) {
1294
1892
  return v;
1295
1893
  };
1296
1894
  }
1895
+ if ('x-typescript-type' in subSchema) {
1896
+ throw new Error(`Unsupported x-typescript-type: ${subSchema['x-typescript-type']}`);
1897
+ }
1297
1898
  if (subSchema.allOf) {
1298
1899
  const checks = subSchema.allOf.map((s) => build(s));
1299
- return (v, path, ctx) => {
1300
- const prevMode = ctx.mode;
1301
- if (ctx.mode !== 'strip') {
1302
- 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;
1303
1904
  }
1304
- let data = validators.objectShell(v, ctx);
1305
- for (const check of checks) {
1306
- 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
+ }
1307
1936
  if (isPlainObject(val) && isPlainObject(data)) {
1308
- 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
+ }
1309
1943
  }
1310
- else {
1944
+ else if (data !== val) {
1311
1945
  data = val;
1312
1946
  }
1313
1947
  }
1314
- ctx.mode = prevMode;
1315
- 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;
1316
1973
  };
1317
1974
  }
1318
1975
  if (subSchema.type === 'string') {
1319
1976
  const minLength = subSchema.minLength;
1320
1977
  const maxLength = subSchema.maxLength;
1321
- const pattern = subSchema.pattern ? new RegExp(subSchema.pattern) : undefined;
1978
+ const pattern = subSchema.pattern ? createSafeRegex(subSchema.pattern) : undefined;
1322
1979
  const patternStr = subSchema.pattern;
1323
1980
  const format = subSchema.format;
1324
1981
  return (v, path, ctx) => {
@@ -1419,7 +2076,7 @@ export function compileSchema(schema) {
1419
2076
  const check = build(s);
1420
2077
  return [key, isOptional, check];
1421
2078
  });
1422
- const knownKeys = Object.keys(subSchema.properties || {});
2079
+ const knownKeys = new Set(Object.keys(subSchema.properties || {}));
1423
2080
  const additional = 'additionalProperties' in subSchema
1424
2081
  ? subSchema.additionalProperties
1425
2082
  : false;