@solongate/proxy 0.51.0 → 0.53.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 +83 -0
- package/dist/index.js +412 -145
- package/dist/login.js +72 -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/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,75 @@ 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
|
+
if (process.platform === "win32") {
|
|
149
|
+
const low = (s) => s.toLowerCase();
|
|
150
|
+
return out.find((l) => low(l).endsWith(".cmd")) || out.find((l) => low(l).endsWith(".exe")) || out.find((l) => low(l).endsWith(".bat")) || out[0] || null;
|
|
151
|
+
}
|
|
152
|
+
return out[0] || null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function shimTargets() {
|
|
158
|
+
if (process.platform === "win32") {
|
|
159
|
+
try {
|
|
160
|
+
const prof = execFileSync("powershell", ["-NoProfile", "-Command", "$PROFILE.CurrentUserAllHosts"], { encoding: "utf-8" }).trim();
|
|
161
|
+
return prof ? [prof] : [];
|
|
162
|
+
} catch {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [".bashrc", ".zshrc", ".profile"].map((f) => join(homedir(), f)).filter((f) => existsSync(f));
|
|
167
|
+
}
|
|
168
|
+
function writeShimBlock(file, block) {
|
|
169
|
+
const re = new RegExp(escapeRe(SHIM_BEGIN) + "[\\s\\S]*?" + escapeRe(SHIM_END) + "\\r?\\n?", "g");
|
|
170
|
+
let content = existsSync(file) ? readFileSync(file, "utf-8") : "";
|
|
171
|
+
content = content.replace(re, "");
|
|
172
|
+
if (block) {
|
|
173
|
+
if (content.length && !content.endsWith("\n")) content += "\n";
|
|
174
|
+
content += block + "\n";
|
|
175
|
+
}
|
|
176
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
177
|
+
writeFileSync(file, content);
|
|
178
|
+
}
|
|
179
|
+
function installClaudeShim(shieldPath) {
|
|
180
|
+
const real = resolveRealClaude();
|
|
181
|
+
if (!real) {
|
|
182
|
+
console.log(" (Claude Code not found on PATH \u2014 skipped auto-shield. Install it, then re-run `login`.)");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const node = process.execPath.replace(/\\/g, "/");
|
|
186
|
+
const shield = shieldPath.replace(/\\/g, "/");
|
|
187
|
+
const win = process.platform === "win32";
|
|
188
|
+
const block = win ? `${SHIM_BEGIN}
|
|
189
|
+
function claude { & "${node}" "${shield}" -- "${real}" @args }
|
|
190
|
+
${SHIM_END}` : `${SHIM_BEGIN}
|
|
191
|
+
claude() { "${node}" "${shield}" -- "${real}" "$@"; }
|
|
192
|
+
${SHIM_END}`;
|
|
193
|
+
const targets = shimTargets();
|
|
194
|
+
if (targets.length === 0) {
|
|
195
|
+
console.log(" (No shell profile found for auto-shield; run `claude` via `solongate shield -- claude` manually.)");
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const file of targets) {
|
|
199
|
+
try {
|
|
200
|
+
writeShimBlock(file, block);
|
|
201
|
+
console.log(` Auto-shield on: \`claude\` now masks secrets (${file})`);
|
|
202
|
+
} catch (e) {
|
|
203
|
+
console.log(` (Could not enable auto-shield in ${file}: ${e.message})`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
console.log(" Open a NEW terminal for it to take effect.");
|
|
207
|
+
}
|
|
138
208
|
async function runGlobalInstall(opts = {}) {
|
|
139
209
|
const p = globalPaths();
|
|
140
210
|
let apiKey = opts.apiKey || process.env["SOLONGATE_API_KEY"] || "";
|
|
@@ -159,7 +229,9 @@ async function runGlobalInstall(opts = {}) {
|
|
|
159
229
|
writeFileSync(join(p.hooksDir, "guard.mjs"), readGuard());
|
|
160
230
|
writeFileSync(join(p.hooksDir, "audit.mjs"), readHook("audit.mjs"));
|
|
161
231
|
writeFileSync(join(p.hooksDir, "stop.mjs"), readHook("stop.mjs"));
|
|
232
|
+
writeFileSync(join(p.hooksDir, "shield.mjs"), readHook("shield.mjs"));
|
|
162
233
|
console.log(` Installed hooks \u2192 ${p.hooksDir}`);
|
|
234
|
+
installClaudeShim(join(p.hooksDir, "shield.mjs"));
|
|
163
235
|
writeFileSync(p.configPath, JSON.stringify({ apiKey, apiUrl }, null, 2) + "\n");
|
|
164
236
|
console.log(` Wrote ${p.configPath}`);
|
|
165
237
|
let existing = {};
|
package/dist/shield.d.ts
ADDED
|
@@ -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
|
+
};
|