@chatpanel/gateway 0.6.6 → 0.6.9

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.6",
3
+ "version": "0.6.9",
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.9",
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/log.js ADDED
@@ -0,0 +1,21 @@
1
+ // Timestamped console. Gateway logs (NER lifecycle, per-request redaction lines,
2
+ // entitlement refresh) are useless for debugging without a clock — prefix every
3
+ // console line with a local YYYY-MM-DD HH:MM:SS stamp. Call once at startup,
4
+ // before anything logs. Idempotent (won't double-wrap if called twice).
5
+
6
+ let installed = false;
7
+
8
+ function stamp(d = new Date()) {
9
+ const p = (n, w = 2) => String(n).padStart(w, '0');
10
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} `
11
+ + `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
12
+ }
13
+
14
+ export function installTimestampedConsole() {
15
+ if (installed) return;
16
+ installed = true;
17
+ for (const method of ['log', 'info', 'warn', 'error']) {
18
+ const orig = console[method].bind(console);
19
+ console[method] = (...args) => orig(`[${stamp()}]`, ...args);
20
+ }
21
+ }
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
@@ -27,6 +27,7 @@ import { streamBridgeChat, readBridgeToken, openBridgeChat } from './bridge.js';
27
27
  import { createRelaySession, getRelaySession, endRelaySession, pumpBridgeStream, deliverToolResult, toolsToSpecs, parseToolCallId } from './toolrelay.js';
28
28
  import { shaperFor } from './shape.js';
29
29
  import { startNer } from './ner.js';
30
+ import { installTimestampedConsole } from './log.js';
30
31
  import * as nerEngine from './ner-engine.js';
31
32
  import { MODEL_CATALOG, isKnownModel } from './models.js';
32
33
  import { resolvePro, checkQuota, consume, usage } from './freegate.js';
@@ -36,7 +37,7 @@ import * as openai from './openai.js';
36
37
  import * as responses from './responses.js';
37
38
  import * as anthropic from './anthropic.js';
38
39
 
39
- export const VERSION = '0.6.6';
40
+ export const VERSION = '0.6.9';
40
41
 
41
42
  const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
42
43
 
@@ -137,7 +138,7 @@ function mkTrace(sink) {
137
138
  const entry = /** @type {any} */ ({ ...this.meta, timings });
138
139
  setImmediate(() => {
139
140
  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)}`);
141
+ 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
142
  });
142
143
  },
143
144
  };
@@ -613,6 +614,7 @@ export function createGateway(cfg = loadConfig()) {
613
614
  let body = null;
614
615
  let outBody = raw;
615
616
  let redactedCount = 0;
617
+ let sanitizedCount = 0;
616
618
  let narrowedTools = 0;
617
619
  let isPro = true;
618
620
  // Off the hot path: only build a trace when logging is on, so it adds nothing
@@ -656,10 +658,11 @@ export function createGateway(cfg = loadConfig()) {
656
658
  // Redact at the configured tier for everyone (free users get genuine
657
659
  // name/org redaction within their allowance, not a downgraded preview);
658
660
  // 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 });
661
+ const { vault: v, count, sanitized } = await redactSegments(segs, cfg.redaction, { signal: ac.signal, isPro });
660
662
  if (trace) trace.lap('redact', rd0);
661
663
  vault = v;
662
664
  redactedCount = count;
665
+ sanitizedCount = sanitized || 0;
663
666
  // Burn one lifetime free credit only when we actually redacted something,
664
667
  // then persist so the count survives a restart. (No-op / no write for Pro.)
665
668
  if (!isPro && count > 0) {
@@ -687,7 +690,7 @@ export function createGateway(cfg = loadConfig()) {
687
690
  // API we forward to). Falls back to the legacy backend when none configured.
688
691
  const dest = resolveDestination(body?.model, cfg, r.kind);
689
692
  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) };
693
+ 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
694
  }
692
695
  if (dest && dest.type === 'api') {
693
696
  if (!dest.baseUrl) { trace?.commit(); return sendJson(res, 502, { error: `destination "${dest.id}" has no baseUrl` }); }
@@ -702,6 +705,7 @@ export function createGateway(cfg = loadConfig()) {
702
705
  }
703
706
 
704
707
  export function start(cfg = loadConfig()) {
708
+ installTimestampedConsole(); // every gateway log line gets a clock — before anything logs
705
709
  const server = createGateway(cfg);
706
710
  const ner = startNer(cfg); // may mutate cfg.redaction when it comes up
707
711
  // Re-validate the stored Pro entitlement online on an interval, so a refunded /