payload-guard-filter 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # payload-guard
2
-
2
+ > Part of the [Professional Node.js Backend Toolkit](https://github.com/sannuk79/PROJECTS-AND-NPM-PACKAGES-)
3
3
  <p align="center">
4
4
  <strong>🛡️ Lightweight, zero-dependency shape-based payload filtering and sanitization</strong>
5
5
  </p>
6
6
 
7
+
7
8
  <p align="center">
8
9
  <img src="https://img.shields.io/badge/bundle%20size-%3C8KB-brightgreen" alt="Bundle Size">
9
10
  <img src="https://img.shields.io/badge/dependencies-0-blue" alt="Zero Dependencies">
@@ -13,6 +14,21 @@
13
14
 
14
15
  ---
15
16
 
17
+ ## 🛡️ Workflow
18
+
19
+ ```mermaid
20
+ graph LR
21
+ A[Request] --> B(Gatekeeper)
22
+ B --> C{Shape Check}
23
+ C -- Valid --> D[Redact & Clean]
24
+ C -- Invalid --> E[Strict Error / Fail Safe]
25
+ D --> F[Secure Response]
26
+ E --> F
27
+ F --> G((Metrics))
28
+ ```
29
+
30
+ ---
31
+
16
32
  ## ✨ Features
17
33
 
18
34
  - **Shape-based filtering** — Define what you want, auto-remove everything else
@@ -260,58 +276,107 @@ const postShape = guard.shape({
260
276
 
261
277
  ---
262
278
 
263
- ## 🏢 Enterprise Features
264
-
265
- These features are designed for high-throughput, production systems where bandwidth, predictability and observability matter.
279
+ ## 🏢 Enterprise Features (v1.2.0+)
266
280
 
267
- - **Payload Metrics (payload reduction):** enable measurements to show before/after sizes and percentage saved. This makes cost and bandwidth impact visible to engineers.
281
+ Designed for high-performance production systems, Payload Guard includes enterprise-grade features for security, observability, and performance.
268
282
 
283
+ ### 🛡️ 1. Header Sanitization
284
+ Automatically remove sensitive headers like `Authorization` or `Cookie` before your business logic handles the request.
269
285
  ```ts
270
- // enable metrics globally
271
- import { guard } from 'payload-guard';
286
+ app.use(guardMiddleware({
287
+ sanitizeHeaders: true,
288
+ sensitiveHeaders: ['x-api-key', 'session-id'] // optional extras
289
+ }));
290
+ ```
272
291
 
273
- guard.config({
274
- payloadMetrics: true,
275
- logger: (msg, level = 'info') => console.log(`[payload-guard:${level}]`, msg),
276
- });
292
+ ### 🧠 2. Memory Safety (`maxArrayLength`)
293
+ Prevent DoS attacks via extremely large arrays by automatically truncating them to a safe limit.
294
+ ```ts
295
+ const shape = guard.shape({ items: guard.array('string') }, { maxArrayLength: 1000 });
277
296
  ```
278
297
 
279
- When enabled middleware or `res.guardJson()` will log reductions such as:
298
+ ### 3. Async Safety (`maxPayloadSize`)
299
+ Avoid processing massive JSON payloads that could block the event loop.
300
+ ```ts
301
+ app.use(guardMiddleware({
302
+ maxPayloadSize: 1024 * 512, // 512KB
303
+ skipLargePayload: true // Skips filtering if too large
304
+ }));
305
+ ```
280
306
 
307
+ ### ⏱️ 4. Middleware Timing Stats
308
+ Track exactly how much time Payload Guard adds to your request cycle.
281
309
  ```
282
- payload reduced: 18KB 4KB (78%)
310
+ [payload-guard] [POST] /api/data: reduced 15KB -> 4KB (73%) in 0.12ms
283
311
  ```
284
312
 
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 });
313
+ ### 📊 5. Human-Readable Metrics
314
+ Visibility into bandwidth savings with formatted byte sizes and percentages.
290
315
 
291
- // or global
292
- guard.config({ strict: true });
316
+ ### 🛠️ 6. CLI Type Sync
317
+ Generate frontend TypeScript types from your backend shapes with one command.
318
+ ```bash
319
+ npx payload-guard sync
293
320
  ```
294
321
 
295
- - **Compile Mode:** pre-compile shapes once and reuse the compiled function for maximum throughput. This avoids repeated runtime parsing and makes performance predictable.
322
+ ### ⚠️ 7. Route-Aware Dev Warnings
323
+ Dev mode warnings now include the full method, path, and nested field locations (e.g., `user.profile.password`) for faster debugging.
296
324
 
325
+ ### 🛣️ 8. Ignore Routes
326
+ Skip processing for high-volume or incompatible routes like file uploads or health checks.
297
327
  ```ts
298
- // compile once at startup
299
- const compiledUser = guard.compile({ id: 'number', name: 'string' });
328
+ app.use(guardMiddleware({ ignoreRoutes: ['/health', '/upload'] }));
329
+ ```
330
+
331
+ ### 🏗️ 9. Shared Schemas & Examples
332
+ Built-in support for reusable schemas across your monorepo and a `examples/real-world` project for reference.
333
+
334
+ ### 🚀 10. Performance Benchmarks
335
+ A dedicated benchmark suite to verify sub-millisecond overhead. `npm run benchmark`.
336
+
337
+ ---
338
+
339
+ ## 🏗️ Production Hardening
340
+
341
+ ### 🛡️ Fail-Safe Mode (`failOpen`)
342
+ 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.
343
+
344
+ ### 🚫 Non-Serializable Protection
345
+ Payload Guard automatically detects and skips `Buffer`, `Stream`, `File`, and `Blob` objects to prevent crashes and performance bottlenecks.
346
+
347
+ ### 🔍 Detailed Debug Logs
348
+ Enable `logRemovedFields: true` to see exactly which fields were stripped from your payloads:
349
+ `[payload-guard] /api/user Removed fields: password, token, internal_id`
300
350
 
301
- // then use per-request
302
- app.get('/user/:id', (req, res) => {
303
- const raw = getUserFromDb();
304
- res.json(compiledUser(raw));
351
+ ---
352
+
353
+ ## 🌐 Multi-Framework Support
354
+
355
+ The core is zero-dependency and framework-agnostic. Use it anywhere:
356
+
357
+ ### Hono / Cloudflare Workers
358
+ ```ts
359
+ app.post('/user', async (c) => {
360
+ const body = await c.req.json();
361
+ return c.json(userShape(body));
305
362
  });
306
363
  ```
307
364
 
308
- - **Ignore Routes:** skip middleware processing for low-value routes (health checks, metrics, webhooks) to avoid adding overhead.
309
-
365
+ ### Fastify
310
366
  ```ts
311
- app.use(guardMiddleware({ ignoreRoutes: ['/health', '/metrics', '/webhook'] }));
367
+ fastify.post('/user', (req, reply) => {
368
+ reply.send(userShape(req.body));
369
+ });
312
370
  ```
313
371
 
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.
372
+ ---
373
+
374
+ ## 🛑 When NOT to use
375
+
376
+ Payload Guard is optimized for JSON APIs. It is **not** suitable for:
377
+ 1. **Binary Data**: PDFs, Images, or raw Buffers.
378
+ 2. **File Streams**: Use `multer` or similar for multipart data.
379
+ 3. **Heavy Computation**: Don't use it on payloads > 10MB without adjusting `maxPayloadSize`.
315
380
 
316
381
  ---
317
382
 
@@ -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.1",
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",