@privacyscrubber/sdk 2.3.9 → 2.4.0

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.
Files changed (45) hide show
  1. package/README.md +85 -27
  2. package/benchmark.js +347 -0
  3. package/browser.js +2 -2
  4. package/cli.js +1 -1
  5. package/examples/python-bridge/MANIFEST.in +5 -0
  6. package/examples/python-bridge/README.md +265 -0
  7. package/examples/python-bridge/bridge_daemon.cjs +241 -0
  8. package/examples/python-bridge/dist/privacyscrubber-2.3.9-py3-none-any.whl +0 -0
  9. package/examples/python-bridge/dist/privacyscrubber-2.3.9.tar.gz +0 -0
  10. package/examples/python-bridge/fastapi_example.py +206 -0
  11. package/examples/python-bridge/langchain_adapter.py +20 -0
  12. package/examples/python-bridge/langchain_example.py +126 -0
  13. package/examples/python-bridge/llamaindex_example.py +103 -0
  14. package/examples/python-bridge/privacyscrubber/__init__.py +42 -0
  15. package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +241 -0
  16. package/examples/python-bridge/privacyscrubber/cli.py +138 -0
  17. package/examples/python-bridge/privacyscrubber/client.py +542 -0
  18. package/examples/python-bridge/privacyscrubber/exceptions.py +26 -0
  19. package/examples/python-bridge/privacyscrubber/langchain.py +161 -0
  20. package/examples/python-bridge/privacyscrubber/types.py +52 -0
  21. package/examples/python-bridge/privacyscrubber.egg-info/PKG-INFO +311 -0
  22. package/examples/python-bridge/privacyscrubber.egg-info/SOURCES.txt +18 -0
  23. package/examples/python-bridge/privacyscrubber.egg-info/dependency_links.txt +1 -0
  24. package/examples/python-bridge/privacyscrubber.egg-info/entry_points.txt +3 -0
  25. package/examples/python-bridge/privacyscrubber.egg-info/requires.txt +18 -0
  26. package/examples/python-bridge/privacyscrubber.egg-info/top_level.txt +1 -0
  27. package/examples/python-bridge/privacyscrubber.py +36 -0
  28. package/examples/python-bridge/pyproject.toml +62 -0
  29. package/examples/python-bridge/setup.py +66 -0
  30. package/examples/python-bridge/test_python_bridge.py +320 -0
  31. package/examples/vercel-ai-stream.js +195 -0
  32. package/fastmcp.cjs +271 -1
  33. package/fastmcp.d.ts +18 -0
  34. package/fastmcp.js +4 -0
  35. package/index.cjs +89 -36
  36. package/index.js +89 -36
  37. package/package.json +15 -3
  38. package/ps-license-manager.js +1 -1
  39. package/ps-pii-engine.cjs +1 -1
  40. package/scrubber-core.cjs +1 -1
  41. package/vercel-ai.cjs +400 -0
  42. package/vercel-ai.d.ts +156 -0
  43. package/vercel-ai.js +36 -0
  44. package/vision.cjs +13 -5
  45. package/vision.js +13 -5
package/fastmcp.cjs CHANGED
@@ -26,6 +26,204 @@ const ZTDS_PATTERNS = {
26
26
 
27
27
  const TOKEN_PATTERN = /\[([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)\]/g;
28
28
 
29
+ function escapeRegex(s) {
30
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
31
+ }
32
+
33
+ function buildStreamReplacer(tokenMap = {}, options = {}) {
34
+ if (!tokenMap || typeof tokenMap !== 'object' || Object.keys(tokenMap).length === 0) {
35
+ return (text) => text || '';
36
+ }
37
+
38
+ const lookup = new Map();
39
+ const patterns = [];
40
+ const sortedKeys = Object.keys(tokenMap).sort((a, b) => b.length - a.length);
41
+
42
+ for (const k of sortedKeys) {
43
+ const val = tokenMap[k];
44
+ const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
45
+ const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
46
+
47
+ if (match) {
48
+ const label = escapeRegex(match[1]);
49
+ const idx = match[2];
50
+ const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
51
+ patterns.push(patternStr);
52
+ const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
53
+ lookup.set(normKey, val);
54
+ lookup.set(k, val);
55
+ lookup.set(k.toUpperCase(), val);
56
+ } else {
57
+ const safe = escapeRegex(k);
58
+ patterns.push(safe);
59
+ lookup.set(k, val);
60
+ lookup.set(k.toUpperCase(), val);
61
+ }
62
+ }
63
+
64
+ const unifiedRegex = new RegExp('(?:' + patterns.join('|') + ')(\'s|’s|s)?', 'gi');
65
+
66
+ return function replaceTokens(text) {
67
+ if (!text) return '';
68
+ unifiedRegex.lastIndex = 0;
69
+ return text.replace(unifiedRegex, (match, suffix) => {
70
+ const isPossessive = suffix && /('s|’s)$/i.test(suffix);
71
+ const coreMatch = suffix ? match.slice(0, match.length - suffix.length) : match;
72
+ let restored = null;
73
+
74
+ if (lookup.has(coreMatch)) {
75
+ restored = lookup.get(coreMatch);
76
+ } else if (lookup.has(coreMatch.toUpperCase())) {
77
+ restored = lookup.get(coreMatch.toUpperCase());
78
+ } else {
79
+ const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
80
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
81
+ if (m) {
82
+ const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
83
+ if (lookup.has(normKey)) {
84
+ restored = lookup.get(normKey);
85
+ }
86
+ }
87
+ }
88
+
89
+ if (restored !== null) {
90
+ return restored + (isPossessive ? "'s" : '');
91
+ }
92
+
93
+ if (lookup.has(match)) {
94
+ return lookup.get(match);
95
+ }
96
+
97
+ return match;
98
+ });
99
+ };
100
+ }
101
+
102
+ class StreamingDetokenizer {
103
+ constructor(tokenMap = {}, options = {}) {
104
+ this.tokenMap = tokenMap && typeof tokenMap === 'object' ? tokenMap : {};
105
+ this.options = options || {};
106
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
107
+ this.pendingBuffer = '';
108
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
109
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
110
+ this.decoder = new TextDecoder('utf-8', { fatal: false });
111
+ }
112
+
113
+ get buffer() {
114
+ return this.pendingBuffer;
115
+ }
116
+
117
+ set buffer(val) {
118
+ this.pendingBuffer = typeof val === 'string' ? val : '';
119
+ }
120
+
121
+ updateTokenMap(newMap) {
122
+ if (newMap && typeof newMap === 'object') {
123
+ this.tokenMap = newMap;
124
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
125
+ this.hasTokens = Object.keys(this.tokenMap).length > 0;
126
+ this._lastTokenCount = Object.keys(this.tokenMap).length;
127
+ }
128
+ }
129
+
130
+ reset() {
131
+ this.pendingBuffer = '';
132
+ }
133
+
134
+ feed(chunk) {
135
+ if (chunk === undefined || chunk === null) return '';
136
+ let text = '';
137
+ if (typeof chunk === 'string') {
138
+ text = chunk;
139
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
140
+ text = this.decoder.decode(chunk, { stream: true });
141
+ } else {
142
+ text = String(chunk);
143
+ }
144
+
145
+ if (!text) return '';
146
+
147
+ const currentTokenCount = Object.keys(this.tokenMap).length;
148
+ if (currentTokenCount !== this._lastTokenCount) {
149
+ this.replacer = buildStreamReplacer(this.tokenMap, this.options);
150
+ this.hasTokens = currentTokenCount > 0;
151
+ this._lastTokenCount = currentTokenCount;
152
+ }
153
+
154
+ if (!this.hasTokens) return text;
155
+
156
+ this.pendingBuffer += text;
157
+
158
+ const tailLen = 160;
159
+ const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
160
+ const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
161
+ const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
162
+
163
+ if (m) {
164
+ const candidate = m[0];
165
+ const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
166
+ if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
167
+ const toEmit = this.pendingBuffer;
168
+ this.pendingBuffer = '';
169
+ return this.replacer(toEmit);
170
+ }
171
+ const splitIdx = offset + m.index;
172
+ const safeToEmit = this.pendingBuffer.slice(0, splitIdx);
173
+ this.pendingBuffer = this.pendingBuffer.slice(splitIdx);
174
+ return safeToEmit ? this.replacer(safeToEmit) : '';
175
+ }
176
+
177
+ const toEmit = this.pendingBuffer;
178
+ this.pendingBuffer = '';
179
+ return this.replacer(toEmit);
180
+ }
181
+
182
+ process(chunk) {
183
+ return this.feed(chunk);
184
+ }
185
+
186
+ flush() {
187
+ let text = '';
188
+ try {
189
+ text = this.decoder.decode(new Uint8Array(), { stream: false });
190
+ } catch (_) {}
191
+
192
+ if (text) {
193
+ this.pendingBuffer += text;
194
+ }
195
+
196
+ if (!this.pendingBuffer) return '';
197
+ const remaining = this.pendingBuffer;
198
+ this.pendingBuffer = '';
199
+ return this.replacer(remaining);
200
+ }
201
+
202
+ async* wrapStream(asyncIterable) {
203
+ for await (const chunk of asyncIterable) {
204
+ if (chunk === null || chunk === undefined) continue;
205
+ let text = '';
206
+ if (typeof chunk === 'string') {
207
+ text = chunk;
208
+ } else if (typeof chunk?.text === 'string') {
209
+ text = chunk.text;
210
+ } else if (typeof chunk?.content === 'string') {
211
+ text = chunk.content;
212
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(chunk))) {
213
+ text = chunk;
214
+ } else {
215
+ text = String(chunk);
216
+ }
217
+
218
+ const emitted = this.feed(text);
219
+ if (emitted) yield emitted;
220
+ }
221
+
222
+ const finalChunk = this.flush();
223
+ if (finalChunk) yield finalChunk;
224
+ }
225
+ }
226
+
29
227
  class ZTDSFastMCPGuard {
30
228
  constructor(options = {}) {
31
229
  this.enabledEntities = options.enabledEntities || Object.keys(ZTDS_PATTERNS);
@@ -36,6 +234,7 @@ class ZTDSFastMCPGuard {
36
234
  this.maxSessionTokens = options.maxSessionTokens || 5000;
37
235
  this._sessionMaps = new Map();
38
236
  this._entityMaps = new Map();
237
+ this._streamDetokenizers = new Map();
39
238
  }
40
239
 
41
240
  _generateToken(entityType, tokenMap) {
@@ -157,6 +356,65 @@ class ZTDSFastMCPGuard {
157
356
  return val;
158
357
  }
159
358
 
359
+ getStreamDetokenizer(sessionId) {
360
+ if (!this._streamDetokenizers.has(sessionId)) {
361
+ const mapProxy = this._sessionMaps.get(sessionId) || new Map();
362
+ const objMap = {};
363
+ for (const [k, v] of mapProxy.entries()) {
364
+ objMap[k] = v;
365
+ }
366
+ this._streamDetokenizers.set(sessionId, new StreamingDetokenizer(objMap));
367
+ } else {
368
+ const mapProxy = this._sessionMaps.get(sessionId);
369
+ if (mapProxy) {
370
+ const objMap = {};
371
+ for (const [k, v] of mapProxy.entries()) {
372
+ objMap[k] = v;
373
+ }
374
+ this._streamDetokenizers.get(sessionId).updateTokenMap(objMap);
375
+ }
376
+ }
377
+ return this._streamDetokenizers.get(sessionId);
378
+ }
379
+
380
+ feedStream(chunk, sessionId) {
381
+ return this.getStreamDetokenizer(sessionId).feed(chunk);
382
+ }
383
+
384
+ flushStream(sessionId) {
385
+ const detok = this._streamDetokenizers.get(sessionId);
386
+ return detok ? detok.flush() : "";
387
+ }
388
+
389
+ async *restoreStream(asyncIterable, sessionId) {
390
+ const detok = this.getStreamDetokenizer(sessionId);
391
+ try {
392
+ for await (const chunk of asyncIterable) {
393
+ if (chunk === null || chunk === undefined) continue;
394
+ let text = "";
395
+ if (typeof chunk === "string") {
396
+ text = chunk;
397
+ } else if (typeof chunk?.text === "string") {
398
+ text = chunk.text;
399
+ } else if (typeof chunk?.content === "string") {
400
+ text = chunk.content;
401
+ } else if (chunk instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(chunk))) {
402
+ text = chunk;
403
+ } else {
404
+ text = String(chunk);
405
+ }
406
+ const emitted = detok.feed(text);
407
+ if (emitted) yield emitted;
408
+ }
409
+ const finalChunk = detok.flush();
410
+ if (finalChunk) yield finalChunk;
411
+ } finally {
412
+ if (this.autoZeroize) {
413
+ this.zeroizeSession(sessionId);
414
+ }
415
+ }
416
+ }
417
+
160
418
  wrapTool(toolName, handler) {
161
419
  return async (args, context = {}) => {
162
420
  const sessionId =
@@ -169,6 +427,7 @@ class ZTDSFastMCPGuard {
169
427
  unmask: (val) => this.restoreObject(val, sessionId),
170
428
  };
171
429
 
430
+ let isStreaming = false;
172
431
  try {
173
432
  let processedArgs = args;
174
433
  if (this.sanitizeInputs && args !== undefined && args !== null) {
@@ -177,6 +436,11 @@ class ZTDSFastMCPGuard {
177
436
 
178
437
  const rawResult = await handler(processedArgs, enrichedContext);
179
438
 
439
+ if (rawResult && typeof rawResult[Symbol.asyncIterator] === "function") {
440
+ isStreaming = true;
441
+ return this.restoreStream(rawResult, sessionId);
442
+ }
443
+
180
444
  let finalResult = rawResult;
181
445
  if (this.sanitizeOutputs && rawResult !== undefined && rawResult !== null) {
182
446
  if (typeof rawResult === "string") {
@@ -234,7 +498,7 @@ class ZTDSFastMCPGuard {
234
498
  _ztds: ztdsMeta,
235
499
  };
236
500
  } finally {
237
- if (this.autoZeroize) {
501
+ if (!isStreaming && this.autoZeroize) {
238
502
  this.zeroizeSession(sessionId);
239
503
  }
240
504
  }
@@ -250,6 +514,10 @@ class ZTDSFastMCPGuard {
250
514
  this._entityMaps.get(sessionId).clear();
251
515
  this._entityMaps.delete(sessionId);
252
516
  }
517
+ if (this._streamDetokenizers.has(sessionId)) {
518
+ this._streamDetokenizers.get(sessionId).reset();
519
+ this._streamDetokenizers.delete(sessionId);
520
+ }
253
521
  }
254
522
  }
255
523
 
@@ -285,6 +553,8 @@ module.exports = {
285
553
  ZTDSFastMCPGuard,
286
554
  ZTDS_PATTERNS,
287
555
  TOKEN_PATTERN,
556
+ StreamingDetokenizer,
557
+ createStreamTokenRestorer: (tokenMap, options) => new StreamingDetokenizer(tokenMap, options),
288
558
  createZTDSGuard,
289
559
  ztdsGuardTool,
290
560
  wrapExecute,
package/fastmcp.d.ts CHANGED
@@ -26,6 +26,19 @@ export interface ZTDSContext {
26
26
  export declare const ZTDS_PATTERNS: Record<string, RegExp>;
27
27
  export declare const TOKEN_PATTERN: RegExp;
28
28
 
29
+ export declare class StreamingDetokenizer {
30
+ constructor(tokenMap?: Record<string, string>, options?: object);
31
+ buffer: string;
32
+ feed(chunk: string | Uint8Array | Buffer): string;
33
+ process(chunk: string | Uint8Array | Buffer): string;
34
+ flush(): string;
35
+ reset(): void;
36
+ updateTokenMap(newMap: Record<string, string>): void;
37
+ wrapStream<T = any>(asyncIterable: AsyncIterable<T>): AsyncGenerator<T>;
38
+ }
39
+
40
+ export declare function createStreamTokenRestorer(tokenMap: Record<string, string>, options?: object): StreamingDetokenizer;
41
+
29
42
  export declare class ZTDSFastMCPGuard {
30
43
  constructor(options?: ZTDSGuardOptions);
31
44
  enabledEntities: string[];
@@ -39,6 +52,11 @@ export declare class ZTDSFastMCPGuard {
39
52
  sanitizeObject(val: unknown, sessionId: string): unknown;
40
53
  restoreObject(val: unknown, sessionId: string): unknown;
41
54
 
55
+ getStreamDetokenizer(sessionId: string): StreamingDetokenizer;
56
+ feedStream(chunk: string, sessionId: string): string;
57
+ flushStream(sessionId: string): string;
58
+ restoreStream<T = any>(asyncIterable: AsyncIterable<T>, sessionId: string): AsyncGenerator<T>;
59
+
42
60
  wrapTool<TArgs = unknown, TResult = unknown>(
43
61
  toolName: string,
44
62
  handler: (args: TArgs, context: ZTDSContext) => Promise<TResult>
package/fastmcp.js CHANGED
@@ -11,6 +11,8 @@ const fastmcpGuard = require('./fastmcp.cjs');
11
11
  export const ZTDSFastMCPGuard = fastmcpGuard.ZTDSFastMCPGuard;
12
12
  export const ZTDS_PATTERNS = fastmcpGuard.ZTDS_PATTERNS;
13
13
  export const TOKEN_PATTERN = fastmcpGuard.TOKEN_PATTERN;
14
+ export const StreamingDetokenizer = fastmcpGuard.StreamingDetokenizer;
15
+ export const createStreamTokenRestorer = fastmcpGuard.createStreamTokenRestorer;
14
16
  export const createZTDSGuard = fastmcpGuard.createZTDSGuard;
15
17
  export const ztdsGuardTool = fastmcpGuard.ztdsGuardTool;
16
18
  export const wrapExecute = fastmcpGuard.wrapExecute;
@@ -20,6 +22,8 @@ export default {
20
22
  ZTDSFastMCPGuard,
21
23
  ZTDS_PATTERNS,
22
24
  TOKEN_PATTERN,
25
+ StreamingDetokenizer,
26
+ createStreamTokenRestorer,
23
27
  createZTDSGuard,
24
28
  ztdsGuardTool,
25
29
  wrapExecute,
package/index.cjs CHANGED
@@ -36,16 +36,33 @@ if (scrubberPkg && typeof scrubberPkg.init === 'function') {
36
36
  * Built-in patterns for DevOps & Cloud Secrets detection.
37
37
  */
38
38
  const DEVOPS_SECRETS_DETECTOR = [
39
- { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{16}\b/g, label: 'AWS_KEY' },
39
+ { name: 'AWS Credentials', regex: /\b(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA)[A-Z0-9]{12,28}\b/g, label: 'AWS_KEY' },
40
+ { name: 'Google Cloud / Gemini API Key', regex: /\bAIza[0-9A-Za-z_-]{35}\b/g, label: 'API_TOKEN' },
41
+ { name: 'Anthropic API Key', regex: /\bsk-ant-(?:api\d{2}|admin\d{2}|sid\d{2})-[a-zA-Z0-9_-]{16,128}\b/g, label: 'API_TOKEN' },
42
+ { name: 'HuggingFace / Perplexity API Key', regex: /\b(?:hf_[a-zA-Z0-9]{20,64}|pplx-[a-zA-Z0-9]{24,64})\b/g, label: 'API_TOKEN' },
43
+ { name: 'SendGrid API Key', regex: /\bSG\.[a-zA-Z0-9_-]{16,32}\.[a-zA-Z0-9_-]{32,64}\b/g, label: 'API_TOKEN' },
44
+ { name: 'Postman API Key', regex: /\bPMAK-[a-fA-F0-9]{24}-[a-fA-F0-9]{34}\b/g, label: 'API_TOKEN' },
45
+ { name: 'GitHub Fine-Grained PAT', regex: /\bgithub_pat_[a-zA-Z0-9]{20,30}_[a-zA-Z0-9]{40,80}\b/g, label: 'API_TOKEN' },
46
+ { name: 'Slack Enterprise / Refresh Token', regex: /\bxoxe\.xox[baprs]-\d-[a-zA-Z0-9-]{10,}\b|\bxox[eapbrs]-[a-zA-Z0-9-]{10,}\b/g, label: 'API_TOKEN' },
47
+ { name: 'Telegram Bot API Token', regex: /\b\d{8,10}:AA[A-Za-z0-9_-]{33}\b/g, label: 'API_TOKEN' },
48
+ { name: 'Age Encryption Secret Key', regex: /\bAGE-SECRET-KEY-1[A-Z0-9]{58}\b/g, label: 'SECRET' },
49
+ { name: 'Modern Cloud & AI Platform Key', regex: /\b(?:sbp_[a-fA-F0-9]{40}|dop_v1_[a-fA-F0-9]{64}|doo_v1_[a-fA-F0-9]{64}|dp\.(?:st|pt|ct|sa)\.[a-zA-Z0-9_-]{20,}|lin_api_[a-zA-Z0-9]{32,64}|ntn_[a-zA-Z0-9]{32,64}|secret_[a-zA-Z0-9]{32,64}|gsk_[a-zA-Z0-9]{32,64}|r8_[a-zA-Z0-9]{24,64}|tvly-[a-zA-Z0-9_-]{20,64}|fc-[a-fA-F0-9]{32}|dapi[a-fA-F0-9]{32}(?:-\d)?|re_[a-zA-Z0-9_]{20,64}|napi_[a-zA-Z0-9]{32,64}|[a-zA-Z0-9]{14}\.atlasv1\.[a-zA-Z0-9]{50,80}|pypi-[A-Za-z0-9_-]{16,128}|whsec_[A-Za-z0-9+/=_-]{24,128}|shpat_[a-fA-F0-9]{32}|shpss_[a-fA-F0-9]{32}|shpca_[a-fA-F0-9]{32}|shppa_[a-fA-F0-9]{32}|hvs\.[A-Za-z0-9_-]{24,128}|hvb\.[A-Za-z0-9_-]{24,128}|glsa_[A-Za-z0-9]{32}_[A-Fa-f0-9]{8}|glc_[A-Za-z0-9+/=_-]{20,128}|pscale_tkn_[A-Za-z0-9_.-]{20,64}|pscale_pw_[A-Za-z0-9_.-]{20,64}|pat[A-Za-z0-9]{14}\.[a-fA-F0-9]{64}|pat-(?:na1|eu1)-[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}|sq0atp-[A-Za-z0-9_-]{22,64}|sq0csp-[A-Za-z0-9_-]{32,64}|NRAK-[A-Z0-9]{27}|v1\.0-[a-fA-F0-9]{24}-[a-fA-F0-9]{16,160}|ATATT3xFfGF0[A-Za-z0-9+/=_-]{24,128}|tskey-(?:auth|api|client)-[A-Za-z0-9_-]{20,96}|sntry[su]_[A-Za-z0-9+/=_-]{32,160}|(?:pk|sk)\.eyJ1[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{16,}|key-[a-f0-9]{32}|[a-f0-9]{32}-us\d{1,2})\b/g, label: 'API_TOKEN' },
50
+ { name: 'Twilio Account SID / API Key', regex: /\b(?:AC|SK)[a-fA-F0-9]{32}\b/g, label: 'API_TOKEN' },
51
+ { name: 'Bcrypt / Argon2 Password Hash', regex: /(?<![A-Za-z0-9])(?:\$2[aby]\$\d{2}\$[./A-Za-z0-9=]{20,60}|\$argon2(?:id|i|d)\$[^\s"']{15,120})/g, label: 'SECRET' },
40
52
  { name: 'JSON Web Token (JWT)', regex: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/g, label: 'JWT_TOKEN' },
41
- { name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_]{10,}\b/g, label: 'API_TOKEN' },
42
- { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{24,}\b/g, label: 'STRIPE_KEY' },
53
+ { name: 'API Token/Key (GitHub/Slack/NPM)', regex: /\b(?:ghp|gho|ghu|ghs|ghr|glpat|npm|xox[baprs])[-_][A-Za-z0-9_-]{10,}\b/g, label: 'API_TOKEN' },
54
+ { name: 'Stripe API Key', regex: /\b(?:[rs]k)_(?:test|live)_[a-zA-Z0-9]{10,}\b/g, label: 'STRIPE_KEY' },
43
55
  { name: 'OpenAI Project API Key', regex: /\b(?:sk|pk)-(?:proj-)?[a-zA-Z0-9_-]{16,}\b/gi, label: 'OPENAI_KEY' },
56
+ { name: 'Slack / Discord Incoming Webhook URL', regex: /\bhttps:\/\/(?:hooks\.slack\.com\/services\/T[A-Z0-9_]+\/B[A-Z0-9_]+\/[A-Za-z0-9_-]+|(?:ptb\.|canary\.)?discord(?:app)?\.com\/api\/webhooks\/\d{17,20}\/[A-Za-z0-9_-]{60,70})\b/gi, label: 'SECRET' },
57
+ { name: 'Azure Connection String / SAS Token', regex: /(?:DefaultEndpointsProtocol=https?;AccountName=[^;\s"']+;AccountKey=[A-Za-z0-9+/=]{20,}(?:;EndpointSuffix=[^;\s"']+)?|Endpoint=sb:\/\/[^;\s"']+;SharedAccessKeyName=[^;\s"']+;SharedAccessKey=[A-Za-z0-9+/=]{16,}|\bSharedAccessSignature=sr=[^\s"']+&sig=[A-Za-z0-9%/+=]{16,}|\bAccountKey=[A-Za-z0-9+/=]{40,88}==)/gi, label: 'SECRET' },
44
58
  { name: 'Database Connection URI', regex: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|mssql):\/\/[^\s"']+/gi, label: 'DB_URI' },
45
- { name: 'Embedded Credentials URI', regex: /\b(?:https?|s?ftp):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
59
+ { name: 'Embedded Credentials URI', regex: /\b(?:https?|hxxps?|s?ftp|fxp|wss?):\/\/[a-zA-Z0-9_.%+-]+:[^\s/@:"']+@[a-zA-Z0-9_.-]+(?:\[\.\][a-zA-Z0-9_.-]+|\(\.\)[a-zA-Z0-9_.-]+)*(?::\d+)?(?:\/[^\s"']*)?/gi, label: 'SECRET' },
46
60
  { name: 'URL Query Parameter Secret', regex: /(?<=[?&](?:api_key|apikey|access_token|client_secret|auth_token|token|secret|password|private_key)=)[^\s"'\x26#)>]{8,}/gi, label: 'SECRET' },
61
+ { name: 'CLI Credential Flag / Curl User Auth', regex: /(?:\bcurl\b[^\r\n]*?\s(?:-u|--user)\s+["']?[A-Za-z0-9_.+-]+:[^\s"']+|\b(?:--password|--api-key|--apikey|--access-token|--auth-token|--client-secret|--secret-key|--private-key)(?:=|\s+)["']?[^\s"']{6,}["']?)/gi, label: 'SECRET' },
62
+ { name: 'HTTP API Key Header', regex: /\b(?:X-API-Key|X-Amz-Security-Token|X-Functions-Key|X-Shopify-Access-Token|X-Auth-Token|Api-Key)\s*:\s*["']?[A-Za-z0-9_.\-~+/=]{12,}["']?/gi, label: 'SECRET' },
63
+ { name: 'Docker Registry Auth / K8s Secret Base64', regex: /(?<=["']?(?:auth|identitytoken|registrytoken|\.dockerconfigjson)["']?\s*:\s*["'])[A-Za-z0-9+/=]{16,}(?=["'])/gi, label: 'SECRET' },
47
64
  { name: 'Bearer / Auth Header Token', regex: /(?<=\b(?:Bearer|Token)\s+)[a-zA-Z0-9_\-\.~+/=]{16,}\b/gi, label: 'SECRET' },
48
- { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}\b/gi, label: 'SECRET' },
65
+ { name: 'Database/API Secret', regex: /\b(DB|POSTGRES|REDIS|MYSQL|AWS|SECRET|PASSWORD|TOKEN|API|KEY)[A-Z0-9_]*\s*[:=]\s*[^ \t\r\n"']{8,}/gi, label: 'SECRET' },
49
66
  { name: 'Private Cryptographic Key (PEM Block)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----[\s\S]{10,8192}?-----END (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' },
50
67
  { name: 'Private Cryptographic Key (Header)', regex: /-----BEGIN (?:RSA |EC |PGP |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY(?: BLOCK)?-----/g, label: 'SECRET' }
51
68
  ];
@@ -141,8 +158,8 @@ function buildAuditTelemetry(currentTokenMap = {}) {
141
158
  frameworksSet.add('CCPA/CPRA');
142
159
  }
143
160
  if (types.includes('ID') || types.includes('SSN') || types.includes('PASSPORT') || types.includes('NATIONAL_ID')) {
144
- frameworksSet.add('SOC 2 Type II');
145
- frameworksSet.add('ISO 27001 (A.8.11)');
161
+ frameworksSet.add('SOC 2 controls');
162
+ frameworksSet.add('ISO 27001 A.8.11 controls');
146
163
  }
147
164
  if (types.some(t => ['CREDIT_CARD', 'BANK', 'IBAN', 'FINANCIAL', 'CARD', 'STRIPE_KEY', 'TAX_ID', 'EIN', 'W2', '1099', 'SALARY', 'WAGE'].includes(t))) {
148
165
  frameworksSet.add('PCI DSS v4.0');
@@ -1466,13 +1483,13 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
1466
1483
 
1467
1484
  for (const k of sortedKeys) {
1468
1485
  const val = tokenMap[k];
1469
- const inner = k.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '');
1486
+ const inner = k.replace(/^\[|<|\{\{?|__|\]|>|\}\}?|__/g, '');
1470
1487
  const match = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
1471
1488
 
1472
1489
  if (match) {
1473
- const label = escapeRegex(match[1]);
1490
+ const label = escapeRegex(match[1]).replace(/[_-]/g, '\\\\?[_-]');
1474
1491
  const idx = match[2];
1475
- const patternStr = '(?:\\[\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\]|<\\s*' + label + '[-_\\s]*0*' + idx + '\\s*>|\\{\\{\\s*' + label + '[-_\\s]*0*' + idx + '\\s*\\}\\}|__\\s*' + label + '[-_\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
1492
+ const patternStr = '(?:(?:\\[|\\\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『)\\s*[*`~]*' + label + '[-_\\\\\\s]*0*' + idx + '[*`~]*[.,;:!?]?\\s*(?:\\]|\\\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』)|<\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*>|\\{\\{?\\s*' + label + '[-_\\\\\\s]*0*' + idx + '[.,;:!?]?\\s*\\}\\}?|__\\s*' + label + '[-_\\\\\\s]*0*' + idx + '\\s*__|\\b' + label + '[-_\\s]+0*' + idx + '\\b)';
1476
1493
  patterns.push(patternStr);
1477
1494
  const normKey = (match[1] + '_' + parseInt(idx, 10)).toUpperCase();
1478
1495
  lookup.set(normKey, val);
@@ -1501,10 +1518,14 @@ function buildStreamReplacer(tokenMap = {}, options = {}) {
1501
1518
  } else if (lookup.has(coreMatch.toUpperCase())) {
1502
1519
  restored = lookup.get(coreMatch.toUpperCase());
1503
1520
  } else {
1504
- const inner = coreMatch.replace(/^\[|<|\{\{|__|\]|>|\}\}|__/g, '').trim();
1505
- const m = inner.match(/^([A-Za-z_0-9]+?)[-_ ]?(\d+)$/);
1521
+ const inner = coreMatch
1522
+ .replace(/^(?:\[|\\u005[Bb]|%5[Bb]|&(?:amp;)?#0*91;|&(?:amp;)?#[xX]0*5[Bb];|&(?:amp;)?lbrack;|【|[|〔|「|『|<|\{\{?|__)|(?:\]|\\u005[Dd]|%5[Dd]|&(?:amp;)?#0*93;|&(?:amp;)?#[xX]0*5[Dd];|&(?:amp;)?rbrack;|】|]|〕|」|』|>|\}\}?|__)$/gi, '')
1523
+ .replace(/^[*`~\s]+|[.,;:!?*`~\s]+$/g, '')
1524
+ .trim();
1525
+ const m = inner.match(/^([A-Za-z_0-9]+?)[-_\\\s]*(\d+)$/);
1506
1526
  if (m) {
1507
- const normKey = (m[1] + '_' + parseInt(m[2], 10)).toUpperCase();
1527
+ const cleanLabel = m[1].replace(/\\([_-])/g, '$1');
1528
+ const normKey = (cleanLabel + '_' + parseInt(m[2], 10)).toUpperCase();
1508
1529
  if (lookup.has(normKey)) {
1509
1530
  restored = lookup.get(normKey);
1510
1531
  }
@@ -1610,18 +1631,17 @@ class StreamingDetokenizer {
1610
1631
  this.pendingBuffer += text;
1611
1632
 
1612
1633
  // Candidate unclosed token at tail of buffer (inspect tail window up to 160 chars)
1613
- // Matches incomplete tokens starting with [, <, {{, or __, plus single delimiter prefixes { and _
1634
+ // Matches incomplete tokens starting with [, 【, [, <, {, __, %5B, \u005b, or HTML entity &#91; / &lbrack;
1614
1635
  const tailLen = 160;
1615
1636
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
1616
1637
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
1617
- const m = tail.match(/(?:\[|<|\{\{?|__?)[A-Za-z0-9_\-А-Яа-яЁё ]{0,128}$/);
1638
+ const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-А-Яа-яЁё *`~.\\]{0,128}(?:%5?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?3?|[xX]0*5?[Dd]?)?|r(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)?|%[0-9A-Fa-f]?|\\(?:u(?:0(?:0(?:5)?)?)?)?|&(?:a(?:m(?:p(?:;)?)?)?)?(?:#(?:0*9?1?|[xX]0*5?)?|l(?:b(?:r(?:a(?:c(?:k)?)?)?)?)?)?)$/);
1618
1639
 
1619
1640
  if (m) {
1620
1641
  const candidate = m[0];
1621
- // Only closed double-underscore tokens (e.g. __NAME_1__) could match m since '_' is in character class.
1622
- // Bracketed tokens ([, <, {{) cannot end with their closing delimiter here and must be buffered.
1623
- const isClosedUnderscoreToken = candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__');
1624
- if (isClosedUnderscoreToken && this.replacer(candidate) !== candidate) {
1642
+ // Closed double-underscore (__NAME_1__) or JSON Unicode (\u005bNAME_1\u005d) tokens match character class and can be emitted immediately.
1643
+ const isClosedUnderscoreOrUnicode = (candidate.length >= 4 && candidate.startsWith('__') && candidate.endsWith('__')) || /\\u005[Dd]/.test(candidate);
1644
+ if (isClosedUnderscoreOrUnicode && this.replacer(candidate) !== candidate) {
1625
1645
  const toEmit = this.pendingBuffer;
1626
1646
  this.pendingBuffer = '';
1627
1647
  return this.replacer(toEmit);
@@ -1843,13 +1863,8 @@ function withPrivacyScrubber(handler, options = {}) {
1843
1863
  tokenMap = res.tokenMap;
1844
1864
  telemetry = res.telemetry;
1845
1865
  } else if (rawBody && typeof rawBody === 'object') {
1846
- const serialized = JSON.stringify(rawBody);
1847
- const res = sanitize(serialized, options);
1848
- try {
1849
- scrubbedBody = JSON.parse(res.scrubbedText);
1850
- } catch (_) {
1851
- scrubbedBody = rawBody;
1852
- }
1866
+ const res = sanitizeObject(rawBody, options);
1867
+ scrubbedBody = res.sanitized;
1853
1868
  tokenMap = res.tokenMap;
1854
1869
  telemetry = res.telemetry;
1855
1870
  }
@@ -1860,7 +1875,8 @@ function withPrivacyScrubber(handler, options = {}) {
1860
1875
  scrubbedBody,
1861
1876
  tokenMap,
1862
1877
  telemetry,
1863
- restore: (text) => restore(text, tokenMap),
1878
+ restore: (text) => (text && typeof text === 'object') ? restoreObject(text, tokenMap) : restore(text, tokenMap),
1879
+ restoreObject: (obj) => restoreObject(obj, tokenMap),
1864
1880
  createStreamRestorer: () => createStreamTokenRestorer(tokenMap)
1865
1881
  };
1866
1882
 
@@ -1878,15 +1894,13 @@ function withPrivacyScrubber(handler, options = {}) {
1878
1894
  function createExpressMiddleware(options = {}) {
1879
1895
  return function privacyScrubberExpress(req, res, next) {
1880
1896
  if (req && req.body && typeof req.body === 'object') {
1881
- const serialized = JSON.stringify(req.body);
1882
- const { scrubbedText, tokenMap, telemetry } = sanitize(serialized, options);
1897
+ const { sanitized, tokenMap, telemetry } = sanitizeObject(req.body, options);
1883
1898
  req.rawBody = req.body;
1884
- try {
1885
- req.body = JSON.parse(scrubbedText);
1886
- } catch (_) {}
1899
+ req.body = sanitized;
1887
1900
  req.psTokenMap = tokenMap;
1888
1901
  req.psTelemetry = telemetry;
1889
- req.restore = (aiResponse) => restore(aiResponse, tokenMap);
1902
+ req.restore = (aiResponse) => (aiResponse && typeof aiResponse === 'object') ? restoreObject(aiResponse, tokenMap) : restore(aiResponse, tokenMap);
1903
+ req.restoreObject = (obj) => restoreObject(obj, tokenMap);
1890
1904
  req.createStreamRestorer = () => createStreamTokenRestorer(tokenMap);
1891
1905
  }
1892
1906
  if (typeof next === 'function') next();
@@ -1903,7 +1917,7 @@ function formatCefEvent(eventData = {}) {
1903
1917
  const {
1904
1918
  vendor = 'PrivacyScrubber',
1905
1919
  product = 'ZTDS',
1906
- version = '2.2.5',
1920
+ version = '2.4.0',
1907
1921
  eventClassId = 'PII_REDACTION',
1908
1922
  eventName = 'Zero-Trust PII Sanitization Executed',
1909
1923
  riskLevel = 'LOW EXPOSURE',
@@ -1972,7 +1986,7 @@ function formatSyslogEvent(eventData = {}) {
1972
1986
  function formatJsonlEvent(eventData = {}) {
1973
1987
  const {
1974
1988
  timestamp = new Date().toISOString(),
1975
- version = '2.2.5',
1989
+ version = '2.4.0',
1976
1990
  protocol = 'ZTDS',
1977
1991
  event = 'sanitization',
1978
1992
  riskLevel = 'LOW EXPOSURE',
@@ -2508,7 +2522,46 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2508
2522
  throw new Error(`[guard_read_file] Path is a directory, not a file: ${resolved}`);
2509
2523
  }
2510
2524
 
2511
- const raw = fs.readFileSync(resolved, 'utf8');
2525
+ const rawBuf = fs.readFileSync(resolved);
2526
+ const checkLen = Math.min(rawBuf.length, 8000);
2527
+ for (let i = 0; i < checkLen; i++) {
2528
+ if (rawBuf[i] === 0) {
2529
+ throw new Error(`[guard_read_file] Binary file detected (${path.basename(resolved)}). Only text, code, and MIME files are supported.`);
2530
+ }
2531
+ }
2532
+ let raw = rawBuf.toString('utf8');
2533
+ const ext = path.extname(resolved).toLowerCase();
2534
+ if (ext === '.eml') {
2535
+ const decodeQpUtf8 = (str) =>
2536
+ str.replace(/=\r?\n/g, '').replace(/(?:=[0-9A-Fa-f]{2})+/g, (seq) => {
2537
+ try {
2538
+ const bytes = Buffer.alloc(seq.length / 3);
2539
+ for (let i = 0; i < bytes.length; i++) {
2540
+ bytes[i] = parseInt(seq.slice(i * 3 + 1, i * 3 + 3), 16);
2541
+ }
2542
+ return bytes.toString('utf8');
2543
+ } catch (_) { return seq; }
2544
+ });
2545
+ raw = raw.replace(/=\?([^?]+)\?([BQbq])\?([^?]+)\?=/g, (full, _cs, enc, payload) => {
2546
+ try {
2547
+ return enc.toUpperCase() === 'B'
2548
+ ? Buffer.from(payload.replace(/\s+/g, ''), 'base64').toString('utf8')
2549
+ : decodeQpUtf8(payload.replace(/_/g, ' '));
2550
+ } catch (_) { return full; }
2551
+ });
2552
+ raw = decodeQpUtf8(raw);
2553
+ } else if (ext === '.rtf') {
2554
+ raw = raw
2555
+ .replace(/\\par\b|\\line\b/gi, '\n')
2556
+ .replace(/\\'([0-9a-fA-F]{2})/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
2557
+ .replace(/\\u(-?\d+)\??/g, (_, code) => {
2558
+ const n = parseInt(code, 10);
2559
+ return String.fromCharCode(n < 0 ? n + 65536 : n);
2560
+ })
2561
+ .replace(/\\[a-zA-Z]+(?:-?\d+)?[ ]?/g, ' ')
2562
+ .replace(/[{}]/g, ' ')
2563
+ .replace(/[ \t]+/g, ' ');
2564
+ }
2512
2565
  const lines = raw.split(/\r?\n/);
2513
2566
  const capped = lines.slice(0, maxLines).join('\n');
2514
2567
  const wasTruncated = lines.length > maxLines;