@chatpanel/gateway 0.4.2 → 0.4.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 +1 -1
- package/src/server.js +52 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.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": {
|
package/src/server.js
CHANGED
|
@@ -33,7 +33,7 @@ import * as openai from './openai.js';
|
|
|
33
33
|
import * as responses from './responses.js';
|
|
34
34
|
import * as anthropic from './anthropic.js';
|
|
35
35
|
|
|
36
|
-
export const VERSION = '0.4.
|
|
36
|
+
export const VERSION = '0.4.3';
|
|
37
37
|
|
|
38
38
|
const KNOWN_AGENTS = new Set(['codex', 'claude', 'opencode', 'pi', 'kiro', 'antigravity']);
|
|
39
39
|
|
|
@@ -150,6 +150,28 @@ function sendJson(res, status, obj) {
|
|
|
150
150
|
res.end(JSON.stringify(obj));
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
// The detector URL (…:9009/ner) and its sibling /health. Returns null when no
|
|
154
|
+
// detector is wired (deterministic-only).
|
|
155
|
+
function nerBaseUrl(cfg) {
|
|
156
|
+
const url = cfg.redaction?.detection?.url;
|
|
157
|
+
if (!url || cfg.redaction?.detection?.backend === 'off') return null;
|
|
158
|
+
return url;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Live health probe of the bundled/configured NER (GET /health → { ok, model }).
|
|
162
|
+
async function probeNerHealth(cfg) {
|
|
163
|
+
const url = nerBaseUrl(cfg);
|
|
164
|
+
if (!url) return { configured: false, ok: false, url: null, model: null };
|
|
165
|
+
try {
|
|
166
|
+
const r = await fetch(url.replace(/\/ner\/?$/, '') + '/health', { signal: AbortSignal.timeout(2000) });
|
|
167
|
+
if (!r.ok) return { configured: true, ok: false, url, model: null };
|
|
168
|
+
const j = await r.json().catch(() => ({}));
|
|
169
|
+
return { configured: true, ok: true, url, model: j.model || null };
|
|
170
|
+
} catch {
|
|
171
|
+
return { configured: true, ok: false, url, model: null };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
153
175
|
function forwardHeaders(headers, base) {
|
|
154
176
|
const out = {};
|
|
155
177
|
for (const [k, v] of Object.entries(headers)) {
|
|
@@ -330,14 +352,41 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
330
352
|
// --- Config API (the extension's "Gateway" tab is a client of these) ---
|
|
331
353
|
if (pathname === '/status' && req.method === 'GET') {
|
|
332
354
|
const proUnlocked = await resolvePro(cfg.pro?.entitlementToken);
|
|
333
|
-
const
|
|
355
|
+
const health = await probeNerHealth(cfg); // live GET /health on the detector
|
|
334
356
|
return sendJson(res, 200, {
|
|
335
357
|
ok: true, version: VERSION, backend: cfg.backend, tier: cfg.redaction.tier,
|
|
336
|
-
ner: {
|
|
358
|
+
ner: {
|
|
359
|
+
autostart: !!cfg.ner?.autostart,
|
|
360
|
+
configured: health.configured,
|
|
361
|
+
ready: health.ok, // the detector actually answered /health
|
|
362
|
+
model: health.model, // e.g. "en_core_web_sm"
|
|
363
|
+
url: health.url,
|
|
364
|
+
},
|
|
337
365
|
pro: { unlocked: proUnlocked }, usage: usage(cfg),
|
|
338
366
|
uptimeSeconds: Math.floor((Date.now() - STARTED_AT) / 1000),
|
|
339
367
|
});
|
|
340
368
|
}
|
|
369
|
+
// Proxy the detector so it's checkable on the gateway's own port (the NER runs
|
|
370
|
+
// on its own port, e.g. 9009 — hitting :4320/ner used to 404). GET → health.
|
|
371
|
+
if (pathname === '/ner') {
|
|
372
|
+
const url = nerBaseUrl(cfg);
|
|
373
|
+
if (!url) return sendJson(res, 503, { error: { message: 'NER not configured — deterministic-only redaction', type: 'ner_off' } });
|
|
374
|
+
if (req.method === 'GET') {
|
|
375
|
+
const health = await probeNerHealth(cfg);
|
|
376
|
+
return sendJson(res, health.ok ? 200 : 503, health);
|
|
377
|
+
}
|
|
378
|
+
if (req.method === 'POST') {
|
|
379
|
+
try {
|
|
380
|
+
const body = await readBody(req, cfg.maxBodyBytes);
|
|
381
|
+
const r = await fetch(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body, signal: AbortSignal.timeout(8000) });
|
|
382
|
+
const text = await r.text();
|
|
383
|
+
res.writeHead(r.status, { 'content-type': 'application/json' });
|
|
384
|
+
return res.end(text);
|
|
385
|
+
} catch (e) {
|
|
386
|
+
return sendJson(res, 502, { error: { message: `NER unreachable: ${e.message}`, type: 'ner_unreachable' } });
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
341
390
|
if (pathname === '/logs' && req.method === 'GET') {
|
|
342
391
|
return sendJson(res, 200, { entries: [...recentRequests].reverse() }); // newest first; counts only
|
|
343
392
|
}
|