snap-validate 0.3.3 → 0.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 (3) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/index.js +347 -108
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![install size](https://img.shields.io/badge/dynamic/json?url=https://packagephobia.com/v2/api.json?p=snap-validate&query=$.install.pretty&label=install%20size&style=flat-square)](https://packagephobia.now.sh/result?p=snap-validate)
7
7
  [![npm bundle size](https://img.shields.io/bundlephobia/minzip/snap-validate?style=flat-square)](https://bundlephobia.com/package/snap-validate@latest)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/snap-validate.svg?style=flat-square)](https://npm-stat.com/charts.html?package=snap-validate)
9
+ ![Codecov](https://img.shields.io/codecov/c/github/aniru-dh21/snap-validate)
9
10
 
10
11
  A lightning-fast, lightweight validation library for common patterns without heavy dependencies. Perfect for client-side and server-side validation with zero external dependencies and built-in protection against ReDoS (Regular Expression Denial of Service) attacks.
11
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snap-validate",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight validation library for common patterns without heavy dependencies",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
package/src/index.js CHANGED
@@ -1,18 +1,16 @@
1
1
  /**
2
2
  * Snap Validate - Enhanced Lightweight validator library
3
- * @version 0.3.2 - Typescript Patch
3
+ * @version 0.4.0 - Phase 1: Arrays, Nested Objects, Transforms
4
4
  */
5
5
 
6
6
  // Utility function to safely test regex with timeout protection
7
7
  const safeRegexTest = (regex, str, timeoutMs = 1000) => {
8
8
  return new Promise((resolve, reject) => {
9
- // SECURITY FIX: Add input length validation before regex test
10
9
  if (str.length > 10000) {
11
10
  reject(new Error('Input too long for regex validation'));
12
11
  return;
13
12
  }
14
13
 
15
- // SECURITY FIX: Add regex safety check before execution
16
14
  if (!isRegexSafe(regex)) {
17
15
  reject(new Error('Unsafe regex pattern detected'));
18
16
  return;
@@ -35,13 +33,9 @@ const safeRegexTest = (regex, str, timeoutMs = 1000) => {
35
33
 
36
34
  // Synchronous safe regex test with input length protection
37
35
  const safeRegexTestSync = (regex, str, maxLength = 10000) => {
38
- // Limit input length to prevent ReDoS
39
36
  if (str.length > maxLength) {
40
37
  throw new Error('Input too long for pattern validation');
41
38
  }
42
-
43
- // For additional safety, we could add a timeout using a worker thread or
44
- // other mechanism, but for now we rely on input length limiting
45
39
  return regex.test(str);
46
40
  };
47
41
 
@@ -49,21 +43,14 @@ const safeRegexTestSync = (regex, str, maxLength = 10000) => {
49
43
  const isRegexSafe = (regex) => {
50
44
  const regexStr = regex.toString();
51
45
 
52
- // Check for common ReDoS patterns - more precise detection
53
46
  const dangerousPatterns = [
54
- // Nested quantifiers like (a+)+ or (a*)* or (a?)?
55
47
  /\([^)]*[+*?][^)]*\)[+*?]/,
56
- // Alternation with overlapping and quantifiers like (a|a)*
57
48
  /\([^)]*\|[^)]*\)[+*]/,
58
- // Catastrophic backtracking with greedy quantifiers
59
49
  /\([^)]*\.\*[^)]*\)\*/,
60
- // Multiple consecutive quantifiers (not separated by characters)
61
50
  /[+*?]{2,}/,
62
- // Exponential alternation patterns
63
51
  /\([^)]*\|[^)]*\)\+.*\([^)]*\|[^)]*\)\+/
64
52
  ];
65
53
 
66
- // Check if the pattern has obvious ReDoS vulnerabilities
67
54
  const isDangerous = dangerousPatterns.some((pattern) =>
68
55
  pattern.test(regexStr)
69
56
  );
@@ -92,12 +79,29 @@ class BaseValidator {
92
79
  this.rules = [];
93
80
  this.asyncRules = [];
94
81
  this.isOptional = false;
95
- this.regexTimeout = 1000; // Default timeout for regex operations
82
+ this.regexTimeout = 1000;
83
+ this.fieldName = null; // Track field name for better error messages
84
+ }
85
+
86
+ // Set field name for contextual error messages
87
+ setFieldName(name) {
88
+ this.fieldName = name;
89
+ return this;
90
+ }
91
+
92
+ // Helper to format error messages with field context
93
+ _formatError(message) {
94
+ if (
95
+ this.fieldName &&
96
+ !message.toLowerCase().includes(this.fieldName.toLowerCase())
97
+ ) {
98
+ return `${this.fieldName}: ${message}`;
99
+ }
100
+ return message;
96
101
  }
97
102
 
98
103
  required(message = 'This field is required') {
99
104
  this.rules.push(() => {
100
- // Skip validation if optional and empty
101
105
  if (
102
106
  this.isOptional &&
103
107
  (this.value === null || this.value === undefined || this.value === '')
@@ -110,7 +114,7 @@ class BaseValidator {
110
114
  this.value === undefined ||
111
115
  this.value === ''
112
116
  ) {
113
- return new ValidationResult(false, [message]);
117
+ return new ValidationResult(false, [this._formatError(message)]);
114
118
  }
115
119
  return new ValidationResult(true);
116
120
  });
@@ -127,9 +131,91 @@ class BaseValidator {
127
131
  return this;
128
132
  }
129
133
 
134
+ // Transform/sanitize data before validation
135
+ transform(fn, errorMessage = 'Transform function failed') {
136
+ this.rules.push(() => {
137
+ if (this.value != null && this.value !== '') {
138
+ try {
139
+ this.value = fn(this.value);
140
+ } catch (error) {
141
+ return new ValidationResult(false, [
142
+ this._formatError(`${errorMessage}: ${error.message}`)
143
+ ]);
144
+ }
145
+ }
146
+ return new ValidationResult(true);
147
+ });
148
+ return this;
149
+ }
150
+
151
+ // Check if value equals another value
152
+ equals(compareValue, message) {
153
+ const defaultMessage = `Must equal ${compareValue}`;
154
+ this.rules.push(() => {
155
+ if (
156
+ this.isOptional &&
157
+ (this.value === null || this.value === undefined || this.value === '')
158
+ ) {
159
+ return new ValidationResult(true);
160
+ }
161
+
162
+ if (this.value !== compareValue) {
163
+ return new ValidationResult(false, [
164
+ this._formatError(message || defaultMessage)
165
+ ]);
166
+ }
167
+ return new ValidationResult(true);
168
+ });
169
+ return this;
170
+ }
171
+
172
+ // Check if value is one of allowed values
173
+ oneOf(allowedValues, message) {
174
+ const defaultMessage = `Must be one of: ${allowedValues.join(', ')}`;
175
+ this.rules.push(() => {
176
+ if (
177
+ this.isOptional &&
178
+ (this.value === null || this.value === undefined || this.value === '')
179
+ ) {
180
+ return new ValidationResult(true);
181
+ }
182
+
183
+ if (!allowedValues.includes(this.value)) {
184
+ return new ValidationResult(false, [
185
+ this._formatError(message || defaultMessage)
186
+ ]);
187
+ }
188
+ return new ValidationResult(true);
189
+ });
190
+ return this;
191
+ }
192
+
193
+ // Check if value is between min and max (for numbers)
194
+ between(min, max, message) {
195
+ const defaultMessage = `Must be between ${min} and ${max}`;
196
+ this.rules.push(() => {
197
+ if (
198
+ this.isOptional &&
199
+ (this.value === null || this.value === undefined || this.value === '')
200
+ ) {
201
+ return new ValidationResult(true);
202
+ }
203
+
204
+ const numValue =
205
+ typeof this.value === 'number' ? this.value : parseFloat(this.value);
206
+
207
+ if (isNaN(numValue) || numValue < min || numValue > max) {
208
+ return new ValidationResult(false, [
209
+ this._formatError(message || defaultMessage)
210
+ ]);
211
+ }
212
+ return new ValidationResult(true);
213
+ });
214
+ return this;
215
+ }
216
+
130
217
  min(length, message = `Minimum length is ${length}`) {
131
218
  this.rules.push(() => {
132
- // Skip validation if optional and empty
133
219
  if (
134
220
  this.isOptional &&
135
221
  (this.value === null || this.value === undefined || this.value === '')
@@ -137,21 +223,18 @@ class BaseValidator {
137
223
  return new ValidationResult(true);
138
224
  }
139
225
 
140
- // Only validate if value exists and a length property
141
226
  if (this.value != null && this.value !== '') {
142
- // Check if value has length property (string, array)
143
227
  if (typeof this.value === 'string' || Array.isArray(this.value)) {
144
228
  if (this.value.length < length) {
145
- return new ValidationResult(false, [message]);
229
+ return new ValidationResult(false, [this._formatError(message)]);
146
230
  }
147
231
  } else if (typeof this.value === 'number') {
148
- // For numbers, compare the value itself
149
232
  if (this.value < length) {
150
- return new ValidationResult(false, [message]);
233
+ return new ValidationResult(false, [this._formatError(message)]);
151
234
  }
152
235
  } else {
153
236
  return new ValidationResult(false, [
154
- 'Value must be a string, array, or number'
237
+ this._formatError('Value must be a string, array, or number')
155
238
  ]);
156
239
  }
157
240
  }
@@ -162,7 +245,6 @@ class BaseValidator {
162
245
 
163
246
  max(length, message = `Maximum length is ${length}`) {
164
247
  this.rules.push(() => {
165
- // Skip validation if optional and empty
166
248
  if (
167
249
  this.isOptional &&
168
250
  (this.value === null || this.value === undefined || this.value === '')
@@ -170,21 +252,18 @@ class BaseValidator {
170
252
  return new ValidationResult(true);
171
253
  }
172
254
 
173
- // Only validate if value exists and has a length property
174
255
  if (this.value != null && this.value !== '') {
175
- // Check if value has length property (string, array)
176
256
  if (typeof this.value === 'string' || Array.isArray(this.value)) {
177
257
  if (this.value.length > length) {
178
- return new ValidationResult(false, [message]);
258
+ return new ValidationResult(false, [this._formatError(message)]);
179
259
  }
180
260
  } else if (typeof this.value === 'number') {
181
- // For numbers, compare the value itself
182
261
  if (this.value > length) {
183
- return new ValidationResult(false, [message]);
262
+ return new ValidationResult(false, [this._formatError(message)]);
184
263
  }
185
264
  } else {
186
265
  return new ValidationResult(false, [
187
- 'Value must be a string, array or number'
266
+ this._formatError('Value must be a string, array or number')
188
267
  ]);
189
268
  }
190
269
  }
@@ -193,8 +272,198 @@ class BaseValidator {
193
272
  return this;
194
273
  }
195
274
 
275
+ // Validate that value is an array
276
+ array(message = 'Must be an array') {
277
+ this.rules.push(() => {
278
+ if (
279
+ this.isOptional &&
280
+ (this.value === null || this.value === undefined)
281
+ ) {
282
+ return new ValidationResult(true);
283
+ }
284
+
285
+ if (!Array.isArray(this.value)) {
286
+ return new ValidationResult(false, [this._formatError(message)]);
287
+ }
288
+ return new ValidationResult(true);
289
+ });
290
+ return this;
291
+ }
292
+
293
+ // Validate each item in an array
294
+ arrayOf(validator, message = 'Invalid array items') {
295
+ this.rules.push(() => {
296
+ if (
297
+ this.isOptional &&
298
+ (this.value === null || this.value === undefined)
299
+ ) {
300
+ return new ValidationResult(true);
301
+ }
302
+
303
+ if (!Array.isArray(this.value)) {
304
+ return new ValidationResult(false, [
305
+ this._formatError('Value must be an array')
306
+ ]);
307
+ }
308
+
309
+ const errors = [];
310
+ this.value.forEach((item, index) => {
311
+ try {
312
+ const itemValidator =
313
+ typeof validator === 'function' ? validator(item) : validator;
314
+ const result = itemValidator.validate();
315
+
316
+ if (!result.isValid) {
317
+ errors.push(`[${index}]: ${result.errors.join(', ')}`);
318
+ }
319
+ } catch (error) {
320
+ errors.push(`[${index}]: Validation error - ${error.message}`);
321
+ }
322
+ });
323
+
324
+ if (errors.length > 0) {
325
+ return new ValidationResult(false, [
326
+ this._formatError(`${message}: ${errors.join('; ')}`)
327
+ ]);
328
+ }
329
+
330
+ return new ValidationResult(true);
331
+ });
332
+ return this;
333
+ }
334
+
335
+ // Async array validation
336
+ arrayOfAsync(validator, message = 'Invalid array items') {
337
+ this.asyncRules.push(async () => {
338
+ if (
339
+ this.isOptional &&
340
+ (this.value === null || this.value === undefined)
341
+ ) {
342
+ return new ValidationResult(true);
343
+ }
344
+
345
+ if (!Array.isArray(this.value)) {
346
+ return new ValidationResult(false, [
347
+ this._formatError('Value must be an array')
348
+ ]);
349
+ }
350
+
351
+ const errors = [];
352
+ for (let index = 0; index < this.value.length; index++) {
353
+ try {
354
+ const item = this.value[index];
355
+ const itemValidator =
356
+ typeof validator === 'function' ? validator(item) : validator;
357
+
358
+ const result =
359
+ itemValidator.asyncRules && itemValidator.asyncRules.length > 0
360
+ ? await itemValidator.validateAsync()
361
+ : itemValidator.validate();
362
+
363
+ if (!result.isValid) {
364
+ errors.push(`[${index}]: ${result.errors.join(', ')}`);
365
+ }
366
+ } catch (error) {
367
+ errors.push(`[${index}]: Validation error - ${error.message}`);
368
+ }
369
+ }
370
+
371
+ if (errors.length > 0) {
372
+ return new ValidationResult(false, [
373
+ this._formatError(`${message}: ${errors.join('; ')}`)
374
+ ]);
375
+ }
376
+
377
+ return new ValidationResult(true);
378
+ });
379
+ return this;
380
+ }
381
+
382
+ // Validate nested objects
383
+ object(schema, message = 'Invalid object structure') {
384
+ this.rules.push(() => {
385
+ if (
386
+ this.isOptional &&
387
+ (this.value === null || this.value === undefined)
388
+ ) {
389
+ return new ValidationResult(true);
390
+ }
391
+
392
+ if (
393
+ typeof this.value !== 'object' ||
394
+ this.value === null ||
395
+ Array.isArray(this.value)
396
+ ) {
397
+ return new ValidationResult(false, [this._formatError(message)]);
398
+ }
399
+
400
+ try {
401
+ const result = validate(schema, this.value);
402
+
403
+ if (!result.isValid) {
404
+ const fieldErrors = result.getErrors();
405
+ const errorMessages = Object.entries(fieldErrors)
406
+ .map(([field, errs]) => `${field}: ${errs.join(', ')}`)
407
+ .join('; ');
408
+
409
+ return new ValidationResult(false, [
410
+ this._formatError(`Object validation failed - ${errorMessages}`)
411
+ ]);
412
+ }
413
+
414
+ return new ValidationResult(true);
415
+ } catch (error) {
416
+ return new ValidationResult(false, [
417
+ this._formatError(`Object validation error: ${error.message}`)
418
+ ]);
419
+ }
420
+ });
421
+ return this;
422
+ }
423
+
424
+ // Async nested object validation
425
+ objectAsync(schema, message = 'Invalid object structure') {
426
+ this.asyncRules.push(async () => {
427
+ if (
428
+ this.isOptional &&
429
+ (this.value === null || this.value === undefined)
430
+ ) {
431
+ return new ValidationResult(true);
432
+ }
433
+
434
+ if (
435
+ typeof this.value !== 'object' ||
436
+ this.value === null ||
437
+ Array.isArray(this.value)
438
+ ) {
439
+ return new ValidationResult(false, [this._formatError(message)]);
440
+ }
441
+
442
+ try {
443
+ const result = await validateAsync(schema, this.value);
444
+
445
+ if (!result.isValid) {
446
+ const fieldErrors = result.getErrors();
447
+ const errorMessages = Object.entries(fieldErrors)
448
+ .map(([field, errs]) => `${field}: ${errs.join(', ')}`)
449
+ .join('; ');
450
+
451
+ return new ValidationResult(false, [
452
+ this._formatError(`Object validation failed - ${errorMessages}`)
453
+ ]);
454
+ }
455
+
456
+ return new ValidationResult(true);
457
+ } catch (error) {
458
+ return new ValidationResult(false, [
459
+ this._formatError(`Object validation error: ${error.message}`)
460
+ ]);
461
+ }
462
+ });
463
+ return this;
464
+ }
465
+
196
466
  pattern(regex, message = 'Invalid format') {
197
- // SECURITY FIX: Add regex safety check
198
467
  if (!isRegexSafe(regex)) {
199
468
  throw new Error(
200
469
  'Potentially unsafe regex pattern detected. Please use a simple pattern.'
@@ -202,7 +471,6 @@ class BaseValidator {
202
471
  }
203
472
 
204
473
  this.rules.push(() => {
205
- // Skip validation if optional and empty
206
474
  if (
207
475
  this.isOptional &&
208
476
  (this.value === null || this.value === undefined || this.value === '')
@@ -210,23 +478,22 @@ class BaseValidator {
210
478
  return new ValidationResult(true);
211
479
  }
212
480
 
213
- // Only test pattern if value exists and is not empty
214
481
  if (this.value != null && this.value !== '') {
215
- // Ensure value is a string before testing regex
216
482
  const stringValue = String(this.value);
217
483
 
218
484
  try {
219
- // SECURITY FIX: Use safe regex test with input length protection
220
485
  if (!safeRegexTestSync(regex, stringValue)) {
221
- return new ValidationResult(false, [message]);
486
+ return new ValidationResult(false, [this._formatError(message)]);
222
487
  }
223
488
  } catch (error) {
224
489
  if (error.message.includes('Input too long')) {
225
490
  return new ValidationResult(false, [
226
- 'Input too long for pattern validation'
491
+ this._formatError('Input too long for pattern validation')
227
492
  ]);
228
493
  }
229
- return new ValidationResult(false, ['Pattern validation failed']);
494
+ return new ValidationResult(false, [
495
+ this._formatError('Pattern validation failed')
496
+ ]);
230
497
  }
231
498
  }
232
499
  return new ValidationResult(true);
@@ -234,9 +501,7 @@ class BaseValidator {
234
501
  return this;
235
502
  }
236
503
 
237
- // New method for async pattern validation with timeout protection
238
504
  patternAsync(regex, message = 'Invalid format') {
239
- // Security Fix: Add regex safety check
240
505
  if (!isRegexSafe(regex)) {
241
506
  throw new Error(
242
507
  'Potentially unsafe regex pattern detected. Please use a simple pattern.'
@@ -244,7 +509,6 @@ class BaseValidator {
244
509
  }
245
510
 
246
511
  this.asyncRules.push(async () => {
247
- // Skip validation if optional and empty
248
512
  if (
249
513
  this.isOptional &&
250
514
  (this.value === null || this.value === undefined || this.value === '')
@@ -252,35 +516,35 @@ class BaseValidator {
252
516
  return new ValidationResult(true);
253
517
  }
254
518
 
255
- // Only test pattern if value exists and is not empty
256
519
  if (this.value != null && this.value !== '') {
257
- // Ensure value is a string before testing regex
258
520
  const stringValue = String(this.value);
259
521
 
260
- // Security Fix: Limit input length to prevent ReDoS
261
522
  if (stringValue.length > 10000) {
262
523
  return new ValidationResult(false, [
263
- 'Input too long for pattern validation'
524
+ this._formatError('Input too long for pattern validation')
264
525
  ]);
265
526
  }
266
527
 
267
528
  try {
268
- // Security Fix: Use timeout protection for regex execution
269
529
  const result = await safeRegexTest(
270
530
  regex,
271
531
  stringValue,
272
532
  this.regexTimeout
273
533
  );
274
534
  if (!result) {
275
- return new ValidationResult(false, [message]);
535
+ return new ValidationResult(false, [this._formatError(message)]);
276
536
  }
277
537
  } catch (error) {
278
538
  if (error.message.includes('timeout')) {
279
539
  return new ValidationResult(false, [
280
- 'Pattern validation timeout - pattern too complex'
540
+ this._formatError(
541
+ 'Pattern validation timeout - pattern too complex'
542
+ )
281
543
  ]);
282
544
  }
283
- return new ValidationResult(false, ['Pattern validation failed']);
545
+ return new ValidationResult(false, [
546
+ this._formatError('Pattern validation failed')
547
+ ]);
284
548
  }
285
549
  }
286
550
  return new ValidationResult(true);
@@ -290,17 +554,14 @@ class BaseValidator {
290
554
 
291
555
  when(condition, validator) {
292
556
  this.rules.push(() => {
293
- // Evaluate condition
294
557
  const shouldValidate =
295
558
  typeof condition === 'function' ? condition(this.value) : condition;
296
559
 
297
560
  if (shouldValidate) {
298
- // Apply the conditional validator
299
561
  if (typeof validator === 'function') {
300
562
  const conditionalValidator = validator(this.value);
301
563
  return conditionalValidator.validate();
302
564
  } else {
303
- // If validator is already a BaseValidator instance
304
565
  return validator.validate();
305
566
  }
306
567
  }
@@ -312,7 +573,6 @@ class BaseValidator {
312
573
 
313
574
  custom(validatorFn, message = 'Custom validation failed') {
314
575
  this.rules.push(() => {
315
- // Skip validation if optional and empty
316
576
  if (
317
577
  this.isOptional &&
318
578
  (this.value === null || this.value === undefined || this.value === '')
@@ -323,28 +583,24 @@ class BaseValidator {
323
583
  try {
324
584
  const result = validatorFn(this.value);
325
585
 
326
- // Handle boolean result
327
586
  if (typeof result === 'boolean') {
328
587
  return result
329
588
  ? new ValidationResult(true)
330
- : new ValidationResult(false, [message]);
589
+ : new ValidationResult(false, [this._formatError(message)]);
331
590
  }
332
591
 
333
- // Handle ValidationResult object
334
592
  if (result && typeof result === 'object' && 'isValid' in result) {
335
593
  return result;
336
594
  }
337
595
 
338
- // Handle string result (error message)
339
596
  if (typeof result === 'string') {
340
- return new ValidationResult(false, [result]);
597
+ return new ValidationResult(false, [this._formatError(result)]);
341
598
  }
342
599
 
343
- // Default to true if no clear result
344
600
  return new ValidationResult(true);
345
601
  } catch (error) {
346
602
  return new ValidationResult(false, [
347
- `Custom validation error: ${error.message}`
603
+ this._formatError(`Custom validation error: ${error.message}`)
348
604
  ]);
349
605
  }
350
606
  });
@@ -353,7 +609,6 @@ class BaseValidator {
353
609
 
354
610
  customAsync(validatorFn, message = 'Async validation failed') {
355
611
  this.asyncRules.push(async () => {
356
- // Skip validation if optional and empty
357
612
  if (
358
613
  this.isOptional &&
359
614
  (this.value === null || this.value === undefined || this.value === '')
@@ -364,28 +619,24 @@ class BaseValidator {
364
619
  try {
365
620
  const result = await validatorFn(this.value);
366
621
 
367
- // Handle boolean result
368
622
  if (typeof result === 'boolean') {
369
623
  return result
370
624
  ? new ValidationResult(true)
371
- : new ValidationResult(false, [message]);
625
+ : new ValidationResult(false, [this._formatError(message)]);
372
626
  }
373
627
 
374
- // Handle ValidationResult object
375
628
  if (result && typeof result === 'object' && 'isValid' in result) {
376
629
  return result;
377
630
  }
378
631
 
379
- // Handle string result (error message)
380
632
  if (typeof result === 'string') {
381
- return new ValidationResult(false, [result]);
633
+ return new ValidationResult(false, [this._formatError(result)]);
382
634
  }
383
635
 
384
- // Default to true if no clear result
385
636
  return new ValidationResult(true);
386
637
  } catch (error) {
387
638
  return new ValidationResult(false, [
388
- `Async validation error: ${error.message}`
639
+ this._formatError(`Async validation error: ${error.message}`)
389
640
  ]);
390
641
  }
391
642
  });
@@ -403,9 +654,10 @@ class BaseValidator {
403
654
  result.errors.push(...ruleResult.errors);
404
655
  }
405
656
  } catch (error) {
406
- // Handle any unexpected errors during validation
407
657
  result.isValid = false;
408
- result.errors.push(`Validation error: ${error.message}`);
658
+ result.errors.push(
659
+ this._formatError(`Validation error: ${error.message}`)
660
+ );
409
661
  }
410
662
  }
411
663
 
@@ -413,14 +665,12 @@ class BaseValidator {
413
665
  }
414
666
 
415
667
  async validateAsync() {
416
- // First run synchronous validations
417
668
  const syncResult = this.validate();
418
669
 
419
670
  if (!syncResult.isValid) {
420
671
  return syncResult;
421
672
  }
422
673
 
423
- // Then run asynchronous validations
424
674
  const result = new ValidationResult(true, [...syncResult.errors]);
425
675
 
426
676
  for (const asyncRule of this.asyncRules) {
@@ -431,9 +681,10 @@ class BaseValidator {
431
681
  result.errors.push(...ruleResult.errors);
432
682
  }
433
683
  } catch (error) {
434
- // Handle any unexpected errors during async validation
435
684
  result.isValid = false;
436
- result.errors.push(`Async validation error: ${error.message}`);
685
+ result.errors.push(
686
+ this._formatError(`Async validation error: ${error.message}`)
687
+ );
437
688
  }
438
689
  }
439
690
 
@@ -442,17 +693,16 @@ class BaseValidator {
442
693
  }
443
694
 
444
695
  // Predefined validators
445
- // Security Fix: Updated predefined validators with safer regex patterns
446
696
  const validators = {
447
697
  email: (value) => {
448
698
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
449
699
  return new BaseValidator(value)
700
+ .transform((v) => (typeof v === 'string' ? v.trim().toLowerCase() : v))
450
701
  .required('Email is required')
451
702
  .pattern(emailRegex, 'Invalid email format');
452
703
  },
453
704
 
454
705
  phone: (value, format = 'us') => {
455
- // FIXED: Much simpler phone regex patterns to avoid ReDoS detection
456
706
  const phoneRegex = {
457
707
  us: /^[+]?[1]?[0-9]{10}$/,
458
708
  international: /^[+][1-9][0-9]{7,14}$/,
@@ -462,7 +712,6 @@ const validators = {
462
712
  return new BaseValidator(value)
463
713
  .required('Phone number is required')
464
714
  .custom((val) => {
465
- // Remove all non-digit characters except +
466
715
  const cleaned = String(val).replace(/[^+0-9]/g, '');
467
716
  const regex = phoneRegex[format] || phoneRegex.simple;
468
717
 
@@ -474,12 +723,10 @@ const validators = {
474
723
  },
475
724
 
476
725
  creditCard: (value) => {
477
- // Luhn algorithm check
478
726
  const luhnCheck = (num) => {
479
727
  let sum = 0;
480
728
  let isEven = false;
481
729
 
482
- // Remove spaces and ensure we have a string
483
730
  const cleanNum = String(num).replace(/\s/g, '');
484
731
 
485
732
  for (let i = cleanNum.length - 1; i >= 0; i--) {
@@ -501,9 +748,7 @@ const validators = {
501
748
  'Credit card number is required'
502
749
  );
503
750
 
504
- // Add custom validation for credit card format and Luhn check
505
751
  validator.rules.push(() => {
506
- // Skip validation if optional and empty
507
752
  if (
508
753
  validator.isOptional &&
509
754
  (validator.value === null ||
@@ -516,14 +761,12 @@ const validators = {
516
761
  if (validator.value) {
517
762
  const cleanValue = String(validator.value).replace(/\s/g, '');
518
763
 
519
- // Check length (13-19 digits) using safe regex
520
764
  if (!safeRegexTestSync(/^\d{13,19}$/, cleanValue)) {
521
765
  return new ValidationResult(false, [
522
766
  'Credit card must be 13-19 digits'
523
767
  ]);
524
768
  }
525
769
 
526
- // Check Luhn algorithm
527
770
  if (!luhnCheck(cleanValue)) {
528
771
  return new ValidationResult(false, ['Invalid credit card number']);
529
772
  }
@@ -606,7 +849,6 @@ const validators = {
606
849
 
607
850
  // Main validation function
608
851
  const validate = (schema, data) => {
609
- // Input validation
610
852
  if (!schema || typeof schema !== 'object') {
611
853
  throw new Error('Schema must be a valid object');
612
854
  }
@@ -621,19 +863,21 @@ const validate = (schema, data) => {
621
863
  for (const [field, validator] of Object.entries(schema)) {
622
864
  try {
623
865
  const fieldValue = data[field];
624
- const result =
625
- typeof validator === 'function'
626
- ? validator(fieldValue).validate()
627
- : validator.validate();
866
+ const validatorInstance =
867
+ typeof validator === 'function' ? validator(fieldValue) : validator;
868
+
869
+ // Set field name for better error context
870
+ validatorInstance.setFieldName(field);
871
+
872
+ const result = validatorInstance.validate();
628
873
 
629
874
  results[field] = result;
630
875
  if (!result.isValid) {
631
876
  isValid = false;
632
877
  }
633
878
  } catch (error) {
634
- // Handle validation setup errors
635
879
  results[field] = new ValidationResult(false, [
636
- `Validation setup error: ${error.message}`
880
+ `${field}: Validation setup error - ${error.message}`
637
881
  ]);
638
882
  isValid = false;
639
883
  }
@@ -656,7 +900,6 @@ const validate = (schema, data) => {
656
900
 
657
901
  // Async validation function
658
902
  const validateAsync = async (schema, data) => {
659
- // Input validation
660
903
  if (!schema || typeof schema !== 'object') {
661
904
  throw new Error('Schema must be a valid object');
662
905
  }
@@ -672,28 +915,24 @@ const validateAsync = async (schema, data) => {
672
915
  try {
673
916
  const fieldValue = data[field];
674
917
 
675
- let result;
676
- if (typeof validator === 'function') {
677
- const validatorInstance = validator(fieldValue);
678
- result =
679
- validatorInstance.asyncRules.length > 0
680
- ? await validatorInstance.validateAsync()
681
- : validatorInstance.validate();
682
- } else {
683
- result =
684
- validator.asyncRules && validator.asyncRules.length > 0
685
- ? await validator.validateAsync()
686
- : validator.validate();
687
- }
918
+ const validatorInstance =
919
+ typeof validator === 'function' ? validator(fieldValue) : validator;
920
+
921
+ // Set field name for better error context
922
+ validatorInstance.setFieldName(field);
923
+
924
+ const result =
925
+ validatorInstance.asyncRules && validatorInstance.asyncRules.length > 0
926
+ ? await validatorInstance.validateAsync()
927
+ : validatorInstance.validate();
688
928
 
689
929
  results[field] = result;
690
930
  if (!result.isValid) {
691
931
  isValid = false;
692
932
  }
693
933
  } catch (error) {
694
- // Handle validation setup errors
695
934
  results[field] = new ValidationResult(false, [
696
- `Validation setup error: ${error.message}`
935
+ `${field}: Validation setup error - ${error.message}`
697
936
  ]);
698
937
  isValid = false;
699
938
  }
@@ -723,4 +962,4 @@ module.exports = {
723
962
  safeRegexTest,
724
963
  safeRegexTestSync,
725
964
  isRegexSafe
726
- };
965
+ };