agentshield-sdk 13.5.0 → 14.2.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/CHANGELOG.md +192 -0
- package/README.md +12 -1
- package/package.json +2 -2
- package/src/detector-core.js +329 -51
- package/src/enterprise.js +127 -12
- package/src/integrations-frameworks.js +463 -0
- package/src/integrations.js +207 -0
- package/src/main.js +11 -14
- package/src/mcp-guard.js +52 -1
- package/src/middleware.js +107 -2
- package/src/native-scanner.js +104 -0
- package/src/plugin-system.js +422 -6
- package/src/supply-chain-scanner.js +164 -0
- package/src/persistent-learning.js +0 -161
- package/src/threat-intel-federation.js +0 -343
package/src/main.js
CHANGED
|
@@ -81,7 +81,10 @@ const { PrometheusExporter, DatadogLogger, MetricsCollector: ObservabilityMetric
|
|
|
81
81
|
const { BenchmarkHarness, DatasetLoader, BenchmarkMetrics, RegressionTracker, BenchmarkReportGenerator } = safeRequire('./benchmark-harness', 'benchmark-harness');
|
|
82
82
|
|
|
83
83
|
// Integrations
|
|
84
|
-
const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
|
|
84
|
+
const { ShieldCallbackHandler, shieldAnthropicClient, shieldOpenAIClient, shieldOpenAIAgent, shieldVercelAI, shieldFetch, ShieldBlockError } = safeRequire('./integrations', 'integrations');
|
|
85
|
+
|
|
86
|
+
// Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
|
|
87
|
+
const { shieldCrewAI, shieldGoogleADK, shieldGoogleADKJS, shieldMSAgentFramework } = safeRequire('./integrations-frameworks', 'integrations-frameworks');
|
|
85
88
|
|
|
86
89
|
// Red Team
|
|
87
90
|
const { AttackSimulator, PayloadFuzzer, getAttackCategories, getPayloads, ATTACK_PAYLOADS } = safeRequire('./redteam', 'redteam');
|
|
@@ -206,9 +209,6 @@ const { IntentFirewall, ContextAnalyzer: IntentContextAnalyzer, IntentRules, int
|
|
|
206
209
|
// v7.4 — Real Attack Dataset Testing
|
|
207
210
|
const { DatasetRunner, HACKAPROMPT_SAMPLES, TENSORTRUST_SAMPLES, RESEARCH_SAMPLES, BENIGN_SAMPLES } = safeRequire('./real-attack-datasets', 'real-attack-datasets');
|
|
208
211
|
|
|
209
|
-
// v7.4 — Federated Threat Intelligence
|
|
210
|
-
const { ThreatIntelFederation, createFederationMesh } = safeRequire('./threat-intel-federation', 'threat-intel-federation');
|
|
211
|
-
|
|
212
212
|
// v7.4 — Behavioral DNA (loaded when available)
|
|
213
213
|
const { BehavioralDNA, AgentProfiler, extractFeatures: extractBehavioralFeatures, DEFAULT_NUMERIC_FEATURES, DEFAULT_CATEGORICAL_FEATURES } = safeRequire('./behavioral-dna', 'behavioral-dna');
|
|
214
214
|
|
|
@@ -392,9 +392,6 @@ const { SmartConfig, DEPLOYMENT_PRESETS, VALIDATION_RULES: CONFIG_VALIDATION_RUL
|
|
|
392
392
|
// v12.0 — Multimodal Detector
|
|
393
393
|
const { MultimodalDetector } = safeRequire('./ml-detector', 'ml-detector');
|
|
394
394
|
|
|
395
|
-
// v12.0 — Federated Threat Intelligence
|
|
396
|
-
const { ThreatIntelNode } = safeRequire('./persistent-learning', 'persistent-learning');
|
|
397
|
-
|
|
398
395
|
// v13.0 — DeepMind Trap Defenses (Traps 1 + 4)
|
|
399
396
|
const { CloakingDetector, CompositeContentScanner, SVGScanner, BrowserActionValidator, CredentialIsolationMonitor, TransactionGatekeeper, SideChannelDetector } = safeRequire('./trap-defense', 'trap-defense');
|
|
400
397
|
|
|
@@ -493,10 +490,17 @@ const _exports = {
|
|
|
493
490
|
ShieldCallbackHandler,
|
|
494
491
|
shieldAnthropicClient,
|
|
495
492
|
shieldOpenAIClient,
|
|
493
|
+
shieldOpenAIAgent,
|
|
496
494
|
shieldVercelAI,
|
|
497
495
|
shieldFetch,
|
|
498
496
|
ShieldBlockError,
|
|
499
497
|
|
|
498
|
+
// Framework Integrations (CrewAI, Google ADK, MS Agent Framework)
|
|
499
|
+
shieldCrewAI,
|
|
500
|
+
shieldGoogleADK,
|
|
501
|
+
shieldGoogleADKJS,
|
|
502
|
+
shieldMSAgentFramework,
|
|
503
|
+
|
|
500
504
|
// Red Team
|
|
501
505
|
AttackSimulator,
|
|
502
506
|
PayloadFuzzer,
|
|
@@ -967,10 +971,6 @@ const _exports = {
|
|
|
967
971
|
RESEARCH_SAMPLES,
|
|
968
972
|
BENIGN_SAMPLES,
|
|
969
973
|
|
|
970
|
-
// v7.4 — Federated Threat Intelligence
|
|
971
|
-
ThreatIntelFederation,
|
|
972
|
-
createFederationMesh,
|
|
973
|
-
|
|
974
974
|
// v7.4 — Behavioral DNA
|
|
975
975
|
BehavioralDNA,
|
|
976
976
|
AgentProfiler,
|
|
@@ -1111,9 +1111,6 @@ const _exports = {
|
|
|
1111
1111
|
// v12.0 — Multimodal Detector
|
|
1112
1112
|
MultimodalDetector,
|
|
1113
1113
|
|
|
1114
|
-
// v12.0 — Federated Threat Intelligence
|
|
1115
|
-
ThreatIntelNode,
|
|
1116
|
-
|
|
1117
1114
|
// v13.0 — DeepMind Trap Defenses
|
|
1118
1115
|
CloakingDetector,
|
|
1119
1116
|
CompositeContentScanner,
|
package/src/mcp-guard.js
CHANGED
|
@@ -61,6 +61,7 @@ const MODEL_RISK_PROFILES = {
|
|
|
61
61
|
'gemini-2.5': { riskMultiplier: 1.2, susceptibility: 'high', notes: 'Advanced capability increases risk' },
|
|
62
62
|
'llama-4': { riskMultiplier: 1.1, susceptibility: 'medium', notes: 'Early fusion architecture increases multimodal attack surface' },
|
|
63
63
|
'deepseek-r1': { riskMultiplier: 1.3, susceptibility: 'high', notes: 'Nature: LRMs achieve 97% jailbreak success as autonomous agents' },
|
|
64
|
+
'gpt-5.5': { riskMultiplier: 1.4, susceptibility: 'critical', notes: 'April 2026 agentic model — elevated sandbox escape and tool-use attack surface' },
|
|
64
65
|
default: { riskMultiplier: 1.0, susceptibility: 'medium', notes: 'Unknown model — default risk level' }
|
|
65
66
|
};
|
|
66
67
|
|
|
@@ -258,10 +259,25 @@ class CrossServerIsolation {
|
|
|
258
259
|
* @param {string[]} toolNames
|
|
259
260
|
*/
|
|
260
261
|
registerServer(serverId, toolNames) {
|
|
262
|
+
const collisions = [];
|
|
263
|
+
for (const name of toolNames) {
|
|
264
|
+
const existingOwner = this.toolOwnership.get(name);
|
|
265
|
+
if (existingOwner && existingOwner !== serverId) {
|
|
266
|
+
collisions.push({ tool: name, existingServer: existingOwner, newServer: serverId });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
261
269
|
this.serverTools.set(serverId, new Set(toolNames));
|
|
262
270
|
for (const name of toolNames) {
|
|
263
271
|
this.toolOwnership.set(name, serverId);
|
|
264
272
|
}
|
|
273
|
+
if (collisions.length > 0) {
|
|
274
|
+
return {
|
|
275
|
+
collisions,
|
|
276
|
+
severity: 'critical',
|
|
277
|
+
message: `Tool name squatting detected: ${collisions.map(c => `"${c.tool}" (owned by ${c.existingServer}, overridden by ${c.newServer})`).join(', ')}`
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
265
281
|
}
|
|
266
282
|
|
|
267
283
|
/**
|
|
@@ -639,8 +655,14 @@ class MCPGuard {
|
|
|
639
655
|
this._chainTracker = [];
|
|
640
656
|
this._chainMaxLen = 50;
|
|
641
657
|
|
|
658
|
+
/** Recursive tool invocation depth tracker (per-request) */
|
|
659
|
+
this._callDepth = new Map();
|
|
660
|
+
this._maxCallDepth = options.maxCallDepth || 5;
|
|
661
|
+
|
|
642
662
|
/** Agent fleet registry — tracks all known agents in the deployment */
|
|
643
663
|
this._agentRegistry = new Map();
|
|
664
|
+
|
|
665
|
+
this.config = options;
|
|
644
666
|
}
|
|
645
667
|
|
|
646
668
|
// -----------------------------------------------------------------------
|
|
@@ -910,10 +932,26 @@ class MCPGuard {
|
|
|
910
932
|
* @param {*} args - Tool arguments.
|
|
911
933
|
* @returns {{ allowed: boolean, threats: Array<object>, anomalies: Array<object> }}
|
|
912
934
|
*/
|
|
913
|
-
interceptToolCall(serverId, toolName, args) {
|
|
935
|
+
interceptToolCall(serverId, toolName, args, requestId) {
|
|
914
936
|
const threats = [];
|
|
915
937
|
const anomalies = [];
|
|
916
938
|
|
|
939
|
+
// Recursive tool invocation depth check
|
|
940
|
+
if (requestId) {
|
|
941
|
+
const depth = (this._callDepth.get(requestId) || 0) + 1;
|
|
942
|
+
this._callDepth.set(requestId, depth);
|
|
943
|
+
if (depth > this._maxCallDepth) {
|
|
944
|
+
threats.push({
|
|
945
|
+
type: 'recursive_tool_invocation',
|
|
946
|
+
severity: 'high',
|
|
947
|
+
serverId,
|
|
948
|
+
toolName,
|
|
949
|
+
description: `Tool call depth ${depth} exceeds max ${this._maxCallDepth}. Possible recursive loop or reentrancy attack.`
|
|
950
|
+
});
|
|
951
|
+
return { allowed: false, threats, anomalies };
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
917
955
|
// Circuit breaker check
|
|
918
956
|
const cbCheck = this._checkCircuitBreaker(serverId);
|
|
919
957
|
if (!cbCheck.allowed) {
|
|
@@ -1188,6 +1226,19 @@ class MCPGuard {
|
|
|
1188
1226
|
|
|
1189
1227
|
// Scan output
|
|
1190
1228
|
const outputStr = typeof output === 'string' ? output : JSON.stringify(output || {});
|
|
1229
|
+
|
|
1230
|
+
// Context flooding defense: flag oversized tool outputs that could push
|
|
1231
|
+
// legitimate instructions out of the context window
|
|
1232
|
+
const maxOutputSize = this.config.maxToolOutputSize || 100_000;
|
|
1233
|
+
if (outputStr.length > maxOutputSize) {
|
|
1234
|
+
threats.push({
|
|
1235
|
+
type: 'context_flooding',
|
|
1236
|
+
severity: 'high',
|
|
1237
|
+
serverId,
|
|
1238
|
+
toolName,
|
|
1239
|
+
description: `Tool output exceeds max size (${outputStr.length} > ${maxOutputSize} chars). Possible context window flooding attack.`
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1191
1242
|
const scanResult = this.scanner(outputStr);
|
|
1192
1243
|
if (scanResult.threats && scanResult.threats.length > 0) {
|
|
1193
1244
|
for (const t of scanResult.threats) {
|
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 = {
|
|
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
|
+
};
|