payload-guard-filter 1.6.2 → 1.8.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 +277 -174
- package/dist/core/compiler.d.ts +5 -1
- package/dist/core/compiler.js +130 -12
- package/dist/core/filter.js +2 -0
- package/dist/core/types.d.ts +45 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,174 +1,277 @@
|
|
|
1
|
-
# payload-guard
|
|
2
|
-
|
|
3
|
-
> Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
|
|
4
|
-
|
|
5
|
-
<p align="center">
|
|
6
|
-
<strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
|
|
7
|
-
</p>
|
|
8
|
-
|
|
9
|
-
<p align="center">
|
|
10
|
-
<img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
|
|
11
|
-
<img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
|
|
12
|
-
<img src="https://img.shields.io/badge/TypeScript-100%25-blue" alt="TypeScript">
|
|
13
|
-
<img src="https://img.shields.io/badge/Node.js-18%2B-green" alt="Node.js 18+">
|
|
14
|
-
</p>
|
|
15
|
-
|
|
16
|
-
---
|
|
17
|
-
|
|
18
|
-
## 🛡️ Workflow
|
|
19
|
-
|
|
20
|
-
```mermaid
|
|
21
|
-
graph LR
|
|
22
|
-
A[Request] --> B(Gatekeeper)
|
|
23
|
-
B --> C{Shape Check}
|
|
24
|
-
C -- Valid --> D[Redact & Clean]
|
|
25
|
-
C -- Invalid --> E[Strict Error / Fail Safe]
|
|
26
|
-
D --> F[Secure Response]
|
|
27
|
-
E --> F
|
|
28
|
-
F --> G((Metrics))
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## ✨ Features
|
|
34
|
-
|
|
35
|
-
- **Shape-based filtering** — Define what you want, auto-remove everything else
|
|
36
|
-
- **Sensitive field protection** — `password`, `token`, `secret` automatically removed
|
|
37
|
-
- **Zero dependencies** — Pure TypeScript, no external packages
|
|
38
|
-
- **Universal** — Works in Node.js, Browser, React Native
|
|
39
|
-
- **TypeScript-first** — Full type inference from shape definitions
|
|
40
|
-
- **Blazing fast** — Compiled schemas for production performance
|
|
41
|
-
- **Never crashes** — Graceful failure mode, production-safe
|
|
42
|
-
|
|
43
|
-
---
|
|
44
|
-
|
|
45
|
-
## 📦 Installation
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npm install payload-guard
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
## 🚀 Quick Start
|
|
54
|
-
|
|
55
|
-
### Basic Usage
|
|
56
|
-
|
|
57
|
-
```typescript
|
|
58
|
-
import { guard } from 'payload-guard';
|
|
59
|
-
|
|
60
|
-
// Define a shape
|
|
61
|
-
const userShape = guard.shape({
|
|
62
|
-
id: 'number',
|
|
63
|
-
name: 'string',
|
|
64
|
-
email: 'string',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Filter data
|
|
68
|
-
const rawData = {
|
|
69
|
-
id: 1,
|
|
70
|
-
name: 'John Doe',
|
|
71
|
-
email: 'john@example.com',
|
|
72
|
-
password: 'secret123', // ❌ Will be removed
|
|
73
|
-
internalNotes: 'VIP user', // ❌ Will be removed
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const safeData = userShape(rawData);
|
|
77
|
-
// Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
### Advanced Validation (v1.4+)
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
const userShape = guard.shape({
|
|
84
|
-
email: guard.string().email().toLowerCase().trim(),
|
|
85
|
-
age: guard.number().min(18).max(100).default(18),
|
|
86
|
-
tags: guard.array(guard.string().min(2)),
|
|
87
|
-
role: guard.string().validate(v => ['admin', 'user'].includes(v)).default('user'),
|
|
88
|
-
bio: guard.string().max(200).optional(),
|
|
89
|
-
});
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
1
|
+
# payload-guard
|
|
2
|
+
|
|
3
|
+
> Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
|
|
11
|
+
<img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
|
|
12
|
+
<img src="https://img.shields.io/badge/TypeScript-100%25-blue" alt="TypeScript">
|
|
13
|
+
<img src="https://img.shields.io/badge/Node.js-18%2B-green" alt="Node.js 18+">
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 🛡️ Workflow
|
|
19
|
+
|
|
20
|
+
```mermaid
|
|
21
|
+
graph LR
|
|
22
|
+
A[Request] --> B(Gatekeeper)
|
|
23
|
+
B --> C{Shape Check}
|
|
24
|
+
C -- Valid --> D[Redact & Clean]
|
|
25
|
+
C -- Invalid --> E[Strict Error / Fail Safe]
|
|
26
|
+
D --> F[Secure Response]
|
|
27
|
+
E --> F
|
|
28
|
+
F --> G((Metrics))
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ✨ Features
|
|
34
|
+
|
|
35
|
+
- **Shape-based filtering** — Define what you want, auto-remove everything else
|
|
36
|
+
- **Sensitive field protection** — `password`, `token`, `secret` automatically removed
|
|
37
|
+
- **Zero dependencies** — Pure TypeScript, no external packages
|
|
38
|
+
- **Universal** — Works in Node.js, Browser, React Native
|
|
39
|
+
- **TypeScript-first** — Full type inference from shape definitions
|
|
40
|
+
- **Blazing fast** — Compiled schemas for production performance
|
|
41
|
+
- **Never crashes** — Graceful failure mode, production-safe
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 📦 Installation
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install payload-guard
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 🚀 Quick Start
|
|
54
|
+
|
|
55
|
+
### Basic Usage
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { guard } from 'payload-guard';
|
|
59
|
+
|
|
60
|
+
// Define a shape
|
|
61
|
+
const userShape = guard.shape({
|
|
62
|
+
id: 'number',
|
|
63
|
+
name: 'string',
|
|
64
|
+
email: 'string',
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Filter data
|
|
68
|
+
const rawData = {
|
|
69
|
+
id: 1,
|
|
70
|
+
name: 'John Doe',
|
|
71
|
+
email: 'john@example.com',
|
|
72
|
+
password: 'secret123', // ❌ Will be removed
|
|
73
|
+
internalNotes: 'VIP user', // ❌ Will be removed
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const safeData = userShape(rawData);
|
|
77
|
+
// Result: { id: 1, name: 'John Doe', email: 'john@example.com' }
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Advanced Validation (v1.4+)
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
const userShape = guard.shape({
|
|
84
|
+
email: guard.string().email().toLowerCase().trim(),
|
|
85
|
+
age: guard.number().min(18).max(100).default(18),
|
|
86
|
+
tags: guard.array(guard.string().min(2)),
|
|
87
|
+
role: guard.string().validate(v => ['admin', 'user'].includes(v)).default('user'),
|
|
88
|
+
bio: guard.string().max(200).optional(),
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Nested Object Validation (v1.7+)
|
|
93
|
+
|
|
94
|
+
Define nested shapes for deep object validation:
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
const userShape = guard.shape({
|
|
98
|
+
id: 'number',
|
|
99
|
+
profile: {
|
|
100
|
+
name: 'string',
|
|
101
|
+
email: guard.string().email(),
|
|
102
|
+
age: guard.number().min(18),
|
|
103
|
+
address: {
|
|
104
|
+
street: 'string',
|
|
105
|
+
city: 'string',
|
|
106
|
+
zipCode: guard.string().regex(/^\d{5}(-\d{4})?$/),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
posts: guard.array({
|
|
110
|
+
id: 'number',
|
|
111
|
+
title: 'string',
|
|
112
|
+
tags: guard.array('string'),
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const input = {
|
|
117
|
+
id: 1,
|
|
118
|
+
profile: {
|
|
119
|
+
name: 'John',
|
|
120
|
+
email: 'john@example.com',
|
|
121
|
+
age: 30,
|
|
122
|
+
address: {
|
|
123
|
+
street: '123 Main St',
|
|
124
|
+
city: 'NYC',
|
|
125
|
+
zipCode: '10001',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
posts: [
|
|
129
|
+
{ id: 1, title: 'Hello', tags: ['intro'] },
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const result = userShape(input);
|
|
134
|
+
// Filters all nested levels automatically
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Custom Error Messages (v1.7+)
|
|
138
|
+
|
|
139
|
+
Add per-field custom error messages:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
const userShape = guard.shape({
|
|
143
|
+
email: guard
|
|
144
|
+
.string()
|
|
145
|
+
.email()
|
|
146
|
+
.error('Please provide a valid email address'),
|
|
147
|
+
|
|
148
|
+
username: guard
|
|
149
|
+
.string()
|
|
150
|
+
.min(5)
|
|
151
|
+
.error((value, field) => `${field} must be at least 5 characters`),
|
|
152
|
+
|
|
153
|
+
age: guard
|
|
154
|
+
.number()
|
|
155
|
+
.min(18)
|
|
156
|
+
.max(100)
|
|
157
|
+
.errorCodes({ min: 'AGE_TOO_YOUNG', max: 'AGE_TOO_OLD' }),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Collect validation errors
|
|
161
|
+
const { compile } = require('payload-guard');
|
|
162
|
+
const errors = [];
|
|
163
|
+
const validator = compile(
|
|
164
|
+
{ email: { type: 'string', email: true } },
|
|
165
|
+
{ collectErrors: true, _errors: errors }
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
validator({ email: 'invalid' });
|
|
169
|
+
// errors: [{ field: 'email', message: '...', code: 'email' }]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## 📖 API Reference
|
|
175
|
+
|
|
176
|
+
### `guard.string()`
|
|
177
|
+
Creates a string builder with chained constraints:
|
|
178
|
+
- `.min(length)` — Minimum character length
|
|
179
|
+
- `.max(length)` — Maximum character length
|
|
180
|
+
- `.email()` — Basic email validation
|
|
181
|
+
- `.regex(pattern)` — Regex pattern match
|
|
182
|
+
- `.trim()` — Auto-trim whitespace (Transformation)
|
|
183
|
+
- `.toLowerCase()` / `.toUpperCase()` — Case transformation
|
|
184
|
+
|
|
185
|
+
### `guard.number()`
|
|
186
|
+
- `.min(value)` / `.max(value)` — Range validation
|
|
187
|
+
- `.integer()` — Ensure number is an integer
|
|
188
|
+
- `.positive()` — Shortcut for `.min(0)`
|
|
189
|
+
|
|
190
|
+
### Common Builder Methods
|
|
191
|
+
- `.required()` / `.optional()` — Toggle requirement
|
|
192
|
+
- `.default(value)` — Value to use if field is missing or invalid
|
|
193
|
+
- `.transform(fn)` — Custom transformation function
|
|
194
|
+
- `.validate(fn)` — Custom validation function (return `false` to fail)
|
|
195
|
+
- `.error(message)` — Custom error message (string or function) **(v1.7+)**
|
|
196
|
+
- `.errorCodes(codes)` — Custom error codes for validations **(v1.7+)**
|
|
197
|
+
|
|
198
|
+
### New in v1.7+
|
|
199
|
+
|
|
200
|
+
**Nested Object Support:**
|
|
201
|
+
- Define nested shapes as plain objects: `{ profile: { name: 'string', email: 'string' } }`
|
|
202
|
+
- Arrays with nested objects: `guard.array({ id: 'number', name: 'string' })`
|
|
203
|
+
- Deep nesting supported at any level
|
|
204
|
+
|
|
205
|
+
**Custom Error Messages:**
|
|
206
|
+
- `.error('Custom message')` — Static error message
|
|
207
|
+
- `.error((value, field) => \`${field} is invalid\`)` — Dynamic error message
|
|
208
|
+
- `.errorCodes({ min: 'TOO_SHORT', email: 'BAD_EMAIL' })` — Error codes
|
|
209
|
+
|
|
210
|
+
**Error Collection:**
|
|
211
|
+
```typescript
|
|
212
|
+
const { compile } = require('payload-guard');
|
|
213
|
+
const errors = [];
|
|
214
|
+
const validator = compile(shape, { collectErrors: true, _errors: errors });
|
|
215
|
+
validator(data);
|
|
216
|
+
// errors array populated with { field, message, code, value }
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Field Aliasing & Mapping (v1.6+)
|
|
220
|
+
|
|
221
|
+
Rename fields from your database to match your frontend API:
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
const userShape = guard.shape({
|
|
225
|
+
id: guard.string().from('_id'), // Map DB _id to id
|
|
226
|
+
workTime: guard.number().from('totalWorkTimeMs').transform(v => v / 1000),
|
|
227
|
+
name: 'string',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Input: { _id: '123', totalWorkTimeMs: 5000, name: 'Sannu' }
|
|
231
|
+
// Output: { id: '123', workTime: 5, name: 'Sannu' }
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 🛡️ "Never Crash" Policy
|
|
237
|
+
|
|
238
|
+
Payload Guard is designed for mission-critical enterprise environments where uptime is non-negotiable.
|
|
239
|
+
|
|
240
|
+
- **Circular Reference Safety**: Automatically detects and handles circular objects without infinite loops or stack overflows.
|
|
241
|
+
- **Hook Isolation**: Custom `.transform()` and `.validate()` callbacks are wrapped in internal try/catch blocks. If your code fails, the library logs the error and safely continues using fallback values.
|
|
242
|
+
- **Middleware Fail-Open**: If internal filtering logic hits an unexpected edge case, `failOpen: true` ensures the original request/response still reaches its destination.
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## ⚡ Performance
|
|
247
|
+
|
|
248
|
+
| Benchmark | ops/sec | avg (ms) |
|
|
249
|
+
|-----------|---------|----------|
|
|
250
|
+
| **Small payload** (5 fields) | 449,365 | **0.0022ms** |
|
|
251
|
+
| **Medium payload** (50 posts) | 7,791 | **0.1284ms** |
|
|
252
|
+
| **Large payload** (1000 users) | 246 | **4.0724ms** |
|
|
253
|
+
|
|
254
|
+
> **Memory Usage**: ~121 MB Heap Used (Stable)
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 🛡️ Fail-Safe Design
|
|
259
|
+
|
|
260
|
+
Built for production reliability:
|
|
261
|
+
- **Isolation**: Monitoring failures **never** break your API responses. If a storage adapter or plugin crashes, the error is caught and logged, while the user's request continues normally.
|
|
262
|
+
- **Async-Only**: All processing is non-blocking to ensure zero impact on event loop latency.
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
### ⛑️ Maintained actively.
|
|
266
|
+
**Bug fixes usually within 24–48 hours.**
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
## 📄 License
|
|
270
|
+
|
|
271
|
+
MIT
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
<p align="center">
|
|
276
|
+
Made with ❤️ for safer APIs
|
|
277
|
+
</p>
|
package/dist/core/compiler.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ShapeDescriptor, ArrayShapeDescriptor, CompiledFilter } from './types';
|
|
1
|
+
import { ShapeDescriptor, ArrayShapeDescriptor, CompiledFilter, ValidationError } from './types';
|
|
2
2
|
interface CompileOptions {
|
|
3
3
|
sensitive?: string[];
|
|
4
4
|
dev?: boolean;
|
|
@@ -9,6 +9,10 @@ interface CompileOptions {
|
|
|
9
9
|
maxArrayLength?: number;
|
|
10
10
|
/** Parent path for nested field warnings (internal) */
|
|
11
11
|
_parentPath?: string;
|
|
12
|
+
/** Collect validation errors */
|
|
13
|
+
_errors?: ValidationError[];
|
|
14
|
+
/** Collect errors flag */
|
|
15
|
+
collectErrors?: boolean;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* Compile a shape descriptor into an optimized filter function
|
package/dist/core/compiler.js
CHANGED
|
@@ -25,35 +25,118 @@ function compile(shape, opts = {}) {
|
|
|
25
25
|
const config = shape.__isBuilder ? shape.config : shape;
|
|
26
26
|
const primType = config.type;
|
|
27
27
|
const defaultVal = config.default;
|
|
28
|
+
const fieldPath = opts._parentPath || 'field';
|
|
28
29
|
return (value) => {
|
|
29
30
|
let val = value == null ? (defaultVal ?? value) : (0, sanitizer_1.coercePrimitive)(primType, value);
|
|
31
|
+
let error = null;
|
|
32
|
+
// Helper to create error
|
|
33
|
+
const createError = (message, code) => {
|
|
34
|
+
const customMsg = typeof config.errorMessage === 'function'
|
|
35
|
+
? config.errorMessage(val, fieldPath)
|
|
36
|
+
: config.errorMessage;
|
|
37
|
+
return {
|
|
38
|
+
field: fieldPath,
|
|
39
|
+
message: customMsg || message,
|
|
40
|
+
code: config.errorCodes?.[code] || code,
|
|
41
|
+
value: val,
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
// Required check
|
|
45
|
+
if (config.required && val == null) {
|
|
46
|
+
error = createError(`${fieldPath} is required`, 'required');
|
|
47
|
+
if (opts.collectErrors && opts._errors) {
|
|
48
|
+
opts._errors.push(error);
|
|
49
|
+
}
|
|
50
|
+
return defaultVal ?? undefined;
|
|
51
|
+
}
|
|
30
52
|
// Perform validation and transformation if value is not null
|
|
31
53
|
if (val != null) {
|
|
54
|
+
// Type check
|
|
55
|
+
if (opts.strict) {
|
|
56
|
+
const typeValid = (primType === 'string' && typeof val === 'string') ||
|
|
57
|
+
(primType === 'number' && typeof val === 'number') ||
|
|
58
|
+
(primType === 'boolean' && typeof val === 'boolean') ||
|
|
59
|
+
(primType === 'any');
|
|
60
|
+
if (!typeValid) {
|
|
61
|
+
error = createError(`${fieldPath} must be of type ${primType}`, 'type');
|
|
62
|
+
if (opts.collectErrors && opts._errors) {
|
|
63
|
+
opts._errors.push(error);
|
|
64
|
+
}
|
|
65
|
+
return defaultVal ?? undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
32
68
|
// String validations
|
|
33
69
|
if (primType === 'string' && typeof val === 'string') {
|
|
34
70
|
val = (0, sanitizer_1.trimString)(val);
|
|
35
|
-
if (config.min !== undefined && val.length < config.min)
|
|
71
|
+
if (config.min !== undefined && val.length < config.min) {
|
|
72
|
+
error = createError(`${fieldPath} must be at least ${config.min} characters`, 'min');
|
|
73
|
+
if (opts.collectErrors && opts._errors) {
|
|
74
|
+
opts._errors.push(error);
|
|
75
|
+
}
|
|
36
76
|
return defaultVal ?? undefined;
|
|
37
|
-
|
|
77
|
+
}
|
|
78
|
+
if (config.max !== undefined && val.length > config.max) {
|
|
79
|
+
error = createError(`${fieldPath} must be at most ${config.max} characters`, 'max');
|
|
80
|
+
if (opts.collectErrors && opts._errors) {
|
|
81
|
+
opts._errors.push(error);
|
|
82
|
+
}
|
|
38
83
|
return defaultVal ?? undefined;
|
|
39
|
-
|
|
84
|
+
}
|
|
85
|
+
if (config.regex && !config.regex.test(val)) {
|
|
86
|
+
error = createError(`${fieldPath} does not match required pattern`, 'pattern');
|
|
87
|
+
if (opts.collectErrors && opts._errors) {
|
|
88
|
+
opts._errors.push(error);
|
|
89
|
+
}
|
|
40
90
|
return defaultVal ?? undefined;
|
|
91
|
+
}
|
|
41
92
|
if (config.email) {
|
|
42
93
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
43
|
-
if (!emailRegex.test(val))
|
|
94
|
+
if (!emailRegex.test(val)) {
|
|
95
|
+
error = createError(`${fieldPath} must be a valid email address`, 'email');
|
|
96
|
+
if (opts.collectErrors && opts._errors) {
|
|
97
|
+
opts._errors.push(error);
|
|
98
|
+
}
|
|
44
99
|
return defaultVal ?? undefined;
|
|
100
|
+
}
|
|
45
101
|
}
|
|
46
102
|
}
|
|
47
103
|
// Number validations
|
|
48
104
|
if (primType === 'number' && typeof val === 'number') {
|
|
49
|
-
if (config.min !== undefined && val < config.min)
|
|
105
|
+
if (config.min !== undefined && val < config.min) {
|
|
106
|
+
error = createError(`${fieldPath} must be at least ${config.min}`, 'min');
|
|
107
|
+
if (opts.collectErrors && opts._errors) {
|
|
108
|
+
opts._errors.push(error);
|
|
109
|
+
}
|
|
50
110
|
return defaultVal ?? undefined;
|
|
51
|
-
|
|
111
|
+
}
|
|
112
|
+
if (config.max !== undefined && val > config.max) {
|
|
113
|
+
error = createError(`${fieldPath} must be at most ${config.max}`, 'max');
|
|
114
|
+
if (opts.collectErrors && opts._errors) {
|
|
115
|
+
opts._errors.push(error);
|
|
116
|
+
}
|
|
52
117
|
return defaultVal ?? undefined;
|
|
118
|
+
}
|
|
53
119
|
}
|
|
54
120
|
// Custom validation
|
|
55
|
-
if (config.validate
|
|
56
|
-
|
|
121
|
+
if (config.validate) {
|
|
122
|
+
try {
|
|
123
|
+
if (!config.validate(val)) {
|
|
124
|
+
error = createError(`${fieldPath} failed validation`, 'custom');
|
|
125
|
+
if (opts.collectErrors && opts._errors) {
|
|
126
|
+
opts._errors.push(error);
|
|
127
|
+
}
|
|
128
|
+
return defaultVal ?? undefined;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
if (opts.dev)
|
|
133
|
+
(opts.logger ?? console.warn)('[payload-guard] validation error: ' + String(e), 'warn');
|
|
134
|
+
error = createError(`${fieldPath} validation threw an error`, 'custom');
|
|
135
|
+
if (opts.collectErrors && opts._errors) {
|
|
136
|
+
opts._errors.push(error);
|
|
137
|
+
}
|
|
138
|
+
return defaultVal ?? undefined;
|
|
139
|
+
}
|
|
57
140
|
}
|
|
58
141
|
// Transformation
|
|
59
142
|
if (config.transform) {
|
|
@@ -97,8 +180,17 @@ function compile(shape, opts = {}) {
|
|
|
97
180
|
if (isArrayShape(shape)) {
|
|
98
181
|
const itemFn = compile(shape.item, { ...opts, _parentPath: (opts._parentPath || '') + '[]' });
|
|
99
182
|
return (value) => {
|
|
100
|
-
if (!Array.isArray(value))
|
|
183
|
+
if (!Array.isArray(value)) {
|
|
184
|
+
if (opts.collectErrors && opts._errors) {
|
|
185
|
+
opts._errors.push({
|
|
186
|
+
field: opts._parentPath || 'array',
|
|
187
|
+
message: `${opts._parentPath || 'array'} must be an array`,
|
|
188
|
+
code: 'type',
|
|
189
|
+
value: value,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
101
192
|
return [];
|
|
193
|
+
}
|
|
102
194
|
// Memory safety: slice array if maxArrayLength is set
|
|
103
195
|
let arr = value;
|
|
104
196
|
if (opts.maxArrayLength && arr.length > opts.maxArrayLength) {
|
|
@@ -108,7 +200,7 @@ function compile(shape, opts = {}) {
|
|
|
108
200
|
}
|
|
109
201
|
arr = arr.slice(0, opts.maxArrayLength);
|
|
110
202
|
}
|
|
111
|
-
return arr.map(item => {
|
|
203
|
+
return arr.map((item, idx) => {
|
|
112
204
|
try {
|
|
113
205
|
return itemFn(item);
|
|
114
206
|
}
|
|
@@ -121,7 +213,7 @@ function compile(shape, opts = {}) {
|
|
|
121
213
|
}).filter(v => v !== undefined);
|
|
122
214
|
};
|
|
123
215
|
}
|
|
124
|
-
// Object shape - compile each field
|
|
216
|
+
// Object shape - compile each field (nested object support)
|
|
125
217
|
const fieldFilters = {};
|
|
126
218
|
const shapeObj = shape;
|
|
127
219
|
for (const key of Object.keys(shapeObj)) {
|
|
@@ -139,7 +231,33 @@ function compile(shape, opts = {}) {
|
|
|
139
231
|
}
|
|
140
232
|
continue;
|
|
141
233
|
}
|
|
142
|
-
|
|
234
|
+
const fieldDescriptor = shapeObj[key];
|
|
235
|
+
// Check if this is a FieldConfig with nested shape
|
|
236
|
+
if (isFieldConfig(fieldDescriptor)) {
|
|
237
|
+
const config = fieldDescriptor.__isBuilder
|
|
238
|
+
? fieldDescriptor.config
|
|
239
|
+
: fieldDescriptor;
|
|
240
|
+
if (config.shape) {
|
|
241
|
+
// Nested object shape - compile both the field validator and nested shape
|
|
242
|
+
const fieldValidator = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
|
|
243
|
+
const nestedShapeFn = compile(config.shape, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
|
|
244
|
+
fieldFilters[key] = (value) => {
|
|
245
|
+
const validated = fieldValidator(value);
|
|
246
|
+
if (validated == null)
|
|
247
|
+
return validated;
|
|
248
|
+
// Apply nested shape filtering
|
|
249
|
+
return nestedShapeFn(validated);
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
// Simple field - just compile the field validator
|
|
254
|
+
fieldFilters[key] = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Regular shape descriptor (primitive, nested object, array, etc.)
|
|
259
|
+
fieldFilters[key] = compile(fieldDescriptor, { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
|
|
260
|
+
}
|
|
143
261
|
}
|
|
144
262
|
return (value) => {
|
|
145
263
|
if (value == null || typeof value !== 'object')
|
package/dist/core/filter.js
CHANGED
|
@@ -78,6 +78,8 @@ function createBuilder(type) {
|
|
|
78
78
|
transform(fn) { config.transform = fn; return builder; },
|
|
79
79
|
validate(fn) { config.validate = fn; return builder; },
|
|
80
80
|
from(key) { config.from = key; return builder; },
|
|
81
|
+
error(msg) { config.errorMessage = msg; return builder; },
|
|
82
|
+
errorCodes(codes) { config.errorCodes = codes; return builder; },
|
|
81
83
|
};
|
|
82
84
|
Object.setPrototypeOf(builder, proto);
|
|
83
85
|
if (type === 'string') {
|
package/dist/core/types.d.ts
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
* Provides strong TypeScript inference from shape definitions
|
|
4
4
|
*/
|
|
5
5
|
export type PrimitiveType = 'string' | 'number' | 'boolean' | 'any';
|
|
6
|
+
export interface ValidationError {
|
|
7
|
+
field: string;
|
|
8
|
+
message: string;
|
|
9
|
+
code?: string;
|
|
10
|
+
value?: unknown;
|
|
11
|
+
}
|
|
12
|
+
export interface ValidationResult<T = unknown> {
|
|
13
|
+
data: T;
|
|
14
|
+
errors: ValidationError[];
|
|
15
|
+
valid: boolean;
|
|
16
|
+
}
|
|
6
17
|
export interface FieldConfig<T extends PrimitiveType = PrimitiveType> {
|
|
7
18
|
type: T;
|
|
8
19
|
required?: boolean;
|
|
@@ -16,6 +27,20 @@ export interface FieldConfig<T extends PrimitiveType = PrimitiveType> {
|
|
|
16
27
|
from?: string;
|
|
17
28
|
/** Custom validation function: return false if invalid */
|
|
18
29
|
validate?: (v: any) => boolean;
|
|
30
|
+
/** Custom error message for validation failures */
|
|
31
|
+
errorMessage?: string | ((value: unknown, field: string) => string);
|
|
32
|
+
/** Error codes for different validation failures */
|
|
33
|
+
errorCodes?: {
|
|
34
|
+
required?: string;
|
|
35
|
+
type?: string;
|
|
36
|
+
min?: string;
|
|
37
|
+
max?: string;
|
|
38
|
+
pattern?: string;
|
|
39
|
+
email?: string;
|
|
40
|
+
custom?: string;
|
|
41
|
+
};
|
|
42
|
+
/** Nested shape for object validation */
|
|
43
|
+
shape?: ShapeDescriptor;
|
|
19
44
|
}
|
|
20
45
|
export interface ShapeBuilder<T extends PrimitiveType> {
|
|
21
46
|
(value: unknown): InferPrimitive<T>;
|
|
@@ -27,6 +52,10 @@ export interface ShapeBuilder<T extends PrimitiveType> {
|
|
|
27
52
|
transform(fn: (v: InferPrimitive<T>) => any): ShapeBuilder<T>;
|
|
28
53
|
validate(fn: (v: InferPrimitive<T>) => boolean): ShapeBuilder<T>;
|
|
29
54
|
from(key: string): ShapeBuilder<T>;
|
|
55
|
+
/** Set custom error message */
|
|
56
|
+
error(msg: string | ((v: unknown, field: string) => string)): ShapeBuilder<T>;
|
|
57
|
+
/** Set error codes */
|
|
58
|
+
errorCodes(codes: FieldConfig<T>['errorCodes']): ShapeBuilder<T>;
|
|
30
59
|
}
|
|
31
60
|
export interface StringBuilder extends ShapeBuilder<'string'> {
|
|
32
61
|
min(len: number): StringBuilder;
|
|
@@ -36,18 +65,29 @@ export interface StringBuilder extends ShapeBuilder<'string'> {
|
|
|
36
65
|
trim(): StringBuilder;
|
|
37
66
|
toLowerCase(): StringBuilder;
|
|
38
67
|
toUpperCase(): StringBuilder;
|
|
68
|
+
error(msg: string | ((v: unknown, field: string) => string)): StringBuilder;
|
|
69
|
+
errorCodes(codes: FieldConfig<'string'>['errorCodes']): StringBuilder;
|
|
39
70
|
}
|
|
40
71
|
export interface NumberBuilder extends ShapeBuilder<'number'> {
|
|
41
72
|
min(val: number): NumberBuilder;
|
|
42
73
|
max(val: number): NumberBuilder;
|
|
43
74
|
integer(): NumberBuilder;
|
|
44
75
|
positive(): NumberBuilder;
|
|
76
|
+
error(msg: string | ((v: unknown, field: string) => string)): NumberBuilder;
|
|
77
|
+
errorCodes(codes: FieldConfig<'number'>['errorCodes']): NumberBuilder;
|
|
45
78
|
}
|
|
46
79
|
export interface BooleanBuilder extends ShapeBuilder<'boolean'> {
|
|
80
|
+
error(msg: string | ((v: unknown, field: string) => string)): BooleanBuilder;
|
|
81
|
+
errorCodes(codes: FieldConfig<'boolean'>['errorCodes']): BooleanBuilder;
|
|
47
82
|
}
|
|
48
83
|
export interface AnyBuilder extends ShapeBuilder<'any'> {
|
|
84
|
+
error(msg: string | ((v: unknown, field: string) => string)): AnyBuilder;
|
|
85
|
+
errorCodes(codes: FieldConfig<'any'>['errorCodes']): AnyBuilder;
|
|
49
86
|
}
|
|
50
87
|
export type InferPrimitive<T extends PrimitiveType> = T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'any' ? unknown : never;
|
|
88
|
+
export type InferFieldConfig<T extends FieldConfig> = T extends {
|
|
89
|
+
shape: infer S;
|
|
90
|
+
} ? InferShape<S & ShapeDescriptor> : T extends FieldConfig<infer P> ? InferPrimitive<P> : unknown;
|
|
51
91
|
export type ShapeDescriptor = PrimitiveType | FieldConfig | {
|
|
52
92
|
[key: string]: ShapeDescriptor;
|
|
53
93
|
} | ArrayShapeDescriptor | CompiledFilter | ShapeBuilder<any>;
|
|
@@ -56,7 +96,9 @@ export interface ArrayShapeDescriptor {
|
|
|
56
96
|
readonly item: ShapeDescriptor;
|
|
57
97
|
}
|
|
58
98
|
export type CompiledFilter<T = unknown> = (value: unknown) => T;
|
|
59
|
-
export type InferShape<S extends ShapeDescriptor> = S extends PrimitiveType ? InferPrimitive<S> : S extends FieldConfig<infer T> ?
|
|
99
|
+
export type InferShape<S extends ShapeDescriptor> = S extends PrimitiveType ? InferPrimitive<S> : S extends FieldConfig<infer T> ? InferFieldConfig<S & FieldConfig<T>> : S extends ArrayShapeDescriptor ? InferShape<S['item']>[] : S extends CompiledFilter<infer T> ? T : S extends {
|
|
100
|
+
[key: string]: any;
|
|
101
|
+
} ? {
|
|
60
102
|
[K in keyof S]: InferShape<S[K] & ShapeDescriptor>;
|
|
61
103
|
} : unknown;
|
|
62
104
|
export interface GuardConfig {
|
|
@@ -96,6 +138,8 @@ export interface GuardConfig {
|
|
|
96
138
|
skipNonSerializable?: boolean;
|
|
97
139
|
/** Log names of fields removed during filtering (debug mode) */
|
|
98
140
|
logRemovedFields?: boolean;
|
|
141
|
+
/** Collect validation errors instead of silently failing */
|
|
142
|
+
collectErrors?: boolean;
|
|
99
143
|
}
|
|
100
144
|
export interface GuardMiddlewareOptions extends GuardConfig {
|
|
101
145
|
/** Sanitize request body */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-guard-filter",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Lightweight, zero-dependency shape-based payload filtering and sanitization for Node.js and browser",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"url": "https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-.git"
|
|
87
87
|
},
|
|
88
88
|
"bugs": {
|
|
89
|
-
"url": "https://
|
|
89
|
+
"url": "https://mypackagedoc.vercel.app/package/payload-guard-filter"
|
|
90
90
|
},
|
|
91
|
-
"homepage": "https://
|
|
91
|
+
"homepage": "https://mypackagedoc.vercel.app/package/payload-guard-filter"
|
|
92
92
|
}
|