@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
package/vercel-ai.cjs
CHANGED
|
@@ -33,6 +33,30 @@ const tokenMapsWeakMap = new WeakMap();
|
|
|
33
33
|
* @param {Record<string, string>} sessionTokenMap - Volatile map populated with detected tokens.
|
|
34
34
|
* @returns {any} Sanitized prompt with tokens replacing authentic PII.
|
|
35
35
|
*/
|
|
36
|
+
function sanitizeDeepValue(val, engine, sessionTokenMap) {
|
|
37
|
+
if (val === null || val === undefined) return val;
|
|
38
|
+
if (typeof val === 'string') {
|
|
39
|
+
if (!val.trim()) return val;
|
|
40
|
+
const res = engine.sanitize(val, { existingSessionMap: sessionTokenMap });
|
|
41
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
42
|
+
return res.scrubbedText;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(val)) {
|
|
45
|
+
return val.map(item => sanitizeDeepValue(item, engine, sessionTokenMap));
|
|
46
|
+
}
|
|
47
|
+
if (val instanceof Date || val instanceof RegExp || val instanceof Uint8Array || (typeof Buffer !== 'undefined' && Buffer.isBuffer(val))) {
|
|
48
|
+
return val;
|
|
49
|
+
}
|
|
50
|
+
if (typeof val === 'object') {
|
|
51
|
+
const out = {};
|
|
52
|
+
for (const [k, v] of Object.entries(val)) {
|
|
53
|
+
out[k] = sanitizeDeepValue(v, engine, sessionTokenMap);
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return val;
|
|
58
|
+
}
|
|
59
|
+
|
|
36
60
|
function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
37
61
|
if (!prompt) return prompt;
|
|
38
62
|
|
|
@@ -57,15 +81,23 @@ function sanitizePromptParts(prompt, engine, sessionTokenMap) {
|
|
|
57
81
|
// Multipart content: { role: 'user', content: [{ type: 'text', text: '...' }] }
|
|
58
82
|
if (Array.isArray(msg.content)) {
|
|
59
83
|
const newContent = msg.content.map(part => {
|
|
60
|
-
if (part && part.type === 'text' && typeof part.text === 'string') {
|
|
84
|
+
if (part && (part.type === 'text' || part.type === 'reasoning') && typeof part.text === 'string') {
|
|
61
85
|
const res = engine.sanitize(part.text, { existingSessionMap: sessionTokenMap });
|
|
62
86
|
Object.assign(sessionTokenMap, res.tokenMap);
|
|
63
87
|
return { ...part, text: res.scrubbedText };
|
|
64
88
|
}
|
|
65
|
-
if (part && part.type === 'tool-result'
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
if (part && part.type === 'tool-result') {
|
|
90
|
+
if (typeof part.result === 'string') {
|
|
91
|
+
const res = engine.sanitize(part.result, { existingSessionMap: sessionTokenMap });
|
|
92
|
+
Object.assign(sessionTokenMap, res.tokenMap);
|
|
93
|
+
return { ...part, result: res.scrubbedText };
|
|
94
|
+
}
|
|
95
|
+
if (part.result && typeof part.result === 'object') {
|
|
96
|
+
return { ...part, result: sanitizeDeepValue(part.result, engine, sessionTokenMap) };
|
|
97
|
+
}
|
|
98
|
+
if (part.output !== undefined) {
|
|
99
|
+
return { ...part, output: sanitizeDeepValue(part.output, engine, sessionTokenMap) };
|
|
100
|
+
}
|
|
69
101
|
}
|
|
70
102
|
return part;
|
|
71
103
|
});
|
|
@@ -145,6 +177,14 @@ function createPrivacyMiddleware(options = {}) {
|
|
|
145
177
|
if (typeof result.reasoning === 'string') {
|
|
146
178
|
result.reasoning = detokenizeText(result.reasoning, tokenMap);
|
|
147
179
|
}
|
|
180
|
+
if (Array.isArray(result.toolCalls)) {
|
|
181
|
+
result.toolCalls = result.toolCalls.map(tc => {
|
|
182
|
+
if (tc && typeof tc.args === 'string') {
|
|
183
|
+
return { ...tc, args: detokenizeText(tc.args, tokenMap) };
|
|
184
|
+
}
|
|
185
|
+
return tc;
|
|
186
|
+
});
|
|
187
|
+
}
|
|
148
188
|
}
|
|
149
189
|
|
|
150
190
|
return result;
|
|
@@ -335,11 +375,13 @@ function createDetokenizerTransformStream(tokenMap = {}, options = {}) {
|
|
|
335
375
|
*/
|
|
336
376
|
async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
337
377
|
const detok = new StreamingDetokenizer(tokenMap, options);
|
|
378
|
+
let isObjectStream = false;
|
|
338
379
|
for await (const chunk of stream) {
|
|
339
380
|
if (typeof chunk === 'string') {
|
|
340
381
|
const out = detok.feed(chunk);
|
|
341
382
|
if (out) yield out;
|
|
342
383
|
} else if (chunk && typeof chunk === 'object' && typeof chunk.textDelta === 'string') {
|
|
384
|
+
isObjectStream = true;
|
|
343
385
|
const out = detok.feed(chunk.textDelta);
|
|
344
386
|
if (out) yield { ...chunk, textDelta: out };
|
|
345
387
|
} else {
|
|
@@ -348,7 +390,11 @@ async function* detokenizeStream(stream, tokenMap = {}, options = {}) {
|
|
|
348
390
|
}
|
|
349
391
|
const rem = detok.flush();
|
|
350
392
|
if (rem) {
|
|
351
|
-
|
|
393
|
+
if (isObjectStream) {
|
|
394
|
+
yield { type: 'text-delta', textDelta: rem };
|
|
395
|
+
} else {
|
|
396
|
+
yield rem;
|
|
397
|
+
}
|
|
352
398
|
}
|
|
353
399
|
}
|
|
354
400
|
|
package/vision.cjs
CHANGED
|
@@ -64,7 +64,7 @@ function stripExifMetadata(inputBuffer) {
|
|
|
64
64
|
|
|
65
65
|
const length = (u8[offset + 2] << 8) | u8[offset + 3];
|
|
66
66
|
const segmentEnd = offset + 2 + length;
|
|
67
|
-
if (segmentEnd > u8.length) {
|
|
67
|
+
if (length < 2 || segmentEnd > u8.length) {
|
|
68
68
|
chunks.push(u8.subarray(offset));
|
|
69
69
|
break;
|
|
70
70
|
}
|
|
@@ -140,11 +140,11 @@ function stripExifMetadata(inputBuffer) {
|
|
|
140
140
|
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs', 'iCCP']);
|
|
141
141
|
|
|
142
142
|
while (offset + 8 <= u8.length) {
|
|
143
|
-
const length = (u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3];
|
|
143
|
+
const length = ((u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3]) >>> 0;
|
|
144
144
|
const type = String.fromCharCode(u8[offset + 4], u8[offset + 5], u8[offset + 6], u8[offset + 7]);
|
|
145
145
|
const chunkTotalLen = 12 + length; // 4 len + 4 type + data + 4 crc
|
|
146
146
|
|
|
147
|
-
if (offset + chunkTotalLen > u8.length) {
|
|
147
|
+
if (length > u8.length || offset + chunkTotalLen > u8.length) {
|
|
148
148
|
chunks.push(u8.subarray(offset));
|
|
149
149
|
break;
|
|
150
150
|
}
|
|
@@ -198,11 +198,11 @@ function stripExifMetadata(inputBuffer) {
|
|
|
198
198
|
|
|
199
199
|
while (offset + 8 <= u8.length) {
|
|
200
200
|
const fourCC = String.fromCharCode(u8[offset], u8[offset + 1], u8[offset + 2], u8[offset + 3]);
|
|
201
|
-
const chunkSize = u8[offset + 4] | (u8[offset + 5] << 8) | (u8[offset + 6] << 16) | (u8[offset + 7] << 24);
|
|
201
|
+
const chunkSize = (u8[offset + 4] | (u8[offset + 5] << 8) | (u8[offset + 6] << 16) | (u8[offset + 7] << 24)) >>> 0;
|
|
202
202
|
const paddedSize = chunkSize + (chunkSize % 2); // WebP chunks are 2-byte aligned
|
|
203
203
|
const totalChunkSize = 8 + paddedSize;
|
|
204
204
|
|
|
205
|
-
if (offset + totalChunkSize > u8.length) {
|
|
205
|
+
if (chunkSize > u8.length || offset + totalChunkSize > u8.length) {
|
|
206
206
|
chunks.push(u8.subarray(offset));
|
|
207
207
|
break;
|
|
208
208
|
}
|
|
@@ -338,16 +338,20 @@ function redactImageBoundingBoxes(canvasOrCtx, boxes = [], options = {}) {
|
|
|
338
338
|
* @returns {string} Standalone SVG markup
|
|
339
339
|
*/
|
|
340
340
|
function generateSvgRedactionOverlay(dimensions = { width: 800, height: 600 }, boxes = [], options = {}) {
|
|
341
|
-
const width = dimensions
|
|
342
|
-
const height = dimensions
|
|
343
|
-
const fillColor = options.fillColor || '#000000';
|
|
344
|
-
const strokeColor = options.borderColor || '#00f0ff';
|
|
341
|
+
const width = Number(dimensions?.width) || 800;
|
|
342
|
+
const height = Number(dimensions?.height) || 600;
|
|
343
|
+
const fillColor = escapeXml(options.fillColor || '#000000');
|
|
344
|
+
const strokeColor = escapeXml(options.borderColor || '#00f0ff');
|
|
345
345
|
|
|
346
346
|
const rects = (boxes || []).map((b, idx) => {
|
|
347
|
-
const
|
|
347
|
+
const bx = Number(b.x) || 0;
|
|
348
|
+
const by = Number(b.y) || 0;
|
|
349
|
+
const bw = Number(b.width) || 0;
|
|
350
|
+
const bh = Number(b.height) || 0;
|
|
351
|
+
const label = b.label ? `<text x="${bx + 5}" y="${by + 14}" font-family="monospace" font-size="11" font-weight="bold" fill="#00f0ff">${escapeXml(b.label)}</text>` : '';
|
|
348
352
|
return `
|
|
349
353
|
<g class="ps-vision-mask" id="mask-${idx}">
|
|
350
|
-
<rect x="${
|
|
354
|
+
<rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="${fillColor}" stroke="${strokeColor}" stroke-width="1.5" />
|
|
351
355
|
${label}
|
|
352
356
|
</g>`;
|
|
353
357
|
}).join('');
|
package/vision.js
CHANGED
|
@@ -64,7 +64,7 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
64
64
|
|
|
65
65
|
const length = (u8[offset + 2] << 8) | u8[offset + 3];
|
|
66
66
|
const segmentEnd = offset + 2 + length;
|
|
67
|
-
if (segmentEnd > u8.length) {
|
|
67
|
+
if (length < 2 || segmentEnd > u8.length) {
|
|
68
68
|
chunks.push(u8.subarray(offset));
|
|
69
69
|
break;
|
|
70
70
|
}
|
|
@@ -140,11 +140,11 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
140
140
|
const ANCILLARY_TYPES = new Set(['eXIf', 'tEXt', 'zTXt', 'iTXt', 'tIME', 'pHYs', 'iCCP']);
|
|
141
141
|
|
|
142
142
|
while (offset + 8 <= u8.length) {
|
|
143
|
-
const length = (u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3];
|
|
143
|
+
const length = ((u8[offset] << 24) | (u8[offset + 1] << 16) | (u8[offset + 2] << 8) | u8[offset + 3]) >>> 0;
|
|
144
144
|
const type = String.fromCharCode(u8[offset + 4], u8[offset + 5], u8[offset + 6], u8[offset + 7]);
|
|
145
145
|
const chunkTotalLen = 12 + length; // 4 len + 4 type + data + 4 crc
|
|
146
146
|
|
|
147
|
-
if (offset + chunkTotalLen > u8.length) {
|
|
147
|
+
if (length > u8.length || offset + chunkTotalLen > u8.length) {
|
|
148
148
|
chunks.push(u8.subarray(offset));
|
|
149
149
|
break;
|
|
150
150
|
}
|
|
@@ -198,11 +198,11 @@ export function stripExifMetadata(inputBuffer) {
|
|
|
198
198
|
|
|
199
199
|
while (offset + 8 <= u8.length) {
|
|
200
200
|
const fourCC = String.fromCharCode(u8[offset], u8[offset + 1], u8[offset + 2], u8[offset + 3]);
|
|
201
|
-
const chunkSize = u8[offset + 4] | (u8[offset + 5] << 8) | (u8[offset + 6] << 16) | (u8[offset + 7] << 24);
|
|
201
|
+
const chunkSize = (u8[offset + 4] | (u8[offset + 5] << 8) | (u8[offset + 6] << 16) | (u8[offset + 7] << 24)) >>> 0;
|
|
202
202
|
const paddedSize = chunkSize + (chunkSize % 2); // WebP chunks are 2-byte aligned
|
|
203
203
|
const totalChunkSize = 8 + paddedSize;
|
|
204
204
|
|
|
205
|
-
if (offset + totalChunkSize > u8.length) {
|
|
205
|
+
if (chunkSize > u8.length || offset + totalChunkSize > u8.length) {
|
|
206
206
|
chunks.push(u8.subarray(offset));
|
|
207
207
|
break;
|
|
208
208
|
}
|
|
@@ -338,16 +338,20 @@ export function redactImageBoundingBoxes(canvasOrCtx, boxes = [], options = {})
|
|
|
338
338
|
* @returns {string} Standalone SVG markup
|
|
339
339
|
*/
|
|
340
340
|
export function generateSvgRedactionOverlay(dimensions = { width: 800, height: 600 }, boxes = [], options = {}) {
|
|
341
|
-
const width = dimensions
|
|
342
|
-
const height = dimensions
|
|
343
|
-
const fillColor = options.fillColor || '#000000';
|
|
344
|
-
const strokeColor = options.borderColor || '#00f0ff';
|
|
341
|
+
const width = Number(dimensions?.width) || 800;
|
|
342
|
+
const height = Number(dimensions?.height) || 600;
|
|
343
|
+
const fillColor = escapeXml(options.fillColor || '#000000');
|
|
344
|
+
const strokeColor = escapeXml(options.borderColor || '#00f0ff');
|
|
345
345
|
|
|
346
346
|
const rects = (boxes || []).map((b, idx) => {
|
|
347
|
-
const
|
|
347
|
+
const bx = Number(b.x) || 0;
|
|
348
|
+
const by = Number(b.y) || 0;
|
|
349
|
+
const bw = Number(b.width) || 0;
|
|
350
|
+
const bh = Number(b.height) || 0;
|
|
351
|
+
const label = b.label ? `<text x="${bx + 5}" y="${by + 14}" font-family="monospace" font-size="11" font-weight="bold" fill="#00f0ff">${escapeXml(b.label)}</text>` : '';
|
|
348
352
|
return `
|
|
349
353
|
<g class="ps-vision-mask" id="mask-${idx}">
|
|
350
|
-
<rect x="${
|
|
354
|
+
<rect x="${bx}" y="${by}" width="${bw}" height="${bh}" fill="${fillColor}" stroke="${strokeColor}" stroke-width="1.5" />
|
|
351
355
|
${label}
|
|
352
356
|
</g>`;
|
|
353
357
|
}).join('');
|