@solongate/proxy 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/create.js +32 -16
  2. package/dist/index.js +668 -547
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -820,14 +820,28 @@ dist/
820
820
  `
821
821
  );
822
822
  }
823
- function installDeps(dir) {
824
- log3(" Installing dependencies with npm...");
823
+ function withSpinner(message, fn) {
824
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
825
+ let i = 0;
826
+ const id = setInterval(() => {
827
+ process.stderr.write(`\r ${frames[i++ % frames.length]} ${message}`);
828
+ }, 80);
825
829
  try {
826
- execSync2("npm install", { cwd: dir, stdio: "pipe" });
830
+ fn();
831
+ clearInterval(id);
832
+ process.stderr.write(`\r \u2713 ${message}
833
+ `);
827
834
  } catch {
828
- log3(" npm install failed \u2014 run it manually.");
835
+ clearInterval(id);
836
+ process.stderr.write(`\r \u2717 ${message} \u2014 failed
837
+ `);
829
838
  }
830
839
  }
840
+ function installDeps(dir) {
841
+ withSpinner("Installing dependencies...", () => {
842
+ execSync2("npm install", { cwd: dir, stdio: "pipe" });
843
+ });
844
+ }
831
845
  async function main3() {
832
846
  const opts = parseCreateArgs(process.argv);
833
847
  const dir = resolve4(opts.name);
@@ -857,18 +871,20 @@ async function main3() {
857
871
  installDeps(dir);
858
872
  log3("");
859
873
  }
860
- log3(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
861
- log3(" \u2502 Project created! \u2502");
862
- log3(" \u2502 \u2502");
863
- log3(` \u2502 cd ${opts.name.padEnd(39)}\u2502`);
864
- log3(" \u2502 \u2502");
865
- log3(" \u2502 npm run build # Build \u2502");
866
- log3(" \u2502 npm run dev # Dev mode (tsx) \u2502");
867
- log3(" \u2502 npm start # Run built server \u2502");
868
- log3(" \u2502 \u2502");
869
- log3(" \u2502 Set your API key: \u2502");
870
- log3(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
871
- log3(" \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\u2518");
874
+ const W = 46;
875
+ const line = (s) => log3(` \u2502 ${s.padEnd(W)} \u2502`);
876
+ log3(` \u250C${"\u2500".repeat(W + 2)}\u2510`);
877
+ line("Project created!");
878
+ line("");
879
+ line(`cd ${opts.name}`);
880
+ line("");
881
+ line("npm run build # Build");
882
+ line("npm run dev # Dev mode (tsx)");
883
+ line("npm start # Run built server");
884
+ line("");
885
+ line("Set your API key:");
886
+ line("export SOLONGATE_API_KEY=sg_live_xxx");
887
+ log3(` \u2514${"\u2500".repeat(W + 2)}\u2518`);
872
888
  log3("");
873
889
  }
874
890
  var init_create = __esm({
@@ -883,67 +899,6 @@ var init_create = __esm({
883
899
 
884
900
  // ../core/dist/index.js
885
901
  import { z } from "zod";
886
- var SolonGateError = class extends Error {
887
- code;
888
- timestamp;
889
- details;
890
- constructor(message, code, details = {}) {
891
- super(message);
892
- this.name = "SolonGateError";
893
- this.code = code;
894
- this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
895
- this.details = Object.freeze({ ...details });
896
- Object.setPrototypeOf(this, new.target.prototype);
897
- }
898
- /**
899
- * Serializable representation for logging and API responses.
900
- * Never includes stack traces (information leakage prevention).
901
- */
902
- toJSON() {
903
- return {
904
- name: this.name,
905
- code: this.code,
906
- message: this.message,
907
- timestamp: this.timestamp,
908
- details: this.details
909
- };
910
- }
911
- };
912
- var PolicyDeniedError = class extends SolonGateError {
913
- constructor(toolName, reason, details = {}) {
914
- super(
915
- `Policy denied execution of tool "${toolName}": ${reason}`,
916
- "POLICY_DENIED",
917
- { toolName, reason, ...details }
918
- );
919
- this.name = "PolicyDeniedError";
920
- }
921
- };
922
- var SchemaValidationError = class extends SolonGateError {
923
- constructor(toolName, validationErrors) {
924
- super(
925
- `Schema validation failed for tool "${toolName}": ${validationErrors.join("; ")}`,
926
- "SCHEMA_VALIDATION_FAILED",
927
- { toolName, validationErrors }
928
- );
929
- this.name = "SchemaValidationError";
930
- }
931
- };
932
- var RateLimitError = class extends SolonGateError {
933
- constructor(toolName, limitPerMinute) {
934
- super(
935
- `Rate limit exceeded for tool "${toolName}": max ${limitPerMinute}/min`,
936
- "RATE_LIMIT_EXCEEDED",
937
- { toolName, limitPerMinute }
938
- );
939
- this.name = "RateLimitError";
940
- }
941
- };
942
- var TrustLevel = {
943
- UNTRUSTED: "UNTRUSTED",
944
- VERIFIED: "VERIFIED",
945
- TRUSTED: "TRUSTED"
946
- };
947
902
  var Permission = {
948
903
  READ: "READ",
949
904
  WRITE: "WRITE",
@@ -956,10 +911,6 @@ var NO_PERMISSIONS = Object.freeze(
956
911
  var READ_ONLY = Object.freeze(
957
912
  /* @__PURE__ */ new Set([Permission.READ])
958
913
  );
959
- var PolicyEffect = {
960
- ALLOW: "ALLOW",
961
- DENY: "DENY"
962
- };
963
914
  var PolicyRuleSchema = z.object({
964
915
  id: z.string().min(1).max(256),
965
916
  description: z.string().max(1024),
@@ -988,45 +939,7 @@ var PolicySetSchema = z.object({
988
939
  createdAt: z.string().datetime(),
989
940
  updatedAt: z.string().datetime()
990
941
  });
991
- function createSecurityContext(params) {
992
- return {
993
- trustLevel: "UNTRUSTED",
994
- grantedPermissions: /* @__PURE__ */ new Set(),
995
- sessionId: null,
996
- metadata: {},
997
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
998
- ...params
999
- };
1000
- }
1001
- var DEFAULT_POLICY_EFFECT = "DENY";
1002
- var MAX_RULES_PER_POLICY_SET = 1e3;
1003
942
  var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
1004
- var POLICY_EVALUATION_TIMEOUT_MS = 100;
1005
- var RATE_LIMIT_WINDOW_MS = 6e4;
1006
- var RATE_LIMIT_MAX_ENTRIES = 1e4;
1007
- var UNSAFE_CONFIGURATION_WARNINGS = {
1008
- WILDCARD_ALLOW: "Wildcard ALLOW rules grant permission to ALL tools. This bypasses the default-deny model.",
1009
- TRUSTED_LEVEL_EXTERNAL: "Setting trust level to TRUSTED for external requests bypasses all security checks.",
1010
- WRITE_WITHOUT_READ: "Granting WRITE without READ is unusual and may indicate a misconfiguration.",
1011
- EXECUTE_WITHOUT_REVIEW: "EXECUTE permission allows tools to perform arbitrary actions. Review carefully.",
1012
- RATE_LIMIT_ZERO: "A rate limit of 0 means unlimited calls. This removes protection against runaway loops.",
1013
- DISABLED_VALIDATION: "Disabling schema validation removes input sanitization protections."
1014
- };
1015
- function createDeniedToolResult(reason) {
1016
- return {
1017
- content: [
1018
- {
1019
- type: "text",
1020
- text: JSON.stringify({
1021
- error: "POLICY_DENIED",
1022
- message: reason,
1023
- hint: "This tool call was blocked by SolonGate security policy. Check your policy configuration."
1024
- })
1025
- }
1026
- ],
1027
- isError: true
1028
- };
1029
- }
1030
943
  var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
1031
944
  pathTraversal: true,
1032
945
  shellInjection: true,
@@ -1036,102 +949,6 @@ var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
1036
949
  ssrf: true,
1037
950
  sqlInjection: true
1038
951
  });
1039
- var PATH_TRAVERSAL_PATTERNS = [
1040
- /\.\.\//,
1041
- // ../
1042
- /\.\.\\/,
1043
- // ..\
1044
- /%2e%2e/i,
1045
- // URL-encoded ..
1046
- /%2e\./i,
1047
- // partial URL-encoded
1048
- /\.%2e/i,
1049
- // partial URL-encoded
1050
- /%252e%252e/i,
1051
- // double URL-encoded
1052
- /\.\.\0/
1053
- // null byte variant
1054
- ];
1055
- var SENSITIVE_PATHS = [
1056
- /\/etc\/passwd/i,
1057
- /\/etc\/shadow/i,
1058
- /\/proc\//i,
1059
- /\/dev\//i,
1060
- /c:\\windows\\system32/i,
1061
- /c:\\windows\\syswow64/i,
1062
- /\/root\//i,
1063
- /~\//,
1064
- /\.env(\.|$)/i,
1065
- // .env, .env.local, .env.production
1066
- /\.aws\/credentials/i,
1067
- // AWS credentials
1068
- /\.ssh\/id_/i,
1069
- // SSH keys
1070
- /\.kube\/config/i,
1071
- // Kubernetes config
1072
- /wp-config\.php/i,
1073
- // WordPress config
1074
- /\.git\/config/i,
1075
- // Git config
1076
- /\.npmrc/i,
1077
- // npm credentials
1078
- /\.pypirc/i
1079
- // PyPI credentials
1080
- ];
1081
- function detectPathTraversal(value) {
1082
- for (const pattern of PATH_TRAVERSAL_PATTERNS) {
1083
- if (pattern.test(value)) return true;
1084
- }
1085
- for (const pattern of SENSITIVE_PATHS) {
1086
- if (pattern.test(value)) return true;
1087
- }
1088
- return false;
1089
- }
1090
- var SHELL_INJECTION_PATTERNS = [
1091
- /[;|&`]/,
1092
- // Command separators and backtick execution
1093
- /\$\(/,
1094
- // Command substitution $(...)
1095
- /\$\{/,
1096
- // Variable expansion ${...}
1097
- />\s*/,
1098
- // Output redirect
1099
- /<\s*/,
1100
- // Input redirect
1101
- /&&/,
1102
- // AND chaining
1103
- /\|\|/,
1104
- // OR chaining
1105
- /\beval\b/i,
1106
- // eval command
1107
- /\bexec\b/i,
1108
- // exec command
1109
- /\bsystem\b/i,
1110
- // system call
1111
- /%0a/i,
1112
- // URL-encoded newline
1113
- /%0d/i,
1114
- // URL-encoded carriage return
1115
- /%09/i,
1116
- // URL-encoded tab
1117
- /\r\n/,
1118
- // CRLF injection
1119
- /\n/
1120
- // Newline (command separator on Unix)
1121
- ];
1122
- function detectShellInjection(value) {
1123
- for (const pattern of SHELL_INJECTION_PATTERNS) {
1124
- if (pattern.test(value)) return true;
1125
- }
1126
- return false;
1127
- }
1128
- var MAX_WILDCARDS_PER_VALUE = 3;
1129
- function detectWildcardAbuse(value) {
1130
- if (value.includes("**")) return true;
1131
- const wildcardCount = (value.match(/\*/g) || []).length;
1132
- if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
1133
- return false;
1134
- }
1135
952
  var SSRF_PATTERNS = [
1136
953
  /^https?:\/\/localhost\b/i,
1137
954
  /^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
@@ -1191,166 +1008,28 @@ function detectSSRF(value) {
1191
1008
  if (detectDecimalIP(value)) return true;
1192
1009
  return false;
1193
1010
  }
1194
- var SQL_INJECTION_PATTERNS = [
1195
- /'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
1196
- // ' OR '1'='1 bounded to prevent ReDoS
1197
- /'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
1198
- // '; DROP TABLE
1199
- /UNION\s+(ALL\s+)?SELECT/i,
1200
- // UNION SELECT
1201
- /--\s*$/m,
1202
- // SQL comment at end of line
1203
- /\/\*.{0,500}?\*\//,
1204
- // SQL block comment bounded + non-greedy
1205
- /\bSLEEP\s*\(/i,
1206
- // Time-based injection
1207
- /\bBENCHMARK\s*\(/i,
1208
- // MySQL benchmark
1209
- /\bWAITFOR\s+DELAY/i,
1210
- // MSSQL delay
1211
- /\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
1212
- // File operations
1213
- ];
1214
- function detectSQLInjection(value) {
1215
- for (const pattern of SQL_INJECTION_PATTERNS) {
1216
- if (pattern.test(value)) return true;
1011
+
1012
+ // src/config.ts
1013
+ import { readFileSync, existsSync } from "fs";
1014
+ import { resolve } from "path";
1015
+ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
1016
+ const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
1017
+ const res = await fetch(url, {
1018
+ headers: { "Authorization": `Bearer ${apiKey}` }
1019
+ });
1020
+ if (!res.ok) {
1021
+ const body = await res.text().catch(() => "");
1022
+ throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
1217
1023
  }
1218
- return false;
1219
- }
1220
- function checkLengthLimits(value, maxLength = 4096) {
1221
- return value.length <= maxLength;
1222
- }
1223
- var ENTROPY_THRESHOLD = 4.5;
1224
- var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
1225
- function checkEntropyLimits(value) {
1226
- if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
1227
- const entropy = calculateShannonEntropy(value);
1228
- return entropy <= ENTROPY_THRESHOLD;
1229
- }
1230
- function calculateShannonEntropy(str) {
1231
- const freq = /* @__PURE__ */ new Map();
1232
- for (const char of str) {
1233
- freq.set(char, (freq.get(char) ?? 0) + 1);
1234
- }
1235
- let entropy = 0;
1236
- const len = str.length;
1237
- for (const count of freq.values()) {
1238
- const p = count / len;
1239
- if (p > 0) {
1240
- entropy -= p * Math.log2(p);
1241
- }
1242
- }
1243
- return entropy;
1244
- }
1245
- function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
1246
- const threats = [];
1247
- if (typeof value !== "string") {
1248
- if (typeof value === "object" && value !== null) {
1249
- return sanitizeObject(field, value, config);
1250
- }
1251
- return { safe: true, threats: [] };
1252
- }
1253
- if (config.pathTraversal && detectPathTraversal(value)) {
1254
- threats.push({
1255
- type: "PATH_TRAVERSAL",
1256
- field,
1257
- value: truncate(value, 100),
1258
- description: "Path traversal pattern detected"
1259
- });
1260
- }
1261
- if (config.shellInjection && detectShellInjection(value)) {
1262
- threats.push({
1263
- type: "SHELL_INJECTION",
1264
- field,
1265
- value: truncate(value, 100),
1266
- description: "Shell injection pattern detected"
1267
- });
1268
- }
1269
- if (config.wildcardAbuse && detectWildcardAbuse(value)) {
1270
- threats.push({
1271
- type: "WILDCARD_ABUSE",
1272
- field,
1273
- value: truncate(value, 100),
1274
- description: "Wildcard abuse pattern detected"
1275
- });
1276
- }
1277
- if (!checkLengthLimits(value, config.lengthLimit)) {
1278
- threats.push({
1279
- type: "LENGTH_EXCEEDED",
1280
- field,
1281
- value: `[${value.length} chars]`,
1282
- description: `Value exceeds maximum length of ${config.lengthLimit}`
1283
- });
1284
- }
1285
- if (config.entropyLimit && !checkEntropyLimits(value)) {
1286
- threats.push({
1287
- type: "HIGH_ENTROPY",
1288
- field,
1289
- value: truncate(value, 100),
1290
- description: "High entropy string detected - possible encoded payload"
1291
- });
1292
- }
1293
- if (config.ssrf && detectSSRF(value)) {
1294
- threats.push({
1295
- type: "SSRF",
1296
- field,
1297
- value: truncate(value, 100),
1298
- description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
1299
- });
1300
- }
1301
- if (config.sqlInjection && detectSQLInjection(value)) {
1302
- threats.push({
1303
- type: "SQL_INJECTION",
1304
- field,
1305
- value: truncate(value, 100),
1306
- description: "SQL injection pattern detected"
1307
- });
1308
- }
1309
- return { safe: threats.length === 0, threats };
1310
- }
1311
- function sanitizeObject(basePath, obj, config) {
1312
- const threats = [];
1313
- if (Array.isArray(obj)) {
1314
- for (let i = 0; i < obj.length; i++) {
1315
- const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
1316
- threats.push(...result.threats);
1317
- }
1318
- } else {
1319
- for (const [key, val] of Object.entries(obj)) {
1320
- const result = sanitizeInput(`${basePath}.${key}`, val, config);
1321
- threats.push(...result.threats);
1322
- }
1323
- }
1324
- return { safe: threats.length === 0, threats };
1325
- }
1326
- function truncate(str, maxLen) {
1327
- return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
1328
- }
1329
- var DEFAULT_TOKEN_TTL_SECONDS = 30;
1330
- var TOKEN_ALGORITHM = "HS256";
1331
- var MIN_SECRET_LENGTH = 32;
1332
-
1333
- // src/config.ts
1334
- import { readFileSync, existsSync } from "fs";
1335
- import { resolve } from "path";
1336
- async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
1337
- const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
1338
- const res = await fetch(url, {
1339
- headers: { "Authorization": `Bearer ${apiKey}` }
1340
- });
1341
- if (!res.ok) {
1342
- const body = await res.text().catch(() => "");
1343
- throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
1344
- }
1345
- const data = await res.json();
1346
- return {
1347
- id: String(data.id ?? "cloud"),
1348
- name: String(data.name ?? "Cloud Policy"),
1349
- version: Number(data._version ?? 1),
1350
- rules: data.rules ?? [],
1351
- createdAt: String(data._created_at ?? ""),
1352
- updatedAt: ""
1353
- };
1024
+ const data = await res.json();
1025
+ return {
1026
+ id: String(data.id ?? "cloud"),
1027
+ name: String(data.name ?? "Cloud Policy"),
1028
+ version: Number(data._version ?? 1),
1029
+ rules: data.rules ?? [],
1030
+ createdAt: String(data._created_at ?? ""),
1031
+ updatedAt: ""
1032
+ };
1354
1033
  }
1355
1034
  async function sendAuditLog(apiKey, apiUrl, entry) {
1356
1035
  try {
@@ -1538,175 +1217,621 @@ function loadPolicy(source) {
1538
1217
  `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
1539
1218
  );
1540
1219
  }
1541
- function parseArgs(argv) {
1542
- const args = argv.slice(2);
1543
- let policySource = "restricted";
1544
- let name = "solongate-proxy";
1545
- let verbose = false;
1546
- let validateInput = true;
1547
- let rateLimitPerTool;
1548
- let globalRateLimit;
1549
- let configFile;
1550
- let apiKey;
1551
- let apiUrl;
1552
- let upstreamUrl;
1553
- let upstreamTransport;
1554
- let port;
1555
- let separatorIndex = args.indexOf("--");
1556
- const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
1557
- const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
1558
- for (let i = 0; i < flags.length; i++) {
1559
- switch (flags[i]) {
1560
- case "--policy":
1561
- policySource = flags[++i];
1562
- break;
1563
- case "--name":
1564
- name = flags[++i];
1565
- break;
1566
- case "--verbose":
1567
- verbose = true;
1568
- break;
1569
- case "--no-input-guard":
1570
- validateInput = false;
1571
- break;
1572
- case "--rate-limit":
1573
- rateLimitPerTool = parseInt(flags[++i], 10);
1574
- break;
1575
- case "--global-rate-limit":
1576
- globalRateLimit = parseInt(flags[++i], 10);
1577
- break;
1578
- case "--config":
1579
- configFile = flags[++i];
1580
- break;
1581
- case "--api-key":
1582
- apiKey = flags[++i];
1583
- break;
1584
- case "--api-url":
1585
- apiUrl = flags[++i];
1586
- break;
1587
- case "--upstream-url":
1588
- upstreamUrl = flags[++i];
1589
- break;
1590
- case "--upstream-transport":
1591
- upstreamTransport = flags[++i];
1592
- break;
1593
- case "--port":
1594
- port = parseInt(flags[++i], 10);
1595
- break;
1220
+ function parseArgs(argv) {
1221
+ const args = argv.slice(2);
1222
+ let policySource = "restricted";
1223
+ let name = "solongate-proxy";
1224
+ let verbose = false;
1225
+ let validateInput = true;
1226
+ let rateLimitPerTool;
1227
+ let globalRateLimit;
1228
+ let configFile;
1229
+ let apiKey;
1230
+ let apiUrl;
1231
+ let upstreamUrl;
1232
+ let upstreamTransport;
1233
+ let port;
1234
+ let separatorIndex = args.indexOf("--");
1235
+ const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
1236
+ const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
1237
+ for (let i = 0; i < flags.length; i++) {
1238
+ switch (flags[i]) {
1239
+ case "--policy":
1240
+ policySource = flags[++i];
1241
+ break;
1242
+ case "--name":
1243
+ name = flags[++i];
1244
+ break;
1245
+ case "--verbose":
1246
+ verbose = true;
1247
+ break;
1248
+ case "--no-input-guard":
1249
+ validateInput = false;
1250
+ break;
1251
+ case "--rate-limit":
1252
+ rateLimitPerTool = parseInt(flags[++i], 10);
1253
+ break;
1254
+ case "--global-rate-limit":
1255
+ globalRateLimit = parseInt(flags[++i], 10);
1256
+ break;
1257
+ case "--config":
1258
+ configFile = flags[++i];
1259
+ break;
1260
+ case "--api-key":
1261
+ apiKey = flags[++i];
1262
+ break;
1263
+ case "--api-url":
1264
+ apiUrl = flags[++i];
1265
+ break;
1266
+ case "--upstream-url":
1267
+ upstreamUrl = flags[++i];
1268
+ break;
1269
+ case "--upstream-transport":
1270
+ upstreamTransport = flags[++i];
1271
+ break;
1272
+ case "--port":
1273
+ port = parseInt(flags[++i], 10);
1274
+ break;
1275
+ }
1276
+ }
1277
+ if (!apiKey) {
1278
+ const envKey = process.env.SOLONGATE_API_KEY;
1279
+ if (envKey) {
1280
+ apiKey = envKey;
1281
+ } else {
1282
+ throw new Error(
1283
+ "A valid SolonGate API key is required.\n\nUsage: solongate-proxy --api-key sg_live_xxx -- <command>\n or: set SOLONGATE_API_KEY=sg_live_xxx\n\nGet your API key at https://solongate.com\n"
1284
+ );
1285
+ }
1286
+ }
1287
+ if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
1288
+ throw new Error(
1289
+ "Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
1290
+ );
1291
+ }
1292
+ if (configFile) {
1293
+ const filePath = resolve(configFile);
1294
+ const content = readFileSync(filePath, "utf-8");
1295
+ const fileConfig = JSON.parse(content);
1296
+ if (!fileConfig.upstream) {
1297
+ throw new Error('Config file must include "upstream" with at least "command" or "url"');
1298
+ }
1299
+ if (fileConfig.upstream.url && detectSSRF(fileConfig.upstream.url)) {
1300
+ throw new Error(
1301
+ `Upstream URL blocked: "${fileConfig.upstream.url}" points to an internal or private network address.`
1302
+ );
1303
+ }
1304
+ return {
1305
+ upstream: fileConfig.upstream,
1306
+ policy: loadPolicy(fileConfig.policy ?? policySource),
1307
+ name: fileConfig.name ?? name,
1308
+ verbose: fileConfig.verbose ?? verbose,
1309
+ validateInput: fileConfig.validateInput ?? validateInput,
1310
+ rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
1311
+ globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
1312
+ apiKey: apiKey ?? fileConfig.apiKey,
1313
+ apiUrl: apiUrl ?? fileConfig.apiUrl,
1314
+ port: port ?? fileConfig.port
1315
+ };
1316
+ }
1317
+ if (upstreamUrl) {
1318
+ if (detectSSRF(upstreamUrl)) {
1319
+ throw new Error(
1320
+ `Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
1321
+ SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
1322
+ );
1323
+ }
1324
+ const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
1325
+ return {
1326
+ upstream: {
1327
+ transport,
1328
+ command: "",
1329
+ // not used for URL-based transports
1330
+ url: upstreamUrl
1331
+ },
1332
+ policy: loadPolicy(policySource),
1333
+ name,
1334
+ verbose,
1335
+ validateInput,
1336
+ rateLimitPerTool,
1337
+ globalRateLimit,
1338
+ apiKey,
1339
+ apiUrl,
1340
+ port
1341
+ };
1342
+ }
1343
+ if (upstreamArgs.length === 0) {
1344
+ throw new Error(
1345
+ "No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
1346
+ );
1347
+ }
1348
+ const [command, ...commandArgs] = upstreamArgs;
1349
+ return {
1350
+ upstream: {
1351
+ transport: upstreamTransport ?? "stdio",
1352
+ command,
1353
+ args: commandArgs,
1354
+ env: { ...process.env }
1355
+ },
1356
+ policy: loadPolicy(policySource),
1357
+ name,
1358
+ verbose,
1359
+ validateInput,
1360
+ rateLimitPerTool,
1361
+ globalRateLimit,
1362
+ apiKey,
1363
+ apiUrl,
1364
+ port
1365
+ };
1366
+ }
1367
+
1368
+ // src/proxy.ts
1369
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1370
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1371
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1372
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1373
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1374
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1375
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1376
+ import {
1377
+ ListToolsRequestSchema,
1378
+ CallToolRequestSchema,
1379
+ ListResourcesRequestSchema,
1380
+ ListPromptsRequestSchema,
1381
+ GetPromptRequestSchema,
1382
+ ReadResourceRequestSchema,
1383
+ ListResourceTemplatesRequestSchema
1384
+ } from "@modelcontextprotocol/sdk/types.js";
1385
+ import { createServer as createHttpServer } from "http";
1386
+
1387
+ // ../sdk-ts/dist/index.js
1388
+ import { z as z2 } from "zod";
1389
+ import { createHash, randomUUID, createHmac } from "crypto";
1390
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1391
+ var SolonGateError = class extends Error {
1392
+ code;
1393
+ timestamp;
1394
+ details;
1395
+ constructor(message, code, details = {}) {
1396
+ super(message);
1397
+ this.name = "SolonGateError";
1398
+ this.code = code;
1399
+ this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
1400
+ this.details = Object.freeze({ ...details });
1401
+ Object.setPrototypeOf(this, new.target.prototype);
1402
+ }
1403
+ /**
1404
+ * Serializable representation for logging and API responses.
1405
+ * Never includes stack traces (information leakage prevention).
1406
+ */
1407
+ toJSON() {
1408
+ return {
1409
+ name: this.name,
1410
+ code: this.code,
1411
+ message: this.message,
1412
+ timestamp: this.timestamp,
1413
+ details: this.details
1414
+ };
1415
+ }
1416
+ };
1417
+ var PolicyDeniedError = class extends SolonGateError {
1418
+ constructor(toolName, reason, details = {}) {
1419
+ super(
1420
+ `Policy denied execution of tool "${toolName}": ${reason}`,
1421
+ "POLICY_DENIED",
1422
+ { toolName, reason, ...details }
1423
+ );
1424
+ this.name = "PolicyDeniedError";
1425
+ }
1426
+ };
1427
+ var SchemaValidationError = class extends SolonGateError {
1428
+ constructor(toolName, validationErrors) {
1429
+ super(
1430
+ `Schema validation failed for tool "${toolName}": ${validationErrors.join("; ")}`,
1431
+ "SCHEMA_VALIDATION_FAILED",
1432
+ { toolName, validationErrors }
1433
+ );
1434
+ this.name = "SchemaValidationError";
1435
+ }
1436
+ };
1437
+ var RateLimitError = class extends SolonGateError {
1438
+ constructor(toolName, limitPerMinute) {
1439
+ super(
1440
+ `Rate limit exceeded for tool "${toolName}": max ${limitPerMinute}/min`,
1441
+ "RATE_LIMIT_EXCEEDED",
1442
+ { toolName, limitPerMinute }
1443
+ );
1444
+ this.name = "RateLimitError";
1445
+ }
1446
+ };
1447
+ var TrustLevel = {
1448
+ UNTRUSTED: "UNTRUSTED",
1449
+ VERIFIED: "VERIFIED",
1450
+ TRUSTED: "TRUSTED"
1451
+ };
1452
+ var Permission2 = {
1453
+ READ: "READ",
1454
+ WRITE: "WRITE",
1455
+ EXECUTE: "EXECUTE"
1456
+ };
1457
+ z2.enum(["READ", "WRITE", "EXECUTE"]);
1458
+ Object.freeze(
1459
+ /* @__PURE__ */ new Set()
1460
+ );
1461
+ Object.freeze(
1462
+ /* @__PURE__ */ new Set([Permission2.READ])
1463
+ );
1464
+ var PolicyEffect = {
1465
+ ALLOW: "ALLOW",
1466
+ DENY: "DENY"
1467
+ };
1468
+ var PolicyRuleSchema2 = z2.object({
1469
+ id: z2.string().min(1).max(256),
1470
+ description: z2.string().max(1024),
1471
+ effect: z2.enum(["ALLOW", "DENY"]),
1472
+ priority: z2.number().int().min(0).max(1e4).default(1e3),
1473
+ toolPattern: z2.string().min(1).max(512),
1474
+ permission: z2.enum(["READ", "WRITE", "EXECUTE"]),
1475
+ minimumTrustLevel: z2.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
1476
+ argumentConstraints: z2.record(z2.unknown()).optional(),
1477
+ pathConstraints: z2.object({
1478
+ allowed: z2.array(z2.string()).optional(),
1479
+ denied: z2.array(z2.string()).optional(),
1480
+ rootDirectory: z2.string().optional(),
1481
+ allowSymlinks: z2.boolean().optional()
1482
+ }).optional(),
1483
+ enabled: z2.boolean().default(true),
1484
+ createdAt: z2.string().datetime(),
1485
+ updatedAt: z2.string().datetime()
1486
+ });
1487
+ var PolicySetSchema2 = z2.object({
1488
+ id: z2.string().min(1).max(256),
1489
+ name: z2.string().min(1).max(256),
1490
+ description: z2.string().max(2048),
1491
+ version: z2.number().int().min(0),
1492
+ rules: z2.array(PolicyRuleSchema2),
1493
+ createdAt: z2.string().datetime(),
1494
+ updatedAt: z2.string().datetime()
1495
+ });
1496
+ function createSecurityContext(params) {
1497
+ return {
1498
+ trustLevel: "UNTRUSTED",
1499
+ grantedPermissions: /* @__PURE__ */ new Set(),
1500
+ sessionId: null,
1501
+ metadata: {},
1502
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1503
+ ...params
1504
+ };
1505
+ }
1506
+ var DEFAULT_POLICY_EFFECT = "DENY";
1507
+ var MAX_RULES_PER_POLICY_SET = 1e3;
1508
+ var POLICY_EVALUATION_TIMEOUT_MS = 100;
1509
+ var RATE_LIMIT_WINDOW_MS = 6e4;
1510
+ var RATE_LIMIT_MAX_ENTRIES = 1e4;
1511
+ var UNSAFE_CONFIGURATION_WARNINGS = {
1512
+ WILDCARD_ALLOW: "Wildcard ALLOW rules grant permission to ALL tools. This bypasses the default-deny model.",
1513
+ TRUSTED_LEVEL_EXTERNAL: "Setting trust level to TRUSTED for external requests bypasses all security checks.",
1514
+ EXECUTE_WITHOUT_REVIEW: "EXECUTE permission allows tools to perform arbitrary actions. Review carefully.",
1515
+ RATE_LIMIT_ZERO: "A rate limit of 0 means unlimited calls. This removes protection against runaway loops.",
1516
+ DISABLED_VALIDATION: "Disabling schema validation removes input sanitization protections."
1517
+ };
1518
+ function createDeniedToolResult(reason) {
1519
+ return {
1520
+ content: [
1521
+ {
1522
+ type: "text",
1523
+ text: JSON.stringify({
1524
+ error: "POLICY_DENIED",
1525
+ message: reason,
1526
+ hint: "This tool call was blocked by SolonGate security policy. Check your policy configuration."
1527
+ })
1528
+ }
1529
+ ],
1530
+ isError: true
1531
+ };
1532
+ }
1533
+ var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
1534
+ pathTraversal: true,
1535
+ shellInjection: true,
1536
+ wildcardAbuse: true,
1537
+ lengthLimit: 4096,
1538
+ entropyLimit: true,
1539
+ ssrf: true,
1540
+ sqlInjection: true
1541
+ });
1542
+ var PATH_TRAVERSAL_PATTERNS = [
1543
+ /\.\.\//,
1544
+ // ../
1545
+ /\.\.\\/,
1546
+ // ..\
1547
+ /%2e%2e/i,
1548
+ // URL-encoded ..
1549
+ /%2e\./i,
1550
+ // partial URL-encoded
1551
+ /\.%2e/i,
1552
+ // partial URL-encoded
1553
+ /%252e%252e/i,
1554
+ // double URL-encoded
1555
+ /\.\.\0/
1556
+ // null byte variant
1557
+ ];
1558
+ var SENSITIVE_PATHS = [
1559
+ /\/etc\/passwd/i,
1560
+ /\/etc\/shadow/i,
1561
+ /\/proc\//i,
1562
+ /\/dev\//i,
1563
+ /c:\\windows\\system32/i,
1564
+ /c:\\windows\\syswow64/i,
1565
+ /\/root\//i,
1566
+ /~\//,
1567
+ /\.env(\.|$)/i,
1568
+ // .env, .env.local, .env.production
1569
+ /\.aws\/credentials/i,
1570
+ // AWS credentials
1571
+ /\.ssh\/id_/i,
1572
+ // SSH keys
1573
+ /\.kube\/config/i,
1574
+ // Kubernetes config
1575
+ /wp-config\.php/i,
1576
+ // WordPress config
1577
+ /\.git\/config/i,
1578
+ // Git config
1579
+ /\.npmrc/i,
1580
+ // npm credentials
1581
+ /\.pypirc/i
1582
+ // PyPI credentials
1583
+ ];
1584
+ function detectPathTraversal(value) {
1585
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
1586
+ if (pattern.test(value)) return true;
1587
+ }
1588
+ for (const pattern of SENSITIVE_PATHS) {
1589
+ if (pattern.test(value)) return true;
1590
+ }
1591
+ return false;
1592
+ }
1593
+ var SHELL_INJECTION_PATTERNS = [
1594
+ /[;|&`]/,
1595
+ // Command separators and backtick execution
1596
+ /\$\(/,
1597
+ // Command substitution $(...)
1598
+ /\$\{/,
1599
+ // Variable expansion ${...}
1600
+ />\s*/,
1601
+ // Output redirect
1602
+ /<\s*/,
1603
+ // Input redirect
1604
+ /&&/,
1605
+ // AND chaining
1606
+ /\|\|/,
1607
+ // OR chaining
1608
+ /\beval\b/i,
1609
+ // eval command
1610
+ /\bexec\b/i,
1611
+ // exec command
1612
+ /\bsystem\b/i,
1613
+ // system call
1614
+ /%0a/i,
1615
+ // URL-encoded newline
1616
+ /%0d/i,
1617
+ // URL-encoded carriage return
1618
+ /%09/i,
1619
+ // URL-encoded tab
1620
+ /\r\n/,
1621
+ // CRLF injection
1622
+ /\n/
1623
+ // Newline (command separator on Unix)
1624
+ ];
1625
+ function detectShellInjection(value) {
1626
+ for (const pattern of SHELL_INJECTION_PATTERNS) {
1627
+ if (pattern.test(value)) return true;
1628
+ }
1629
+ return false;
1630
+ }
1631
+ var MAX_WILDCARDS_PER_VALUE = 3;
1632
+ function detectWildcardAbuse(value) {
1633
+ if (value.includes("**")) return true;
1634
+ const wildcardCount = (value.match(/\*/g) || []).length;
1635
+ if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
1636
+ return false;
1637
+ }
1638
+ var SSRF_PATTERNS2 = [
1639
+ /^https?:\/\/localhost\b/i,
1640
+ /^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
1641
+ /^https?:\/\/0\.0\.0\.0/,
1642
+ /^https?:\/\/\[::1\]/,
1643
+ // IPv6 loopback
1644
+ /^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
1645
+ // 10.x.x.x
1646
+ /^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
1647
+ // 172.16-31.x.x
1648
+ /^https?:\/\/192\.168\./,
1649
+ // 192.168.x.x
1650
+ /^https?:\/\/169\.254\./,
1651
+ // Link-local / AWS metadata
1652
+ /metadata\.google\.internal/i,
1653
+ // GCP metadata
1654
+ /^https?:\/\/metadata\b/i,
1655
+ // Generic metadata endpoint
1656
+ // IPv6 bypass patterns
1657
+ /^https?:\/\/\[fe80:/i,
1658
+ // IPv6 link-local
1659
+ /^https?:\/\/\[fc00:/i,
1660
+ // IPv6 unique local
1661
+ /^https?:\/\/\[fd[0-9a-f]{2}:/i,
1662
+ // IPv6 unique local (fd00::/8)
1663
+ /^https?:\/\/\[::ffff:127\./i,
1664
+ // IPv4-mapped IPv6 loopback
1665
+ /^https?:\/\/\[::ffff:10\./i,
1666
+ // IPv4-mapped IPv6 private
1667
+ /^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
1668
+ // IPv4-mapped IPv6 private
1669
+ /^https?:\/\/\[::ffff:192\.168\./i,
1670
+ // IPv4-mapped IPv6 private
1671
+ /^https?:\/\/\[::ffff:169\.254\./i,
1672
+ // IPv4-mapped IPv6 link-local
1673
+ // Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
1674
+ /^https?:\/\/0x[0-9a-f]+\b/i,
1675
+ // Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
1676
+ /^https?:\/\/0[0-7]{1,3}\./
1677
+ ];
1678
+ function detectDecimalIP2(value) {
1679
+ const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
1680
+ if (!match || !match[1]) return false;
1681
+ const decimal = parseInt(match[1], 10);
1682
+ if (isNaN(decimal) || decimal > 4294967295) return false;
1683
+ return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
1684
+ decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
1685
+ decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
1686
+ decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
1687
+ decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
1688
+ decimal === 0;
1689
+ }
1690
+ function detectSSRF2(value) {
1691
+ for (const pattern of SSRF_PATTERNS2) {
1692
+ if (pattern.test(value)) return true;
1693
+ }
1694
+ if (detectDecimalIP2(value)) return true;
1695
+ return false;
1696
+ }
1697
+ var SQL_INJECTION_PATTERNS = [
1698
+ /'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
1699
+ // ' OR '1'='1 — bounded to prevent ReDoS
1700
+ /'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
1701
+ // '; DROP TABLE
1702
+ /UNION\s+(ALL\s+)?SELECT/i,
1703
+ // UNION SELECT
1704
+ /--\s*$/m,
1705
+ // SQL comment at end of line
1706
+ /\/\*.{0,500}?\*\//,
1707
+ // SQL block comment — bounded + non-greedy
1708
+ /\bSLEEP\s*\(/i,
1709
+ // Time-based injection
1710
+ /\bBENCHMARK\s*\(/i,
1711
+ // MySQL benchmark
1712
+ /\bWAITFOR\s+DELAY/i,
1713
+ // MSSQL delay
1714
+ /\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
1715
+ // File operations
1716
+ ];
1717
+ function detectSQLInjection(value) {
1718
+ for (const pattern of SQL_INJECTION_PATTERNS) {
1719
+ if (pattern.test(value)) return true;
1720
+ }
1721
+ return false;
1722
+ }
1723
+ function checkLengthLimits(value, maxLength = 4096) {
1724
+ return value.length <= maxLength;
1725
+ }
1726
+ var ENTROPY_THRESHOLD = 4.5;
1727
+ var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
1728
+ function checkEntropyLimits(value) {
1729
+ if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
1730
+ const entropy = calculateShannonEntropy(value);
1731
+ return entropy <= ENTROPY_THRESHOLD;
1732
+ }
1733
+ function calculateShannonEntropy(str) {
1734
+ const freq = /* @__PURE__ */ new Map();
1735
+ for (const char of str) {
1736
+ freq.set(char, (freq.get(char) ?? 0) + 1);
1737
+ }
1738
+ let entropy = 0;
1739
+ const len = str.length;
1740
+ for (const count of freq.values()) {
1741
+ const p = count / len;
1742
+ if (p > 0) {
1743
+ entropy -= p * Math.log2(p);
1596
1744
  }
1597
1745
  }
1598
- if (!apiKey) {
1599
- const envKey = process.env.SOLONGATE_API_KEY;
1600
- if (envKey) {
1601
- apiKey = envKey;
1602
- } else {
1603
- throw new Error(
1604
- "A valid SolonGate API key is required.\n\nUsage: solongate-proxy --api-key sg_live_xxx -- <command>\n or: set SOLONGATE_API_KEY=sg_live_xxx\n\nGet your API key at https://solongate.com\n"
1605
- );
1746
+ return entropy;
1747
+ }
1748
+ function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
1749
+ const threats = [];
1750
+ if (typeof value !== "string") {
1751
+ if (typeof value === "object" && value !== null) {
1752
+ return sanitizeObject(field, value, config);
1606
1753
  }
1754
+ return { safe: true, threats: [] };
1607
1755
  }
1608
- if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
1609
- throw new Error(
1610
- "Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
1611
- );
1756
+ if (config.pathTraversal && detectPathTraversal(value)) {
1757
+ threats.push({
1758
+ type: "PATH_TRAVERSAL",
1759
+ field,
1760
+ value: truncate(value, 100),
1761
+ description: "Path traversal pattern detected"
1762
+ });
1612
1763
  }
1613
- if (configFile) {
1614
- const filePath = resolve(configFile);
1615
- const content = readFileSync(filePath, "utf-8");
1616
- const fileConfig = JSON.parse(content);
1617
- if (!fileConfig.upstream) {
1618
- throw new Error('Config file must include "upstream" with at least "command" or "url"');
1619
- }
1620
- if (fileConfig.upstream.url && detectSSRF(fileConfig.upstream.url)) {
1621
- throw new Error(
1622
- `Upstream URL blocked: "${fileConfig.upstream.url}" points to an internal or private network address.`
1623
- );
1624
- }
1625
- return {
1626
- upstream: fileConfig.upstream,
1627
- policy: loadPolicy(fileConfig.policy ?? policySource),
1628
- name: fileConfig.name ?? name,
1629
- verbose: fileConfig.verbose ?? verbose,
1630
- validateInput: fileConfig.validateInput ?? validateInput,
1631
- rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
1632
- globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
1633
- apiKey: apiKey ?? fileConfig.apiKey,
1634
- apiUrl: apiUrl ?? fileConfig.apiUrl,
1635
- port: port ?? fileConfig.port
1636
- };
1764
+ if (config.shellInjection && detectShellInjection(value)) {
1765
+ threats.push({
1766
+ type: "SHELL_INJECTION",
1767
+ field,
1768
+ value: truncate(value, 100),
1769
+ description: "Shell injection pattern detected"
1770
+ });
1637
1771
  }
1638
- if (upstreamUrl) {
1639
- if (detectSSRF(upstreamUrl)) {
1640
- throw new Error(
1641
- `Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
1642
- SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
1643
- );
1644
- }
1645
- const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
1646
- return {
1647
- upstream: {
1648
- transport,
1649
- command: "",
1650
- // not used for URL-based transports
1651
- url: upstreamUrl
1652
- },
1653
- policy: loadPolicy(policySource),
1654
- name,
1655
- verbose,
1656
- validateInput,
1657
- rateLimitPerTool,
1658
- globalRateLimit,
1659
- apiKey,
1660
- apiUrl,
1661
- port
1662
- };
1772
+ if (config.wildcardAbuse && detectWildcardAbuse(value)) {
1773
+ threats.push({
1774
+ type: "WILDCARD_ABUSE",
1775
+ field,
1776
+ value: truncate(value, 100),
1777
+ description: "Wildcard abuse pattern detected"
1778
+ });
1663
1779
  }
1664
- if (upstreamArgs.length === 0) {
1665
- throw new Error(
1666
- "No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
1667
- );
1780
+ if (!checkLengthLimits(value, config.lengthLimit)) {
1781
+ threats.push({
1782
+ type: "LENGTH_EXCEEDED",
1783
+ field,
1784
+ value: `[${value.length} chars]`,
1785
+ description: `Value exceeds maximum length of ${config.lengthLimit}`
1786
+ });
1668
1787
  }
1669
- const [command, ...commandArgs] = upstreamArgs;
1670
- return {
1671
- upstream: {
1672
- transport: upstreamTransport ?? "stdio",
1673
- command,
1674
- args: commandArgs,
1675
- env: { ...process.env }
1676
- },
1677
- policy: loadPolicy(policySource),
1678
- name,
1679
- verbose,
1680
- validateInput,
1681
- rateLimitPerTool,
1682
- globalRateLimit,
1683
- apiKey,
1684
- apiUrl,
1685
- port
1686
- };
1788
+ if (config.entropyLimit && !checkEntropyLimits(value)) {
1789
+ threats.push({
1790
+ type: "HIGH_ENTROPY",
1791
+ field,
1792
+ value: truncate(value, 100),
1793
+ description: "High entropy string detected - possible encoded payload"
1794
+ });
1795
+ }
1796
+ if (config.ssrf && detectSSRF2(value)) {
1797
+ threats.push({
1798
+ type: "SSRF",
1799
+ field,
1800
+ value: truncate(value, 100),
1801
+ description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
1802
+ });
1803
+ }
1804
+ if (config.sqlInjection && detectSQLInjection(value)) {
1805
+ threats.push({
1806
+ type: "SQL_INJECTION",
1807
+ field,
1808
+ value: truncate(value, 100),
1809
+ description: "SQL injection pattern detected"
1810
+ });
1811
+ }
1812
+ return { safe: threats.length === 0, threats };
1687
1813
  }
1688
-
1689
- // src/proxy.ts
1690
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1691
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1692
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1693
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
1694
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
1695
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1696
- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1697
- import {
1698
- ListToolsRequestSchema,
1699
- CallToolRequestSchema,
1700
- ListResourcesRequestSchema,
1701
- ListPromptsRequestSchema,
1702
- GetPromptRequestSchema,
1703
- ReadResourceRequestSchema,
1704
- ListResourceTemplatesRequestSchema
1705
- } from "@modelcontextprotocol/sdk/types.js";
1706
- import { createServer as createHttpServer } from "http";
1707
-
1708
- // ../policy-engine/dist/index.js
1709
- import { createHash } from "crypto";
1814
+ function sanitizeObject(basePath, obj, config) {
1815
+ const threats = [];
1816
+ if (Array.isArray(obj)) {
1817
+ for (let i = 0; i < obj.length; i++) {
1818
+ const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
1819
+ threats.push(...result.threats);
1820
+ }
1821
+ } else {
1822
+ for (const [key, val] of Object.entries(obj)) {
1823
+ const result = sanitizeInput(`${basePath}.${key}`, val, config);
1824
+ threats.push(...result.threats);
1825
+ }
1826
+ }
1827
+ return { safe: threats.length === 0, threats };
1828
+ }
1829
+ function truncate(str, maxLen) {
1830
+ return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
1831
+ }
1832
+ var DEFAULT_TOKEN_TTL_SECONDS = 30;
1833
+ var TOKEN_ALGORITHM = "HS256";
1834
+ var MIN_SECRET_LENGTH = 32;
1710
1835
  function normalizePath(path) {
1711
1836
  let normalized = path.replace(/\\/g, "/");
1712
1837
  if (normalized.length > 1 && normalized.endsWith("/")) {
@@ -1945,7 +2070,7 @@ function evaluatePolicy(policySet, request) {
1945
2070
  function validatePolicyRule(input) {
1946
2071
  const errors = [];
1947
2072
  const warnings = [];
1948
- const result = PolicyRuleSchema.safeParse(input);
2073
+ const result = PolicyRuleSchema2.safeParse(input);
1949
2074
  if (!result.success) {
1950
2075
  return {
1951
2076
  valid: false,
@@ -1970,7 +2095,7 @@ function validatePolicyRule(input) {
1970
2095
  function validatePolicySet(input) {
1971
2096
  const errors = [];
1972
2097
  const warnings = [];
1973
- const result = PolicySetSchema.safeParse(input);
2098
+ const result = PolicySetSchema2.safeParse(input);
1974
2099
  if (!result.success) {
1975
2100
  return {
1976
2101
  valid: false,
@@ -2064,7 +2189,7 @@ function createDefaultDenyPolicySet() {
2064
2189
  effect: PolicyEffect.DENY,
2065
2190
  priority: 1e4,
2066
2191
  toolPattern: "*",
2067
- permission: Permission.EXECUTE,
2192
+ permission: Permission2.EXECUTE,
2068
2193
  minimumTrustLevel: TrustLevel.UNTRUSTED,
2069
2194
  enabled: true,
2070
2195
  createdAt: now,
@@ -2076,7 +2201,7 @@ function createDefaultDenyPolicySet() {
2076
2201
  effect: PolicyEffect.DENY,
2077
2202
  priority: 1e4,
2078
2203
  toolPattern: "*",
2079
- permission: Permission.WRITE,
2204
+ permission: Permission2.WRITE,
2080
2205
  minimumTrustLevel: TrustLevel.UNTRUSTED,
2081
2206
  enabled: true,
2082
2207
  createdAt: now,
@@ -2088,7 +2213,7 @@ function createDefaultDenyPolicySet() {
2088
2213
  effect: PolicyEffect.DENY,
2089
2214
  priority: 1e4,
2090
2215
  toolPattern: "*",
2091
- permission: Permission.READ,
2216
+ permission: Permission2.READ,
2092
2217
  minimumTrustLevel: TrustLevel.UNTRUSTED,
2093
2218
  enabled: true,
2094
2219
  createdAt: now,
@@ -2258,10 +2383,6 @@ var PolicyStore = class {
2258
2383
  return createHash("sha256").update(serialized).digest("hex");
2259
2384
  }
2260
2385
  };
2261
-
2262
- // ../sdk-ts/dist/index.js
2263
- import { randomUUID, createHmac } from "crypto";
2264
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2265
2386
  var DEFAULT_CONFIG = Object.freeze({
2266
2387
  validateSchemas: true,
2267
2388
  enableLogging: true,
@@ -2271,7 +2392,7 @@ var DEFAULT_CONFIG = Object.freeze({
2271
2392
  globalRateLimitPerMinute: 600,
2272
2393
  rateLimitPerTool: 60,
2273
2394
  tokenTtlSeconds: 30,
2274
- inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG,
2395
+ inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG2,
2275
2396
  enableVersionedPolicies: true
2276
2397
  });
2277
2398
  function resolveConfig(userConfig) {
@@ -2304,7 +2425,7 @@ async function interceptToolCall(params, upstreamCall, options) {
2304
2425
  toolName: params.name,
2305
2426
  serverName: "default",
2306
2427
  arguments: params.arguments ?? {},
2307
- requiredPermission: Permission.EXECUTE,
2428
+ requiredPermission: Permission2.EXECUTE,
2308
2429
  timestamp
2309
2430
  };
2310
2431
  if (options.rateLimiter) {
@@ -2343,7 +2464,7 @@ async function interceptToolCall(params, upstreamCall, options) {
2343
2464
  }
2344
2465
  }
2345
2466
  if (options.validateSchemas && params.arguments) {
2346
- const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG;
2467
+ const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG2;
2347
2468
  const sanitization = sanitizeInput("arguments", params.arguments, guardConfig);
2348
2469
  if (!sanitization.safe) {
2349
2470
  const threatDescriptions = sanitization.threats.map(
@@ -2376,7 +2497,7 @@ async function interceptToolCall(params, upstreamCall, options) {
2376
2497
  if (options.tokenIssuer) {
2377
2498
  capabilityToken = options.tokenIssuer.issue(
2378
2499
  requestId,
2379
- [Permission.EXECUTE],
2500
+ [Permission2.EXECUTE],
2380
2501
  [params.name]
2381
2502
  );
2382
2503
  }