agentshield-sdk 13.5.0 → 14.0.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/src/middleware.js CHANGED
@@ -14,11 +14,87 @@ const { createShieldError } = require('./errors');
14
14
  /** Coerce any value to a scannable string. */
15
15
  const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.stringify(val) : '');
16
16
 
17
+ /**
18
+ * Default maximum body size (in bytes) enforced by expressMiddleware
19
+ * when `options.maxBodySize` is not provided. Defaults to 1 MB.
20
+ */
21
+ const DEFAULT_MAX_BODY_SIZE = 1 * 1024 * 1024;
22
+
23
+ /**
24
+ * Computes the approximate size in bytes of a parsed request body.
25
+ * - String: exact UTF-8 byte length
26
+ * - Buffer: exact length
27
+ * - Object: JSON.stringify length (fallback)
28
+ *
29
+ * @param {*} body
30
+ * @returns {number}
31
+ */
32
+ const computeBodySize = (body) => {
33
+ if (body == null) return 0;
34
+ if (Buffer.isBuffer(body)) return body.length;
35
+ if (typeof body === 'string') return Buffer.byteLength(body, 'utf8');
36
+ if (typeof body === 'object') {
37
+ try {
38
+ return JSON.stringify(body).length;
39
+ } catch (_) {
40
+ return 0;
41
+ }
42
+ }
43
+ return 0;
44
+ };
45
+
46
+ /**
47
+ * Attaches a cumulative byte-counter to the raw request stream and aborts
48
+ * the request with 413 once the configured limit is exceeded. This runs
49
+ * in addition to the post-parse body size check so attackers cannot
50
+ * bypass the limit by streaming a huge payload before the body parser
51
+ * buffers it.
52
+ *
53
+ * @param {import('http').IncomingMessage} req
54
+ * @param {import('http').ServerResponse} res
55
+ * @param {number} limit
56
+ * @returns {boolean} True if the stream watcher was attached.
57
+ */
58
+ const attachRawSizeGuard = (req, res, limit) => {
59
+ if (!req || typeof req.on !== 'function') return false;
60
+ // Already read/parsed — nothing to guard.
61
+ if (req._agentShieldRawGuardAttached) return false;
62
+ req._agentShieldRawGuardAttached = true;
63
+
64
+ let received = 0;
65
+ const onData = (chunk) => {
66
+ received += chunk ? chunk.length : 0;
67
+ if (received > limit) {
68
+ req.removeListener('data', onData);
69
+ try {
70
+ if (typeof req.pause === 'function') req.pause();
71
+ if (!res.headersSent) {
72
+ res.status(413).json({
73
+ error: 'Payload Too Large',
74
+ message: `Request body exceeds maximum allowed size of ${limit} bytes`,
75
+ maxBodySize: limit
76
+ });
77
+ }
78
+ if (typeof req.destroy === 'function') req.destroy();
79
+ } catch (_) {
80
+ // Swallow — the response has already been sent or the socket closed.
81
+ }
82
+ }
83
+ };
84
+ req.on('data', onData);
85
+ return true;
86
+ };
87
+
17
88
  /**
18
89
  * Creates an Express/Connect-style middleware that scans request bodies
19
90
  * for AI-specific threats before they reach your agent endpoint.
20
91
  *
92
+ * Enforces a configurable body-size limit (default 1MB) so callers do
93
+ * not need to configure body-parser separately. Oversized payloads are
94
+ * rejected with HTTP 413 before any scanning takes place.
95
+ *
21
96
  * @param {object} [config] - AgentShield configuration.
97
+ * @param {number} [config.maxBodySize=1048576] - Maximum accepted request body size in bytes.
22
98
  * @returns {Function} Express middleware function.
23
99
  *
24
100
  * @example
@@ -27,7 +103,7 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
27
103
  *
28
104
  * const app = express();
29
105
  * app.use(express.json());
30
- * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high' }));
106
+ * app.use(expressMiddleware({ blockOnThreat: true, blockThreshold: 'high', maxBodySize: 512 * 1024 }));
31
107
  *
32
108
  * app.post('/agent', (req, res) => {
33
109
  * // req.agentShield contains scan results
@@ -39,13 +115,33 @@ const textify = (val) => typeof val === 'string' ? val : (val != null ? JSON.str
39
115
  */
40
116
  const expressMiddleware = (config = {}) => {
41
117
  const shield = new AgentShield({ blockOnThreat: true, ...config });
118
+ const maxBodySize = Number.isFinite(config.maxBodySize) && config.maxBodySize > 0
119
+ ? config.maxBodySize
120
+ : DEFAULT_MAX_BODY_SIZE;
121
+
122
+ console.log('[Agent Shield] Middleware body size limit: %dKB. Configure options.maxBodySize to override.', Math.round(maxBodySize / 1024));
42
123
 
43
124
  return (req, res, next) => {
125
+ // Attach raw-stream guard for unparsed requests so attackers cannot
126
+ // bypass the post-parse size check with huge streamed payloads.
127
+ attachRawSizeGuard(req, res, maxBodySize);
128
+
44
129
  if (!req.body) {
45
130
  req.agentShield = { status: 'safe', threats: [], blocked: false };
46
131
  return next();
47
132
  }
48
133
 
134
+ // Enforce body-size limit before scanning to avoid DoS via huge inputs.
135
+ const bodySize = computeBodySize(req.body);
136
+ if (bodySize > maxBodySize) {
137
+ return res.status(413).json({
138
+ error: 'Payload Too Large',
139
+ message: `Request body (${bodySize} bytes) exceeds maximum allowed size of ${maxBodySize} bytes`,
140
+ maxBodySize,
141
+ receivedSize: bodySize
142
+ });
143
+ }
144
+
49
145
  // Extract text from common request body shapes
50
146
  const text = extractTextFromBody(req.body);
51
147
 
@@ -306,4 +402,13 @@ const shieldMiddleware = (config = {}) => {
306
402
  };
307
403
  };
308
404
 
309
- module.exports = { expressMiddleware, wrapAgent, shieldTools, extractTextFromBody, rateLimitMiddleware, shieldMiddleware };
405
+ module.exports = {
406
+ expressMiddleware,
407
+ wrapAgent,
408
+ shieldTools,
409
+ extractTextFromBody,
410
+ rateLimitMiddleware,
411
+ shieldMiddleware,
412
+ computeBodySize,
413
+ DEFAULT_MAX_BODY_SIZE
414
+ };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Shield — Native Rust Scanner Bridge
5
+ *
6
+ * Provides a transparent bridge to the Rust-core pattern matching engine
7
+ * compiled via NAPI-RS. When the native module is available, scans run
8
+ * through Rust's RegexSet for O(n) multi-pattern matching — typically
9
+ * 5-10x faster than the pure-JS scanner on long inputs.
10
+ *
11
+ * Falls back silently to the pure-JS scanner if the native module is
12
+ * not compiled or unavailable for the current platform.
13
+ *
14
+ * Build the native module:
15
+ * cd rust-core && cargo build --release --features node
16
+ * cp target/release/libagent_shield_core.so agent-shield-core.node # Linux
17
+ * cp target/release/libagent_shield_core.dylib agent-shield-core.node # macOS
18
+ *
19
+ * @module native-scanner
20
+ */
21
+
22
+ const path = require('path');
23
+
24
+ let nativeModule = null;
25
+ let nativeAvailable = false;
26
+
27
+ const NATIVE_PATHS = [
28
+ path.join(__dirname, '..', 'rust-core', 'agent-shield-core.node'),
29
+ path.join(__dirname, '..', 'rust-core', 'target', 'release', 'agent-shield-core.node'),
30
+ path.join(__dirname, '..', 'native', 'agent-shield-core.node'),
31
+ ];
32
+
33
+ for (const p of NATIVE_PATHS) {
34
+ try {
35
+ nativeModule = require(p);
36
+ nativeAvailable = true;
37
+ console.log('[Agent Shield] Native Rust scanner loaded from: ' + path.basename(p));
38
+ break;
39
+ } catch {
40
+ // Not available at this path, try next
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Returns true if the native Rust scanner is available.
46
+ * @returns {boolean}
47
+ */
48
+ function isNativeAvailable() {
49
+ return nativeAvailable;
50
+ }
51
+
52
+ /**
53
+ * Scan text using the native Rust engine.
54
+ * Returns null if native is not available (caller should fall back to JS).
55
+ *
56
+ * @param {string} text - Text to scan.
57
+ * @returns {object|null} ScanResult or null if native unavailable.
58
+ */
59
+ function nativeScan(text) {
60
+ if (!nativeAvailable || !text || typeof text !== 'string') return null;
61
+ try {
62
+ const json = nativeModule.scanText(text);
63
+ return JSON.parse(json);
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Batch scan multiple texts using the native Rust engine.
71
+ *
72
+ * @param {string[]} texts - Array of texts to scan.
73
+ * @returns {object[]|null} Array of ScanResults or null if native unavailable.
74
+ */
75
+ function nativeScanBatch(texts) {
76
+ if (!nativeAvailable || !Array.isArray(texts)) return null;
77
+ try {
78
+ const json = nativeModule.scanBatch(texts.filter(t => typeof t === 'string'));
79
+ return JSON.parse(json);
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get all patterns from the native Rust engine.
87
+ *
88
+ * @returns {object[]|null} Array of patterns or null if native unavailable.
89
+ */
90
+ function nativeGetPatterns() {
91
+ if (!nativeAvailable) return null;
92
+ try {
93
+ return JSON.parse(nativeModule.getPatterns());
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ module.exports = {
100
+ isNativeAvailable,
101
+ nativeScan,
102
+ nativeScanBatch,
103
+ nativeGetPatterns,
104
+ };