@solongate/proxy 0.8.1 → 0.8.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.
package/dist/init.js CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  // src/init.ts
4
4
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
5
- import { resolve, join } from "path";
5
+ import { resolve, join, dirname } from "path";
6
+ import { fileURLToPath } from "url";
6
7
  import { createInterface } from "readline";
7
8
  var SEARCH_PATHS = [
8
9
  ".mcp.json",
@@ -134,353 +135,19 @@ EXAMPLES
134
135
  `;
135
136
  console.log(help);
136
137
  }
137
- var GUARD_SCRIPT = `#!/usr/bin/env node
138
- /**
139
- * SolonGate Policy Guard Hook (PreToolUse)
140
- * Reads policy.json and blocks tool calls that violate constraints.
141
- * Exit code 2 = BLOCK, exit code 0 = ALLOW.
142
- * Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
143
- * Auto-installed by: npx @solongate/proxy init
144
- */
145
- import { readFileSync, existsSync } from 'node:fs';
146
- import { resolve } from 'node:path';
147
-
148
- // \u2500\u2500 Load API key from .env file (Claude Code doesn't load .env into process.env) \u2500\u2500
149
- function loadEnvKey(dir) {
150
- try {
151
- const envPath = resolve(dir, '.env');
152
- if (!existsSync(envPath)) return {};
153
- const lines = readFileSync(envPath, 'utf-8').split('\\n');
154
- const env = {};
155
- for (const line of lines) {
156
- const m = line.match(/^([A-Z_]+)=(.*)$/);
157
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
158
- }
159
- return env;
160
- } catch { return {}; }
161
- }
162
-
163
- const hookCwdEarly = process.cwd();
164
- const dotenv = loadEnvKey(hookCwdEarly);
165
- const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
166
- const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
167
-
168
- // \u2500\u2500 Glob Matching \u2500\u2500
169
- function matchGlob(str, pattern) {
170
- if (pattern === '*') return true;
171
- const s = str.toLowerCase();
172
- const p = pattern.toLowerCase();
173
- if (s === p) return true;
174
- const startsW = p.startsWith('*');
175
- const endsW = p.endsWith('*');
176
- if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
177
- if (startsW) return s.endsWith(p.slice(1));
178
- if (endsW) return s.startsWith(p.slice(0, -1));
179
- const idx = p.indexOf('*');
180
- if (idx !== -1) {
181
- const pre = p.slice(0, idx);
182
- const suf = p.slice(idx + 1);
183
- return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
184
- }
185
- return false;
186
- }
187
-
188
- // \u2500\u2500 Path Glob (supports **) \u2500\u2500
189
- function matchPathGlob(path, pattern) {
190
- const p = path.replace(/\\\\/g, '/').toLowerCase();
191
- const g = pattern.replace(/\\\\/g, '/').toLowerCase();
192
- if (p === g) return true;
193
- if (g.includes('**')) {
194
- const parts = g.split('**').filter(s => s.length > 0);
195
- if (parts.length === 0) return true; // just ** or ****
196
- return parts.every(segment => p.includes(segment));
197
- }
198
- return matchGlob(p, g);
199
- }
200
-
201
- // \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
202
- function scanStrings(obj) {
203
- const strings = [];
204
- function walk(v) {
205
- if (typeof v === 'string' && v.trim()) strings.push(v.trim());
206
- else if (Array.isArray(v)) v.forEach(walk);
207
- else if (v && typeof v === 'object') Object.values(v).forEach(walk);
208
- }
209
- walk(obj);
210
- return strings;
211
- }
212
-
213
- function looksLikeFilename(s) {
214
- if (s.startsWith('.')) return true;
215
- if (/\\.\\w+$/.test(s)) return true;
216
- const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
217
- return known.includes(s.toLowerCase());
218
- }
219
-
220
- function extractFilenames(args) {
221
- const names = new Set();
222
- for (const s of scanStrings(args)) {
223
- if (/^https?:\\/\\//i.test(s)) continue;
224
- if (s.includes('/') || s.includes('\\\\')) {
225
- const base = s.replace(/\\\\/g, '/').split('/').pop();
226
- if (base) names.add(base);
227
- continue;
228
- }
229
- if (s.includes(' ')) {
230
- for (const tok of s.split(/\\s+/)) {
231
- if (tok.includes('/') || tok.includes('\\\\')) {
232
- const b = tok.replace(/\\\\/g, '/').split('/').pop();
233
- if (b && looksLikeFilename(b)) names.add(b);
234
- } else if (looksLikeFilename(tok)) names.add(tok);
235
- }
236
- continue;
237
- }
238
- if (looksLikeFilename(s)) names.add(s);
239
- }
240
- return [...names];
241
- }
242
-
243
- function extractUrls(args) {
244
- const urls = new Set();
245
- for (const s of scanStrings(args)) {
246
- if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
247
- if (s.includes(' ')) {
248
- for (const tok of s.split(/\\s+/)) {
249
- if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
250
- }
251
- }
252
- }
253
- return [...urls];
254
- }
255
-
256
- function extractCommands(args) {
257
- const cmds = [];
258
- const fields = ['command', 'cmd', 'function', 'script', 'shell'];
259
- if (typeof args === 'object' && args) {
260
- for (const [k, v] of Object.entries(args)) {
261
- if (fields.includes(k.toLowerCase()) && typeof v === 'string') {
262
- // Split chained commands: cd /path && npm install \u2192 [cd /path, npm install]
263
- for (const part of v.split(/\\s*(?:&&|\\|\\||;|\\|)\\s*/)) {
264
- const trimmed = part.trim();
265
- if (trimmed) cmds.push(trimmed);
266
- }
267
- }
268
- }
269
- }
270
- return cmds;
271
- }
272
-
273
- function extractPaths(args) {
274
- const paths = [];
275
- for (const s of scanStrings(args)) {
276
- if (/^https?:\\/\\//i.test(s)) continue;
277
- if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
278
- }
279
- return paths;
280
- }
281
-
282
- // \u2500\u2500 Policy Evaluation \u2500\u2500
283
- function evaluate(policy, args) {
284
- if (!policy || !policy.rules) return null;
285
- const denyRules = policy.rules
286
- .filter(r => r.effect === 'DENY' && r.enabled !== false)
287
- .sort((a, b) => (a.priority || 100) - (b.priority || 100));
288
-
289
- for (const rule of denyRules) {
290
- // Filename constraints
291
- if (rule.filenameConstraints && rule.filenameConstraints.denied) {
292
- const filenames = extractFilenames(args);
293
- for (const fn of filenames) {
294
- for (const pat of rule.filenameConstraints.denied) {
295
- if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
296
- }
297
- }
298
- }
299
- // URL constraints
300
- if (rule.urlConstraints && rule.urlConstraints.denied) {
301
- const urls = extractUrls(args);
302
- for (const url of urls) {
303
- for (const pat of rule.urlConstraints.denied) {
304
- if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
305
- }
306
- }
307
- }
308
- // Command constraints
309
- if (rule.commandConstraints && rule.commandConstraints.denied) {
310
- const cmds = extractCommands(args);
311
- for (const cmd of cmds) {
312
- for (const pat of rule.commandConstraints.denied) {
313
- if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
314
- }
315
- }
316
- }
317
- // Path constraints
318
- if (rule.pathConstraints && rule.pathConstraints.denied) {
319
- const paths = extractPaths(args);
320
- for (const p of paths) {
321
- for (const pat of rule.pathConstraints.denied) {
322
- if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
323
- }
324
- }
325
- }
326
- }
327
- return null;
138
+ var __dirname = dirname(fileURLToPath(import.meta.url));
139
+ var HOOKS_DIR = resolve(__dirname, "..", "hooks");
140
+ function readHookScript(filename) {
141
+ return readFileSync(join(HOOKS_DIR, filename), "utf-8");
328
142
  }
329
-
330
- // \u2500\u2500 Main \u2500\u2500
331
- let input = '';
332
- process.stdin.on('data', c => input += c);
333
- process.stdin.on('end', async () => {
334
- try {
335
- const data = JSON.parse(input);
336
- const args = data.tool_input || {};
337
-
338
- // \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
339
- const allStrings = scanStrings(args).map(s => s.replace(/\\\\/g, '/').toLowerCase());
340
- const protectedPaths = ['.solongate', '.claude', '.cursor', 'policy.json', '.mcp.json'];
341
- for (const s of allStrings) {
342
- for (const p of protectedPaths) {
343
- if (s.includes(p)) {
344
- const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
345
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
346
- try {
347
- await fetch(API_URL + '/api/v1/audit-logs', {
348
- method: 'POST',
349
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
350
- body: JSON.stringify({
351
- tool: data.tool_name || '', arguments: args,
352
- decision: 'DENY', reason: msg,
353
- source: 'claude-code-guard',
354
- }),
355
- signal: AbortSignal.timeout(3000),
356
- });
357
- } catch {}
358
- }
359
- process.stderr.write(msg);
360
- process.exit(2);
361
- }
362
- }
363
- }
364
-
365
- // Load policy (use cwd from hook data if available)
366
- const hookCwd = data.cwd || process.cwd();
367
- let policy;
368
- try {
369
- const policyPath = resolve(hookCwd, 'policy.json');
370
- policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
371
- } catch {
372
- process.exit(0); // No policy = allow all
373
- }
374
-
375
- const reason = evaluate(policy, args);
376
- const decision = reason ? 'DENY' : 'ALLOW';
377
-
378
- // \u2500\u2500 Log ALL decisions to SolonGate Cloud \u2500\u2500
379
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
380
- try {
381
- await fetch(API_URL + '/api/v1/audit-logs', {
382
- method: 'POST',
383
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
384
- body: JSON.stringify({
385
- tool: data.tool_name || '', arguments: args,
386
- decision, reason: reason || 'allowed by policy',
387
- source: 'claude-code-guard',
388
- }),
389
- signal: AbortSignal.timeout(3000),
390
- });
391
- } catch {}
392
- }
393
-
394
- if (reason) {
395
- process.stderr.write(reason);
396
- process.exit(2);
397
- }
398
- } catch {}
399
- process.exit(0);
400
- });
401
- `;
402
- var AUDIT_SCRIPT = `#!/usr/bin/env node
403
- /**
404
- * SolonGate Audit Hook for Claude Code (PostToolUse)
405
- * Logs tool execution results to SolonGate Cloud.
406
- * Auto-installed by: npx @solongate/proxy init
407
- */
408
- import { readFileSync, existsSync } from 'node:fs';
409
- import { resolve } from 'node:path';
410
-
411
- function loadEnvKey(dir) {
412
- try {
413
- const envPath = resolve(dir, '.env');
414
- if (!existsSync(envPath)) return {};
415
- const lines = readFileSync(envPath, 'utf-8').split('\\n');
416
- const env = {};
417
- for (const line of lines) {
418
- const m = line.match(/^([A-Z_]+)=(.*)$/);
419
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
420
- }
421
- return env;
422
- } catch { return {}; }
423
- }
424
-
425
- const dotenv = loadEnvKey(process.cwd());
426
- const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
427
- const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
428
-
429
- if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
430
-
431
- let input = '';
432
- process.stdin.on('data', c => input += c);
433
- process.stdin.on('end', async () => {
434
- try {
435
- const data = JSON.parse(input);
436
- const toolName = data.tool_name || 'unknown';
437
- const toolInput = data.tool_input || {};
438
-
439
- if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
440
- process.exit(0);
441
- }
442
-
443
- const hasError = data.tool_response?.error ||
444
- data.tool_response?.exitCode > 0 ||
445
- data.tool_response?.isError;
446
-
447
- const argsSummary = {};
448
- for (const [k, v] of Object.entries(toolInput)) {
449
- argsSummary[k] = typeof v === 'string' && v.length > 200
450
- ? v.slice(0, 200) + '...'
451
- : v;
452
- }
453
-
454
- await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
455
- method: 'POST',
456
- headers: {
457
- 'Authorization': \`Bearer \${API_KEY}\`,
458
- 'Content-Type': 'application/json',
459
- },
460
- body: JSON.stringify({
461
- tool: toolName,
462
- arguments: argsSummary,
463
- decision: hasError ? 'DENY' : 'ALLOW',
464
- reason: hasError ? 'tool returned error' : 'allowed',
465
- source: 'claude-code-hook',
466
- evaluationTimeMs: 0,
467
- }),
468
- signal: AbortSignal.timeout(5000),
469
- });
470
- } catch {
471
- // Silent
472
- }
473
- process.exit(0);
474
- });
475
- `;
476
143
  function installHooks() {
477
144
  const hooksDir = resolve(".solongate", "hooks");
478
145
  mkdirSync(hooksDir, { recursive: true });
479
146
  const guardPath = join(hooksDir, "guard.mjs");
480
- writeFileSync(guardPath, GUARD_SCRIPT);
147
+ writeFileSync(guardPath, readHookScript("guard.mjs"));
481
148
  console.log(` Created ${guardPath}`);
482
149
  const auditPath = join(hooksDir, "audit.mjs");
483
- writeFileSync(auditPath, AUDIT_SCRIPT);
150
+ writeFileSync(auditPath, readHookScript("audit.mjs"));
484
151
  console.log(` Created ${auditPath}`);
485
152
  const hookSettings = {
486
153
  hooks: {
@@ -575,7 +242,7 @@ async function main() {
575
242
  blue5: "\x1B[38;2;130;170;240m",
576
243
  blue6: "\x1B[38;2;170;200;250m"
577
244
  };
578
- const bannerLines = [
245
+ const fullBanner = [
579
246
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
580
247
  " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
581
248
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
@@ -583,10 +250,36 @@ async function main() {
583
250
  " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
584
251
  " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
585
252
  ];
253
+ const mediumBanner = [
254
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557",
255
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551",
256
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551",
257
+ " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551",
258
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551",
259
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D",
260
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
261
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D",
262
+ " \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
263
+ " \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ",
264
+ " \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
265
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
266
+ ];
267
+ const smallBanner = [
268
+ " \u250F\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513",
269
+ " \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2566 \u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557 \u2503",
270
+ " \u2503 \u255A\u2550\u2557\u2551 \u2551\u2551 \u2551 \u2551\u2551\u2551\u2551\u2551 \u2566 \u2503",
271
+ " \u2503 \u255A\u2550\u255D\u255A\u2550\u255D\u2569\u2550\u255D\u255A\u2550\u255D\u255D\u255A\u255D\u255A\u2550\u255D \u2503",
272
+ " \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2554\u2550\u2557 \u2503",
273
+ " \u2503 \u2551 \u2566\u2560\u2550\u2563 \u2551 \u2551\u2563 \u2503",
274
+ " \u2503 \u255A\u2550\u255D\u2569 \u2569 \u2569 \u255A\u2550\u255D \u2503",
275
+ " \u2517\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u251B"
276
+ ];
277
+ const cols = process.stdout.columns || 80;
278
+ const bannerLines = cols >= 82 ? fullBanner : cols >= 50 ? mediumBanner : smallBanner;
586
279
  const bannerColors = [_c.blue1, _c.blue2, _c.blue3, _c.blue4, _c.blue5, _c.blue6];
587
280
  console.log("");
588
281
  for (let i = 0; i < bannerLines.length; i++) {
589
- console.log(`${_c.bold}${bannerColors[i]}${bannerLines[i]}${_c.reset}`);
282
+ console.log(`${_c.bold}${bannerColors[i % bannerColors.length]}${bannerLines[i]}${_c.reset}`);
590
283
  }
591
284
  console.log("");
592
285
  console.log(` ${_c.dim}${_c.italic}Init Setup${_c.reset}`);
package/dist/pull-push.js CHANGED
@@ -5,7 +5,7 @@ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2
5
5
  import { resolve as resolve2 } from "path";
6
6
 
7
7
  // src/config.ts
8
- import { readFileSync, existsSync } from "fs";
8
+ import { readFileSync, existsSync, appendFileSync } from "fs";
9
9
  import { resolve } from "path";
10
10
  async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
11
11
  let resolvedId = policyId;
@@ -42,6 +42,7 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
42
42
  updatedAt: ""
43
43
  };
44
44
  }
45
+ var AUDIT_LOG_BACKUP_PATH = resolve(".solongate-audit-backup.jsonl");
45
46
 
46
47
  // src/pull-push.ts
47
48
  var log = (...args) => process.stderr.write(`${args.map(String).join(" ")}
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SolonGate Audit Hook for Claude Code (PostToolUse)
4
+ * Logs tool execution results to SolonGate Cloud.
5
+ * Auto-installed by: npx @solongate/proxy init
6
+ */
7
+ import { readFileSync, existsSync } from 'node:fs';
8
+ import { resolve } from 'node:path';
9
+
10
+ function loadEnvKey(dir) {
11
+ try {
12
+ const envPath = resolve(dir, '.env');
13
+ if (!existsSync(envPath)) return {};
14
+ const lines = readFileSync(envPath, 'utf-8').split('\n');
15
+ const env = {};
16
+ for (const line of lines) {
17
+ const m = line.match(/^([A-Z_]+)=(.*)$/);
18
+ if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
19
+ }
20
+ return env;
21
+ } catch { return {}; }
22
+ }
23
+
24
+ const dotenv = loadEnvKey(process.cwd());
25
+ const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
26
+ const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
27
+
28
+ if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
29
+
30
+ let input = '';
31
+ process.stdin.on('data', c => input += c);
32
+ process.stdin.on('end', async () => {
33
+ try {
34
+ const data = JSON.parse(input);
35
+ const toolName = data.tool_name || 'unknown';
36
+ const toolInput = data.tool_input || {};
37
+
38
+ if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
39
+ process.exit(0);
40
+ }
41
+
42
+ const hasError = data.tool_response?.error ||
43
+ data.tool_response?.exitCode > 0 ||
44
+ data.tool_response?.isError;
45
+
46
+ const argsSummary = {};
47
+ for (const [k, v] of Object.entries(toolInput)) {
48
+ argsSummary[k] = typeof v === 'string' && v.length > 200
49
+ ? v.slice(0, 200) + '...'
50
+ : v;
51
+ }
52
+
53
+ await fetch(`${API_URL}/api/v1/audit-logs`, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'Authorization': `Bearer ${API_KEY}`,
57
+ 'Content-Type': 'application/json',
58
+ },
59
+ body: JSON.stringify({
60
+ tool: toolName,
61
+ arguments: argsSummary,
62
+ decision: hasError ? 'DENY' : 'ALLOW',
63
+ reason: hasError ? 'tool returned error' : 'allowed',
64
+ source: 'claude-code-hook',
65
+ evaluationTimeMs: 0,
66
+ }),
67
+ signal: AbortSignal.timeout(5000),
68
+ });
69
+ } catch {
70
+ // Silent
71
+ }
72
+ process.exit(0);
73
+ });