payload-guard-filter 1.2.0 → 1.3.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 +94 -30
- package/dist/core/filter.d.ts +3 -18
- package/dist/core/filter.js +21 -3
- package/dist/core/types.d.ts +6 -0
- package/dist/middleware/express.js +64 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
|
+
## 🛡️ Workflow
|
|
17
|
+
|
|
18
|
+
```mermaid
|
|
19
|
+
graph LR
|
|
20
|
+
A[Request] --> B(Gatekeeper)
|
|
21
|
+
B --> C{Shape Check}
|
|
22
|
+
C -- Valid --> D[Redact & Clean]
|
|
23
|
+
C -- Invalid --> E[Strict Error / Fail Safe]
|
|
24
|
+
D --> F[Secure Response]
|
|
25
|
+
E --> F
|
|
26
|
+
F --> G((Metrics))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
16
31
|
## ✨ Features
|
|
17
32
|
|
|
18
33
|
- **Shape-based filtering** — Define what you want, auto-remove everything else
|
|
@@ -260,58 +275,107 @@ const postShape = guard.shape({
|
|
|
260
275
|
|
|
261
276
|
---
|
|
262
277
|
|
|
263
|
-
## 🏢 Enterprise Features
|
|
264
|
-
|
|
265
|
-
These features are designed for high-throughput, production systems where bandwidth, predictability and observability matter.
|
|
278
|
+
## 🏢 Enterprise Features (v1.2.0+)
|
|
266
279
|
|
|
267
|
-
|
|
280
|
+
Designed for high-performance production systems, Payload Guard includes enterprise-grade features for security, observability, and performance.
|
|
268
281
|
|
|
282
|
+
### 🛡️ 1. Header Sanitization
|
|
283
|
+
Automatically remove sensitive headers like `Authorization` or `Cookie` before your business logic handles the request.
|
|
269
284
|
```ts
|
|
270
|
-
|
|
271
|
-
|
|
285
|
+
app.use(guardMiddleware({
|
|
286
|
+
sanitizeHeaders: true,
|
|
287
|
+
sensitiveHeaders: ['x-api-key', 'session-id'] // optional extras
|
|
288
|
+
}));
|
|
289
|
+
```
|
|
272
290
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
});
|
|
291
|
+
### 🧠 2. Memory Safety (`maxArrayLength`)
|
|
292
|
+
Prevent DoS attacks via extremely large arrays by automatically truncating them to a safe limit.
|
|
293
|
+
```ts
|
|
294
|
+
const shape = guard.shape({ items: guard.array('string') }, { maxArrayLength: 1000 });
|
|
277
295
|
```
|
|
278
296
|
|
|
279
|
-
|
|
297
|
+
### ⚡ 3. Async Safety (`maxPayloadSize`)
|
|
298
|
+
Avoid processing massive JSON payloads that could block the event loop.
|
|
299
|
+
```ts
|
|
300
|
+
app.use(guardMiddleware({
|
|
301
|
+
maxPayloadSize: 1024 * 512, // 512KB
|
|
302
|
+
skipLargePayload: true // Skips filtering if too large
|
|
303
|
+
}));
|
|
304
|
+
```
|
|
280
305
|
|
|
306
|
+
### ⏱️ 4. Middleware Timing Stats
|
|
307
|
+
Track exactly how much time Payload Guard adds to your request cycle.
|
|
281
308
|
```
|
|
282
|
-
payload
|
|
309
|
+
[payload-guard] [POST] /api/data: reduced 15KB -> 4KB (73%) in 0.12ms
|
|
283
310
|
```
|
|
284
311
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
```ts
|
|
288
|
-
// per-shape strict mode
|
|
289
|
-
const userStrict = guard.shape({ id: 'number', email: 'string' }, { strict: true });
|
|
312
|
+
### 📊 5. Human-Readable Metrics
|
|
313
|
+
Visibility into bandwidth savings with formatted byte sizes and percentages.
|
|
290
314
|
|
|
291
|
-
|
|
292
|
-
|
|
315
|
+
### 🛠️ 6. CLI Type Sync
|
|
316
|
+
Generate frontend TypeScript types from your backend shapes with one command.
|
|
317
|
+
```bash
|
|
318
|
+
npx payload-guard sync
|
|
293
319
|
```
|
|
294
320
|
|
|
295
|
-
|
|
321
|
+
### ⚠️ 7. Route-Aware Dev Warnings
|
|
322
|
+
Dev mode warnings now include the full method, path, and nested field locations (e.g., `user.profile.password`) for faster debugging.
|
|
296
323
|
|
|
324
|
+
### 🛣️ 8. Ignore Routes
|
|
325
|
+
Skip processing for high-volume or incompatible routes like file uploads or health checks.
|
|
297
326
|
```ts
|
|
298
|
-
|
|
299
|
-
|
|
327
|
+
app.use(guardMiddleware({ ignoreRoutes: ['/health', '/upload'] }));
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### 🏗️ 9. Shared Schemas & Examples
|
|
331
|
+
Built-in support for reusable schemas across your monorepo and a `examples/real-world` project for reference.
|
|
332
|
+
|
|
333
|
+
### 🚀 10. Performance Benchmarks
|
|
334
|
+
A dedicated benchmark suite to verify sub-millisecond overhead. `npm run benchmark`.
|
|
335
|
+
|
|
336
|
+
---
|
|
300
337
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
338
|
+
## 🏗️ Production Hardening
|
|
339
|
+
|
|
340
|
+
### 🛡️ Fail-Safe Mode (`failOpen`)
|
|
341
|
+
In production, we prioritize availability. If filtering fails for any reason, `failOpen: true` (default) ensures the original data is sent instead of breaking the request.
|
|
342
|
+
|
|
343
|
+
### 🚫 Non-Serializable Protection
|
|
344
|
+
Payload Guard automatically detects and skips `Buffer`, `Stream`, `File`, and `Blob` objects to prevent crashes and performance bottlenecks.
|
|
345
|
+
|
|
346
|
+
### 🔍 Detailed Debug Logs
|
|
347
|
+
Enable `logRemovedFields: true` to see exactly which fields were stripped from your payloads:
|
|
348
|
+
`[payload-guard] /api/user Removed fields: password, token, internal_id`
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 🌐 Multi-Framework Support
|
|
353
|
+
|
|
354
|
+
The core is zero-dependency and framework-agnostic. Use it anywhere:
|
|
355
|
+
|
|
356
|
+
### Hono / Cloudflare Workers
|
|
357
|
+
```ts
|
|
358
|
+
app.post('/user', async (c) => {
|
|
359
|
+
const body = await c.req.json();
|
|
360
|
+
return c.json(userShape(body));
|
|
305
361
|
});
|
|
306
362
|
```
|
|
307
363
|
|
|
308
|
-
|
|
309
|
-
|
|
364
|
+
### Fastify
|
|
310
365
|
```ts
|
|
311
|
-
|
|
366
|
+
fastify.post('/user', (req, reply) => {
|
|
367
|
+
reply.send(userShape(req.body));
|
|
368
|
+
});
|
|
312
369
|
```
|
|
313
370
|
|
|
314
|
-
|
|
371
|
+
---
|
|
372
|
+
|
|
373
|
+
## 🛑 When NOT to use
|
|
374
|
+
|
|
375
|
+
Payload Guard is optimized for JSON APIs. It is **not** suitable for:
|
|
376
|
+
1. **Binary Data**: PDFs, Images, or raw Buffers.
|
|
377
|
+
2. **File Streams**: Use `multer` or similar for multipart data.
|
|
378
|
+
3. **Heavy Computation**: Don't use it on payloads > 10MB without adjusting `maxPayloadSize`.
|
|
315
379
|
|
|
316
380
|
---
|
|
317
381
|
|
package/dist/core/filter.d.ts
CHANGED
|
@@ -12,13 +12,7 @@ export declare function getConfig(): GuardConfig;
|
|
|
12
12
|
* Build a shape filter function from a shape descriptor
|
|
13
13
|
* Returns a callable function that filters objects to match the shape
|
|
14
14
|
*/
|
|
15
|
-
export declare function buildShape<S extends ShapeDescriptor>(shape: S, opts?:
|
|
16
|
-
sensitive?: string[];
|
|
17
|
-
dev?: boolean;
|
|
18
|
-
strict?: boolean;
|
|
19
|
-
redact?: (k: string, v: unknown) => unknown;
|
|
20
|
-
logger?: (msg: string, level?: 'warn' | 'error' | 'info') => void;
|
|
21
|
-
}): ShapeFunction<S>;
|
|
15
|
+
export declare function buildShape<S extends ShapeDescriptor>(shape: S, opts?: GuardConfig): ShapeFunction<S>;
|
|
22
16
|
/**
|
|
23
17
|
* Main guard API object
|
|
24
18
|
*/
|
|
@@ -29,18 +23,9 @@ export declare const guard: {
|
|
|
29
23
|
* const userShape = guard.shape({ id: 'number', name: 'string' });
|
|
30
24
|
* const filtered = userShape(userData);
|
|
31
25
|
*/
|
|
32
|
-
shape: <S extends ShapeDescriptor>(descriptor: S, opts?:
|
|
33
|
-
sensitive?: string[];
|
|
34
|
-
dev?: boolean;
|
|
35
|
-
}) => ShapeFunction<S>;
|
|
26
|
+
shape: <S extends ShapeDescriptor>(descriptor: S, opts?: GuardConfig) => ShapeFunction<S>;
|
|
36
27
|
/** Compile a descriptor into a fast filter function directly */
|
|
37
|
-
compile: <S extends ShapeDescriptor>(descriptor: S, opts?:
|
|
38
|
-
sensitive?: string[];
|
|
39
|
-
dev?: boolean;
|
|
40
|
-
strict?: boolean;
|
|
41
|
-
redact?: (k: string, v: unknown) => unknown;
|
|
42
|
-
logger?: (msg: string) => void;
|
|
43
|
-
}) => import("./types").CompiledFilter;
|
|
28
|
+
compile: <S extends ShapeDescriptor>(descriptor: S, opts?: GuardConfig) => import("./types").CompiledFilter;
|
|
44
29
|
/**
|
|
45
30
|
* Create an array shape descriptor
|
|
46
31
|
* @example
|
package/dist/core/filter.js
CHANGED
|
@@ -17,6 +17,12 @@ let globalConfig = {
|
|
|
17
17
|
maxPayloadSize: undefined,
|
|
18
18
|
ignoreRoutes: undefined,
|
|
19
19
|
payloadMetrics: false,
|
|
20
|
+
maxArrayLength: undefined,
|
|
21
|
+
sanitizeHeaders: false,
|
|
22
|
+
sensitiveHeaders: ['authorization', 'cookie', 'x-api-key'],
|
|
23
|
+
failOpen: true,
|
|
24
|
+
skipNonSerializable: true,
|
|
25
|
+
logRemovedFields: false,
|
|
20
26
|
};
|
|
21
27
|
/**
|
|
22
28
|
* Configure global guard settings
|
|
@@ -36,11 +42,15 @@ function getConfig() {
|
|
|
36
42
|
*/
|
|
37
43
|
function buildShape(shape, opts) {
|
|
38
44
|
const mergedOpts = {
|
|
39
|
-
sensitive: [...(opts?.
|
|
40
|
-
dev: opts?.
|
|
45
|
+
sensitive: [...(opts?.sensitiveFields || []), ...(globalConfig.sensitiveFields || [])],
|
|
46
|
+
dev: opts?.devMode ?? globalConfig.devMode,
|
|
41
47
|
strict: opts?.strict ?? globalConfig.strict,
|
|
42
48
|
redact: opts?.redact ?? globalConfig.redact,
|
|
43
49
|
logger: opts?.logger ?? globalConfig.logger,
|
|
50
|
+
maxArrayLength: opts?.maxArrayLength ?? globalConfig.maxArrayLength,
|
|
51
|
+
failOpen: opts?.failOpen ?? globalConfig.failOpen,
|
|
52
|
+
skipNonSerializable: opts?.skipNonSerializable ?? globalConfig.skipNonSerializable,
|
|
53
|
+
logRemovedFields: opts?.logRemovedFields ?? globalConfig.logRemovedFields,
|
|
44
54
|
};
|
|
45
55
|
const compiledFn = (0, compiler_1.compile)(shape, mergedOpts);
|
|
46
56
|
// Create callable wrapper with compile method attached
|
|
@@ -70,7 +80,15 @@ exports.guard = {
|
|
|
70
80
|
*/
|
|
71
81
|
shape: (descriptor, opts) => buildShape(descriptor, opts),
|
|
72
82
|
/** Compile a descriptor into a fast filter function directly */
|
|
73
|
-
compile: (descriptor, opts) => (0, compiler_1.compile)(descriptor, {
|
|
83
|
+
compile: (descriptor, opts) => (0, compiler_1.compile)(descriptor, {
|
|
84
|
+
...opts,
|
|
85
|
+
sensitive: [...(opts?.sensitiveFields || []), ...(globalConfig.sensitiveFields || [])],
|
|
86
|
+
dev: opts?.devMode ?? globalConfig.devMode,
|
|
87
|
+
strict: opts?.strict ?? globalConfig.strict,
|
|
88
|
+
redact: opts?.redact ?? globalConfig.redact,
|
|
89
|
+
logger: opts?.logger ?? globalConfig.logger,
|
|
90
|
+
maxArrayLength: opts?.maxArrayLength ?? globalConfig.maxArrayLength,
|
|
91
|
+
}),
|
|
74
92
|
/**
|
|
75
93
|
* Create an array shape descriptor
|
|
76
94
|
* @example
|
package/dist/core/types.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface GuardConfig {
|
|
|
51
51
|
sanitizeHeaders?: boolean;
|
|
52
52
|
/** Sensitive header names to filter (default: authorization, cookie, x-api-key) */
|
|
53
53
|
sensitiveHeaders?: string[];
|
|
54
|
+
/** Fail-safe mode: if filtering fails, return original data instead of error (default: true) */
|
|
55
|
+
failOpen?: boolean;
|
|
56
|
+
/** Skip filtering for non-serializable objects (Buffer, Stream, File) (default: true) */
|
|
57
|
+
skipNonSerializable?: boolean;
|
|
58
|
+
/** Log names of fields removed during filtering (debug mode) */
|
|
59
|
+
logRemovedFields?: boolean;
|
|
54
60
|
}
|
|
55
61
|
export interface GuardMiddlewareOptions extends GuardConfig {
|
|
56
62
|
/** Sanitize request body */
|
|
@@ -4,6 +4,7 @@ exports.guardMiddleware = guardMiddleware;
|
|
|
4
4
|
exports.routeGuard = routeGuard;
|
|
5
5
|
const compiler_1 = require("../core/compiler");
|
|
6
6
|
const security_1 = require("../core/security");
|
|
7
|
+
const perf_1 = require("../core/perf");
|
|
7
8
|
/**
|
|
8
9
|
* Express middleware for payload sanitization and response filtering
|
|
9
10
|
*
|
|
@@ -49,47 +50,41 @@ function guardMiddleware(opts = {}) {
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
// Sanitize request body
|
|
52
|
-
if (sanitizeBody && req.body
|
|
53
|
-
//
|
|
54
|
-
|
|
53
|
+
if (sanitizeBody && req.body) {
|
|
54
|
+
// streaming/non-serializable safety
|
|
55
|
+
const isNonSerializable = req.body && (Buffer.isBuffer(req.body) ||
|
|
56
|
+
typeof req.body.pipe === 'function' ||
|
|
57
|
+
(typeof File !== 'undefined' && req.body instanceof File) ||
|
|
58
|
+
(typeof Blob !== 'undefined' && req.body instanceof Blob));
|
|
59
|
+
if (!(isNonSerializable && (opts.skipNonSerializable !== false))) {
|
|
60
|
+
// execution with fail-open safety
|
|
55
61
|
try {
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (!skipLargePayload) {
|
|
63
|
-
if (compiledRequestShape) {
|
|
64
|
-
req.body = compiledRequestShape(req.body);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
req.body = (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
62
|
+
// max payload size check
|
|
63
|
+
if (typeof maxPayloadSize === 'number') {
|
|
64
|
+
const size = Buffer.byteLength(JSON.stringify(req.body || ''), 'utf8');
|
|
65
|
+
if (size > maxPayloadSize && skipLargePayload) {
|
|
66
|
+
if (devMode) {
|
|
67
|
+
(opts.logger ?? console.warn)(`[payload-guard] ${routeName}: payload too large, skipping`, 'info');
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
if (compiledRequestShape) {
|
|
73
|
-
req.body = compiledRequestShape(req.body);
|
|
74
|
-
}
|
|
75
70
|
else {
|
|
76
|
-
req.body = (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
71
|
+
req.body = compiledRequestShape ? compiledRequestShape(req.body) : (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
77
72
|
}
|
|
78
73
|
}
|
|
74
|
+
else {
|
|
75
|
+
req.body = compiledRequestShape ? compiledRequestShape(req.body) : (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
76
|
+
}
|
|
79
77
|
}
|
|
80
78
|
catch (e) {
|
|
81
|
-
// if serialization fails, skip and continue
|
|
82
79
|
if (devMode)
|
|
83
|
-
(opts.logger ?? console.warn)('[payload-guard]
|
|
80
|
+
(opts.logger ?? console.warn)('[payload-guard] request filtering error: ' + String(e), 'warn');
|
|
81
|
+
if (opts.failOpen === false)
|
|
82
|
+
throw e;
|
|
83
|
+
// fail-open: keep original req.body
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
else {
|
|
87
|
-
|
|
88
|
-
req.body = compiledRequestShape(req.body);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
req.body = (0, security_1.removeSensitive)(req.body, sensitiveFields);
|
|
92
|
-
}
|
|
86
|
+
else if (isNonSerializable && devMode) {
|
|
87
|
+
(opts.logger ?? console.info)('[payload-guard] skip: non-serializable request body', 'info');
|
|
93
88
|
}
|
|
94
89
|
}
|
|
95
90
|
// Dev mode: warn about sensitive fields in request with full path
|
|
@@ -136,24 +131,54 @@ function guardMiddleware(opts = {}) {
|
|
|
136
131
|
}
|
|
137
132
|
catch { }
|
|
138
133
|
}
|
|
139
|
-
// streaming safety
|
|
140
|
-
const
|
|
141
|
-
|
|
134
|
+
// streaming/non-serializable safety
|
|
135
|
+
const isNonSerializable = data && (Buffer.isBuffer(data) ||
|
|
136
|
+
typeof data.pipe === 'function' ||
|
|
137
|
+
(typeof File !== 'undefined' && data instanceof File) ||
|
|
138
|
+
(typeof Blob !== 'undefined' && data instanceof Blob));
|
|
139
|
+
if (isNonSerializable && (opts.skipNonSerializable !== false)) {
|
|
140
|
+
if (devMode)
|
|
141
|
+
opts.logger?.('[payload-guard] skip: non-serializable payload detected', 'info');
|
|
142
142
|
return res.json(data);
|
|
143
|
-
|
|
143
|
+
}
|
|
144
|
+
// execution with fail-open safety
|
|
145
|
+
let filtered;
|
|
144
146
|
const start = performance.now();
|
|
145
|
-
|
|
147
|
+
try {
|
|
148
|
+
filtered = filterFn(data);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (opts.failOpen !== false) {
|
|
152
|
+
if (devMode)
|
|
153
|
+
opts.logger?.('[payload-guard] fail-open: filtering failed, sending original', 'warn');
|
|
154
|
+
return res.json(data);
|
|
155
|
+
}
|
|
156
|
+
throw e;
|
|
157
|
+
}
|
|
146
158
|
const end = performance.now();
|
|
147
159
|
const duration = (end - start).toFixed(2);
|
|
160
|
+
// log removed fields for debugging
|
|
161
|
+
if (opts.logRemovedFields && data && typeof data === 'object' && filtered && typeof filtered === 'object') {
|
|
162
|
+
const originalKeys = Object.keys(data);
|
|
163
|
+
const filteredKeys = Object.keys(filtered);
|
|
164
|
+
const removed = originalKeys.filter(k => !filteredKeys.includes(k));
|
|
165
|
+
if (removed.length > 0) {
|
|
166
|
+
const logger = opts.logger || console.info;
|
|
167
|
+
logger(`[payload-guard] ${routeName} Removed fields: ${removed.join(', ')}`, 'info');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
148
170
|
// metrics
|
|
149
171
|
if (payloadMetrics) {
|
|
150
172
|
try {
|
|
151
|
-
const
|
|
152
|
-
const m = metric(data, filtered);
|
|
173
|
+
const m = (0, perf_1.metric)(data, filtered);
|
|
153
174
|
const reduction = `reduced ${m.formatBefore} -> ${m.formatAfter} (${m.percent}%)`;
|
|
154
|
-
|
|
175
|
+
const logger = opts.logger || console.info;
|
|
176
|
+
logger(`[payload-guard] ${routeName}: ${reduction} in ${duration}ms`, 'info');
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
if (devMode)
|
|
180
|
+
console.warn('[payload-guard] metrics error:', e);
|
|
155
181
|
}
|
|
156
|
-
catch { }
|
|
157
182
|
}
|
|
158
183
|
return res.json(filtered);
|
|
159
184
|
}
|
package/package.json
CHANGED