@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.
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' && typeof part.result === 'string') {
66
- const res = engine.sanitize(part.result, { existingSessionMap: sessionTokenMap });
67
- Object.assign(sessionTokenMap, res.tokenMap);
68
- return { ...part, result: res.scrubbedText };
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
- yield rem;
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.width || 800;
342
- const height = dimensions.height || 600;
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 label = b.label ? `<text x="${b.x + 5}" y="${b.y + 14}" font-family="monospace" font-size="11" font-weight="bold" fill="#00f0ff">${escapeXml(b.label)}</text>` : '';
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="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" fill="${fillColor}" stroke="${strokeColor}" stroke-width="1.5" />
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.width || 800;
342
- const height = dimensions.height || 600;
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 label = b.label ? `<text x="${b.x + 5}" y="${b.y + 14}" font-family="monospace" font-size="11" font-weight="bold" fill="#00f0ff">${escapeXml(b.label)}</text>` : '';
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="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" fill="${fillColor}" stroke="${strokeColor}" stroke-width="1.5" />
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('');