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 +199 -14
- package/package.json +1 -1
- package/src/index.js +525 -34
- package/types/index.d.ts +129 -9
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
# Snap Validate โก
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/snap-validate)
|
|
4
4
|
[](https://github.com/aniru-dh21/snap-validate/actions)
|
|
5
|
-
[](https://codecov.io/gh/aniru-dh21/snap-validate)
|
|
6
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://packagephobia.now.sh/result?p=snap-validate)
|
|
7
|
+
[](https://bundlephobia.com/package/snap-validate@latest)
|
|
8
|
+
[](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
|
-
- `
|
|
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
|
-
|
|
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
package/src/index.js
CHANGED
|
@@ -1,8 +1,77 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Snap Validate - Lightweight validator library
|
|
3
|
-
* @version 0.
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
60
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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(
|
|
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:
|
|
94
|
-
international:
|
|
95
|
-
simple:
|
|
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
|
-
.
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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 '
|
|
2
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
}
|