@solongate/proxy 0.51.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/login.js CHANGED
@@ -96,6 +96,7 @@ function protectedTargets() {
96
96
  join(p.hooksDir, "guard.mjs"),
97
97
  join(p.hooksDir, "audit.mjs"),
98
98
  join(p.hooksDir, "stop.mjs"),
99
+ join(p.hooksDir, "shield.mjs"),
99
100
  p.configPath,
100
101
  p.settingsPath
101
102
  ];
@@ -135,6 +136,71 @@ function ask(question) {
135
136
  res(a.trim());
136
137
  }));
137
138
  }
139
+ var SHIM_BEGIN = "# >>> SolonGate shield (auto secret redaction) >>>";
140
+ var SHIM_END = "# <<< SolonGate shield <<<";
141
+ function escapeRe(s) {
142
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
143
+ }
144
+ function resolveRealClaude() {
145
+ try {
146
+ const finder = process.platform === "win32" ? "where" : "which";
147
+ const out = execFileSync(finder, ["claude"], { encoding: "utf-8" }).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
148
+ return out[0] || null;
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function shimTargets() {
154
+ if (process.platform === "win32") {
155
+ try {
156
+ const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
157
+ return prof ? [prof] : [];
158
+ } catch {
159
+ return [];
160
+ }
161
+ }
162
+ return [".bashrc", ".zshrc", ".profile"].map((f) => join(homedir(), f)).filter((f) => existsSync(f));
163
+ }
164
+ function writeShimBlock(file, block) {
165
+ const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
166
+ let content = existsSync(file) ? readFileSync(file, "utf-8") : "";
167
+ content = content.replace(re, "");
168
+ if (block) {
169
+ if (content.length && !content.endsWith("\n")) content += "\n";
170
+ content += block + "\n";
171
+ }
172
+ mkdirSync(dirname(file), { recursive: true });
173
+ writeFileSync(file, content);
174
+ }
175
+ function installClaudeShim(shieldPath) {
176
+ const real = resolveRealClaude();
177
+ if (!real) {
178
+ console.log(" (Claude Code not found on PATH \u2014 skipped auto-shield. Install it, then re-run `login`.)");
179
+ return;
180
+ }
181
+ const node = process.execPath.replace(/\\/g, "/");
182
+ const shield = shieldPath.replace(/\\/g, "/");
183
+ const win = process.platform === "win32";
184
+ const block = win ? `${SHIM_BEGIN}
185
+ function claude { & "${node}" "${shield}" -- "${real}" @args }
186
+ ${SHIM_END}` : `${SHIM_BEGIN}
187
+ claude() { "${node}" "${shield}" -- "${real}" "$@"; }
188
+ ${SHIM_END}`;
189
+ const targets = shimTargets();
190
+ if (targets.length === 0) {
191
+ console.log(" (No shell profile found for auto-shield; run `claude` via `solongate shield -- claude` manually.)");
192
+ return;
193
+ }
194
+ for (const file of targets) {
195
+ try {
196
+ writeShimBlock(file, block);
197
+ console.log(` Auto-shield on: \`claude\` now masks secrets (${file})`);
198
+ } catch (e) {
199
+ console.log(` (Could not enable auto-shield in ${file}: ${e.message})`);
200
+ }
201
+ }
202
+ console.log(" Open a NEW terminal for it to take effect.");
203
+ }
138
204
  async function runGlobalInstall(opts = {}) {
139
205
  const p = globalPaths();
140
206
  let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
@@ -159,7 +225,9 @@ async function runGlobalInstall(opts = {}) {
159
225
  writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
160
226
  writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
161
227
  writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
228
+ writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
162
229
  console.log(` Installed hooks \u2192 ${p.hooksDir}`);
230
+ installClaudeShim(join(p.hooksDir, "shield.mjs"));
163
231
  writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
164
232
  console.log(` Wrote ${p.configPath}`);
165
233
  let existing = {};
@@ -0,0 +1 @@
1
+ export declare function runShield(): Promise<void>;
package/dist/shield.js ADDED
@@ -0,0 +1,171 @@
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 } 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-----/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: "Google API key", re: /AIza[0-9A-Za-z_-]{35}/g },
21
+ { name: "Stripe key", re: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/g },
22
+ { name: "SendGrid key", re: /SG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
23
+ { name: "Twilio key", re: /SK[0-9a-fA-F]{32}/g },
24
+ { name: "npm token", re: /npm_[A-Za-z0-9]{36}/g },
25
+ { name: "JWT", re: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g },
26
+ { name: "Bearer token", re: /bearer\s+[A-Za-z0-9._-]{20,}/gi },
27
+ { name: "secret assignment", re: /(api[_-]?key|secret|token|password|passwd|access[_-]?key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+_.-]{12,}/gi }
28
+ ];
29
+ function loadCfg() {
30
+ try {
31
+ const sel = (process.env.SOLONGATE_AGENT_ID || "default").replace(/[^a-zA-Z0-9_-]/g, "_");
32
+ const f = resolve(homedir(), ".solongate", ".policy-cache-" + sel + ".json");
33
+ if (existsSync(f)) {
34
+ const c = JSON.parse(readFileSync(f, "utf-8"));
35
+ const d = c?.security?.dlpRedact;
36
+ if (d && Array.isArray(d.patterns)) return { patterns: d.patterns, custom: Array.isArray(d.custom) ? d.custom : [] };
37
+ }
38
+ } catch {
39
+ }
40
+ return { patterns: DLP_PATTERNS.map((p) => p.name), custom: [] };
41
+ }
42
+ function redactString(s, cfg) {
43
+ if (!cfg || typeof s !== "string" || !s) return s;
44
+ const allow = new Set(cfg.patterns);
45
+ let out = s;
46
+ for (const p of DLP_PATTERNS) if (allow.has(p.name)) out = out.replace(p.re, `[REDACTED: ${p.name}]`);
47
+ for (const c of cfg.custom) {
48
+ try {
49
+ out = out.replace(new RegExp(c.re, "g"), `[REDACTED: ${c.name || "custom"}]`);
50
+ } catch {
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function redactDeep(value, cfg) {
56
+ if (typeof value === "string") return redactString(value, cfg);
57
+ if (Array.isArray(value)) return value.map((v) => redactDeep(v, cfg));
58
+ if (value && typeof value === "object") {
59
+ const out = {};
60
+ for (const [k, v] of Object.entries(value)) out[k] = redactDeep(v, cfg);
61
+ return out;
62
+ }
63
+ return value;
64
+ }
65
+ function pickUpstream() {
66
+ const raw = process.env.SOLONGATE_SHIELD_UPSTREAM || process.env.ANTHROPIC_BASE_URL || "https://api.anthropic.com";
67
+ try {
68
+ return new URL(raw);
69
+ } catch {
70
+ return new URL("https://api.anthropic.com");
71
+ }
72
+ }
73
+ function startProxy(upstream) {
74
+ const cfg = loadCfg();
75
+ const forward = upstream.protocol === "https:" ? httpsRequest : httpRequest;
76
+ const server = createServer((req, res) => {
77
+ const chunks = [];
78
+ req.on("data", (c) => chunks.push(c));
79
+ req.on("end", () => {
80
+ let body = Buffer.concat(chunks);
81
+ try {
82
+ if (body.length && (req.headers["content-type"] || "").includes("json")) {
83
+ const parsed = JSON.parse(body.toString("utf-8"));
84
+ const redacted = redactDeep(parsed, cfg);
85
+ body = Buffer.from(JSON.stringify(redacted), "utf-8");
86
+ }
87
+ } catch {
88
+ }
89
+ const headers = { ...req.headers };
90
+ delete headers["host"];
91
+ delete headers["content-length"];
92
+ delete headers["accept-encoding"];
93
+ headers["content-length"] = String(body.length);
94
+ const upstreamReq = forward(
95
+ {
96
+ protocol: upstream.protocol,
97
+ hostname: upstream.hostname,
98
+ port: upstream.port || (upstream.protocol === "https:" ? 443 : 80),
99
+ method: req.method,
100
+ path: req.url,
101
+ headers: { ...headers, host: upstream.host }
102
+ },
103
+ (upRes) => {
104
+ res.writeHead(upRes.statusCode || 502, upRes.headers);
105
+ upRes.pipe(res);
106
+ }
107
+ );
108
+ upstreamReq.on("error", (e) => {
109
+ log("upstream error:", e.message);
110
+ if (!res.headersSent) res.writeHead(502, { "content-type": "text/plain" });
111
+ res.end("shield upstream error");
112
+ });
113
+ upstreamReq.end(body);
114
+ });
115
+ req.on("error", () => {
116
+ try {
117
+ res.destroy();
118
+ } catch {
119
+ }
120
+ });
121
+ });
122
+ return new Promise((resolveP) => {
123
+ server.listen(0, "127.0.0.1", () => {
124
+ const addr = server.address();
125
+ const port = typeof addr === "object" && addr ? addr.port : 0;
126
+ resolveP({ port, close: () => server.close() });
127
+ });
128
+ });
129
+ }
130
+ async function runShield() {
131
+ const sep = process.argv.indexOf("--");
132
+ const cmd = sep !== -1 ? process.argv.slice(sep + 1) : [];
133
+ if (cmd.length === 0) {
134
+ log("usage: npx @solongate/proxy shield -- <command> [args...] (e.g. shield -- claude)");
135
+ process.exit(1);
136
+ }
137
+ const upstream = pickUpstream();
138
+ const { port, close } = await startProxy(upstream);
139
+ log(`redacting secrets on the LLM path \u2192 masking before ${upstream.host} (127.0.0.1:${port})`);
140
+ const child = spawn(cmd[0], cmd.slice(1), {
141
+ stdio: "inherit",
142
+ env: { ...process.env, ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}` },
143
+ shell: process.platform === "win32"
144
+ // resolve `claude.cmd` etc. on Windows
145
+ });
146
+ const shutdown = () => {
147
+ try {
148
+ close();
149
+ } catch {
150
+ }
151
+ };
152
+ child.on("exit", (code, signal) => {
153
+ shutdown();
154
+ if (signal) process.kill(process.pid, signal);
155
+ else process.exit(code ?? 0);
156
+ });
157
+ child.on("error", (e) => {
158
+ log("failed to launch command:", e.message);
159
+ shutdown();
160
+ process.exit(1);
161
+ });
162
+ for (const sig of ["SIGINT", "SIGTERM"]) process.on(sig, () => {
163
+ try {
164
+ child.kill(sig);
165
+ } catch {
166
+ }
167
+ });
168
+ }
169
+ export {
170
+ runShield
171
+ };