@privacyscrubber/sdk 2.4.5 → 2.4.7
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 +50 -1
- package/browser.js +431 -26
- package/cli.js +119 -11
- package/examples/python-bridge/README.md +3 -3
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/langchain_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +2 -2
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/fastmcp.cjs +56 -0
- package/fastmcp.d.ts +5 -0
- package/index.cjs +1186 -88
- package/index.d.ts +156 -2
- package/index.js +1186 -88
- package/package.json +13 -3
- package/ps-license-manager.js +1 -1
- package/ps-pii-engine.cjs +1 -1
- package/sarif.cjs +14 -2
- package/scrubber-core.cjs +1 -1
- package/vercel-ai.cjs +123 -34
- package/vercel-ai.d.ts +7 -0
- package/benchmark.js +0 -355
- package/examples/python-bridge/bridge_daemon.cjs +0 -260
- package/examples/python-bridge/langchain_adapter.py +0 -20
- package/examples/python-bridge/privacyscrubber.py +0 -36
- package/examples/python-bridge/test_python_bridge.py +0 -320
package/README.md
CHANGED
|
@@ -109,6 +109,55 @@ const completion = await openai.chat.completions.create({
|
|
|
109
109
|
console.log(completion.choices[0].message.content);
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
#### 1b. Google Gemini SDK Wrapper (`wrapGemini` / `wrapGoogleGenAI`)
|
|
113
|
+
|
|
114
|
+
Supports both the official `@google/genai` SDK (`ai.models.generateContent`, `ai.models.generateContentStream`) and `@google/generative-ai` (`model.generateContent`, `getGenerativeModel`):
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
import { GoogleGenAI } from '@google/genai';
|
|
118
|
+
import { wrapGemini } from '@privacyscrubber/sdk';
|
|
119
|
+
|
|
120
|
+
const ai = wrapGemini(new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }), {
|
|
121
|
+
profile: 'General'
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const response = await ai.models.generateContent({
|
|
125
|
+
model: 'gemini-2.5-flash',
|
|
126
|
+
contents: 'Summarize onboarding notes for Sarah Connor (sarah.connor@cyberdyne.io).'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Response .text and candidates[].content.parts[].text are automatically restored in RAM:
|
|
130
|
+
console.log(response.text);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### 1c. Universal HTTP & SSE `fetch` Interceptor (`createPrivacyFetch`)
|
|
134
|
+
|
|
135
|
+
Drop-in `fetch` wrapper for **OpenAI, Anthropic, OpenRouter, Groq, DeepSeek, Mistral, Together, Ollama**, or Cloudflare AI Gateway. Scrubs outbound JSON request payloads (`messages`, `prompt`, `input`, `system`, `contents`) in local RAM and transparently detokenizes both standard JSON responses and `text/event-stream` Server-Sent Events (SSE) streams:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
import OpenAI from 'openai';
|
|
139
|
+
import { createPrivacyFetch } from '@privacyscrubber/sdk';
|
|
140
|
+
|
|
141
|
+
const client = new OpenAI({
|
|
142
|
+
baseURL: 'https://openrouter.ai/api/v1',
|
|
143
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
144
|
+
fetch: createPrivacyFetch({ profile: 'Dev', detectSecrets: true })
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
#### 1d. OpenAI Agents SDK Guardrails (`createOpenAIAgentGuardrails`)
|
|
149
|
+
|
|
150
|
+
Plug-and-play `inputGuardrail` and `outputGuardrail` descriptors compatible with `@openai/agents`, complete with optional `tripwireTriggered` enforcement for critical secrets or strict zero-PII boundaries:
|
|
151
|
+
|
|
152
|
+
```javascript
|
|
153
|
+
import { createOpenAIAgentGuardrails } from '@privacyscrubber/sdk';
|
|
154
|
+
|
|
155
|
+
const { inputGuardrail, outputGuardrail, sanitizeInput, restoreOutput } = createOpenAIAgentGuardrails({
|
|
156
|
+
profile: 'Dev',
|
|
157
|
+
tripwireOnCritical: true // Triggers agent tripwire if raw keys/SSN are detected
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
112
161
|
---
|
|
113
162
|
|
|
114
163
|
### 2. Direct In-Memory Sanitization & Restoration
|
|
@@ -166,7 +215,7 @@ const { scrubbedText, tokenMap } = sanitize(rawCrmPayload, {
|
|
|
166
215
|
|
|
167
216
|
### 3. Vercel AI SDK Integration (`@privacyscrubber/sdk/vercel-ai`)
|
|
168
217
|
|
|
169
|
-
Dedicated zero-dependency submodule for Next.js App Router and Node.js applications built with the Vercel AI SDK (`ai`):
|
|
218
|
+
Dedicated zero-dependency submodule for Next.js App Router and Node.js applications built with the Vercel AI SDK (`ai` v4, v5, and v6 — compatible with `LanguageModelV1` `textDelta` as well as `LanguageModelV2`/`LanguageModelV3` `delta` and `reasoning-delta` streams, `result.content` arrays, and `tool-result` output sanitization):
|
|
170
219
|
|
|
171
220
|
```bash
|
|
172
221
|
npm install @privacyscrubber/sdk
|
package/browser.js
CHANGED
|
@@ -70,7 +70,10 @@ export class PrivacyScrubberLicenseError extends Error {
|
|
|
70
70
|
this.currentLength = currentLength;
|
|
71
71
|
this.limit = limit;
|
|
72
72
|
this.profile = profile;
|
|
73
|
-
|
|
73
|
+
const safeProf = encodeURIComponent(String(profile || 'General'));
|
|
74
|
+
this.upgradeUrl = `https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
75
|
+
this.trialUrl = `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${safeProf}&utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell`;
|
|
76
|
+
this.trialCommand = 'npx @privacyscrubber/sdk trial';
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -170,11 +173,27 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
170
173
|
trademark: 'ZTDS™ Reg. #182655957 (ILPO Cl 9 & 42)'
|
|
171
174
|
};
|
|
172
175
|
|
|
176
|
+
const agenticProtocol = {
|
|
177
|
+
protocolsSupported: ['MCP-1.0 (stdio/SSE)', 'A2A-1.0 (Agent2Agent)', 'ZTDS-RFC-v1.0'],
|
|
178
|
+
agentCardUrl: 'https://privacyscrubber.com/.well-known/agent-card.json',
|
|
179
|
+
ztdsStandardCardUrl: 'https://ztds.ai/.well-known/agent-card.json',
|
|
180
|
+
regulatorySafeHarbor: [
|
|
181
|
+
'GDPR-Art-4(5)-Pseudonymization',
|
|
182
|
+
'GDPR-Art-25-Privacy-by-Design',
|
|
183
|
+
'GDPR-Art-32-TOMs',
|
|
184
|
+
'EU-AI-Act-Art-50',
|
|
185
|
+
'EU-AI-Act-Art-17-EN-18286-QMS',
|
|
186
|
+
'CNIL-ICO-Agentic-Memory-Compartmentalization',
|
|
187
|
+
'IL-PPL-Amendment-13-Sec-17'
|
|
188
|
+
],
|
|
189
|
+
zeroCleartextEgress: true
|
|
190
|
+
};
|
|
191
|
+
|
|
173
192
|
const statutoryCitations = [
|
|
174
193
|
{
|
|
175
|
-
statute: 'EU AI Act (Art. 50)',
|
|
176
|
-
scope: 'AI Transparency &
|
|
177
|
-
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models. Informational mapping, not a compliance determination.'
|
|
194
|
+
statute: 'EU AI Act (Art. 50 & Art. 17 / EN 18286:2026 QMS)',
|
|
195
|
+
scope: 'AI Transparency, Data Minimization & Harmonized QMS Governance',
|
|
196
|
+
rule: 'Sanitization runs locally by design; reduces the content sent to external AI models and supports Article 17 Quality Management System (EN 18286:2026) data governance controls. Informational mapping, not a compliance determination.'
|
|
178
197
|
},
|
|
179
198
|
{
|
|
180
199
|
statute: 'GDPR (Art. 4, 25, 28, 32)',
|
|
@@ -206,6 +225,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
|
|
|
206
225
|
frameworksList: Array.from(frameworksSet),
|
|
207
226
|
statutoryCitations,
|
|
208
227
|
ipHoldings,
|
|
228
|
+
agenticProtocol,
|
|
209
229
|
multimodalEgress: '0.00 Bytes (Solid Blackout & Burn-in Verified)'
|
|
210
230
|
};
|
|
211
231
|
}
|
|
@@ -296,7 +316,8 @@ export function sanitize(text, options = {}) {
|
|
|
296
316
|
|
|
297
317
|
if (!isPro && text.length > limit) {
|
|
298
318
|
if (shouldThrow) {
|
|
299
|
-
const
|
|
319
|
+
const safeProf = encodeURIComponent(String(profile || 'General'));
|
|
320
|
+
const errorMsg = `[PrivacyScrubber] Free Tier character limit exceeded (${limit.toLocaleString()} chars for profile '${profile}', received ${text.length.toLocaleString()} chars). Upgrade to Developer SDK ($299/mo) for unlimited throughput: https://privacyscrubber.com/pricing?tier=sdk&profile=${safeProf}&src=dev_limit (Start 7-day B2B trial: 'npx @privacyscrubber/sdk trial')`;
|
|
300
321
|
throw new PrivacyScrubberLicenseError(errorMsg, {
|
|
301
322
|
tier: 'FREE',
|
|
302
323
|
currentLength: text.length,
|
|
@@ -307,7 +328,14 @@ export function sanitize(text, options = {}) {
|
|
|
307
328
|
wasTruncated = true;
|
|
308
329
|
}
|
|
309
330
|
|
|
310
|
-
const
|
|
331
|
+
const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
|
|
332
|
+
const safeCustomRules = (Array.isArray(customRules) ? customRules : []).filter(r => {
|
|
333
|
+
const pat = typeof r === 'string' ? r : (r && (r.pattern || (r.regex && r.regex.source)));
|
|
334
|
+
if (!pat || typeof pat !== 'string' || pat.length > 500) return false;
|
|
335
|
+
if (nestedQuantifierRegex.test(pat)) return false;
|
|
336
|
+
return true;
|
|
337
|
+
});
|
|
338
|
+
const activeRules = [...safeCustomRules];
|
|
311
339
|
if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
|
|
312
340
|
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
313
341
|
activeRules.push({
|
|
@@ -366,11 +394,15 @@ export function sanitize(text, options = {}) {
|
|
|
366
394
|
limit: limit,
|
|
367
395
|
wasTruncated: wasTruncated,
|
|
368
396
|
...(isSeatTier && {
|
|
369
|
-
upgradeUrl:
|
|
397
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General'))}&src=seat_tier_mismatch`,
|
|
398
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General'))}&src=seat_tier_mismatch`,
|
|
399
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
370
400
|
recommendation: `Your ${rawTier} license is valid for interactive browser seats only. Upgrade to Developer SDK ($299/mo) for unlimited programmatic SDK throughput: https://privacyscrubber.com/pricing?tier=sdk`
|
|
371
401
|
}),
|
|
372
402
|
...(!isPro && !isSeatTier && {
|
|
373
|
-
upgradeUrl:
|
|
403
|
+
upgradeUrl: `https://privacyscrubber.com/pricing?tier=sdk&profile=${encodeURIComponent(String(profile || 'General'))}&src=dev_upsell`,
|
|
404
|
+
trialUrl: `https://privacyscrubber.com/pricing?tier=sdk&trial=true&profile=${encodeURIComponent(String(profile || 'General'))}&src=dev_upsell`,
|
|
405
|
+
trialCommand: 'npx @privacyscrubber/sdk trial',
|
|
374
406
|
recommendation: 'Upgrade to Developer SDK ($299/mo) for unlimited text processing, custom regex rules, and specialized compliance profiles.'
|
|
375
407
|
})
|
|
376
408
|
},
|
|
@@ -448,6 +480,21 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
448
480
|
|
|
449
481
|
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
450
482
|
let totalCount = 0;
|
|
483
|
+
const seen = new WeakMap();
|
|
484
|
+
|
|
485
|
+
function isNonPlainObject(val) {
|
|
486
|
+
return (
|
|
487
|
+
val instanceof Date ||
|
|
488
|
+
val instanceof RegExp ||
|
|
489
|
+
val instanceof Error ||
|
|
490
|
+
val instanceof Promise ||
|
|
491
|
+
val instanceof WeakMap ||
|
|
492
|
+
val instanceof WeakSet ||
|
|
493
|
+
(typeof URL !== 'undefined' && val instanceof URL) ||
|
|
494
|
+
ArrayBuffer.isView(val) ||
|
|
495
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
496
|
+
);
|
|
497
|
+
}
|
|
451
498
|
|
|
452
499
|
function traverseAndSanitize(val) {
|
|
453
500
|
if (val === null || val === undefined) return val;
|
|
@@ -462,20 +509,42 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
462
509
|
totalCount += res.count;
|
|
463
510
|
return res.scrubbedText;
|
|
464
511
|
}
|
|
512
|
+
if (typeof val !== 'object') return val;
|
|
513
|
+
if (isNonPlainObject(val)) return val;
|
|
514
|
+
if (seen.has(val)) return seen.get(val);
|
|
515
|
+
|
|
465
516
|
if (Array.isArray(val)) {
|
|
466
|
-
|
|
517
|
+
const outArr = [];
|
|
518
|
+
seen.set(val, outArr);
|
|
519
|
+
for (let i = 0; i < val.length; i++) {
|
|
520
|
+
outArr.push(traverseAndSanitize(val[i]));
|
|
521
|
+
}
|
|
522
|
+
return outArr;
|
|
467
523
|
}
|
|
468
|
-
if (val instanceof
|
|
469
|
-
|
|
524
|
+
if (val instanceof Map) {
|
|
525
|
+
const outMap = new Map();
|
|
526
|
+
seen.set(val, outMap);
|
|
527
|
+
for (const [k, v] of val.entries()) {
|
|
528
|
+
outMap.set(k, traverseAndSanitize(v));
|
|
529
|
+
}
|
|
530
|
+
return outMap;
|
|
470
531
|
}
|
|
471
|
-
if (
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
532
|
+
if (val instanceof Set) {
|
|
533
|
+
const outSet = new Set();
|
|
534
|
+
seen.set(val, outSet);
|
|
535
|
+
for (const item of val.values()) {
|
|
536
|
+
outSet.add(traverseAndSanitize(item));
|
|
475
537
|
}
|
|
476
|
-
return
|
|
538
|
+
return outSet;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const out = {};
|
|
542
|
+
seen.set(val, out);
|
|
543
|
+
for (const [k, v] of Object.entries(val)) {
|
|
544
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
545
|
+
out[k] = traverseAndSanitize(v);
|
|
477
546
|
}
|
|
478
|
-
return
|
|
547
|
+
return out;
|
|
479
548
|
}
|
|
480
549
|
|
|
481
550
|
const sanitizedResult = traverseAndSanitize(obj);
|
|
@@ -498,23 +567,62 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
498
567
|
};
|
|
499
568
|
}
|
|
500
569
|
|
|
501
|
-
export function restoreObject(obj, tokenMap = {}) {
|
|
570
|
+
export function restoreObject(obj, tokenMap = {}, _seen = null) {
|
|
502
571
|
if (obj === null || obj === undefined) return obj;
|
|
503
572
|
if (typeof obj === 'string') {
|
|
504
573
|
const res = restore(obj, tokenMap);
|
|
505
574
|
return res.restoredText;
|
|
506
575
|
}
|
|
576
|
+
if (typeof obj !== 'object') return obj;
|
|
577
|
+
if (
|
|
578
|
+
obj instanceof Date ||
|
|
579
|
+
obj instanceof RegExp ||
|
|
580
|
+
obj instanceof Error ||
|
|
581
|
+
obj instanceof Promise ||
|
|
582
|
+
obj instanceof WeakMap ||
|
|
583
|
+
obj instanceof WeakSet ||
|
|
584
|
+
(typeof URL !== 'undefined' && obj instanceof URL) ||
|
|
585
|
+
ArrayBuffer.isView(obj) ||
|
|
586
|
+
(typeof ArrayBuffer !== 'undefined' && obj instanceof ArrayBuffer)
|
|
587
|
+
) {
|
|
588
|
+
return obj;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const seen = _seen || new WeakMap();
|
|
592
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
593
|
+
|
|
507
594
|
if (Array.isArray(obj)) {
|
|
508
|
-
|
|
595
|
+
const outArr = [];
|
|
596
|
+
seen.set(obj, outArr);
|
|
597
|
+
for (let i = 0; i < obj.length; i++) {
|
|
598
|
+
outArr.push(restoreObject(obj[i], tokenMap, seen));
|
|
599
|
+
}
|
|
600
|
+
return outArr;
|
|
509
601
|
}
|
|
510
|
-
if (
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
602
|
+
if (obj instanceof Map) {
|
|
603
|
+
const outMap = new Map();
|
|
604
|
+
seen.set(obj, outMap);
|
|
605
|
+
for (const [k, v] of obj.entries()) {
|
|
606
|
+
outMap.set(k, restoreObject(v, tokenMap, seen));
|
|
514
607
|
}
|
|
515
|
-
return
|
|
608
|
+
return outMap;
|
|
609
|
+
}
|
|
610
|
+
if (obj instanceof Set) {
|
|
611
|
+
const outSet = new Set();
|
|
612
|
+
seen.set(obj, outSet);
|
|
613
|
+
for (const item of obj.values()) {
|
|
614
|
+
outSet.add(restoreObject(item, tokenMap, seen));
|
|
615
|
+
}
|
|
616
|
+
return outSet;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const out = {};
|
|
620
|
+
seen.set(obj, out);
|
|
621
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
622
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
623
|
+
out[k] = restoreObject(v, tokenMap, seen);
|
|
516
624
|
}
|
|
517
|
-
return
|
|
625
|
+
return out;
|
|
518
626
|
}
|
|
519
627
|
|
|
520
628
|
/**
|
|
@@ -546,12 +654,16 @@ export class PrivacyScrubberEngine {
|
|
|
546
654
|
.forEach(v => mergedIgnoreList.add(v));
|
|
547
655
|
}
|
|
548
656
|
|
|
657
|
+
const effectiveExistingMap = options.persistSession !== false
|
|
658
|
+
? { ...this.sessionMap, ...(options.existingSessionMap || {}) }
|
|
659
|
+
: (options.existingSessionMap || {});
|
|
660
|
+
|
|
549
661
|
const mergedOptions = {
|
|
550
662
|
profile: options.profile || this.defaultProfile,
|
|
551
663
|
licenseKey: options.licenseKey || this.licenseKey,
|
|
552
664
|
customRules: [...this.customRules, ...(options.customRules || [])],
|
|
553
665
|
tokenLabelMap: { ...this.tokenLabelMap, ...(options.tokenLabelMap || {}) },
|
|
554
|
-
existingSessionMap:
|
|
666
|
+
existingSessionMap: effectiveExistingMap,
|
|
555
667
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
556
668
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
557
669
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
@@ -1116,6 +1228,295 @@ export function generateAuditReport(sessionMap = {}, options = {}) {
|
|
|
1116
1228
|
return formatPsr2Report({ format: format === 'summary' ? 'summary' : 'markdown', receipt, telemetry, company_name: companyName, department, utmMedium: 'markdown' });
|
|
1117
1229
|
}
|
|
1118
1230
|
|
|
1231
|
+
export function wrapGemini(geminiClientOrModel, options = {}) {
|
|
1232
|
+
if (!geminiClientOrModel || typeof geminiClientOrModel !== 'object') {
|
|
1233
|
+
throw new Error('[PrivacyScrubber] Invalid Gemini client or model passed to wrapGemini.');
|
|
1234
|
+
}
|
|
1235
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1236
|
+
const sharedSessionMap = {};
|
|
1237
|
+
|
|
1238
|
+
function sanitizeGeminiContentNode(node, sessionTokenMap) {
|
|
1239
|
+
if (node === null || node === undefined) return node;
|
|
1240
|
+
if (typeof node === 'string') {
|
|
1241
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1242
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1243
|
+
return res.scrubbedText;
|
|
1244
|
+
}
|
|
1245
|
+
if (Array.isArray(node)) {
|
|
1246
|
+
return node.map(item => sanitizeGeminiContentNode(item, sessionTokenMap));
|
|
1247
|
+
}
|
|
1248
|
+
if (typeof node === 'object') {
|
|
1249
|
+
const copy = { ...node };
|
|
1250
|
+
if (typeof copy.text === 'string') {
|
|
1251
|
+
const res = engine.sanitize(copy.text, { existingSessionMap: sessionTokenMap });
|
|
1252
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1253
|
+
copy.text = res.scrubbedText;
|
|
1254
|
+
}
|
|
1255
|
+
if (Array.isArray(copy.parts)) {
|
|
1256
|
+
copy.parts = copy.parts.map(p => sanitizeGeminiContentNode(p, sessionTokenMap));
|
|
1257
|
+
}
|
|
1258
|
+
if (copy.contents !== undefined) {
|
|
1259
|
+
copy.contents = sanitizeGeminiContentNode(copy.contents, sessionTokenMap);
|
|
1260
|
+
}
|
|
1261
|
+
if (copy.systemInstruction !== undefined) {
|
|
1262
|
+
copy.systemInstruction = sanitizeGeminiContentNode(copy.systemInstruction, sessionTokenMap);
|
|
1263
|
+
}
|
|
1264
|
+
if (copy.config && typeof copy.config === 'object') {
|
|
1265
|
+
copy.config = { ...copy.config };
|
|
1266
|
+
if (copy.config.systemInstruction !== undefined) {
|
|
1267
|
+
copy.config.systemInstruction = sanitizeGeminiContentNode(copy.config.systemInstruction, sessionTokenMap);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return copy;
|
|
1271
|
+
}
|
|
1272
|
+
return node;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function restoreGeminiResponseObject(respObj, sessionTokenMap, detok = null) {
|
|
1276
|
+
if (!respObj || typeof respObj !== 'object') return respObj;
|
|
1277
|
+
const restoreFn = (str) => detok ? detok.feed(str) : engine.restore(str, sessionTokenMap).restoredText;
|
|
1278
|
+
let combinedText = '';
|
|
1279
|
+
if (Array.isArray(respObj.candidates)) {
|
|
1280
|
+
respObj.candidates = respObj.candidates.map(cand => {
|
|
1281
|
+
if (!cand || typeof cand !== 'object') return cand;
|
|
1282
|
+
const candCopy = { ...cand };
|
|
1283
|
+
if (candCopy.content && Array.isArray(candCopy.content.parts)) {
|
|
1284
|
+
candCopy.content = {
|
|
1285
|
+
...candCopy.content,
|
|
1286
|
+
parts: candCopy.content.parts.map(part => {
|
|
1287
|
+
if (part && typeof part.text === 'string') {
|
|
1288
|
+
const restoredText = restoreFn(part.text);
|
|
1289
|
+
combinedText += restoredText;
|
|
1290
|
+
return { ...part, text: restoredText };
|
|
1291
|
+
}
|
|
1292
|
+
return part;
|
|
1293
|
+
})
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
return candCopy;
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (typeof respObj.text === 'function') {
|
|
1300
|
+
const origTextFn = respObj.text.bind(respObj);
|
|
1301
|
+
respObj.text = () => {
|
|
1302
|
+
if (combinedText) return combinedText;
|
|
1303
|
+
const raw = origTextFn();
|
|
1304
|
+
return typeof raw === 'string' ? restoreFn(raw) : raw;
|
|
1305
|
+
};
|
|
1306
|
+
} else if (typeof respObj.text === 'string') {
|
|
1307
|
+
respObj.text = combinedText || restoreFn(respObj.text);
|
|
1308
|
+
} else if (combinedText) {
|
|
1309
|
+
respObj.text = combinedText;
|
|
1310
|
+
}
|
|
1311
|
+
if (respObj.response && typeof respObj.response === 'object') {
|
|
1312
|
+
restoreGeminiResponseObject(respObj.response, sessionTokenMap, detok);
|
|
1313
|
+
}
|
|
1314
|
+
return respObj;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function wrapTargetObject(target) {
|
|
1318
|
+
if (typeof target.generateContent === 'function') {
|
|
1319
|
+
const origGen = target.generateContent.bind(target);
|
|
1320
|
+
target.generateContent = async function(req, ...rest) {
|
|
1321
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1322
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1323
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1324
|
+
const res = await origGen(sanitizedReq, ...rest);
|
|
1325
|
+
return restoreGeminiResponseObject(res, sessionTokenMap);
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (typeof target.generateContentStream === 'function') {
|
|
1329
|
+
const origStream = target.generateContentStream.bind(target);
|
|
1330
|
+
target.generateContentStream = async function(req, ...rest) {
|
|
1331
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1332
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1333
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1334
|
+
const streamRes = await origStream(sanitizedReq, ...rest);
|
|
1335
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1336
|
+
async function* wrapAsyncChunks(iterable) {
|
|
1337
|
+
let lastChunk = null;
|
|
1338
|
+
for await (const chunk of iterable) {
|
|
1339
|
+
if (!chunk) continue;
|
|
1340
|
+
lastChunk = chunk;
|
|
1341
|
+
const cloned = { ...chunk };
|
|
1342
|
+
restoreGeminiResponseObject(cloned, sessionTokenMap, detok);
|
|
1343
|
+
yield cloned;
|
|
1344
|
+
}
|
|
1345
|
+
const tail = detok.flush();
|
|
1346
|
+
if (tail && lastChunk) {
|
|
1347
|
+
yield { ...lastChunk, candidates: [{ content: { role: 'model', parts: [{ text: tail }] } }], text: tail };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (streamRes && typeof streamRes[Symbol.asyncIterator] === 'function') {
|
|
1351
|
+
return wrapAsyncChunks(streamRes);
|
|
1352
|
+
}
|
|
1353
|
+
if (streamRes && streamRes.stream && typeof streamRes.stream[Symbol.asyncIterator] === 'function') {
|
|
1354
|
+
return { ...streamRes, stream: wrapAsyncChunks(streamRes.stream) };
|
|
1355
|
+
}
|
|
1356
|
+
return streamRes;
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (geminiClientOrModel.models && typeof geminiClientOrModel.models === 'object') {
|
|
1362
|
+
wrapTargetObject(geminiClientOrModel.models);
|
|
1363
|
+
}
|
|
1364
|
+
if (typeof geminiClientOrModel.generateContent === 'function' || typeof geminiClientOrModel.generateContentStream === 'function') {
|
|
1365
|
+
wrapTargetObject(geminiClientOrModel);
|
|
1366
|
+
}
|
|
1367
|
+
geminiClientOrModel._ztdsEngine = engine;
|
|
1368
|
+
return geminiClientOrModel;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
export const wrapGoogleGenAI = wrapGemini;
|
|
1372
|
+
|
|
1373
|
+
export function createPrivacyFetch(options = {}) {
|
|
1374
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1375
|
+
const baseFetch = options.fetch || (typeof globalThis !== 'undefined' && globalThis.fetch);
|
|
1376
|
+
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
1377
|
+
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
1378
|
+
|
|
1379
|
+
const PROMPT_KEYS = new Set([
|
|
1380
|
+
'messages', 'prompt', 'input', 'contents', 'system',
|
|
1381
|
+
'systemInstruction', 'system_instruction', 'instructions', 'text', 'content'
|
|
1382
|
+
]);
|
|
1383
|
+
const SAFE_SKIP_KEYS = new Set([
|
|
1384
|
+
'model', 'role', 'type', 'id', 'object', 'finish_reason', 'stop_reason',
|
|
1385
|
+
'encoding_format', 'response_format', 'tool_choice', 'stream'
|
|
1386
|
+
]);
|
|
1387
|
+
|
|
1388
|
+
function sanitizeJsonNode(node, sessionTokenMap) {
|
|
1389
|
+
if (node === null || node === undefined) return node;
|
|
1390
|
+
if (typeof node === 'string') {
|
|
1391
|
+
if (!node.trim()) return node;
|
|
1392
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1393
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1394
|
+
return res.scrubbedText;
|
|
1395
|
+
}
|
|
1396
|
+
if (Array.isArray(node)) {
|
|
1397
|
+
return node.map(item => sanitizeJsonNode(item, sessionTokenMap));
|
|
1398
|
+
}
|
|
1399
|
+
if (typeof node === 'object') {
|
|
1400
|
+
const out = {};
|
|
1401
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1402
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
1403
|
+
if (SAFE_SKIP_KEYS.has(k)) {
|
|
1404
|
+
out[k] = v;
|
|
1405
|
+
} else if (PROMPT_KEYS.has(k) || typeof v === 'object') {
|
|
1406
|
+
out[k] = sanitizeJsonNode(v, sessionTokenMap);
|
|
1407
|
+
} else {
|
|
1408
|
+
out[k] = v;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return out;
|
|
1412
|
+
}
|
|
1413
|
+
return node;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const privacyFetch = async function(input, init = {}) {
|
|
1417
|
+
if (typeof baseFetch !== 'function') {
|
|
1418
|
+
throw new Error('[PrivacyScrubber] No global fetch implementation found; pass { fetch } to createPrivacyFetch.');
|
|
1419
|
+
}
|
|
1420
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1421
|
+
let modifiedInit = init ? { ...init } : {};
|
|
1422
|
+
if (sanitizeInputs && modifiedInit && typeof modifiedInit.body === 'string') {
|
|
1423
|
+
const trimmed = modifiedInit.body.trim();
|
|
1424
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
1425
|
+
try {
|
|
1426
|
+
const parsed = JSON.parse(modifiedInit.body);
|
|
1427
|
+
modifiedInit.body = JSON.stringify(sanitizeJsonNode(parsed, sessionTokenMap));
|
|
1428
|
+
} catch (_) {}
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
const response = await baseFetch(input, modifiedInit);
|
|
1432
|
+
if (!sanitizeOutputs || !response || Object.keys(sessionTokenMap).length === 0) {
|
|
1433
|
+
return response;
|
|
1434
|
+
}
|
|
1435
|
+
const contentType = (response.headers && typeof response.headers.get === 'function')
|
|
1436
|
+
? (response.headers.get('content-type') || '').toLowerCase()
|
|
1437
|
+
: '';
|
|
1438
|
+
if (contentType.includes('application/json') || typeof response.json === 'function') {
|
|
1439
|
+
try {
|
|
1440
|
+
const rawText = typeof response.text === 'function'
|
|
1441
|
+
? await response.text()
|
|
1442
|
+
: JSON.stringify(await response.json());
|
|
1443
|
+
const restoredObj = restoreObject(JSON.parse(rawText), sessionTokenMap);
|
|
1444
|
+
const restoredText = JSON.stringify(restoredObj);
|
|
1445
|
+
if (typeof Response !== 'undefined') {
|
|
1446
|
+
const headers = new Headers(response.headers || {});
|
|
1447
|
+
headers.delete('content-length');
|
|
1448
|
+
return new Response(restoredText, { status: response.status, statusText: response.statusText, headers });
|
|
1449
|
+
}
|
|
1450
|
+
return { ...response, text: async () => restoredText, json: async () => restoredObj };
|
|
1451
|
+
} catch (_) {
|
|
1452
|
+
return response;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return response;
|
|
1456
|
+
};
|
|
1457
|
+
privacyFetch.engine = engine;
|
|
1458
|
+
return privacyFetch;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
export function createOpenAIAgentGuardrails(options = {}) {
|
|
1462
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1463
|
+
const tripwireOnCritical = options.tripwireOnCritical === true;
|
|
1464
|
+
const tripwireOnAnyPii = options.tripwireOnAnyPii === true;
|
|
1465
|
+
|
|
1466
|
+
const inputGuardrail = {
|
|
1467
|
+
name: options.inputGuardrailName || 'PrivacyScrubber ZTDS Input Guardrail',
|
|
1468
|
+
async execute({ input, context } = {}) {
|
|
1469
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1470
|
+
let scrubbedInput = input;
|
|
1471
|
+
let count = 0;
|
|
1472
|
+
if (typeof input === 'string') {
|
|
1473
|
+
const res = engine.sanitize(input, { existingSessionMap: sessionTokenMap });
|
|
1474
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1475
|
+
scrubbedInput = res.scrubbedText;
|
|
1476
|
+
count = res.count;
|
|
1477
|
+
}
|
|
1478
|
+
Object.assign(engine.sessionMap, sessionTokenMap);
|
|
1479
|
+
if (context && typeof context === 'object') {
|
|
1480
|
+
context.ztdsTokenMap = engine.sessionMap;
|
|
1481
|
+
context.ztdsScrubbedInput = scrubbedInput;
|
|
1482
|
+
}
|
|
1483
|
+
const telemetry = buildAuditTelemetry(sessionTokenMap);
|
|
1484
|
+
const isCritical = telemetry.riskLevel && telemetry.riskLevel.includes('CRITICAL');
|
|
1485
|
+
const tripwireTriggered = Boolean((tripwireOnAnyPii && count > 0) || (tripwireOnCritical && count > 0 && isCritical));
|
|
1486
|
+
return {
|
|
1487
|
+
tripwireTriggered,
|
|
1488
|
+
outputInfo: { scrubbedInput, tokenMap: { ...sessionTokenMap }, tokenCount: count, riskLevel: telemetry.riskLevel, telemetry }
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
const outputGuardrail = {
|
|
1494
|
+
name: options.outputGuardrailName || 'PrivacyScrubber ZTDS Output Guardrail',
|
|
1495
|
+
async execute({ agentOutput, context } = {}) {
|
|
1496
|
+
const activeMap = (context && context.ztdsTokenMap) || engine.sessionMap || {};
|
|
1497
|
+
let restoredOutput = agentOutput;
|
|
1498
|
+
let restoredCount = 0;
|
|
1499
|
+
if (typeof agentOutput === 'string') {
|
|
1500
|
+
const res = engine.restore(agentOutput, activeMap);
|
|
1501
|
+
restoredOutput = res.restoredText;
|
|
1502
|
+
restoredCount = res.restoredCount;
|
|
1503
|
+
} else if (agentOutput && typeof agentOutput === 'object') {
|
|
1504
|
+
restoredOutput = restoreObject(agentOutput, activeMap);
|
|
1505
|
+
if (options.mutateInPlace !== false) Object.assign(agentOutput, restoredOutput);
|
|
1506
|
+
}
|
|
1507
|
+
return { tripwireTriggered: false, outputInfo: { restoredOutput, restoredCount } };
|
|
1508
|
+
}
|
|
1509
|
+
};
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
inputGuardrail,
|
|
1513
|
+
outputGuardrail,
|
|
1514
|
+
inputGuardrails: [inputGuardrail],
|
|
1515
|
+
outputGuardrails: [outputGuardrail],
|
|
1516
|
+
engine
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1119
1520
|
// Vision & Multimodal exports
|
|
1120
1521
|
export {
|
|
1121
1522
|
stripExifMetadata,
|
|
@@ -1153,6 +1554,10 @@ export default {
|
|
|
1153
1554
|
createStreamTokenRestorer,
|
|
1154
1555
|
buildStreamReplacer,
|
|
1155
1556
|
wrapAiStream,
|
|
1557
|
+
wrapGemini,
|
|
1558
|
+
wrapGoogleGenAI,
|
|
1559
|
+
createPrivacyFetch,
|
|
1560
|
+
createOpenAIAgentGuardrails,
|
|
1156
1561
|
scrubText,
|
|
1157
1562
|
unscrubText,
|
|
1158
1563
|
DEVOPS_SECRETS_DETECTOR,
|