@solongate/proxy 0.50.0 → 0.52.0
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/dist/global-install.d.ts +2 -0
- package/dist/global-install.js +79 -0
- package/dist/index.js +408 -145
- package/dist/login.js +68 -0
- package/dist/shield.d.ts +1 -0
- package/dist/shield.js +171 -0
- package/hooks/guard.bundled.mjs +7740 -7740
- package/hooks/shield.mjs +144 -0
- package/package.json +1 -1
package/hooks/shield.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SolonGate Shield (standalone hook) — secret redaction on the LLM request path.
|
|
4
|
+
*
|
|
5
|
+
* Wraps a command (the real `claude`) with a local proxy that masks secrets in
|
|
6
|
+
* the request body BEFORE it reaches the Anthropic API, so the model never sees
|
|
7
|
+
* them. The model's RESPONSE is streamed back untouched. Lifecycle is tied to
|
|
8
|
+
* the wrapped process — no daemon. FAIL OPEN everywhere: any parse/redact/upstream
|
|
9
|
+
* error forwards the original bytes rather than breaking the model connection.
|
|
10
|
+
*
|
|
11
|
+
* Installed to ~/.solongate/hooks/shield.mjs and invoked by the `claude` shim
|
|
12
|
+
* that `login` adds to the shell so every terminal session is masked automatically:
|
|
13
|
+
* node ~/.solongate/hooks/shield.mjs -- "<real claude>" <args...>
|
|
14
|
+
*
|
|
15
|
+
* Logic mirrors src/shield.ts — keep the two in sync.
|
|
16
|
+
*/
|
|
17
|
+
import { createServer, request as httpRequest } from 'node:http';
|
|
18
|
+
import { request as httpsRequest } from 'node:https';
|
|
19
|
+
import { spawn } from 'node:child_process';
|
|
20
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
21
|
+
import { resolve } from 'node:path';
|
|
22
|
+
import { homedir } from 'node:os';
|
|
23
|
+
|
|
24
|
+
const log = (...a) => process.stderr.write(`[SolonGate shield] ${a.map(String).join(' ')}\n`);
|
|
25
|
+
|
|
26
|
+
const DLP_PATTERNS = [
|
|
27
|
+
{ name: 'AWS access key', re: /AKIA[0-9A-Z]{16}/g },
|
|
28
|
+
{ name: 'private key block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
|
|
29
|
+
{ name: 'Anthropic key', re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
|
|
30
|
+
{ name: 'OpenAI key', re: /sk-(proj-)?[A-Za-z0-9_-]{20,}/g },
|
|
31
|
+
{ name: 'GitHub token', re: /gh[pousr]_[A-Za-z0-9]{20,}/g },
|
|
32
|
+
{ name: 'GitHub fine-grained PAT', re: /github_pat_[A-Za-z0-9_]{20,}/g },
|
|
33
|
+
{ name: 'GitLab token', re: /glpat-[A-Za-z0-9_-]{20,}/g },
|
|
34
|
+
{ name: 'Slack token', re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
|
|
35
|
+
{ name: 'Google API key', re: /AIza[0-9A-Za-z_-]{35}/g },
|
|
36
|
+
{ name: 'Stripe key', re: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/g },
|
|
37
|
+
{ name: 'SendGrid key', re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
|
|
38
|
+
{ name: 'Twilio key', re: /SK[0-9a-fA-F]{32}/g },
|
|
39
|
+
{ name: 'npm token', re: /npm_[A-Za-z0-9]{36}/g },
|
|
40
|
+
{ name: 'JWT', re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
|
|
41
|
+
{ name: 'Bearer token', re: /bearer\s+[A-Za-z0-9._-]{20,}/gi },
|
|
42
|
+
{ name: 'secret assignment', re: /(api[_-]?key|secret|token|password|passwd|access[_-]?key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+_.-]{12,}/gi },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
function loadCfg() {
|
|
46
|
+
try {
|
|
47
|
+
const sel = (process.env.SOLONGATE_AGENT_ID || 'default').replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
48
|
+
const f = resolve(homedir(), '.solongate', '.policy-cache-' + sel + '.json');
|
|
49
|
+
if (existsSync(f)) {
|
|
50
|
+
const c = JSON.parse(readFileSync(f, 'utf-8'));
|
|
51
|
+
const d = c && c.security && c.security.dlpRedact;
|
|
52
|
+
if (d && Array.isArray(d.patterns)) return { patterns: d.patterns, custom: Array.isArray(d.custom) ? d.custom : [] };
|
|
53
|
+
}
|
|
54
|
+
} catch { /* default below */ }
|
|
55
|
+
return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [] };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function redactString(s, cfg) {
|
|
59
|
+
if (!cfg || typeof s !== 'string' || !s) return s;
|
|
60
|
+
const allow = new Set(cfg.patterns);
|
|
61
|
+
let out = s;
|
|
62
|
+
for (const p of DLP_PATTERNS) if (allow.has(p.name)) out = out.replace(p.re, `[REDACTED: ${p.name}]`);
|
|
63
|
+
for (const c of cfg.custom) {
|
|
64
|
+
try { out = out.replace(new RegExp(c.re, 'g'), `[REDACTED: ${c.name || 'custom'}]`); } catch { /* skip */ }
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function redactDeep(value, cfg) {
|
|
70
|
+
if (typeof value === 'string') return redactString(value, cfg);
|
|
71
|
+
if (Array.isArray(value)) return value.map((v) => redactDeep(v, cfg));
|
|
72
|
+
if (value && typeof value === 'object') {
|
|
73
|
+
const out = {};
|
|
74
|
+
for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, cfg);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pickUpstream() {
|
|
81
|
+
const raw = process.env.SOLONGATE_SHIELD_UPSTREAM || process.env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com';
|
|
82
|
+
try { return new URL(raw); } catch { return new URL('https://api.anthropic.com'); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function startProxy(upstream) {
|
|
86
|
+
const cfg = loadCfg();
|
|
87
|
+
const forward = upstream.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
88
|
+
const server = createServer((req, res) => {
|
|
89
|
+
const chunks = [];
|
|
90
|
+
req.on('data', (c) => chunks.push(c));
|
|
91
|
+
req.on('end', () => {
|
|
92
|
+
let body = Buffer.concat(chunks);
|
|
93
|
+
try {
|
|
94
|
+
if (body.length && String(req.headers['content-type'] || '').includes('json')) {
|
|
95
|
+
const parsed = JSON.parse(body.toString('utf-8'));
|
|
96
|
+
body = Buffer.from(JSON.stringify(redactDeep(parsed, cfg)), 'utf-8');
|
|
97
|
+
}
|
|
98
|
+
} catch { /* fail open */ }
|
|
99
|
+
const headers = { ...req.headers };
|
|
100
|
+
delete headers['host']; delete headers['content-length']; delete headers['accept-encoding'];
|
|
101
|
+
headers['content-length'] = String(body.length);
|
|
102
|
+
const upReq = forward({
|
|
103
|
+
protocol: upstream.protocol,
|
|
104
|
+
hostname: upstream.hostname,
|
|
105
|
+
port: upstream.port || (upstream.protocol === 'https:' ? 443 : 80),
|
|
106
|
+
method: req.method,
|
|
107
|
+
path: req.url,
|
|
108
|
+
headers: { ...headers, host: upstream.host },
|
|
109
|
+
}, (upRes) => { res.writeHead(upRes.statusCode || 502, upRes.headers); upRes.pipe(res); });
|
|
110
|
+
upReq.on('error', (e) => {
|
|
111
|
+
log('upstream error:', e.message);
|
|
112
|
+
if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain' });
|
|
113
|
+
res.end('shield upstream error');
|
|
114
|
+
});
|
|
115
|
+
upReq.end(body);
|
|
116
|
+
});
|
|
117
|
+
req.on('error', () => { try { res.destroy(); } catch { /* ignore */ } });
|
|
118
|
+
});
|
|
119
|
+
return new Promise((resolveP) => {
|
|
120
|
+
server.listen(0, '127.0.0.1', () => {
|
|
121
|
+
const addr = server.address();
|
|
122
|
+
resolveP({ port: addr && typeof addr === 'object' ? addr.port : 0, close: () => server.close() });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function main() {
|
|
128
|
+
const sep = process.argv.indexOf('--');
|
|
129
|
+
const cmd = sep !== -1 ? process.argv.slice(sep + 1) : [];
|
|
130
|
+
if (cmd.length === 0) { log('usage: node shield.mjs -- <command> [args...]'); process.exit(1); }
|
|
131
|
+
const upstream = pickUpstream();
|
|
132
|
+
const { port, close } = await startProxy(upstream);
|
|
133
|
+
const child = spawn(cmd[0], cmd.slice(1), {
|
|
134
|
+
stdio: 'inherit',
|
|
135
|
+
env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
|
|
136
|
+
shell: process.platform === 'win32',
|
|
137
|
+
});
|
|
138
|
+
const shutdown = () => { try { close(); } catch { /* ignore */ } };
|
|
139
|
+
child.on('exit', (code, signal) => { shutdown(); if (signal) process.kill(process.pid, signal); else process.exit(code ?? 0); });
|
|
140
|
+
child.on('error', (e) => { log('failed to launch command:', e.message); shutdown(); process.exit(1); });
|
|
141
|
+
for (const sig of ['SIGINT', 'SIGTERM']) process.on(sig, () => { try { child.kill(sig); } catch { /* ignore */ } });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
main();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.52.0",
|
|
4
4
|
"description": "AI tool security proxy — protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|