@chatpanel/gateway 0.6.6 → 0.6.8
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/src/redact.js +18 -3
- package/src/server.js +6 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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.
|
|
30
|
+
"@chatpanel/pii": "^0.2.10",
|
|
31
31
|
"@huggingface/transformers": "^4.2.0",
|
|
32
32
|
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c"
|
|
33
33
|
},
|
package/src/redact.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// mapping is self-consistent within the request. (Same reasoning as the
|
|
9
9
|
// extension's pii-pipeline.)
|
|
10
10
|
|
|
11
|
-
import { createVault, redactText, detectEntities, gatedDictionary } from '@chatpanel/pii';
|
|
11
|
+
import { createVault, redactText, detectEntities, gatedDictionary, sanitizeUnicode } from '@chatpanel/pii';
|
|
12
12
|
import * as engine from './ner-engine.js';
|
|
13
13
|
|
|
14
14
|
// tier: 'basic' | 'full'. For 'full' we run the local detector over the combined
|
|
@@ -20,8 +20,23 @@ import * as engine from './ner-engine.js';
|
|
|
20
20
|
// still a Pro power feature: gatedDictionary caps it to FREE_DICT_LIMIT for free.
|
|
21
21
|
export async function redactSegments(segments, redactionCfg, { signal, isPro = true } = {}) {
|
|
22
22
|
const vault = createVault();
|
|
23
|
+
|
|
24
|
+
// De-steganography FIRST (before detection). Invisible/format Unicode is a triple
|
|
25
|
+
// threat at this boundary: it can split a value so the detector misses it and the
|
|
26
|
+
// model reassembles real PII (redaction bypass), smuggle a hidden instruction via
|
|
27
|
+
// Tag chars (ASCII smuggling), or carry a fingerprint/watermark a client injected.
|
|
28
|
+
// We strip it in place so detection sees clean text and the forwarded request is
|
|
29
|
+
// clean too. Counted (not silently dropped) so the server can report it.
|
|
30
|
+
let sanitized = 0;
|
|
31
|
+
for (const seg of segments) {
|
|
32
|
+
const before = seg.get();
|
|
33
|
+
if (typeof before !== 'string' || !before) continue;
|
|
34
|
+
const { clean, removed } = sanitizeUnicode(before);
|
|
35
|
+
if (removed) { seg.set(clean); sanitized += removed; }
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
const texts = segments.map((s) => s.get()).filter((t) => typeof t === 'string' && t);
|
|
24
|
-
if (texts.length === 0) return { vault, count: 0 };
|
|
39
|
+
if (texts.length === 0) return { vault, count: 0, sanitized };
|
|
25
40
|
|
|
26
41
|
// Use the configured tier as-is (no free downgrade — the quota is the free gate),
|
|
27
42
|
// but keep the dictionary capped for free via the shared chatpanel-pii gate.
|
|
@@ -64,7 +79,7 @@ export async function redactSegments(segments, redactionCfg, { signal, isPro = t
|
|
|
64
79
|
if (after !== before) count++;
|
|
65
80
|
seg.set(after);
|
|
66
81
|
}
|
|
67
|
-
return { vault, count };
|
|
82
|
+
return { vault, count, sanitized };
|
|
68
83
|
}
|
|
69
84
|
|
|
70
85
|
// A `segment` is a tiny getter/setter over wherever the text lives in the parsed
|
package/src/server.js
CHANGED
|
@@ -36,7 +36,7 @@ import * as openai from './openai.js';
|
|
|
36
36
|
import * as responses from './responses.js';
|
|
37
37
|
import * as anthropic from './anthropic.js';
|
|
38
38
|
|
|
39
|
-
export const VERSION = '0.6.
|
|
39
|
+
export const VERSION = '0.6.8';
|
|
40
40
|
|
|
41
41
|
const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
|
|
42
42
|
|
|
@@ -137,7 +137,7 @@ function mkTrace(sink) {
|
|
|
137
137
|
const entry = /** @type {any} */ ({ ...this.meta, timings });
|
|
138
138
|
setImmediate(() => {
|
|
139
139
|
sink(entry);
|
|
140
|
-
console.log(`[gateway] model=${entry.model || '-'} → ${entry.dest ? `${entry.dest}(${entry.type})` : 'none'} · redacted ${entry.redacted || 0}${entry.narrowed ? ` · narrowed -${entry.narrowed}` : ''} · ${fmtTimings(timings)}`);
|
|
140
|
+
console.log(`[gateway] model=${entry.model || '-'} → ${entry.dest ? `${entry.dest}(${entry.type})` : 'none'} · redacted ${entry.redacted || 0}${entry.sanitized ? ` · scrubbed ${entry.sanitized} hidden` : ''}${entry.narrowed ? ` · narrowed -${entry.narrowed}` : ''} · ${fmtTimings(timings)}`);
|
|
141
141
|
});
|
|
142
142
|
},
|
|
143
143
|
};
|
|
@@ -613,6 +613,7 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
613
613
|
let body = null;
|
|
614
614
|
let outBody = raw;
|
|
615
615
|
let redactedCount = 0;
|
|
616
|
+
let sanitizedCount = 0;
|
|
616
617
|
let narrowedTools = 0;
|
|
617
618
|
let isPro = true;
|
|
618
619
|
// Off the hot path: only build a trace when logging is on, so it adds nothing
|
|
@@ -656,10 +657,11 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
656
657
|
// Redact at the configured tier for everyone (free users get genuine
|
|
657
658
|
// name/org redaction within their allowance, not a downgraded preview);
|
|
658
659
|
// the custom dictionary stays capped for free (isPro decides that inside).
|
|
659
|
-
const { vault: v, count } = await redactSegments(segs, cfg.redaction, { signal: ac.signal, isPro });
|
|
660
|
+
const { vault: v, count, sanitized } = await redactSegments(segs, cfg.redaction, { signal: ac.signal, isPro });
|
|
660
661
|
if (trace) trace.lap('redact', rd0);
|
|
661
662
|
vault = v;
|
|
662
663
|
redactedCount = count;
|
|
664
|
+
sanitizedCount = sanitized || 0;
|
|
663
665
|
// Burn one lifetime free credit only when we actually redacted something,
|
|
664
666
|
// then persist so the count survives a restart. (No-op / no write for Pro.)
|
|
665
667
|
if (!isPro && count > 0) {
|
|
@@ -687,7 +689,7 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
687
689
|
// API we forward to). Falls back to the legacy backend when none configured.
|
|
688
690
|
const dest = resolveDestination(body?.model, cfg, r.kind);
|
|
689
691
|
if (trace) {
|
|
690
|
-
trace.meta = { t: Date.now(), model: body?.model || null, dest: dest ? dest.id : null, type: dest ? dest.type : null, redacted: redactedCount, narrowed: narrowedTools, detail: redactionDetail(vault, cfg.logDetail) };
|
|
692
|
+
trace.meta = { t: Date.now(), model: body?.model || null, dest: dest ? dest.id : null, type: dest ? dest.type : null, redacted: redactedCount, sanitized: sanitizedCount, narrowed: narrowedTools, detail: redactionDetail(vault, cfg.logDetail) };
|
|
691
693
|
}
|
|
692
694
|
if (dest && dest.type === 'api') {
|
|
693
695
|
if (!dest.baseUrl) { trace?.commit(); return sendJson(res, 502, { error: `destination "${dest.id}" has no baseUrl` }); }
|