@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/index.js +258 -421
- package/dist/init.js +36 -343
- package/dist/pull-push.js +2 -1
- package/hooks/audit.mjs +73 -0
- package/hooks/guard.mjs +264 -0
- package/package.json +2 -1
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
431
|
+
mkdirSync2(hooksDir, { recursive: true });
|
|
410
432
|
const guardPath = join(hooksDir, "guard.mjs");
|
|
411
|
-
writeFileSync2(guardPath,
|
|
433
|
+
writeFileSync2(guardPath, readHookScript("guard.mjs"));
|
|
412
434
|
console.log(` Created ${guardPath}`);
|
|
413
435
|
const auditPath = join(hooksDir, "audit.mjs");
|
|
414
|
-
writeFileSync2(auditPath,
|
|
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
|
-
|
|
464
|
+
mkdirSync2(clientDir, { recursive: true });
|
|
443
465
|
const settingsPath = join(clientDir, "settings.json");
|
|
444
466
|
let existing = {};
|
|
445
467
|
try {
|
|
@@ -506,7 +528,7 @@ async function main() {
|
|
|
506
528
|
blue5: "\x1B[38;2;130;170;240m",
|
|
507
529
|
blue6: "\x1B[38;2;170;200;250m"
|
|
508
530
|
};
|
|
509
|
-
const
|
|
531
|
+
const fullBanner = [
|
|
510
532
|
" \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",
|
|
511
533
|
" \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",
|
|
512
534
|
" \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 ",
|
|
@@ -514,10 +536,36 @@ async function main() {
|
|
|
514
536
|
" \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",
|
|
515
537
|
" \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"
|
|
516
538
|
];
|
|
539
|
+
const mediumBanner = [
|
|
540
|
+
" \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",
|
|
541
|
+
" \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",
|
|
542
|
+
" \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",
|
|
543
|
+
" \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",
|
|
544
|
+
" \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",
|
|
545
|
+
" \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",
|
|
546
|
+
" \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",
|
|
547
|
+
" \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",
|
|
548
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2557 ",
|
|
549
|
+
" \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u255D ",
|
|
550
|
+
" \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",
|
|
551
|
+
" \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D"
|
|
552
|
+
];
|
|
553
|
+
const smallBanner = [
|
|
554
|
+
" \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",
|
|
555
|
+
" \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2566 \u2554\u2550\u2557\u2554\u2557\u2554\u2554\u2550\u2557 \u2503",
|
|
556
|
+
" \u2503 \u255A\u2550\u2557\u2551 \u2551\u2551 \u2551 \u2551\u2551\u2551\u2551\u2551 \u2566 \u2503",
|
|
557
|
+
" \u2503 \u255A\u2550\u255D\u255A\u2550\u255D\u2569\u2550\u255D\u255A\u2550\u255D\u255D\u255A\u255D\u255A\u2550\u255D \u2503",
|
|
558
|
+
" \u2503 \u2554\u2550\u2557\u2554\u2550\u2557\u2554\u2566\u2557\u2554\u2550\u2557 \u2503",
|
|
559
|
+
" \u2503 \u2551 \u2566\u2560\u2550\u2563 \u2551 \u2551\u2563 \u2503",
|
|
560
|
+
" \u2503 \u255A\u2550\u255D\u2569 \u2569 \u2569 \u255A\u2550\u255D \u2503",
|
|
561
|
+
" \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"
|
|
562
|
+
];
|
|
563
|
+
const cols = process.stdout.columns || 80;
|
|
564
|
+
const bannerLines = cols >= 82 ? fullBanner : cols >= 50 ? mediumBanner : smallBanner;
|
|
517
565
|
const bannerColors = [_c.blue1, _c.blue2, _c.blue3, _c.blue4, _c.blue5, _c.blue6];
|
|
518
566
|
console.log("");
|
|
519
567
|
for (let i = 0; i < bannerLines.length; i++) {
|
|
520
|
-
console.log(`${_c.bold}${bannerColors[i]}${bannerLines[i]}${_c.reset}`);
|
|
568
|
+
console.log(`${_c.bold}${bannerColors[i % bannerColors.length]}${bannerLines[i]}${_c.reset}`);
|
|
521
569
|
}
|
|
522
570
|
console.log("");
|
|
523
571
|
console.log(` ${_c.dim}${_c.italic}Init Setup${_c.reset}`);
|
|
@@ -688,7 +736,7 @@ async function main() {
|
|
|
688
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");
|
|
689
737
|
console.log("");
|
|
690
738
|
}
|
|
691
|
-
var SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, sleep,
|
|
739
|
+
var SEARCH_PATHS, CLAUDE_DESKTOP_PATHS, sleep, __dirname, HOOKS_DIR;
|
|
692
740
|
var init_init = __esm({
|
|
693
741
|
"src/init.ts"() {
|
|
694
742
|
"use strict";
|
|
@@ -699,345 +747,8 @@ var init_init = __esm({
|
|
|
699
747
|
];
|
|
700
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")];
|
|
701
749
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
* SolonGate Policy Guard Hook (PreToolUse)
|
|
705
|
-
* Reads policy.json and blocks tool calls that violate constraints.
|
|
706
|
-
* Exit code 2 = BLOCK, exit code 0 = ALLOW.
|
|
707
|
-
* Logs ALL decisions (ALLOW + DENY) to SolonGate Cloud.
|
|
708
|
-
* Auto-installed by: npx @solongate/proxy init
|
|
709
|
-
*/
|
|
710
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
711
|
-
import { resolve } from 'node:path';
|
|
712
|
-
|
|
713
|
-
// \u2500\u2500 Load API key from .env file (Claude Code doesn't load .env into process.env) \u2500\u2500
|
|
714
|
-
function loadEnvKey(dir) {
|
|
715
|
-
try {
|
|
716
|
-
const envPath = resolve(dir, '.env');
|
|
717
|
-
if (!existsSync(envPath)) return {};
|
|
718
|
-
const lines = readFileSync(envPath, 'utf-8').split('\\n');
|
|
719
|
-
const env = {};
|
|
720
|
-
for (const line of lines) {
|
|
721
|
-
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
722
|
-
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
|
|
723
|
-
}
|
|
724
|
-
return env;
|
|
725
|
-
} catch { return {}; }
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
const hookCwdEarly = process.cwd();
|
|
729
|
-
const dotenv = loadEnvKey(hookCwdEarly);
|
|
730
|
-
const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
|
|
731
|
-
const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
732
|
-
|
|
733
|
-
// \u2500\u2500 Glob Matching \u2500\u2500
|
|
734
|
-
function matchGlob(str, pattern) {
|
|
735
|
-
if (pattern === '*') return true;
|
|
736
|
-
const s = str.toLowerCase();
|
|
737
|
-
const p = pattern.toLowerCase();
|
|
738
|
-
if (s === p) return true;
|
|
739
|
-
const startsW = p.startsWith('*');
|
|
740
|
-
const endsW = p.endsWith('*');
|
|
741
|
-
if (startsW && endsW) { const infix = p.slice(1, -1); return infix.length > 0 && s.includes(infix); }
|
|
742
|
-
if (startsW) return s.endsWith(p.slice(1));
|
|
743
|
-
if (endsW) return s.startsWith(p.slice(0, -1));
|
|
744
|
-
const idx = p.indexOf('*');
|
|
745
|
-
if (idx !== -1) {
|
|
746
|
-
const pre = p.slice(0, idx);
|
|
747
|
-
const suf = p.slice(idx + 1);
|
|
748
|
-
return s.startsWith(pre) && s.endsWith(suf) && s.length >= pre.length + suf.length;
|
|
749
|
-
}
|
|
750
|
-
return false;
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
// \u2500\u2500 Path Glob (supports **) \u2500\u2500
|
|
754
|
-
function matchPathGlob(path, pattern) {
|
|
755
|
-
const p = path.replace(/\\\\/g, '/').toLowerCase();
|
|
756
|
-
const g = pattern.replace(/\\\\/g, '/').toLowerCase();
|
|
757
|
-
if (p === g) return true;
|
|
758
|
-
if (g.includes('**')) {
|
|
759
|
-
const parts = g.split('**').filter(s => s.length > 0);
|
|
760
|
-
if (parts.length === 0) return true; // just ** or ****
|
|
761
|
-
return parts.every(segment => p.includes(segment));
|
|
762
|
-
}
|
|
763
|
-
return matchGlob(p, g);
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
// \u2500\u2500 Extract Functions (deep scan all string values) \u2500\u2500
|
|
767
|
-
function scanStrings(obj) {
|
|
768
|
-
const strings = [];
|
|
769
|
-
function walk(v) {
|
|
770
|
-
if (typeof v === 'string' && v.trim()) strings.push(v.trim());
|
|
771
|
-
else if (Array.isArray(v)) v.forEach(walk);
|
|
772
|
-
else if (v && typeof v === 'object') Object.values(v).forEach(walk);
|
|
773
|
-
}
|
|
774
|
-
walk(obj);
|
|
775
|
-
return strings;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function looksLikeFilename(s) {
|
|
779
|
-
if (s.startsWith('.')) return true;
|
|
780
|
-
if (/\\.\\w+$/.test(s)) return true;
|
|
781
|
-
const known = ['id_rsa','id_dsa','id_ecdsa','id_ed25519','authorized_keys','known_hosts','makefile','dockerfile'];
|
|
782
|
-
return known.includes(s.toLowerCase());
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function extractFilenames(args) {
|
|
786
|
-
const names = new Set();
|
|
787
|
-
for (const s of scanStrings(args)) {
|
|
788
|
-
if (/^https?:\\/\\//i.test(s)) continue;
|
|
789
|
-
if (s.includes('/') || s.includes('\\\\')) {
|
|
790
|
-
const base = s.replace(/\\\\/g, '/').split('/').pop();
|
|
791
|
-
if (base) names.add(base);
|
|
792
|
-
continue;
|
|
793
|
-
}
|
|
794
|
-
if (s.includes(' ')) {
|
|
795
|
-
for (const tok of s.split(/\\s+/)) {
|
|
796
|
-
if (tok.includes('/') || tok.includes('\\\\')) {
|
|
797
|
-
const b = tok.replace(/\\\\/g, '/').split('/').pop();
|
|
798
|
-
if (b && looksLikeFilename(b)) names.add(b);
|
|
799
|
-
} else if (looksLikeFilename(tok)) names.add(tok);
|
|
800
|
-
}
|
|
801
|
-
continue;
|
|
802
|
-
}
|
|
803
|
-
if (looksLikeFilename(s)) names.add(s);
|
|
804
|
-
}
|
|
805
|
-
return [...names];
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function extractUrls(args) {
|
|
809
|
-
const urls = new Set();
|
|
810
|
-
for (const s of scanStrings(args)) {
|
|
811
|
-
if (/^https?:\\/\\//i.test(s)) { urls.add(s); continue; }
|
|
812
|
-
if (s.includes(' ')) {
|
|
813
|
-
for (const tok of s.split(/\\s+/)) {
|
|
814
|
-
if (/^https?:\\/\\//i.test(tok)) urls.add(tok);
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
return [...urls];
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
function extractCommands(args) {
|
|
822
|
-
const cmds = [];
|
|
823
|
-
const fields = ['command', 'cmd', 'function', 'script', 'shell'];
|
|
824
|
-
if (typeof args === 'object' && args) {
|
|
825
|
-
for (const [k, v] of Object.entries(args)) {
|
|
826
|
-
if (fields.includes(k.toLowerCase()) && typeof v === 'string') {
|
|
827
|
-
// Split chained commands: cd /path && npm install \u2192 [cd /path, npm install]
|
|
828
|
-
for (const part of v.split(/\\s*(?:&&|\\|\\||;|\\|)\\s*/)) {
|
|
829
|
-
const trimmed = part.trim();
|
|
830
|
-
if (trimmed) cmds.push(trimmed);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return cmds;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function extractPaths(args) {
|
|
839
|
-
const paths = [];
|
|
840
|
-
for (const s of scanStrings(args)) {
|
|
841
|
-
if (/^https?:\\/\\//i.test(s)) continue;
|
|
842
|
-
if (s.includes('/') || s.includes('\\\\') || s.startsWith('.')) paths.push(s);
|
|
843
|
-
}
|
|
844
|
-
return paths;
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
// \u2500\u2500 Policy Evaluation \u2500\u2500
|
|
848
|
-
function evaluate(policy, args) {
|
|
849
|
-
if (!policy || !policy.rules) return null;
|
|
850
|
-
const denyRules = policy.rules
|
|
851
|
-
.filter(r => r.effect === 'DENY' && r.enabled !== false)
|
|
852
|
-
.sort((a, b) => (a.priority || 100) - (b.priority || 100));
|
|
853
|
-
|
|
854
|
-
for (const rule of denyRules) {
|
|
855
|
-
// Filename constraints
|
|
856
|
-
if (rule.filenameConstraints && rule.filenameConstraints.denied) {
|
|
857
|
-
const filenames = extractFilenames(args);
|
|
858
|
-
for (const fn of filenames) {
|
|
859
|
-
for (const pat of rule.filenameConstraints.denied) {
|
|
860
|
-
if (matchGlob(fn, pat)) return 'Blocked by policy: filename "' + fn + '" matches "' + pat + '"';
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
// URL constraints
|
|
865
|
-
if (rule.urlConstraints && rule.urlConstraints.denied) {
|
|
866
|
-
const urls = extractUrls(args);
|
|
867
|
-
for (const url of urls) {
|
|
868
|
-
for (const pat of rule.urlConstraints.denied) {
|
|
869
|
-
if (matchGlob(url, pat)) return 'Blocked by policy: URL "' + url + '" matches "' + pat + '"';
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
// Command constraints
|
|
874
|
-
if (rule.commandConstraints && rule.commandConstraints.denied) {
|
|
875
|
-
const cmds = extractCommands(args);
|
|
876
|
-
for (const cmd of cmds) {
|
|
877
|
-
for (const pat of rule.commandConstraints.denied) {
|
|
878
|
-
if (matchGlob(cmd, pat)) return 'Blocked by policy: command "' + cmd.slice(0,60) + '" matches "' + pat + '"';
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
// Path constraints
|
|
883
|
-
if (rule.pathConstraints && rule.pathConstraints.denied) {
|
|
884
|
-
const paths = extractPaths(args);
|
|
885
|
-
for (const p of paths) {
|
|
886
|
-
for (const pat of rule.pathConstraints.denied) {
|
|
887
|
-
if (matchPathGlob(p, pat)) return 'Blocked by policy: path "' + p + '" matches "' + pat + '"';
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
return null;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// \u2500\u2500 Main \u2500\u2500
|
|
896
|
-
let input = '';
|
|
897
|
-
process.stdin.on('data', c => input += c);
|
|
898
|
-
process.stdin.on('end', async () => {
|
|
899
|
-
try {
|
|
900
|
-
const data = JSON.parse(input);
|
|
901
|
-
const args = data.tool_input || {};
|
|
902
|
-
|
|
903
|
-
// \u2500\u2500 Self-protection: block access to hook files and settings \u2500\u2500
|
|
904
|
-
const allStrings = scanStrings(args).map(s => s.replace(/\\\\/g, '/').toLowerCase());
|
|
905
|
-
const protectedPaths = ['.solongate', '.claude', '.cursor', 'policy.json', '.mcp.json'];
|
|
906
|
-
for (const s of allStrings) {
|
|
907
|
-
for (const p of protectedPaths) {
|
|
908
|
-
if (s.includes(p)) {
|
|
909
|
-
const msg = 'SOLONGATE: Access to protected file "' + p + '" is blocked';
|
|
910
|
-
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
911
|
-
try {
|
|
912
|
-
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
913
|
-
method: 'POST',
|
|
914
|
-
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
915
|
-
body: JSON.stringify({
|
|
916
|
-
tool: data.tool_name || '', arguments: args,
|
|
917
|
-
decision: 'DENY', reason: msg,
|
|
918
|
-
source: 'claude-code-guard',
|
|
919
|
-
}),
|
|
920
|
-
signal: AbortSignal.timeout(3000),
|
|
921
|
-
});
|
|
922
|
-
} catch {}
|
|
923
|
-
}
|
|
924
|
-
process.stderr.write(msg);
|
|
925
|
-
process.exit(2);
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// Load policy (use cwd from hook data if available)
|
|
931
|
-
const hookCwd = data.cwd || process.cwd();
|
|
932
|
-
let policy;
|
|
933
|
-
try {
|
|
934
|
-
const policyPath = resolve(hookCwd, 'policy.json');
|
|
935
|
-
policy = JSON.parse(readFileSync(policyPath, 'utf-8'));
|
|
936
|
-
} catch {
|
|
937
|
-
process.exit(0); // No policy = allow all
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const reason = evaluate(policy, args);
|
|
941
|
-
const decision = reason ? 'DENY' : 'ALLOW';
|
|
942
|
-
|
|
943
|
-
// \u2500\u2500 Log ALL decisions to SolonGate Cloud \u2500\u2500
|
|
944
|
-
if (API_KEY && API_KEY.startsWith('sg_live_')) {
|
|
945
|
-
try {
|
|
946
|
-
await fetch(API_URL + '/api/v1/audit-logs', {
|
|
947
|
-
method: 'POST',
|
|
948
|
-
headers: { 'Authorization': 'Bearer ' + API_KEY, 'Content-Type': 'application/json' },
|
|
949
|
-
body: JSON.stringify({
|
|
950
|
-
tool: data.tool_name || '', arguments: args,
|
|
951
|
-
decision, reason: reason || 'allowed by policy',
|
|
952
|
-
source: 'claude-code-guard',
|
|
953
|
-
}),
|
|
954
|
-
signal: AbortSignal.timeout(3000),
|
|
955
|
-
});
|
|
956
|
-
} catch {}
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
if (reason) {
|
|
960
|
-
process.stderr.write(reason);
|
|
961
|
-
process.exit(2);
|
|
962
|
-
}
|
|
963
|
-
} catch {}
|
|
964
|
-
process.exit(0);
|
|
965
|
-
});
|
|
966
|
-
`;
|
|
967
|
-
AUDIT_SCRIPT = `#!/usr/bin/env node
|
|
968
|
-
/**
|
|
969
|
-
* SolonGate Audit Hook for Claude Code (PostToolUse)
|
|
970
|
-
* Logs tool execution results to SolonGate Cloud.
|
|
971
|
-
* Auto-installed by: npx @solongate/proxy init
|
|
972
|
-
*/
|
|
973
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
974
|
-
import { resolve } from 'node:path';
|
|
975
|
-
|
|
976
|
-
function loadEnvKey(dir) {
|
|
977
|
-
try {
|
|
978
|
-
const envPath = resolve(dir, '.env');
|
|
979
|
-
if (!existsSync(envPath)) return {};
|
|
980
|
-
const lines = readFileSync(envPath, 'utf-8').split('\\n');
|
|
981
|
-
const env = {};
|
|
982
|
-
for (const line of lines) {
|
|
983
|
-
const m = line.match(/^([A-Z_]+)=(.*)$/);
|
|
984
|
-
if (m) env[m[1]] = m[2].replace(/^["']|["']$/g, '').trim();
|
|
985
|
-
}
|
|
986
|
-
return env;
|
|
987
|
-
} catch { return {}; }
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
const dotenv = loadEnvKey(process.cwd());
|
|
991
|
-
const API_KEY = process.env.SOLONGATE_API_KEY || dotenv.SOLONGATE_API_KEY || '';
|
|
992
|
-
const API_URL = process.env.SOLONGATE_API_URL || dotenv.SOLONGATE_API_URL || 'https://api.solongate.com';
|
|
993
|
-
|
|
994
|
-
if (!API_KEY || !API_KEY.startsWith('sg_live_')) process.exit(0);
|
|
995
|
-
|
|
996
|
-
let input = '';
|
|
997
|
-
process.stdin.on('data', c => input += c);
|
|
998
|
-
process.stdin.on('end', async () => {
|
|
999
|
-
try {
|
|
1000
|
-
const data = JSON.parse(input);
|
|
1001
|
-
const toolName = data.tool_name || 'unknown';
|
|
1002
|
-
const toolInput = data.tool_input || {};
|
|
1003
|
-
|
|
1004
|
-
if (toolName === 'Bash' && JSON.stringify(toolInput).includes('audit-logs')) {
|
|
1005
|
-
process.exit(0);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
const hasError = data.tool_response?.error ||
|
|
1009
|
-
data.tool_response?.exitCode > 0 ||
|
|
1010
|
-
data.tool_response?.isError;
|
|
1011
|
-
|
|
1012
|
-
const argsSummary = {};
|
|
1013
|
-
for (const [k, v] of Object.entries(toolInput)) {
|
|
1014
|
-
argsSummary[k] = typeof v === 'string' && v.length > 200
|
|
1015
|
-
? v.slice(0, 200) + '...'
|
|
1016
|
-
: v;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
await fetch(\`\${API_URL}/api/v1/audit-logs\`, {
|
|
1020
|
-
method: 'POST',
|
|
1021
|
-
headers: {
|
|
1022
|
-
'Authorization': \`Bearer \${API_KEY}\`,
|
|
1023
|
-
'Content-Type': 'application/json',
|
|
1024
|
-
},
|
|
1025
|
-
body: JSON.stringify({
|
|
1026
|
-
tool: toolName,
|
|
1027
|
-
arguments: argsSummary,
|
|
1028
|
-
decision: hasError ? 'DENY' : 'ALLOW',
|
|
1029
|
-
reason: hasError ? 'tool returned error' : 'allowed',
|
|
1030
|
-
source: 'claude-code-hook',
|
|
1031
|
-
evaluationTimeMs: 0,
|
|
1032
|
-
}),
|
|
1033
|
-
signal: AbortSignal.timeout(5000),
|
|
1034
|
-
});
|
|
1035
|
-
} catch {
|
|
1036
|
-
// Silent
|
|
1037
|
-
}
|
|
1038
|
-
process.exit(0);
|
|
1039
|
-
});
|
|
1040
|
-
`;
|
|
750
|
+
__dirname = dirname2(fileURLToPath(import.meta.url));
|
|
751
|
+
HOOKS_DIR = resolve2(__dirname, "..", "hooks");
|
|
1041
752
|
main().catch((err) => {
|
|
1042
753
|
console.log(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
1043
754
|
process.exit(1);
|
|
@@ -1419,7 +1130,7 @@ var init_inject = __esm({
|
|
|
1419
1130
|
|
|
1420
1131
|
// src/create.ts
|
|
1421
1132
|
var create_exports = {};
|
|
1422
|
-
import { mkdirSync as
|
|
1133
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
1423
1134
|
import { resolve as resolve4, join as join2 } from "path";
|
|
1424
1135
|
import { execSync as execSync2 } from "child_process";
|
|
1425
1136
|
function log4(msg) {
|
|
@@ -1571,7 +1282,7 @@ function createProject(dir, name, _policy) {
|
|
|
1571
1282
|
2
|
|
1572
1283
|
) + "\n"
|
|
1573
1284
|
);
|
|
1574
|
-
|
|
1285
|
+
mkdirSync3(join2(dir, "src"), { recursive: true });
|
|
1575
1286
|
writeFileSync4(
|
|
1576
1287
|
join2(dir, "src", "index.ts"),
|
|
1577
1288
|
`#!/usr/bin/env node
|
|
@@ -1660,7 +1371,7 @@ async function main3() {
|
|
|
1660
1371
|
process.exit(1);
|
|
1661
1372
|
}
|
|
1662
1373
|
withSpinner(`Setting up ${opts.name}...`, () => {
|
|
1663
|
-
|
|
1374
|
+
mkdirSync3(dir, { recursive: true });
|
|
1664
1375
|
createProject(dir, opts.name, opts.policy);
|
|
1665
1376
|
});
|
|
1666
1377
|
if (!opts.noInstall) {
|
|
@@ -2168,7 +1879,7 @@ var PolicyRuleSchema = z.object({
|
|
|
2168
1879
|
effect: z.enum(["ALLOW", "DENY"]),
|
|
2169
1880
|
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
2170
1881
|
toolPattern: z.string().min(1).max(512),
|
|
2171
|
-
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
1882
|
+
permission: z.enum(["READ", "WRITE", "EXECUTE"]).optional(),
|
|
2172
1883
|
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
2173
1884
|
argumentConstraints: z.record(z.unknown()).optional(),
|
|
2174
1885
|
pathConstraints: z.object({
|
|
@@ -2215,6 +1926,7 @@ function createSecurityContext(params) {
|
|
|
2215
1926
|
var DEFAULT_POLICY_EFFECT = "DENY";
|
|
2216
1927
|
var MAX_RULES_PER_POLICY_SET = 1e3;
|
|
2217
1928
|
var POLICY_EVALUATION_TIMEOUT_MS = 100;
|
|
1929
|
+
var TOKEN_MAX_AGE_SECONDS = 300;
|
|
2218
1930
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
2219
1931
|
var RATE_LIMIT_MAX_ENTRIES = 1e4;
|
|
2220
1932
|
var UNSAFE_CONFIGURATION_WARNINGS = {
|
|
@@ -2687,7 +2399,7 @@ function extractUrlArguments(args) {
|
|
|
2687
2399
|
addUrl(value);
|
|
2688
2400
|
return;
|
|
2689
2401
|
}
|
|
2690
|
-
if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+
|
|
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)) {
|
|
2691
2403
|
addUrl(value);
|
|
2692
2404
|
return;
|
|
2693
2405
|
}
|
|
@@ -2750,7 +2462,7 @@ function isUrlAllowed(url, constraints) {
|
|
|
2750
2462
|
}
|
|
2751
2463
|
function ruleMatchesRequest(rule, request) {
|
|
2752
2464
|
if (!rule.enabled) return false;
|
|
2753
|
-
if (rule.permission !== request.requiredPermission) return false;
|
|
2465
|
+
if (rule.permission && rule.permission !== request.requiredPermission) return false;
|
|
2754
2466
|
if (!toolPatternMatches(rule.toolPattern, request.toolName)) return false;
|
|
2755
2467
|
if (!trustLevelMeetsMinimum(request.context.trustLevel, rule.minimumTrustLevel)) {
|
|
2756
2468
|
return false;
|
|
@@ -2946,7 +2658,7 @@ function validatePolicyRule(input) {
|
|
|
2946
2658
|
if (rule.minimumTrustLevel === "TRUSTED") {
|
|
2947
2659
|
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.TRUSTED_LEVEL_EXTERNAL);
|
|
2948
2660
|
}
|
|
2949
|
-
if (rule.permission === "EXECUTE") {
|
|
2661
|
+
if (!rule.permission || rule.permission === "EXECUTE") {
|
|
2950
2662
|
warnings.push(UNSAFE_CONFIGURATION_WARNINGS.EXECUTE_WITHOUT_REVIEW);
|
|
2951
2663
|
}
|
|
2952
2664
|
return { valid: true, errors, warnings };
|
|
@@ -3023,7 +2735,7 @@ function analyzeRuleWarnings(rule) {
|
|
|
3023
2735
|
recommendation: "Set minimumTrustLevel to VERIFIED or higher for ALLOW rules."
|
|
3024
2736
|
});
|
|
3025
2737
|
}
|
|
3026
|
-
if (rule.effect === "ALLOW" && rule.permission === "EXECUTE") {
|
|
2738
|
+
if (rule.effect === "ALLOW" && (!rule.permission || rule.permission === "EXECUTE")) {
|
|
3027
2739
|
warnings.push({
|
|
3028
2740
|
level: "WARNING",
|
|
3029
2741
|
code: "ALLOW_EXECUTE",
|
|
@@ -3428,12 +3140,47 @@ var SecurityLogger = class {
|
|
|
3428
3140
|
}
|
|
3429
3141
|
}
|
|
3430
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
|
+
};
|
|
3431
3178
|
var TokenIssuer = class {
|
|
3432
3179
|
secret;
|
|
3433
3180
|
ttlSeconds;
|
|
3434
3181
|
issuer;
|
|
3435
|
-
usedNonces
|
|
3436
|
-
revokedTokens
|
|
3182
|
+
usedNonces;
|
|
3183
|
+
revokedTokens;
|
|
3437
3184
|
constructor(config) {
|
|
3438
3185
|
if (config.secret.length < MIN_SECRET_LENGTH) {
|
|
3439
3186
|
throw new Error(
|
|
@@ -3443,6 +3190,9 @@ var TokenIssuer = class {
|
|
|
3443
3190
|
this.secret = config.secret;
|
|
3444
3191
|
this.ttlSeconds = config.ttlSeconds || DEFAULT_TOKEN_TTL_SECONDS;
|
|
3445
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);
|
|
3446
3196
|
}
|
|
3447
3197
|
/**
|
|
3448
3198
|
* Issues a signed capability token.
|
|
@@ -3537,13 +3287,14 @@ function base64UrlDecode(str) {
|
|
|
3537
3287
|
var ServerVerifier = class {
|
|
3538
3288
|
gatewaySecret;
|
|
3539
3289
|
maxAgeMs;
|
|
3540
|
-
usedNonces
|
|
3290
|
+
usedNonces;
|
|
3541
3291
|
constructor(config) {
|
|
3542
3292
|
if (config.gatewaySecret.length < 32) {
|
|
3543
3293
|
throw new Error("Gateway secret must be at least 32 characters");
|
|
3544
3294
|
}
|
|
3545
3295
|
this.gatewaySecret = config.gatewaySecret;
|
|
3546
3296
|
this.maxAgeMs = config.maxAgeMs ?? 6e4;
|
|
3297
|
+
this.usedNonces = new ExpiringSet(this.maxAgeMs * 2);
|
|
3547
3298
|
}
|
|
3548
3299
|
/**
|
|
3549
3300
|
* Computes HMAC signature for request data.
|
|
@@ -3604,12 +3355,75 @@ var ServerVerifier = class {
|
|
|
3604
3355
|
return { valid: true };
|
|
3605
3356
|
}
|
|
3606
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
|
+
};
|
|
3607
3418
|
var RateLimiter = class {
|
|
3608
3419
|
windowMs;
|
|
3609
|
-
|
|
3610
|
-
|
|
3420
|
+
maxEntries;
|
|
3421
|
+
buffers = /* @__PURE__ */ new Map();
|
|
3422
|
+
globalBuffer;
|
|
3611
3423
|
constructor(options) {
|
|
3612
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);
|
|
3613
3427
|
}
|
|
3614
3428
|
/**
|
|
3615
3429
|
* Checks if a tool call is within the rate limit.
|
|
@@ -3618,11 +3432,15 @@ var RateLimiter = class {
|
|
|
3618
3432
|
checkLimit(toolName, limitPerWindow) {
|
|
3619
3433
|
const now = Date.now();
|
|
3620
3434
|
const windowStart = now - this.windowMs;
|
|
3621
|
-
const
|
|
3622
|
-
|
|
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);
|
|
3623
3440
|
const allowed = count < limitPerWindow;
|
|
3624
3441
|
const remaining = Math.max(0, limitPerWindow - count);
|
|
3625
|
-
const
|
|
3442
|
+
const oldest = buffer.oldestInWindow(windowStart);
|
|
3443
|
+
const resetAt = oldest !== null ? oldest + this.windowMs : now + this.windowMs;
|
|
3626
3444
|
return { allowed, remaining, resetAt };
|
|
3627
3445
|
}
|
|
3628
3446
|
/**
|
|
@@ -3631,13 +3449,11 @@ var RateLimiter = class {
|
|
|
3631
3449
|
checkGlobalLimit(limitPerWindow) {
|
|
3632
3450
|
const now = Date.now();
|
|
3633
3451
|
const windowStart = now - this.windowMs;
|
|
3634
|
-
|
|
3635
|
-
(r) => r.timestamp > windowStart
|
|
3636
|
-
);
|
|
3637
|
-
const count = this.globalRecords.length;
|
|
3452
|
+
const count = this.globalBuffer.countAfter(windowStart);
|
|
3638
3453
|
const allowed = count < limitPerWindow;
|
|
3639
3454
|
const remaining = Math.max(0, limitPerWindow - count);
|
|
3640
|
-
const
|
|
3455
|
+
const oldest = this.globalBuffer.oldestInWindow(windowStart);
|
|
3456
|
+
const resetAt = oldest !== null ? oldest + this.windowMs : now + this.windowMs;
|
|
3641
3457
|
return { allowed, remaining, resetAt };
|
|
3642
3458
|
}
|
|
3643
3459
|
/**
|
|
@@ -3665,23 +3481,13 @@ var RateLimiter = class {
|
|
|
3665
3481
|
*/
|
|
3666
3482
|
recordCall(toolName) {
|
|
3667
3483
|
const now = Date.now();
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
const windowStart = now - this.windowMs;
|
|
3673
|
-
const cleaned = records.filter((r) => r.timestamp > windowStart);
|
|
3674
|
-
this.records.set(toolName, cleaned);
|
|
3675
|
-
} else {
|
|
3676
|
-
this.records.set(toolName, records);
|
|
3677
|
-
}
|
|
3678
|
-
this.globalRecords.push(record);
|
|
3679
|
-
if (this.globalRecords.length > RATE_LIMIT_MAX_ENTRIES) {
|
|
3680
|
-
const windowStart = now - this.windowMs;
|
|
3681
|
-
this.globalRecords = this.globalRecords.filter(
|
|
3682
|
-
(r) => r.timestamp > windowStart
|
|
3683
|
-
);
|
|
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);
|
|
3684
3488
|
}
|
|
3489
|
+
buffer.push(now);
|
|
3490
|
+
this.globalBuffer.push(now);
|
|
3685
3491
|
}
|
|
3686
3492
|
/**
|
|
3687
3493
|
* Gets usage stats for a tool.
|
|
@@ -3689,29 +3495,22 @@ var RateLimiter = class {
|
|
|
3689
3495
|
getUsage(toolName) {
|
|
3690
3496
|
const now = Date.now();
|
|
3691
3497
|
const windowStart = now - this.windowMs;
|
|
3692
|
-
const
|
|
3693
|
-
|
|
3498
|
+
const buffer = this.buffers.get(toolName);
|
|
3499
|
+
const count = buffer ? buffer.countAfter(windowStart) : 0;
|
|
3500
|
+
return { count, windowStart };
|
|
3694
3501
|
}
|
|
3695
3502
|
/**
|
|
3696
3503
|
* Resets rate tracking for a specific tool.
|
|
3697
3504
|
*/
|
|
3698
3505
|
resetTool(toolName) {
|
|
3699
|
-
this.
|
|
3506
|
+
this.buffers.delete(toolName);
|
|
3700
3507
|
}
|
|
3701
3508
|
/**
|
|
3702
3509
|
* Resets all rate tracking.
|
|
3703
3510
|
*/
|
|
3704
3511
|
resetAll() {
|
|
3705
|
-
this.
|
|
3706
|
-
this.
|
|
3707
|
-
}
|
|
3708
|
-
getActiveRecords(toolName, windowStart) {
|
|
3709
|
-
const records = this.records.get(toolName) ?? [];
|
|
3710
|
-
const active = records.filter((r) => r.timestamp > windowStart);
|
|
3711
|
-
if (active.length !== records.length) {
|
|
3712
|
-
this.records.set(toolName, active);
|
|
3713
|
-
}
|
|
3714
|
-
return active;
|
|
3512
|
+
this.buffers.clear();
|
|
3513
|
+
this.globalBuffer = new CircularTimestampBuffer(this.maxEntries);
|
|
3715
3514
|
}
|
|
3716
3515
|
};
|
|
3717
3516
|
var LicenseError = class extends Error {
|
|
@@ -3734,6 +3533,7 @@ var SolonGate = class {
|
|
|
3734
3533
|
rateLimiter;
|
|
3735
3534
|
apiKey;
|
|
3736
3535
|
licenseValidated = false;
|
|
3536
|
+
pollingTimer = null;
|
|
3737
3537
|
constructor(options) {
|
|
3738
3538
|
const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
3739
3539
|
if (!apiKey) {
|
|
@@ -3845,7 +3645,7 @@ var SolonGate = class {
|
|
|
3845
3645
|
startPolicyPolling() {
|
|
3846
3646
|
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
3847
3647
|
let currentVersion = 0;
|
|
3848
|
-
setInterval(async () => {
|
|
3648
|
+
this.pollingTimer = setInterval(async () => {
|
|
3849
3649
|
try {
|
|
3850
3650
|
const res = await fetch(`${apiUrl}/api/v1/policies/default`, {
|
|
3851
3651
|
headers: { "Authorization": `Bearer ${this.apiKey}` },
|
|
@@ -3952,6 +3752,13 @@ var SolonGate = class {
|
|
|
3952
3752
|
getTokenIssuer() {
|
|
3953
3753
|
return this.tokenIssuer;
|
|
3954
3754
|
}
|
|
3755
|
+
/** Stop policy polling and release resources. */
|
|
3756
|
+
destroy() {
|
|
3757
|
+
if (this.pollingTimer) {
|
|
3758
|
+
clearInterval(this.pollingTimer);
|
|
3759
|
+
this.pollingTimer = null;
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3955
3762
|
};
|
|
3956
3763
|
|
|
3957
3764
|
// ../core/dist/index.js
|
|
@@ -4522,13 +4329,22 @@ var log2 = (...args) => process.stderr.write(`[SolonGate] ${args.map(String).joi
|
|
|
4522
4329
|
var Mutex = class {
|
|
4523
4330
|
queue = [];
|
|
4524
4331
|
locked = false;
|
|
4525
|
-
async acquire() {
|
|
4332
|
+
async acquire(timeoutMs = 3e4) {
|
|
4526
4333
|
if (!this.locked) {
|
|
4527
4334
|
this.locked = true;
|
|
4528
4335
|
return;
|
|
4529
4336
|
}
|
|
4530
|
-
return new Promise((resolve6) => {
|
|
4531
|
-
|
|
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);
|
|
4532
4348
|
});
|
|
4533
4349
|
}
|
|
4534
4350
|
release() {
|
|
@@ -4540,12 +4356,23 @@ var Mutex = class {
|
|
|
4540
4356
|
}
|
|
4541
4357
|
}
|
|
4542
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
|
+
};
|
|
4543
4370
|
var SolonGateProxy = class {
|
|
4544
4371
|
config;
|
|
4545
4372
|
gate;
|
|
4546
4373
|
client = null;
|
|
4547
4374
|
server = null;
|
|
4548
|
-
|
|
4375
|
+
toolMutexes = new ToolMutexMap();
|
|
4549
4376
|
syncManager = null;
|
|
4550
4377
|
upstreamTools = [];
|
|
4551
4378
|
constructor(config) {
|
|
@@ -4707,9 +4534,10 @@ var SolonGateProxy = class {
|
|
|
4707
4534
|
return { tools: this.upstreamTools };
|
|
4708
4535
|
});
|
|
4709
4536
|
const MAX_ARGUMENT_SIZE = 1024 * 1024;
|
|
4537
|
+
const MUTEX_TIMEOUT_MS = 3e4;
|
|
4710
4538
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
4711
4539
|
const { name, arguments: args } = request.params;
|
|
4712
|
-
const argsSize = JSON.stringify(args ?? {}).length;
|
|
4540
|
+
const argsSize = new TextEncoder().encode(JSON.stringify(args ?? {})).length;
|
|
4713
4541
|
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
4714
4542
|
log2(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
4715
4543
|
return {
|
|
@@ -4718,7 +4546,16 @@ var SolonGateProxy = class {
|
|
|
4718
4546
|
};
|
|
4719
4547
|
}
|
|
4720
4548
|
log2(`Tool call: ${name}`);
|
|
4721
|
-
|
|
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
|
+
}
|
|
4722
4559
|
const startTime = Date.now();
|
|
4723
4560
|
try {
|
|
4724
4561
|
const result = await this.gate.executeToolCall(
|
|
@@ -4767,7 +4604,7 @@ var SolonGateProxy = class {
|
|
|
4767
4604
|
isError: result.isError
|
|
4768
4605
|
};
|
|
4769
4606
|
} finally {
|
|
4770
|
-
|
|
4607
|
+
mutex.release();
|
|
4771
4608
|
}
|
|
4772
4609
|
});
|
|
4773
4610
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|