@solongate/proxy 0.2.1 → 0.2.2
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/index.js +153 -41
- package/dist/init.js +149 -37
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,20 +6,9 @@ var __esm = (fn, res) => function __init() {
|
|
|
6
6
|
|
|
7
7
|
// src/init.ts
|
|
8
8
|
var init_exports = {};
|
|
9
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync } from "fs";
|
|
10
|
-
import { resolve as resolve2, join
|
|
9
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync, mkdirSync } from "fs";
|
|
10
|
+
import { resolve as resolve2, join } from "path";
|
|
11
11
|
import { createInterface } from "readline";
|
|
12
|
-
function findProxyPath() {
|
|
13
|
-
const candidates = [
|
|
14
|
-
resolve2(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
|
|
15
|
-
resolve2("node_modules", "@solongate", "proxy", "dist", "index.js"),
|
|
16
|
-
resolve2("packages", "proxy", "dist", "index.js")
|
|
17
|
-
];
|
|
18
|
-
for (const p of candidates) {
|
|
19
|
-
if (existsSync2(p)) return p.replace(/\\/g, "/");
|
|
20
|
-
}
|
|
21
|
-
return "solongate-proxy";
|
|
22
|
-
}
|
|
23
12
|
function findConfigFile(explicitPath) {
|
|
24
13
|
if (explicitPath) {
|
|
25
14
|
if (existsSync2(explicitPath)) {
|
|
@@ -46,11 +35,14 @@ function isAlreadyProtected(server) {
|
|
|
46
35
|
const cmdStr = [server.command, ...server.args ?? []].join(" ");
|
|
47
36
|
return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
|
|
48
37
|
}
|
|
49
|
-
function wrapServer(server, policy,
|
|
38
|
+
function wrapServer(server, policy, apiKey) {
|
|
50
39
|
return {
|
|
51
|
-
command: "
|
|
40
|
+
command: "npx",
|
|
52
41
|
args: [
|
|
53
|
-
|
|
42
|
+
"-y",
|
|
43
|
+
"@solongate/proxy",
|
|
44
|
+
"--api-key",
|
|
45
|
+
apiKey,
|
|
54
46
|
"--policy",
|
|
55
47
|
policy,
|
|
56
48
|
"--verbose",
|
|
@@ -58,10 +50,7 @@ function wrapServer(server, policy, proxyPath) {
|
|
|
58
50
|
server.command,
|
|
59
51
|
...server.args ?? []
|
|
60
52
|
],
|
|
61
|
-
env: {
|
|
62
|
-
...server.env,
|
|
63
|
-
SOLONGATE_API_KEY: server.env?.SOLONGATE_API_KEY ?? "sg_live_YOUR_KEY_HERE"
|
|
64
|
-
}
|
|
53
|
+
...server.env ? { env: server.env } : {}
|
|
65
54
|
};
|
|
66
55
|
}
|
|
67
56
|
async function prompt(question) {
|
|
@@ -89,6 +78,9 @@ function parseInitArgs(argv) {
|
|
|
89
78
|
case "--policy":
|
|
90
79
|
options.policy = args[++i];
|
|
91
80
|
break;
|
|
81
|
+
case "--api-key":
|
|
82
|
+
options.apiKey = args[++i];
|
|
83
|
+
break;
|
|
92
84
|
case "--all":
|
|
93
85
|
options.all = true;
|
|
94
86
|
break;
|
|
@@ -124,13 +116,14 @@ OPTIONS
|
|
|
124
116
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
125
117
|
--policy <preset> Policy preset or JSON file (default: restricted)
|
|
126
118
|
Presets: ${POLICY_PRESETS.join(", ")}
|
|
119
|
+
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
127
120
|
--all Protect all servers without prompting
|
|
128
121
|
--dry-run Preview changes without writing
|
|
129
122
|
--restore Restore original config from backup
|
|
130
123
|
-h, --help Show this help message
|
|
131
124
|
|
|
132
125
|
EXAMPLES
|
|
133
|
-
solongate-init
|
|
126
|
+
solongate-init --api-key sg_live_xxx # Setup with API key
|
|
134
127
|
solongate-init --all # Protect everything
|
|
135
128
|
solongate-init --policy read-only # Use read-only policy
|
|
136
129
|
solongate-init --dry-run # Preview changes
|
|
@@ -144,6 +137,44 @@ POLICY PRESETS
|
|
|
144
137
|
`;
|
|
145
138
|
console.error(help);
|
|
146
139
|
}
|
|
140
|
+
function installClaudeCodeHooks(apiKey) {
|
|
141
|
+
const hooksDir = resolve2(".claude", "hooks");
|
|
142
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
143
|
+
const hookPath = join(hooksDir, "audit.mjs");
|
|
144
|
+
writeFileSync(hookPath, HOOK_SCRIPT);
|
|
145
|
+
console.error(` Created ${hookPath}`);
|
|
146
|
+
const settingsPath = resolve2(".claude", "settings.json");
|
|
147
|
+
let settings = {};
|
|
148
|
+
if (existsSync2(settingsPath)) {
|
|
149
|
+
try {
|
|
150
|
+
settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
settings.hooks = {
|
|
155
|
+
PostToolUse: [
|
|
156
|
+
{
|
|
157
|
+
matcher: ".*",
|
|
158
|
+
hooks: [
|
|
159
|
+
{
|
|
160
|
+
type: "command",
|
|
161
|
+
command: "node .claude/hooks/audit.mjs",
|
|
162
|
+
timeout: 10,
|
|
163
|
+
async: true
|
|
164
|
+
}
|
|
165
|
+
]
|
|
166
|
+
}
|
|
167
|
+
]
|
|
168
|
+
};
|
|
169
|
+
const envObj = settings.env || {};
|
|
170
|
+
envObj.SOLONGATE_API_KEY = apiKey;
|
|
171
|
+
settings.env = envObj;
|
|
172
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
173
|
+
console.error(` Created ${settingsPath}`);
|
|
174
|
+
console.error("");
|
|
175
|
+
console.error(" Claude Code hooks installed!");
|
|
176
|
+
console.error(" Built-in tools (Read, Write, Edit, Bash) will now be audit-logged.");
|
|
177
|
+
}
|
|
147
178
|
function ensureEnvFile() {
|
|
148
179
|
const envPath = resolve2(".env");
|
|
149
180
|
if (!existsSync2(envPath)) {
|
|
@@ -281,14 +312,34 @@ async function main() {
|
|
|
281
312
|
process.exit(0);
|
|
282
313
|
}
|
|
283
314
|
console.error("");
|
|
284
|
-
|
|
315
|
+
let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
316
|
+
if (!apiKey) {
|
|
317
|
+
const envPath = resolve2(".env");
|
|
318
|
+
if (existsSync2(envPath)) {
|
|
319
|
+
const envContent = readFileSync2(envPath, "utf-8");
|
|
320
|
+
const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
|
|
321
|
+
if (match) apiKey = match[1];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
325
|
+
apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
|
|
326
|
+
if (!apiKey) {
|
|
327
|
+
console.error(" API key is required. Get one at https://dashboard.solongate.com");
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
332
|
+
console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
console.error(` Policy: ${options.policy}`);
|
|
336
|
+
console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
285
337
|
console.error(` Protecting: ${toProtect.join(", ")}`);
|
|
286
338
|
console.error("");
|
|
287
|
-
const proxyPath = findProxyPath();
|
|
288
339
|
const newConfig = { mcpServers: {} };
|
|
289
340
|
for (const name of serverNames) {
|
|
290
341
|
if (toProtect.includes(name)) {
|
|
291
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy,
|
|
342
|
+
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
|
|
292
343
|
} else {
|
|
293
344
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
294
345
|
}
|
|
@@ -313,22 +364,12 @@ async function main() {
|
|
|
313
364
|
}
|
|
314
365
|
console.error(" Config updated!");
|
|
315
366
|
console.error("");
|
|
316
|
-
|
|
317
|
-
console.error(" \u2502 Your MCP servers are now protected by \u2502");
|
|
318
|
-
console.error(" \u2502 SolonGate security policies. \u2502");
|
|
319
|
-
console.error(" \u2502 \u2502");
|
|
320
|
-
console.error(" \u2502 Next steps: \u2502");
|
|
321
|
-
console.error(" \u2502 1. Replace sg_live_YOUR_KEY_HERE in your \u2502");
|
|
322
|
-
console.error(" \u2502 config with your real API key from \u2502");
|
|
323
|
-
console.error(" \u2502 https://solongate.com \u2502");
|
|
324
|
-
console.error(" \u2502 2. Restart your MCP client (Claude Code \u2502");
|
|
325
|
-
console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
|
|
326
|
-
console.error(" \u2502 \u2502");
|
|
327
|
-
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
328
|
-
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
367
|
+
installClaudeCodeHooks(apiKey);
|
|
329
368
|
console.error("");
|
|
330
369
|
ensureEnvFile();
|
|
331
370
|
console.error("");
|
|
371
|
+
console.error(" \u2500\u2500 Summary \u2500\u2500");
|
|
372
|
+
console.error("");
|
|
332
373
|
for (const name of toProtect) {
|
|
333
374
|
console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
|
|
334
375
|
}
|
|
@@ -339,8 +380,20 @@ async function main() {
|
|
|
339
380
|
console.error(` \u25CB ${name} \u2014 skipped`);
|
|
340
381
|
}
|
|
341
382
|
console.error("");
|
|
383
|
+
console.error(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
384
|
+
console.error(" \u2502 Setup complete! \u2502");
|
|
385
|
+
console.error(" \u2502 \u2502");
|
|
386
|
+
console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
|
|
387
|
+
console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
|
|
388
|
+
console.error(" \u2502 \u2502");
|
|
389
|
+
console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
|
|
390
|
+
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
391
|
+
console.error(" \u2502 \u2502");
|
|
392
|
+
console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
|
|
393
|
+
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
394
|
+
console.error("");
|
|
342
395
|
}
|
|
343
|
-
var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS;
|
|
396
|
+
var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, HOOK_SCRIPT;
|
|
344
397
|
var init_init = __esm({
|
|
345
398
|
"src/init.ts"() {
|
|
346
399
|
"use strict";
|
|
@@ -351,6 +404,65 @@ var init_init = __esm({
|
|
|
351
404
|
".claude/mcp.json"
|
|
352
405
|
];
|
|
353
406
|
CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
407
|
+
HOOK_SCRIPT = `#!/usr/bin/env node
|
|
408
|
+
/**
|
|
409
|
+
* SolonGate Audit Hook for Claude Code
|
|
410
|
+
* Captures ALL tool calls (built-in + MCP) and sends to SolonGate Cloud.
|
|
411
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
412
|
+
*/
|
|
413
|
+
|
|
414
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
415
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
416
|
+
|
|
417
|
+
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
418
|
+
|
|
419
|
+
let input = '';
|
|
420
|
+
process.stdin.on('data', c => input += c);
|
|
421
|
+
process.stdin.on('end', async () => {
|
|
422
|
+
try {
|
|
423
|
+
const data = JSON.parse(input);
|
|
424
|
+
const toolName = data.tool_name || 'unknown';
|
|
425
|
+
const toolInput = data.tool_input || {};
|
|
426
|
+
|
|
427
|
+
// Skip logging the audit hook itself to avoid loops
|
|
428
|
+
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
429
|
+
process.exit(0);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const hasError = data.tool_response?.error ||
|
|
433
|
+
data.tool_response?.exitCode > 0 ||
|
|
434
|
+
data.tool_response?.isError;
|
|
435
|
+
|
|
436
|
+
// Truncate large argument values for security
|
|
437
|
+
const argsSummary = {};
|
|
438
|
+
for (const [k, v] of Object.entries(toolInput)) {
|
|
439
|
+
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
440
|
+
? v.slice(0, 200) + '...'
|
|
441
|
+
: v;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: {
|
|
447
|
+
'Authorization': \`Bearer \${API_KEY}\`,
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
},
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
tool: toolName,
|
|
452
|
+
arguments: argsSummary,
|
|
453
|
+
decision: hasError ? 'DENY' : 'ALLOW',
|
|
454
|
+
reason: hasError ? 'tool returned error' : 'allowed',
|
|
455
|
+
source: 'claude-code-hook',
|
|
456
|
+
evaluationTimeMs: 0,
|
|
457
|
+
}),
|
|
458
|
+
signal: AbortSignal.timeout(5000),
|
|
459
|
+
});
|
|
460
|
+
} catch {
|
|
461
|
+
// Silent
|
|
462
|
+
}
|
|
463
|
+
process.exit(0);
|
|
464
|
+
});
|
|
465
|
+
`;
|
|
354
466
|
main().catch((err) => {
|
|
355
467
|
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
356
468
|
process.exit(1);
|
|
@@ -732,7 +844,7 @@ var init_inject = __esm({
|
|
|
732
844
|
|
|
733
845
|
// src/create.ts
|
|
734
846
|
var create_exports = {};
|
|
735
|
-
import { mkdirSync, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
847
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
736
848
|
import { resolve as resolve4, join as join2 } from "path";
|
|
737
849
|
import { execSync as execSync2 } from "child_process";
|
|
738
850
|
function log3(msg) {
|
|
@@ -884,7 +996,7 @@ function createProject(dir, name, _policy) {
|
|
|
884
996
|
2
|
|
885
997
|
) + "\n"
|
|
886
998
|
);
|
|
887
|
-
|
|
999
|
+
mkdirSync2(join2(dir, "src"), { recursive: true });
|
|
888
1000
|
writeFileSync3(
|
|
889
1001
|
join2(dir, "src", "index.ts"),
|
|
890
1002
|
`#!/usr/bin/env node
|
|
@@ -973,7 +1085,7 @@ async function main3() {
|
|
|
973
1085
|
process.exit(1);
|
|
974
1086
|
}
|
|
975
1087
|
withSpinner(`Setting up ${opts.name}...`, () => {
|
|
976
|
-
|
|
1088
|
+
mkdirSync2(dir, { recursive: true });
|
|
977
1089
|
createProject(dir, opts.name, opts.policy);
|
|
978
1090
|
});
|
|
979
1091
|
if (!opts.noInstall) {
|
package/dist/init.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/init.ts
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, copyFileSync } from "fs";
|
|
5
|
-
import { resolve, join
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync, mkdirSync } from "fs";
|
|
5
|
+
import { resolve, join } from "path";
|
|
6
6
|
import { createInterface } from "readline";
|
|
7
7
|
var POLICY_PRESETS = ["restricted", "read-only", "permissive", "deny-all"];
|
|
8
8
|
var SEARCH_PATHS = [
|
|
@@ -11,17 +11,6 @@ var SEARCH_PATHS = [
|
|
|
11
11
|
".claude/mcp.json"
|
|
12
12
|
];
|
|
13
13
|
var CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
|
|
14
|
-
function findProxyPath() {
|
|
15
|
-
const candidates = [
|
|
16
|
-
resolve(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
|
|
17
|
-
resolve("node_modules", "@solongate", "proxy", "dist", "index.js"),
|
|
18
|
-
resolve("packages", "proxy", "dist", "index.js")
|
|
19
|
-
];
|
|
20
|
-
for (const p of candidates) {
|
|
21
|
-
if (existsSync(p)) return p.replace(/\\/g, "/");
|
|
22
|
-
}
|
|
23
|
-
return "solongate-proxy";
|
|
24
|
-
}
|
|
25
14
|
function findConfigFile(explicitPath) {
|
|
26
15
|
if (explicitPath) {
|
|
27
16
|
if (existsSync(explicitPath)) {
|
|
@@ -48,11 +37,14 @@ function isAlreadyProtected(server) {
|
|
|
48
37
|
const cmdStr = [server.command, ...server.args ?? []].join(" ");
|
|
49
38
|
return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
|
|
50
39
|
}
|
|
51
|
-
function wrapServer(server, policy,
|
|
40
|
+
function wrapServer(server, policy, apiKey) {
|
|
52
41
|
return {
|
|
53
|
-
command: "
|
|
42
|
+
command: "npx",
|
|
54
43
|
args: [
|
|
55
|
-
|
|
44
|
+
"-y",
|
|
45
|
+
"@solongate/proxy",
|
|
46
|
+
"--api-key",
|
|
47
|
+
apiKey,
|
|
56
48
|
"--policy",
|
|
57
49
|
policy,
|
|
58
50
|
"--verbose",
|
|
@@ -60,10 +52,7 @@ function wrapServer(server, policy, proxyPath) {
|
|
|
60
52
|
server.command,
|
|
61
53
|
...server.args ?? []
|
|
62
54
|
],
|
|
63
|
-
env: {
|
|
64
|
-
...server.env,
|
|
65
|
-
SOLONGATE_API_KEY: server.env?.SOLONGATE_API_KEY ?? "sg_live_YOUR_KEY_HERE"
|
|
66
|
-
}
|
|
55
|
+
...server.env ? { env: server.env } : {}
|
|
67
56
|
};
|
|
68
57
|
}
|
|
69
58
|
async function prompt(question) {
|
|
@@ -91,6 +80,9 @@ function parseInitArgs(argv) {
|
|
|
91
80
|
case "--policy":
|
|
92
81
|
options.policy = args[++i];
|
|
93
82
|
break;
|
|
83
|
+
case "--api-key":
|
|
84
|
+
options.apiKey = args[++i];
|
|
85
|
+
break;
|
|
94
86
|
case "--all":
|
|
95
87
|
options.all = true;
|
|
96
88
|
break;
|
|
@@ -126,13 +118,14 @@ OPTIONS
|
|
|
126
118
|
--config <path> Path to MCP config file (default: auto-detect)
|
|
127
119
|
--policy <preset> Policy preset or JSON file (default: restricted)
|
|
128
120
|
Presets: ${POLICY_PRESETS.join(", ")}
|
|
121
|
+
--api-key <key> SolonGate API key (sg_live_... or sg_test_...)
|
|
129
122
|
--all Protect all servers without prompting
|
|
130
123
|
--dry-run Preview changes without writing
|
|
131
124
|
--restore Restore original config from backup
|
|
132
125
|
-h, --help Show this help message
|
|
133
126
|
|
|
134
127
|
EXAMPLES
|
|
135
|
-
solongate-init
|
|
128
|
+
solongate-init --api-key sg_live_xxx # Setup with API key
|
|
136
129
|
solongate-init --all # Protect everything
|
|
137
130
|
solongate-init --policy read-only # Use read-only policy
|
|
138
131
|
solongate-init --dry-run # Preview changes
|
|
@@ -146,6 +139,103 @@ POLICY PRESETS
|
|
|
146
139
|
`;
|
|
147
140
|
console.error(help);
|
|
148
141
|
}
|
|
142
|
+
var HOOK_SCRIPT = `#!/usr/bin/env node
|
|
143
|
+
/**
|
|
144
|
+
* SolonGate Audit Hook for Claude Code
|
|
145
|
+
* Captures ALL tool calls (built-in + MCP) and sends to SolonGate Cloud.
|
|
146
|
+
* Auto-installed by: npx @solongate/proxy init
|
|
147
|
+
*/
|
|
148
|
+
|
|
149
|
+
const API_KEY = process.env.SOLONGATE_API_KEY || '';
|
|
150
|
+
const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
151
|
+
|
|
152
|
+
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
153
|
+
|
|
154
|
+
let input = '';
|
|
155
|
+
process.stdin.on('data', c => input += c);
|
|
156
|
+
process.stdin.on('end', async () => {
|
|
157
|
+
try {
|
|
158
|
+
const data = JSON.parse(input);
|
|
159
|
+
const toolName = data.tool_name || 'unknown';
|
|
160
|
+
const toolInput = data.tool_input || {};
|
|
161
|
+
|
|
162
|
+
// Skip logging the audit hook itself to avoid loops
|
|
163
|
+
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const hasError = data.tool_response?.error ||
|
|
168
|
+
data.tool_response?.exitCode > 0 ||
|
|
169
|
+
data.tool_response?.isError;
|
|
170
|
+
|
|
171
|
+
// Truncate large argument values for security
|
|
172
|
+
const argsSummary = {};
|
|
173
|
+
for (const [k, v] of Object.entries(toolInput)) {
|
|
174
|
+
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
175
|
+
? v.slice(0, 200) + '...'
|
|
176
|
+
: v;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
180
|
+
method: 'POST',
|
|
181
|
+
headers: {
|
|
182
|
+
'Authorization': \`Bearer \${API_KEY}\`,
|
|
183
|
+
'Content-Type': 'application/json',
|
|
184
|
+
},
|
|
185
|
+
body: JSON.stringify({
|
|
186
|
+
tool: toolName,
|
|
187
|
+
arguments: argsSummary,
|
|
188
|
+
decision: hasError ? 'DENY' : 'ALLOW',
|
|
189
|
+
reason: hasError ? 'tool returned error' : 'allowed',
|
|
190
|
+
source: 'claude-code-hook',
|
|
191
|
+
evaluationTimeMs: 0,
|
|
192
|
+
}),
|
|
193
|
+
signal: AbortSignal.timeout(5000),
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
// Silent
|
|
197
|
+
}
|
|
198
|
+
process.exit(0);
|
|
199
|
+
});
|
|
200
|
+
`;
|
|
201
|
+
function installClaudeCodeHooks(apiKey) {
|
|
202
|
+
const hooksDir = resolve(".claude", "hooks");
|
|
203
|
+
mkdirSync(hooksDir, { recursive: true });
|
|
204
|
+
const hookPath = join(hooksDir, "audit.mjs");
|
|
205
|
+
writeFileSync(hookPath, HOOK_SCRIPT);
|
|
206
|
+
console.error(` Created ${hookPath}`);
|
|
207
|
+
const settingsPath = resolve(".claude", "settings.json");
|
|
208
|
+
let settings = {};
|
|
209
|
+
if (existsSync(settingsPath)) {
|
|
210
|
+
try {
|
|
211
|
+
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
settings.hooks = {
|
|
216
|
+
PostToolUse: [
|
|
217
|
+
{
|
|
218
|
+
matcher: ".*",
|
|
219
|
+
hooks: [
|
|
220
|
+
{
|
|
221
|
+
type: "command",
|
|
222
|
+
command: "node .claude/hooks/audit.mjs",
|
|
223
|
+
timeout: 10,
|
|
224
|
+
async: true
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
};
|
|
230
|
+
const envObj = settings.env || {};
|
|
231
|
+
envObj.SOLONGATE_API_KEY = apiKey;
|
|
232
|
+
settings.env = envObj;
|
|
233
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
234
|
+
console.error(` Created ${settingsPath}`);
|
|
235
|
+
console.error("");
|
|
236
|
+
console.error(" Claude Code hooks installed!");
|
|
237
|
+
console.error(" Built-in tools (Read, Write, Edit, Bash) will now be audit-logged.");
|
|
238
|
+
}
|
|
149
239
|
function ensureEnvFile() {
|
|
150
240
|
const envPath = resolve(".env");
|
|
151
241
|
if (!existsSync(envPath)) {
|
|
@@ -283,14 +373,34 @@ async function main() {
|
|
|
283
373
|
process.exit(0);
|
|
284
374
|
}
|
|
285
375
|
console.error("");
|
|
286
|
-
|
|
376
|
+
let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
377
|
+
if (!apiKey) {
|
|
378
|
+
const envPath = resolve(".env");
|
|
379
|
+
if (existsSync(envPath)) {
|
|
380
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
381
|
+
const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
|
|
382
|
+
if (match) apiKey = match[1];
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!apiKey || apiKey === "sg_live_your_key_here") {
|
|
386
|
+
apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
|
|
387
|
+
if (!apiKey) {
|
|
388
|
+
console.error(" API key is required. Get one at https://dashboard.solongate.com");
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
393
|
+
console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
console.error(` Policy: ${options.policy}`);
|
|
397
|
+
console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
287
398
|
console.error(` Protecting: ${toProtect.join(", ")}`);
|
|
288
399
|
console.error("");
|
|
289
|
-
const proxyPath = findProxyPath();
|
|
290
400
|
const newConfig = { mcpServers: {} };
|
|
291
401
|
for (const name of serverNames) {
|
|
292
402
|
if (toProtect.includes(name)) {
|
|
293
|
-
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy,
|
|
403
|
+
newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
|
|
294
404
|
} else {
|
|
295
405
|
newConfig.mcpServers[name] = config.mcpServers[name];
|
|
296
406
|
}
|
|
@@ -315,22 +425,12 @@ async function main() {
|
|
|
315
425
|
}
|
|
316
426
|
console.error(" Config updated!");
|
|
317
427
|
console.error("");
|
|
318
|
-
|
|
319
|
-
console.error(" \u2502 Your MCP servers are now protected by \u2502");
|
|
320
|
-
console.error(" \u2502 SolonGate security policies. \u2502");
|
|
321
|
-
console.error(" \u2502 \u2502");
|
|
322
|
-
console.error(" \u2502 Next steps: \u2502");
|
|
323
|
-
console.error(" \u2502 1. Replace sg_live_YOUR_KEY_HERE in your \u2502");
|
|
324
|
-
console.error(" \u2502 config with your real API key from \u2502");
|
|
325
|
-
console.error(" \u2502 https://solongate.com \u2502");
|
|
326
|
-
console.error(" \u2502 2. Restart your MCP client (Claude Code \u2502");
|
|
327
|
-
console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
|
|
328
|
-
console.error(" \u2502 \u2502");
|
|
329
|
-
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
330
|
-
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
428
|
+
installClaudeCodeHooks(apiKey);
|
|
331
429
|
console.error("");
|
|
332
430
|
ensureEnvFile();
|
|
333
431
|
console.error("");
|
|
432
|
+
console.error(" \u2500\u2500 Summary \u2500\u2500");
|
|
433
|
+
console.error("");
|
|
334
434
|
for (const name of toProtect) {
|
|
335
435
|
console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
|
|
336
436
|
}
|
|
@@ -341,6 +441,18 @@ async function main() {
|
|
|
341
441
|
console.error(` \u25CB ${name} \u2014 skipped`);
|
|
342
442
|
}
|
|
343
443
|
console.error("");
|
|
444
|
+
console.error(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
445
|
+
console.error(" \u2502 Setup complete! \u2502");
|
|
446
|
+
console.error(" \u2502 \u2502");
|
|
447
|
+
console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
|
|
448
|
+
console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
|
|
449
|
+
console.error(" \u2502 \u2502");
|
|
450
|
+
console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
|
|
451
|
+
console.error(" \u2502 To undo: solongate-init --restore \u2502");
|
|
452
|
+
console.error(" \u2502 \u2502");
|
|
453
|
+
console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
|
|
454
|
+
console.error(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
455
|
+
console.error("");
|
|
344
456
|
}
|
|
345
457
|
main().catch((err) => {
|
|
346
458
|
console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solongate/proxy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "MCP security proxy \u00e2\u20ac\u201d protect any MCP server with policies, input validation, rate limiting, and audit logging. Zero code changes required.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|