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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/index.js +347 -108
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://packagephobia.now.sh/result?p=snap-validate)
|
|
7
7
|
[](https://bundlephobia.com/package/snap-validate@latest)
|
|
8
8
|
[](https://npm-stat.com/charts.html?package=snap-validate)
|
|
9
|
+

|
|
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
package/src/index.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Snap Validate - Enhanced Lightweight validator library
|
|
3
|
-
* @version 0.
|
|
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;
|
|
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, [
|
|
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
|
-
|
|
540
|
+
this._formatError(
|
|
541
|
+
'Pattern validation timeout - pattern too complex'
|
|
542
|
+
)
|
|
281
543
|
]);
|
|
282
544
|
}
|
|
283
|
-
return new ValidationResult(false, [
|
|
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(
|
|
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(
|
|
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
|
|
625
|
-
typeof validator === 'function'
|
|
626
|
-
|
|
627
|
-
|
|
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
|
-
|
|
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
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
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
|
+
};
|