@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 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
- this.upgradeUrl = 'https://privacyscrubber.com/pricing?utm_source=npm_sdk&utm_medium=sdk_error&utm_campaign=dev_upsell';
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 & Data Minimization',
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 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&src=dev_limit`;
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 activeRules = [...customRules];
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: 'https://privacyscrubber.com/pricing?tier=sdk&src=seat_tier_mismatch',
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: 'https://privacyscrubber.com/pricing?tier=sdk&src=dev_upsell',
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
- return val.map(item => traverseAndSanitize(item));
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 Date || val instanceof RegExp) {
469
- return val;
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 (typeof val === 'object') {
472
- const out = {};
473
- for (const [k, v] of Object.entries(val)) {
474
- out[k] = traverseAndSanitize(v);
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 out;
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 val;
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
- return obj.map(item => restoreObject(item, tokenMap));
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 (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);
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 out;
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 obj;
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: options.persistSession !== false ? this.sessionMap : (options.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,