@chatpanel/gateway 0.6.1 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Local privacy gateway — redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "node": ">=18"
28
28
  },
29
29
  "dependencies": {
30
- "@chatpanel/pii": "^0.2.5",
30
+ "@chatpanel/pii": "^0.2.6",
31
31
  "@huggingface/transformers": "^4.2.0",
32
32
  "onnxruntime-web": "1.26.0-dev.20260416-b7804b056c"
33
33
  },
package/src/ner-engine.js CHANGED
@@ -76,7 +76,8 @@ export function health() {
76
76
  export async function detect(text, { signal } = {}) {
77
77
  if (!isReady()) return [];
78
78
  if (signal?.aborted) return [];
79
- const out = await _pipe(String(text || ''), { aggregation_strategy: 'simple' });
79
+ const src = String(text || '');
80
+ const out = await _pipe(src, { aggregation_strategy: 'simple' });
80
81
  const ents = [];
81
82
  for (const e of out || []) {
82
83
  let word = String(e.word ?? '');
@@ -90,8 +91,37 @@ export async function detect(text, { signal } = {}) {
90
91
  if (!word) continue;
91
92
  ents.push({ value: word, type });
92
93
  }
93
- // Drop noise: a lone 1-char span that didn't get stitched is never useful PII.
94
- return ents.filter((e) => e.value.length > 1);
94
+ // Heal subword truncation AFTER merging (expanding before would corrupt a span the
95
+ // next "##" token still extends, e.g. "Sure"→"Suresh" then +"sh Kumar"). Then drop
96
+ // 1-char noise + dedupe (expansion can make two spans collide).
97
+ const seen = new Set();
98
+ const out2 = [];
99
+ for (const e of ents) {
100
+ const value = expandToWord(src, e.value.trim());
101
+ if (value.length <= 1) continue;
102
+ const k = `${e.type}:${value.toLowerCase()}`;
103
+ if (seen.has(k)) continue;
104
+ seen.add(k);
105
+ out2.push({ value, type: e.type });
106
+ }
107
+ return out2;
108
+ }
109
+
110
+ // Grow a detected span to its full surrounding word in the ORIGINAL text. A cased
111
+ // model can truncate a name to a subword ("Suresh" → "Sure"), which would leave a
112
+ // dangling fragment ("…[[PERSON]]sh") un-redacted — a leak. We extend across word
113
+ // characters at both edges so the whole token is captured. Multi-word spans are
114
+ // unaffected (their edges are already at spaces).
115
+ function expandToWord(text, value) {
116
+ if (!value) return value;
117
+ const idx = text.indexOf(value);
118
+ if (idx < 0) return value;
119
+ const isWord = (c) => !!c && /[\p{L}\p{N}'’-]/u.test(c);
120
+ let s = idx;
121
+ let e = idx + value.length;
122
+ while (s > 0 && isWord(text[s - 1])) s--;
123
+ while (e < text.length && isWord(text[e])) e++;
124
+ return text.slice(s, e);
95
125
  }
96
126
 
97
127
  // Lets @chatpanel/pii's existing `endpoint` detection backend call us with NO real
package/src/server.js CHANGED
@@ -35,7 +35,7 @@ import * as openai from './openai.js';
35
35
  import * as responses from './responses.js';
36
36
  import * as anthropic from './anthropic.js';
37
37
 
38
- export const VERSION = '0.6.1';
38
+ export const VERSION = '0.6.3';
39
39
 
40
40
  const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
41
41