@privacyscrubber/sdk 2.4.5 → 2.4.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 +50 -1
- package/browser.js +399 -19
- 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/index.cjs +1153 -81
- package/index.d.ts +147 -2
- package/index.js +1153 -81
- 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
|
@@ -307,7 +307,14 @@ export function sanitize(text, options = {}) {
|
|
|
307
307
|
wasTruncated = true;
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
const
|
|
310
|
+
const nestedQuantifierRegex = /\([^)]*(\+|\*|\{\d+,?\d*\})\)[+*]|\([^)]*(\+|\*|\{\d+,?\d*\})\)\{\d+,?\d*\}|\([^)]*\|[^)]*\)(?:[+*]|\{\d+,?\d*\})/;
|
|
311
|
+
const safeCustomRules = (Array.isArray(customRules) ? customRules : []).filter(r => {
|
|
312
|
+
const pat = typeof r === 'string' ? r : (r && (r.pattern || (r.regex && r.regex.source)));
|
|
313
|
+
if (!pat || typeof pat !== 'string' || pat.length > 500) return false;
|
|
314
|
+
if (nestedQuantifierRegex.test(pat)) return false;
|
|
315
|
+
return true;
|
|
316
|
+
});
|
|
317
|
+
const activeRules = [...safeCustomRules];
|
|
311
318
|
if (profile.toLowerCase() === 'dev' || detectSecrets === true) {
|
|
312
319
|
DEVOPS_SECRETS_DETECTOR.forEach(rule => {
|
|
313
320
|
activeRules.push({
|
|
@@ -448,6 +455,21 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
448
455
|
|
|
449
456
|
const mergedSessionMap = { ...(options.existingSessionMap || {}) };
|
|
450
457
|
let totalCount = 0;
|
|
458
|
+
const seen = new WeakMap();
|
|
459
|
+
|
|
460
|
+
function isNonPlainObject(val) {
|
|
461
|
+
return (
|
|
462
|
+
val instanceof Date ||
|
|
463
|
+
val instanceof RegExp ||
|
|
464
|
+
val instanceof Error ||
|
|
465
|
+
val instanceof Promise ||
|
|
466
|
+
val instanceof WeakMap ||
|
|
467
|
+
val instanceof WeakSet ||
|
|
468
|
+
(typeof URL !== 'undefined' && val instanceof URL) ||
|
|
469
|
+
ArrayBuffer.isView(val) ||
|
|
470
|
+
(typeof ArrayBuffer !== 'undefined' && val instanceof ArrayBuffer)
|
|
471
|
+
);
|
|
472
|
+
}
|
|
451
473
|
|
|
452
474
|
function traverseAndSanitize(val) {
|
|
453
475
|
if (val === null || val === undefined) return val;
|
|
@@ -462,20 +484,42 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
462
484
|
totalCount += res.count;
|
|
463
485
|
return res.scrubbedText;
|
|
464
486
|
}
|
|
487
|
+
if (typeof val !== 'object') return val;
|
|
488
|
+
if (isNonPlainObject(val)) return val;
|
|
489
|
+
if (seen.has(val)) return seen.get(val);
|
|
490
|
+
|
|
465
491
|
if (Array.isArray(val)) {
|
|
466
|
-
|
|
492
|
+
const outArr = [];
|
|
493
|
+
seen.set(val, outArr);
|
|
494
|
+
for (let i = 0; i < val.length; i++) {
|
|
495
|
+
outArr.push(traverseAndSanitize(val[i]));
|
|
496
|
+
}
|
|
497
|
+
return outArr;
|
|
467
498
|
}
|
|
468
|
-
if (val instanceof
|
|
469
|
-
|
|
499
|
+
if (val instanceof Map) {
|
|
500
|
+
const outMap = new Map();
|
|
501
|
+
seen.set(val, outMap);
|
|
502
|
+
for (const [k, v] of val.entries()) {
|
|
503
|
+
outMap.set(k, traverseAndSanitize(v));
|
|
504
|
+
}
|
|
505
|
+
return outMap;
|
|
470
506
|
}
|
|
471
|
-
if (
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
507
|
+
if (val instanceof Set) {
|
|
508
|
+
const outSet = new Set();
|
|
509
|
+
seen.set(val, outSet);
|
|
510
|
+
for (const item of val.values()) {
|
|
511
|
+
outSet.add(traverseAndSanitize(item));
|
|
475
512
|
}
|
|
476
|
-
return
|
|
513
|
+
return outSet;
|
|
477
514
|
}
|
|
478
|
-
|
|
515
|
+
|
|
516
|
+
const out = {};
|
|
517
|
+
seen.set(val, out);
|
|
518
|
+
for (const [k, v] of Object.entries(val)) {
|
|
519
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
520
|
+
out[k] = traverseAndSanitize(v);
|
|
521
|
+
}
|
|
522
|
+
return out;
|
|
479
523
|
}
|
|
480
524
|
|
|
481
525
|
const sanitizedResult = traverseAndSanitize(obj);
|
|
@@ -498,23 +542,62 @@ export function sanitizeObject(obj, options = {}) {
|
|
|
498
542
|
};
|
|
499
543
|
}
|
|
500
544
|
|
|
501
|
-
export function restoreObject(obj, tokenMap = {}) {
|
|
545
|
+
export function restoreObject(obj, tokenMap = {}, _seen = null) {
|
|
502
546
|
if (obj === null || obj === undefined) return obj;
|
|
503
547
|
if (typeof obj === 'string') {
|
|
504
548
|
const res = restore(obj, tokenMap);
|
|
505
549
|
return res.restoredText;
|
|
506
550
|
}
|
|
551
|
+
if (typeof obj !== 'object') return obj;
|
|
552
|
+
if (
|
|
553
|
+
obj instanceof Date ||
|
|
554
|
+
obj instanceof RegExp ||
|
|
555
|
+
obj instanceof Error ||
|
|
556
|
+
obj instanceof Promise ||
|
|
557
|
+
obj instanceof WeakMap ||
|
|
558
|
+
obj instanceof WeakSet ||
|
|
559
|
+
(typeof URL !== 'undefined' && obj instanceof URL) ||
|
|
560
|
+
ArrayBuffer.isView(obj) ||
|
|
561
|
+
(typeof ArrayBuffer !== 'undefined' && obj instanceof ArrayBuffer)
|
|
562
|
+
) {
|
|
563
|
+
return obj;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const seen = _seen || new WeakMap();
|
|
567
|
+
if (seen.has(obj)) return seen.get(obj);
|
|
568
|
+
|
|
507
569
|
if (Array.isArray(obj)) {
|
|
508
|
-
|
|
570
|
+
const outArr = [];
|
|
571
|
+
seen.set(obj, outArr);
|
|
572
|
+
for (let i = 0; i < obj.length; i++) {
|
|
573
|
+
outArr.push(restoreObject(obj[i], tokenMap, seen));
|
|
574
|
+
}
|
|
575
|
+
return outArr;
|
|
509
576
|
}
|
|
510
|
-
if (
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
577
|
+
if (obj instanceof Map) {
|
|
578
|
+
const outMap = new Map();
|
|
579
|
+
seen.set(obj, outMap);
|
|
580
|
+
for (const [k, v] of obj.entries()) {
|
|
581
|
+
outMap.set(k, restoreObject(v, tokenMap, seen));
|
|
514
582
|
}
|
|
515
|
-
return
|
|
583
|
+
return outMap;
|
|
516
584
|
}
|
|
517
|
-
|
|
585
|
+
if (obj instanceof Set) {
|
|
586
|
+
const outSet = new Set();
|
|
587
|
+
seen.set(obj, outSet);
|
|
588
|
+
for (const item of obj.values()) {
|
|
589
|
+
outSet.add(restoreObject(item, tokenMap, seen));
|
|
590
|
+
}
|
|
591
|
+
return outSet;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const out = {};
|
|
595
|
+
seen.set(obj, out);
|
|
596
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
597
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
598
|
+
out[k] = restoreObject(v, tokenMap, seen);
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
518
601
|
}
|
|
519
602
|
|
|
520
603
|
/**
|
|
@@ -546,12 +629,16 @@ export class PrivacyScrubberEngine {
|
|
|
546
629
|
.forEach(v => mergedIgnoreList.add(v));
|
|
547
630
|
}
|
|
548
631
|
|
|
632
|
+
const effectiveExistingMap = options.persistSession !== false
|
|
633
|
+
? { ...this.sessionMap, ...(options.existingSessionMap || {}) }
|
|
634
|
+
: (options.existingSessionMap || {});
|
|
635
|
+
|
|
549
636
|
const mergedOptions = {
|
|
550
637
|
profile: options.profile || this.defaultProfile,
|
|
551
638
|
licenseKey: options.licenseKey || this.licenseKey,
|
|
552
639
|
customRules: [...this.customRules, ...(options.customRules || [])],
|
|
553
640
|
tokenLabelMap: { ...this.tokenLabelMap, ...(options.tokenLabelMap || {}) },
|
|
554
|
-
existingSessionMap:
|
|
641
|
+
existingSessionMap: effectiveExistingMap,
|
|
555
642
|
generateReceipt: options.generateReceipt !== undefined ? options.generateReceipt : this.generateReceipt,
|
|
556
643
|
throwOnLimit: options.throwOnLimit !== undefined ? options.throwOnLimit : true,
|
|
557
644
|
detectSecrets: options.detectSecrets !== undefined ? options.detectSecrets : false,
|
|
@@ -1116,6 +1203,295 @@ export function generateAuditReport(sessionMap = {}, options = {}) {
|
|
|
1116
1203
|
return formatPsr2Report({ format: format === 'summary' ? 'summary' : 'markdown', receipt, telemetry, company_name: companyName, department, utmMedium: 'markdown' });
|
|
1117
1204
|
}
|
|
1118
1205
|
|
|
1206
|
+
export function wrapGemini(geminiClientOrModel, options = {}) {
|
|
1207
|
+
if (!geminiClientOrModel || typeof geminiClientOrModel !== 'object') {
|
|
1208
|
+
throw new Error('[PrivacyScrubber] Invalid Gemini client or model passed to wrapGemini.');
|
|
1209
|
+
}
|
|
1210
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1211
|
+
const sharedSessionMap = {};
|
|
1212
|
+
|
|
1213
|
+
function sanitizeGeminiContentNode(node, sessionTokenMap) {
|
|
1214
|
+
if (node === null || node === undefined) return node;
|
|
1215
|
+
if (typeof node === 'string') {
|
|
1216
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1217
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1218
|
+
return res.scrubbedText;
|
|
1219
|
+
}
|
|
1220
|
+
if (Array.isArray(node)) {
|
|
1221
|
+
return node.map(item => sanitizeGeminiContentNode(item, sessionTokenMap));
|
|
1222
|
+
}
|
|
1223
|
+
if (typeof node === 'object') {
|
|
1224
|
+
const copy = { ...node };
|
|
1225
|
+
if (typeof copy.text === 'string') {
|
|
1226
|
+
const res = engine.sanitize(copy.text, { existingSessionMap: sessionTokenMap });
|
|
1227
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1228
|
+
copy.text = res.scrubbedText;
|
|
1229
|
+
}
|
|
1230
|
+
if (Array.isArray(copy.parts)) {
|
|
1231
|
+
copy.parts = copy.parts.map(p => sanitizeGeminiContentNode(p, sessionTokenMap));
|
|
1232
|
+
}
|
|
1233
|
+
if (copy.contents !== undefined) {
|
|
1234
|
+
copy.contents = sanitizeGeminiContentNode(copy.contents, sessionTokenMap);
|
|
1235
|
+
}
|
|
1236
|
+
if (copy.systemInstruction !== undefined) {
|
|
1237
|
+
copy.systemInstruction = sanitizeGeminiContentNode(copy.systemInstruction, sessionTokenMap);
|
|
1238
|
+
}
|
|
1239
|
+
if (copy.config && typeof copy.config === 'object') {
|
|
1240
|
+
copy.config = { ...copy.config };
|
|
1241
|
+
if (copy.config.systemInstruction !== undefined) {
|
|
1242
|
+
copy.config.systemInstruction = sanitizeGeminiContentNode(copy.config.systemInstruction, sessionTokenMap);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return copy;
|
|
1246
|
+
}
|
|
1247
|
+
return node;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function restoreGeminiResponseObject(respObj, sessionTokenMap, detok = null) {
|
|
1251
|
+
if (!respObj || typeof respObj !== 'object') return respObj;
|
|
1252
|
+
const restoreFn = (str) => detok ? detok.feed(str) : engine.restore(str, sessionTokenMap).restoredText;
|
|
1253
|
+
let combinedText = '';
|
|
1254
|
+
if (Array.isArray(respObj.candidates)) {
|
|
1255
|
+
respObj.candidates = respObj.candidates.map(cand => {
|
|
1256
|
+
if (!cand || typeof cand !== 'object') return cand;
|
|
1257
|
+
const candCopy = { ...cand };
|
|
1258
|
+
if (candCopy.content && Array.isArray(candCopy.content.parts)) {
|
|
1259
|
+
candCopy.content = {
|
|
1260
|
+
...candCopy.content,
|
|
1261
|
+
parts: candCopy.content.parts.map(part => {
|
|
1262
|
+
if (part && typeof part.text === 'string') {
|
|
1263
|
+
const restoredText = restoreFn(part.text);
|
|
1264
|
+
combinedText += restoredText;
|
|
1265
|
+
return { ...part, text: restoredText };
|
|
1266
|
+
}
|
|
1267
|
+
return part;
|
|
1268
|
+
})
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
return candCopy;
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (typeof respObj.text === 'function') {
|
|
1275
|
+
const origTextFn = respObj.text.bind(respObj);
|
|
1276
|
+
respObj.text = () => {
|
|
1277
|
+
if (combinedText) return combinedText;
|
|
1278
|
+
const raw = origTextFn();
|
|
1279
|
+
return typeof raw === 'string' ? restoreFn(raw) : raw;
|
|
1280
|
+
};
|
|
1281
|
+
} else if (typeof respObj.text === 'string') {
|
|
1282
|
+
respObj.text = combinedText || restoreFn(respObj.text);
|
|
1283
|
+
} else if (combinedText) {
|
|
1284
|
+
respObj.text = combinedText;
|
|
1285
|
+
}
|
|
1286
|
+
if (respObj.response && typeof respObj.response === 'object') {
|
|
1287
|
+
restoreGeminiResponseObject(respObj.response, sessionTokenMap, detok);
|
|
1288
|
+
}
|
|
1289
|
+
return respObj;
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function wrapTargetObject(target) {
|
|
1293
|
+
if (typeof target.generateContent === 'function') {
|
|
1294
|
+
const origGen = target.generateContent.bind(target);
|
|
1295
|
+
target.generateContent = async function(req, ...rest) {
|
|
1296
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1297
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1298
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1299
|
+
const res = await origGen(sanitizedReq, ...rest);
|
|
1300
|
+
return restoreGeminiResponseObject(res, sessionTokenMap);
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof target.generateContentStream === 'function') {
|
|
1304
|
+
const origStream = target.generateContentStream.bind(target);
|
|
1305
|
+
target.generateContentStream = async function(req, ...rest) {
|
|
1306
|
+
const sessionTokenMap = { ...sharedSessionMap };
|
|
1307
|
+
const sanitizedReq = sanitizeGeminiContentNode(req, sessionTokenMap);
|
|
1308
|
+
Object.assign(sharedSessionMap, sessionTokenMap);
|
|
1309
|
+
const streamRes = await origStream(sanitizedReq, ...rest);
|
|
1310
|
+
const detok = new StreamingDetokenizer(sessionTokenMap);
|
|
1311
|
+
async function* wrapAsyncChunks(iterable) {
|
|
1312
|
+
let lastChunk = null;
|
|
1313
|
+
for await (const chunk of iterable) {
|
|
1314
|
+
if (!chunk) continue;
|
|
1315
|
+
lastChunk = chunk;
|
|
1316
|
+
const cloned = { ...chunk };
|
|
1317
|
+
restoreGeminiResponseObject(cloned, sessionTokenMap, detok);
|
|
1318
|
+
yield cloned;
|
|
1319
|
+
}
|
|
1320
|
+
const tail = detok.flush();
|
|
1321
|
+
if (tail && lastChunk) {
|
|
1322
|
+
yield { ...lastChunk, candidates: [{ content: { role: 'model', parts: [{ text: tail }] } }], text: tail };
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
if (streamRes && typeof streamRes[Symbol.asyncIterator] === 'function') {
|
|
1326
|
+
return wrapAsyncChunks(streamRes);
|
|
1327
|
+
}
|
|
1328
|
+
if (streamRes && streamRes.stream && typeof streamRes.stream[Symbol.asyncIterator] === 'function') {
|
|
1329
|
+
return { ...streamRes, stream: wrapAsyncChunks(streamRes.stream) };
|
|
1330
|
+
}
|
|
1331
|
+
return streamRes;
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
if (geminiClientOrModel.models && typeof geminiClientOrModel.models === 'object') {
|
|
1337
|
+
wrapTargetObject(geminiClientOrModel.models);
|
|
1338
|
+
}
|
|
1339
|
+
if (typeof geminiClientOrModel.generateContent === 'function' || typeof geminiClientOrModel.generateContentStream === 'function') {
|
|
1340
|
+
wrapTargetObject(geminiClientOrModel);
|
|
1341
|
+
}
|
|
1342
|
+
geminiClientOrModel._ztdsEngine = engine;
|
|
1343
|
+
return geminiClientOrModel;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
export const wrapGoogleGenAI = wrapGemini;
|
|
1347
|
+
|
|
1348
|
+
export function createPrivacyFetch(options = {}) {
|
|
1349
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1350
|
+
const baseFetch = options.fetch || (typeof globalThis !== 'undefined' && globalThis.fetch);
|
|
1351
|
+
const sanitizeInputs = options.sanitizeInputs !== false;
|
|
1352
|
+
const sanitizeOutputs = options.sanitizeOutputs !== false;
|
|
1353
|
+
|
|
1354
|
+
const PROMPT_KEYS = new Set([
|
|
1355
|
+
'messages', 'prompt', 'input', 'contents', 'system',
|
|
1356
|
+
'systemInstruction', 'system_instruction', 'instructions', 'text', 'content'
|
|
1357
|
+
]);
|
|
1358
|
+
const SAFE_SKIP_KEYS = new Set([
|
|
1359
|
+
'model', 'role', 'type', 'id', 'object', 'finish_reason', 'stop_reason',
|
|
1360
|
+
'encoding_format', 'response_format', 'tool_choice', 'stream'
|
|
1361
|
+
]);
|
|
1362
|
+
|
|
1363
|
+
function sanitizeJsonNode(node, sessionTokenMap) {
|
|
1364
|
+
if (node === null || node === undefined) return node;
|
|
1365
|
+
if (typeof node === 'string') {
|
|
1366
|
+
if (!node.trim()) return node;
|
|
1367
|
+
const res = engine.sanitize(node, { existingSessionMap: sessionTokenMap });
|
|
1368
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1369
|
+
return res.scrubbedText;
|
|
1370
|
+
}
|
|
1371
|
+
if (Array.isArray(node)) {
|
|
1372
|
+
return node.map(item => sanitizeJsonNode(item, sessionTokenMap));
|
|
1373
|
+
}
|
|
1374
|
+
if (typeof node === 'object') {
|
|
1375
|
+
const out = {};
|
|
1376
|
+
for (const [k, v] of Object.entries(node)) {
|
|
1377
|
+
if (k === '__proto__' || k === 'constructor' || k === 'prototype') continue;
|
|
1378
|
+
if (SAFE_SKIP_KEYS.has(k)) {
|
|
1379
|
+
out[k] = v;
|
|
1380
|
+
} else if (PROMPT_KEYS.has(k) || typeof v === 'object') {
|
|
1381
|
+
out[k] = sanitizeJsonNode(v, sessionTokenMap);
|
|
1382
|
+
} else {
|
|
1383
|
+
out[k] = v;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
return node;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const privacyFetch = async function(input, init = {}) {
|
|
1392
|
+
if (typeof baseFetch !== 'function') {
|
|
1393
|
+
throw new Error('[PrivacyScrubber] No global fetch implementation found; pass { fetch } to createPrivacyFetch.');
|
|
1394
|
+
}
|
|
1395
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1396
|
+
let modifiedInit = init ? { ...init } : {};
|
|
1397
|
+
if (sanitizeInputs && modifiedInit && typeof modifiedInit.body === 'string') {
|
|
1398
|
+
const trimmed = modifiedInit.body.trim();
|
|
1399
|
+
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
1400
|
+
try {
|
|
1401
|
+
const parsed = JSON.parse(modifiedInit.body);
|
|
1402
|
+
modifiedInit.body = JSON.stringify(sanitizeJsonNode(parsed, sessionTokenMap));
|
|
1403
|
+
} catch (_) {}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const response = await baseFetch(input, modifiedInit);
|
|
1407
|
+
if (!sanitizeOutputs || !response || Object.keys(sessionTokenMap).length === 0) {
|
|
1408
|
+
return response;
|
|
1409
|
+
}
|
|
1410
|
+
const contentType = (response.headers && typeof response.headers.get === 'function')
|
|
1411
|
+
? (response.headers.get('content-type') || '').toLowerCase()
|
|
1412
|
+
: '';
|
|
1413
|
+
if (contentType.includes('application/json') || typeof response.json === 'function') {
|
|
1414
|
+
try {
|
|
1415
|
+
const rawText = typeof response.text === 'function'
|
|
1416
|
+
? await response.text()
|
|
1417
|
+
: JSON.stringify(await response.json());
|
|
1418
|
+
const restoredObj = restoreObject(JSON.parse(rawText), sessionTokenMap);
|
|
1419
|
+
const restoredText = JSON.stringify(restoredObj);
|
|
1420
|
+
if (typeof Response !== 'undefined') {
|
|
1421
|
+
const headers = new Headers(response.headers || {});
|
|
1422
|
+
headers.delete('content-length');
|
|
1423
|
+
return new Response(restoredText, { status: response.status, statusText: response.statusText, headers });
|
|
1424
|
+
}
|
|
1425
|
+
return { ...response, text: async () => restoredText, json: async () => restoredObj };
|
|
1426
|
+
} catch (_) {
|
|
1427
|
+
return response;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
return response;
|
|
1431
|
+
};
|
|
1432
|
+
privacyFetch.engine = engine;
|
|
1433
|
+
return privacyFetch;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
export function createOpenAIAgentGuardrails(options = {}) {
|
|
1437
|
+
const engine = options.engine || new PrivacyScrubberEngine(options);
|
|
1438
|
+
const tripwireOnCritical = options.tripwireOnCritical === true;
|
|
1439
|
+
const tripwireOnAnyPii = options.tripwireOnAnyPii === true;
|
|
1440
|
+
|
|
1441
|
+
const inputGuardrail = {
|
|
1442
|
+
name: options.inputGuardrailName || 'PrivacyScrubber ZTDS Input Guardrail',
|
|
1443
|
+
async execute({ input, context } = {}) {
|
|
1444
|
+
const sessionTokenMap = { ...engine.sessionMap };
|
|
1445
|
+
let scrubbedInput = input;
|
|
1446
|
+
let count = 0;
|
|
1447
|
+
if (typeof input === 'string') {
|
|
1448
|
+
const res = engine.sanitize(input, { existingSessionMap: sessionTokenMap });
|
|
1449
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
1450
|
+
scrubbedInput = res.scrubbedText;
|
|
1451
|
+
count = res.count;
|
|
1452
|
+
}
|
|
1453
|
+
Object.assign(engine.sessionMap, sessionTokenMap);
|
|
1454
|
+
if (context && typeof context === 'object') {
|
|
1455
|
+
context.ztdsTokenMap = engine.sessionMap;
|
|
1456
|
+
context.ztdsScrubbedInput = scrubbedInput;
|
|
1457
|
+
}
|
|
1458
|
+
const telemetry = buildAuditTelemetry(sessionTokenMap);
|
|
1459
|
+
const isCritical = telemetry.riskLevel && telemetry.riskLevel.includes('CRITICAL');
|
|
1460
|
+
const tripwireTriggered = Boolean((tripwireOnAnyPii && count > 0) || (tripwireOnCritical && count > 0 && isCritical));
|
|
1461
|
+
return {
|
|
1462
|
+
tripwireTriggered,
|
|
1463
|
+
outputInfo: { scrubbedInput, tokenMap: { ...sessionTokenMap }, tokenCount: count, riskLevel: telemetry.riskLevel, telemetry }
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
const outputGuardrail = {
|
|
1469
|
+
name: options.outputGuardrailName || 'PrivacyScrubber ZTDS Output Guardrail',
|
|
1470
|
+
async execute({ agentOutput, context } = {}) {
|
|
1471
|
+
const activeMap = (context && context.ztdsTokenMap) || engine.sessionMap || {};
|
|
1472
|
+
let restoredOutput = agentOutput;
|
|
1473
|
+
let restoredCount = 0;
|
|
1474
|
+
if (typeof agentOutput === 'string') {
|
|
1475
|
+
const res = engine.restore(agentOutput, activeMap);
|
|
1476
|
+
restoredOutput = res.restoredText;
|
|
1477
|
+
restoredCount = res.restoredCount;
|
|
1478
|
+
} else if (agentOutput && typeof agentOutput === 'object') {
|
|
1479
|
+
restoredOutput = restoreObject(agentOutput, activeMap);
|
|
1480
|
+
if (options.mutateInPlace !== false) Object.assign(agentOutput, restoredOutput);
|
|
1481
|
+
}
|
|
1482
|
+
return { tripwireTriggered: false, outputInfo: { restoredOutput, restoredCount } };
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
return {
|
|
1487
|
+
inputGuardrail,
|
|
1488
|
+
outputGuardrail,
|
|
1489
|
+
inputGuardrails: [inputGuardrail],
|
|
1490
|
+
outputGuardrails: [outputGuardrail],
|
|
1491
|
+
engine
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1119
1495
|
// Vision & Multimodal exports
|
|
1120
1496
|
export {
|
|
1121
1497
|
stripExifMetadata,
|
|
@@ -1153,6 +1529,10 @@ export default {
|
|
|
1153
1529
|
createStreamTokenRestorer,
|
|
1154
1530
|
buildStreamReplacer,
|
|
1155
1531
|
wrapAiStream,
|
|
1532
|
+
wrapGemini,
|
|
1533
|
+
wrapGoogleGenAI,
|
|
1534
|
+
createPrivacyFetch,
|
|
1535
|
+
createOpenAIAgentGuardrails,
|
|
1156
1536
|
scrubText,
|
|
1157
1537
|
unscrubText,
|
|
1158
1538
|
DEVOPS_SECRETS_DETECTOR,
|