snap-validate 0.2.1 โ†’ 0.3.1

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 CHANGED
@@ -1,11 +1,13 @@
1
1
  # Snap Validate โšก
2
2
 
3
- [![npm version](https://badge.fury.io/js/snap-validate.svg)](https://badge.fury.io/js/snap-validate)
3
+ [![npm version](https://img.shields.io/npm/v/snap-validate.svg?style=flat-square)](https://www.npmjs.com/package/snap-validate)
4
4
  [![Build Status](https://github.com/aniru-dh21/snap-validate/workflows/CI%2FCD%20Pipeline/badge.svg)](https://github.com/aniru-dh21/snap-validate/actions)
5
- [![Coverage Status](https://codecov.io/gh/aniru-dh21/snap-validate/branch/main/graph/badge.svg)](https://codecov.io/gh/aniru-dh21/snap-validate)
6
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
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
+ [![npm bundle size](https://img.shields.io/bundlephobia/minzip/snap-validate?style=flat-square)](https://bundlephobia.com/package/snap-validate@latest)
8
+ [![npm downloads](https://img.shields.io/npm/dm/snap-validate.svg?style=flat-square)](https://npm-stat.com/charts.html?package=snap-validate)
7
9
 
8
- A lightning-fast, lightweight validation library for common patterns without heavy dependencies. Perfect for client-side and server-side validation with zero external dependencies.
10
+ 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.
9
11
 
10
12
  ## Features
11
13
 
@@ -14,6 +16,11 @@ A lightning-fast, lightweight validation library for common patterns without hea
14
16
  - ๐Ÿ”ง **Flexible**: Chainable validation rules and custom validators
15
17
  - ๐Ÿ“ง **Common Patterns**: Email, phone, credit card, URL, password validation
16
18
  - ๐ŸŒ **International**: Support for different formats (US/International phone, postal codes)
19
+ - ๐Ÿ”„ **Async Support**: Full async validation support for database checks and API calls
20
+ - ๐ŸŽฏ **Conditional**: Advanced conditional validation with `when()` and `optional()`
21
+ - ๐Ÿ› ๏ธ **Custom Validators**: Add your own sync and async validation logic
22
+ - ๐Ÿ”’ **Security First**: Built-in protection against ReDoS attacks and unsafe regex patterns
23
+ - ๐Ÿ›ก๏ธ **Timeout Protection**: Configurable timeout for regex operations to prevent DoS attacks
17
24
  - ๐Ÿงช **Well Tested**: Comprehensive test suite with high coverage
18
25
  - ๐Ÿ“ฆ **Easy Integration**: Works in Node.js and browsers
19
26
  - ๐Ÿ”— **Chainable API**: Intuitive fluent interface
@@ -50,6 +57,30 @@ const result = validate(schema, data);
50
57
  console.log(result.isValid); // true
51
58
  ```
52
59
 
60
+ ## Security Features
61
+
62
+ ### ReDoS Protection
63
+
64
+ Snap Validate includes built-in protection against Regular Expression Denial of Service (ReDoS) attacks:
65
+
66
+ - **Regex Safety Detection**: Automatically detects and prevents potentially dangerous regex patterns
67
+ - **Input Length Limits**: Protects against extremely long input strings (10,000 character limit)
68
+ - **Timeout Protection**: Configurable timeout for regex operations (default: 1 second)
69
+ - **Safe Defaults**: All predefined validators use safe, optimized regex patterns
70
+
71
+ ```javascript
72
+ // Set custom timeout for regex operations
73
+ const validator = new BaseValidator(value)
74
+ .setRegexTimeout(2000) // 2 second timeout
75
+ .pattern(/your-pattern/, 'Error message');
76
+
77
+ // Use async pattern validation for complex patterns with timeout protection
78
+ const validator = new BaseValidator(value)
79
+ .patternAsync(/complex-pattern/, 'Error message');
80
+
81
+ const result = await validator.validateAsync();
82
+ ```
83
+
53
84
  ## Available Validators
54
85
 
55
86
  ### Email Validation
@@ -126,6 +157,122 @@ validators.zipCode('K1A 0A6', 'ca').validate();
126
157
  validators.zipCode('SW1A 1AA', 'uk').validate();
127
158
  ```
128
159
 
160
+ ## Advanced Validation Features
161
+
162
+ ### Conditional Validation
163
+
164
+ ```javascript
165
+ const { BaseValidator } = require('snap-validate');
166
+
167
+ // Validate only when condition is met
168
+ const validator = new BaseValidator(value)
169
+ .when(user.isAdmin, validators.required('Admin field required'))
170
+ .min(5, 'Must be at least 5 characters');
171
+
172
+ // Optional validation - skip if empty/null/undefined
173
+ const optionalValidator = new BaseValidator(value)
174
+ .optional()
175
+ .email('Must be a valid email if provided');
176
+
177
+ // Function-based conditions
178
+ const conditionalValidator = new BaseValidator(value)
179
+ .when(() => user.role === 'admin', validators.required())
180
+ .max(100);
181
+ ```
182
+
183
+ ### Custom Validators
184
+
185
+ ```javascript
186
+ const { BaseValidator } = require('snap-validate');
187
+
188
+ // Synchronous custom validation
189
+ const customValidator = new BaseValidator(value)
190
+ .custom((val) => val !== 'forbidden', 'Value cannot be forbidden')
191
+ .custom((val) => {
192
+ if (val.includes('admin') && !user.isAdmin) {
193
+ return 'Only admins can use this value';
194
+ }
195
+ return true;
196
+ });
197
+
198
+ // Asynchronous custom validation
199
+ const asyncValidator = new BaseValidator(email)
200
+ .email()
201
+ .customAsync(async (email) => {
202
+ const exists = await checkEmailExists(email);
203
+ return !exists || 'Email already exists';
204
+ }, 'Email validation failed');
205
+
206
+ // Use async validation
207
+ const result = await asyncValidator.validateAsync();
208
+ ```
209
+
210
+ ### Async Validation
211
+
212
+ ```javascript
213
+ // Async validation for single field
214
+ const validator = new BaseValidator(username)
215
+ .required()
216
+ .min(3)
217
+ .customAsync(async (username) => {
218
+ const available = await checkUsernameAvailable(username);
219
+ return available || 'Username is already taken';
220
+ });
221
+
222
+ const result = await validator.validateAsync();
223
+
224
+ // Async schema validation
225
+ const asyncSchema = {
226
+ username: (value) => new BaseValidator(value)
227
+ .required()
228
+ .customAsync(async (val) => {
229
+ const available = await checkUsernameAvailable(val);
230
+ return available || 'Username taken';
231
+ }),
232
+
233
+ email: (value) => validators.email(value)
234
+ .customAsync(async (val) => {
235
+ const exists = await checkEmailExists(val);
236
+ return !exists || 'Email already registered';
237
+ })
238
+ };
239
+
240
+ const asyncResult = await validate.async(asyncSchema, userData);
241
+ ```
242
+
243
+ ## Security and Pattern Validation
244
+
245
+ ### Safe Pattern Validation
246
+
247
+ ```javascript
248
+ const { BaseValidator } = require('snap-validate');
249
+
250
+ // Synchronous pattern validation with built-in safety checks
251
+ const validator = new BaseValidator(value)
252
+ .pattern(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed');
253
+
254
+ // Asynchronous pattern validation with timeout protection
255
+ const asyncValidator = new BaseValidator(value)
256
+ .patternAsync(/^[a-zA-Z0-9]+$/, 'Only alphanumeric characters allowed')
257
+ .setRegexTimeout(5000); // 5 second timeout
258
+
259
+ const result = await asyncValidator.validateAsync();
260
+ ```
261
+
262
+ ### Configurable Security Settings
263
+
264
+ ```javascript
265
+ const validator = new BaseValidator(value)
266
+ .setRegexTimeout(3000) // Set custom timeout (3 seconds)
267
+ .pattern(/your-pattern/, 'Error message');
268
+
269
+ // The library automatically:
270
+ // - Detects unsafe regex patterns
271
+ // - Limits input length to prevent ReDoS
272
+ // - Applies timeout protection for complex patterns
273
+ // - Provides clear error messages for security violations
274
+ ```
275
+
129
276
  ## Custom Validation
130
277
 
131
278
  ### Using BaseValidator
@@ -157,6 +304,7 @@ const schema = {
157
304
  age: (value) => new BaseValidator(value)
158
305
  .required()
159
306
  .pattern(/^\d+$/, 'Age must be a number')
307
+ .custom((val) => parseInt(val) >= 18, 'Must be 18 or older')
160
308
  };
161
309
 
162
310
  const userData = {
@@ -185,6 +333,23 @@ if (!schemaResult.isValid) {
185
333
  console.log('Field errors:', errors);
186
334
  // Output: { email: ['Invalid email format'], password: ['Password too weak'] }
187
335
  }
336
+
337
+ // Async error handling
338
+ try {
339
+ const asyncResult = await validator.validateAsync();
340
+ if (!asyncResult.isValid) {
341
+ console.log('Async validation errors:', asyncResult.errors);
342
+ }
343
+ } catch (error) {
344
+ console.log('Validation exception:', error.message);
345
+ }
346
+
347
+ // Security-related errors
348
+ const unsafeResult = validator.pattern(/potentially-dangerous-pattern/, 'Error').validate();
349
+ if (!unsafeResult.isValid) {
350
+ console.log('Security errors:', unsafeResult.errors);
351
+ // Output: ['Potentially unsafe regex pattern detected']
352
+ }
188
353
  ```
189
354
 
190
355
  ## Browser Usage
@@ -211,8 +376,15 @@ if (!schemaResult.isValid) {
211
376
  - `required(message?)` - Field is required
212
377
  - `min(length, message?)` - Minimum length validation
213
378
  - `max(length, message?)` - Maximum length validation
214
- - `pattern(regex, message?)` - Pattern matching validation
215
- - `validate()` - Execute validation and return result
379
+ - `pattern(regex, message?)` - Pattern matching validation with safety checks
380
+ - `patternAsync(regex, message?)` - Async pattern validation with timeout protection
381
+ - `setRegexTimeout(timeoutMs)` - Set custom timeout for regex operations
382
+ - `when(condition, validator)` - Conditional validation
383
+ - `optional()` - Skip validation if empty/null/undefined
384
+ - `custom(fn, message?)` - Custom synchronous validation
385
+ - `customAsync(fn, message?)` - Custom asynchronous validation
386
+ - `validate()` - Execute synchronous validation
387
+ - `validateAsync()` - Execute asynchronous validation
216
388
 
217
389
  ### Available Validators
218
390
 
@@ -225,6 +397,23 @@ if (!schemaResult.isValid) {
225
397
  - `validators.numeric(value)`
226
398
  - `validators.zipCode(value, country?)`
227
399
 
400
+ ### Validation Functions
401
+
402
+ - `validate(schema, data)` - Synchronous schema validation
403
+ - `validate.async(schema, data)` - Asynchronous schema validation
404
+
405
+ ### Security Functions
406
+
407
+ - `isRegexSafe(regex)` - Check if a regex pattern is safe to use
408
+ - `safeRegexText(regex, str, timeoutMs)` - Execute regex with timeout protection
409
+
410
+ ## Security Best Practices
411
+
412
+ 1. **Use Built-in Validators**: The predefined validators are optimized for security and performance
413
+ 2. **Validate Input Length**: Large inputs are automatically limited to prevent ReDoS attacks
414
+ 3. **Set Appropriate Timeouts**: Configure regex timeouts based on your application's needs
415
+ 4. **Test Custom Patterns**: Use `isRegexSafe()` to check custom regex patterns before deployment
416
+ 5. **Handle Async Errors**: Always use try-catch blocks with async validation
228
417
 
229
418
  ## Contributing
230
419
 
@@ -256,6 +445,9 @@ npm run lint
256
445
 
257
446
  # Format code
258
447
  npm run format
448
+
449
+ # Security audit
450
+ npm audit
259
451
  ```
260
452
 
261
453
  ## License
@@ -264,15 +456,8 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
264
456
 
265
457
  ## Changelog
266
458
 
267
- ### v0.1.0
268
-
269
- - Initial release
270
- - Basic validation patterns (email, phone, credit card, URL, password)
271
- - Schema validation support
272
- - Comprehensive test suite
273
- - CI/CD pipeline setup
274
- - Lightning-fast performance optimizations
459
+ See [CHANGELOG.md](CHANGELOG.md) for a detailed history of changes.
275
460
 
276
461
  ---
277
462
 
278
- Made with โšก by [Ramachandra Anirudh Vemulapalli](https://github.com/aniru-dh21)
463
+ Made with โšก by [Ramachandra Anirudh Vemulapalli](https://github.com/aniru-dh21)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "snap-validate",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
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,8 +1,77 @@
1
1
  /**
2
- * Snap Validate - Lightweight validator library
3
- * @version 0.0.1
2
+ * Snap Validate - Enhanced Lightweight validator library
3
+ * @version 0.3.1 - Security Fixes
4
4
  */
5
5
 
6
+ // Utility function to safely test regex with timeout protection
7
+ // Utility function to safely test regex with timeout protection
8
+ const safeRegexTest = (regex, str, timeoutMs = 1000) => {
9
+ return new Promise((resolve, reject) => {
10
+ // SECURITY FIX: Add input length validation before regex test
11
+ if (str.length > 10000) {
12
+ reject(new Error('Input too long for regex validation'));
13
+ return;
14
+ }
15
+
16
+ // SECURITY FIX: Add regex safety check before execution
17
+ if (!isRegexSafe(regex)) {
18
+ reject(new Error('Unsafe regex pattern detected'));
19
+ return;
20
+ }
21
+
22
+ const timeout = setTimeout(() => {
23
+ reject(new Error('Regex execution timeout - potential ReDoS attack'));
24
+ }, timeoutMs);
25
+
26
+ try {
27
+ const result = regex.test(str);
28
+ clearTimeout(timeout);
29
+ resolve(result);
30
+ } catch (error) {
31
+ clearTimeout(timeout);
32
+ reject(error);
33
+ }
34
+ });
35
+ };
36
+
37
+ // Synchronous safe regex test with input length protection
38
+ const safeRegexTestSync = (regex, str, maxLength = 10000) => {
39
+ // Limit input length to prevent ReDoS
40
+ if (str.length > maxLength) {
41
+ throw new Error('Input too long for pattern validation');
42
+ }
43
+
44
+ // For additional safety, we could add a timeout using a worker thread or
45
+ // other mechanism, but for now we rely on input length limiting
46
+ return regex.test(str);
47
+ };
48
+
49
+ // Function to detect potentially dangerous regex patterns
50
+ const isRegexSafe = (regex) => {
51
+ const regexStr = regex.toString();
52
+
53
+ // Check for common ReDoS patterns - more precise detection
54
+ const dangerousPatterns = [
55
+ // Nested quantifiers like (a+)+ or (a*)* or (a?)?
56
+ /\([^)]*[+*?][^)]*\)[+*?]/,
57
+ // Alternation with overlapping and quantifiers like (a|a)*
58
+ /\([^)]*\|[^)]*\)[+*]/,
59
+ // Catastrophic backtracking with greedy quantifiers
60
+ /\([^)]*\.\*[^)]*\)\*/,
61
+ // Multiple consecutive quantifiers (not separated by characters)
62
+ /[+*?]{2,}/,
63
+ // Exponential alternation patterns
64
+ /\([^)]*\|[^)]*\)\+.*\([^)]*\|[^)]*\)\+/
65
+ ];
66
+
67
+ // Check if the pattern has obvious ReDoS vulnerabilities
68
+ const isDangerous = dangerousPatterns.some((pattern) =>
69
+ pattern.test(regexStr)
70
+ );
71
+
72
+ return !isDangerous;
73
+ };
74
+
6
75
  // Core validation class
7
76
  class ValidationResult {
8
77
  constructor(isValid, errors = []) {
@@ -22,11 +91,26 @@ class BaseValidator {
22
91
  constructor(value) {
23
92
  this.value = value;
24
93
  this.rules = [];
94
+ this.asyncRules = [];
95
+ this.isOptional = false;
96
+ this.regexTimeout = 1000; // Default timeout for regex operations
25
97
  }
26
98
 
27
99
  required(message = 'This field is required') {
28
100
  this.rules.push(() => {
29
- if (this.value === null || this.value === undefined || this.value === '') {
101
+ // Skip validation if optional and empty
102
+ if (
103
+ this.isOptional &&
104
+ (this.value === null || this.value === undefined || this.value === '')
105
+ ) {
106
+ return new ValidationResult(true);
107
+ }
108
+
109
+ if (
110
+ this.value === null ||
111
+ this.value === undefined ||
112
+ this.value === ''
113
+ ) {
30
114
  return new ValidationResult(false, [message]);
31
115
  }
32
116
  return new ValidationResult(true);
@@ -34,10 +118,43 @@ class BaseValidator {
34
118
  return this;
35
119
  }
36
120
 
121
+ optional() {
122
+ this.isOptional = true;
123
+ return this;
124
+ }
125
+
126
+ setRegexTimeout(timeoutMs) {
127
+ this.regexTimeout = timeoutMs;
128
+ return this;
129
+ }
130
+
37
131
  min(length, message = `Minimum length is ${length}`) {
38
132
  this.rules.push(() => {
39
- if (this.value && this.value.length < length) {
40
- return new ValidationResult(false, [message]);
133
+ // Skip validation if optional and empty
134
+ if (
135
+ this.isOptional &&
136
+ (this.value === null || this.value === undefined || this.value === '')
137
+ ) {
138
+ return new ValidationResult(true);
139
+ }
140
+
141
+ // Only validate if value exists and a length property
142
+ if (this.value != null && this.value !== '') {
143
+ // Check if value has length property (string, array)
144
+ if (typeof this.value === 'string' || Array.isArray(this.value)) {
145
+ if (this.value.length < length) {
146
+ return new ValidationResult(false, [message]);
147
+ }
148
+ } else if (typeof this.value === 'number') {
149
+ // For numbers, compare the value itself
150
+ if (this.value < length) {
151
+ return new ValidationResult(false, [message]);
152
+ }
153
+ } else {
154
+ return new ValidationResult(false, [
155
+ 'Value must be a string, array, or number'
156
+ ]);
157
+ }
41
158
  }
42
159
  return new ValidationResult(true);
43
160
  });
@@ -46,8 +163,31 @@ class BaseValidator {
46
163
 
47
164
  max(length, message = `Maximum length is ${length}`) {
48
165
  this.rules.push(() => {
49
- if (this.value && this.value.length > length) {
50
- return new ValidationResult(false, [message]);
166
+ // Skip validation if optional and empty
167
+ if (
168
+ this.isOptional &&
169
+ (this.value === null || this.value === undefined || this.value === '')
170
+ ) {
171
+ return new ValidationResult(true);
172
+ }
173
+
174
+ // Only validate if value exists and has a length property
175
+ if (this.value != null && this.value !== '') {
176
+ // Check if value has length property (string, array)
177
+ if (typeof this.value === 'string' || Array.isArray(this.value)) {
178
+ if (this.value.length > length) {
179
+ return new ValidationResult(false, [message]);
180
+ }
181
+ } else if (typeof this.value === 'number') {
182
+ // For numbers, compare the value itself
183
+ if (this.value > length) {
184
+ return new ValidationResult(false, [message]);
185
+ }
186
+ } else {
187
+ return new ValidationResult(false, [
188
+ 'Value must be a string, array or number'
189
+ ]);
190
+ }
51
191
  }
52
192
  return new ValidationResult(true);
53
193
  });
@@ -55,23 +195,246 @@ class BaseValidator {
55
195
  }
56
196
 
57
197
  pattern(regex, message = 'Invalid format') {
198
+ // SECURITY FIX: Add regex safety check
199
+ if (!isRegexSafe(regex)) {
200
+ throw new Error(
201
+ 'Potentially unsafe regex pattern detected. Please use a simple pattern.'
202
+ );
203
+ }
204
+
58
205
  this.rules.push(() => {
59
- if (this.value && !regex.test(this.value)) {
60
- return new ValidationResult(false, [message]);
206
+ // Skip validation if optional and empty
207
+ if (
208
+ this.isOptional &&
209
+ (this.value === null || this.value === undefined || this.value === '')
210
+ ) {
211
+ return new ValidationResult(true);
212
+ }
213
+
214
+ // Only test pattern if value exists and is not empty
215
+ if (this.value != null && this.value !== '') {
216
+ // Ensure value is a string before testing regex
217
+ const stringValue = String(this.value);
218
+
219
+ try {
220
+ // SECURITY FIX: Use safe regex test with input length protection
221
+ if (!safeRegexTestSync(regex, stringValue)) {
222
+ return new ValidationResult(false, [message]);
223
+ }
224
+ } catch (error) {
225
+ if (error.message.includes('Input too long')) {
226
+ return new ValidationResult(false, [
227
+ 'Input too long for pattern validation'
228
+ ]);
229
+ }
230
+ return new ValidationResult(false, ['Pattern validation failed']);
231
+ }
232
+ }
233
+ return new ValidationResult(true);
234
+ });
235
+ return this;
236
+ }
237
+
238
+ // New method for async pattern validation with timeout protection
239
+ patternAsync(regex, message = 'Invalid format') {
240
+ // Security Fix: Add regex safety check
241
+ if (!isRegexSafe(regex)) {
242
+ throw new Error(
243
+ 'Potentially unsafe regex pattern detected. Please use a simple pattern.'
244
+ );
245
+ }
246
+
247
+ this.asyncRules.push(async () => {
248
+ // Skip validation if optional and empty
249
+ if (
250
+ this.isOptional &&
251
+ (this.value === null || this.value === undefined || this.value === '')
252
+ ) {
253
+ return new ValidationResult(true);
254
+ }
255
+
256
+ // Only test pattern if value exists and is not empty
257
+ if (this.value != null && this.value !== '') {
258
+ // Ensure value is a string before testing regex
259
+ const stringValue = String(this.value);
260
+
261
+ // Security Fix: Limit input length to prevent ReDoS
262
+ if (stringValue.length > 10000) {
263
+ return new ValidationResult(false, [
264
+ 'Input too long for pattern validation'
265
+ ]);
266
+ }
267
+
268
+ try {
269
+ // Security Fix: Use timeout protection for regex execution
270
+ const result = await safeRegexTest(
271
+ regex,
272
+ stringValue,
273
+ this.regexTimeout
274
+ );
275
+ if (!result) {
276
+ return new ValidationResult(false, [message]);
277
+ }
278
+ } catch (error) {
279
+ if (error.message.includes('timeout')) {
280
+ return new ValidationResult(false, [
281
+ 'Pattern validation timeout - pattern too complex'
282
+ ]);
283
+ }
284
+ return new ValidationResult(false, ['Pattern validation failed']);
285
+ }
286
+ }
287
+ return new ValidationResult(true);
288
+ });
289
+ return this;
290
+ }
291
+
292
+ when(condition, validator) {
293
+ this.rules.push(() => {
294
+ // Evaluate condition
295
+ const shouldValidate =
296
+ typeof condition === 'function' ? condition(this.value) : condition;
297
+
298
+ if (shouldValidate) {
299
+ // Apply the conditional validator
300
+ if (typeof validator === 'function') {
301
+ const conditionalValidator = validator(this.value);
302
+ return conditionalValidator.validate();
303
+ } else {
304
+ // If validator is already a BaseValidator instance
305
+ return validator.validate();
306
+ }
61
307
  }
308
+
62
309
  return new ValidationResult(true);
63
310
  });
64
311
  return this;
65
312
  }
66
313
 
314
+ custom(validatorFn, message = 'Custom validation failed') {
315
+ this.rules.push(() => {
316
+ // Skip validation if optional and empty
317
+ if (
318
+ this.isOptional &&
319
+ (this.value === null || this.value === undefined || this.value === '')
320
+ ) {
321
+ return new ValidationResult(true);
322
+ }
323
+
324
+ try {
325
+ const result = validatorFn(this.value);
326
+
327
+ // Handle boolean result
328
+ if (typeof result === 'boolean') {
329
+ return result
330
+ ? new ValidationResult(true)
331
+ : new ValidationResult(false, [message]);
332
+ }
333
+
334
+ // Handle ValidationResult object
335
+ if (result && typeof result === 'object' && 'isValid' in result) {
336
+ return result;
337
+ }
338
+
339
+ // Handle string result (error message)
340
+ if (typeof result === 'string') {
341
+ return new ValidationResult(false, [result]);
342
+ }
343
+
344
+ // Default to true if no clear result
345
+ return new ValidationResult(true);
346
+ } catch (error) {
347
+ return new ValidationResult(false, [
348
+ `Custom validation error: ${error.message}`
349
+ ]);
350
+ }
351
+ });
352
+ return this;
353
+ }
354
+
355
+ customAsync(validatorFn, message = 'Async validation failed') {
356
+ this.asyncRules.push(async () => {
357
+ // Skip validation if optional and empty
358
+ if (
359
+ this.isOptional &&
360
+ (this.value === null || this.value === undefined || this.value === '')
361
+ ) {
362
+ return new ValidationResult(true);
363
+ }
364
+
365
+ try {
366
+ const result = await validatorFn(this.value);
367
+
368
+ // Handle boolean result
369
+ if (typeof result === 'boolean') {
370
+ return result
371
+ ? new ValidationResult(true)
372
+ : new ValidationResult(false, [message]);
373
+ }
374
+
375
+ // Handle ValidationResult object
376
+ if (result && typeof result === 'object' && 'isValid' in result) {
377
+ return result;
378
+ }
379
+
380
+ // Handle string result (error message)
381
+ if (typeof result === 'string') {
382
+ return new ValidationResult(false, [result]);
383
+ }
384
+
385
+ // Default to true if no clear result
386
+ return new ValidationResult(true);
387
+ } catch (error) {
388
+ return new ValidationResult(false, [
389
+ `Async validation error: ${error.message}`
390
+ ]);
391
+ }
392
+ });
393
+ return this;
394
+ }
395
+
67
396
  validate() {
68
397
  const result = new ValidationResult(true);
69
398
 
70
399
  for (const rule of this.rules) {
71
- const ruleResult = rule();
72
- if (!ruleResult.isValid) {
400
+ try {
401
+ const ruleResult = rule();
402
+ if (!ruleResult.isValid) {
403
+ result.isValid = false;
404
+ result.errors.push(...ruleResult.errors);
405
+ }
406
+ } catch (error) {
407
+ // Handle any unexpected errors during validation
408
+ result.isValid = false;
409
+ result.errors.push(`Validation error: ${error.message}`);
410
+ }
411
+ }
412
+
413
+ return result;
414
+ }
415
+
416
+ async validateAsync() {
417
+ // First run synchronous validations
418
+ const syncResult = this.validate();
419
+
420
+ if (!syncResult.isValid) {
421
+ return syncResult;
422
+ }
423
+
424
+ // Then run asynchronous validations
425
+ const result = new ValidationResult(true, [...syncResult.errors]);
426
+
427
+ for (const asyncRule of this.asyncRules) {
428
+ try {
429
+ const ruleResult = await asyncRule();
430
+ if (!ruleResult.isValid) {
431
+ result.isValid = false;
432
+ result.errors.push(...ruleResult.errors);
433
+ }
434
+ } catch (error) {
435
+ // Handle any unexpected errors during async validation
73
436
  result.isValid = false;
74
- result.errors.push(...ruleResult.errors);
437
+ result.errors.push(`Async validation error: ${error.message}`);
75
438
  }
76
439
  }
77
440
 
@@ -80,6 +443,7 @@ class BaseValidator {
80
443
  }
81
444
 
82
445
  // Predefined validators
446
+ // Security Fix: Updated predefined validators with safer regex patterns
83
447
  const validators = {
84
448
  email: (value) => {
85
449
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@@ -89,15 +453,25 @@ const validators = {
89
453
  },
90
454
 
91
455
  phone: (value, format = 'us') => {
456
+ // FIXED: Much simpler phone regex patterns to avoid ReDoS detection
92
457
  const phoneRegex = {
93
- us: /^\+?1?[-.\s]?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})$/,
94
- international: /^\+[1-9]\d{1,14}$/,
95
- simple: /^\d{10,15}$/
458
+ us: /^[+]?[1]?[0-9]{10}$/,
459
+ international: /^[+][1-9][0-9]{7,14}$/,
460
+ simple: /^[0-9]{10,15}$/
96
461
  };
97
462
 
98
463
  return new BaseValidator(value)
99
464
  .required('Phone number is required')
100
- .pattern(phoneRegex[format] || phoneRegex.simple, 'Invalid phone number format');
465
+ .custom((val) => {
466
+ // Remove all non-digit characters except +
467
+ const cleaned = String(val).replace(/[^+0-9]/g, '');
468
+ const regex = phoneRegex[format] || phoneRegex.simple;
469
+
470
+ if (!safeRegexTestSync(regex, cleaned)) {
471
+ return 'Invalid phone number format';
472
+ }
473
+ return true;
474
+ });
101
475
  },
102
476
 
103
477
  creditCard: (value) => {
@@ -106,8 +480,11 @@ const validators = {
106
480
  let sum = 0;
107
481
  let isEven = false;
108
482
 
109
- for (let i = num.length - 1; i >= 0; i--) {
110
- let digit = parseInt(num[i]);
483
+ // Remove spaces and ensure we have a string
484
+ const cleanNum = String(num).replace(/\s/g, '');
485
+
486
+ for (let i = cleanNum.length - 1; i >= 0; i--) {
487
+ let digit = parseInt(cleanNum[i]);
111
488
 
112
489
  if (isEven) {
113
490
  digit *= 2;
@@ -121,13 +498,36 @@ const validators = {
121
498
  return sum % 10 === 0;
122
499
  };
123
500
 
124
- const validator = new BaseValidator(value)
125
- .required('Credit card number is required')
126
- .pattern(/^\d{13,19}$/, 'Credit card must be 13-19 digits');
501
+ const validator = new BaseValidator(value).required(
502
+ 'Credit card number is required'
503
+ );
127
504
 
505
+ // Add custom validation for credit card format and Luhn check
128
506
  validator.rules.push(() => {
129
- if (value && !luhnCheck(value.replace(/\s/g, ''))) {
130
- return new ValidationResult(false, ['Invalid credit card number']);
507
+ // Skip validation if optional and empty
508
+ if (
509
+ validator.isOptional &&
510
+ (validator.value === null ||
511
+ validator.value === undefined ||
512
+ validator.value === '')
513
+ ) {
514
+ return new ValidationResult(true);
515
+ }
516
+
517
+ if (validator.value) {
518
+ const cleanValue = String(validator.value).replace(/\s/g, '');
519
+
520
+ // Check length (13-19 digits) using safe regex
521
+ if (!safeRegexTestSync(/^\d{13,19}$/, cleanValue)) {
522
+ return new ValidationResult(false, [
523
+ 'Credit card must be 13-19 digits'
524
+ ]);
525
+ }
526
+
527
+ // Check Luhn algorithm
528
+ if (!luhnCheck(cleanValue)) {
529
+ return new ValidationResult(false, ['Invalid credit card number']);
530
+ }
131
531
  }
132
532
  return new ValidationResult(true);
133
533
  });
@@ -156,16 +556,25 @@ const validators = {
156
556
  .min(minLength, `Password must be at least ${minLength} characters`);
157
557
 
158
558
  if (requireUppercase) {
159
- validator.pattern(/[A-Z]/, 'Password must contain at least one uppercase letter');
559
+ validator.pattern(
560
+ /[A-Z]/,
561
+ 'Password must contain at least one uppercase letter'
562
+ );
160
563
  }
161
564
  if (requireLowercase) {
162
- validator.pattern(/[a-z]/, 'Password must contain at least one lowercase letter');
565
+ validator.pattern(
566
+ /[a-z]/,
567
+ 'Password must contain at least one lowercase letter'
568
+ );
163
569
  }
164
570
  if (requireNumbers) {
165
571
  validator.pattern(/\d/, 'Password must contain at least one number');
166
572
  }
167
573
  if (requireSpecialChars) {
168
- validator.pattern(/[!@#$%^&*(),.?":{}|<>]/, 'Password must contain at least one special character');
574
+ validator.pattern(
575
+ /[!@#$%^&*(),.?":{}|<>]/,
576
+ 'Password must contain at least one special character'
577
+ );
169
578
  }
170
579
 
171
580
  return validator;
@@ -174,7 +583,7 @@ const validators = {
174
583
  alphanumeric: (value) => {
175
584
  return new BaseValidator(value)
176
585
  .required('This field is required')
177
- .pattern(/^[a-zA-Z0-9]+$/, 'Only letters and number are allowed');
586
+ .pattern(/^[a-zA-Z0-9]+$/, 'Only letters and numbers are allowed');
178
587
  },
179
588
 
180
589
  numeric: (value) => {
@@ -198,17 +607,95 @@ const validators = {
198
607
 
199
608
  // Main validation function
200
609
  const validate = (schema, data) => {
610
+ // Input validation
611
+ if (!schema || typeof schema !== 'object') {
612
+ throw new Error('Schema must be a valid object');
613
+ }
614
+
615
+ if (!data || typeof data !== 'object') {
616
+ throw new Error('Data must be a valid object');
617
+ }
618
+
619
+ const results = {};
620
+ let isValid = true;
621
+
622
+ for (const [field, validator] of Object.entries(schema)) {
623
+ try {
624
+ const fieldValue = data[field];
625
+ const result =
626
+ typeof validator === 'function'
627
+ ? validator(fieldValue).validate()
628
+ : validator.validate();
629
+
630
+ results[field] = result;
631
+ if (!result.isValid) {
632
+ isValid = false;
633
+ }
634
+ } catch (error) {
635
+ // Handle validation setup errors
636
+ results[field] = new ValidationResult(false, [
637
+ `Validation setup error: ${error.message}`
638
+ ]);
639
+ isValid = false;
640
+ }
641
+ }
642
+
643
+ return {
644
+ isValid,
645
+ errors: results,
646
+ getErrors: () => {
647
+ const errors = {};
648
+ for (const [field, result] of Object.entries(results)) {
649
+ if (!result.isValid) {
650
+ errors[field] = result.errors;
651
+ }
652
+ }
653
+ return errors;
654
+ }
655
+ };
656
+ };
657
+
658
+ // Async validation function
659
+ const validateAsync = async (schema, data) => {
660
+ // Input validation
661
+ if (!schema || typeof schema !== 'object') {
662
+ throw new Error('Schema must be a valid object');
663
+ }
664
+
665
+ if (!data || typeof data !== 'object') {
666
+ throw new Error('Data must be a valid object');
667
+ }
668
+
201
669
  const results = {};
202
670
  let isValid = true;
203
671
 
204
672
  for (const [field, validator] of Object.entries(schema)) {
205
- const fieldValue = data[field];
206
- const result = typeof validator === 'function'
207
- ? validator(fieldValue).validate()
208
- : validator.validate();
673
+ try {
674
+ const fieldValue = data[field];
675
+
676
+ let result;
677
+ if (typeof validator === 'function') {
678
+ const validatorInstance = validator(fieldValue);
679
+ result =
680
+ validatorInstance.asyncRules.length > 0
681
+ ? await validatorInstance.validateAsync()
682
+ : validatorInstance.validate();
683
+ } else {
684
+ result =
685
+ validator.asyncRules && validator.asyncRules.length > 0
686
+ ? await validator.validateAsync()
687
+ : validator.validate();
688
+ }
209
689
 
210
- results[field] = result;
211
- if (!result.isValid) {
690
+ results[field] = result;
691
+ if (!result.isValid) {
692
+ isValid = false;
693
+ }
694
+ } catch (error) {
695
+ // Handle validation setup errors
696
+ results[field] = new ValidationResult(false, [
697
+ `Validation setup error: ${error.message}`
698
+ ]);
212
699
  isValid = false;
213
700
  }
214
701
  }
@@ -232,5 +719,9 @@ module.exports = {
232
719
  BaseValidator,
233
720
  ValidationResult,
234
721
  validators,
235
- validate
722
+ validate,
723
+ validateAsync,
724
+ safeRegexTest,
725
+ safeRegexTestSync,
726
+ isRegexSafe
236
727
  };
package/types/index.d.ts CHANGED
@@ -1,10 +1,17 @@
1
- declare module 'mini-validator' {
2
- export interface ValidationResult {
1
+ declare module 'snap-validate' {
2
+ /**
3
+ * Result of a validation operation
4
+ */
5
+ export class ValidationResult {
6
+ constructor(isValid: boolean, errors?: string[]);
3
7
  isValid: boolean;
4
8
  errors: string[];
5
9
  addError(message: string): ValidationResult;
6
10
  }
7
11
 
12
+ /**
13
+ * Password validation options
14
+ */
8
15
  export interface PasswordOptions {
9
16
  minLength?: number;
10
17
  requireUppercase?: boolean;
@@ -13,25 +20,113 @@ declare module 'mini-validator' {
13
20
  requireSpecialChars?: boolean;
14
21
  }
15
22
 
23
+ /**
24
+ * Phone number format types
25
+ */
16
26
  export type PhoneFormat = 'us' | 'international' | 'simple';
27
+
28
+ /**
29
+ * Country codes for zip code validation
30
+ */
17
31
  export type CountryCode = 'us' | 'ca' | 'uk';
18
32
 
33
+ /**
34
+ * Custom validation function that returns boolean
35
+ */
36
+ export type CustomValidatorFunction = (
37
+ value: any
38
+ ) => boolean | string | ValidationResult;
39
+
40
+ /**
41
+ * Async custom validation function
42
+ */
43
+ export type AsyncValidatorFunction = (
44
+ value: any
45
+ ) => Promise<boolean | string | ValidationResult>;
46
+
47
+ /**
48
+ * Conditional validation condition
49
+ */
50
+ export type ConditionalFunction = (value: any) => boolean;
51
+
52
+ /**
53
+ * Conditional validator function
54
+ */
55
+ export type ConditionalValidatorFunction = (value: any) => BaseValidator;
56
+
57
+ /**
58
+ * Base validator class with chainable validation methods
59
+ */
19
60
  export class BaseValidator {
20
61
  constructor(value: any);
62
+ value: any;
63
+ rules: Array<() => ValidationResult>;
64
+ asyncRules: Array<() => Promise<ValidationResult>>;
65
+ isOptional: boolean;
66
+
67
+ /**
68
+ * Make field required
69
+ */
21
70
  required(message?: string): BaseValidator;
71
+
72
+ /**
73
+ * Make field optional (skips validation if empty)
74
+ */
75
+ optional(): BaseValidator;
76
+
77
+ /**
78
+ * Set minimum length/value
79
+ */
22
80
  min(length: number, message?: string): BaseValidator;
81
+
82
+ /**
83
+ * Set maximum length/value
84
+ */
23
85
  max(length: number, message?: string): BaseValidator;
86
+
87
+ /**
88
+ * Validate against regex pattern
89
+ */
24
90
  pattern(regex: RegExp, message?: string): BaseValidator;
91
+
92
+ /**
93
+ * Conditional validation
94
+ */
95
+ when(
96
+ condition: boolean | ConditionalFunction,
97
+ validator: BaseValidator | ConditionalValidatorFunction
98
+ ): BaseValidator;
99
+
100
+ /**
101
+ * Custom synchronous validation
102
+ */
103
+ custom(
104
+ validatorFn: CustomValidatorFunction,
105
+ message?: string
106
+ ): BaseValidator;
107
+
108
+ /**
109
+ * Custom asynchronous validation
110
+ */
111
+ customAsync(
112
+ validatorFn: AsyncValidatorFunction,
113
+ message?: string
114
+ ): BaseValidator;
115
+
116
+ /**
117
+ * Execute synchronous validation
118
+ */
25
119
  validate(): ValidationResult;
26
- }
27
120
 
28
- export class ValidationResult {
29
- constructor(isValid: boolean, errors?: string[]);
30
- isValid: boolean;
31
- errors: string[];
32
- addError(message: string): ValidationResult;
121
+ /**
122
+ * Execute asynchronous validation (includes sync rules)
123
+ */
124
+ validateAsync(): Promise<ValidationResult>;
33
125
  }
34
126
 
127
+ /**
128
+ * Predefined validators
129
+ */
35
130
  export interface Validators {
36
131
  email(value: string): BaseValidator;
37
132
  phone(value: string, format?: PhoneFormat): BaseValidator;
@@ -43,18 +138,43 @@ declare module 'mini-validator' {
43
138
  zipCode(value: string, country?: CountryCode): BaseValidator;
44
139
  }
45
140
 
141
+ /**
142
+ * Result of schema validation
143
+ */
46
144
  export interface SchemaValidationResult {
47
145
  isValid: boolean;
48
146
  errors: { [field: string]: ValidationResult };
49
147
  getErrors(): { [field: string]: string[] };
50
148
  }
51
149
 
150
+ /**
151
+ * Validation function type for schema
152
+ */
52
153
  export type ValidationFunction = (value: any) => BaseValidator;
53
- export type Schema = { [field: string]: ValidationFunction };
54
154
 
155
+ /**
156
+ * Schema definition type
157
+ */
158
+ export type Schema = { [field: string]: ValidationFunction | BaseValidator };
159
+
160
+ /**
161
+ * Predefined validator instances
162
+ */
55
163
  export const validators: Validators;
164
+
165
+ /**
166
+ * Validate data against schema synchronously
167
+ */
56
168
  export function validate(
57
169
  schema: Schema,
58
170
  data: { [key: string]: any }
59
171
  ): SchemaValidationResult;
172
+
173
+ /**
174
+ * Validate data against schema asynchronously
175
+ */
176
+ export function validateAsync(
177
+ schema: Schema,
178
+ data: { [key: string]: any }
179
+ ): Promise<SchemaValidationResult>;
60
180
  }