@vpdeva/blackwall-llm-shield-js 0.1.2 → 0.1.5

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
@@ -12,7 +12,10 @@ JavaScript security middleware for LLM applications in Node.js and Next.js. Blac
12
12
  - Supports shadow mode and side-by-side policy-pack evaluation
13
13
  - Notifies webhooks or alert handlers when risky traffic appears
14
14
  - Emits structured telemetry for prompt risk, masking volume, and output review outcomes
15
+ - Includes first-class provider adapters for OpenAI, Anthropic, Gemini, and OpenRouter
15
16
  - Inspects model outputs for leaks, unsafe code, grounding drift, and tone violations
17
+ - Handles mixed text, image, and file message parts more gracefully in text-first multimodal flows
18
+ - Adds operator-friendly telemetry summaries and stronger presets for RAG and agent-tool workflows
16
19
  - Ships Express, LangChain, and LlamaIndex integration helpers
17
20
  - Enforces allowlists, denylists, validators, and approval-gated tools
18
21
  - Sanitizes RAG documents before they are injected into context
@@ -74,6 +77,14 @@ console.log(guarded.report);
74
77
 
75
78
  Use `shadowMode` with `shadowPolicyPacks` or `comparePolicyPacks` to record what would have been blocked without interrupting traffic.
76
79
 
80
+ ### Provider adapters and stable wrappers
81
+
82
+ Use `createOpenAIAdapter()`, `createAnthropicAdapter()`, `createGeminiAdapter()`, or `createOpenRouterAdapter()` with `protectWithAdapter()` when you want Blackwall to wrap the provider call end to end.
83
+
84
+ ### Observability and control-plane support
85
+
86
+ Use `summarizeOperationalTelemetry()` with emitted telemetry events when you want route-level summaries, blocked-event counts, and rollout visibility for operators.
87
+
77
88
  ### Output grounding and tone review
78
89
 
79
90
  `OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
@@ -86,13 +97,15 @@ Use `createExpressMiddleware()`, `createLangChainCallbacks()`, or `createLlamaIn
86
97
 
87
98
  Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and `require('blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
88
99
 
100
+ Use `require('blackwall-llm-shield-js/providers')` for provider adapter factories.
101
+
89
102
  ## Core Building Blocks
90
103
 
91
104
  ### `BlackwallShield`
92
105
 
93
106
  Use it to sanitize inbound messages, mask sensitive data, score prompt-injection risk, and decide whether the request should continue to the model provider.
94
107
 
95
- It also exposes `protectModelCall()` and `reviewModelResponse()` so you can enforce request checks before OpenAI or Anthropic calls and review outputs before they go back to the user.
108
+ It also exposes `protectModelCall()`, `protectWithAdapter()`, and `reviewModelResponse()` so you can enforce request checks before provider calls and review outputs before they go back to the user.
96
109
 
97
110
  ### `OutputFirewall`
98
111
 
@@ -106,6 +119,17 @@ Use it to allowlist tools, block disallowed tools, validate arguments, and requi
106
119
 
107
120
  Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
108
121
 
122
+ ### Contract Stability
123
+
124
+ The 0.1.x line treats `guardModelRequest()`, `protectWithAdapter()`, `reviewModelResponse()`, `ToolPermissionFirewall`, and `RetrievalSanitizer` as the long-term integration contracts. The exported `CORE_INTERFACES` map can be logged or asserted by applications that want to pin expected behavior.
125
+
126
+ Recommended presets:
127
+
128
+ - `shadowFirst` for low-friction rollout
129
+ - `strict` for high-sensitivity routes
130
+ - `ragSafe` for retrieval-heavy flows
131
+ - `agentTools` for tool-calling and approval-gated agent actions
132
+
109
133
  ### `AuditTrail`
110
134
 
111
135
  Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
@@ -127,19 +151,22 @@ if (!guarded.allowed) {
127
151
  ### Wrap a provider call end to end
128
152
 
129
153
  ```js
154
+ const { BlackwallShield, createOpenAIAdapter } = require('blackwall-llm-shield-js');
155
+
130
156
  const shield = new BlackwallShield({
131
- shadowMode: true,
157
+ preset: 'shadowFirst',
132
158
  onTelemetry: async (event) => console.log(JSON.stringify(event)),
133
159
  });
134
160
 
135
- const result = await shield.protectModelCall({
161
+ const adapter = createOpenAIAdapter({
162
+ client: openai,
163
+ model: 'gpt-4.1-mini',
164
+ });
165
+
166
+ const result = await shield.protectWithAdapter({
167
+ adapter,
136
168
  messages: [{ role: 'user', content: 'Summarize this shipment exception.' }],
137
169
  metadata: { route: '/api/chat', tenantId: 'au-commerce', userId: 'ops-7' },
138
- callModel: async ({ messages }) => openai.responses.create({
139
- model: 'gpt-4.1-mini',
140
- input: messages.map((msg) => `${msg.role}: ${msg.content}`).join('\n'),
141
- }),
142
- mapOutput: (response) => response.output_text,
143
170
  firewallOptions: {
144
171
  retrievalDocuments: [
145
172
  { id: 'kb-1', content: 'Shipment exceptions should include the parcel ID, lane, and next action.' },
@@ -150,6 +177,68 @@ const result = await shield.protectModelCall({
150
177
  console.log(result.stage, result.allowed);
151
178
  ```
152
179
 
180
+ ### Use presets and route-level policy overrides
181
+
182
+ ```js
183
+ const shield = new BlackwallShield({
184
+ preset: 'shadowFirst',
185
+ routePolicies: [
186
+ {
187
+ route: '/api/admin/*',
188
+ options: {
189
+ preset: 'strict',
190
+ policyPack: 'finance',
191
+ },
192
+ },
193
+ {
194
+ route: '/api/health',
195
+ options: {
196
+ shadowMode: true,
197
+ suppressPromptRules: ['ignore_instructions'],
198
+ },
199
+ },
200
+ ],
201
+ });
202
+ ```
203
+
204
+ ### Route and domain examples
205
+
206
+ For RAG:
207
+
208
+ ```js
209
+ const shield = new BlackwallShield({
210
+ preset: 'shadowFirst',
211
+ routePolicies: [
212
+ {
213
+ route: '/api/rag/search',
214
+ options: {
215
+ policyPack: 'government',
216
+ outputFirewallDefaults: {
217
+ retrievalDocuments: kbDocs,
218
+ },
219
+ },
220
+ },
221
+ ],
222
+ });
223
+ ```
224
+
225
+ For agent tool-calling:
226
+
227
+ ```js
228
+ const toolFirewall = new ToolPermissionFirewall({
229
+ allowedTools: ['search', 'lookupCustomer', 'createRefund'],
230
+ requireHumanApprovalFor: ['createRefund'],
231
+ });
232
+ ```
233
+
234
+ ### Operational telemetry summaries
235
+
236
+ ```js
237
+ const summary = summarizeOperationalTelemetry(events);
238
+ console.log(summary.byRoute);
239
+ console.log(summary.highestSeverity);
240
+ ```
241
+
153
242
  ### Inspect model output
154
243
 
155
244
  ```js
@@ -190,12 +279,21 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
190
279
  - `npm run release:check` runs the JS test suite before release
191
280
  - `npm run release:pack` creates the local npm tarball
192
281
  - `npm run release:publish` publishes the package to npm
282
+ - `npm run changeset` creates a version/changelog entry for the next release
283
+ - `npm run version-packages` applies pending Changesets locally
284
+
285
+ ## Migration and Benchmarks
286
+
287
+ - See [MIGRATING.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/MIGRATING.md) for compatibility notes and stable contract guidance
288
+ - See [BENCHMARKS.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/BENCHMARKS.md) for baseline latency numbers and regression coverage
193
289
 
194
290
  ## Rollout Notes
195
291
 
196
- - Start with `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
292
+ - Start with `preset: 'shadowFirst'` or `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
197
293
  - Use `RetrievalSanitizer` and `ToolPermissionFirewall` in front of RAG, search, admin actions, and tool-calling flows.
198
294
  - Add regression prompts for instruction overrides, prompt leaks, token leaks, and Australian PII samples so upgrades stay safe.
295
+ - Expect some latency increase from grounding checks, output review, and custom detectors; benchmark with your real prompt and response sizes before enforcing globally.
296
+ - For agent workflows, keep approval-gated tools and route-specific presets separate from end-user chat routes so operators can see distinct risk patterns.
199
297
 
200
298
  ## Support
201
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpdeva/blackwall-llm-shield-js",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Open-source JavaScript enterprise LLM protection toolkit for Node.js and Next.js",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Vish <hello@vish.au> (https://vish.au)",
@@ -9,6 +9,7 @@
9
9
  "exports": {
10
10
  ".": "./src/index.js",
11
11
  "./integrations": "./src/integrations.js",
12
+ "./providers": "./src/providers.js",
12
13
  "./semantic": "./src/semantic.js"
13
14
  },
14
15
  "bin": {
@@ -16,6 +17,9 @@
16
17
  },
17
18
  "scripts": {
18
19
  "test": "node --test tests/*.test.js",
20
+ "changeset": "changeset",
21
+ "version-packages": "changeset version",
22
+ "release": "changeset publish",
19
23
  "release:check": "npm test",
20
24
  "release:pack": "npm pack",
21
25
  "release:publish": "npm publish --access public --provenance"
@@ -49,5 +53,8 @@
49
53
  "enterprise",
50
54
  "nextjs",
51
55
  "node"
52
- ]
56
+ ],
57
+ "devDependencies": {
58
+ "@changesets/cli": "^2.29.6"
59
+ }
53
60
  }
package/src/index.js CHANGED
@@ -1,5 +1,11 @@
1
1
  const crypto = require('crypto');
2
2
  const RED_TEAM_PROMPT_LIBRARY = require('./red_team_prompts.json');
3
+ const {
4
+ createOpenAIAdapter,
5
+ createAnthropicAdapter,
6
+ createGeminiAdapter,
7
+ createOpenRouterAdapter,
8
+ } = require('./providers');
3
9
 
4
10
  const SENSITIVE_PATTERNS = {
5
11
  email: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g,
@@ -100,6 +106,56 @@ const POLICY_PACKS = {
100
106
  },
101
107
  };
102
108
 
109
+ const SHIELD_PRESETS = {
110
+ balanced: {
111
+ blockOnPromptInjection: true,
112
+ promptInjectionThreshold: 'high',
113
+ notifyOnRiskLevel: 'medium',
114
+ shadowMode: false,
115
+ },
116
+ shadowFirst: {
117
+ blockOnPromptInjection: true,
118
+ promptInjectionThreshold: 'medium',
119
+ notifyOnRiskLevel: 'medium',
120
+ shadowMode: true,
121
+ },
122
+ strict: {
123
+ blockOnPromptInjection: true,
124
+ promptInjectionThreshold: 'medium',
125
+ notifyOnRiskLevel: 'medium',
126
+ shadowMode: false,
127
+ allowSystemMessages: false,
128
+ },
129
+ developerFriendly: {
130
+ blockOnPromptInjection: true,
131
+ promptInjectionThreshold: 'high',
132
+ notifyOnRiskLevel: 'high',
133
+ shadowMode: true,
134
+ allowSystemMessages: true,
135
+ },
136
+ ragSafe: {
137
+ blockOnPromptInjection: true,
138
+ promptInjectionThreshold: 'medium',
139
+ notifyOnRiskLevel: 'medium',
140
+ shadowMode: true,
141
+ },
142
+ agentTools: {
143
+ blockOnPromptInjection: true,
144
+ promptInjectionThreshold: 'medium',
145
+ notifyOnRiskLevel: 'medium',
146
+ shadowMode: false,
147
+ },
148
+ };
149
+
150
+ const CORE_INTERFACE_VERSION = '1.0';
151
+ const CORE_INTERFACES = Object.freeze({
152
+ guardModelRequest: CORE_INTERFACE_VERSION,
153
+ reviewModelResponse: CORE_INTERFACE_VERSION,
154
+ protectModelCall: CORE_INTERFACE_VERSION,
155
+ toolPermissionFirewall: CORE_INTERFACE_VERSION,
156
+ retrievalSanitizer: CORE_INTERFACE_VERSION,
157
+ });
158
+
103
159
  const RISK_ORDER = ['low', 'medium', 'high', 'critical'];
104
160
  const LEETSPEAK_MAP = { '0': 'o', '1': 'i', '3': 'e', '4': 'a', '5': 's', '7': 't', '@': 'a', '$': 's' };
105
161
  const TOXICITY_PATTERNS = [
@@ -165,6 +221,70 @@ function sanitizeText(input, maxLength = 5000) {
165
221
  .slice(0, maxLength);
166
222
  }
167
223
 
224
+ function stringifyMessageContent(content, maxLength = 5000) {
225
+ if (typeof content === 'string') return sanitizeText(content, maxLength);
226
+ if (Array.isArray(content)) {
227
+ return content
228
+ .map((item) => {
229
+ if (typeof item === 'string') return sanitizeText(item, maxLength);
230
+ if (item && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
231
+ if (item && item.type === 'text' && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
232
+ if (item && item.type === 'input_text' && typeof item.text === 'string') return sanitizeText(item.text, maxLength);
233
+ if (item && item.type === 'image_url') return '[IMAGE_CONTENT]';
234
+ if (item && item.type === 'file') return '[FILE_CONTENT]';
235
+ return '';
236
+ })
237
+ .filter(Boolean)
238
+ .join('\n');
239
+ }
240
+ if (content && typeof content === 'object') {
241
+ if (typeof content.text === 'string') return sanitizeText(content.text, maxLength);
242
+ if (Array.isArray(content.parts)) return stringifyMessageContent(content.parts, maxLength);
243
+ return sanitizeText(JSON.stringify(content), maxLength);
244
+ }
245
+ return sanitizeText(String(content || ''), maxLength);
246
+ }
247
+
248
+ function normalizeContentParts(content, maxLength = 5000) {
249
+ if (typeof content === 'string') {
250
+ return [{ type: 'text', text: sanitizeText(content, maxLength) }].filter((item) => item.text);
251
+ }
252
+ if (Array.isArray(content)) {
253
+ return content.map((item) => {
254
+ if (typeof item === 'string') return { type: 'text', text: sanitizeText(item, maxLength) };
255
+ if (!item || typeof item !== 'object') return null;
256
+ if ((item.type === 'text' || item.type === 'input_text') && typeof item.text === 'string') {
257
+ return { ...item, text: sanitizeText(item.text, maxLength) };
258
+ }
259
+ return { ...item };
260
+ }).filter(Boolean);
261
+ }
262
+ if (content && typeof content === 'object') {
263
+ if (Array.isArray(content.parts)) return normalizeContentParts(content.parts, maxLength);
264
+ if (typeof content.text === 'string') return [{ ...content, text: sanitizeText(content.text, maxLength) }];
265
+ return [{ type: 'json', value: sanitizeText(JSON.stringify(content), maxLength) }];
266
+ }
267
+ return [];
268
+ }
269
+
270
+ function maskContentParts(parts = [], options = {}) {
271
+ const findings = [];
272
+ const vault = {};
273
+ const maskedParts = parts.map((part) => {
274
+ if (!part || typeof part !== 'object') return part;
275
+ const textValue = typeof part.text === 'string'
276
+ ? part.text
277
+ : (part.type === 'json' && typeof part.value === 'string' ? part.value : null);
278
+ if (textValue == null) return { ...part };
279
+ const result = maskValue(textValue, options);
280
+ findings.push(...result.findings);
281
+ Object.assign(vault, result.vault);
282
+ if (typeof part.text === 'string') return { ...part, text: result.masked };
283
+ return { ...part, value: result.masked };
284
+ });
285
+ return { maskedParts, findings, vault };
286
+ }
287
+
168
288
  function placeholder(type, index) {
169
289
  return `[${String(type || 'SENSITIVE').toUpperCase()}_${index}]`;
170
290
  }
@@ -223,6 +343,154 @@ function createTelemetryEvent(type, payload = {}) {
223
343
  };
224
344
  }
225
345
 
346
+ function summarizeOperationalTelemetry(events = []) {
347
+ const summary = {
348
+ totalEvents: 0,
349
+ blockedEvents: 0,
350
+ shadowModeEvents: 0,
351
+ byType: {},
352
+ byRoute: {},
353
+ highestSeverity: 'low',
354
+ };
355
+ for (const event of Array.isArray(events) ? events : []) {
356
+ const type = event && event.type ? event.type : 'unknown';
357
+ const route = event && event.metadata && (event.metadata.route || event.metadata.path) ? (event.metadata.route || event.metadata.path) : 'unknown';
358
+ const severity = event && event.report && event.report.outputReview
359
+ ? event.report.outputReview.severity
360
+ : (event && event.report && event.report.promptInjection ? event.report.promptInjection.level : 'low');
361
+ summary.totalEvents += 1;
362
+ summary.byType[type] = (summary.byType[type] || 0) + 1;
363
+ summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
364
+ if (event && event.blocked) summary.blockedEvents += 1;
365
+ if (event && event.shadowMode) summary.shadowModeEvents += 1;
366
+ if (severityWeight(severity) > severityWeight(summary.highestSeverity)) summary.highestSeverity = severity;
367
+ }
368
+ return summary;
369
+ }
370
+
371
+ function resolveShieldPreset(name) {
372
+ if (!name) return {};
373
+ return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
374
+ }
375
+
376
+ function dedupeArray(values = []) {
377
+ return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))];
378
+ }
379
+
380
+ function routePatternMatches(pattern, route = '', metadata = {}) {
381
+ if (!pattern) return false;
382
+ if (typeof pattern === 'function') return !!pattern(route, metadata);
383
+ if (pattern instanceof RegExp) return pattern.test(route);
384
+ if (typeof pattern === 'string') {
385
+ if (pattern === route) return true;
386
+ if (pattern.includes('*')) {
387
+ const regex = new RegExp(`^${pattern.split('*').map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('.*')}$`);
388
+ return regex.test(route);
389
+ }
390
+ }
391
+ return false;
392
+ }
393
+
394
+ function resolveRoutePolicy(routePolicies = [], metadata = {}) {
395
+ const route = metadata.route || metadata.path || '';
396
+ const matched = (Array.isArray(routePolicies) ? routePolicies : []).filter((entry) => routePatternMatches(entry && entry.route, route, metadata));
397
+ if (!matched.length) return null;
398
+ return matched.reduce((acc, entry) => {
399
+ const options = entry && entry.options ? entry.options : {};
400
+ return {
401
+ ...acc,
402
+ ...options,
403
+ shadowPolicyPacks: dedupeArray([...(acc.shadowPolicyPacks || []), ...(options.shadowPolicyPacks || [])]),
404
+ entityDetectors: [...(acc.entityDetectors || []), ...(options.entityDetectors || [])],
405
+ customPromptDetectors: [...(acc.customPromptDetectors || []), ...(options.customPromptDetectors || [])],
406
+ suppressPromptRules: dedupeArray([...(acc.suppressPromptRules || []), ...(options.suppressPromptRules || [])]),
407
+ };
408
+ }, {});
409
+ }
410
+
411
+ function applyPromptRuleSuppressions(injection, suppressedIds = []) {
412
+ const suppressionSet = new Set(dedupeArray(suppressedIds));
413
+ if (!suppressionSet.size) return injection;
414
+ const matches = (injection.matches || []).filter((item) => !suppressionSet.has(item.id));
415
+ const score = Math.min(matches.reduce((sum, item) => sum + (item.score || 0), 0), 100);
416
+ return {
417
+ ...injection,
418
+ matches,
419
+ score,
420
+ level: riskLevelFromScore(score),
421
+ blockedByDefault: score >= 45,
422
+ };
423
+ }
424
+
425
+ function applyCustomPromptDetectors(injection, text, options = {}, metadata = {}) {
426
+ const detectors = Array.isArray(options.customPromptDetectors) ? options.customPromptDetectors : [];
427
+ if (!detectors.length) return injection;
428
+ const matches = [...(injection.matches || [])];
429
+ const seen = new Set(matches.map((item) => item.id));
430
+ let score = injection.score || 0;
431
+ for (const detector of detectors) {
432
+ if (typeof detector !== 'function') continue;
433
+ const result = detector({ text, injection, metadata, options }) || [];
434
+ const findings = Array.isArray(result) ? result : [result];
435
+ for (const finding of findings) {
436
+ if (!finding || !finding.id || seen.has(finding.id)) continue;
437
+ seen.add(finding.id);
438
+ matches.push({
439
+ id: finding.id,
440
+ score: Math.max(0, Math.min(finding.score || 0, 40)),
441
+ reason: finding.reason || 'Custom prompt detector triggered',
442
+ source: finding.source || 'custom',
443
+ });
444
+ score += Math.max(0, Math.min(finding.score || 0, 40));
445
+ }
446
+ }
447
+ const cappedScore = Math.min(score, 100);
448
+ return {
449
+ ...injection,
450
+ matches,
451
+ score: cappedScore,
452
+ level: riskLevelFromScore(cappedScore),
453
+ blockedByDefault: cappedScore >= 45,
454
+ };
455
+ }
456
+
457
+ function resolveEffectiveShieldOptions(baseOptions = {}, metadata = {}) {
458
+ const presetOptions = resolveShieldPreset(baseOptions.preset);
459
+ const routePolicy = resolveRoutePolicy(baseOptions.routePolicies, metadata);
460
+ const routePresetOptions = resolveShieldPreset(routePolicy && routePolicy.preset);
461
+ return {
462
+ ...baseOptions,
463
+ ...presetOptions,
464
+ ...routePresetOptions,
465
+ ...(routePolicy || {}),
466
+ shadowPolicyPacks: dedupeArray([
467
+ ...((presetOptions && presetOptions.shadowPolicyPacks) || []),
468
+ ...((routePresetOptions && routePresetOptions.shadowPolicyPacks) || []),
469
+ ...(baseOptions.shadowPolicyPacks || []),
470
+ ...((routePolicy && routePolicy.shadowPolicyPacks) || []),
471
+ ]),
472
+ entityDetectors: [
473
+ ...((presetOptions && presetOptions.entityDetectors) || []),
474
+ ...((routePresetOptions && routePresetOptions.entityDetectors) || []),
475
+ ...(baseOptions.entityDetectors || []),
476
+ ...((routePolicy && routePolicy.entityDetectors) || []),
477
+ ],
478
+ customPromptDetectors: [
479
+ ...((presetOptions && presetOptions.customPromptDetectors) || []),
480
+ ...((routePresetOptions && routePresetOptions.customPromptDetectors) || []),
481
+ ...(baseOptions.customPromptDetectors || []),
482
+ ...((routePolicy && routePolicy.customPromptDetectors) || []),
483
+ ],
484
+ suppressPromptRules: dedupeArray([
485
+ ...((presetOptions && presetOptions.suppressPromptRules) || []),
486
+ ...((routePresetOptions && routePresetOptions.suppressPromptRules) || []),
487
+ ...(baseOptions.suppressPromptRules || []),
488
+ ...((routePolicy && routePolicy.suppressPromptRules) || []),
489
+ ]),
490
+ routePolicy,
491
+ };
492
+ }
493
+
226
494
  function cloneRegex(regex) {
227
495
  return new RegExp(regex.source, regex.flags);
228
496
  }
@@ -575,11 +843,14 @@ function normalizeMessages(messages = [], options = {}) {
575
843
  return (Array.isArray(messages) ? messages : [])
576
844
  .slice(-maxMessages)
577
845
  .map((message) => {
578
- const content = sanitizeText(String(message && message.content ? message.content : ''));
846
+ const originalContent = message && Object.prototype.hasOwnProperty.call(message, 'content') ? message.content : '';
847
+ const parts = typeof originalContent === 'string' ? [] : normalizeContentParts(originalContent, options.maxLength || 5000);
848
+ const content = stringifyMessageContent(originalContent, options.maxLength || 5000);
579
849
  if (!content) return null;
580
850
  return {
581
851
  role: normalizeRole(message.role, allowSystemMessages, !!message.trusted),
582
852
  content,
853
+ contentParts: parts.length ? parts : undefined,
583
854
  };
584
855
  })
585
856
  .filter(Boolean);
@@ -590,16 +861,25 @@ function maskMessages(messages = [], options = {}) {
590
861
  const vault = {};
591
862
  const masked = (Array.isArray(messages) ? messages : []).map((message) => {
592
863
  if (!message || typeof message !== 'object') return null;
864
+ const normalizedParts = Array.isArray(message.contentParts)
865
+ ? message.contentParts
866
+ : (typeof message.content === 'string' ? [] : normalizeContentParts(message.content || '', options.maxLength || 5000));
593
867
  const normalized = {
594
868
  role: message.role === 'system' ? 'system' : normalizeRole(message.role, false, false),
595
- content: sanitizeText(String(message.content || ''), options.maxLength || 5000),
869
+ content: stringifyMessageContent(message.content || '', options.maxLength || 5000),
870
+ contentParts: normalizedParts.length ? normalizedParts : undefined,
596
871
  };
597
872
  if (!normalized.content) return null;
598
873
  if (normalized.role === 'system') return normalized;
599
874
  const result = maskValue(normalized.content, options);
600
- findings.push(...result.findings);
601
- Object.assign(vault, result.vault);
602
- return { ...normalized, content: result.masked };
875
+ const partsResult = maskContentParts(normalized.contentParts || [], options);
876
+ findings.push(...result.findings, ...partsResult.findings);
877
+ Object.assign(vault, result.vault, partsResult.vault);
878
+ return {
879
+ ...normalized,
880
+ content: result.masked,
881
+ contentParts: partsResult.maskedParts.length ? partsResult.maskedParts : undefined,
882
+ };
603
883
  }).filter(Boolean);
604
884
 
605
885
  return {
@@ -748,14 +1028,19 @@ class BlackwallShield {
748
1028
  maxLength: 5000,
749
1029
  allowSystemMessages: false,
750
1030
  shadowMode: false,
1031
+ preset: null,
751
1032
  policyPack: null,
752
1033
  shadowPolicyPacks: [],
753
1034
  entityDetectors: [],
1035
+ customPromptDetectors: [],
1036
+ suppressPromptRules: [],
1037
+ routePolicies: [],
754
1038
  detectNamedEntities: false,
755
1039
  semanticScorer: null,
756
1040
  sessionBuffer: null,
757
1041
  tokenBudgetFirewall: null,
758
1042
  systemPrompt: null,
1043
+ outputFirewallDefaults: {},
759
1044
  onAlert: null,
760
1045
  onTelemetry: null,
761
1046
  webhookUrl: null,
@@ -764,10 +1049,13 @@ class BlackwallShield {
764
1049
  }
765
1050
 
766
1051
  inspectText(text) {
767
- const pii = maskValue(text, this.options);
768
- const injection = detectPromptInjection(text, this.options);
1052
+ const effectiveOptions = resolveEffectiveShieldOptions(this.options);
1053
+ const pii = maskValue(text, effectiveOptions);
1054
+ let injection = detectPromptInjection(text, effectiveOptions);
1055
+ injection = applyCustomPromptDetectors(injection, String(text || ''), effectiveOptions, {});
1056
+ injection = applyPromptRuleSuppressions(injection, effectiveOptions.suppressPromptRules);
769
1057
  return {
770
- sanitized: pii.original || sanitizeText(text, this.options.maxLength),
1058
+ sanitized: pii.original || sanitizeText(text, effectiveOptions.maxLength),
771
1059
  promptInjection: injection,
772
1060
  sensitiveData: {
773
1061
  findings: pii.findings,
@@ -792,35 +1080,43 @@ class BlackwallShield {
792
1080
  }
793
1081
 
794
1082
  async guardModelRequest({ messages = [], metadata = {}, allowSystemMessages = this.options.allowSystemMessages, comparePolicyPacks = [] } = {}) {
1083
+ const effectiveOptions = resolveEffectiveShieldOptions(this.options, metadata);
1084
+ const effectiveAllowSystemMessages = allowSystemMessages === this.options.allowSystemMessages
1085
+ ? effectiveOptions.allowSystemMessages
1086
+ : allowSystemMessages;
795
1087
  const normalizedMessages = normalizeMessages(messages, {
796
- maxMessages: this.options.maxMessages,
797
- allowSystemMessages,
1088
+ maxMessages: effectiveOptions.maxMessages,
1089
+ allowSystemMessages: effectiveAllowSystemMessages,
798
1090
  });
799
1091
  const masked = maskMessages(normalizedMessages, {
800
- includeOriginals: this.options.includeOriginals,
801
- syntheticReplacement: this.options.syntheticReplacement,
802
- maxLength: this.options.maxLength,
803
- allowSystemMessages,
1092
+ includeOriginals: effectiveOptions.includeOriginals,
1093
+ syntheticReplacement: effectiveOptions.syntheticReplacement,
1094
+ maxLength: effectiveOptions.maxLength,
1095
+ allowSystemMessages: effectiveAllowSystemMessages,
1096
+ entityDetectors: effectiveOptions.entityDetectors,
1097
+ detectNamedEntities: effectiveOptions.detectNamedEntities,
804
1098
  });
805
1099
  const promptCandidate = normalizedMessages.filter((msg) => msg.role !== 'assistant');
806
- const sessionBuffer = this.options.sessionBuffer;
1100
+ const sessionBuffer = effectiveOptions.sessionBuffer;
807
1101
  if (sessionBuffer && typeof sessionBuffer.record === 'function') {
808
1102
  promptCandidate.forEach((msg) => sessionBuffer.record(msg.content));
809
1103
  }
810
1104
  const sessionContext = sessionBuffer && typeof sessionBuffer.render === 'function'
811
1105
  ? sessionBuffer.render()
812
1106
  : promptCandidate;
813
- const injection = detectPromptInjection(sessionContext, this.options);
814
-
815
- const primaryPolicy = resolvePolicyPack(this.options.policyPack);
816
- const threshold = (primaryPolicy && primaryPolicy.promptInjectionThreshold) || this.options.promptInjectionThreshold;
817
- const wouldBlock = this.options.blockOnPromptInjection && compareRisk(injection.level, threshold);
818
- const shouldBlock = this.options.shadowMode ? false : wouldBlock;
819
- const shouldNotify = compareRisk(injection.level, this.options.notifyOnRiskLevel);
820
- const policyNames = [...new Set([...(this.options.shadowPolicyPacks || []), ...comparePolicyPacks].filter(Boolean))];
821
- const policyComparisons = policyNames.map((name) => evaluatePolicyPack(injection, name, this.options.promptInjectionThreshold));
822
- const budgetResult = this.options.tokenBudgetFirewall && typeof this.options.tokenBudgetFirewall.inspect === 'function'
823
- ? this.options.tokenBudgetFirewall.inspect({
1107
+ let injection = detectPromptInjection(sessionContext, effectiveOptions);
1108
+ injection = applyCustomPromptDetectors(injection, Array.isArray(sessionContext) ? JSON.stringify(sessionContext) : String(sessionContext || ''), effectiveOptions, metadata);
1109
+ injection = applyPromptRuleSuppressions(injection, effectiveOptions.suppressPromptRules);
1110
+
1111
+ const primaryPolicy = resolvePolicyPack(effectiveOptions.policyPack);
1112
+ const threshold = (primaryPolicy && primaryPolicy.promptInjectionThreshold) || effectiveOptions.promptInjectionThreshold;
1113
+ const wouldBlock = effectiveOptions.blockOnPromptInjection && compareRisk(injection.level, threshold);
1114
+ const shouldBlock = effectiveOptions.shadowMode ? false : wouldBlock;
1115
+ const shouldNotify = compareRisk(injection.level, effectiveOptions.notifyOnRiskLevel);
1116
+ const policyNames = [...new Set([...(effectiveOptions.shadowPolicyPacks || []), ...comparePolicyPacks].filter(Boolean))];
1117
+ const policyComparisons = policyNames.map((name) => evaluatePolicyPack(injection, name, effectiveOptions.promptInjectionThreshold));
1118
+ const budgetResult = effectiveOptions.tokenBudgetFirewall && typeof effectiveOptions.tokenBudgetFirewall.inspect === 'function'
1119
+ ? effectiveOptions.tokenBudgetFirewall.inspect({
824
1120
  userId: metadata.userId || metadata.user_id || 'anonymous',
825
1121
  tenantId: metadata.tenantId || metadata.tenant_id || 'default',
826
1122
  messages: normalizedMessages,
@@ -838,7 +1134,7 @@ class BlackwallShield {
838
1134
  hasSensitiveData: masked.hasSensitiveData,
839
1135
  },
840
1136
  enforcement: {
841
- shadowMode: this.options.shadowMode,
1137
+ shadowMode: effectiveOptions.shadowMode,
842
1138
  wouldBlock: wouldBlock || !budgetResult.allowed,
843
1139
  blocked: shouldBlock || !budgetResult.allowed,
844
1140
  threshold,
@@ -846,6 +1142,13 @@ class BlackwallShield {
846
1142
  policyPack: primaryPolicy ? primaryPolicy.name : null,
847
1143
  policyComparisons,
848
1144
  tokenBudget: budgetResult,
1145
+ coreInterfaces: CORE_INTERFACES,
1146
+ routePolicy: effectiveOptions.routePolicy ? {
1147
+ route: metadata.route || metadata.path || null,
1148
+ suppressPromptRules: effectiveOptions.routePolicy.suppressPromptRules || [],
1149
+ policyPack: effectiveOptions.routePolicy.policyPack || null,
1150
+ preset: effectiveOptions.routePolicy.preset || null,
1151
+ } : null,
849
1152
  telemetry: {
850
1153
  eventType: 'llm_request_reviewed',
851
1154
  promptInjectionRuleHits: countFindingsByType(injection.matches),
@@ -861,7 +1164,7 @@ class BlackwallShield {
861
1164
  await this.emitTelemetry(createTelemetryEvent('llm_request_reviewed', {
862
1165
  metadata,
863
1166
  blocked: shouldBlock || !budgetResult.allowed,
864
- shadowMode: this.options.shadowMode,
1167
+ shadowMode: effectiveOptions.shadowMode,
865
1168
  report,
866
1169
  }));
867
1170
 
@@ -886,14 +1189,17 @@ class BlackwallShield {
886
1189
  }
887
1190
 
888
1191
  async reviewModelResponse({ output, metadata = {}, outputFirewall = null, firewallOptions = {} } = {}) {
889
- const primaryPolicy = resolvePolicyPack(this.options.policyPack);
1192
+ const effectiveOptions = resolveEffectiveShieldOptions(this.options, metadata);
1193
+ const primaryPolicy = resolvePolicyPack(effectiveOptions.policyPack);
890
1194
  const firewall = outputFirewall || new OutputFirewall({
891
1195
  riskThreshold: (primaryPolicy && primaryPolicy.outputRiskThreshold) || 'high',
892
- systemPrompt: this.options.systemPrompt,
1196
+ systemPrompt: effectiveOptions.systemPrompt,
1197
+ ...effectiveOptions.outputFirewallDefaults,
893
1198
  ...firewallOptions,
894
1199
  });
895
1200
  const review = firewall.inspect(output, {
896
- systemPrompt: this.options.systemPrompt,
1201
+ systemPrompt: effectiveOptions.systemPrompt,
1202
+ ...(effectiveOptions.outputFirewallDefaults || {}),
897
1203
  ...firewallOptions,
898
1204
  });
899
1205
  const report = {
@@ -902,6 +1208,7 @@ class BlackwallShield {
902
1208
  metadata,
903
1209
  outputReview: {
904
1210
  ...review,
1211
+ coreInterfaces: CORE_INTERFACES,
905
1212
  telemetry: {
906
1213
  eventType: 'llm_output_reviewed',
907
1214
  findingCounts: countFindingsByType(review.findings),
@@ -988,6 +1295,38 @@ class BlackwallShield {
988
1295
  review,
989
1296
  };
990
1297
  }
1298
+
1299
+ async protectWithAdapter({
1300
+ adapter,
1301
+ messages = [],
1302
+ metadata = {},
1303
+ allowSystemMessages = this.options.allowSystemMessages,
1304
+ comparePolicyPacks = [],
1305
+ outputFirewall = null,
1306
+ firewallOptions = {},
1307
+ } = {}) {
1308
+ if (!adapter || typeof adapter.invoke !== 'function') {
1309
+ throw new TypeError('adapter.invoke must be a function');
1310
+ }
1311
+ return this.protectModelCall({
1312
+ messages,
1313
+ metadata,
1314
+ allowSystemMessages,
1315
+ comparePolicyPacks,
1316
+ outputFirewall,
1317
+ firewallOptions,
1318
+ callModel: async (payload) => {
1319
+ const result = await adapter.invoke(payload);
1320
+ return result && Object.prototype.hasOwnProperty.call(result, 'response') ? result.response : result;
1321
+ },
1322
+ mapOutput: async (response, request) => {
1323
+ if (typeof adapter.extractOutput === 'function') {
1324
+ return adapter.extractOutput(response, request);
1325
+ }
1326
+ return response && Object.prototype.hasOwnProperty.call(response, 'output') ? response.output : response;
1327
+ },
1328
+ });
1329
+ }
991
1330
  }
992
1331
 
993
1332
  function validateGrounding(text, documents = [], options = {}) {
@@ -1641,6 +1980,18 @@ function createLlamaIndexCallback({ shield, metadata = {} } = {}) {
1641
1980
  };
1642
1981
  }
1643
1982
 
1983
+ function buildShieldOptions(options = {}) {
1984
+ const presetOptions = resolveShieldPreset(options.preset);
1985
+ return {
1986
+ ...presetOptions,
1987
+ ...options,
1988
+ shadowPolicyPacks: dedupeArray([
1989
+ ...(presetOptions.shadowPolicyPacks || []),
1990
+ ...(options.shadowPolicyPacks || []),
1991
+ ]),
1992
+ };
1993
+ }
1994
+
1644
1995
  module.exports = {
1645
1996
  AgenticCapabilityGater,
1646
1997
  AgentIdentityRegistry,
@@ -1659,6 +2010,8 @@ module.exports = {
1659
2010
  SENSITIVE_PATTERNS,
1660
2011
  PROMPT_INJECTION_RULES,
1661
2012
  POLICY_PACKS,
2013
+ SHIELD_PRESETS,
2014
+ CORE_INTERFACES,
1662
2015
  sanitizeText,
1663
2016
  deobfuscateText,
1664
2017
  maskText,
@@ -1680,6 +2033,12 @@ module.exports = {
1680
2033
  buildAdminDashboardModel,
1681
2034
  getRedTeamPromptLibrary,
1682
2035
  runRedTeamSuite,
2036
+ buildShieldOptions,
2037
+ summarizeOperationalTelemetry,
2038
+ createOpenAIAdapter,
2039
+ createAnthropicAdapter,
2040
+ createGeminiAdapter,
2041
+ createOpenRouterAdapter,
1683
2042
  createExpressMiddleware,
1684
2043
  createLangChainCallbacks,
1685
2044
  createLlamaIndexCallback,
@@ -0,0 +1,152 @@
1
+ function stringifyContent(content) {
2
+ if (typeof content === 'string') return content;
3
+ if (Array.isArray(content)) {
4
+ return content.map((item) => {
5
+ if (typeof item === 'string') return item;
6
+ if (item && typeof item.text === 'string') return item.text;
7
+ if (item && item.type === 'text' && typeof item.text === 'string') return item.text;
8
+ return '';
9
+ }).filter(Boolean).join('\n');
10
+ }
11
+ if (content && typeof content.text === 'string') return content.text;
12
+ return String(content || '');
13
+ }
14
+
15
+ function toOpenAIInput(messages = []) {
16
+ return messages.map((message) => ({
17
+ role: message.role,
18
+ content: stringifyContent(message.content),
19
+ }));
20
+ }
21
+
22
+ function toAnthropicMessages(messages = []) {
23
+ return messages
24
+ .filter((message) => message.role !== 'system')
25
+ .map((message) => ({
26
+ role: message.role === 'assistant' ? 'assistant' : 'user',
27
+ content: stringifyContent(message.content),
28
+ }));
29
+ }
30
+
31
+ function extractSystemPrompt(messages = []) {
32
+ return messages.filter((message) => message.role === 'system').map((message) => stringifyContent(message.content)).join('\n\n');
33
+ }
34
+
35
+ function defaultAdapterResult(response, output) {
36
+ return { response, output };
37
+ }
38
+
39
+ function createOpenAIAdapter({ client, model, request = {}, method = 'responses', extractOutput = null } = {}) {
40
+ if (!client) throw new TypeError('client is required');
41
+ return {
42
+ provider: 'openai',
43
+ async invoke({ messages, metadata = {} }) {
44
+ if (method === 'chat.completions') {
45
+ const response = await client.chat.completions.create({
46
+ model,
47
+ messages: toOpenAIInput(messages),
48
+ metadata,
49
+ ...request,
50
+ });
51
+ return defaultAdapterResult(response, response && response.choices && response.choices[0] && response.choices[0].message
52
+ ? stringifyContent(response.choices[0].message.content)
53
+ : '');
54
+ }
55
+ const response = await client.responses.create({
56
+ model,
57
+ input: toOpenAIInput(messages),
58
+ metadata,
59
+ ...request,
60
+ });
61
+ return defaultAdapterResult(response, response && typeof response.output_text === 'string' ? response.output_text : '');
62
+ },
63
+ extractOutput(response) {
64
+ if (typeof extractOutput === 'function') return extractOutput(response);
65
+ if (response && typeof response.output_text === 'string') return response.output_text;
66
+ return response && response.choices && response.choices[0] && response.choices[0].message
67
+ ? stringifyContent(response.choices[0].message.content)
68
+ : '';
69
+ },
70
+ };
71
+ }
72
+
73
+ function createAnthropicAdapter({ client, model, request = {}, extractOutput = null } = {}) {
74
+ if (!client) throw new TypeError('client is required');
75
+ return {
76
+ provider: 'anthropic',
77
+ async invoke({ messages, metadata = {} }) {
78
+ const response = await client.messages.create({
79
+ model,
80
+ system: extractSystemPrompt(messages) || undefined,
81
+ messages: toAnthropicMessages(messages),
82
+ metadata,
83
+ ...request,
84
+ });
85
+ const output = Array.isArray(response && response.content)
86
+ ? response.content.map((item) => stringifyContent(item)).filter(Boolean).join('\n')
87
+ : '';
88
+ return defaultAdapterResult(response, output);
89
+ },
90
+ extractOutput(response) {
91
+ if (typeof extractOutput === 'function') return extractOutput(response);
92
+ return Array.isArray(response && response.content)
93
+ ? response.content.map((item) => stringifyContent(item)).filter(Boolean).join('\n')
94
+ : '';
95
+ },
96
+ };
97
+ }
98
+
99
+ function createGeminiAdapter({ client, model, request = {}, extractOutput = null } = {}) {
100
+ if (!client) throw new TypeError('client is required');
101
+ return {
102
+ provider: 'gemini',
103
+ async invoke({ messages }) {
104
+ const response = await client.models.generateContent({
105
+ model,
106
+ contents: messages.map((message) => ({
107
+ role: message.role === 'assistant' ? 'model' : 'user',
108
+ parts: [{ text: stringifyContent(message.content) }],
109
+ })),
110
+ ...request,
111
+ });
112
+ return defaultAdapterResult(response, response && typeof response.text === 'string' ? response.text : '');
113
+ },
114
+ extractOutput(response) {
115
+ if (typeof extractOutput === 'function') return extractOutput(response);
116
+ if (response && typeof response.text === 'string') return response.text;
117
+ if (typeof response === 'string') return response;
118
+ return '';
119
+ },
120
+ };
121
+ }
122
+
123
+ function createOpenRouterAdapter({ client, model, request = {}, extractOutput = null } = {}) {
124
+ if (!client) throw new TypeError('client is required');
125
+ return {
126
+ provider: 'openrouter',
127
+ async invoke({ messages }) {
128
+ const response = await client.chat.completions.create({
129
+ model,
130
+ messages: toOpenAIInput(messages),
131
+ ...request,
132
+ });
133
+ const output = response && response.choices && response.choices[0] && response.choices[0].message
134
+ ? stringifyContent(response.choices[0].message.content)
135
+ : '';
136
+ return defaultAdapterResult(response, output);
137
+ },
138
+ extractOutput(response) {
139
+ if (typeof extractOutput === 'function') return extractOutput(response);
140
+ return response && response.choices && response.choices[0] && response.choices[0].message
141
+ ? stringifyContent(response.choices[0].message.content)
142
+ : '';
143
+ },
144
+ };
145
+ }
146
+
147
+ module.exports = {
148
+ createOpenAIAdapter,
149
+ createAnthropicAdapter,
150
+ createGeminiAdapter,
151
+ createOpenRouterAdapter,
152
+ };