@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 +39 -5
- package/package.json +7 -4
- package/src/index.js +153 -0
- package/src/integrations.js +6 -2
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
|
-
##
|
|
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
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
}
|
package/src/integrations.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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;
|