@solongate/proxy 0.2.1 → 0.2.3

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.
Files changed (3) hide show
  1. package/dist/index.js +296 -41
  2. package/dist/init.js +292 -37
  3. 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, dirname } from "path";
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, proxyPath) {
38
+ function wrapServer(server, policy, apiKey) {
50
39
  return {
51
- command: "node",
40
+ command: "npx",
52
41
  args: [
53
- proxyPath,
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 # Interactive setup
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,60 @@ 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 guardPath = join(hooksDir, "guard.mjs");
144
+ writeFileSync(guardPath, GUARD_SCRIPT);
145
+ console.error(` Created ${guardPath}`);
146
+ const auditPath = join(hooksDir, "audit.mjs");
147
+ writeFileSync(auditPath, AUDIT_SCRIPT);
148
+ console.error(` Created ${auditPath}`);
149
+ const settingsPath = resolve2(".claude", "settings.json");
150
+ let settings = {};
151
+ if (existsSync2(settingsPath)) {
152
+ try {
153
+ settings = JSON.parse(readFileSync2(settingsPath, "utf-8"));
154
+ } catch {
155
+ }
156
+ }
157
+ settings.hooks = {
158
+ PreToolUse: [
159
+ {
160
+ matcher: ".*",
161
+ hooks: [
162
+ {
163
+ type: "command",
164
+ command: "node .claude/hooks/guard.mjs",
165
+ timeout: 5
166
+ }
167
+ ]
168
+ }
169
+ ],
170
+ PostToolUse: [
171
+ {
172
+ matcher: ".*",
173
+ hooks: [
174
+ {
175
+ type: "command",
176
+ command: "node .claude/hooks/audit.mjs",
177
+ timeout: 10,
178
+ async: true
179
+ }
180
+ ]
181
+ }
182
+ ]
183
+ };
184
+ const envObj = settings.env || {};
185
+ envObj.SOLONGATE_API_KEY = apiKey;
186
+ settings.env = envObj;
187
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
188
+ console.error(` Created ${settingsPath}`);
189
+ console.error("");
190
+ console.error(" Claude Code hooks installed!");
191
+ console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
192
+ console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
193
+ }
147
194
  function ensureEnvFile() {
148
195
  const envPath = resolve2(".env");
149
196
  if (!existsSync2(envPath)) {
@@ -281,14 +328,34 @@ async function main() {
281
328
  process.exit(0);
282
329
  }
283
330
  console.error("");
284
- console.error(` Policy: ${options.policy}`);
331
+ let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
332
+ if (!apiKey) {
333
+ const envPath = resolve2(".env");
334
+ if (existsSync2(envPath)) {
335
+ const envContent = readFileSync2(envPath, "utf-8");
336
+ const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
337
+ if (match) apiKey = match[1];
338
+ }
339
+ }
340
+ if (!apiKey || apiKey === "sg_live_your_key_here") {
341
+ apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
342
+ if (!apiKey) {
343
+ console.error(" API key is required. Get one at https://dashboard.solongate.com");
344
+ process.exit(1);
345
+ }
346
+ }
347
+ if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
348
+ console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
349
+ process.exit(1);
350
+ }
351
+ console.error(` Policy: ${options.policy}`);
352
+ console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
285
353
  console.error(` Protecting: ${toProtect.join(", ")}`);
286
354
  console.error("");
287
- const proxyPath = findProxyPath();
288
355
  const newConfig = { mcpServers: {} };
289
356
  for (const name of serverNames) {
290
357
  if (toProtect.includes(name)) {
291
- newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, proxyPath);
358
+ newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
292
359
  } else {
293
360
  newConfig.mcpServers[name] = config.mcpServers[name];
294
361
  }
@@ -313,22 +380,12 @@ async function main() {
313
380
  }
314
381
  console.error(" Config updated!");
315
382
  console.error("");
316
- 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");
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");
383
+ installClaudeCodeHooks(apiKey);
329
384
  console.error("");
330
385
  ensureEnvFile();
331
386
  console.error("");
387
+ console.error(" \u2500\u2500 Summary \u2500\u2500");
388
+ console.error("");
332
389
  for (const name of toProtect) {
333
390
  console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
334
391
  }
@@ -339,8 +396,20 @@ async function main() {
339
396
  console.error(` \u25CB ${name} \u2014 skipped`);
340
397
  }
341
398
  console.error("");
399
+ 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");
400
+ console.error(" \u2502 Setup complete! \u2502");
401
+ console.error(" \u2502 \u2502");
402
+ console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
403
+ console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
404
+ console.error(" \u2502 \u2502");
405
+ console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
406
+ console.error(" \u2502 To undo: solongate-init --restore \u2502");
407
+ console.error(" \u2502 \u2502");
408
+ console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
409
+ 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");
410
+ console.error("");
342
411
  }
343
- var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS;
412
+ var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, GUARD_SCRIPT, AUDIT_SCRIPT;
344
413
  var init_init = __esm({
345
414
  "src/init.ts"() {
346
415
  "use strict";
@@ -351,6 +420,192 @@ var init_init = __esm({
351
420
  ".claude/mcp.json"
352
421
  ];
353
422
  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")];
423
+ GUARD_SCRIPT = `#!/usr/bin/env node
424
+ /**
425
+ * SolonGate Guard Hook for Claude Code (PreToolUse)
426
+ * Blocks dangerous tool calls BEFORE they execute.
427
+ * Exit code 2 = BLOCK, exit code 0 = ALLOW.
428
+ * Auto-installed by: npx @solongate/proxy init
429
+ */
430
+
431
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
432
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
433
+
434
+ // \u2500\u2500 Input Guard Patterns \u2500\u2500
435
+ const PATH_TRAVERSAL = [
436
+ /\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
437
+ ];
438
+ const SENSITIVE_PATHS = [
439
+ /\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
440
+ /c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
441
+ /\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
442
+ /\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
443
+ ];
444
+ const SHELL_INJECTION = [
445
+ /\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
446
+ /%0a/i, /%0d/i,
447
+ ];
448
+ const SSRF = [
449
+ /^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
450
+ /^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
451
+ /^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
452
+ /metadata\\.google\\.internal/i,
453
+ ];
454
+ const SQL_INJECTION = [
455
+ /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
456
+ /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
457
+ /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
458
+ ];
459
+
460
+ // Dangerous command patterns for Bash tool
461
+ const DANGEROUS_COMMANDS = [
462
+ /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
463
+ /\\brm\\s+-rf\\b/i,
464
+ /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
465
+ /\\b(shutdown|reboot|halt|poweroff)\\b/i,
466
+ /\\bchmod\\s+777\\b/,
467
+ /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
468
+ /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
469
+ /\\bnc\\s+-[a-z]*l/i, // netcat listener
470
+ />(\\s*)\\/dev\\/sd/, // writing to raw disk
471
+ /\\bgit\\s+push\\s+.*--force\\b/i,
472
+ /\\bgit\\s+reset\\s+--hard\\b/i,
473
+ ];
474
+
475
+ function checkValue(val) {
476
+ if (typeof val !== 'string' || val.length < 2) return null;
477
+ for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
478
+ for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
479
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
480
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
481
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
482
+ return null;
483
+ }
484
+
485
+ function checkBashCommand(cmd) {
486
+ if (typeof cmd !== 'string') return null;
487
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
488
+ for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
489
+ return null;
490
+ }
491
+
492
+ let input = '';
493
+ process.stdin.on('data', c => input += c);
494
+ process.stdin.on('end', async () => {
495
+ try {
496
+ const data = JSON.parse(input);
497
+ const tool = data.tool_name || '';
498
+ const args = data.tool_input || {};
499
+ const start = Date.now();
500
+
501
+ let threat = null;
502
+
503
+ // Check Bash commands for dangerous patterns
504
+ if (tool === 'Bash' && args.command) {
505
+ threat = checkBashCommand(args.command);
506
+ }
507
+
508
+ // Check all string arguments for injection patterns
509
+ if (!threat) {
510
+ for (const [key, val] of Object.entries(args)) {
511
+ if (tool === 'Bash' && key === 'command') continue; // already checked
512
+ threat = checkValue(val);
513
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
514
+ // Check nested strings
515
+ if (typeof val === 'object' && val) {
516
+ for (const v of Object.values(val)) {
517
+ threat = checkValue(v);
518
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
519
+ }
520
+ if (threat) break;
521
+ }
522
+ }
523
+ }
524
+
525
+ const ms = Date.now() - start;
526
+
527
+ if (threat) {
528
+ // Send DENY audit log
529
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
530
+ fetch(API_URL + '/api/v1/audit-logs', {
531
+ method: 'POST',
532
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
533
+ body: JSON.stringify({
534
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
535
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
536
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
537
+ evaluationTimeMs: ms,
538
+ }),
539
+ signal: AbortSignal.timeout(5000),
540
+ }).catch(() => {});
541
+ }
542
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
543
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
544
+ process.exit(2);
545
+ }
546
+ } catch {
547
+ // On error, allow (fail-open)
548
+ }
549
+ process.exit(0);
550
+ });
551
+ `;
552
+ AUDIT_SCRIPT = `#!/usr/bin/env node
553
+ /**
554
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
555
+ * Logs ALL tool calls to SolonGate Cloud after execution.
556
+ * Auto-installed by: npx @solongate/proxy init
557
+ */
558
+
559
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
560
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
561
+
562
+ if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
563
+
564
+ let input = '';
565
+ process.stdin.on('data', c => input += c);
566
+ process.stdin.on('end', async () => {
567
+ try {
568
+ const data = JSON.parse(input);
569
+ const toolName = data.tool_name || 'unknown';
570
+ const toolInput = data.tool_input || {};
571
+
572
+ if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
573
+ process.exit(0);
574
+ }
575
+
576
+ const hasError = data.tool_response?.error ||
577
+ data.tool_response?.exitCode > 0 ||
578
+ data.tool_response?.isError;
579
+
580
+ const argsSummary = {};
581
+ for (const [k, v] of Object.entries(toolInput)) {
582
+ argsSummary[k] = typeof v === 'string' && v.length > 200
583
+ ? v.slice(0, 200) + '...'
584
+ : v;
585
+ }
586
+
587
+ await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
588
+ method: 'POST',
589
+ headers: {
590
+ 'Authorization': \`Bearer \${API_KEY}\`,
591
+ 'Content-Type': 'application/json',
592
+ },
593
+ body: JSON.stringify({
594
+ tool: toolName,
595
+ arguments: argsSummary,
596
+ decision: hasError ? 'DENY' : 'ALLOW',
597
+ reason: hasError ? 'tool returned error' : 'allowed',
598
+ source: 'claude-code-hook',
599
+ evaluationTimeMs: 0,
600
+ }),
601
+ signal: AbortSignal.timeout(5000),
602
+ });
603
+ } catch {
604
+ // Silent
605
+ }
606
+ process.exit(0);
607
+ });
608
+ `;
354
609
  main().catch((err) => {
355
610
  console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
356
611
  process.exit(1);
@@ -732,7 +987,7 @@ var init_inject = __esm({
732
987
 
733
988
  // src/create.ts
734
989
  var create_exports = {};
735
- import { mkdirSync, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
990
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
736
991
  import { resolve as resolve4, join as join2 } from "path";
737
992
  import { execSync as execSync2 } from "child_process";
738
993
  function log3(msg) {
@@ -884,7 +1139,7 @@ function createProject(dir, name, _policy) {
884
1139
  2
885
1140
  ) + "\n"
886
1141
  );
887
- mkdirSync(join2(dir, "src"), { recursive: true });
1142
+ mkdirSync2(join2(dir, "src"), { recursive: true });
888
1143
  writeFileSync3(
889
1144
  join2(dir, "src", "index.ts"),
890
1145
  `#!/usr/bin/env node
@@ -973,7 +1228,7 @@ async function main3() {
973
1228
  process.exit(1);
974
1229
  }
975
1230
  withSpinner(`Setting up ${opts.name}...`, () => {
976
- mkdirSync(dir, { recursive: true });
1231
+ mkdirSync2(dir, { recursive: true });
977
1232
  createProject(dir, opts.name, opts.policy);
978
1233
  });
979
1234
  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, dirname } from "path";
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, proxyPath) {
40
+ function wrapServer(server, policy, apiKey) {
52
41
  return {
53
- command: "node",
42
+ command: "npx",
54
43
  args: [
55
- proxyPath,
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 # Interactive setup
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,246 @@ POLICY PRESETS
146
139
  `;
147
140
  console.error(help);
148
141
  }
142
+ var GUARD_SCRIPT = `#!/usr/bin/env node
143
+ /**
144
+ * SolonGate Guard Hook for Claude Code (PreToolUse)
145
+ * Blocks dangerous tool calls BEFORE they execute.
146
+ * Exit code 2 = BLOCK, exit code 0 = ALLOW.
147
+ * Auto-installed by: npx @solongate/proxy init
148
+ */
149
+
150
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
151
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
152
+
153
+ // \u2500\u2500 Input Guard Patterns \u2500\u2500
154
+ const PATH_TRAVERSAL = [
155
+ /\\.\\.\\//, /\\\\.\\.\\\\\\\\/, /%2e%2e/i, /%2e\\./i, /\\.%2e/i, /%252e%252e/i,
156
+ ];
157
+ const SENSITIVE_PATHS = [
158
+ /\\/etc\\/passwd/i, /\\/etc\\/shadow/i, /\\/proc\\//i,
159
+ /c:\\\\windows\\\\system32/i, /\\.env(\\.|$)/i,
160
+ /\\.aws\\/credentials/i, /\\.ssh\\/id_/i, /\\.kube\\/config/i,
161
+ /\\.git\\/config/i, /\\.npmrc/i, /\\.pypirc/i,
162
+ ];
163
+ const SHELL_INJECTION = [
164
+ /\\$\\(/, /\\$\\{/, /\\\`/, /\\beval\\b/i, /\\bexec\\b/i, /\\bsystem\\b/i,
165
+ /%0a/i, /%0d/i,
166
+ ];
167
+ const SSRF = [
168
+ /^https?:\\/\\/localhost\\b/i, /^https?:\\/\\/127\\./, /^https?:\\/\\/0\\.0\\.0\\.0/,
169
+ /^https?:\\/\\/10\\./, /^https?:\\/\\/172\\.(1[6-9]|2\\d|3[01])\\./,
170
+ /^https?:\\/\\/192\\.168\\./, /^https?:\\/\\/169\\.254\\./,
171
+ /metadata\\.google\\.internal/i,
172
+ ];
173
+ const SQL_INJECTION = [
174
+ /'\\s{0,20}(OR|AND)\\s{0,20}'.{0,200}'/i,
175
+ /'\\s{0,10};\\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
176
+ /UNION\\s+(ALL\\s+)?SELECT/i, /\\bSLEEP\\s*\\(/i, /\\bWAITFOR\\s+DELAY/i,
177
+ ];
178
+
179
+ // Dangerous command patterns for Bash tool
180
+ const DANGEROUS_COMMANDS = [
181
+ /\\brm\\s+(-[a-zA-Z]*f|-[a-zA-Z]*r|--force|--recursive)/i,
182
+ /\\brm\\s+-rf\\b/i,
183
+ /\\bmkfs\\b/i, /\\bdd\\s+if=/i,
184
+ /\\b(shutdown|reboot|halt|poweroff)\\b/i,
185
+ /\\bchmod\\s+777\\b/,
186
+ /\\bcurl\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
187
+ /\\bwget\\b.{0,200}\\|\\s*(sh|bash)\\b/i,
188
+ /\\bnc\\s+-[a-z]*l/i, // netcat listener
189
+ />(\\s*)\\/dev\\/sd/, // writing to raw disk
190
+ /\\bgit\\s+push\\s+.*--force\\b/i,
191
+ /\\bgit\\s+reset\\s+--hard\\b/i,
192
+ ];
193
+
194
+ function checkValue(val) {
195
+ if (typeof val !== 'string' || val.length < 2) return null;
196
+ for (const p of PATH_TRAVERSAL) if (p.test(val)) return 'Path traversal detected';
197
+ for (const p of SENSITIVE_PATHS) if (p.test(val)) return 'Sensitive file access blocked';
198
+ for (const p of SSRF) if (p.test(val)) return 'SSRF attempt blocked';
199
+ for (const p of SQL_INJECTION) if (p.test(val)) return 'SQL injection detected';
200
+ if (val.length > 10000) return 'Input too long (max 10000 chars)';
201
+ return null;
202
+ }
203
+
204
+ function checkBashCommand(cmd) {
205
+ if (typeof cmd !== 'string') return null;
206
+ for (const p of DANGEROUS_COMMANDS) if (p.test(cmd)) return 'Dangerous command blocked: ' + cmd.slice(0, 80);
207
+ for (const p of SHELL_INJECTION) if (p.test(cmd)) return null; // shell injection is normal for Bash
208
+ return null;
209
+ }
210
+
211
+ let input = '';
212
+ process.stdin.on('data', c => input += c);
213
+ process.stdin.on('end', async () => {
214
+ try {
215
+ const data = JSON.parse(input);
216
+ const tool = data.tool_name || '';
217
+ const args = data.tool_input || {};
218
+ const start = Date.now();
219
+
220
+ let threat = null;
221
+
222
+ // Check Bash commands for dangerous patterns
223
+ if (tool === 'Bash' && args.command) {
224
+ threat = checkBashCommand(args.command);
225
+ }
226
+
227
+ // Check all string arguments for injection patterns
228
+ if (!threat) {
229
+ for (const [key, val] of Object.entries(args)) {
230
+ if (tool === 'Bash' && key === 'command') continue; // already checked
231
+ threat = checkValue(val);
232
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
233
+ // Check nested strings
234
+ if (typeof val === 'object' && val) {
235
+ for (const v of Object.values(val)) {
236
+ threat = checkValue(v);
237
+ if (threat) { threat = threat + ' (in ' + key + ')'; break; }
238
+ }
239
+ if (threat) break;
240
+ }
241
+ }
242
+ }
243
+
244
+ const ms = Date.now() - start;
245
+
246
+ if (threat) {
247
+ // Send DENY audit log
248
+ if (API_KEY && API_KEY.startsWith('sg_live_')) {
249
+ fetch(API_URL + '/api/v1/audit-logs', {
250
+ method: 'POST',
251
+ headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
252
+ body: JSON.stringify({
253
+ tool, arguments: Object.fromEntries(Object.entries(args).map(([k,v]) =>
254
+ [k, typeof v === 'string' && v.length > 200 ? v.slice(0,200)+'...' : v])),
255
+ decision: 'DENY', reason: threat, source: 'claude-code-guard',
256
+ evaluationTimeMs: ms,
257
+ }),
258
+ signal: AbortSignal.timeout(5000),
259
+ }).catch(() => {});
260
+ }
261
+ // Exit 2 = BLOCK. Message printed to stdout is shown to user.
262
+ process.stdout.write('SolonGate BLOCKED: ' + threat);
263
+ process.exit(2);
264
+ }
265
+ } catch {
266
+ // On error, allow (fail-open)
267
+ }
268
+ process.exit(0);
269
+ });
270
+ `;
271
+ var AUDIT_SCRIPT = `#!/usr/bin/env node
272
+ /**
273
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
274
+ * Logs ALL tool calls to SolonGate Cloud after execution.
275
+ * Auto-installed by: npx @solongate/proxy init
276
+ */
277
+
278
+ const API_KEY = process.env.SOLONGATE_API_KEY || '';
279
+ const API_URL = process.env.SOLONGATE_API_URL || 'https://api.solongate.com';
280
+
281
+ if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
282
+
283
+ let input = '';
284
+ process.stdin.on('data', c => input += c);
285
+ process.stdin.on('end', async () => {
286
+ try {
287
+ const data = JSON.parse(input);
288
+ const toolName = data.tool_name || 'unknown';
289
+ const toolInput = data.tool_input || {};
290
+
291
+ if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
292
+ process.exit(0);
293
+ }
294
+
295
+ const hasError = data.tool_response?.error ||
296
+ data.tool_response?.exitCode > 0 ||
297
+ data.tool_response?.isError;
298
+
299
+ const argsSummary = {};
300
+ for (const [k, v] of Object.entries(toolInput)) {
301
+ argsSummary[k] = typeof v === 'string' && v.length > 200
302
+ ? v.slice(0, 200) + '...'
303
+ : v;
304
+ }
305
+
306
+ await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
307
+ method: 'POST',
308
+ headers: {
309
+ 'Authorization': \`Bearer \${API_KEY}\`,
310
+ 'Content-Type': 'application/json',
311
+ },
312
+ body: JSON.stringify({
313
+ tool: toolName,
314
+ arguments: argsSummary,
315
+ decision: hasError ? 'DENY' : 'ALLOW',
316
+ reason: hasError ? 'tool returned error' : 'allowed',
317
+ source: 'claude-code-hook',
318
+ evaluationTimeMs: 0,
319
+ }),
320
+ signal: AbortSignal.timeout(5000),
321
+ });
322
+ } catch {
323
+ // Silent
324
+ }
325
+ process.exit(0);
326
+ });
327
+ `;
328
+ function installClaudeCodeHooks(apiKey) {
329
+ const hooksDir = resolve(".claude", "hooks");
330
+ mkdirSync(hooksDir, { recursive: true });
331
+ const guardPath = join(hooksDir, "guard.mjs");
332
+ writeFileSync(guardPath, GUARD_SCRIPT);
333
+ console.error(` Created ${guardPath}`);
334
+ const auditPath = join(hooksDir, "audit.mjs");
335
+ writeFileSync(auditPath, AUDIT_SCRIPT);
336
+ console.error(` Created ${auditPath}`);
337
+ const settingsPath = resolve(".claude", "settings.json");
338
+ let settings = {};
339
+ if (existsSync(settingsPath)) {
340
+ try {
341
+ settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
342
+ } catch {
343
+ }
344
+ }
345
+ settings.hooks = {
346
+ PreToolUse: [
347
+ {
348
+ matcher: ".*",
349
+ hooks: [
350
+ {
351
+ type: "command",
352
+ command: "node .claude/hooks/guard.mjs",
353
+ timeout: 5
354
+ }
355
+ ]
356
+ }
357
+ ],
358
+ PostToolUse: [
359
+ {
360
+ matcher: ".*",
361
+ hooks: [
362
+ {
363
+ type: "command",
364
+ command: "node .claude/hooks/audit.mjs",
365
+ timeout: 10,
366
+ async: true
367
+ }
368
+ ]
369
+ }
370
+ ]
371
+ };
372
+ const envObj = settings.env || {};
373
+ envObj.SOLONGATE_API_KEY = apiKey;
374
+ settings.env = envObj;
375
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
376
+ console.error(` Created ${settingsPath}`);
377
+ console.error("");
378
+ console.error(" Claude Code hooks installed!");
379
+ console.error(" PreToolUse \u2192 guard.mjs (blocks dangerous calls)");
380
+ console.error(" PostToolUse \u2192 audit.mjs (logs all calls to dashboard)");
381
+ }
149
382
  function ensureEnvFile() {
150
383
  const envPath = resolve(".env");
151
384
  if (!existsSync(envPath)) {
@@ -283,14 +516,34 @@ async function main() {
283
516
  process.exit(0);
284
517
  }
285
518
  console.error("");
286
- console.error(` Policy: ${options.policy}`);
519
+ let apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
520
+ if (!apiKey) {
521
+ const envPath = resolve(".env");
522
+ if (existsSync(envPath)) {
523
+ const envContent = readFileSync(envPath, "utf-8");
524
+ const match = envContent.match(/^SOLONGATE_API_KEY=(sg_(?:live|test)_\w+)/m);
525
+ if (match) apiKey = match[1];
526
+ }
527
+ }
528
+ if (!apiKey || apiKey === "sg_live_your_key_here") {
529
+ apiKey = await prompt(" Enter your SolonGate API key (from https://dashboard.solongate.com): ");
530
+ if (!apiKey) {
531
+ console.error(" API key is required. Get one at https://dashboard.solongate.com");
532
+ process.exit(1);
533
+ }
534
+ }
535
+ if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
536
+ console.error(" Invalid API key format. Must start with sg_live_ or sg_test_");
537
+ process.exit(1);
538
+ }
539
+ console.error(` Policy: ${options.policy}`);
540
+ console.error(` API Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
287
541
  console.error(` Protecting: ${toProtect.join(", ")}`);
288
542
  console.error("");
289
- const proxyPath = findProxyPath();
290
543
  const newConfig = { mcpServers: {} };
291
544
  for (const name of serverNames) {
292
545
  if (toProtect.includes(name)) {
293
- newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, proxyPath);
546
+ newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, apiKey);
294
547
  } else {
295
548
  newConfig.mcpServers[name] = config.mcpServers[name];
296
549
  }
@@ -315,22 +568,12 @@ async function main() {
315
568
  }
316
569
  console.error(" Config updated!");
317
570
  console.error("");
318
- 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");
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");
571
+ installClaudeCodeHooks(apiKey);
331
572
  console.error("");
332
573
  ensureEnvFile();
333
574
  console.error("");
575
+ console.error(" \u2500\u2500 Summary \u2500\u2500");
576
+ console.error("");
334
577
  for (const name of toProtect) {
335
578
  console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
336
579
  }
@@ -341,6 +584,18 @@ async function main() {
341
584
  console.error(` \u25CB ${name} \u2014 skipped`);
342
585
  }
343
586
  console.error("");
587
+ 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");
588
+ console.error(" \u2502 Setup complete! \u2502");
589
+ console.error(" \u2502 \u2502");
590
+ console.error(" \u2502 MCP tools \u2192 Proxy audit logging \u2502");
591
+ console.error(" \u2502 Built-in tools \u2192 Hook audit logging \u2502");
592
+ console.error(" \u2502 \u2502");
593
+ console.error(" \u2502 View logs: https://dashboard.solongate.com \u2502");
594
+ console.error(" \u2502 To undo: solongate-init --restore \u2502");
595
+ console.error(" \u2502 \u2502");
596
+ console.error(" \u2502 Restart your MCP client to apply changes. \u2502");
597
+ 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");
598
+ console.error("");
344
599
  }
345
600
  main().catch((err) => {
346
601
  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.1",
3
+ "version": "0.2.3",
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": {