@privacyscrubber/sdk 2.4.2 → 2.4.3

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.
@@ -74,6 +74,9 @@ class ZTDSCallbackHandler(BaseCallbackHandler):
74
74
  print(f"[ZTDS Guard] Sanitized {res.count} PII tokens for run {key}")
75
75
 
76
76
  self._session_token_maps[key] = combined_token_map
77
+ if len(self._session_token_maps) > 512:
78
+ oldest_key = next(iter(self._session_token_maps))
79
+ self._session_token_maps.pop(oldest_key, None)
77
80
 
78
81
  def on_llm_end(
79
82
  self,
@@ -92,16 +95,37 @@ class ZTDSCallbackHandler(BaseCallbackHandler):
92
95
  if generations and isinstance(generations, list):
93
96
  for gen_list in generations:
94
97
  for gen in gen_list:
95
- if hasattr(gen, "text") and gen.text:
96
- restored = self.scrubber.restore(gen.text, token_map=token_map)
97
- gen.text = restored.restored_text
98
- elif hasattr(gen, "message") and hasattr(gen.message, "content"):
99
- if isinstance(gen.message.content, str):
100
- restored = self.scrubber.restore(gen.message.content, token_map=token_map)
101
- gen.message.content = restored.restored_text
98
+ restored_str: Optional[str] = None
99
+ if hasattr(gen, "message") and hasattr(gen.message, "content"):
100
+ msg_content = gen.message.content
101
+ if isinstance(msg_content, str) and msg_content:
102
+ restored_str = self.scrubber.restore(msg_content, token_map=token_map).restored_text
103
+ gen.message.content = restored_str
104
+ elif isinstance(msg_content, list):
105
+ for block in msg_content:
106
+ if isinstance(block, dict) and isinstance(block.get("text"), str) and block["text"]:
107
+ block["text"] = self.scrubber.restore(block["text"], token_map=token_map).restored_text
108
+ if hasattr(gen, "text") and isinstance(getattr(gen, "text", None), str) and gen.text:
109
+ try:
110
+ gen.text = restored_str if restored_str is not None else self.scrubber.restore(gen.text, token_map=token_map).restored_text
111
+ except (AttributeError, TypeError):
112
+ pass
102
113
 
103
114
  return response
104
115
 
116
+ def on_llm_error(
117
+ self,
118
+ error: BaseException,
119
+ *,
120
+ run_id: Optional[Any] = None,
121
+ parent_run_id: Optional[Any] = None,
122
+ tags: Optional[List[str]] = None,
123
+ **kwargs: Any
124
+ ) -> Any:
125
+ """Cleans up ephemeral token maps if the LLM call fails."""
126
+ key = str(run_id) if run_id else "default"
127
+ self._session_token_maps.pop(key, None)
128
+
105
129
 
106
130
  class ZTDSDocumentTransformer:
107
131
  """
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "privacyscrubber"
7
- version = "2.4.2"
7
+ version = "2.4.3"
8
8
  description = "Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -13,7 +13,7 @@ with open(os.path.join(here, "README.md"), encoding="utf-8") as f:
13
13
 
14
14
  setup(
15
15
  name="privacyscrubber",
16
- version="2.4.2",
16
+ version="2.4.3",
17
17
  description="Zero-Trust Data Sanitization & In-Memory PII Shield for Python AI Pipelines (ZTDS RFC v1.0)",
18
18
  long_description=long_description,
19
19
  long_description_content_type="text/markdown",
@@ -312,7 +312,7 @@ class TestPrivacyScrubberBridge(unittest.TestCase):
312
312
  cwd=bridge_dir,
313
313
  env=sub_env
314
314
  )
315
- self.assertIn("2.4.2", ver_out)
315
+ self.assertIn("2.4.3", ver_out)
316
316
 
317
317
 
318
318
  if __name__ == "__main__":
package/fastmcp.cjs CHANGED
@@ -19,12 +19,12 @@ const ZTDS_PATTERNS = {
19
19
  CREDIT_CARD: /\b(?:\d{4}[-\s]?){3}\d{4}\b/g,
20
20
  EMAIL: /[A-Za-z0-9._%+-]{1,64}@[A-Za-z0-9-]{1,63}(?:\.[A-Za-z0-9-]{1,63})*\.[A-Za-z]{2,24}/g,
21
21
  IBAN: /\b[A-Z]{2}[0-9]{2}[A-Z0-9]{4}[0-9]{7}(?:[A-Z0-9]?){0,16}\b/g,
22
- IPV4: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
22
+ IPV4: /\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g,
23
23
  PHONE: /(?:(?:\+?1\s*(?:[.-]\s*)?)?(?:\(\s*([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9])\s*\)|([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8][02-9]))\s*(?:[.-]\s*)?)?([2-9]1[02-9]|[2-9][02-8]1|[2-9][02-8]{2})\s*(?:[.-]\s*)?([0-9]{4})\b/g,
24
24
  SSN: /\b\d{3}-\d{2}-\d{4}\b/g,
25
25
  };
26
26
 
27
- const TOKEN_PATTERN = /(?:%5[Bb]|\\u005[Bb]|&(?:amp;)?#0*91;|\[|<|\{\{?|__|【|[)[*`_~\s]*([A-Z_]+)_TOKEN_([a-zA-Z0-9_-]+)[*`_~.,;:!?\s]*(?:%5[Dd]|\\u005[Dd]|&(?:amp;)?#0*93;|\]|>|\}\}?|__|】|])/gi;
27
+ const TOKEN_PATTERN = /(?:%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;|lsqb;)|\[|<|\{\{?|__|【|[|〔|「|『)[*`_~\s]*([A-Z0-9_]+)[*`_~]*[-_\s]+TOKEN[*`_~]*[-_\s]+([a-zA-Z0-9_-]+)[*`_~.,;:!?\s]*(?:%5[Dd]|\\u005[Dd]|&(?:amp;)?(?:#0*93;|#[xX]0*5[Dd];|rbrack;|rsqb;)|\]|>|\}\}?|__|】|]|〕|」|』)/gi;
28
28
 
29
29
  function escapeRegex(s) {
30
30
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -166,7 +166,7 @@ class StreamingDetokenizer {
166
166
  const tailLen = 160;
167
167
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
168
168
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
169
- 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)?)?)?)?)?)?)$/);
169
+ const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{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)?)?)?)?)?)?)$/);
170
170
 
171
171
  if (m) {
172
172
  const candidate = m[0];
@@ -242,10 +242,11 @@ class ZTDSFastMCPGuard {
242
242
  this.maxSessionTokens = options.maxSessionTokens || 5000;
243
243
  this._sessionMaps = new Map();
244
244
  this._entityMaps = new Map();
245
+ this._sessionCounters = new Map();
245
246
  this._streamDetokenizers = new Map();
246
247
  }
247
248
 
248
- _generateToken(entityType, tokenMap) {
249
+ _generateToken(entityType, tokenMap, counters) {
249
250
  if (this.useCryptoTokens) {
250
251
  for (let i = 0; i < 100; i++) {
251
252
  const hex = crypto.randomBytes(4).toString("hex");
@@ -255,13 +256,25 @@ class ZTDSFastMCPGuard {
255
256
  }
256
257
  }
257
258
  }
259
+ if (counters && typeof counters.get === "function") {
260
+ let nextIdx = (counters.get(entityType) || 0) + 1;
261
+ while (tokenMap.has(`[${entityType}_TOKEN_${nextIdx}]`)) {
262
+ nextIdx++;
263
+ }
264
+ counters.set(entityType, nextIdx);
265
+ return `[${entityType}_TOKEN_${nextIdx}]`;
266
+ }
258
267
  let count = 0;
259
268
  for (const k of tokenMap.keys()) {
260
269
  if (k.startsWith(`[${entityType}_TOKEN_`)) {
261
270
  count++;
262
271
  }
263
272
  }
264
- return `[${entityType}_TOKEN_${count + 1}]`;
273
+ let idx = count + 1;
274
+ while (tokenMap.has(`[${entityType}_TOKEN_${idx}]`)) {
275
+ idx++;
276
+ }
277
+ return `[${entityType}_TOKEN_${idx}]`;
265
278
  }
266
279
 
267
280
  sanitizeText(text, sessionId) {
@@ -272,10 +285,12 @@ class ZTDSFastMCPGuard {
272
285
  if (!this._sessionMaps.has(sessionId)) {
273
286
  this._sessionMaps.set(sessionId, new Map());
274
287
  this._entityMaps.set(sessionId, new Map());
288
+ this._sessionCounters.set(sessionId, new Map());
275
289
  }
276
290
 
277
291
  const tokenMap = this._sessionMaps.get(sessionId);
278
292
  const entityMap = this._entityMaps.get(sessionId);
293
+ const counters = this._sessionCounters.get(sessionId);
279
294
  let sanitized = text
280
295
  .replace(/[\u200B-\u200F\u202A-\u202E\u2060-\u206F\uFEFF\u00AD]/g, "")
281
296
  .replace(/[\uFF01-\uFF5E]/g, (ch) => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0));
@@ -288,11 +303,14 @@ class ZTDSFastMCPGuard {
288
303
  continue;
289
304
  }
290
305
 
291
- const regex = new RegExp(`(\\[[A-Z_]+_TOKEN_[a-zA-Z0-9_-]+\\])|(?:${pattern.source})`, "g");
306
+ const regex = new RegExp(`(\\[[A-Z0-9_]+_TOKEN_[a-zA-Z0-9_-]+\\])|(?:${pattern.source})`, "g");
292
307
  sanitized = sanitized.replace(regex, (match, existingToken) => {
293
308
  if (existingToken) {
294
309
  return existingToken;
295
310
  }
311
+ if (entityType === "IPV4" && (match.startsWith("127.0.0.") || match === "0.0.0.0")) {
312
+ return match;
313
+ }
296
314
  if (entityMap.has(match)) {
297
315
  return entityMap.get(match);
298
316
  }
@@ -304,7 +322,7 @@ class ZTDSFastMCPGuard {
304
322
  entityMap.delete(oldestMatch);
305
323
  }
306
324
  }
307
- const token = this._generateToken(entityType, tokenMap);
325
+ const token = this._generateToken(entityType, tokenMap, counters);
308
326
  tokenMap.set(token, match);
309
327
  entityMap.set(match, token);
310
328
  return token;
@@ -328,8 +346,16 @@ class ZTDSFastMCPGuard {
328
346
  }
329
347
 
330
348
  return text.replace(TOKEN_PATTERN, (match, entityType, tokenId) => {
331
- const canonical = `[${entityType.toUpperCase()}_TOKEN_${tokenId}]`;
332
- return tokenMap.get(canonical) || tokenMap.get(match) || match;
349
+ const upperType = entityType.toUpperCase();
350
+ const canonical = `[${upperType}_TOKEN_${tokenId}]`;
351
+ if (tokenMap.has(canonical)) return tokenMap.get(canonical);
352
+ const lowerHex = `[${upperType}_TOKEN_${tokenId.toLowerCase()}]`;
353
+ if (tokenMap.has(lowerHex)) return tokenMap.get(lowerHex);
354
+ if (/^\d+$/.test(tokenId)) {
355
+ const stripped = `[${upperType}_TOKEN_${parseInt(tokenId, 10)}]`;
356
+ if (tokenMap.has(stripped)) return tokenMap.get(stripped);
357
+ }
358
+ return tokenMap.get(match) || match;
333
359
  });
334
360
  }
335
361
 
@@ -340,6 +366,9 @@ class ZTDSFastMCPGuard {
340
366
  if (Array.isArray(val)) {
341
367
  return val.map((item) => this.sanitizeObject(item, sessionId));
342
368
  }
369
+ if (val instanceof Date || val instanceof RegExp || val instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(val))) {
370
+ return val;
371
+ }
343
372
  if (val !== null && typeof val === "object") {
344
373
  const result = {};
345
374
  for (const [k, v] of Object.entries(val)) {
@@ -357,6 +386,9 @@ class ZTDSFastMCPGuard {
357
386
  if (Array.isArray(val)) {
358
387
  return val.map((item) => this.restoreObject(item, sessionId));
359
388
  }
389
+ if (val instanceof Date || val instanceof RegExp || val instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(val))) {
390
+ return val;
391
+ }
360
392
  if (val !== null && typeof val === "object") {
361
393
  const result = {};
362
394
  for (const [k, v] of Object.entries(val)) {
@@ -525,6 +557,10 @@ class ZTDSFastMCPGuard {
525
557
  this._entityMaps.get(sessionId).clear();
526
558
  this._entityMaps.delete(sessionId);
527
559
  }
560
+ if (this._sessionCounters && this._sessionCounters.has(sessionId)) {
561
+ this._sessionCounters.get(sessionId).clear();
562
+ this._sessionCounters.delete(sessionId);
563
+ }
528
564
  if (this._streamDetokenizers.has(sessionId)) {
529
565
  this._streamDetokenizers.get(sessionId).reset();
530
566
  this._streamDetokenizers.delete(sessionId);
package/index.cjs CHANGED
@@ -129,7 +129,7 @@ function buildAuditTelemetry(currentTokenMap = {}) {
129
129
  if (!t) continue;
130
130
  const tokenStr = typeof t === 'string' ? t : ((t && (t.token || t.mask)) ? String(t.token || t.mask) : '');
131
131
  if (!tokenStr) continue;
132
- const match = tokenStr.match(/\[([A-Z_]+)_\d+\]/);
132
+ const match = tokenStr.match(/\[([A-Z0-9_]+)_\d+\]/);
133
133
  const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
134
134
  entities[baseType] = (entities[baseType] || 0) + 1;
135
135
  totalCount++;
@@ -455,7 +455,11 @@ function sanitize(text, options = {}) {
455
455
  if (!text || typeof text !== 'string') {
456
456
  return {
457
457
  scrubbedText: '',
458
+ scrubbed: '',
458
459
  tokenMap: {},
460
+ sessionMap: {},
461
+ matches: [],
462
+ spans: [],
459
463
  count: 0,
460
464
  executionMs: 0,
461
465
  license: { isPro: false, tier: 'FREE', limit: 15000, wasTruncated: false },
@@ -466,7 +470,7 @@ function sanitize(text, options = {}) {
466
470
 
467
471
  const {
468
472
  profile = 'General',
469
- licenseKey = (typeof process !== 'undefined' && process.env?.PRIVACYSCRUBBER_KEY) || '',
473
+ licenseKey = (typeof process !== 'undefined' && (process.env?.PRIVACYSCRUBBER_KEY || process.env?.PRIVACYSCRUBBER_LICENSE_KEY)) || '',
470
474
  customRules = [],
471
475
  tokenLabelMap = {},
472
476
  existingSessionMap = {},
@@ -585,6 +589,7 @@ function sanitize(text, options = {}) {
585
589
 
586
590
  return {
587
591
  scrubbedText: finalScrubbedText,
592
+ scrubbed: finalScrubbedText,
588
593
  tokenMap: returnFullSessionMap ? sessionMap : (rawResult.tokenMap || {}),
589
594
  sessionMap: sessionMap,
590
595
  matches: rawResult.matches || [],
@@ -899,6 +904,9 @@ class PrivacyScrubberEngine {
899
904
  * @param {object} [options={}]
900
905
  */
901
906
  createStreamRestorer(tokenMap = null, options = {}) {
907
+ if (tokenMap && typeof tokenMap === 'object' && typeof tokenMap.onTokenRestored === 'function' && Object.keys(options).length === 0) {
908
+ return createStreamTokenRestorer(this.sessionMap, tokenMap);
909
+ }
902
910
  const activeMap = tokenMap || this.sessionMap;
903
911
  return createStreamTokenRestorer(activeMap, options);
904
912
  }
@@ -1649,7 +1657,7 @@ class StreamingDetokenizer {
1649
1657
  const tailLen = 160;
1650
1658
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
1651
1659
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
1652
- 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)?)?)?)?)?)?)$/);
1660
+ const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{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)?)?)?)?)?)?)$/);
1653
1661
 
1654
1662
  if (m) {
1655
1663
  const candidate = m[0];
@@ -1738,16 +1746,20 @@ class StreamingDetokenizer {
1738
1746
  * @returns {AsyncGenerator<any>}
1739
1747
  */
1740
1748
  async* wrapStream(asyncIterable) {
1749
+ let lastOpenAIChunk = null;
1741
1750
  for await (const chunk of asyncIterable) {
1742
1751
  if (chunk === null || chunk === undefined) continue;
1743
1752
  // Handle OpenAI chunk object
1744
1753
  if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
1754
+ lastOpenAIChunk = chunk;
1745
1755
  const delta = chunk.choices[0].delta.content;
1746
1756
  if (typeof delta === 'string') {
1747
1757
  const restored = this.feed(delta);
1748
1758
  if (restored) {
1749
- chunk.choices[0].delta.content = restored;
1750
- yield chunk;
1759
+ const clonedChoices = chunk.choices.map((c, idx) =>
1760
+ idx === 0 ? { ...c, delta: { ...c.delta, content: restored } } : c
1761
+ );
1762
+ yield { ...chunk, choices: clonedChoices };
1751
1763
  }
1752
1764
  } else {
1753
1765
  yield chunk;
@@ -1773,7 +1785,16 @@ class StreamingDetokenizer {
1773
1785
  }
1774
1786
 
1775
1787
  const finalChunk = this.flush();
1776
- if (finalChunk) yield finalChunk;
1788
+ if (finalChunk) {
1789
+ if (lastOpenAIChunk) {
1790
+ const clonedChoices = lastOpenAIChunk.choices.map((c, idx) =>
1791
+ idx === 0 ? { ...c, delta: { ...c.delta, content: finalChunk } } : c
1792
+ );
1793
+ yield { ...lastOpenAIChunk, choices: clonedChoices };
1794
+ } else {
1795
+ yield finalChunk;
1796
+ }
1797
+ }
1777
1798
  }
1778
1799
  }
1779
1800
 
@@ -1840,8 +1861,8 @@ function createNodeStreamTokenRestorer(tokenMap = {}, options = {}) {
1840
1861
  * @param {Record<string, string>} tokenMap - Volatile token map.
1841
1862
  * @returns {AsyncGenerator<any>}
1842
1863
  */
1843
- async function* wrapAiStream(stream, tokenMap = {}) {
1844
- const detok = new StreamingDetokenizer(tokenMap);
1864
+ async function* wrapAiStream(stream, tokenMap = {}, options = {}) {
1865
+ const detok = new StreamingDetokenizer(tokenMap, options);
1845
1866
  yield* detok.wrapStream(stream);
1846
1867
  }
1847
1868
 
@@ -1931,7 +1952,7 @@ function formatCefEvent(eventData = {}) {
1931
1952
  const {
1932
1953
  vendor = 'PrivacyScrubber',
1933
1954
  product = 'ZTDS',
1934
- version = '2.4.2',
1955
+ version = '2.4.3',
1935
1956
  eventClassId = 'PII_REDACTION',
1936
1957
  eventName = 'Zero-Trust PII Sanitization Executed',
1937
1958
  riskLevel = 'LOW EXPOSURE',
@@ -2000,7 +2021,7 @@ function formatSyslogEvent(eventData = {}) {
2000
2021
  function formatJsonlEvent(eventData = {}) {
2001
2022
  const {
2002
2023
  timestamp = new Date().toISOString(),
2003
- version = '2.4.2',
2024
+ version = '2.4.3',
2004
2025
  protocol = 'ZTDS',
2005
2026
  event = 'sanitization',
2006
2027
  riskLevel = 'LOW EXPOSURE',
@@ -2189,7 +2210,7 @@ function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString()) {
2189
2210
  const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
2190
2211
  const counts = {};
2191
2212
  for (const t of labels) {
2192
- const m = t.match(/\[([A-Z_]+)_\d+\]/);
2213
+ const m = String(t).trim().match(/^(?:\[|<|\{\{|__)([A-Z0-9_]+)_\d+(?:\]|>|\}\}|__)$/);
2193
2214
  const type = m ? m[1] : 'CUSTOM';
2194
2215
  counts[type] = (counts[type] || 0) + 1;
2195
2216
  }
@@ -2705,8 +2726,22 @@ function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2705
2726
  return tools;
2706
2727
  }
2707
2728
 
2708
- // Low-level backwards-compatible exports
2709
- const scrubText = scrubberPkg.scrubText;
2729
+ // Low-level backwards-compatible exports (supports both positional customRules array and high-level options object)
2730
+ function scrubText(text, customRulesOrOptions = [], ...args) {
2731
+ if (
2732
+ customRulesOrOptions &&
2733
+ typeof customRulesOrOptions === 'object' &&
2734
+ !Array.isArray(customRulesOrOptions) &&
2735
+ args.length === 0
2736
+ ) {
2737
+ return sanitize(text, customRulesOrOptions);
2738
+ }
2739
+ const res = scrubberPkg.scrubText(text, customRulesOrOptions, ...args);
2740
+ if (res && typeof res === 'object' && typeof res.scrubbed === 'undefined') {
2741
+ res.scrubbed = res.scrubbedText;
2742
+ }
2743
+ return res;
2744
+ }
2710
2745
  const unscrubText = scrubberPkg.unscrubText;
2711
2746
 
2712
2747
  // Vision & Multimodal exports
package/index.d.ts CHANGED
@@ -3,8 +3,9 @@
3
3
  */
4
4
 
5
5
  export interface CustomRule {
6
- name: string;
7
- regex: RegExp;
6
+ name?: string;
7
+ pattern?: string;
8
+ regex?: RegExp;
8
9
  mask?: string;
9
10
  type?: string;
10
11
  label?: string;
package/index.js CHANGED
@@ -131,7 +131,7 @@ export function buildAuditTelemetry(currentTokenMap = {}) {
131
131
  if (!t) continue;
132
132
  const tokenStr = typeof t === 'string' ? t : ((t && (t.token || t.mask)) ? String(t.token || t.mask) : '');
133
133
  if (!tokenStr) continue;
134
- const match = tokenStr.match(/\[([A-Z_]+)_\d+\]/);
134
+ const match = tokenStr.match(/\[([A-Z0-9_]+)_\d+\]/);
135
135
  const baseType = match ? match[1] : (tokenStr.replace(/\[|\]/g, '').replace(/_[0-9]+$/, '') || 'CUSTOM');
136
136
  entities[baseType] = (entities[baseType] || 0) + 1;
137
137
  totalCount++;
@@ -457,7 +457,11 @@ export function sanitize(text, options = {}) {
457
457
  if (!text || typeof text !== 'string') {
458
458
  return {
459
459
  scrubbedText: '',
460
+ scrubbed: '',
460
461
  tokenMap: {},
462
+ sessionMap: {},
463
+ matches: [],
464
+ spans: [],
461
465
  count: 0,
462
466
  executionMs: 0,
463
467
  license: { isPro: false, tier: 'FREE', limit: 15000, wasTruncated: false },
@@ -468,7 +472,7 @@ export function sanitize(text, options = {}) {
468
472
 
469
473
  const {
470
474
  profile = 'General',
471
- licenseKey = (typeof process !== 'undefined' && process.env?.PRIVACYSCRUBBER_KEY) || '',
475
+ licenseKey = (typeof process !== 'undefined' && (process.env?.PRIVACYSCRUBBER_KEY || process.env?.PRIVACYSCRUBBER_LICENSE_KEY)) || '',
472
476
  customRules = [],
473
477
  tokenLabelMap = {},
474
478
  existingSessionMap = {},
@@ -587,6 +591,7 @@ export function sanitize(text, options = {}) {
587
591
 
588
592
  return {
589
593
  scrubbedText: finalScrubbedText,
594
+ scrubbed: finalScrubbedText,
590
595
  tokenMap: returnFullSessionMap ? sessionMap : (rawResult.tokenMap || {}),
591
596
  sessionMap: sessionMap,
592
597
  matches: rawResult.matches || [],
@@ -901,6 +906,9 @@ export class PrivacyScrubberEngine {
901
906
  * @param {object} [options={}]
902
907
  */
903
908
  createStreamRestorer(tokenMap = null, options = {}) {
909
+ if (tokenMap && typeof tokenMap === 'object' && typeof tokenMap.onTokenRestored === 'function' && Object.keys(options).length === 0) {
910
+ return createStreamTokenRestorer(this.sessionMap, tokenMap);
911
+ }
904
912
  const activeMap = tokenMap || this.sessionMap;
905
913
  return createStreamTokenRestorer(activeMap, options);
906
914
  }
@@ -1651,7 +1659,7 @@ export class StreamingDetokenizer {
1651
1659
  const tailLen = 160;
1652
1660
  const tail = this.pendingBuffer.length > tailLen ? this.pendingBuffer.slice(-tailLen) : this.pendingBuffer;
1653
1661
  const offset = this.pendingBuffer.length > tailLen ? this.pendingBuffer.length - tailLen : 0;
1654
- 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)?)?)?)?)?)?)$/);
1662
+ const m = tail.match(/(?:(?:\[|【|[|〔|「|『|<|\{\{?|__?|%5[Bb]|\\u005[Bb]|&(?:amp;)?(?:#0*91;|#[xX]0*5[Bb];|lbrack;))[A-Za-z0-9_\-\u0410-\u044F\u0401\u0451 *`~.\\]{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)?)?)?)?)?)?)$/);
1655
1663
 
1656
1664
  if (m) {
1657
1665
  const candidate = m[0];
@@ -1740,16 +1748,20 @@ export class StreamingDetokenizer {
1740
1748
  * @returns {AsyncGenerator<any>}
1741
1749
  */
1742
1750
  async* wrapStream(asyncIterable) {
1751
+ let lastOpenAIChunk = null;
1743
1752
  for await (const chunk of asyncIterable) {
1744
1753
  if (chunk === null || chunk === undefined) continue;
1745
1754
  // Handle OpenAI chunk object
1746
1755
  if (chunk.choices && Array.isArray(chunk.choices) && chunk.choices[0]?.delta?.content !== undefined) {
1756
+ lastOpenAIChunk = chunk;
1747
1757
  const delta = chunk.choices[0].delta.content;
1748
1758
  if (typeof delta === 'string') {
1749
1759
  const restored = this.feed(delta);
1750
1760
  if (restored) {
1751
- chunk.choices[0].delta.content = restored;
1752
- yield chunk;
1761
+ const clonedChoices = chunk.choices.map((c, idx) =>
1762
+ idx === 0 ? { ...c, delta: { ...c.delta, content: restored } } : c
1763
+ );
1764
+ yield { ...chunk, choices: clonedChoices };
1753
1765
  }
1754
1766
  } else {
1755
1767
  yield chunk;
@@ -1775,7 +1787,16 @@ export class StreamingDetokenizer {
1775
1787
  }
1776
1788
 
1777
1789
  const finalChunk = this.flush();
1778
- if (finalChunk) yield finalChunk;
1790
+ if (finalChunk) {
1791
+ if (lastOpenAIChunk) {
1792
+ const clonedChoices = lastOpenAIChunk.choices.map((c, idx) =>
1793
+ idx === 0 ? { ...c, delta: { ...c.delta, content: finalChunk } } : c
1794
+ );
1795
+ yield { ...lastOpenAIChunk, choices: clonedChoices };
1796
+ } else {
1797
+ yield finalChunk;
1798
+ }
1799
+ }
1779
1800
  }
1780
1801
  }
1781
1802
 
@@ -1842,8 +1863,8 @@ export function createNodeStreamTokenRestorer(tokenMap = {}, options = {}) {
1842
1863
  * @param {Record<string, string>} tokenMap - Volatile token map.
1843
1864
  * @returns {AsyncGenerator<any>}
1844
1865
  */
1845
- export async function* wrapAiStream(stream, tokenMap = {}) {
1846
- const detok = new StreamingDetokenizer(tokenMap);
1866
+ export async function* wrapAiStream(stream, tokenMap = {}, options = {}) {
1867
+ const detok = new StreamingDetokenizer(tokenMap, options);
1847
1868
  yield* detok.wrapStream(stream);
1848
1869
  }
1849
1870
 
@@ -1933,7 +1954,7 @@ export function formatCefEvent(eventData = {}) {
1933
1954
  const {
1934
1955
  vendor = 'PrivacyScrubber',
1935
1956
  product = 'ZTDS',
1936
- version = '2.4.2',
1957
+ version = '2.4.3',
1937
1958
  eventClassId = 'PII_REDACTION',
1938
1959
  eventName = 'Zero-Trust PII Sanitization Executed',
1939
1960
  riskLevel = 'LOW EXPOSURE',
@@ -2002,7 +2023,7 @@ export function formatSyslogEvent(eventData = {}) {
2002
2023
  export function formatJsonlEvent(eventData = {}) {
2003
2024
  const {
2004
2025
  timestamp = new Date().toISOString(),
2005
- version = '2.4.2',
2026
+ version = '2.4.3',
2006
2027
  protocol = 'ZTDS',
2007
2028
  event = 'sanitization',
2008
2029
  riskLevel = 'LOW EXPOSURE',
@@ -2191,7 +2212,7 @@ export function buildPsr2Receipt(tokenMap, timestamp = new Date().toISOString())
2191
2212
  const labels = (tokenMap && typeof tokenMap === 'object' ? Object.keys(tokenMap) : []).slice().sort();
2192
2213
  const counts = {};
2193
2214
  for (const t of labels) {
2194
- const m = t.match(/\[([A-Z_]+)_\d+\]/);
2215
+ const m = String(t).trim().match(/^(?:\[|<|\{\{|__)([A-Z0-9_]+)_\d+(?:\]|>|\}\}|__)$/);
2195
2216
  const type = m ? m[1] : 'CUSTOM';
2196
2217
  counts[type] = (counts[type] || 0) + 1;
2197
2218
  }
@@ -2707,8 +2728,22 @@ export function createGuardedTools(engineOrOptions = {}, toolConfig = {}) {
2707
2728
  return tools;
2708
2729
  }
2709
2730
 
2710
- // Low-level backwards-compatible exports
2711
- export const scrubText = scrubberPkg.scrubText;
2731
+ // Low-level backwards-compatible exports (supports both positional customRules array and high-level options object)
2732
+ export function scrubText(text, customRulesOrOptions = [], ...args) {
2733
+ if (
2734
+ customRulesOrOptions &&
2735
+ typeof customRulesOrOptions === 'object' &&
2736
+ !Array.isArray(customRulesOrOptions) &&
2737
+ args.length === 0
2738
+ ) {
2739
+ return sanitize(text, customRulesOrOptions);
2740
+ }
2741
+ const res = scrubberPkg.scrubText(text, customRulesOrOptions, ...args);
2742
+ if (res && typeof res === 'object' && typeof res.scrubbed === 'undefined') {
2743
+ res.scrubbed = res.scrubbedText;
2744
+ }
2745
+ return res;
2746
+ }
2712
2747
  export const unscrubText = scrubberPkg.unscrubText;
2713
2748
 
2714
2749
  // Vision & Multimodal exports
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@privacyscrubber/sdk",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "In-process consumer privacy defense and pre-emptive PII interception SDK for Node.js, Next.js, and AI pipelines. Zero data loss, zero third parties, sub-millisecond in-memory execution.",
5
5
  "main": "index.cjs",
6
6
  "module": "index.js",
@@ -31,6 +31,11 @@
31
31
  "import": "./vercel-ai.js",
32
32
  "require": "./vercel-ai.cjs",
33
33
  "default": "./vercel-ai.js"
34
+ },
35
+ "./sarif": {
36
+ "import": "./sarif.js",
37
+ "require": "./sarif.cjs",
38
+ "default": "./sarif.js"
34
39
  }
35
40
  },
36
41
  "publishConfig": {
@@ -49,6 +54,8 @@
49
54
  "vercel-ai.js",
50
55
  "vercel-ai.cjs",
51
56
  "vercel-ai.d.ts",
57
+ "sarif.js",
58
+ "sarif.cjs",
52
59
  "index.d.ts",
53
60
  "cli.js",
54
61
  "benchmark.js",
package/polyfill.js CHANGED
@@ -1,33 +1,51 @@
1
- import crypto from 'crypto';
1
+ const _root = typeof globalThis !== 'undefined'
2
+ ? globalThis
3
+ : (typeof global !== 'undefined' ? global : (typeof self !== 'undefined' ? self : null));
2
4
 
3
- if (typeof global !== 'undefined') {
4
- if (typeof global.window === 'undefined') {
5
- global.window = {
5
+ if (_root) {
6
+ if (typeof _root.window === 'undefined') {
7
+ _root.window = {
6
8
  crypto: {
7
- getRandomValues: (arr) => crypto.randomFillSync(arr)
9
+ getRandomValues: (arr) => {
10
+ if (typeof _root.crypto !== 'undefined' && typeof _root.crypto.getRandomValues === 'function') {
11
+ return _root.crypto.getRandomValues(arr);
12
+ }
13
+ try {
14
+ if (typeof require === 'function') {
15
+ const nodeCrypto = require('crypto');
16
+ if (nodeCrypto && typeof nodeCrypto.randomFillSync === 'function') {
17
+ return nodeCrypto.randomFillSync(arr);
18
+ }
19
+ }
20
+ } catch (_) {}
21
+ for (let i = 0; i < arr.length; i++) {
22
+ arr[i] = Math.floor(Math.random() * 256);
23
+ }
24
+ return arr;
25
+ }
8
26
  },
9
27
  addEventListener: () => {},
10
28
  location: { search: '', hash: '' }
11
29
  };
12
30
  }
13
- if (typeof global.document === 'undefined') {
14
- global.document = {
31
+ if (typeof _root.document === 'undefined') {
32
+ _root.document = {
15
33
  readyState: 'complete',
16
34
  getElementById: () => null,
17
35
  addEventListener: () => {}
18
36
  };
19
37
  }
20
- if (typeof global.localStorage === 'undefined') {
21
- global.localStorage = {
38
+ if (typeof _root.localStorage === 'undefined') {
39
+ _root.localStorage = {
22
40
  getItem: () => null,
23
41
  setItem: () => {},
24
42
  removeItem: () => {}
25
43
  };
26
44
  }
27
- if (typeof global.atob === 'undefined') {
28
- global.atob = (str) => Buffer.from(str, 'base64').toString('binary');
45
+ if (typeof _root.atob === 'undefined' && typeof Buffer !== 'undefined') {
46
+ _root.atob = (str) => Buffer.from(str, 'base64').toString('binary');
29
47
  }
30
- if (typeof global.btoa === 'undefined') {
31
- global.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
48
+ if (typeof _root.btoa === 'undefined' && typeof Buffer !== 'undefined') {
49
+ _root.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
32
50
  }
33
51
  }