@vpdeva/blackwall-llm-shield-js 0.1.3 → 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.
Files changed (3) hide show
  1. package/README.md +61 -0
  2. package/package.json +1 -1
  3. package/src/index.js +119 -5
package/README.md CHANGED
@@ -14,6 +14,8 @@ JavaScript security middleware for LLM applications in Node.js and Next.js. Blac
14
14
  - Emits structured telemetry for prompt risk, masking volume, and output review outcomes
15
15
  - Includes first-class provider adapters for OpenAI, Anthropic, Gemini, and OpenRouter
16
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
17
19
  - Ships Express, LangChain, and LlamaIndex integration helpers
18
20
  - Enforces allowlists, denylists, validators, and approval-gated tools
19
21
  - Sanitizes RAG documents before they are injected into context
@@ -79,6 +81,10 @@ Use `shadowMode` with `shadowPolicyPacks` or `comparePolicyPacks` to record what
79
81
 
80
82
  Use `createOpenAIAdapter()`, `createAnthropicAdapter()`, `createGeminiAdapter()`, or `createOpenRouterAdapter()` with `protectWithAdapter()` when you want Blackwall to wrap the provider call end to end.
81
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
+
82
88
  ### Output grounding and tone review
83
89
 
84
90
  `OutputFirewall` can compare responses against retrieved documents and flag hallucination-style unsupported claims or unprofessional tone.
@@ -113,6 +119,17 @@ Use it to allowlist tools, block disallowed tools, validate arguments, and requi
113
119
 
114
120
  Use it before injecting retrieved documents into context so hostile instructions in your RAG data store do not quietly become model instructions.
115
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
+
116
133
  ### `AuditTrail`
117
134
 
118
135
  Use it to record signed events, summarize security activity, and power dashboards or downstream analysis.
@@ -184,6 +201,44 @@ const shield = new BlackwallShield({
184
201
  });
185
202
  ```
186
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
+
187
242
  ### Inspect model output
188
243
 
189
244
  ```js
@@ -227,12 +282,18 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
227
282
  - `npm run changeset` creates a version/changelog entry for the next release
228
283
  - `npm run version-packages` applies pending Changesets locally
229
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
289
+
230
290
  ## Rollout Notes
231
291
 
232
292
  - Start with `preset: 'shadowFirst'` or `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
233
293
  - Use `RetrievalSanitizer` and `ToolPermissionFirewall` in front of RAG, search, admin actions, and tool-calling flows.
234
294
  - Add regression prompts for instruction overrides, prompt leaks, token leaks, and Australian PII samples so upgrades stay safe.
235
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.
236
297
 
237
298
  ## Support
238
299
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpdeva/blackwall-llm-shield-js",
3
- "version": "0.1.3",
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)",
package/src/index.js CHANGED
@@ -133,6 +133,18 @@ const SHIELD_PRESETS = {
133
133
  shadowMode: true,
134
134
  allowSystemMessages: true,
135
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
+ },
136
148
  };
137
149
 
138
150
  const CORE_INTERFACE_VERSION = '1.0';
@@ -209,6 +221,70 @@ function sanitizeText(input, maxLength = 5000) {
209
221
  .slice(0, maxLength);
210
222
  }
211
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
+
212
288
  function placeholder(type, index) {
213
289
  return `[${String(type || 'SENSITIVE').toUpperCase()}_${index}]`;
214
290
  }
@@ -267,6 +343,31 @@ function createTelemetryEvent(type, payload = {}) {
267
343
  };
268
344
  }
269
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
+
270
371
  function resolveShieldPreset(name) {
271
372
  if (!name) return {};
272
373
  return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
@@ -742,11 +843,14 @@ function normalizeMessages(messages = [], options = {}) {
742
843
  return (Array.isArray(messages) ? messages : [])
743
844
  .slice(-maxMessages)
744
845
  .map((message) => {
745
- 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);
746
849
  if (!content) return null;
747
850
  return {
748
851
  role: normalizeRole(message.role, allowSystemMessages, !!message.trusted),
749
852
  content,
853
+ contentParts: parts.length ? parts : undefined,
750
854
  };
751
855
  })
752
856
  .filter(Boolean);
@@ -757,16 +861,25 @@ function maskMessages(messages = [], options = {}) {
757
861
  const vault = {};
758
862
  const masked = (Array.isArray(messages) ? messages : []).map((message) => {
759
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));
760
867
  const normalized = {
761
868
  role: message.role === 'system' ? 'system' : normalizeRole(message.role, false, false),
762
- content: sanitizeText(String(message.content || ''), options.maxLength || 5000),
869
+ content: stringifyMessageContent(message.content || '', options.maxLength || 5000),
870
+ contentParts: normalizedParts.length ? normalizedParts : undefined,
763
871
  };
764
872
  if (!normalized.content) return null;
765
873
  if (normalized.role === 'system') return normalized;
766
874
  const result = maskValue(normalized.content, options);
767
- findings.push(...result.findings);
768
- Object.assign(vault, result.vault);
769
- 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
+ };
770
883
  }).filter(Boolean);
771
884
 
772
885
  return {
@@ -1921,6 +2034,7 @@ module.exports = {
1921
2034
  getRedTeamPromptLibrary,
1922
2035
  runRedTeamSuite,
1923
2036
  buildShieldOptions,
2037
+ summarizeOperationalTelemetry,
1924
2038
  createOpenAIAdapter,
1925
2039
  createAnthropicAdapter,
1926
2040
  createGeminiAdapter,