@vpdeva/blackwall-llm-shield-js 0.1.3 → 0.1.6
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 +74 -6
- package/index.d.ts +92 -0
- package/integrations.d.ts +18 -0
- package/package.json +22 -5
- package/providers.d.ts +6 -0
- package/semantic.d.ts +4 -0
- package/src/index.js +144 -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
|
|
@@ -30,7 +32,7 @@ npm install @xenova/transformers
|
|
|
30
32
|
## Fast Start
|
|
31
33
|
|
|
32
34
|
```js
|
|
33
|
-
const { BlackwallShield } = require('blackwall-llm-shield-js');
|
|
35
|
+
const { BlackwallShield } = require('@vpdeva/blackwall-llm-shield-js');
|
|
34
36
|
|
|
35
37
|
const shield = new BlackwallShield({
|
|
36
38
|
blockOnPromptInjection: true,
|
|
@@ -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.
|
|
@@ -89,9 +95,9 @@ Use `createExpressMiddleware()`, `createLangChainCallbacks()`, or `createLlamaIn
|
|
|
89
95
|
|
|
90
96
|
### Subpath modules
|
|
91
97
|
|
|
92
|
-
Use `require('blackwall-llm-shield-js/integrations')` for callback wrappers and `require('blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
|
|
98
|
+
Use `require('@vpdeva/blackwall-llm-shield-js/integrations')` for callback wrappers and `require('@vpdeva/blackwall-llm-shield-js/semantic')` for optional local semantic scoring adapters.
|
|
93
99
|
|
|
94
|
-
Use `require('blackwall-llm-shield-js/providers')` for provider adapter factories.
|
|
100
|
+
Use `require('@vpdeva/blackwall-llm-shield-js/providers')` for provider adapter factories.
|
|
95
101
|
|
|
96
102
|
## Core Building Blocks
|
|
97
103
|
|
|
@@ -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.
|
|
@@ -134,7 +151,7 @@ if (!guarded.allowed) {
|
|
|
134
151
|
### Wrap a provider call end to end
|
|
135
152
|
|
|
136
153
|
```js
|
|
137
|
-
const { BlackwallShield, createOpenAIAdapter } = require('blackwall-llm-shield-js');
|
|
154
|
+
const { BlackwallShield, createOpenAIAdapter } = require('@vpdeva/blackwall-llm-shield-js');
|
|
138
155
|
|
|
139
156
|
const shield = new BlackwallShield({
|
|
140
157
|
preset: 'shadowFirst',
|
|
@@ -184,10 +201,53 @@ 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 { summarizeOperationalTelemetry } = require('@vpdeva/blackwall-llm-shield-js');
|
|
238
|
+
const summary = summarizeOperationalTelemetry(events);
|
|
239
|
+
console.log(summary.byRoute);
|
|
240
|
+
console.log(summary.highestSeverity);
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### TypeScript
|
|
244
|
+
|
|
245
|
+
The package now ships first-class declaration files for the main entry point plus `integrations`, `providers`, and `semantic` subpaths, so local declaration shims should no longer be necessary in TypeScript apps.
|
|
246
|
+
|
|
187
247
|
### Inspect model output
|
|
188
248
|
|
|
189
249
|
```js
|
|
190
|
-
const { OutputFirewall } = require('blackwall-llm-shield-js');
|
|
250
|
+
const { OutputFirewall } = require('@vpdeva/blackwall-llm-shield-js');
|
|
191
251
|
|
|
192
252
|
const firewall = new OutputFirewall({
|
|
193
253
|
riskThreshold: 'high',
|
|
@@ -204,7 +264,7 @@ console.log(review.allowed);
|
|
|
204
264
|
### Gate tool execution
|
|
205
265
|
|
|
206
266
|
```js
|
|
207
|
-
const { ToolPermissionFirewall } = require('blackwall-llm-shield-js');
|
|
267
|
+
const { ToolPermissionFirewall } = require('@vpdeva/blackwall-llm-shield-js');
|
|
208
268
|
|
|
209
269
|
const tools = new ToolPermissionFirewall({
|
|
210
270
|
allowedTools: ['search', 'lookupCustomer'],
|
|
@@ -219,6 +279,8 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
|
|
|
219
279
|
- [`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
|
|
220
280
|
- [`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
|
|
221
281
|
|
|
282
|
+
For Next.js, the most production-real patterns are App Router route handlers, server actions for trusted internal mutations, and streaming endpoints that apply output review to assembled or final chunks instead of raw intermediate tokens.
|
|
283
|
+
|
|
222
284
|
## Release Commands
|
|
223
285
|
|
|
224
286
|
- `npm run release:check` runs the JS test suite before release
|
|
@@ -227,12 +289,18 @@ console.log(tools.inspectCall({ tool: 'lookupCustomer', args: { id: 'cus_123' }
|
|
|
227
289
|
- `npm run changeset` creates a version/changelog entry for the next release
|
|
228
290
|
- `npm run version-packages` applies pending Changesets locally
|
|
229
291
|
|
|
292
|
+
## Migration and Benchmarks
|
|
293
|
+
|
|
294
|
+
- See [MIGRATING.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/MIGRATING.md) for compatibility notes and stable contract guidance
|
|
295
|
+
- See [BENCHMARKS.md](/Users/vishnu/Documents/blackwall-llm-shield/blackwall-llm-shield-js/BENCHMARKS.md) for baseline latency numbers and regression coverage
|
|
296
|
+
|
|
230
297
|
## Rollout Notes
|
|
231
298
|
|
|
232
299
|
- Start with `preset: 'shadowFirst'` or `shadowMode: true` and inspect `report.telemetry` plus `onTelemetry` events before enabling hard blocking.
|
|
233
300
|
- Use `RetrievalSanitizer` and `ToolPermissionFirewall` in front of RAG, search, admin actions, and tool-calling flows.
|
|
234
301
|
- Add regression prompts for instruction overrides, prompt leaks, token leaks, and Australian PII samples so upgrades stay safe.
|
|
235
302
|
- Expect some latency increase from grounding checks, output review, and custom detectors; benchmark with your real prompt and response sizes before enforcing globally.
|
|
303
|
+
- 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
304
|
|
|
237
305
|
## Support
|
|
238
306
|
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type RiskLevel = 'low' | 'medium' | 'high' | 'critical';
|
|
2
|
+
|
|
3
|
+
export interface MessagePart {
|
|
4
|
+
type: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
image_url?: string;
|
|
7
|
+
file_id?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ShieldMessage {
|
|
12
|
+
role: 'system' | 'user' | 'assistant';
|
|
13
|
+
content: string | MessagePart[] | Record<string, unknown>;
|
|
14
|
+
trusted?: boolean;
|
|
15
|
+
contentParts?: MessagePart[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GuardResult {
|
|
19
|
+
allowed: boolean;
|
|
20
|
+
blocked: boolean;
|
|
21
|
+
reason: string | null;
|
|
22
|
+
messages: Array<ShieldMessage & { content: string; contentParts?: MessagePart[] }>;
|
|
23
|
+
report: Record<string, unknown>;
|
|
24
|
+
vault: Record<string, string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReviewResult {
|
|
28
|
+
allowed: boolean;
|
|
29
|
+
severity: RiskLevel;
|
|
30
|
+
findings: Array<Record<string, unknown>>;
|
|
31
|
+
report: Record<string, unknown>;
|
|
32
|
+
[key: string]: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProviderAdapter {
|
|
36
|
+
provider: string;
|
|
37
|
+
invoke(payload: { messages: ShieldMessage[]; metadata?: Record<string, unknown>; guard?: GuardResult }): Promise<unknown> | unknown;
|
|
38
|
+
extractOutput?(response: unknown, request?: GuardResult): unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ShieldOptions {
|
|
42
|
+
preset?: string | null;
|
|
43
|
+
policyPack?: string | null;
|
|
44
|
+
shadowMode?: boolean;
|
|
45
|
+
routePolicies?: Array<{ route: string | RegExp | ((route: string, metadata: Record<string, unknown>) => boolean); options: Record<string, unknown> }>;
|
|
46
|
+
customPromptDetectors?: Array<(payload: Record<string, unknown>) => Record<string, unknown> | Array<Record<string, unknown>> | null>;
|
|
47
|
+
onTelemetry?: (event: Record<string, unknown>) => void | Promise<void>;
|
|
48
|
+
[key: string]: unknown;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class BlackwallShield {
|
|
52
|
+
constructor(options?: ShieldOptions);
|
|
53
|
+
inspectText(text: unknown): Record<string, unknown>;
|
|
54
|
+
guardModelRequest(input?: { messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[] }): Promise<GuardResult>;
|
|
55
|
+
reviewModelResponse(input?: { output: unknown; metadata?: Record<string, unknown>; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<ReviewResult>;
|
|
56
|
+
protectModelCall(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
57
|
+
protectWithAdapter(input: { adapter: ProviderAdapter; messages?: ShieldMessage[]; metadata?: Record<string, unknown>; allowSystemMessages?: boolean; comparePolicyPacks?: string[]; outputFirewall?: OutputFirewall | null; firewallOptions?: Record<string, unknown> }): Promise<Record<string, unknown>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class OutputFirewall {
|
|
61
|
+
constructor(options?: Record<string, unknown>);
|
|
62
|
+
inspect(output: unknown, options?: Record<string, unknown>): ReviewResult;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export class ToolPermissionFirewall {
|
|
66
|
+
constructor(options?: Record<string, unknown>);
|
|
67
|
+
inspectCall(input: Record<string, unknown>): Record<string, unknown>;
|
|
68
|
+
inspectCallAsync?(input: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class RetrievalSanitizer {
|
|
72
|
+
constructor(options?: Record<string, unknown>);
|
|
73
|
+
sanitizeDocuments(documents: Array<Record<string, unknown>>): Array<Record<string, unknown>>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class AuditTrail {
|
|
77
|
+
constructor(options?: Record<string, unknown>);
|
|
78
|
+
record(event?: Record<string, unknown>): Record<string, unknown>;
|
|
79
|
+
summarize(): Record<string, unknown>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const SHIELD_PRESETS: Record<string, Record<string, unknown>>;
|
|
83
|
+
export const CORE_INTERFACES: Record<string, string>;
|
|
84
|
+
export const POLICY_PACKS: Record<string, Record<string, unknown>>;
|
|
85
|
+
|
|
86
|
+
export function buildShieldOptions(options?: Record<string, unknown>): Record<string, unknown>;
|
|
87
|
+
export function summarizeOperationalTelemetry(events?: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
88
|
+
|
|
89
|
+
export function createOpenAIAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
90
|
+
export function createAnthropicAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
91
|
+
export function createGeminiAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
92
|
+
export function createOpenRouterAdapter(input: Record<string, unknown>): ProviderAdapter;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { BlackwallShield, OutputFirewall, type ProviderAdapter } from './index';
|
|
2
|
+
|
|
3
|
+
export class BlackwallLangChainCallback {
|
|
4
|
+
constructor(options?: Record<string, unknown>);
|
|
5
|
+
handleLLMStart(llm: unknown, prompts?: string[]): Promise<unknown>;
|
|
6
|
+
guardMessages(messages: unknown, metadata?: Record<string, unknown>): Promise<unknown>;
|
|
7
|
+
handleLLMEnd(output: unknown): Promise<unknown>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class BlackwallLlamaIndexCallback {
|
|
11
|
+
constructor(options?: Record<string, unknown>);
|
|
12
|
+
onEventStart(event: unknown): Promise<unknown>;
|
|
13
|
+
onEventEnd(event: unknown): Promise<unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createExpressMiddleware(options?: Record<string, unknown>): (req: unknown, res: unknown, next: () => void) => Promise<void>;
|
|
17
|
+
export function createLangChainCallbacks(options?: Record<string, unknown>): Record<string, unknown>;
|
|
18
|
+
export function createLlamaIndexCallback(options?: Record<string, unknown>): Record<string, unknown>;
|
package/package.json
CHANGED
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vpdeva/blackwall-llm-shield-js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
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)",
|
|
7
7
|
"type": "commonjs",
|
|
8
8
|
"main": "src/index.js",
|
|
9
|
+
"types": "./index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./index.d.ts",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./integrations": {
|
|
16
|
+
"types": "./integrations.d.ts",
|
|
17
|
+
"default": "./src/integrations.js"
|
|
18
|
+
},
|
|
19
|
+
"./providers": {
|
|
20
|
+
"types": "./providers.d.ts",
|
|
21
|
+
"default": "./src/providers.js"
|
|
22
|
+
},
|
|
23
|
+
"./semantic": {
|
|
24
|
+
"types": "./semantic.d.ts",
|
|
25
|
+
"default": "./src/semantic.js"
|
|
26
|
+
}
|
|
14
27
|
},
|
|
15
28
|
"bin": {
|
|
16
29
|
"blackwall-scorecard": "src/scorecard.js"
|
|
@@ -26,6 +39,10 @@
|
|
|
26
39
|
},
|
|
27
40
|
"files": [
|
|
28
41
|
"src",
|
|
42
|
+
"index.d.ts",
|
|
43
|
+
"integrations.d.ts",
|
|
44
|
+
"providers.d.ts",
|
|
45
|
+
"semantic.d.ts",
|
|
29
46
|
"README.md",
|
|
30
47
|
"LICENSE",
|
|
31
48
|
"NOTICE"
|
package/providers.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { ProviderAdapter } from './index';
|
|
2
|
+
|
|
3
|
+
export function createOpenAIAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
4
|
+
export function createAnthropicAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
5
|
+
export function createGeminiAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
|
6
|
+
export function createOpenRouterAdapter(input: Record<string, unknown>): import('./index').ProviderAdapter;
|
package/semantic.d.ts
ADDED
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,56 @@ 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
|
+
byTenant: {},
|
|
354
|
+
byModel: {},
|
|
355
|
+
byPolicyOutcome: {
|
|
356
|
+
blocked: 0,
|
|
357
|
+
shadowBlocked: 0,
|
|
358
|
+
allowed: 0,
|
|
359
|
+
},
|
|
360
|
+
topRules: {},
|
|
361
|
+
highestSeverity: 'low',
|
|
362
|
+
};
|
|
363
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
364
|
+
const type = event && event.type ? event.type : 'unknown';
|
|
365
|
+
const metadata = event && event.metadata ? event.metadata : {};
|
|
366
|
+
const route = metadata.route || metadata.path || 'unknown';
|
|
367
|
+
const tenant = metadata.tenantId || metadata.tenant_id || 'unknown';
|
|
368
|
+
const model = metadata.model || metadata.modelName || 'unknown';
|
|
369
|
+
const severity = event && event.report && event.report.outputReview
|
|
370
|
+
? event.report.outputReview.severity
|
|
371
|
+
: (event && event.report && event.report.promptInjection ? event.report.promptInjection.level : 'low');
|
|
372
|
+
summary.totalEvents += 1;
|
|
373
|
+
summary.byType[type] = (summary.byType[type] || 0) + 1;
|
|
374
|
+
summary.byRoute[route] = (summary.byRoute[route] || 0) + 1;
|
|
375
|
+
summary.byTenant[tenant] = (summary.byTenant[tenant] || 0) + 1;
|
|
376
|
+
summary.byModel[model] = (summary.byModel[model] || 0) + 1;
|
|
377
|
+
if (event && event.blocked) summary.blockedEvents += 1;
|
|
378
|
+
if (event && event.shadowMode) summary.shadowModeEvents += 1;
|
|
379
|
+
if (event && event.blocked) summary.byPolicyOutcome.blocked += 1;
|
|
380
|
+
else if (event && event.shadowMode) summary.byPolicyOutcome.shadowBlocked += 1;
|
|
381
|
+
else summary.byPolicyOutcome.allowed += 1;
|
|
382
|
+
const rules = event && event.report && event.report.promptInjection && Array.isArray(event.report.promptInjection.matches)
|
|
383
|
+
? event.report.promptInjection.matches.map((item) => item.id).filter(Boolean)
|
|
384
|
+
: [];
|
|
385
|
+
rules.forEach((rule) => {
|
|
386
|
+
summary.topRules[rule] = (summary.topRules[rule] || 0) + 1;
|
|
387
|
+
});
|
|
388
|
+
if (severityWeight(severity) > severityWeight(summary.highestSeverity)) summary.highestSeverity = severity;
|
|
389
|
+
}
|
|
390
|
+
summary.topRules = Object.fromEntries(
|
|
391
|
+
Object.entries(summary.topRules).sort((a, b) => b[1] - a[1]).slice(0, 10)
|
|
392
|
+
);
|
|
393
|
+
return summary;
|
|
394
|
+
}
|
|
395
|
+
|
|
270
396
|
function resolveShieldPreset(name) {
|
|
271
397
|
if (!name) return {};
|
|
272
398
|
return SHIELD_PRESETS[name] ? { ...SHIELD_PRESETS[name] } : {};
|
|
@@ -742,11 +868,14 @@ function normalizeMessages(messages = [], options = {}) {
|
|
|
742
868
|
return (Array.isArray(messages) ? messages : [])
|
|
743
869
|
.slice(-maxMessages)
|
|
744
870
|
.map((message) => {
|
|
745
|
-
const
|
|
871
|
+
const originalContent = message && Object.prototype.hasOwnProperty.call(message, 'content') ? message.content : '';
|
|
872
|
+
const parts = typeof originalContent === 'string' ? [] : normalizeContentParts(originalContent, options.maxLength || 5000);
|
|
873
|
+
const content = stringifyMessageContent(originalContent, options.maxLength || 5000);
|
|
746
874
|
if (!content) return null;
|
|
747
875
|
return {
|
|
748
876
|
role: normalizeRole(message.role, allowSystemMessages, !!message.trusted),
|
|
749
877
|
content,
|
|
878
|
+
contentParts: parts.length ? parts : undefined,
|
|
750
879
|
};
|
|
751
880
|
})
|
|
752
881
|
.filter(Boolean);
|
|
@@ -757,16 +886,25 @@ function maskMessages(messages = [], options = {}) {
|
|
|
757
886
|
const vault = {};
|
|
758
887
|
const masked = (Array.isArray(messages) ? messages : []).map((message) => {
|
|
759
888
|
if (!message || typeof message !== 'object') return null;
|
|
889
|
+
const normalizedParts = Array.isArray(message.contentParts)
|
|
890
|
+
? message.contentParts
|
|
891
|
+
: (typeof message.content === 'string' ? [] : normalizeContentParts(message.content || '', options.maxLength || 5000));
|
|
760
892
|
const normalized = {
|
|
761
893
|
role: message.role === 'system' ? 'system' : normalizeRole(message.role, false, false),
|
|
762
|
-
content:
|
|
894
|
+
content: stringifyMessageContent(message.content || '', options.maxLength || 5000),
|
|
895
|
+
contentParts: normalizedParts.length ? normalizedParts : undefined,
|
|
763
896
|
};
|
|
764
897
|
if (!normalized.content) return null;
|
|
765
898
|
if (normalized.role === 'system') return normalized;
|
|
766
899
|
const result = maskValue(normalized.content, options);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
900
|
+
const partsResult = maskContentParts(normalized.contentParts || [], options);
|
|
901
|
+
findings.push(...result.findings, ...partsResult.findings);
|
|
902
|
+
Object.assign(vault, result.vault, partsResult.vault);
|
|
903
|
+
return {
|
|
904
|
+
...normalized,
|
|
905
|
+
content: result.masked,
|
|
906
|
+
contentParts: partsResult.maskedParts.length ? partsResult.maskedParts : undefined,
|
|
907
|
+
};
|
|
770
908
|
}).filter(Boolean);
|
|
771
909
|
|
|
772
910
|
return {
|
|
@@ -1921,6 +2059,7 @@ module.exports = {
|
|
|
1921
2059
|
getRedTeamPromptLibrary,
|
|
1922
2060
|
runRedTeamSuite,
|
|
1923
2061
|
buildShieldOptions,
|
|
2062
|
+
summarizeOperationalTelemetry,
|
|
1924
2063
|
createOpenAIAdapter,
|
|
1925
2064
|
createAnthropicAdapter,
|
|
1926
2065
|
createGeminiAdapter,
|