@vpdeva/blackwall-llm-shield-js 0.1.0 → 0.1.1

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
@@ -11,6 +11,7 @@ JavaScript security middleware for LLM applications in Node.js and Next.js. Blac
11
11
  - Blocks requests when risk exceeds policy thresholds
12
12
  - Supports shadow mode and side-by-side policy-pack evaluation
13
13
  - Notifies webhooks or alert handlers when risky traffic appears
14
+ - Emits structured telemetry for prompt risk, masking volume, and output review outcomes
14
15
  - Inspects model outputs for leaks, unsafe code, grounding drift, and tone violations
15
16
  - Ships Express, LangChain, and LlamaIndex integration helpers
16
17
  - Enforces allowlists, denylists, validators, and approval-gated tools
@@ -91,6 +92,8 @@ Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and
91
92
 
92
93
  Use it to sanitize inbound messages, mask sensitive data, score prompt-injection risk, and decide whether the request should continue to the model provider.
93
94
 
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.
96
+
94
97
  ### `OutputFirewall`
95
98
 
96
99
  Use it after the model responds to catch leaked secrets, dangerous code patterns, and schema regressions before returning output to the user or agent runtime.
@@ -121,6 +124,32 @@ if (!guarded.allowed) {
121
124
  }
122
125
  ```
123
126
 
127
+ ### Wrap a provider call end to end
128
+
129
+ ```js
130
+ const shield = new BlackwallShield({
131
+ shadowMode: true,
132
+ onTelemetry: async (event) => console.log(JSON.stringify(event)),
133
+ });
134
+
135
+ const result = await shield.protectModelCall({
136
+ messages: [{ role: 'user', content: 'Summarize this shipment exception.' }],
137
+ 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
+ firewallOptions: {
144
+ retrievalDocuments: [
145
+ { id: 'kb-1', content: 'Shipment exceptions should include the parcel ID, lane, and next action.' },
146
+ ],
147
+ },
148
+ });
149
+
150
+ console.log(result.stage, result.allowed);
151
+ ```
152
+
124
153
  ### Inspect model output
125
154
 
126
155
  ```js
@@ -156,12 +185,17 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
156
185
  - [`examples/nextjs-app-router/app/api/chat/route.js`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/nextjs-app-router/app/api/chat/route.js) shows guarded request handling in a Next.js route
157
186
  - [`examples/admin-dashboard/index.html`](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/examples/admin-dashboard/index.html) shows a polished security command center demo
158
187
 
159
- ## What Would Make This Production-Ready Even Faster
188
+ ## Release Commands
189
+
190
+ - `npm run release:check` runs the JS test suite before release
191
+ - `npm run release:pack` creates the local npm tarball
192
+ - `npm run release:publish` publishes the package to npm
193
+
194
+ ## Rollout Notes
160
195
 
161
- - Provider adapters for OpenAI, Anthropic, and open-source model gateways
162
- - OpenTelemetry spans and structured logs
163
- - More benchmark data for latency and false-positive rates
164
- - More adversarial scenarios in the red-team suite
196
+ - Start with `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
197
+ - Use `RetrievalSanitizer` and `ToolPermissionFirewall` in front of RAG, search, admin actions, and tool-calling flows.
198
+ - Add regression prompts for instruction overrides, prompt leaks, token leaks, and Australian PII samples so upgrades stay safe.
165
199
 
166
200
  ## Support
167
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vpdeva/blackwall-llm-shield-js",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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)",
@@ -15,7 +15,10 @@
15
15
  "blackwall-scorecard": "src/scorecard.js"
16
16
  },
17
17
  "scripts": {
18
- "test": "node --test tests/*.test.js"
18
+ "test": "node --test tests/*.test.js",
19
+ "release:check": "npm test",
20
+ "release:pack": "npm pack",
21
+ "release:publish": "npm publish --access public --provenance"
19
22
  },
20
23
  "files": [
21
24
  "src",
@@ -28,11 +31,11 @@
28
31
  },
29
32
  "repository": {
30
33
  "type": "git",
31
- "url": "git+https://github.com/vishnud23/blackwall-llm-shield.git"
34
+ "url": "git+https://github.com/vpdeva/blackwall-llm-shield-js.git"
32
35
  },
33
36
  "homepage": "https://vish.au",
34
37
  "bugs": {
35
- "url": "https://github.com/vishnud23/blackwall-llm-shield/issues"
38
+ "url": "https://github.com/vpdeva/blackwall-llm-shield-js/issues"
36
39
  },
37
40
  "funding": {
38
41
  "type": "Buy Vish a coffee",
package/src/index.js CHANGED
@@ -199,6 +199,30 @@ function mapCompliance(ids = []) {
199
199
  return [...new Set(ids.flatMap((id) => COMPLIANCE_MAP[id] || []))];
200
200
  }
201
201
 
202
+ function countFindingsByType(findings = []) {
203
+ return findings.reduce((acc, finding) => {
204
+ const key = finding && (finding.type || finding.id || finding.category || 'unknown');
205
+ acc[key] = (acc[key] || 0) + 1;
206
+ return acc;
207
+ }, {});
208
+ }
209
+
210
+ function summarizeSensitiveFindings(findings = []) {
211
+ return findings.reduce((acc, finding) => {
212
+ const key = finding && finding.type ? finding.type : 'unknown';
213
+ acc[key] = (acc[key] || 0) + 1;
214
+ return acc;
215
+ }, {});
216
+ }
217
+
218
+ function createTelemetryEvent(type, payload = {}) {
219
+ return {
220
+ type,
221
+ createdAt: new Date().toISOString(),
222
+ ...payload,
223
+ };
224
+ }
225
+
202
226
  function cloneRegex(regex) {
203
227
  return new RegExp(regex.source, regex.flags);
204
228
  }
@@ -733,6 +757,7 @@ class BlackwallShield {
733
757
  tokenBudgetFirewall: null,
734
758
  systemPrompt: null,
735
759
  onAlert: null,
760
+ onTelemetry: null,
736
761
  webhookUrl: null,
737
762
  ...options,
738
763
  };
@@ -760,6 +785,12 @@ class BlackwallShield {
760
785
  }
761
786
  }
762
787
 
788
+ async emitTelemetry(event) {
789
+ if (typeof this.options.onTelemetry === 'function') {
790
+ await this.options.onTelemetry(event);
791
+ }
792
+ }
793
+
763
794
  async guardModelRequest({ messages = [], metadata = {}, allowSystemMessages = this.options.allowSystemMessages, comparePolicyPacks = [] } = {}) {
764
795
  const normalizedMessages = normalizeMessages(messages, {
765
796
  maxMessages: this.options.maxMessages,
@@ -815,8 +846,25 @@ class BlackwallShield {
815
846
  policyPack: primaryPolicy ? primaryPolicy.name : null,
816
847
  policyComparisons,
817
848
  tokenBudget: budgetResult,
849
+ telemetry: {
850
+ eventType: 'llm_request_reviewed',
851
+ promptInjectionRuleHits: countFindingsByType(injection.matches),
852
+ maskedEntityCounts: summarizeSensitiveFindings(masked.findings),
853
+ promptTokenEstimate: budgetResult.estimatedTokens,
854
+ complianceMap: mapCompliance([
855
+ ...injection.matches.map((item) => item.id),
856
+ ...(budgetResult.allowed ? [] : ['token_budget_exceeded']),
857
+ ]),
858
+ },
818
859
  };
819
860
 
861
+ await this.emitTelemetry(createTelemetryEvent('llm_request_reviewed', {
862
+ metadata,
863
+ blocked: shouldBlock || !budgetResult.allowed,
864
+ shadowMode: this.options.shadowMode,
865
+ report,
866
+ }));
867
+
820
868
  if (shouldNotify || wouldBlock) {
821
869
  await this.notify({
822
870
  type: shouldBlock ? 'llm_request_blocked' : (wouldBlock ? 'llm_request_shadow_blocked' : 'llm_request_risky'),
@@ -836,6 +884,110 @@ class BlackwallShield {
836
884
  vault: masked.vault,
837
885
  };
838
886
  }
887
+
888
+ async reviewModelResponse({ output, metadata = {}, outputFirewall = null, firewallOptions = {} } = {}) {
889
+ const primaryPolicy = resolvePolicyPack(this.options.policyPack);
890
+ const firewall = outputFirewall || new OutputFirewall({
891
+ riskThreshold: (primaryPolicy && primaryPolicy.outputRiskThreshold) || 'high',
892
+ systemPrompt: this.options.systemPrompt,
893
+ ...firewallOptions,
894
+ });
895
+ const review = firewall.inspect(output, {
896
+ systemPrompt: this.options.systemPrompt,
897
+ ...firewallOptions,
898
+ });
899
+ const report = {
900
+ package: 'blackwall-llm-shield-js',
901
+ createdAt: new Date().toISOString(),
902
+ metadata,
903
+ outputReview: {
904
+ ...review,
905
+ telemetry: {
906
+ eventType: 'llm_output_reviewed',
907
+ findingCounts: countFindingsByType(review.findings),
908
+ piiEntityCounts: summarizeSensitiveFindings(review.piiFindings),
909
+ complianceMap: mapCompliance(review.findings.map((item) => item.id)),
910
+ },
911
+ },
912
+ };
913
+
914
+ await this.emitTelemetry(createTelemetryEvent('llm_output_reviewed', {
915
+ metadata,
916
+ blocked: !review.allowed,
917
+ report,
918
+ }));
919
+
920
+ if (!review.allowed || compareRisk(review.severity, 'high')) {
921
+ await this.notify({
922
+ type: !review.allowed ? 'llm_output_blocked' : 'llm_output_risky',
923
+ severity: review.severity,
924
+ reason: !review.allowed ? 'Model output failed Blackwall review' : 'Model output triggered Blackwall findings',
925
+ report,
926
+ });
927
+ }
928
+
929
+ return {
930
+ ...review,
931
+ report,
932
+ };
933
+ }
934
+
935
+ async protectModelCall({
936
+ messages = [],
937
+ metadata = {},
938
+ allowSystemMessages = this.options.allowSystemMessages,
939
+ comparePolicyPacks = [],
940
+ callModel,
941
+ mapMessages = null,
942
+ mapOutput = null,
943
+ outputFirewall = null,
944
+ firewallOptions = {},
945
+ } = {}) {
946
+ if (typeof callModel !== 'function') {
947
+ throw new TypeError('callModel must be a function');
948
+ }
949
+ const request = await this.guardModelRequest({
950
+ messages,
951
+ metadata,
952
+ allowSystemMessages,
953
+ comparePolicyPacks,
954
+ });
955
+ if (!request.allowed) {
956
+ return {
957
+ allowed: false,
958
+ blocked: true,
959
+ stage: 'request',
960
+ reason: request.reason,
961
+ request,
962
+ response: null,
963
+ review: null,
964
+ };
965
+ }
966
+ const guardedMessages = typeof mapMessages === 'function'
967
+ ? await mapMessages(request.messages, request)
968
+ : request.messages;
969
+ const response = await callModel({
970
+ messages: guardedMessages,
971
+ metadata,
972
+ guard: request,
973
+ });
974
+ const output = typeof mapOutput === 'function' ? await mapOutput(response, request) : response;
975
+ const review = await this.reviewModelResponse({
976
+ output,
977
+ metadata,
978
+ outputFirewall,
979
+ firewallOptions,
980
+ });
981
+ return {
982
+ allowed: review.allowed,
983
+ blocked: !review.allowed,
984
+ stage: review.allowed ? 'complete' : 'output',
985
+ reason: review.allowed ? null : 'Model output failed Blackwall review',
986
+ request,
987
+ response,
988
+ review,
989
+ };
990
+ }
839
991
  }
840
992
 
841
993
  function validateGrounding(text, documents = [], options = {}) {
@@ -1101,6 +1253,7 @@ class OutputFirewall {
1101
1253
  grounding,
1102
1254
  tone,
1103
1255
  cot,
1256
+ complianceMap: mapCompliance(findings.map((item) => item.id)),
1104
1257
  };
1105
1258
  }
1106
1259
  }
@@ -35,7 +35,9 @@ class BlackwallLangChainCallback {
35
35
  const text = Array.isArray(generations) && generations[0] && generations[0][0]
36
36
  ? (generations[0][0].text || generations[0][0].message?.content || '')
37
37
  : '';
38
- const review = this.outputFirewall.inspect(text);
38
+ const review = this.options.shield && typeof this.options.shield.reviewModelResponse === 'function'
39
+ ? await this.options.shield.reviewModelResponse({ output: text, outputFirewall: this.outputFirewall })
40
+ : this.outputFirewall.inspect(text);
39
41
  this.lastOutputReview = review;
40
42
  if (review && review.allowed === false) throw new Error('Blackwall blocked model output');
41
43
  return review;
@@ -62,7 +64,9 @@ class BlackwallLlamaIndexCallback {
62
64
  if (!this.outputFirewall || typeof this.outputFirewall.inspect !== 'function') return null;
63
65
  const payload = event && event.payload ? event.payload : {};
64
66
  const text = payload.response || payload.output || '';
65
- const review = this.outputFirewall.inspect(text);
67
+ const review = this.options.shield && typeof this.options.shield.reviewModelResponse === 'function'
68
+ ? await this.options.shield.reviewModelResponse({ output: text, outputFirewall: this.outputFirewall })
69
+ : this.outputFirewall.inspect(text);
66
70
  this.lastOutputReview = review;
67
71
  if (review && review.allowed === false) throw new Error('Blackwall blocked model output');
68
72
  return review;