@chatpanel/pii 0.2.14 → 0.2.15
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/package.json +2 -2
- package/pii-redact.js +44 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/pii",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "The canonical ChatPanel privacy engine
|
|
3
|
+
"version": "0.2.15",
|
|
4
|
+
"description": "The canonical ChatPanel privacy engine \u2014 reversible PII redaction + pseudonymization with local entity detection. Pure, dependency-free ESM shared by the ChatPanel extension, gateway, and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"exports": {
|
package/pii-redact.js
CHANGED
|
@@ -345,3 +345,47 @@ export function hasToken(text) {
|
|
|
345
345
|
TOKEN_RE.lastIndex = 0;
|
|
346
346
|
return TOKEN_RE.test(String(text || ''));
|
|
347
347
|
}
|
|
348
|
+
|
|
349
|
+
// ── What was redacted, for the user's own eyes ──────────────────────────────────────────
|
|
350
|
+
//
|
|
351
|
+
// The privacy promise is invisible unless you can SEE it: which entity types were caught,
|
|
352
|
+
// how many of each, and (on request) the actual before → after pairs. All of that already
|
|
353
|
+
// exists in the vault — this just summarises it, so every client renders the same shape
|
|
354
|
+
// instead of each one re-deriving it.
|
|
355
|
+
//
|
|
356
|
+
// PRIVACY OF THE SUMMARY ITSELF: `types` carries counts only, never values, so it is safe
|
|
357
|
+
// to render, log or persist. Real values live behind `pairs`, which a caller must ask for
|
|
358
|
+
// explicitly (`includeValues: true`) — they are the user's own data, shown on their own
|
|
359
|
+
// device, and must never be written anywhere durable.
|
|
360
|
+
|
|
361
|
+
/** Entity type + count for everything this vault redacted, most-frequent first. */
|
|
362
|
+
export function redactionSummary(vault, { includeValues = false, maxPairs = 200 } = {}) {
|
|
363
|
+
const byType = new Map();
|
|
364
|
+
const pairs = [];
|
|
365
|
+
for (const [token, value] of vault?.byToken || new Map()) {
|
|
366
|
+
const m = /^\[\[([A-Z][A-Z0-9]*)_(\d+)\]\]$/.exec(token);
|
|
367
|
+
const type = m ? m[1] : 'OTHER';
|
|
368
|
+
byType.set(type, (byType.get(type) || 0) + 1);
|
|
369
|
+
if (includeValues && pairs.length < maxPairs) pairs.push({ token, value, type });
|
|
370
|
+
}
|
|
371
|
+
const types = [...byType.entries()]
|
|
372
|
+
.map(([type, count]) => ({ type, count }))
|
|
373
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
374
|
+
return {
|
|
375
|
+
total: types.reduce((n, t) => n + t.count, 0),
|
|
376
|
+
types,
|
|
377
|
+
...(includeValues ? { pairs } : {}),
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Merge several vault summaries (e.g. every conversation) into one. Counts only. */
|
|
382
|
+
export function mergeRedactionSummaries(summaries) {
|
|
383
|
+
const byType = new Map();
|
|
384
|
+
for (const s of summaries || []) {
|
|
385
|
+
for (const t of s?.types || []) byType.set(t.type, (byType.get(t.type) || 0) + t.count);
|
|
386
|
+
}
|
|
387
|
+
const types = [...byType.entries()]
|
|
388
|
+
.map(([type, count]) => ({ type, count }))
|
|
389
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type));
|
|
390
|
+
return { total: types.reduce((n, t) => n + t.count, 0), types };
|
|
391
|
+
}
|