@solongate/proxy 0.28.4 → 0.28.5

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 CHANGED
@@ -678,6 +678,9 @@ function installHooks(selectedTools = []) {
678
678
  const auditPath = join(hooksDir, "audit.mjs");
679
679
  writeFileSync2(auditPath, readHookScript("audit.mjs"));
680
680
  console.log(` Created ${auditPath}`);
681
+ const stopPath = join(hooksDir, "stop.mjs");
682
+ writeFileSync2(stopPath, readHookScript("stop.mjs"));
683
+ console.log(` Created ${stopPath}`);
681
684
  const hookSettings = {
682
685
  hooks: {
683
686
  PreToolUse: [
@@ -695,6 +698,14 @@ function installHooks(selectedTools = []) {
695
698
  { type: "command", command: "node .solongate/hooks/audit.mjs" }
696
699
  ]
697
700
  }
701
+ ],
702
+ Stop: [
703
+ {
704
+ matcher: "",
705
+ hooks: [
706
+ { type: "command", command: "node .solongate/hooks/stop.mjs" }
707
+ ]
708
+ }
698
709
  ]
699
710
  }
700
711
  };
@@ -726,6 +737,7 @@ function installHooks(selectedTools = []) {
726
737
  console.log(" Hooks installed:");
727
738
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
728
739
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
740
+ console.log(" stop.mjs \u2192 tracks text-only responses (no tool calls)");
729
741
  console.log(` Activated for: ${activatedNames.join(", ")}`);
730
742
  }
731
743
  function ensureEnvFile() {
package/dist/init.js CHANGED
@@ -261,6 +261,9 @@ function installHooks(selectedTools = []) {
261
261
  const auditPath = join(hooksDir, "audit.mjs");
262
262
  writeFileSync(auditPath, readHookScript("audit.mjs"));
263
263
  console.log(` Created ${auditPath}`);
264
+ const stopPath = join(hooksDir, "stop.mjs");
265
+ writeFileSync(stopPath, readHookScript("stop.mjs"));
266
+ console.log(` Created ${stopPath}`);
264
267
  const hookSettings = {
265
268
  hooks: {
266
269
  PreToolUse: [
@@ -278,6 +281,14 @@ function installHooks(selectedTools = []) {
278
281
  { type: "command", command: "node .solongate/hooks/audit.mjs" }
279
282
  ]
280
283
  }
284
+ ],
285
+ Stop: [
286
+ {
287
+ matcher: "",
288
+ hooks: [
289
+ { type: "command", command: "node .solongate/hooks/stop.mjs" }
290
+ ]
291
+ }
281
292
  ]
282
293
  }
283
294
  };
@@ -309,6 +320,7 @@ function installHooks(selectedTools = []) {
309
320
  console.log(" Hooks installed:");
310
321
  console.log(" guard.mjs \u2192 blocks policy-violating calls (pre-execution)");
311
322
  console.log(" audit.mjs \u2192 logs all calls to dashboard (post-execution)");
323
+ console.log(" stop.mjs \u2192 tracks text-only responses (no tool calls)");
312
324
  console.log(` Activated for: ${activatedNames.join(", ")}`);
313
325
  }
314
326
  function ensureEnvFile() {
package/hooks/audit.mjs CHANGED
@@ -4,8 +4,8 @@
4
4
  * Logs tool execution results to SolonGate Cloud.
5
5
  * Auto-installed by: npx @solongate/proxy init
6
6
  */
7
- import { readFileSync, existsSync } from 'node:fs';
8
- import { resolve } from 'node:path';
7
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
8
+ import { resolve, join } from 'node:path';
9
9
 
10
10
  function loadEnvKey(dir) {
11
11
  try {
@@ -50,6 +50,13 @@ process.stdin.on('end', async () => {
50
50
  : v;
51
51
  }
52
52
 
53
+ // Write flag so stop.mjs knows tool calls happened (skip text-only ALLOW)
54
+ try {
55
+ const flagDir = resolve('.solongate');
56
+ mkdirSync(flagDir, { recursive: true });
57
+ writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
58
+ } catch {}
59
+
53
60
  await fetch(`${API_URL}/api/v1/audit-logs`, {
54
61
  method: 'POST',
55
62
  headers: {
package/hooks/guard.mjs CHANGED
@@ -4,11 +4,11 @@
4
4
  * Reads policy.json and blocks tool calls that violate constraints.
5
5
  * Also runs prompt injection detection (Stage 1 rules) on tool arguments.
6
6
  * Exit code 2 = BLOCK, exit code 0 = ALLOW.
7
- * Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
7
+ * Logs DENY decisions to SolonGate Cloud. ALLOWs are logged by audit.mjs.
8
8
  * Auto-installed by: npx @solongate/proxy init
9
9
  */
10
- import { readFileSync, existsSync, statSync } from 'node:fs';
11
- import { resolve } from 'node:path';
10
+ import { readFileSync, existsSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
11
+ import { resolve, join } from 'node:path';
12
12
 
13
13
  // Safe file read with size limit (1MB max) to prevent DoS via large files
14
14
  const MAX_FILE_READ = 1024 * 1024; // 1MB
@@ -40,6 +40,15 @@ const dotenv = loadEnvKey(hookCwdEarly);
40
40
  const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
41
41
  const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
42
42
 
43
+ // Write flag file so stop.mjs knows a tool call (DENY) happened and doesn't log extra ALLOW
44
+ function writeDenyFlag() {
45
+ try {
46
+ const flagDir = resolve('.solongate');
47
+ mkdirSync(flagDir, { recursive: true });
48
+ writeFileSync(join(flagDir, '.last-tool-call'), Date.now().toString());
49
+ } catch {}
50
+ }
51
+
43
52
  // ── Prompt Injection Detection (Stage 1: Rule-Based) ──
44
53
  const PI_CATEGORIES = [
45
54
  {
@@ -352,6 +361,7 @@ process.stdin.on('end', async () => {
352
361
  });
353
362
  } catch {}
354
363
  }
364
+ writeDenyFlag();
355
365
  process.stderr.write(reason);
356
366
  process.exit(2);
357
367
  }
@@ -1015,6 +1025,7 @@ process.stdin.on('end', async () => {
1015
1025
  process.stderr.write(msg);
1016
1026
  // Fall through to policy evaluation (don't exit)
1017
1027
  } else {
1028
+ writeDenyFlag();
1018
1029
  process.stderr.write(msg);
1019
1030
  process.exit(2);
1020
1031
  }
@@ -1205,6 +1216,7 @@ Respond with ONLY valid JSON: {"decision": "ALLOW" or "DENY", "reason": "brief e
1205
1216
  });
1206
1217
  } catch {}
1207
1218
  }
1219
+ writeDenyFlag();
1208
1220
  process.stderr.write(reason);
1209
1221
  process.exit(2);
1210
1222
  }
package/hooks/stop.mjs ADDED
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SolonGate Stop Hook (Stop event)
4
+ * Logs a single ALLOW when Claude responds WITHOUT any tool calls.
5
+ * If tool calls were made, audit.mjs already logged them — this hook skips.
6
+ * Auto-installed by: npx @solongate/proxy init
7
+ */
8
+ import { readFileSync, existsSync, unlinkSync, writeFileSync } from 'node:fs';
9
+ import { resolve, join } from 'node:path';
10
+
11
+ function loadEnvKey(dir) {
12
+ try {
13
+ const envPath = resolve(dir, '.env');
14
+ if (!existsSync(envPath)) return {};
15
+ const lines = readFileSync(envPath, 'utf-8').split('\n');
16
+ const env = {};
17
+ for (const line of lines) {
18
+ const m = line.match(/^([A-Z_]+)=(.*)$/);
19
+ if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
20
+ }
21
+ return env;
22
+ } catch { return {}; }
23
+ }
24
+
25
+ const dotenv = loadEnvKey(process.cwd());
26
+ const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
27
+ const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
28
+
29
+ if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
30
+
31
+ // Flag file: audit.mjs writes this when a tool call was logged in this turn.
32
+ // If the flag exists, tool calls were made → skip (already logged).
33
+ // If the flag doesn't exist, no tool calls → log 1 ALLOW for the text response.
34
+ const flagDir = resolve('.solongate');
35
+ const flagFile = join(flagDir, '.last-tool-call');
36
+
37
+ let input = '';
38
+ process.stdin.on('data', c => input += c);
39
+ process.stdin.on('end', async () => {
40
+ try {
41
+ // Check if tool calls were made in this turn
42
+ if (existsSync(flagFile)) {
43
+ // Tool calls happened → audit.mjs already logged them. Clean up flag.
44
+ try { unlinkSync(flagFile); } catch {}
45
+ process.exit(0);
46
+ }
47
+
48
+ // No tool calls → log 1 ALLOW for the text-only response
49
+ await fetch(`${API_URL}/api/v1/audit-logs`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'Authorization': `Bearer ${API_KEY}`,
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ body: JSON.stringify({
56
+ tool: '_response',
57
+ arguments: {},
58
+ decision: 'ALLOW',
59
+ reason: 'text response (no tool calls)',
60
+ source: 'claude-code-hook',
61
+ evaluationTimeMs: 0,
62
+ agent_id: 'claude-code',
63
+ agent_name: 'Claude Code',
64
+ }),
65
+ signal: AbortSignal.timeout(5000),
66
+ });
67
+ } catch {
68
+ // Silent
69
+ }
70
+ process.exit(0);
71
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.28.4",
3
+ "version": "0.28.5",
4
4
  "description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {