payload-guard-filter 1.0.1 → 1.1.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.
@@ -5,6 +5,10 @@ interface CompileOptions {
5
5
  strict?: boolean;
6
6
  redact?: (key: string, value: unknown) => unknown;
7
7
  logger?: (msg: string, level?: 'warn' | 'error' | 'info') => void;
8
+ /** Maximum array length to process (memory safety) */
9
+ maxArrayLength?: number;
10
+ /** Parent path for nested field warnings (internal) */
11
+ _parentPath?: string;
8
12
  }
9
13
  /**
10
14
  * Compile a shape descriptor into an optimized filter function
@@ -57,11 +57,20 @@ function compile(shape, opts = {}) {
57
57
  }
58
58
  // Array shape
59
59
  if (isArrayShape(shape)) {
60
- const itemFn = compile(shape.item, opts);
60
+ const itemFn = compile(shape.item, { ...opts, _parentPath: (opts._parentPath || '') + '[]' });
61
61
  return (value) => {
62
62
  if (!Array.isArray(value))
63
63
  return [];
64
- return value.map(item => {
64
+ // Memory safety: slice array if maxArrayLength is set
65
+ let arr = value;
66
+ if (opts.maxArrayLength && arr.length > opts.maxArrayLength) {
67
+ if (opts.dev) {
68
+ const path = opts._parentPath || 'array';
69
+ (opts.logger ?? console.warn)(`[payload-guard] ${path}: array truncated from ${arr.length} to ${opts.maxArrayLength} items (maxArrayLength)`, 'warn');
70
+ }
71
+ arr = arr.slice(0, opts.maxArrayLength);
72
+ }
73
+ return arr.map(item => {
65
74
  try {
66
75
  return itemFn(item);
67
76
  }
@@ -92,7 +101,7 @@ function compile(shape, opts = {}) {
92
101
  }
93
102
  continue;
94
103
  }
95
- fieldFilters[key] = compile(shapeObj[key], opts);
104
+ fieldFilters[key] = compile(shapeObj[key], { ...opts, _parentPath: (opts._parentPath ? opts._parentPath + '.' : '') + key });
96
105
  }
97
106
  return (value) => {
98
107
  if (value == null || typeof value !== 'object')
@@ -31,14 +31,26 @@ export interface GuardConfig {
31
31
  strict?: boolean;
32
32
  /** Redaction hook: return replacement value for a key */
33
33
  redact?: (key: string, value: unknown) => unknown;
34
- /** Logger integration hook */
34
+ /**
35
+ * Logger integration hook for custom logging (pino, winston, etc.)
36
+ * @example
37
+ * logger: (msg, level) => pino[level || 'info'](msg)
38
+ */
35
39
  logger?: (msg: string, level?: 'warn' | 'error' | 'info') => void;
36
40
  /** Maximum payload size (bytes) for middleware processing; if exceeded, processing is skipped */
37
41
  maxPayloadSize?: number;
42
+ /** Skip processing if payload exceeds maxPayloadSize (default: true) */
43
+ skipLargePayload?: boolean;
44
+ /** Maximum array length to process (memory safety) */
45
+ maxArrayLength?: number;
38
46
  /** Routes to ignore (prefix match) */
39
47
  ignoreRoutes?: string[];
40
48
  /** Enable payload metrics (measure before/after sizes) */
41
49
  payloadMetrics?: boolean;
50
+ /** Sanitize request headers (remove sensitive headers) */
51
+ sanitizeHeaders?: boolean;
52
+ /** Sensitive header names to filter (default: authorization, cookie, x-api-key) */
53
+ sensitiveHeaders?: string[];
42
54
  }
43
55
  export interface GuardMiddlewareOptions extends GuardConfig {
44
56
  /** Sanitize request body */
@@ -19,10 +19,12 @@ const security_1 = require("../core/security");
19
19
  * ```
20
20
  */
21
21
  function guardMiddleware(opts = {}) {
22
- const { sanitizeBody = false, requestShape, sensitiveFields = [], devMode = false, filterResponse = false, maxPayloadSize, ignoreRoutes = [], payloadMetrics = false, } = opts;
22
+ const { sanitizeBody = false, requestShape, sensitiveFields = [], devMode = false, filterResponse = false, maxPayloadSize, skipLargePayload = true, maxArrayLength, ignoreRoutes = [], payloadMetrics = false, sanitizeHeaders = false, sensitiveHeaders = ['authorization', 'cookie', 'x-api-key', 'x-auth-token'], } = opts;
23
+ // Default sensitive headers
24
+ const DEFAULT_SENSITIVE_HEADERS = ['authorization', 'cookie', 'x-api-key', 'x-auth-token', 'x-session-id'];
23
25
  // Pre-compile request shape if provided
24
26
  const compiledRequestShape = requestShape
25
- ? (0, compiler_1.compile)(requestShape, { sensitive: sensitiveFields, dev: devMode })
27
+ ? (0, compiler_1.compile)(requestShape, { sensitive: sensitiveFields, dev: devMode, maxArrayLength, logger: opts.logger })
26
28
  : null;
27
29
  // Response filter cache for shapes to avoid recompiling per request
28
30
  const responseFilterCache = new WeakMap();
@@ -32,6 +34,20 @@ function guardMiddleware(opts = {}) {
32
34
  if (ignoreRoutes && ignoreRoutes.some(p => req.path.startsWith(p))) {
33
35
  return next();
34
36
  }
37
+ const routeName = `[${req.method}] ${req.path}`;
38
+ // Sanitize headers if enabled
39
+ if (sanitizeHeaders && req.headers) {
40
+ const allSensitiveHeaders = [...DEFAULT_SENSITIVE_HEADERS, ...(sensitiveHeaders || [])];
41
+ for (const header of allSensitiveHeaders) {
42
+ const headerLower = header.toLowerCase();
43
+ if (req.headers[headerLower]) {
44
+ if (devMode) {
45
+ (opts.logger ?? console.warn)(`[payload-guard] ${routeName}: Sensitive header "${header}" removed`, 'warn');
46
+ }
47
+ delete req.headers[headerLower];
48
+ }
49
+ }
50
+ }
35
51
  // Sanitize request body
36
52
  if (sanitizeBody && req.body && typeof req.body === 'object') {
37
53
  // max payload size check
@@ -40,9 +56,17 @@ function guardMiddleware(opts = {}) {
40
56
  const size = Buffer.byteLength(JSON.stringify(req.body || ''), 'utf8');
41
57
  if (size > maxPayloadSize) {
42
58
  if (devMode) {
43
- opts.logger?.('[payload-guard] payload exceeds maxPayloadSize; skipping sanitization', 'warn');
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);
68
+ }
44
69
  }
45
- // do not block; skip processing
46
70
  }
47
71
  else {
48
72
  if (compiledRequestShape) {
@@ -56,7 +80,7 @@ function guardMiddleware(opts = {}) {
56
80
  catch (e) {
57
81
  // if serialization fails, skip and continue
58
82
  if (devMode)
59
- opts.logger?.('[payload-guard] error checking payload size: ' + String(e), 'warn');
83
+ (opts.logger ?? console.warn)('[payload-guard] error checking payload size: ' + String(e), 'warn');
60
84
  }
61
85
  }
62
86
  else {
@@ -68,13 +92,20 @@ function guardMiddleware(opts = {}) {
68
92
  }
69
93
  }
70
94
  }
71
- // Dev mode: warn about sensitive fields in request
95
+ // Dev mode: warn about sensitive fields in request with full path
72
96
  if (devMode && req.body && typeof req.body === 'object') {
73
- for (const key of Object.keys(req.body)) {
74
- if ((0, security_1.isSensitive)(key, sensitiveFields)) {
75
- console.warn(`[payload-guard] ⚠️ Sensitive field "${key}" present in request body`);
97
+ const warnSensitive = (obj, path) => {
98
+ for (const key of Object.keys(obj)) {
99
+ const fullPath = path ? `${path}.${key}` : key;
100
+ if ((0, security_1.isSensitive)(key, sensitiveFields)) {
101
+ (opts.logger ?? console.warn)(`[payload-guard] ${routeName}: Unexpected field: ${fullPath}`, 'warn');
102
+ }
103
+ if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
104
+ warnSensitive(obj[key], fullPath);
105
+ }
76
106
  }
77
- }
107
+ };
108
+ warnSensitive(req.body, '');
78
109
  }
79
110
  // Attach guardJson helper to response
80
111
  res.guardJson = function (shape, data) {
package/package.json CHANGED
@@ -1,23 +1,23 @@
1
1
  {
2
2
  "name": "payload-guard-filter",
3
- "version": "1.0.1",
3
+ "version": "1.1.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
- "module": "dist/index.mjs",
6
+ "module": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "import": "./dist/index.mjs",
10
+ "import": "./dist/index.js",
11
11
  "require": "./dist/index.js",
12
12
  "types": "./dist/index.d.ts"
13
13
  },
14
14
  "./express": {
15
- "import": "./dist/express.mjs",
15
+ "import": "./dist/express.js",
16
16
  "require": "./dist/express.js",
17
17
  "types": "./dist/express.d.ts"
18
18
  },
19
19
  "./client": {
20
- "import": "./dist/client.mjs",
20
+ "import": "./dist/client.js",
21
21
  "require": "./dist/client.js",
22
22
  "types": "./dist/client.d.ts"
23
23
  }
@@ -41,7 +41,8 @@
41
41
  "test:watch": "vitest",
42
42
  "test:coverage": "vitest run --coverage",
43
43
  "lint": "eslint src --ext .ts",
44
- "format": "prettier --write \"src/**/*.ts\""
44
+ "format": "prettier --write \"src/**/*.ts\"",
45
+ "benchmark": "npx ts-node benchmarks/run.ts"
45
46
  },
46
47
  "keywords": [
47
48
  "payload",
@@ -59,6 +60,7 @@
59
60
  ],
60
61
  "author": "",
61
62
  "license": "MIT",
63
+ "sideEffects": false,
62
64
  "engines": {
63
65
  "node": ">=18.0.0"
64
66
  },