@privacyscrubber/sdk 2.3.7 → 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 +86 -28
  2. package/benchmark.js +347 -0
  3. package/browser.js +2 -2
  4. package/cli.js +54 -2
  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 +291 -2
  33. package/fastmcp.d.ts +18 -0
  34. package/fastmcp.js +4 -0
  35. package/index.cjs +211 -134
  36. package/index.js +211 -134
  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);
@@ -33,8 +231,10 @@ class ZTDSFastMCPGuard {
33
231
  this.sanitizeOutputs = options.sanitizeOutputs !== false;
34
232
  this.useCryptoTokens = options.useCryptoTokens !== false;
35
233
  this.autoZeroize = options.autoZeroize !== false;
234
+ this.maxSessionTokens = options.maxSessionTokens || 5000;
36
235
  this._sessionMaps = new Map();
37
236
  this._entityMaps = new Map();
237
+ this._streamDetokenizers = new Map();
38
238
  }
39
239
 
40
240
  _generateToken(entityType, tokenMap) {
@@ -86,6 +286,14 @@ class ZTDSFastMCPGuard {
86
286
  if (entityMap.has(match)) {
87
287
  return entityMap.get(match);
88
288
  }
289
+ if (tokenMap.size >= this.maxSessionTokens) {
290
+ const oldestToken = tokenMap.keys().next().value;
291
+ const oldestMatch = tokenMap.get(oldestToken);
292
+ tokenMap.delete(oldestToken);
293
+ if (oldestMatch && entityMap.has(oldestMatch)) {
294
+ entityMap.delete(oldestMatch);
295
+ }
296
+ }
89
297
  const token = this._generateToken(entityType, tokenMap);
90
298
  tokenMap.set(token, match);
91
299
  entityMap.set(match, token);
@@ -148,6 +356,65 @@ class ZTDSFastMCPGuard {
148
356
  return val;
149
357
  }
150
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
+
151
418
  wrapTool(toolName, handler) {
152
419
  return async (args, context = {}) => {
153
420
  const sessionId =
@@ -160,6 +427,7 @@ class ZTDSFastMCPGuard {
160
427
  unmask: (val) => this.restoreObject(val, sessionId),
161
428
  };
162
429
 
430
+ let isStreaming = false;
163
431
  try {
164
432
  let processedArgs = args;
165
433
  if (this.sanitizeInputs && args !== undefined && args !== null) {
@@ -168,6 +436,11 @@ class ZTDSFastMCPGuard {
168
436
 
169
437
  const rawResult = await handler(processedArgs, enrichedContext);
170
438
 
439
+ if (rawResult && typeof rawResult[Symbol.asyncIterator] === "function") {
440
+ isStreaming = true;
441
+ return this.restoreStream(rawResult, sessionId);
442
+ }
443
+
171
444
  let finalResult = rawResult;
172
445
  if (this.sanitizeOutputs && rawResult !== undefined && rawResult !== null) {
173
446
  if (typeof rawResult === "string") {
@@ -176,12 +449,22 @@ class ZTDSFastMCPGuard {
176
449
  finalResult = {
177
450
  ...rawResult,
178
451
  content: rawResult.content.map((block) => {
179
- if (block && block.type === "text" && typeof block.text === "string") {
452
+ if (!block || typeof block !== "object") return block;
453
+ if (block.type === "text" && typeof block.text === "string") {
180
454
  return {
181
455
  ...block,
182
456
  text: this.sanitizeText(block.text, sessionId).sanitized,
183
457
  };
184
458
  }
459
+ if ((block.type === "resource" || block.type === "embeddedResource") && block.resource && typeof block.resource.text === "string") {
460
+ return {
461
+ ...block,
462
+ resource: {
463
+ ...block.resource,
464
+ text: this.sanitizeText(block.resource.text, sessionId).sanitized,
465
+ },
466
+ };
467
+ }
185
468
  return block;
186
469
  }),
187
470
  };
@@ -215,7 +498,7 @@ class ZTDSFastMCPGuard {
215
498
  _ztds: ztdsMeta,
216
499
  };
217
500
  } finally {
218
- if (this.autoZeroize) {
501
+ if (!isStreaming && this.autoZeroize) {
219
502
  this.zeroizeSession(sessionId);
220
503
  }
221
504
  }
@@ -231,6 +514,10 @@ class ZTDSFastMCPGuard {
231
514
  this._entityMaps.get(sessionId).clear();
232
515
  this._entityMaps.delete(sessionId);
233
516
  }
517
+ if (this._streamDetokenizers.has(sessionId)) {
518
+ this._streamDetokenizers.get(sessionId).reset();
519
+ this._streamDetokenizers.delete(sessionId);
520
+ }
234
521
  }
235
522
  }
236
523
 
@@ -266,6 +553,8 @@ module.exports = {
266
553
  ZTDSFastMCPGuard,
267
554
  ZTDS_PATTERNS,
268
555
  TOKEN_PATTERN,
556
+ StreamingDetokenizer,
557
+ createStreamTokenRestorer: (tokenMap, options) => new StreamingDetokenizer(tokenMap, options),
269
558
  createZTDSGuard,
270
559
  ztdsGuardTool,
271
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,