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 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
- - **Payload Metrics (payload reduction):** enable measurements to show before/after sizes and percentage saved. This makes cost and bandwidth impact visible to engineers.
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
- // enable metrics globally
271
- import { guard } from 'payload-guard';
285
+ app.use(guardMiddleware({
286
+ sanitizeHeaders: true,
287
+ sensitiveHeaders: ['x-api-key', 'session-id'] // optional extras
288
+ }));
289
+ ```
272
290
 
273
- guard.config({
274
- payloadMetrics: true,
275
- logger: (msg, level = 'info') => console.log(`[payload-guard:${level}]`, msg),
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
- When enabled middleware or `res.guardJson()` will log reductions such as:
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 reduced: 18KB 4KB (78%)
309
+ [payload-guard] [POST] /api/data: reduced 15KB -> 4KB (73%) in 0.12ms
283
310
  ```
284
311
 
285
- - **Strict Mode:** in strict mode the filter will throw on invalid or missing required fields instead of silently ignoring them. Use when you want validation failures to fail-fast in production.
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
- // or global
292
- guard.config({ strict: true });
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
- - **Compile Mode:** pre-compile shapes once and reuse the compiled function for maximum throughput. This avoids repeated runtime parsing and makes performance predictable.
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
- // compile once at startup
299
- const compiledUser = guard.compile({ id: 'number', name: 'string' });
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
- // then use per-request
302
- app.get('/user/:id', (req, res) => {
303
- const raw = getUserFromDb();
304
- res.json(compiledUser(raw));
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
- - **Ignore Routes:** skip middleware processing for low-value routes (health checks, metrics, webhooks) to avoid adding overhead.
309
-
364
+ ### Fastify
310
365
  ```ts
311
- app.use(guardMiddleware({ ignoreRoutes: ['/health', '/metrics', '/webhook'] }));
366
+ fastify.post('/user', (req, reply) => {
367
+ reply.send(userShape(req.body));
368
+ });
312
369
  ```
313
370
 
314
- - **Middleware non-blocking guarantee:** the middleware is defensive — any internal error, oversized payload, or stream-like body will be skipped and the response path will continue unblocked. Use `logger` or `devMode` to surface warnings.
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
 
@@ -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
@@ -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?.sensitive || []), ...(globalConfig.sensitiveFields || [])],
40
- dev: opts?.dev ?? globalConfig.devMode,
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, { sensitive: opts?.sensitive, dev: opts?.dev, strict: opts?.strict, redact: opts?.redact, logger: opts?.logger }),
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
@@ -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 && typeof req.body === 'object') {
53
- // max payload size check
54
- if (typeof maxPayloadSize === 'number') {
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
- const size = Buffer.byteLength(JSON.stringify(req.body || ''), 'utf8');
57
- if (size > maxPayloadSize) {
58
- if (devMode) {
59
- (opts.logger ?? console.warn)(`[payload-guard] ${routeName}: payload exceeds maxPayloadSize (${size} > ${maxPayloadSize} bytes)`, 'warn');
60
- }
61
- // Skip processing if skipLargePayload is true (default), else still process
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] error checking payload size: ' + String(e), 'warn');
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
- if (compiledRequestShape) {
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: if body looks like a stream, skip
140
- const isStreamLike = data && typeof data.pipe === 'function';
141
- if (isStreamLike)
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
- // timing start
143
+ }
144
+ // execution with fail-open safety
145
+ let filtered;
144
146
  const start = performance.now();
145
- const filtered = filterFn(data);
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 { metric } = require('../core/perf');
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
- (opts.logger ?? console.info)(`[payload-guard] ${routeName}: ${reduction} in ${duration}ms`, 'info');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payload-guard-filter",
3
- "version": "1.2.0",
3
+ "version": "1.3.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",