@chatpanel/gateway 0.6.61 → 0.6.63
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 +17 -4
- package/src/server.js +42 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.63",
|
|
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.7.
|
|
30
|
+
"@chatpanel/pii": "^0.7.1",
|
|
31
31
|
"@huggingface/transformers": "^4.2.0",
|
|
32
32
|
"onnxruntime-web": "1.26.0-dev.20260416-b7804b056c",
|
|
33
33
|
"phonemizer": "^1.2.1"
|
package/src/redact.js
CHANGED
|
@@ -20,7 +20,14 @@ import * as engine from './ner-engine.js';
|
|
|
20
20
|
// quality — free requests get the full tier (names/orgs via NER) within their
|
|
21
21
|
// allowance. The custom dictionary is capped for free: gatedDictionary limits it
|
|
22
22
|
// to FREE_DICT_LIMIT via the shared chatpanel-pii gate.
|
|
23
|
-
export async function redactSegments(segments, redactionCfg, {
|
|
23
|
+
export async function redactSegments(segments, redactionCfg, {
|
|
24
|
+
signal, isPro = true, onEgress = null, fetchImpl: fetchOverride = null,
|
|
25
|
+
// The bundled detector, INJECTED rather than reached for. An ES module namespace cannot
|
|
26
|
+
// be monkey-patched, so with a hard import there is no way to ask "does an entity the
|
|
27
|
+
// engine found actually come out the other side as a token" without downloading a model.
|
|
28
|
+
// That question went unasked for exactly that reason, and the answer was no.
|
|
29
|
+
nerEngine = engine,
|
|
30
|
+
} = {}) {
|
|
24
31
|
const vault = createVault();
|
|
25
32
|
|
|
26
33
|
// De-steganography FIRST (before detection). Invisible/format Unicode is a triple
|
|
@@ -51,7 +58,7 @@ export async function redactSegments(segments, redactionCfg, { signal, isPro = t
|
|
|
51
58
|
// @chatpanel/pii's caching / timeout / type-gating — one source of truth.
|
|
52
59
|
const det = redactionCfg.detection;
|
|
53
60
|
const useExternal = !!(det && det.backend && det.backend !== 'off');
|
|
54
|
-
const useEngine = !useExternal &&
|
|
61
|
+
const useEngine = !useExternal && nerEngine.isReady();
|
|
55
62
|
|
|
56
63
|
let entities = [];
|
|
57
64
|
if (tier === 'full' && (useExternal || useEngine)) {
|
|
@@ -61,12 +68,18 @@ export async function redactSegments(segments, redactionCfg, { signal, isPro = t
|
|
|
61
68
|
// under a second, but a cold one must be allowed to finish; on timeout the turn
|
|
62
69
|
// falls back to dictionary/deterministic-only redaction.
|
|
63
70
|
const detection = useEngine
|
|
64
|
-
|
|
71
|
+
// `transport: 'in-process'` is load-bearing, not decoration. Without it the sentinel
|
|
72
|
+
// URL below fails @chatpanel/pii's SSRF scheme check, the throw is swallowed by the
|
|
73
|
+
// fail-open path, and the bundled engine contributes NOTHING to any redaction while
|
|
74
|
+
// reporting itself ready — names and organisations reach the model in full under a
|
|
75
|
+
// config that says "full". The flag is only honoured alongside an injected fetch,
|
|
76
|
+
// which is `engine.fetchAdapter` on the next line.
|
|
77
|
+
? { backend: 'endpoint', url: 'inproc:ner', transport: 'in-process', timeoutMs: 30000, maxChars: 8000, types: det?.types }
|
|
65
78
|
: { ...det, timeoutMs: Math.max(Number(det.timeoutMs) || 0, 30000) };
|
|
66
79
|
// The in-process engine is already injected this way; `fetchOverride` is the same seam
|
|
67
80
|
// for a test, so the detector hop can be exercised without a network. Never used in
|
|
68
81
|
// production — nothing passes it but tests.
|
|
69
|
-
const fetchImpl = useEngine ?
|
|
82
|
+
const fetchImpl = useEngine ? nerEngine.fetchAdapter : (fetchOverride || undefined);
|
|
70
83
|
try {
|
|
71
84
|
entities = await detectEntities(texts.join('\n\n'), { detection }, {
|
|
72
85
|
signal,
|
package/src/server.js
CHANGED
|
@@ -55,7 +55,7 @@ import * as openai from './openai.js';
|
|
|
55
55
|
import * as responses from './responses.js';
|
|
56
56
|
import * as anthropic from './anthropic.js';
|
|
57
57
|
|
|
58
|
-
export const VERSION = '0.6.
|
|
58
|
+
export const VERSION = '0.6.63';
|
|
59
59
|
|
|
60
60
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
61
61
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
|
@@ -907,6 +907,47 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
907
907
|
}
|
|
908
908
|
}
|
|
909
909
|
}
|
|
910
|
+
// WHAT THE MODEL WOULD RECEIVE. A client asks "if I sent this, what leaves the
|
|
911
|
+
// machine?" and gets back the redacted text plus the token→type map.
|
|
912
|
+
//
|
|
913
|
+
// WHY THIS IS A GATEWAY ROUTE AND NOT A CLIENT FUNCTION. A preview computed in the
|
|
914
|
+
// client is a SECOND redactor, and the day the two disagree the client is confidently
|
|
915
|
+
// showing the user something other than what was sent — which is worse than showing
|
|
916
|
+
// nothing, because it is believed. This runs the SAME redactSegments over the SAME
|
|
917
|
+
// config, tier, dictionary and detector as a real request, so it cannot drift: if the
|
|
918
|
+
// preview is wrong, the redaction is wrong too, and that is one bug rather than two.
|
|
919
|
+
//
|
|
920
|
+
// Real values NEVER appear in the response. The mapping is token→type only ('types'),
|
|
921
|
+
// never the 'values' detail an operator can opt into for their own logs: this answer
|
|
922
|
+
// crosses a process boundary to a UI, and the client already has the original text.
|
|
923
|
+
// The vault is discarded when this returns, so these tokens are not reusable.
|
|
924
|
+
if (pathname === '/redact' && req.method === 'POST') {
|
|
925
|
+
let text = '';
|
|
926
|
+
try { text = String(JSON.parse((await readBody(req, cfg.maxBodyBytes)).toString('utf8'))?.text || ''); }
|
|
927
|
+
catch { text = ''; }
|
|
928
|
+
if (!text) return sendJson(res, 200, { text: '', count: 0, sanitized: 0, entities: [] });
|
|
929
|
+
try {
|
|
930
|
+
let out = text;
|
|
931
|
+
const isPro = await resolvePro(cfg.pro?.entitlementToken);
|
|
932
|
+
const r = await redactSegments(
|
|
933
|
+
[segment(() => out, (v) => { out = v; })],
|
|
934
|
+
cfg.redaction,
|
|
935
|
+
{ isPro },
|
|
936
|
+
);
|
|
937
|
+
return sendJson(res, 200, {
|
|
938
|
+
text: out,
|
|
939
|
+
count: r.count || 0,
|
|
940
|
+
sanitized: r.sanitized || 0,
|
|
941
|
+
tier: cfg.redaction?.tier === 'full' ? 'full' : 'basic',
|
|
942
|
+
entities: redactionDetail(r.vault, 'types') || [],
|
|
943
|
+
});
|
|
944
|
+
} catch (e) {
|
|
945
|
+
// Fail LOUD. A preview that silently returns the original text would tell the user
|
|
946
|
+
// "nothing here is sensitive" at the exact moment redaction is broken.
|
|
947
|
+
return sendJson(res, 500, { error: { message: `redaction preview failed: ${e.message}`, type: 'redact_error' } });
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
910
951
|
// Model manager (the extension's Gateway settings drive these). GET lists the
|
|
911
952
|
// catalog with install state + live download progress; POST switches the active
|
|
912
953
|
// model (downloading it first if needed) and persists the choice.
|