@privacyscrubber/sdk 2.4.2 → 2.4.4
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 +23 -0
- package/browser.js +84 -29
- package/cli.js +65 -27
- package/examples/python-bridge/bridge_daemon.cjs +24 -5
- package/examples/python-bridge/fastapi_example.py +1 -1
- package/examples/python-bridge/privacyscrubber/__init__.py +1 -1
- package/examples/python-bridge/privacyscrubber/bridge_daemon.cjs +24 -5
- package/examples/python-bridge/privacyscrubber/cli.py +1 -1
- package/examples/python-bridge/privacyscrubber/client.py +12 -0
- package/examples/python-bridge/privacyscrubber/langchain.py +31 -7
- package/examples/python-bridge/pyproject.toml +1 -1
- package/examples/python-bridge/setup.py +1 -1
- package/examples/python-bridge/test_python_bridge.py +1 -1
- package/fastmcp.cjs +45 -9
- package/index.cjs +48 -13
- package/index.d.ts +3 -2
- package/index.js +48 -13
- package/package.json +8 -1
- package/polyfill.js +31 -13
- package/ps-license-manager.js +11 -261
- package/ps-pii-engine.cjs +8 -3611
- package/sarif.cjs +388 -0
- package/sarif.js +37 -0
- package/scrubber-core.cjs +8 -1801
- package/vercel-ai.cjs +52 -6
- package/vision.cjs +15 -11
- package/vision.js +15 -11
|
@@ -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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
7
|
+
version = "2.4.4"
|
|
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.
|
|
16
|
+
version="2.4.4",
|
|
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",
|
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(
|
|
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;)
|
|
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_
|
|
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
|
-
|
|
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-
|
|
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
|
|
332
|
-
|
|
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-
|
|
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_
|
|
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
|
|
1750
|
-
|
|
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)
|
|
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.
|
|
1955
|
+
version = '2.4.4',
|
|
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.
|
|
2024
|
+
version = '2.4.4',
|
|
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(
|
|
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
|
-
|
|
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
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-
|
|
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_
|
|
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
|
|
1752
|
-
|
|
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)
|
|
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.
|
|
1957
|
+
version = '2.4.4',
|
|
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.
|
|
2026
|
+
version = '2.4.4',
|
|
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(
|
|
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
|
|
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.
|
|
3
|
+
"version": "2.4.4",
|
|
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
|
-
|
|
1
|
+
const _root = typeof globalThis !== 'undefined'
|
|
2
|
+
? globalThis
|
|
3
|
+
: (typeof global !== 'undefined' ? global : (typeof self !== 'undefined' ? self : null));
|
|
2
4
|
|
|
3
|
-
if (
|
|
4
|
-
if (typeof
|
|
5
|
-
|
|
5
|
+
if (_root) {
|
|
6
|
+
if (typeof _root.window === 'undefined') {
|
|
7
|
+
_root.window = {
|
|
6
8
|
crypto: {
|
|
7
|
-
getRandomValues: (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
|
|
14
|
-
|
|
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
|
|
21
|
-
|
|
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
|
|
28
|
-
|
|
45
|
+
if (typeof _root.atob === 'undefined' && typeof Buffer !== 'undefined') {
|
|
46
|
+
_root.atob = (str) => Buffer.from(str, 'base64').toString('binary');
|
|
29
47
|
}
|
|
30
|
-
if (typeof
|
|
31
|
-
|
|
48
|
+
if (typeof _root.btoa === 'undefined' && typeof Buffer !== 'undefined') {
|
|
49
|
+
_root.btoa = (str) => Buffer.from(str, 'binary').toString('base64');
|
|
32
50
|
}
|
|
33
51
|
}
|