@privacyscrubber/sdk 2.4.4 → 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 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 activeRules = [...customRules];
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
- return val.map(item => traverseAndSanitize(item));
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 Date || val instanceof RegExp) {
469
- return val;
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 (typeof val === 'object') {
472
- const out = {};
473
- for (const [k, v] of Object.entries(val)) {
474
- out[k] = traverseAndSanitize(v);
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 out;
513
+ return outSet;
477
514
  }
478
- return val;
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
- return obj.map(item => restoreObject(item, tokenMap));
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 (typeof obj === 'object' && !(obj instanceof Date) && !(obj instanceof RegExp)) {
511
- const out = {};
512
- for (const [k, v] of Object.entries(obj)) {
513
- out[k] = restoreObject(v, tokenMap);
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 out;
583
+ return outMap;
516
584
  }
517
- return obj;
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: options.persistSession !== false ? this.sessionMap : (options.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,