@solongate/proxy 0.8.2 → 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/index.js CHANGED
@@ -5,7 +5,7 @@ var __esm = (fn, res) => function __init() {
5
5
  };
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;
@@ -44,22 +44,38 @@ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
44
44
  }
45
45
  async function sendAuditLog(apiKey, apiUrl, entry) {
46
46
  const url = `${apiUrl}/api/v1/audit-logs`;
47
- try {
48
- const res = await fetch(url, {
49
- method: "POST",
50
- headers: {
51
- "Authorization": `Bearer ${apiKey}`,
52
- "Content-Type": "application/json"
53
- },
54
- body: JSON.stringify(entry)
55
- });
56
- if (!res.ok) {
57
- const body = await res.text().catch(() => "");
58
- process.stderr.write(`[SolonGate] Audit log failed (${res.status}): ${body}
47
+ const body = JSON.stringify(entry);
48
+ for (let attempt = 0; attempt < AUDIT_MAX_RETRIES; attempt++) {
49
+ try {
50
+ const res = await fetch(url, {
51
+ method: "POST",
52
+ headers: {
53
+ "Authorization": `Bearer ${apiKey}`,
54
+ "Content-Type": "application/json"
55
+ },
56
+ body,
57
+ signal: AbortSignal.timeout(5e3)
58
+ });
59
+ if (res.ok) return;
60
+ if (res.status >= 400 && res.status < 500) {
61
+ const resBody = await res.text().catch(() => "");
62
+ process.stderr.write(`[SolonGate] Audit log rejected (${res.status}): ${resBody}
59
63
  `);
64
+ return;
65
+ }
66
+ } catch {
60
67
  }
68
+ if (attempt < AUDIT_MAX_RETRIES - 1) {
69
+ await new Promise((r) => setTimeout(r, 500 * Math.pow(2, attempt)));
70
+ }
71
+ }
72
+ process.stderr.write(`[SolonGate] Audit log failed after ${AUDIT_MAX_RETRIES} retries, saving to local backup.
73
+ `);
74
+ try {
75
+ const line = JSON.stringify({ ...entry, timestamp: (/* @__PURE__ */ new Date()).toISOString() }) + "\n";
76
+ appendFileSync(AUDIT_LOG_BACKUP_PATH, line, "utf-8");
61
77
  } catch (err) {
62
- process.stderr.write(`[SolonGate] Audit log error: ${err instanceof Error ? err.message : String(err)}
78
+ process.stderr.write(`[SolonGate] Audit backup write error: ${err instanceof Error ? err.message : String(err)}
63
79
  `);
64
80
  }
65
81
  }
@@ -260,10 +276,12 @@ function resolvePolicyPath(source) {
260
276
  if (existsSync(filePath)) return filePath;
261
277
  return null;
262
278
  }
263
- var DEFAULT_POLICY;
279
+ var AUDIT_MAX_RETRIES, AUDIT_LOG_BACKUP_PATH, DEFAULT_POLICY;
264
280
  var init_config = __esm({
265
281
  "src/config.ts"() {
266
282
  "use strict";
283
+ AUDIT_MAX_RETRIES = 3;
284
+ AUDIT_LOG_BACKUP_PATH = resolve(".solongate-audit-backup.jsonl");
267
285
  DEFAULT_POLICY = {
268
286
  id: "default",
269
287
  name: "Default (Deny All)",
@@ -278,8 +296,9 @@ var init_config = __esm({
278
296
 
279
297
  // src/init.ts
280
298
  var init_exports = {};
281
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync } from "fs";
282
- import { resolve as resolve2, join } from "path";
299
+ import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
300
+ import { resolve as resolve2, join, dirname as dirname2 } from "path";
301
+ import { fileURLToPath } from "url";
283
302
  import { createInterface } from "readline";
284
303
  function findConfigFile(explicitPath, createIfMissing = false) {
285
304
  if (explicitPath) {
@@ -404,14 +423,17 @@ EXAMPLES
404
423
  `;
405
424
  console.log(help);
406
425
  }
426
+ function readHookScript(filename) {
427
+ return readFileSync3(join(HOOKS_DIR, filename), "utf-8");
428
+ }
407
429
  function installHooks() {
408
430
  const hooksDir = resolve2(".solongate", "hooks");
409
- mkdirSync(hooksDir, { recursive: true });
431
+ mkdirSync2(hooksDir, { recursive: true });
410
432
  const guardPath = join(hooksDir, "guard.mjs");
411
- writeFileSync2(guardPath, GUARD_SCRIPT);
433
+ writeFileSync2(guardPath, readHookScript("guard.mjs"));
412
434
  console.log(` Created ${guardPath}`);
413
435
  const auditPath = join(hooksDir, "audit.mjs");
414
- writeFileSync2(auditPath, AUDIT_SCRIPT);
436
+ writeFileSync2(auditPath, readHookScript("audit.mjs"));
415
437
  console.log(` Created ${auditPath}`);
416
438
  const hookSettings = {
417
439
  hooks: {
@@ -439,7 +461,7 @@ function installHooks() {
439
461
  ];
440
462
  for (const client of clients) {
441
463
  const clientDir = resolve2(client.dir);
442
- mkdirSync(clientDir, { recursive: true });
464
+ mkdirSync2(clientDir, { recursive: true });
443
465
  const settingsPath = join(clientDir, "settings.json");
444
466
  let existing = {};
445
467
  try {
@@ -714,7 +736,7 @@ async function main() {
714
736
  console.log(" \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");
715
737
  console.log("");
716
738
  }
717
- var SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, sleep, GUARD_SCRIPT, AUDIT_SCRIPT;
739
+ var SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, sleep, __dirname, HOOKS_DIR;
718
740
  var init_init = __esm({
719
741
  "src/init.ts"() {
720
742
  "use strict";
@@ -725,345 +747,8 @@ var init_init = __esm({
725
747
  ];
726
748
  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")];
727
749
  sleep = (ms) => new Promise((r) => setTimeout(r, ms));
728
- GUARD_SCRIPT = `#!/usr/bin/env node
729
- /**
730
- * SolonGate Policy Guard Hook (PreToolUse)
731
- * Reads policy.json and blocks tool calls that violate constraints.
732
- * Exit code 2 = BLOCK, exit code 0 = ALLOW.
733
- * Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
734
- * Auto-installed by: npx @solongate/proxy init
735
- */
736
- import { readFileSync, existsSync } from 'node:fs';
737
- import { resolve } from 'node:path';
738
-
739
- // \u2500\u2500 Load API key from .env file (Claude Code doesn't load .env into process.env) \u2500\u2500
740
- function loadEnvKey(dir) {
741
- try {
742
- const envPath = resolve(dir, '.env');
743
- if (!existsSync(envPath)) return {};
744
- const lines = readFileSync(envPath, 'utf-8').split('\\n');
745
- const env = {};
746
- for (const line of lines) {
747
- const m = line.match(/^([A-Z_]+)=(.*)$/);
748
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
749
- }
750
- return env;
751
- } catch { return {}; }
752
- }
753
-
754
- const hookCwdEarly = process.cwd();
755
- const dotenv = loadEnvKey(hookCwdEarly);
756
- const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
757
- const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
758
-
759
- // \u2500\u2500 Glob Matching \u2500\u2500
760
- function matchGlob(str, pattern) {
761
- if (pattern === '*') return true;
762
- const s = str.toLowerCase();
763
- const p = pattern.toLowerCase();
764
- if (s === p) return true;
765
- const startsW = p.startsWith('*');
766
- const endsW = p.endsWith('*');
767
- if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
768
- if (startsW) return s.endsWith(p.slice(1));
769
- if (endsW) return s.startsWith(p.slice(0, -1));
770
- const idx = p.indexOf('*');
771
- if (idx !== -1) {
772
- const pre = p.slice(0, idx);
773
- const suf = p.slice(idx + 1);
774
- return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
775
- }
776
- return false;
777
- }
778
-
779
- // \u2500\u2500 Path Glob (supports **) \u2500\u2500
780
- function matchPathGlob(path, pattern) {
781
- const p = path.replace(/\\\\/g, '/').toLowerCase();
782
- const g = pattern.replace(/\\\\/g, '/').toLowerCase();
783
- if (p === g) return true;
784
- if (g.includes('**')) {
785
- const parts = g.split('**').filter(s => s.length > 0);
786
- if (parts.length === 0) return true; // just ** or ****
787
- return parts.every(segment => p.includes(segment));
788
- }
789
- return matchGlob(p, g);
790
- }
791
-
792
- // \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
793
- function scanStrings(obj) {
794
- const strings = [];
795
- function walk(v) {
796
- if (typeof v === 'string' && v.trim()) strings.push(v.trim());
797
- else if (Array.isArray(v)) v.forEach(walk);
798
- else if (v && typeof v === 'object') Object.values(v).forEach(walk);
799
- }
800
- walk(obj);
801
- return strings;
802
- }
803
-
804
- function looksLikeFilename(s) {
805
- if (s.startsWith('.')) return true;
806
- if (/\\.\\w+$/.test(s)) return true;
807
- const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
808
- return known.includes(s.toLowerCase());
809
- }
810
-
811
- function extractFilenames(args) {
812
- const names = new Set();
813
- for (const s of scanStrings(args)) {
814
- if (/^https?:\\/\\//i.test(s)) continue;
815
- if (s.includes('/') || s.includes('\\\\')) {
816
- const base = s.replace(/\\\\/g, '/').split('/').pop();
817
- if (base) names.add(base);
818
- continue;
819
- }
820
- if (s.includes(' ')) {
821
- for (const tok of s.split(/\\s+/)) {
822
- if (tok.includes('/') || tok.includes('\\\\')) {
823
- const b = tok.replace(/\\\\/g, '/').split('/').pop();
824
- if (b && looksLikeFilename(b)) names.add(b);
825
- } else if (looksLikeFilename(tok)) names.add(tok);
826
- }
827
- continue;
828
- }
829
- if (looksLikeFilename(s)) names.add(s);
830
- }
831
- return [...names];
832
- }
833
-
834
- function extractUrls(args) {
835
- const urls = new Set();
836
- for (const s of scanStrings(args)) {
837
- if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
838
- if (s.includes(' ')) {
839
- for (const tok of s.split(/\\s+/)) {
840
- if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
841
- }
842
- }
843
- }
844
- return [...urls];
845
- }
846
-
847
- function extractCommands(args) {
848
- const cmds = [];
849
- const fields = ['command', 'cmd', 'function', 'script', 'shell'];
850
- if (typeof args === 'object' && args) {
851
- for (const [k, v] of Object.entries(args)) {
852
- if (fields.includes(k.toLowerCase()) && typeof v === 'string') {
853
- // Split chained commands: cd /path && npm install \u2192 [cd /path, npm install]
854
- for (const part of v.split(/\\s*(?:&&|\\|\\||;|\\|)\\s*/)) {
855
- const trimmed = part.trim();
856
- if (trimmed) cmds.push(trimmed);
857
- }
858
- }
859
- }
860
- }
861
- return cmds;
862
- }
863
-
864
- function extractPaths(args) {
865
- const paths = [];
866
- for (const s of scanStrings(args)) {
867
- if (/^https?:\\/\\//i.test(s)) continue;
868
- if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
869
- }
870
- return paths;
871
- }
872
-
873
- // \u2500\u2500 Policy Evaluation \u2500\u2500
874
- function evaluate(policy, args) {
875
- if (!policy || !policy.rules) return null;
876
- const denyRules = policy.rules
877
- .filter(r => r.effect === 'DENY' && r.enabled !== false)
878
- .sort((a, b) => (a.priority || 100) - (b.priority || 100));
879
-
880
- for (const rule of denyRules) {
881
- // Filename constraints
882
- if (rule.filenameConstraints && rule.filenameConstraints.denied) {
883
- const filenames = extractFilenames(args);
884
- for (const fn of filenames) {
885
- for (const pat of rule.filenameConstraints.denied) {
886
- if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
887
- }
888
- }
889
- }
890
- // URL constraints
891
- if (rule.urlConstraints && rule.urlConstraints.denied) {
892
- const urls = extractUrls(args);
893
- for (const url of urls) {
894
- for (const pat of rule.urlConstraints.denied) {
895
- if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
896
- }
897
- }
898
- }
899
- // Command constraints
900
- if (rule.commandConstraints && rule.commandConstraints.denied) {
901
- const cmds = extractCommands(args);
902
- for (const cmd of cmds) {
903
- for (const pat of rule.commandConstraints.denied) {
904
- if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
905
- }
906
- }
907
- }
908
- // Path constraints
909
- if (rule.pathConstraints && rule.pathConstraints.denied) {
910
- const paths = extractPaths(args);
911
- for (const p of paths) {
912
- for (const pat of rule.pathConstraints.denied) {
913
- if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
914
- }
915
- }
916
- }
917
- }
918
- return null;
919
- }
920
-
921
- // \u2500\u2500 Main \u2500\u2500
922
- let input = '';
923
- process.stdin.on('data', c => input += c);
924
- process.stdin.on('end', async () => {
925
- try {
926
- const data = JSON.parse(input);
927
- const args = data.tool_input || {};
928
-
929
- // \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
930
- const allStrings = scanStrings(args).map(s => s.replace(/\\\\/g, '/').toLowerCase());
931
- const protectedPaths = ['.solongate', '.claude', '.cursor', 'policy.json', '.mcp.json'];
932
- for (const s of allStrings) {
933
- for (const p of protectedPaths) {
934
- if (s.includes(p)) {
935
- const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
936
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
937
- try {
938
- await fetch(API_URL + '/api/v1/audit-logs', {
939
- method: 'POST',
940
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
941
- body: JSON.stringify({
942
- tool: data.tool_name || '', arguments: args,
943
- decision: 'DENY', reason: msg,
944
- source: 'claude-code-guard',
945
- }),
946
- signal: AbortSignal.timeout(3000),
947
- });
948
- } catch {}
949
- }
950
- process.stderr.write(msg);
951
- process.exit(2);
952
- }
953
- }
954
- }
955
-
956
- // Load policy (use cwd from hook data if available)
957
- const hookCwd = data.cwd || process.cwd();
958
- let policy;
959
- try {
960
- const policyPath = resolve(hookCwd, 'policy.json');
961
- policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
962
- } catch {
963
- process.exit(0); // No policy = allow all
964
- }
965
-
966
- const reason = evaluate(policy, args);
967
- const decision = reason ? 'DENY' : 'ALLOW';
968
-
969
- // \u2500\u2500 Log ALL decisions to SolonGate Cloud \u2500\u2500
970
- if (API_KEY && API_KEY.startsWith('sg_live_')) {
971
- try {
972
- await fetch(API_URL + '/api/v1/audit-logs', {
973
- method: 'POST',
974
- headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
975
- body: JSON.stringify({
976
- tool: data.tool_name || '', arguments: args,
977
- decision, reason: reason || 'allowed by policy',
978
- source: 'claude-code-guard',
979
- }),
980
- signal: AbortSignal.timeout(3000),
981
- });
982
- } catch {}
983
- }
984
-
985
- if (reason) {
986
- process.stderr.write(reason);
987
- process.exit(2);
988
- }
989
- } catch {}
990
- process.exit(0);
991
- });
992
- `;
993
- AUDIT_SCRIPT = `#!/usr/bin/env node
994
- /**
995
- * SolonGate Audit Hook for Claude Code (PostToolUse)
996
- * Logs tool execution results to SolonGate Cloud.
997
- * Auto-installed by: npx @solongate/proxy init
998
- */
999
- import { readFileSync, existsSync } from 'node:fs';
1000
- import { resolve } from 'node:path';
1001
-
1002
- function loadEnvKey(dir) {
1003
- try {
1004
- const envPath = resolve(dir, '.env');
1005
- if (!existsSync(envPath)) return {};
1006
- const lines = readFileSync(envPath, 'utf-8').split('\\n');
1007
- const env = {};
1008
- for (const line of lines) {
1009
- const m = line.match(/^([A-Z_]+)=(.*)$/);
1010
- if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
1011
- }
1012
- return env;
1013
- } catch { return {}; }
1014
- }
1015
-
1016
- const dotenv = loadEnvKey(process.cwd());
1017
- const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
1018
- const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
1019
-
1020
- if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
1021
-
1022
- let input = '';
1023
- process.stdin.on('data', c => input += c);
1024
- process.stdin.on('end', async () => {
1025
- try {
1026
- const data = JSON.parse(input);
1027
- const toolName = data.tool_name || 'unknown';
1028
- const toolInput = data.tool_input || {};
1029
-
1030
- if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
1031
- process.exit(0);
1032
- }
1033
-
1034
- const hasError = data.tool_response?.error ||
1035
- data.tool_response?.exitCode > 0 ||
1036
- data.tool_response?.isError;
1037
-
1038
- const argsSummary = {};
1039
- for (const [k, v] of Object.entries(toolInput)) {
1040
- argsSummary[k] = typeof v === 'string' && v.length > 200
1041
- ? v.slice(0, 200) + '...'
1042
- : v;
1043
- }
1044
-
1045
- await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
1046
- method: 'POST',
1047
- headers: {
1048
- 'Authorization': \`Bearer \${API_KEY}\`,
1049
- 'Content-Type': 'application/json',
1050
- },
1051
- body: JSON.stringify({
1052
- tool: toolName,
1053
- arguments: argsSummary,
1054
- decision: hasError ? 'DENY' : 'ALLOW',
1055
- reason: hasError ? 'tool returned error' : 'allowed',
1056
- source: 'claude-code-hook',
1057
- evaluationTimeMs: 0,
1058
- }),
1059
- signal: AbortSignal.timeout(5000),
1060
- });
1061
- } catch {
1062
- // Silent
1063
- }
1064
- process.exit(0);
1065
- });
1066
- `;
750
+ __dirname = dirname2(fileURLToPath(import.meta.url));
751
+ HOOKS_DIR = resolve2(__dirname, "..", "hooks");
1067
752
  main().catch((err) => {
1068
753
  console.log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
1069
754
  process.exit(1);
@@ -1445,7 +1130,7 @@ var init_inject = __esm({
1445
1130
 
1446
1131
  // src/create.ts
1447
1132
  var create_exports = {};
1448
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
1133
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
1449
1134
  import { resolve as resolve4, join as join2 } from "path";
1450
1135
  import { execSync as execSync2 } from "child_process";
1451
1136
  function log4(msg) {
@@ -1597,7 +1282,7 @@ function createProject(dir, name, _policy) {
1597
1282
  2
1598
1283
  ) + "\n"
1599
1284
  );
1600
- mkdirSync2(join2(dir, "src"), { recursive: true });
1285
+ mkdirSync3(join2(dir, "src"), { recursive: true });
1601
1286
  writeFileSync4(
1602
1287
  join2(dir, "src", "index.ts"),
1603
1288
  `#!/usr/bin/env node
@@ -1686,7 +1371,7 @@ async function main3() {
1686
1371
  process.exit(1);
1687
1372
  }
1688
1373
  withSpinner(`Setting up ${opts.name}...`, () => {
1689
- mkdirSync2(dir, { recursive: true });
1374
+ mkdirSync3(dir, { recursive: true });
1690
1375
  createProject(dir, opts.name, opts.policy);
1691
1376
  });
1692
1377
  if (!opts.noInstall) {
@@ -2194,7 +1879,7 @@ var PolicyRuleSchema = z.object({
2194
1879
  effect: z.enum(["ALLOW", "DENY"]),
2195
1880
  priority: z.number().int().min(0).max(1e4).default(1e3),
2196
1881
  toolPattern: z.string().min(1).max(512),
2197
- permission: z.enum(["READ", "WRITE", "EXECUTE"]),
1882
+ permission: z.enum(["READ", "WRITE", "EXECUTE"]).optional(),
2198
1883
  minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
2199
1884
  argumentConstraints: z.record(z.unknown()).optional(),
2200
1885
  pathConstraints: z.object({
@@ -2241,6 +1926,7 @@ function createSecurityContext(params) {
2241
1926
  var DEFAULT_POLICY_EFFECT = "DENY";
2242
1927
  var MAX_RULES_PER_POLICY_SET = 1e3;
2243
1928
  var POLICY_EVALUATION_TIMEOUT_MS = 100;
1929
+ var TOKEN_MAX_AGE_SECONDS = 300;
2244
1930
  var RATE_LIMIT_WINDOW_MS = 6e4;
2245
1931
  var RATE_LIMIT_MAX_ENTRIES = 1e4;
2246
1932
  var UNSAFE_CONFIGURATION_WARNINGS = {
@@ -2713,7 +2399,7 @@ function extractUrlArguments(args) {
2713
2399
  addUrl(value);
2714
2400
  return;
2715
2401
  }
2716
- if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}(\/.*)?$/.test(value)) {
2402
+ if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+(?:com|net|org|io|dev|app|co|me|info|biz|gov|edu|mil|onion|xyz|ai|cloud|sh|run|so|to|cc|tv|fm|am|gg|id)(\/.*)?$/.test(value)) {
2717
2403
  addUrl(value);
2718
2404
  return;
2719
2405
  }
@@ -2776,7 +2462,7 @@ function isUrlAllowed(url, constraints) {
2776
2462
  }
2777
2463
  function ruleMatchesRequest(rule, request) {
2778
2464
  if (!rule.enabled) return false;
2779
- if (rule.permission !== request.requiredPermission) return false;
2465
+ if (rule.permission && rule.permission !== request.requiredPermission) return false;
2780
2466
  if (!toolPatternMatches(rule.toolPattern, request.toolName)) return false;
2781
2467
  if (!trustLevelMeetsMinimum(request.context.trustLevel, rule.minimumTrustLevel)) {
2782
2468
  return false;
@@ -2972,7 +2658,7 @@ function validatePolicyRule(input) {
2972
2658
  if (rule.minimumTrustLevel === "TRUSTED") {
2973
2659
  warnings.push(UNSAFE_CONFIGURATION_WARNINGS.TRUSTED_LEVEL_EXTERNAL);
2974
2660
  }
2975
- if (rule.permission === "EXECUTE") {
2661
+ if (!rule.permission || rule.permission === "EXECUTE") {
2976
2662
  warnings.push(UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW);
2977
2663
  }
2978
2664
  return { valid: true, errors, warnings };
@@ -3049,7 +2735,7 @@ function analyzeRuleWarnings(rule) {
3049
2735
  recommendation: "Set minimumTrustLevel to VERIFIED or higher for ALLOW rules."
3050
2736
  });
3051
2737
  }
3052
- if (rule.effect === "ALLOW" && rule.permission === "EXECUTE") {
2738
+ if (rule.effect === "ALLOW" && (!rule.permission || rule.permission === "EXECUTE")) {
3053
2739
  warnings.push({
3054
2740
  level: "WARNING",
3055
2741
  code: "ALLOW_EXECUTE",
@@ -3454,12 +3140,47 @@ var SecurityLogger = class {
3454
3140
  }
3455
3141
  }
3456
3142
  };
3143
+ var ExpiringSet = class {
3144
+ entries = /* @__PURE__ */ new Map();
3145
+ ttlMs;
3146
+ sweepIntervalMs;
3147
+ lastSweep = 0;
3148
+ constructor(ttlMs, sweepIntervalMs) {
3149
+ this.ttlMs = ttlMs;
3150
+ this.sweepIntervalMs = sweepIntervalMs ?? Math.max(ttlMs, 6e4);
3151
+ }
3152
+ add(value) {
3153
+ this.entries.set(value, Date.now());
3154
+ this.maybeSweep();
3155
+ }
3156
+ has(value) {
3157
+ const ts = this.entries.get(value);
3158
+ if (ts === void 0) return false;
3159
+ if (Date.now() - ts > this.ttlMs) {
3160
+ this.entries.delete(value);
3161
+ return false;
3162
+ }
3163
+ return true;
3164
+ }
3165
+ get size() {
3166
+ return this.entries.size;
3167
+ }
3168
+ maybeSweep() {
3169
+ const now = Date.now();
3170
+ if (now - this.lastSweep < this.sweepIntervalMs) return;
3171
+ this.lastSweep = now;
3172
+ const cutoff = now - this.ttlMs;
3173
+ for (const [key, ts] of this.entries) {
3174
+ if (ts < cutoff) this.entries.delete(key);
3175
+ }
3176
+ }
3177
+ };
3457
3178
  var TokenIssuer = class {
3458
3179
  secret;
3459
3180
  ttlSeconds;
3460
3181
  issuer;
3461
- usedNonces = /* @__PURE__ */ new Set();
3462
- revokedTokens = /* @__PURE__ */ new Set();
3182
+ usedNonces;
3183
+ revokedTokens;
3463
3184
  constructor(config) {
3464
3185
  if (config.secret.length < MIN_SECRET_LENGTH) {
3465
3186
  throw new Error(
@@ -3469,6 +3190,9 @@ var TokenIssuer = class {
3469
3190
  this.secret = config.secret;
3470
3191
  this.ttlSeconds = config.ttlSeconds || DEFAULT_TOKEN_TTL_SECONDS;
3471
3192
  this.issuer = config.issuer;
3193
+ const maxAgMs = TOKEN_MAX_AGE_SECONDS * 1e3;
3194
+ this.usedNonces = new ExpiringSet(maxAgMs);
3195
+ this.revokedTokens = new ExpiringSet(maxAgMs);
3472
3196
  }
3473
3197
  /**
3474
3198
  * Issues a signed capability token.
@@ -3563,13 +3287,14 @@ function base64UrlDecode(str) {
3563
3287
  var ServerVerifier = class {
3564
3288
  gatewaySecret;
3565
3289
  maxAgeMs;
3566
- usedNonces = /* @__PURE__ */ new Set();
3290
+ usedNonces;
3567
3291
  constructor(config) {
3568
3292
  if (config.gatewaySecret.length < 32) {
3569
3293
  throw new Error("Gateway secret must be at least 32 characters");
3570
3294
  }
3571
3295
  this.gatewaySecret = config.gatewaySecret;
3572
3296
  this.maxAgeMs = config.maxAgeMs ?? 6e4;
3297
+ this.usedNonces = new ExpiringSet(this.maxAgeMs * 2);
3573
3298
  }
3574
3299
  /**
3575
3300
  * Computes HMAC signature for request data.
@@ -3630,12 +3355,75 @@ var ServerVerifier = class {
3630
3355
  return { valid: true };
3631
3356
  }
3632
3357
  };
3358
+ var CircularTimestampBuffer = class {
3359
+ buf;
3360
+ head = 0;
3361
+ // next write position
3362
+ size = 0;
3363
+ // current number of entries
3364
+ constructor(capacity) {
3365
+ this.buf = new Float64Array(capacity);
3366
+ }
3367
+ push(timestamp) {
3368
+ this.buf[this.head] = timestamp;
3369
+ this.head = (this.head + 1) % this.buf.length;
3370
+ if (this.size < this.buf.length) this.size++;
3371
+ }
3372
+ /**
3373
+ * Count entries with timestamp > windowStart.
3374
+ * Since timestamps are monotonically increasing in the ring,
3375
+ * we use binary search on the logical sorted order.
3376
+ */
3377
+ countAfter(windowStart) {
3378
+ if (this.size === 0) return 0;
3379
+ const oldest = this.at(0);
3380
+ if (oldest > windowStart) return this.size;
3381
+ let lo = 0;
3382
+ let hi = this.size;
3383
+ while (lo < hi) {
3384
+ const mid = lo + hi >>> 1;
3385
+ if (this.at(mid) > windowStart) {
3386
+ hi = mid;
3387
+ } else {
3388
+ lo = mid + 1;
3389
+ }
3390
+ }
3391
+ return this.size - lo;
3392
+ }
3393
+ /** Get the oldest entry timestamp (for resetAt calculation) */
3394
+ oldestInWindow(windowStart) {
3395
+ if (this.size === 0) return null;
3396
+ let lo = 0;
3397
+ let hi = this.size;
3398
+ while (lo < hi) {
3399
+ const mid = lo + hi >>> 1;
3400
+ if (this.at(mid) > windowStart) {
3401
+ hi = mid;
3402
+ } else {
3403
+ lo = mid + 1;
3404
+ }
3405
+ }
3406
+ return lo < this.size ? this.at(lo) : null;
3407
+ }
3408
+ /** Access logical index (0 = oldest) */
3409
+ at(logicalIndex) {
3410
+ const start = this.size < this.buf.length ? 0 : this.head;
3411
+ return this.buf[(start + logicalIndex) % this.buf.length];
3412
+ }
3413
+ clear() {
3414
+ this.head = 0;
3415
+ this.size = 0;
3416
+ }
3417
+ };
3633
3418
  var RateLimiter = class {
3634
3419
  windowMs;
3635
- records = /* @__PURE__ */ new Map();
3636
- globalRecords = [];
3420
+ maxEntries;
3421
+ buffers = /* @__PURE__ */ new Map();
3422
+ globalBuffer;
3637
3423
  constructor(options) {
3638
3424
  this.windowMs = options?.windowMs ?? RATE_LIMIT_WINDOW_MS;
3425
+ this.maxEntries = options?.maxEntries ?? RATE_LIMIT_MAX_ENTRIES;
3426
+ this.globalBuffer = new CircularTimestampBuffer(this.maxEntries);
3639
3427
  }
3640
3428
  /**
3641
3429
  * Checks if a tool call is within the rate limit.
@@ -3644,11 +3432,15 @@ var RateLimiter = class {
3644
3432
  checkLimit(toolName, limitPerWindow) {
3645
3433
  const now = Date.now();
3646
3434
  const windowStart = now - this.windowMs;
3647
- const records = this.getActiveRecords(toolName, windowStart);
3648
- const count = records.length;
3435
+ const buffer = this.buffers.get(toolName);
3436
+ if (!buffer) {
3437
+ return { allowed: true, remaining: limitPerWindow, resetAt: now + this.windowMs };
3438
+ }
3439
+ const count = buffer.countAfter(windowStart);
3649
3440
  const allowed = count < limitPerWindow;
3650
3441
  const remaining = Math.max(0, limitPerWindow - count);
3651
- const resetAt = records.length > 0 ? records[0].timestamp + this.windowMs : now + this.windowMs;
3442
+ const oldest = buffer.oldestInWindow(windowStart);
3443
+ const resetAt = oldest !== null ? oldest + this.windowMs : now + this.windowMs;
3652
3444
  return { allowed, remaining, resetAt };
3653
3445
  }
3654
3446
  /**
@@ -3657,13 +3449,11 @@ var RateLimiter = class {
3657
3449
  checkGlobalLimit(limitPerWindow) {
3658
3450
  const now = Date.now();
3659
3451
  const windowStart = now - this.windowMs;
3660
- this.globalRecords = this.globalRecords.filter(
3661
- (r) => r.timestamp > windowStart
3662
- );
3663
- const count = this.globalRecords.length;
3452
+ const count = this.globalBuffer.countAfter(windowStart);
3664
3453
  const allowed = count < limitPerWindow;
3665
3454
  const remaining = Math.max(0, limitPerWindow - count);
3666
- const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
3455
+ const oldest = this.globalBuffer.oldestInWindow(windowStart);
3456
+ const resetAt = oldest !== null ? oldest + this.windowMs : now + this.windowMs;
3667
3457
  return { allowed, remaining, resetAt };
3668
3458
  }
3669
3459
  /**
@@ -3691,23 +3481,13 @@ var RateLimiter = class {
3691
3481
  */
3692
3482
  recordCall(toolName) {
3693
3483
  const now = Date.now();
3694
- const record = { timestamp: now };
3695
- const records = this.records.get(toolName) ?? [];
3696
- records.push(record);
3697
- if (records.length > RATE_LIMIT_MAX_ENTRIES) {
3698
- const windowStart = now - this.windowMs;
3699
- const cleaned = records.filter((r) => r.timestamp > windowStart);
3700
- this.records.set(toolName, cleaned);
3701
- } else {
3702
- this.records.set(toolName, records);
3703
- }
3704
- this.globalRecords.push(record);
3705
- if (this.globalRecords.length > RATE_LIMIT_MAX_ENTRIES) {
3706
- const windowStart = now - this.windowMs;
3707
- this.globalRecords = this.globalRecords.filter(
3708
- (r) => r.timestamp > windowStart
3709
- );
3484
+ let buffer = this.buffers.get(toolName);
3485
+ if (!buffer) {
3486
+ buffer = new CircularTimestampBuffer(Math.min(this.maxEntries, 1e3));
3487
+ this.buffers.set(toolName, buffer);
3710
3488
  }
3489
+ buffer.push(now);
3490
+ this.globalBuffer.push(now);
3711
3491
  }
3712
3492
  /**
3713
3493
  * Gets usage stats for a tool.
@@ -3715,29 +3495,22 @@ var RateLimiter = class {
3715
3495
  getUsage(toolName) {
3716
3496
  const now = Date.now();
3717
3497
  const windowStart = now - this.windowMs;
3718
- const records = this.getActiveRecords(toolName, windowStart);
3719
- return { count: records.length, windowStart };
3498
+ const buffer = this.buffers.get(toolName);
3499
+ const count = buffer ? buffer.countAfter(windowStart) : 0;
3500
+ return { count, windowStart };
3720
3501
  }
3721
3502
  /**
3722
3503
  * Resets rate tracking for a specific tool.
3723
3504
  */
3724
3505
  resetTool(toolName) {
3725
- this.records.delete(toolName);
3506
+ this.buffers.delete(toolName);
3726
3507
  }
3727
3508
  /**
3728
3509
  * Resets all rate tracking.
3729
3510
  */
3730
3511
  resetAll() {
3731
- this.records.clear();
3732
- this.globalRecords = [];
3733
- }
3734
- getActiveRecords(toolName, windowStart) {
3735
- const records = this.records.get(toolName) ?? [];
3736
- const active = records.filter((r) => r.timestamp > windowStart);
3737
- if (active.length !== records.length) {
3738
- this.records.set(toolName, active);
3739
- }
3740
- return active;
3512
+ this.buffers.clear();
3513
+ this.globalBuffer = new CircularTimestampBuffer(this.maxEntries);
3741
3514
  }
3742
3515
  };
3743
3516
  var LicenseError = class extends Error {
@@ -3760,6 +3533,7 @@ var SolonGate = class {
3760
3533
  rateLimiter;
3761
3534
  apiKey;
3762
3535
  licenseValidated = false;
3536
+ pollingTimer = null;
3763
3537
  constructor(options) {
3764
3538
  const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
3765
3539
  if (!apiKey) {
@@ -3871,7 +3645,7 @@ var SolonGate = class {
3871
3645
  startPolicyPolling() {
3872
3646
  const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
3873
3647
  let currentVersion = 0;
3874
- setInterval(async () => {
3648
+ this.pollingTimer = setInterval(async () => {
3875
3649
  try {
3876
3650
  const res = await fetch(`${apiUrl}/api/v1/policies/default`, {
3877
3651
  headers: { "Authorization": `Bearer ${this.apiKey}` },
@@ -3978,6 +3752,13 @@ var SolonGate = class {
3978
3752
  getTokenIssuer() {
3979
3753
  return this.tokenIssuer;
3980
3754
  }
3755
+ /** Stop policy polling and release resources. */
3756
+ destroy() {
3757
+ if (this.pollingTimer) {
3758
+ clearInterval(this.pollingTimer);
3759
+ this.pollingTimer = null;
3760
+ }
3761
+ }
3981
3762
  };
3982
3763
 
3983
3764
  // ../core/dist/index.js
@@ -4548,13 +4329,22 @@ var log2 = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).joi
4548
4329
  var Mutex = class {
4549
4330
  queue = [];
4550
4331
  locked = false;
4551
- async acquire() {
4332
+ async acquire(timeoutMs = 3e4) {
4552
4333
  if (!this.locked) {
4553
4334
  this.locked = true;
4554
4335
  return;
4555
4336
  }
4556
- return new Promise((resolve6) => {
4557
- this.queue.push(resolve6);
4337
+ return new Promise((resolve6, reject) => {
4338
+ const timer = setTimeout(() => {
4339
+ const idx = this.queue.indexOf(onReady);
4340
+ if (idx !== -1) this.queue.splice(idx, 1);
4341
+ reject(new Error("Mutex acquire timeout"));
4342
+ }, timeoutMs);
4343
+ const onReady = () => {
4344
+ clearTimeout(timer);
4345
+ resolve6();
4346
+ };
4347
+ this.queue.push(onReady);
4558
4348
  });
4559
4349
  }
4560
4350
  release() {
@@ -4566,12 +4356,23 @@ var Mutex = class {
4566
4356
  }
4567
4357
  }
4568
4358
  };
4359
+ var ToolMutexMap = class {
4360
+ mutexes = /* @__PURE__ */ new Map();
4361
+ get(toolName) {
4362
+ let mutex = this.mutexes.get(toolName);
4363
+ if (!mutex) {
4364
+ mutex = new Mutex();
4365
+ this.mutexes.set(toolName, mutex);
4366
+ }
4367
+ return mutex;
4368
+ }
4369
+ };
4569
4370
  var SolonGateProxy = class {
4570
4371
  config;
4571
4372
  gate;
4572
4373
  client = null;
4573
4374
  server = null;
4574
- callMutex = new Mutex();
4375
+ toolMutexes = new ToolMutexMap();
4575
4376
  syncManager = null;
4576
4377
  upstreamTools = [];
4577
4378
  constructor(config) {
@@ -4733,9 +4534,10 @@ var SolonGateProxy = class {
4733
4534
  return { tools: this.upstreamTools };
4734
4535
  });
4735
4536
  const MAX_ARGUMENT_SIZE = 1024 * 1024;
4537
+ const MUTEX_TIMEOUT_MS = 3e4;
4736
4538
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
4737
4539
  const { name, arguments: args } = request.params;
4738
- const argsSize = JSON.stringify(args ?? {}).length;
4540
+ const argsSize = new TextEncoder().encode(JSON.stringify(args ?? {})).length;
4739
4541
  if (argsSize > MAX_ARGUMENT_SIZE) {
4740
4542
  log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
4741
4543
  return {
@@ -4744,7 +4546,16 @@ var SolonGateProxy = class {
4744
4546
  };
4745
4547
  }
4746
4548
  log2(`Tool call: ${name}`);
4747
- await this.callMutex.acquire();
4549
+ const mutex = this.toolMutexes.get(name);
4550
+ try {
4551
+ await mutex.acquire(MUTEX_TIMEOUT_MS);
4552
+ } catch {
4553
+ log2(`DENY: ${name} \u2014 mutex timeout (${MUTEX_TIMEOUT_MS}ms)`);
4554
+ return {
4555
+ content: [{ type: "text", text: `Tool call queued too long (>${MUTEX_TIMEOUT_MS / 1e3}s). Try again.` }],
4556
+ isError: true
4557
+ };
4558
+ }
4748
4559
  const startTime = Date.now();
4749
4560
  try {
4750
4561
  const result = await this.gate.executeToolCall(
@@ -4793,7 +4604,7 @@ var SolonGateProxy = class {
4793
4604
  isError: result.isError
4794
4605
  };
4795
4606
  } finally {
4796
- this.callMutex.release();
4607
+ mutex.release();
4797
4608
  }
4798
4609
  });
4799
4610
  this.server.setRequestHandler(ListResourcesRequestSchema, async () => {