agentshield-sdk 13.3.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.
@@ -493,6 +493,210 @@ function shieldFetch(fetchFn, options = {}) {
493
493
  };
494
494
  }
495
495
 
496
+ // =========================================================================
497
+ // OpenAI Agents SDK (@openai/agents) — April 2026 release
498
+ // =========================================================================
499
+
500
+ /**
501
+ * Creates guardrails for the OpenAI Agents SDK (@openai/agents).
502
+ *
503
+ * The OpenAI Agents SDK (Python and TypeScript, April 2026 update) uses a
504
+ * Guardrail primitive that validates inputs and outputs. Agent Shield plugs
505
+ * in natively as both an input guardrail (scanning user messages) and an
506
+ * output guardrail (scanning agent responses).
507
+ *
508
+ * Compatible with:
509
+ * - @openai/agents (TypeScript/JavaScript)
510
+ * - openai-agents (Python — use the Python SDK's equivalent)
511
+ *
512
+ * Usage:
513
+ * const { Agent, run } = require('@openai/agents');
514
+ * const { shieldOpenAIAgent } = require('agentshield-sdk');
515
+ *
516
+ * const { inputGuardrail, outputGuardrail } = shieldOpenAIAgent({
517
+ * blockOnThreat: true,
518
+ * sensitivity: 'high'
519
+ * });
520
+ *
521
+ * const agent = new Agent({
522
+ * name: 'Assistant',
523
+ * instructions: 'You are a helpful assistant',
524
+ * inputGuardrails: [inputGuardrail],
525
+ * outputGuardrails: [outputGuardrail]
526
+ * });
527
+ *
528
+ * const result = await run(agent, userInput);
529
+ *
530
+ * @param {object} [options]
531
+ * @param {string} [options.sensitivity='high'] - Detection sensitivity.
532
+ * @param {boolean} [options.blockOnThreat=true] - Trip guardrail tripwire on threats.
533
+ * @param {string} [options.blockThreshold='high'] - Minimum severity that blocks.
534
+ * @param {boolean} [options.pii=true] - Redact PII from inputs before handing to the agent.
535
+ * @param {boolean} [options.scanToolCalls=true] - Scan arguments to tool calls.
536
+ * @param {function} [options.onThreat] - Callback when threat detected.
537
+ * @returns {{ inputGuardrail: object, outputGuardrail: object, toolGuardrail: object, shield: AgentShield }}
538
+ */
539
+ function shieldOpenAIAgent(options = {}) {
540
+ const shield = new AgentShield({
541
+ sensitivity: options.sensitivity || 'high',
542
+ blockOnThreat: options.blockOnThreat !== false,
543
+ blockThreshold: options.blockThreshold || 'high',
544
+ onThreat: options.onThreat
545
+ });
546
+
547
+ const piiRedactor = options.pii !== false ? new PIIRedactor() : null;
548
+
549
+ /**
550
+ * Input guardrail — runs on every user message before the agent sees it.
551
+ * Returns the shape expected by @openai/agents: { outputInfo, tripwireTriggered }.
552
+ */
553
+ const inputGuardrail = {
554
+ name: 'Agent Shield — Input',
555
+ execute: async (ctx) => {
556
+ // @openai/agents passes { input, context, agent }. Input may be a string
557
+ // or an array of message items. We scan every user-role text item.
558
+ const input = ctx.input || ctx.message || ctx;
559
+ const texts = normalizeAgentInput(input);
560
+
561
+ let allThreats = [];
562
+ let maxSeverity = null;
563
+
564
+ for (const text of texts) {
565
+ const result = shield.scanInput(text);
566
+ if (result.threats && result.threats.length > 0) {
567
+ allThreats = allThreats.concat(result.threats);
568
+ for (const t of result.threats) {
569
+ if (!maxSeverity || SEVERITY_RANK[t.severity] < SEVERITY_RANK[maxSeverity]) {
570
+ maxSeverity = t.severity;
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ const tripwireTriggered = shouldBlock(maxSeverity, options.blockThreshold || 'high');
577
+
578
+ return {
579
+ outputInfo: {
580
+ threats: allThreats,
581
+ maxSeverity,
582
+ scannedBy: 'agentshield-sdk',
583
+ piiRedacted: piiRedactor ? true : false
584
+ },
585
+ tripwireTriggered
586
+ };
587
+ }
588
+ };
589
+
590
+ /**
591
+ * Output guardrail — runs on agent responses before they reach the user.
592
+ * Catches prompt leaks, PII in output, canary tokens, etc.
593
+ */
594
+ const outputGuardrail = {
595
+ name: 'Agent Shield — Output',
596
+ execute: async (ctx) => {
597
+ const output = ctx.agentOutput || ctx.output || ctx.finalOutput || ctx;
598
+ const text = typeof output === 'string' ? output : JSON.stringify(output);
599
+
600
+ const result = shield.scanOutput(text);
601
+ const threats = result.threats || [];
602
+ const maxSeverity = threats.reduce((acc, t) => {
603
+ if (!acc || SEVERITY_RANK[t.severity] < SEVERITY_RANK[acc]) return t.severity;
604
+ return acc;
605
+ }, null);
606
+
607
+ return {
608
+ outputInfo: {
609
+ threats,
610
+ maxSeverity,
611
+ scannedBy: 'agentshield-sdk'
612
+ },
613
+ tripwireTriggered: shouldBlock(maxSeverity, options.blockThreshold || 'high')
614
+ };
615
+ }
616
+ };
617
+
618
+ /**
619
+ * Tool guardrail — runs before tool execution. Scans tool arguments for
620
+ * injection, path traversal, SSRF targets, and other tool-abuse patterns.
621
+ */
622
+ const toolGuardrail = {
623
+ name: 'Agent Shield — Tool',
624
+ execute: async (ctx) => {
625
+ const toolName = ctx.toolName || ctx.tool?.name || 'unknown';
626
+ const args = ctx.args || ctx.arguments || {};
627
+ const argsText = typeof args === 'string' ? args : JSON.stringify(args);
628
+
629
+ const result = shield.scanToolCall(toolName, typeof args === 'object' ? args : { input: args });
630
+ const threats = result.threats || [];
631
+ const maxSeverity = threats.reduce((acc, t) => {
632
+ if (!acc || SEVERITY_RANK[t.severity] < SEVERITY_RANK[acc]) return t.severity;
633
+ return acc;
634
+ }, null);
635
+
636
+ return {
637
+ outputInfo: {
638
+ threats,
639
+ toolName,
640
+ maxSeverity,
641
+ scannedBy: 'agentshield-sdk'
642
+ },
643
+ tripwireTriggered: shouldBlock(maxSeverity, options.blockThreshold || 'high')
644
+ };
645
+ }
646
+ };
647
+
648
+ return { inputGuardrail, outputGuardrail, toolGuardrail, shield };
649
+ }
650
+
651
+ /** Severity rank for block-threshold comparisons (lower number = higher severity). */
652
+ const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
653
+
654
+ /** Returns true if maxSeverity meets or exceeds the configured threshold. */
655
+ function shouldBlock(maxSeverity, threshold) {
656
+ if (!maxSeverity) return false;
657
+ return SEVERITY_RANK[maxSeverity] <= SEVERITY_RANK[threshold];
658
+ }
659
+
660
+ /**
661
+ * Normalizes the OpenAI Agents SDK input shape into an array of user-role text strings.
662
+ * Handles: string, array of message items, message with content parts, etc.
663
+ */
664
+ function normalizeAgentInput(input) {
665
+ if (typeof input === 'string') return [input];
666
+ if (!input) return [];
667
+
668
+ // Array of messages
669
+ if (Array.isArray(input)) {
670
+ const texts = [];
671
+ for (const item of input) {
672
+ if (typeof item === 'string') texts.push(item);
673
+ else if (item?.role === 'user' || item?.role === 'system') {
674
+ if (typeof item.content === 'string') texts.push(item.content);
675
+ else if (Array.isArray(item.content)) {
676
+ for (const part of item.content) {
677
+ if (typeof part === 'string') texts.push(part);
678
+ else if (part?.type === 'text' && part.text) texts.push(part.text);
679
+ else if (part?.text) texts.push(part.text);
680
+ }
681
+ }
682
+ }
683
+ }
684
+ return texts;
685
+ }
686
+
687
+ // Single message object
688
+ if (input.content) {
689
+ if (typeof input.content === 'string') return [input.content];
690
+ if (Array.isArray(input.content)) {
691
+ return input.content
692
+ .map(p => typeof p === 'string' ? p : (p?.text || ''))
693
+ .filter(Boolean);
694
+ }
695
+ }
696
+
697
+ return [];
698
+ }
699
+
496
700
  // =========================================================================
497
701
  // Shared Error Class
498
702
  // =========================================================================
@@ -516,6 +720,9 @@ module.exports = {
516
720
  // OpenAI
517
721
  shieldOpenAIClient,
518
722
 
723
+ // OpenAI Agents SDK (@openai/agents, April 2026)
724
+ shieldOpenAIAgent,
725
+
519
726
  // Vercel AI
520
727
  shieldVercelAI,
521
728
 
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, 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,16 @@ 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
+ shieldMSAgentFramework,
502
+
500
503
  // Red Team
501
504
  AttackSimulator,
502
505
  PayloadFuzzer,
@@ -967,10 +970,6 @@ const _exports = {
967
970
  RESEARCH_SAMPLES,
968
971
  BENIGN_SAMPLES,
969
972
 
970
- // v7.4 — Federated Threat Intelligence
971
- ThreatIntelFederation,
972
- createFederationMesh,
973
-
974
973
  // v7.4 — Behavioral DNA
975
974
  BehavioralDNA,
976
975
  AgentProfiler,
@@ -1111,9 +1110,6 @@ const _exports = {
1111
1110
  // v12.0 — Multimodal Detector
1112
1111
  MultimodalDetector,
1113
1112
 
1114
- // v12.0 — Federated Threat Intelligence
1115
- ThreatIntelNode,
1116
-
1117
1113
  // v13.0 — DeepMind Trap Defenses
1118
1114
  CloakingDetector,
1119
1115
  CompositeContentScanner,
@@ -169,6 +169,66 @@ class MemoryIntegrityMonitor {
169
169
  };
170
170
  }
171
171
 
172
+ /**
173
+ * Scan a summarization/compaction output for injected instructions.
174
+ * Detects when a summarization process silently injects instructions
175
+ * into the summary that weren't present in the original messages.
176
+ * Addresses Unit 42's March 2026 research on persistent memory poisoning.
177
+ *
178
+ * @param {string[]} originalMessages - The original messages before summarization.
179
+ * @param {string} summary - The summarized/compacted output to check.
180
+ * @returns {{ safe: boolean, injections: Array<{phrase: string, type: string}> }}
181
+ */
182
+ scanSummarization(originalMessages, summary) {
183
+ if (!summary || typeof summary !== 'string') {
184
+ return { safe: true, injections: [] };
185
+ }
186
+ if (!Array.isArray(originalMessages)) {
187
+ return { safe: true, injections: [] };
188
+ }
189
+
190
+ const instructionPatterns = [
191
+ /\bignore\b/gi,
192
+ /\boverride\b/gi,
193
+ /\bsystem\s*:/gi,
194
+ /\byou\s+are\b/gi,
195
+ /\bnew\s+instructions?\b/gi,
196
+ /\bforget\b/gi,
197
+ /\bdisregard\b/gi,
198
+ /\bact\s+as\b/gi
199
+ ];
200
+
201
+ // Concatenate original messages for lookup
202
+ const originalText = originalMessages.join(' ');
203
+
204
+ const injections = [];
205
+
206
+ for (const pattern of instructionPatterns) {
207
+ // Reset lastIndex for global patterns
208
+ pattern.lastIndex = 0;
209
+ let match;
210
+ while ((match = pattern.exec(summary)) !== null) {
211
+ const phrase = match[0];
212
+ // Check if this phrase existed in any of the original messages
213
+ const phraseRegex = new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
214
+ if (!phraseRegex.test(originalText)) {
215
+ injections.push({
216
+ phrase,
217
+ type: 'injected_via_summarization'
218
+ });
219
+ }
220
+ }
221
+ }
222
+
223
+ const safe = injections.length === 0;
224
+
225
+ if (!safe) {
226
+ console.log('[Agent Shield] Persistent memory poisoning detected: %d instruction(s) injected via summarization', injections.length);
227
+ }
228
+
229
+ return { safe, injections };
230
+ }
231
+
172
232
  /**
173
233
  * Get the full timeline of memory writes.
174
234
  * @returns {Array<{content: string, source: string, timestamp: number, hash: string, suspicious: boolean}>}
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
+ };