@solongate/proxy 0.82.19 → 0.82.21

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": "@solongate/proxy",
3
- "version": "0.82.19",
3
+ "version": "0.82.21",
4
4
  "description": "AI tool security proxy: protect any AI tool server with customizable policies, path/command constraints, rate limiting, and audit logging. No code changes required.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +0,0 @@
1
- export declare function run(argv: string[]): Promise<number>;
@@ -1 +0,0 @@
1
- export declare function run(argv: string[]): Promise<number>;
package/dist/shield.d.ts DELETED
@@ -1 +0,0 @@
1
- export declare function runShield(): Promise<void>;
package/dist/shield.js DELETED
@@ -1,314 +0,0 @@
1
- // src/shield.ts
2
- import { createServer, request as httpRequest } from "http";
3
- import { request as httpsRequest } from "https";
4
- import { spawn } from "child_process";
5
- import { URL } from "url";
6
- import { readFileSync, existsSync, readdirSync, statSync } from "fs";
7
- import { resolve } from "path";
8
- import { homedir } from "os";
9
- var log = (...a) => process.stderr.write(`[SolonGate shield] ${a.map(String).join(" ")}
10
- `);
11
- var DLP_PATTERNS = [
12
- { name: "AWS access key", re: /AKIA[0-9A-Z]{16}/g },
13
- { name: "Private key block", re: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----|-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
14
- { name: "Anthropic key", re: /sk-ant-[A-Za-z0-9_-]{20,}/g },
15
- { name: "OpenAI key", re: /sk-(proj-)?[A-Za-z0-9_-]{20,}/g },
16
- { name: "GitHub token", re: /gh[pousr]_[A-Za-z0-9]{20,}/g },
17
- { name: "GitHub fine-grained PAT", re: /github_pat_[A-Za-z0-9_]{20,}/g },
18
- { name: "GitLab token", re: /glpat-[A-Za-z0-9_-]{20,}/g },
19
- { name: "Slack token", re: /xox[baprs]-[A-Za-z0-9-]{10,}/g },
20
- { name: "Stripe key", re: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/g },
21
- { name: "SendGrid key", re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
22
- { name: "Twilio key", re: /SK[0-9a-fA-F]{32}/g },
23
- { name: "npm token", re: /npm_[A-Za-z0-9]{36}/g },
24
- { name: "JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
25
- { name: "Bearer token", re: /bearer\s+[A-Za-z0-9._-]{20,}/gi }
26
- ];
27
- function findCacheFile() {
28
- const dir = resolve(homedir(), ".solongate");
29
- const envSel = process.env.SOLONGATE_AGENT_ID;
30
- if (envSel) {
31
- const f = resolve(dir, ".policy-cache-" + envSel.replace(/[^a-zA-Z0-9_-]/g, "_") + ".json");
32
- if (existsSync(f)) return f;
33
- }
34
- let best = null, bestTs = -1;
35
- try {
36
- for (const name of readdirSync(dir)) {
37
- if (name.startsWith(".policy-cache-") && name.endsWith(".json")) {
38
- const full = resolve(dir, name);
39
- const ts = statSync(full).mtimeMs;
40
- if (ts > bestTs) {
41
- bestTs = ts;
42
- best = full;
43
- }
44
- }
45
- }
46
- } catch {
47
- }
48
- return best;
49
- }
50
- function loadCfg() {
51
- try {
52
- const f = findCacheFile();
53
- if (f && existsSync(f)) {
54
- const c = JSON.parse(readFileSync(f, "utf-8"));
55
- const d = c?.security?.dlpRedact;
56
- const g = c?.security?.ghost;
57
- const ghost = g && Array.isArray(g.patterns) ? g.patterns : [];
58
- if (d && Array.isArray(d.patterns)) return { patterns: d.patterns, custom: Array.isArray(d.custom) ? d.custom : [], ghost };
59
- return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [], ghost };
60
- }
61
- } catch {
62
- }
63
- return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [], ghost: [] };
64
- }
65
- function ghostGlobToRegExp(glob) {
66
- let re = "";
67
- for (let i = 0; i < glob.length; i++) {
68
- const c = glob[i];
69
- if (c === "*") {
70
- if (glob[i + 1] === "*") {
71
- re += ".*";
72
- i++;
73
- } else re += "[^/]*";
74
- } else if (c === "?") re += "[^/]";
75
- else if ("\\^$.|+()[]{}".indexOf(c) !== -1) re += "\\" + c;
76
- else re += c;
77
- }
78
- try {
79
- return new RegExp("^" + re + "$");
80
- } catch {
81
- return null;
82
- }
83
- }
84
- function ghostMatch(targetPath, patterns) {
85
- if (!targetPath || !Array.isArray(patterns) || patterns.length === 0) return false;
86
- const norm = String(targetPath).replace(/\\/g, "/").replace(/\/+$/, "");
87
- if (!norm) return false;
88
- const segments = norm.split("/").filter(Boolean);
89
- const base = segments.length ? segments[segments.length - 1] : norm;
90
- for (let pat of patterns) {
91
- pat = String(pat || "").trim();
92
- if (!pat) continue;
93
- let dirOnly = false;
94
- if (pat.endsWith("/")) {
95
- dirOnly = true;
96
- pat = pat.slice(0, -1);
97
- }
98
- if (!pat) continue;
99
- const hasSlash = pat.indexOf("/") !== -1;
100
- const hasWild = /[*?]/.test(pat);
101
- const re = ghostGlobToRegExp(pat);
102
- if (!re) continue;
103
- if (dirOnly) {
104
- if (!hasSlash && !hasWild) {
105
- if (segments.indexOf(pat) !== -1) return true;
106
- continue;
107
- }
108
- let acc = "";
109
- for (const s of segments) {
110
- acc = acc ? acc + "/" + s : s;
111
- if (re.test(acc) || re.test(s)) return true;
112
- }
113
- continue;
114
- }
115
- if (!hasSlash) {
116
- if (re.test(base)) return true;
117
- if (segments.some((s) => re.test(s))) return true;
118
- continue;
119
- }
120
- if (re.test(norm)) return true;
121
- }
122
- return false;
123
- }
124
- function ghostCleanToken(tok) {
125
- let t = String(tok || "").trim();
126
- t = t.replace(/^[<>|;&(]+/, "").replace(/[);&|]+$/, "");
127
- t = t.replace(/^['"]+/, "").replace(/['"]+$/, "");
128
- t = t.replace(/^\d*>>?/, "");
129
- return t.trim();
130
- }
131
- function ghostStripLines(text, pats) {
132
- if (!Array.isArray(pats) || pats.length === 0) return text;
133
- const lines = String(text).split("\n");
134
- const kept = [];
135
- for (const line of lines) {
136
- const trimmed = line.trim();
137
- if (!trimmed) {
138
- kept.push(line);
139
- continue;
140
- }
141
- if (ghostMatch(trimmed, pats)) continue;
142
- const toks = trimmed.split(/\s+/);
143
- const anyHit = toks.some((t) => ghostMatch(ghostCleanToken(t), pats));
144
- if (!anyHit) {
145
- kept.push(line);
146
- continue;
147
- }
148
- if (toks.length > 3) continue;
149
- const remaining = toks.filter((t) => !ghostMatch(ghostCleanToken(t), pats));
150
- if (remaining.length === 0) continue;
151
- kept.push(remaining.join(" "));
152
- }
153
- return kept.join("\n");
154
- }
155
- function dlpGlobToRe(glob, flags) {
156
- let re = "";
157
- for (const ch of String(glob || "")) {
158
- if (ch === "*") re += "[^\\s]*";
159
- else if (".+?^${}()|[]\\".indexOf(ch) !== -1) re += "\\" + ch;
160
- else re += ch;
161
- }
162
- return new RegExp(re, flags);
163
- }
164
- function redactString(s, cfg) {
165
- if (!cfg || typeof s !== "string" || !s) return s;
166
- const allow = new Set(cfg.patterns);
167
- const NUL = String.fromCharCode(0);
168
- const labels = [];
169
- const stash = (label) => NUL + (labels.push(label) - 1) + NUL;
170
- let out = s;
171
- for (const p of DLP_PATTERNS) if (allow.has(p.name)) out = out.replace(p.re, () => stash(`[REDACTED: ${p.name}]`));
172
- for (const c of cfg.custom) {
173
- try {
174
- out = out.replace(dlpGlobToRe(c.re, "gi"), () => stash(`[REDACTED: ${c.name || "custom"}]`));
175
- } catch {
176
- }
177
- }
178
- return out.replace(new RegExp(NUL + "(\\d+)" + NUL, "g"), (_, i) => labels[+i] || "");
179
- }
180
- function redactDeep(value, cfg) {
181
- const ghost = cfg && Array.isArray(cfg.ghost) ? cfg.ghost : null;
182
- if (typeof value === "string") {
183
- let out = redactString(value, cfg);
184
- if (ghost && ghost.length) out = ghostStripLines(out, ghost);
185
- if (out === "" && value !== "") out = "\n";
186
- return out;
187
- }
188
- if (Array.isArray(value)) {
189
- const arr = ghost && ghost.length ? value.filter((v) => !(typeof v === "string" && ghostMatch(v.trim(), ghost))) : value;
190
- return arr.map((v) => redactDeep(v, cfg));
191
- }
192
- if (value && typeof value === "object") {
193
- const out = {};
194
- for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, cfg);
195
- return out;
196
- }
197
- return value;
198
- }
199
- function pickUpstream() {
200
- const raw = process.env.SOLONGATE_SHIELD_UPSTREAM || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
201
- try {
202
- return new URL(raw);
203
- } catch {
204
- return new URL("https://api.anthropic.com");
205
- }
206
- }
207
- function startProxy(upstream) {
208
- const forward = upstream.protocol === "https:" ? httpsRequest : httpRequest;
209
- const server = createServer((req, res) => {
210
- const cfg = loadCfg();
211
- const chunks = [];
212
- req.on("data", (c) => chunks.push(c));
213
- req.on("end", () => {
214
- let body = Buffer.concat(chunks);
215
- try {
216
- if (body.length && (req.headers["content-type"] || "").includes("json")) {
217
- const parsed = JSON.parse(body.toString("utf-8"));
218
- if (parsed && typeof parsed === "object" && Array.isArray(parsed["messages"])) {
219
- if (parsed["system"] !== void 0) parsed["system"] = redactDeep(parsed["system"], cfg);
220
- const msgs = parsed["messages"];
221
- for (let i = 0; i < msgs.length; i++) {
222
- const m = msgs[i];
223
- if (m && m["role"] !== "assistant") msgs[i] = redactDeep(m, cfg);
224
- }
225
- body = Buffer.from(JSON.stringify(parsed), "utf-8");
226
- } else {
227
- body = Buffer.from(JSON.stringify(redactDeep(parsed, cfg)), "utf-8");
228
- }
229
- }
230
- } catch {
231
- }
232
- const headers = { ...req.headers };
233
- delete headers["host"];
234
- delete headers["content-length"];
235
- delete headers["accept-encoding"];
236
- headers["content-length"] = String(body.length);
237
- const upstreamReq = forward(
238
- {
239
- protocol: upstream.protocol,
240
- hostname: upstream.hostname,
241
- port: upstream.port || (upstream.protocol === "https:" ? 443 : 80),
242
- method: req.method,
243
- path: req.url,
244
- headers: { ...headers, host: upstream.host }
245
- },
246
- (upRes) => {
247
- res.writeHead(upRes.statusCode || 502, upRes.headers);
248
- upRes.pipe(res);
249
- }
250
- );
251
- upstreamReq.on("error", (e) => {
252
- log("upstream error:", e.message);
253
- if (!res.headersSent) res.writeHead(502, { "content-type": "text/plain" });
254
- res.end("shield upstream error");
255
- });
256
- upstreamReq.end(body);
257
- });
258
- req.on("error", () => {
259
- try {
260
- res.destroy();
261
- } catch {
262
- }
263
- });
264
- });
265
- return new Promise((resolveP) => {
266
- server.listen(0, "127.0.0.1", () => {
267
- const addr = server.address();
268
- const port = typeof addr === "object" && addr ? addr.port : 0;
269
- resolveP({ port, close: () => server.close() });
270
- });
271
- });
272
- }
273
- async function runShield() {
274
- const sep = process.argv.indexOf("--");
275
- const cmd = sep !== -1 ? process.argv.slice(sep + 1) : [];
276
- if (cmd.length === 0) {
277
- log("usage: npx @solongate/proxy shield -- <command> [args...] (e.g. shield -- claude)");
278
- process.exit(1);
279
- }
280
- const upstream = pickUpstream();
281
- const { port, close } = await startProxy(upstream);
282
- log(`redacting secrets on the LLM path \u2192 masking before ${upstream.host} (127.0.0.1:${port})`);
283
- const child = spawn(cmd[0], cmd.slice(1), {
284
- stdio: "inherit",
285
- env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
286
- shell: process.platform === "win32"
287
- // resolve `claude.cmd` etc. on Windows
288
- });
289
- const shutdown = () => {
290
- try {
291
- close();
292
- } catch {
293
- }
294
- };
295
- child.on("exit", (code, signal) => {
296
- shutdown();
297
- if (signal) process.kill(process.pid, signal);
298
- else process.exit(code ?? 0);
299
- });
300
- child.on("error", (e) => {
301
- log("failed to launch command:", e.message);
302
- shutdown();
303
- process.exit(1);
304
- });
305
- for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => {
306
- try {
307
- child.kill(sig);
308
- } catch {
309
- }
310
- });
311
- }
312
- export {
313
- runShield
314
- };