@solongate/proxy 0.7.0 → 0.8.0

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/create.js CHANGED
@@ -68,7 +68,7 @@ function parseCreateArgs(argv) {
68
68
  const args = argv.slice(2);
69
69
  const opts = {
70
70
  name: "",
71
- policy: "restricted",
71
+ policy: "",
72
72
  noInstall: false
73
73
  };
74
74
  for (let i = 0; i < args.length; i++) {
@@ -111,13 +111,13 @@ USAGE
111
111
  npx @solongate/proxy create <name> [options]
112
112
 
113
113
  OPTIONS
114
- --policy <preset> Policy preset (default: restricted)
114
+ --policy <file> Policy JSON file (default: cloud-managed)
115
115
  --no-install Skip dependency installation
116
116
  -h, --help Show this help message
117
117
 
118
118
  EXAMPLES
119
119
  npx @solongate/proxy create my-server
120
- npx @solongate/proxy create db-tools --policy read-only
120
+ npx @solongate/proxy create db-tools --policy ./policy.json
121
121
  `);
122
122
  }
123
123
  function createProject(dir, name, _policy) {
package/dist/index.js CHANGED
@@ -65,19 +65,16 @@ async function sendAuditLog(apiKey, apiUrl, entry) {
65
65
  }
66
66
  function loadPolicy(source) {
67
67
  if (typeof source === "object") return source;
68
- if (PRESETS[source]) return PRESETS[source];
69
68
  const filePath = resolve(source);
70
69
  if (existsSync(filePath)) {
71
70
  const content = readFileSync(filePath, "utf-8");
72
71
  return JSON.parse(content);
73
72
  }
74
- throw new Error(
75
- `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
76
- );
73
+ return DEFAULT_POLICY;
77
74
  }
78
75
  function parseArgs(argv) {
79
76
  const args = argv.slice(2);
80
- let policySource = "restricted";
77
+ let policySource;
81
78
  let name = "solongate-proxy";
82
79
  let verbose = false;
83
80
  let rateLimitPerTool;
@@ -159,7 +156,7 @@ function parseArgs(argv) {
159
156
  "Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
160
157
  );
161
158
  }
162
- const resolvedPolicyPath = resolvePolicyPath(policySource);
159
+ const resolvedPolicyPath = policySource ? resolvePolicyPath(policySource) : null;
163
160
  if (configFile) {
164
161
  const filePath = resolve(configFile);
165
162
  const content = readFileSync(filePath, "utf-8");
@@ -167,7 +164,7 @@ function parseArgs(argv) {
167
164
  if (!fileConfig.upstream) {
168
165
  throw new Error('Config file must include "upstream" with at least "command" or "url"');
169
166
  }
170
- const cfgPolicySource = fileConfig.policy ?? policySource;
167
+ const cfgPolicySource = fileConfig.policy ?? policySource ?? "policy.json";
171
168
  return {
172
169
  upstream: fileConfig.upstream,
173
170
  policy: loadPolicy(cfgPolicySource),
@@ -191,7 +188,7 @@ function parseArgs(argv) {
191
188
  // not used for URL-based transports
192
189
  url: upstreamUrl
193
190
  },
194
- policy: loadPolicy(policySource),
191
+ policy: loadPolicy(policySource ?? "policy.json"),
195
192
  name,
196
193
  verbose,
197
194
  rateLimitPerTool,
@@ -205,7 +202,7 @@ function parseArgs(argv) {
205
202
  }
206
203
  if (upstreamArgs.length === 0) {
207
204
  throw new Error(
208
- "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 @playwright/mcp@latest\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
205
+ "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 ./policy.json -- npx @playwright/mcp@latest\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
209
206
  );
210
207
  }
211
208
  const [command, ...commandArgs] = upstreamArgs;
@@ -216,7 +213,7 @@ function parseArgs(argv) {
216
213
  args: commandArgs,
217
214
  env: { ...process.env }
218
215
  },
219
- policy: loadPolicy(policySource),
216
+ policy: loadPolicy(policySource ?? "policy.json"),
220
217
  name,
221
218
  verbose,
222
219
  rateLimitPerTool,
@@ -229,303 +226,22 @@ function parseArgs(argv) {
229
226
  };
230
227
  }
231
228
  function resolvePolicyPath(source) {
232
- if (PRESETS[source]) return null;
233
229
  const filePath = resolve(source);
234
230
  if (existsSync(filePath)) return filePath;
235
231
  return null;
236
232
  }
237
- var PRESETS;
233
+ var DEFAULT_POLICY;
238
234
  var init_config = __esm({
239
235
  "src/config.ts"() {
240
236
  "use strict";
241
- PRESETS = {
242
- restricted: {
243
- id: "restricted",
244
- name: "Restricted",
245
- description: "Blocks dangerous commands (rm -rf, curl|bash, dd, etc.), allows safe tools with path restrictions",
246
- version: 1,
247
- rules: [
248
- {
249
- id: "deny-dangerous-commands",
250
- description: "Block dangerous shell commands",
251
- effect: "DENY",
252
- priority: 50,
253
- toolPattern: "*",
254
- permission: "EXECUTE",
255
- minimumTrustLevel: "UNTRUSTED",
256
- enabled: true,
257
- commandConstraints: {
258
- denied: [
259
- "rm -rf *",
260
- "rm -r /*",
261
- "mkfs*",
262
- "dd if=*",
263
- "curl*|*bash*",
264
- "curl*|*sh*",
265
- "wget*|*bash*",
266
- "wget*|*sh*",
267
- "shutdown*",
268
- "reboot*",
269
- "kill -9*",
270
- "chmod*777*",
271
- "iptables*",
272
- "passwd*",
273
- "useradd*",
274
- "userdel*"
275
- ]
276
- },
277
- createdAt: "",
278
- updatedAt: ""
279
- },
280
- {
281
- id: "deny-sensitive-paths",
282
- description: "Block access to sensitive files",
283
- effect: "DENY",
284
- priority: 51,
285
- toolPattern: "*",
286
- permission: "EXECUTE",
287
- minimumTrustLevel: "UNTRUSTED",
288
- enabled: true,
289
- pathConstraints: {
290
- denied: [
291
- "**/.env*",
292
- "**/.ssh/**",
293
- "**/.aws/**",
294
- "**/.kube/**",
295
- "**/credentials*",
296
- "**/secrets*",
297
- "**/*.pem",
298
- "**/*.key",
299
- "/etc/passwd",
300
- "/etc/shadow",
301
- "/proc/**",
302
- "/dev/**"
303
- ]
304
- },
305
- createdAt: "",
306
- updatedAt: ""
307
- },
308
- {
309
- id: "allow-rest",
310
- description: "Allow all other tool calls",
311
- effect: "ALLOW",
312
- priority: 1e3,
313
- toolPattern: "*",
314
- permission: "EXECUTE",
315
- minimumTrustLevel: "UNTRUSTED",
316
- enabled: true,
317
- createdAt: "",
318
- updatedAt: ""
319
- }
320
- ],
321
- createdAt: "",
322
- updatedAt: ""
323
- },
324
- "read-only": {
325
- id: "read-only",
326
- name: "Read Only",
327
- description: "Only allows read/list/get/search/query tools",
328
- version: 1,
329
- rules: [
330
- {
331
- id: "allow-read",
332
- description: "Allow read tools",
333
- effect: "ALLOW",
334
- priority: 100,
335
- toolPattern: "*read*",
336
- permission: "EXECUTE",
337
- minimumTrustLevel: "UNTRUSTED",
338
- enabled: true,
339
- createdAt: "",
340
- updatedAt: ""
341
- },
342
- {
343
- id: "allow-list",
344
- description: "Allow list tools",
345
- effect: "ALLOW",
346
- priority: 101,
347
- toolPattern: "*list*",
348
- permission: "EXECUTE",
349
- minimumTrustLevel: "UNTRUSTED",
350
- enabled: true,
351
- createdAt: "",
352
- updatedAt: ""
353
- },
354
- {
355
- id: "allow-get",
356
- description: "Allow get tools",
357
- effect: "ALLOW",
358
- priority: 102,
359
- toolPattern: "*get*",
360
- permission: "EXECUTE",
361
- minimumTrustLevel: "UNTRUSTED",
362
- enabled: true,
363
- createdAt: "",
364
- updatedAt: ""
365
- },
366
- {
367
- id: "allow-search",
368
- description: "Allow search tools",
369
- effect: "ALLOW",
370
- priority: 103,
371
- toolPattern: "*search*",
372
- permission: "EXECUTE",
373
- minimumTrustLevel: "UNTRUSTED",
374
- enabled: true,
375
- createdAt: "",
376
- updatedAt: ""
377
- },
378
- {
379
- id: "allow-query",
380
- description: "Allow query tools",
381
- effect: "ALLOW",
382
- priority: 104,
383
- toolPattern: "*query*",
384
- permission: "EXECUTE",
385
- minimumTrustLevel: "UNTRUSTED",
386
- enabled: true,
387
- createdAt: "",
388
- updatedAt: ""
389
- }
390
- ],
391
- createdAt: "",
392
- updatedAt: ""
393
- },
394
- sandbox: {
395
- id: "sandbox",
396
- name: "Sandbox",
397
- description: "Allows file ops within working directory only, blocks dangerous commands, allows safe shell commands",
398
- version: 1,
399
- rules: [
400
- {
401
- id: "deny-dangerous-commands",
402
- description: "Block dangerous shell commands",
403
- effect: "DENY",
404
- priority: 50,
405
- toolPattern: "*",
406
- permission: "EXECUTE",
407
- minimumTrustLevel: "UNTRUSTED",
408
- enabled: true,
409
- commandConstraints: {
410
- denied: [
411
- "rm -rf *",
412
- "rm -r /*",
413
- "mkfs*",
414
- "dd if=*",
415
- "curl*|*bash*",
416
- "wget*|*sh*",
417
- "shutdown*",
418
- "reboot*",
419
- "chmod*777*"
420
- ]
421
- },
422
- createdAt: "",
423
- updatedAt: ""
424
- },
425
- {
426
- id: "allow-safe-commands",
427
- description: "Allow safe shell commands only",
428
- effect: "ALLOW",
429
- priority: 100,
430
- toolPattern: "*shell*",
431
- permission: "EXECUTE",
432
- minimumTrustLevel: "UNTRUSTED",
433
- enabled: true,
434
- commandConstraints: {
435
- allowed: [
436
- "ls*",
437
- "cat*",
438
- "head*",
439
- "tail*",
440
- "wc*",
441
- "grep*",
442
- "find*",
443
- "echo*",
444
- "pwd",
445
- "whoami",
446
- "date",
447
- "env",
448
- "git*",
449
- "npm*",
450
- "pnpm*",
451
- "yarn*",
452
- "node*",
453
- "python*",
454
- "pip*"
455
- ]
456
- },
457
- createdAt: "",
458
- updatedAt: ""
459
- },
460
- {
461
- id: "deny-sensitive-paths",
462
- description: "Block access to sensitive files",
463
- effect: "DENY",
464
- priority: 51,
465
- toolPattern: "*",
466
- permission: "EXECUTE",
467
- minimumTrustLevel: "UNTRUSTED",
468
- enabled: true,
469
- pathConstraints: {
470
- denied: [
471
- "**/.env*",
472
- "**/.ssh/**",
473
- "**/.aws/**",
474
- "**/credentials*",
475
- "**/*.pem",
476
- "**/*.key"
477
- ]
478
- },
479
- createdAt: "",
480
- updatedAt: ""
481
- },
482
- {
483
- id: "allow-rest",
484
- description: "Allow all other tools",
485
- effect: "ALLOW",
486
- priority: 1e3,
487
- toolPattern: "*",
488
- permission: "EXECUTE",
489
- minimumTrustLevel: "UNTRUSTED",
490
- enabled: true,
491
- createdAt: "",
492
- updatedAt: ""
493
- }
494
- ],
495
- createdAt: "",
496
- updatedAt: ""
497
- },
498
- permissive: {
499
- id: "permissive",
500
- name: "Permissive",
501
- description: "Allows all tool calls (monitoring only)",
502
- version: 1,
503
- rules: [
504
- {
505
- id: "allow-all",
506
- description: "Allow all",
507
- effect: "ALLOW",
508
- priority: 1e3,
509
- toolPattern: "*",
510
- permission: "EXECUTE",
511
- minimumTrustLevel: "UNTRUSTED",
512
- enabled: true,
513
- createdAt: "",
514
- updatedAt: ""
515
- }
516
- ],
517
- createdAt: "",
518
- updatedAt: ""
519
- },
520
- "deny-all": {
521
- id: "deny-all",
522
- name: "Deny All",
523
- description: "Blocks all tool calls",
524
- version: 1,
525
- rules: [],
526
- createdAt: "",
527
- updatedAt: ""
528
- }
237
+ DEFAULT_POLICY = {
238
+ id: "default",
239
+ name: "Default (Deny All)",
240
+ description: "Default-deny policy. Create rules in the dashboard or push a policy.json.",
241
+ version: 1,
242
+ rules: [],
243
+ createdAt: "",
244
+ updatedAt: ""
529
245
  };
530
246
  }
531
247
  });
@@ -591,18 +307,14 @@ function isAlreadyProtected(server) {
591
307
  function wrapServer(server, policy) {
592
308
  const env = { ...server.env ?? {} };
593
309
  env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
310
+ const proxyArgs = ["-y", "@solongate/proxy"];
311
+ if (policy) {
312
+ proxyArgs.push("--policy", policy);
313
+ }
314
+ proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
594
315
  return {
595
316
  command: "npx",
596
- args: [
597
- "-y",
598
- "@solongate/proxy",
599
- "--policy",
600
- policy,
601
- "--verbose",
602
- "--",
603
- server.command,
604
- ...server.args ?? []
605
- ],
317
+ args: proxyArgs,
606
318
  env
607
319
  };
608
320
  }
@@ -651,8 +363,7 @@ USAGE
651
363
 
652
364
  OPTIONS
653
365
  --config <path> Path to MCP config file (default: auto-detect)
654
- --policy <file> Custom policy JSON file (default: restricted)
655
- Auto-detects policy.json in current directory
366
+ --policy <file> Custom policy JSON file (auto-detects policy.json)
656
367
  --api-key <key> SolonGate API key (sg_live_... or sg_test_...)
657
368
  --all Protect all servers without prompting
658
369
  -h, --help Show this help message
@@ -870,7 +581,7 @@ async function main() {
870
581
  console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
871
582
  process.exit(1);
872
583
  }
873
- let policyValue = "restricted";
584
+ let policyValue;
874
585
  if (options.policy) {
875
586
  const policyPath = resolve2(options.policy);
876
587
  if (existsSync3(policyPath)) {
@@ -886,7 +597,7 @@ async function main() {
886
597
  policyValue = "./policy.json";
887
598
  console.log(` Policy: ${defaultPolicy} (auto-detected)`);
888
599
  } else {
889
- console.log(` Policy: restricted (default)`);
600
+ console.log(` Policy: cloud-managed (fetched via API key)`);
890
601
  }
891
602
  }
892
603
  await sleep(300);
@@ -1726,7 +1437,7 @@ function parseCreateArgs(argv) {
1726
1437
  const args = argv.slice(2);
1727
1438
  const opts = {
1728
1439
  name: "",
1729
- policy: "restricted",
1440
+ policy: "",
1730
1441
  noInstall: false
1731
1442
  };
1732
1443
  for (let i = 0; i < args.length; i++) {
@@ -1769,13 +1480,13 @@ USAGE
1769
1480
  npx @solongate/proxy create <name> [options]
1770
1481
 
1771
1482
  OPTIONS
1772
- --policy <preset> Policy preset (default: restricted)
1483
+ --policy <file> Policy JSON file (default: cloud-managed)
1773
1484
  --no-install Skip dependency installation
1774
1485
  -h, --help Show this help message
1775
1486
 
1776
1487
  EXAMPLES
1777
1488
  npx @solongate/proxy create my-server
1778
- npx @solongate/proxy create db-tools --policy read-only
1489
+ npx @solongate/proxy create db-tools --policy ./policy.json
1779
1490
  `);
1780
1491
  }
1781
1492
  function createProject(dir, name, _policy) {
@@ -2440,6 +2151,14 @@ var PolicyRuleSchema = z.object({
2440
2151
  allowed: z.array(z.string()).optional(),
2441
2152
  denied: z.array(z.string()).optional()
2442
2153
  }).optional(),
2154
+ filenameConstraints: z.object({
2155
+ allowed: z.array(z.string()).optional(),
2156
+ denied: z.array(z.string()).optional()
2157
+ }).optional(),
2158
+ urlConstraints: z.object({
2159
+ allowed: z.array(z.string()).optional(),
2160
+ denied: z.array(z.string()).optional()
2161
+ }).optional(),
2443
2162
  enabled: z.boolean().default(true),
2444
2163
  createdAt: z.string().datetime(),
2445
2164
  updatedAt: z.string().datetime()
@@ -2625,11 +2344,48 @@ function matchSegmentGlob(segment, pattern) {
2625
2344
  }
2626
2345
  return segment === pattern;
2627
2346
  }
2347
+ var PATH_FIELDS = /* @__PURE__ */ new Set([
2348
+ "path",
2349
+ "file",
2350
+ "file_path",
2351
+ "filepath",
2352
+ "filename",
2353
+ "directory",
2354
+ "dir",
2355
+ "folder",
2356
+ "source",
2357
+ "destination",
2358
+ "dest",
2359
+ "target",
2360
+ "input",
2361
+ "output",
2362
+ "cwd",
2363
+ "root",
2364
+ "notebook_path"
2365
+ ]);
2628
2366
  function extractPathArguments(args) {
2629
2367
  const paths = [];
2630
- for (const value of Object.values(args)) {
2631
- if (typeof value === "string" && (value.includes("/") || value.includes("\\"))) {
2632
- paths.push(value);
2368
+ const seen = /* @__PURE__ */ new Set();
2369
+ function addPath(value) {
2370
+ const trimmed = value.trim();
2371
+ if (trimmed && !seen.has(trimmed)) {
2372
+ seen.add(trimmed);
2373
+ paths.push(trimmed);
2374
+ }
2375
+ }
2376
+ for (const [key, value] of Object.entries(args)) {
2377
+ if (typeof value !== "string") continue;
2378
+ if (PATH_FIELDS.has(key.toLowerCase())) {
2379
+ addPath(value);
2380
+ continue;
2381
+ }
2382
+ if (value.includes("/") || value.includes("\\")) {
2383
+ addPath(value);
2384
+ continue;
2385
+ }
2386
+ if (value.startsWith(".")) {
2387
+ addPath(value);
2388
+ continue;
2633
2389
  }
2634
2390
  }
2635
2391
  return paths;
@@ -2643,16 +2399,63 @@ var COMMAND_FIELDS = /* @__PURE__ */ new Set([
2643
2399
  "shell",
2644
2400
  "exec",
2645
2401
  "sql",
2646
- "expression"
2402
+ "expression",
2403
+ "function"
2647
2404
  ]);
2405
+ var COMMAND_HEURISTICS = [
2406
+ /^(sh|bash|cmd|powershell|zsh|fish)\s+-c\s+/i,
2407
+ // shell -c "..."
2408
+ /^(sudo|doas)\s+/i,
2409
+ // privilege escalation
2410
+ /^\w+\s+&&\s+/,
2411
+ // cmd1 && cmd2
2412
+ /^\w+\s*\|\s*\w+/,
2413
+ // cmd1 | cmd2
2414
+ /^\w+\s*;\s*\w+/,
2415
+ // cmd1; cmd2
2416
+ /^(curl|wget|nc|ncat)\s+/i,
2417
+ // network commands
2418
+ /^(rm|del|rmdir)\s+/i,
2419
+ // destructive commands
2420
+ /^(cat|type|more|less)\s+.*[/\\]/i
2421
+ // file read commands with paths
2422
+ ];
2648
2423
  function extractCommandArguments(args) {
2649
2424
  const commands = [];
2650
- for (const [key, value] of Object.entries(args)) {
2651
- if (typeof value !== "string") continue;
2652
- if (COMMAND_FIELDS.has(key.toLowerCase())) {
2653
- commands.push(value);
2425
+ const seen = /* @__PURE__ */ new Set();
2426
+ function addCommand(value) {
2427
+ const trimmed = value.trim();
2428
+ if (trimmed && !seen.has(trimmed)) {
2429
+ seen.add(trimmed);
2430
+ commands.push(trimmed);
2431
+ }
2432
+ }
2433
+ function scanValue(key, value) {
2434
+ if (typeof value === "string") {
2435
+ if (COMMAND_FIELDS.has(key.toLowerCase())) {
2436
+ addCommand(value);
2437
+ return;
2438
+ }
2439
+ for (const pattern of COMMAND_HEURISTICS) {
2440
+ if (pattern.test(value)) {
2441
+ addCommand(value);
2442
+ return;
2443
+ }
2444
+ }
2445
+ }
2446
+ if (Array.isArray(value)) {
2447
+ for (const item of value) {
2448
+ scanValue(key, item);
2449
+ }
2450
+ } else if (typeof value === "object" && value !== null) {
2451
+ for (const [k, v] of Object.entries(value)) {
2452
+ scanValue(k, v);
2453
+ }
2654
2454
  }
2655
2455
  }
2456
+ for (const [key, value] of Object.entries(args)) {
2457
+ scanValue(key, value);
2458
+ }
2656
2459
  return commands;
2657
2460
  }
2658
2461
  function matchCommandPattern(command, pattern) {
@@ -2697,6 +2500,224 @@ function isCommandAllowed(command, constraints) {
2697
2500
  }
2698
2501
  return true;
2699
2502
  }
2503
+ function extractFilenames(args) {
2504
+ const filenames = [];
2505
+ const seen = /* @__PURE__ */ new Set();
2506
+ function addFilename(name) {
2507
+ const trimmed = name.trim();
2508
+ if (trimmed && !seen.has(trimmed)) {
2509
+ seen.add(trimmed);
2510
+ filenames.push(trimmed);
2511
+ }
2512
+ }
2513
+ function scanValue(value) {
2514
+ if (typeof value === "string") {
2515
+ const trimmed = value.trim();
2516
+ if (!trimmed) return;
2517
+ if (/^https?:\/\//i.test(trimmed)) return;
2518
+ if (trimmed.includes("/") || trimmed.includes("\\")) {
2519
+ const normalized = trimmed.replace(/\\/g, "/");
2520
+ const parts = normalized.split("/");
2521
+ const basename = parts[parts.length - 1];
2522
+ if (basename && basename.length > 0) {
2523
+ addFilename(basename);
2524
+ }
2525
+ return;
2526
+ }
2527
+ if (trimmed.includes(" ")) {
2528
+ for (const token of trimmed.split(/\s+/)) {
2529
+ if (token.includes("/") || token.includes("\\")) {
2530
+ const parts = token.replace(/\\/g, "/").split("/");
2531
+ const basename = parts[parts.length - 1];
2532
+ if (basename && looksLikeFilename(basename)) addFilename(basename);
2533
+ } else if (looksLikeFilename(token)) {
2534
+ addFilename(token);
2535
+ }
2536
+ }
2537
+ return;
2538
+ }
2539
+ if (looksLikeFilename(trimmed)) {
2540
+ addFilename(trimmed);
2541
+ }
2542
+ return;
2543
+ }
2544
+ if (Array.isArray(value)) {
2545
+ for (const item of value) {
2546
+ scanValue(item);
2547
+ }
2548
+ } else if (typeof value === "object" && value !== null) {
2549
+ for (const v of Object.values(value)) {
2550
+ scanValue(v);
2551
+ }
2552
+ }
2553
+ }
2554
+ for (const value of Object.values(args)) {
2555
+ scanValue(value);
2556
+ }
2557
+ return filenames;
2558
+ }
2559
+ function looksLikeFilename(s) {
2560
+ if (s.startsWith(".")) return true;
2561
+ if (/\.\w+$/.test(s)) return true;
2562
+ const knownFiles = /* @__PURE__ */ new Set([
2563
+ "id_rsa",
2564
+ "id_dsa",
2565
+ "id_ecdsa",
2566
+ "id_ed25519",
2567
+ "authorized_keys",
2568
+ "known_hosts",
2569
+ "makefile",
2570
+ "dockerfile",
2571
+ "vagrantfile",
2572
+ "gemfile",
2573
+ "rakefile",
2574
+ "procfile"
2575
+ ]);
2576
+ if (knownFiles.has(s.toLowerCase())) return true;
2577
+ return false;
2578
+ }
2579
+ function matchFilenamePattern(filename, pattern) {
2580
+ if (pattern === "*") return true;
2581
+ const normalizedFilename = filename.toLowerCase();
2582
+ const normalizedPattern = pattern.toLowerCase();
2583
+ if (normalizedFilename === normalizedPattern) return true;
2584
+ const startsWithStar = normalizedPattern.startsWith("*");
2585
+ const endsWithStar = normalizedPattern.endsWith("*");
2586
+ if (startsWithStar && endsWithStar) {
2587
+ const infix = normalizedPattern.slice(1, -1);
2588
+ return infix.length > 0 && normalizedFilename.includes(infix);
2589
+ }
2590
+ if (startsWithStar) {
2591
+ const suffix = normalizedPattern.slice(1);
2592
+ return normalizedFilename.endsWith(suffix);
2593
+ }
2594
+ if (endsWithStar) {
2595
+ const prefix = normalizedPattern.slice(0, -1);
2596
+ return normalizedFilename.startsWith(prefix);
2597
+ }
2598
+ const starIdx = normalizedPattern.indexOf("*");
2599
+ if (starIdx !== -1) {
2600
+ const prefix = normalizedPattern.slice(0, starIdx);
2601
+ const suffix = normalizedPattern.slice(starIdx + 1);
2602
+ return normalizedFilename.startsWith(prefix) && normalizedFilename.endsWith(suffix) && normalizedFilename.length >= prefix.length + suffix.length;
2603
+ }
2604
+ return false;
2605
+ }
2606
+ function isFilenameAllowed(filename, constraints) {
2607
+ if (constraints.denied && constraints.denied.length > 0) {
2608
+ for (const pattern of constraints.denied) {
2609
+ if (matchFilenamePattern(filename, pattern)) {
2610
+ return false;
2611
+ }
2612
+ }
2613
+ }
2614
+ if (constraints.allowed && constraints.allowed.length > 0) {
2615
+ let matchesAllowed = false;
2616
+ for (const pattern of constraints.allowed) {
2617
+ if (matchFilenamePattern(filename, pattern)) {
2618
+ matchesAllowed = true;
2619
+ break;
2620
+ }
2621
+ }
2622
+ if (!matchesAllowed) return false;
2623
+ }
2624
+ return true;
2625
+ }
2626
+ var URL_FIELDS = /* @__PURE__ */ new Set([
2627
+ "url",
2628
+ "href",
2629
+ "uri",
2630
+ "endpoint",
2631
+ "link",
2632
+ "src",
2633
+ "source",
2634
+ "target",
2635
+ "redirect",
2636
+ "callback",
2637
+ "webhook"
2638
+ ]);
2639
+ function extractUrlArguments(args) {
2640
+ const urls = [];
2641
+ const seen = /* @__PURE__ */ new Set();
2642
+ function addUrl(value) {
2643
+ const trimmed = value.trim();
2644
+ if (trimmed && !seen.has(trimmed)) {
2645
+ seen.add(trimmed);
2646
+ urls.push(trimmed);
2647
+ }
2648
+ }
2649
+ function scanValue(key, value) {
2650
+ if (typeof value === "string") {
2651
+ const lower = key.toLowerCase();
2652
+ if (URL_FIELDS.has(lower)) {
2653
+ addUrl(value);
2654
+ return;
2655
+ }
2656
+ if (/https?:\/\//i.test(value)) {
2657
+ addUrl(value);
2658
+ return;
2659
+ }
2660
+ if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}(\/.*)?$/.test(value)) {
2661
+ addUrl(value);
2662
+ return;
2663
+ }
2664
+ }
2665
+ if (Array.isArray(value)) {
2666
+ for (const item of value) {
2667
+ scanValue(key, item);
2668
+ }
2669
+ } else if (typeof value === "object" && value !== null) {
2670
+ for (const [k, v] of Object.entries(value)) {
2671
+ scanValue(k, v);
2672
+ }
2673
+ }
2674
+ }
2675
+ for (const [key, value] of Object.entries(args)) {
2676
+ scanValue(key, value);
2677
+ }
2678
+ return urls;
2679
+ }
2680
+ function matchUrlPattern(url, pattern) {
2681
+ if (pattern === "*") return true;
2682
+ const normalizedUrl = url.trim().toLowerCase();
2683
+ const normalizedPattern = pattern.trim().toLowerCase();
2684
+ if (normalizedPattern === normalizedUrl) return true;
2685
+ const startsWithStar = normalizedPattern.startsWith("*");
2686
+ const endsWithStar = normalizedPattern.endsWith("*");
2687
+ if (startsWithStar && endsWithStar) {
2688
+ const infix = normalizedPattern.slice(1, -1);
2689
+ return infix.length > 0 && normalizedUrl.includes(infix);
2690
+ }
2691
+ if (endsWithStar) {
2692
+ const prefix = normalizedPattern.slice(0, -1);
2693
+ return normalizedUrl.startsWith(prefix);
2694
+ }
2695
+ if (startsWithStar) {
2696
+ const suffix = normalizedPattern.slice(1);
2697
+ return normalizedUrl.endsWith(suffix);
2698
+ }
2699
+ return false;
2700
+ }
2701
+ function isUrlAllowed(url, constraints) {
2702
+ if (constraints.denied && constraints.denied.length > 0) {
2703
+ for (const pattern of constraints.denied) {
2704
+ if (matchUrlPattern(url, pattern)) {
2705
+ return false;
2706
+ }
2707
+ }
2708
+ }
2709
+ if (constraints.allowed && constraints.allowed.length > 0) {
2710
+ let matchesAllowed = false;
2711
+ for (const pattern of constraints.allowed) {
2712
+ if (matchUrlPattern(url, pattern)) {
2713
+ matchesAllowed = true;
2714
+ break;
2715
+ }
2716
+ }
2717
+ if (!matchesAllowed) return false;
2718
+ }
2719
+ return true;
2720
+ }
2700
2721
  function ruleMatchesRequest(rule, request) {
2701
2722
  if (!rule.enabled) return false;
2702
2723
  if (rule.permission !== request.requiredPermission) return false;
@@ -2725,6 +2746,22 @@ function ruleMatchesRequest(rule, request) {
2725
2746
  if (!satisfied) return false;
2726
2747
  }
2727
2748
  }
2749
+ if (rule.filenameConstraints) {
2750
+ const satisfied = filenameConstraintsMatch(rule.filenameConstraints, request.arguments);
2751
+ if (rule.effect === "DENY") {
2752
+ if (satisfied) return false;
2753
+ } else {
2754
+ if (!satisfied) return false;
2755
+ }
2756
+ }
2757
+ if (rule.urlConstraints) {
2758
+ const satisfied = urlConstraintsMatch(rule.urlConstraints, request.arguments);
2759
+ if (rule.effect === "DENY") {
2760
+ if (satisfied) return false;
2761
+ } else {
2762
+ if (!satisfied) return false;
2763
+ }
2764
+ }
2728
2765
  return true;
2729
2766
  }
2730
2767
  function toolPatternMatches(pattern, toolName) {
@@ -2815,6 +2852,16 @@ function commandConstraintsMatch(constraints, args) {
2815
2852
  if (commands.length === 0) return true;
2816
2853
  return commands.every((cmd) => isCommandAllowed(cmd, constraints));
2817
2854
  }
2855
+ function filenameConstraintsMatch(constraints, args) {
2856
+ const filenames = extractFilenames(args);
2857
+ if (filenames.length === 0) return true;
2858
+ return filenames.every((name) => isFilenameAllowed(name, constraints));
2859
+ }
2860
+ function urlConstraintsMatch(constraints, args) {
2861
+ const urls = extractUrlArguments(args);
2862
+ if (urls.length === 0) return true;
2863
+ return urls.every((url) => isUrlAllowed(url, constraints));
2864
+ }
2818
2865
  function evaluatePolicy(policySet, request) {
2819
2866
  const startTime = performance.now();
2820
2867
  const sortedRules = [...policySet.rules].sort(
@@ -3195,6 +3242,11 @@ function resolveConfig(userConfig) {
3195
3242
  "Token secret is shorter than 32 characters. Use a longer secret for production."
3196
3243
  );
3197
3244
  }
3245
+ if (config.apiUrl && config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("http://localhost") && !config.apiUrl.startsWith("http://127.0.0.1")) {
3246
+ warnings.push(
3247
+ "API URL uses plaintext HTTP. API keys will be sent unencrypted. Use HTTPS in production."
3248
+ );
3249
+ }
3198
3250
  return { config, warnings };
3199
3251
  }
3200
3252
  async function interceptToolCall(params, upstreamCall, options) {
@@ -3700,6 +3752,12 @@ var SolonGate = class {
3700
3752
  async validateLicense() {
3701
3753
  if (this.licenseValidated) return;
3702
3754
  if (this.apiKey.startsWith("sg_test_")) {
3755
+ const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : "";
3756
+ if (nodeEnv === "production") {
3757
+ throw new LicenseError(
3758
+ "Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key instead."
3759
+ );
3760
+ }
3703
3761
  this.licenseValidated = true;
3704
3762
  return;
3705
3763
  }
@@ -3866,6 +3924,361 @@ var SolonGate = class {
3866
3924
  }
3867
3925
  };
3868
3926
 
3927
+ // ../core/dist/index.js
3928
+ import { z as z2 } from "zod";
3929
+ var Permission2 = {
3930
+ READ: "READ",
3931
+ WRITE: "WRITE",
3932
+ EXECUTE: "EXECUTE"
3933
+ };
3934
+ var PermissionSchema = z2.enum(["READ", "WRITE", "EXECUTE"]);
3935
+ var NO_PERMISSIONS = Object.freeze(
3936
+ /* @__PURE__ */ new Set()
3937
+ );
3938
+ var READ_ONLY = Object.freeze(
3939
+ /* @__PURE__ */ new Set([Permission2.READ])
3940
+ );
3941
+ var PolicyRuleSchema2 = z2.object({
3942
+ id: z2.string().min(1).max(256),
3943
+ description: z2.string().max(1024),
3944
+ effect: z2.enum(["ALLOW", "DENY"]),
3945
+ priority: z2.number().int().min(0).max(1e4).default(1e3),
3946
+ toolPattern: z2.string().min(1).max(512),
3947
+ permission: z2.enum(["READ", "WRITE", "EXECUTE"]),
3948
+ minimumTrustLevel: z2.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
3949
+ argumentConstraints: z2.record(z2.unknown()).optional(),
3950
+ pathConstraints: z2.object({
3951
+ allowed: z2.array(z2.string()).optional(),
3952
+ denied: z2.array(z2.string()).optional(),
3953
+ rootDirectory: z2.string().optional(),
3954
+ allowSymlinks: z2.boolean().optional()
3955
+ }).optional(),
3956
+ commandConstraints: z2.object({
3957
+ allowed: z2.array(z2.string()).optional(),
3958
+ denied: z2.array(z2.string()).optional()
3959
+ }).optional(),
3960
+ filenameConstraints: z2.object({
3961
+ allowed: z2.array(z2.string()).optional(),
3962
+ denied: z2.array(z2.string()).optional()
3963
+ }).optional(),
3964
+ urlConstraints: z2.object({
3965
+ allowed: z2.array(z2.string()).optional(),
3966
+ denied: z2.array(z2.string()).optional()
3967
+ }).optional(),
3968
+ enabled: z2.boolean().default(true),
3969
+ createdAt: z2.string().datetime(),
3970
+ updatedAt: z2.string().datetime()
3971
+ });
3972
+ var PolicySetSchema2 = z2.object({
3973
+ id: z2.string().min(1).max(256),
3974
+ name: z2.string().min(1).max(256),
3975
+ description: z2.string().max(2048),
3976
+ version: z2.number().int().min(0),
3977
+ rules: z2.array(PolicyRuleSchema2),
3978
+ createdAt: z2.string().datetime(),
3979
+ updatedAt: z2.string().datetime()
3980
+ });
3981
+ var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
3982
+ var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
3983
+ pathTraversal: true,
3984
+ shellInjection: true,
3985
+ wildcardAbuse: true,
3986
+ lengthLimit: 4096,
3987
+ entropyLimit: true,
3988
+ ssrf: true,
3989
+ sqlInjection: true
3990
+ });
3991
+ var PATH_TRAVERSAL_PATTERNS = [
3992
+ /\.\.\//,
3993
+ // ../
3994
+ /\.\.\\/,
3995
+ // ..\
3996
+ /%2e%2e/i,
3997
+ // URL-encoded ..
3998
+ /%2e\./i,
3999
+ // partial URL-encoded
4000
+ /\.%2e/i,
4001
+ // partial URL-encoded
4002
+ /%252e%252e/i,
4003
+ // double URL-encoded
4004
+ /\.\.\0/
4005
+ // null byte variant
4006
+ ];
4007
+ var SENSITIVE_PATHS = [
4008
+ /\/etc\/passwd/i,
4009
+ /\/etc\/shadow/i,
4010
+ /\/proc\//i,
4011
+ /\/dev\//i,
4012
+ /c:\\windows\\system32/i,
4013
+ /c:\\windows\\syswow64/i,
4014
+ /\/root\//i,
4015
+ /~\//,
4016
+ /\.env(\.|$)/i,
4017
+ // .env, .env.local, .env.production
4018
+ /\.aws\/credentials/i,
4019
+ // AWS credentials
4020
+ /\.ssh\/id_/i,
4021
+ // SSH keys
4022
+ /\.kube\/config/i,
4023
+ // Kubernetes config
4024
+ /wp-config\.php/i,
4025
+ // WordPress config
4026
+ /\.git\/config/i,
4027
+ // Git config
4028
+ /\.npmrc/i,
4029
+ // npm credentials
4030
+ /\.pypirc/i
4031
+ // PyPI credentials
4032
+ ];
4033
+ function detectPathTraversal(value) {
4034
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
4035
+ if (pattern.test(value)) return true;
4036
+ }
4037
+ for (const pattern of SENSITIVE_PATHS) {
4038
+ if (pattern.test(value)) return true;
4039
+ }
4040
+ return false;
4041
+ }
4042
+ var SHELL_INJECTION_PATTERNS = [
4043
+ /[;|&`]/,
4044
+ // Command separators and backtick execution
4045
+ /\$\(/,
4046
+ // Command substitution $(...)
4047
+ /\$\{/,
4048
+ // Variable expansion ${...}
4049
+ />\s*/,
4050
+ // Output redirect
4051
+ /<\s*/,
4052
+ // Input redirect
4053
+ /&&/,
4054
+ // AND chaining
4055
+ /\|\|/,
4056
+ // OR chaining
4057
+ /\beval\b/i,
4058
+ // eval command
4059
+ /\bexec\b/i,
4060
+ // exec command
4061
+ /\bsystem\b/i,
4062
+ // system call
4063
+ /%0a/i,
4064
+ // URL-encoded newline
4065
+ /%0d/i,
4066
+ // URL-encoded carriage return
4067
+ /%09/i,
4068
+ // URL-encoded tab
4069
+ /\r\n/,
4070
+ // CRLF injection
4071
+ /\n/
4072
+ // Newline (command separator on Unix)
4073
+ ];
4074
+ function detectShellInjection(value) {
4075
+ for (const pattern of SHELL_INJECTION_PATTERNS) {
4076
+ if (pattern.test(value)) return true;
4077
+ }
4078
+ return false;
4079
+ }
4080
+ var MAX_WILDCARDS_PER_VALUE = 3;
4081
+ function detectWildcardAbuse(value) {
4082
+ if (value.includes("**")) return true;
4083
+ const wildcardCount = (value.match(/\*/g) || []).length;
4084
+ if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
4085
+ return false;
4086
+ }
4087
+ var SSRF_PATTERNS = [
4088
+ /^https?:\/\/localhost\b/i,
4089
+ /^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
4090
+ /^https?:\/\/0\.0\.0\.0/,
4091
+ /^https?:\/\/\[::1\]/,
4092
+ // IPv6 loopback
4093
+ /^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
4094
+ // 10.x.x.x
4095
+ /^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
4096
+ // 172.16-31.x.x
4097
+ /^https?:\/\/192\.168\./,
4098
+ // 192.168.x.x
4099
+ /^https?:\/\/169\.254\./,
4100
+ // Link-local / AWS metadata
4101
+ /metadata\.google\.internal/i,
4102
+ // GCP metadata
4103
+ /^https?:\/\/metadata\b/i,
4104
+ // Generic metadata endpoint
4105
+ // IPv6 bypass patterns
4106
+ /^https?:\/\/\[fe80:/i,
4107
+ // IPv6 link-local
4108
+ /^https?:\/\/\[fc00:/i,
4109
+ // IPv6 unique local
4110
+ /^https?:\/\/\[fd[0-9a-f]{2}:/i,
4111
+ // IPv6 unique local (fd00::/8)
4112
+ /^https?:\/\/\[::ffff:127\./i,
4113
+ // IPv4-mapped IPv6 loopback
4114
+ /^https?:\/\/\[::ffff:10\./i,
4115
+ // IPv4-mapped IPv6 private
4116
+ /^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
4117
+ // IPv4-mapped IPv6 private
4118
+ /^https?:\/\/\[::ffff:192\.168\./i,
4119
+ // IPv4-mapped IPv6 private
4120
+ /^https?:\/\/\[::ffff:169\.254\./i,
4121
+ // IPv4-mapped IPv6 link-local
4122
+ // Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
4123
+ /^https?:\/\/0x[0-9a-f]+\b/i,
4124
+ // Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
4125
+ /^https?:\/\/0[0-7]{1,3}\./
4126
+ ];
4127
+ function detectDecimalIP(value) {
4128
+ const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
4129
+ if (!match || !match[1]) return false;
4130
+ const decimal = parseInt(match[1], 10);
4131
+ if (isNaN(decimal) || decimal > 4294967295) return false;
4132
+ return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
4133
+ decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
4134
+ decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
4135
+ decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
4136
+ decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
4137
+ decimal === 0;
4138
+ }
4139
+ function detectSSRF(value) {
4140
+ for (const pattern of SSRF_PATTERNS) {
4141
+ if (pattern.test(value)) return true;
4142
+ }
4143
+ if (detectDecimalIP(value)) return true;
4144
+ return false;
4145
+ }
4146
+ var SQL_INJECTION_PATTERNS = [
4147
+ /'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
4148
+ // ' OR '1'='1 — bounded to prevent ReDoS
4149
+ /'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
4150
+ // '; DROP TABLE
4151
+ /UNION\s+(ALL\s+)?SELECT/i,
4152
+ // UNION SELECT
4153
+ /--\s*$/m,
4154
+ // SQL comment at end of line
4155
+ /\/\*.{0,500}?\*\//,
4156
+ // SQL block comment — bounded + non-greedy
4157
+ /\bSLEEP\s*\(/i,
4158
+ // Time-based injection
4159
+ /\bBENCHMARK\s*\(/i,
4160
+ // MySQL benchmark
4161
+ /\bWAITFOR\s+DELAY/i,
4162
+ // MSSQL delay
4163
+ /\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
4164
+ // File operations
4165
+ ];
4166
+ function detectSQLInjection(value) {
4167
+ for (const pattern of SQL_INJECTION_PATTERNS) {
4168
+ if (pattern.test(value)) return true;
4169
+ }
4170
+ return false;
4171
+ }
4172
+ function checkLengthLimits(value, maxLength = 4096) {
4173
+ return value.length <= maxLength;
4174
+ }
4175
+ var ENTROPY_THRESHOLD = 4.5;
4176
+ var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
4177
+ function checkEntropyLimits(value) {
4178
+ if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
4179
+ const entropy = calculateShannonEntropy(value);
4180
+ return entropy <= ENTROPY_THRESHOLD;
4181
+ }
4182
+ function calculateShannonEntropy(str) {
4183
+ const freq = /* @__PURE__ */ new Map();
4184
+ for (const char of str) {
4185
+ freq.set(char, (freq.get(char) ?? 0) + 1);
4186
+ }
4187
+ let entropy = 0;
4188
+ const len = str.length;
4189
+ for (const count of freq.values()) {
4190
+ const p = count / len;
4191
+ if (p > 0) {
4192
+ entropy -= p * Math.log2(p);
4193
+ }
4194
+ }
4195
+ return entropy;
4196
+ }
4197
+ function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
4198
+ const threats = [];
4199
+ if (typeof value !== "string") {
4200
+ if (typeof value === "object" && value !== null) {
4201
+ return sanitizeObject(field, value, config);
4202
+ }
4203
+ return { safe: true, threats: [] };
4204
+ }
4205
+ if (config.pathTraversal && detectPathTraversal(value)) {
4206
+ threats.push({
4207
+ type: "PATH_TRAVERSAL",
4208
+ field,
4209
+ value: truncate(value, 100),
4210
+ description: "Path traversal pattern detected"
4211
+ });
4212
+ }
4213
+ if (config.shellInjection && detectShellInjection(value)) {
4214
+ threats.push({
4215
+ type: "SHELL_INJECTION",
4216
+ field,
4217
+ value: truncate(value, 100),
4218
+ description: "Shell injection pattern detected"
4219
+ });
4220
+ }
4221
+ if (config.wildcardAbuse && detectWildcardAbuse(value)) {
4222
+ threats.push({
4223
+ type: "WILDCARD_ABUSE",
4224
+ field,
4225
+ value: truncate(value, 100),
4226
+ description: "Wildcard abuse pattern detected"
4227
+ });
4228
+ }
4229
+ if (!checkLengthLimits(value, config.lengthLimit)) {
4230
+ threats.push({
4231
+ type: "LENGTH_EXCEEDED",
4232
+ field,
4233
+ value: `[${value.length} chars]`,
4234
+ description: `Value exceeds maximum length of ${config.lengthLimit}`
4235
+ });
4236
+ }
4237
+ if (config.entropyLimit && !checkEntropyLimits(value)) {
4238
+ threats.push({
4239
+ type: "HIGH_ENTROPY",
4240
+ field,
4241
+ value: truncate(value, 100),
4242
+ description: "High entropy string detected - possible encoded payload"
4243
+ });
4244
+ }
4245
+ if (config.ssrf && detectSSRF(value)) {
4246
+ threats.push({
4247
+ type: "SSRF",
4248
+ field,
4249
+ value: truncate(value, 100),
4250
+ description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
4251
+ });
4252
+ }
4253
+ if (config.sqlInjection && detectSQLInjection(value)) {
4254
+ threats.push({
4255
+ type: "SQL_INJECTION",
4256
+ field,
4257
+ value: truncate(value, 100),
4258
+ description: "SQL injection pattern detected"
4259
+ });
4260
+ }
4261
+ return { safe: threats.length === 0, threats };
4262
+ }
4263
+ function sanitizeObject(basePath, obj, config) {
4264
+ const threats = [];
4265
+ if (Array.isArray(obj)) {
4266
+ for (let i = 0; i < obj.length; i++) {
4267
+ const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
4268
+ threats.push(...result.threats);
4269
+ }
4270
+ } else {
4271
+ for (const [key, val] of Object.entries(obj)) {
4272
+ const result = sanitizeInput(`${basePath}.${key}`, val, config);
4273
+ threats.push(...result.threats);
4274
+ }
4275
+ }
4276
+ return { safe: threats.length === 0, threats };
4277
+ }
4278
+ function truncate(str, maxLen) {
4279
+ return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
4280
+ }
4281
+
3869
4282
  // src/proxy.ts
3870
4283
  init_config();
3871
4284
 
@@ -4131,7 +4544,12 @@ var SolonGateProxy = class {
4131
4544
  const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
4132
4545
  if (this.config.apiKey) {
4133
4546
  if (this.config.apiKey.startsWith("sg_test_")) {
4134
- log2("Using test API key \u2014 skipping online validation.");
4547
+ const nodeEnv = process.env.NODE_ENV ?? "";
4548
+ if (nodeEnv === "production") {
4549
+ log2("ERROR: Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key.");
4550
+ process.exit(1);
4551
+ }
4552
+ log2("Using test API key \u2014 skipping online validation (non-production mode).");
4135
4553
  } else {
4136
4554
  log2(`Validating license with ${apiUrl}...`);
4137
4555
  try {
@@ -4332,7 +4750,26 @@ var SolonGateProxy = class {
4332
4750
  });
4333
4751
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4334
4752
  if (!this.client) throw new Error("Upstream client disconnected");
4335
- return await this.client.readResource({ uri: request.params.uri });
4753
+ const uri = request.params.uri;
4754
+ const uriCheck = sanitizeInput("resource.uri", uri);
4755
+ if (!uriCheck.safe) {
4756
+ const threats = uriCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
4757
+ log2(`DENY resource read: ${uri} \u2014 ${threats}`);
4758
+ throw new Error(`Resource URI blocked by security policy: ${threats}`);
4759
+ }
4760
+ if (/^file:\/\//i.test(uri)) {
4761
+ const path = uri.replace(/^file:\/\/\/?/i, "/");
4762
+ if (detectPathTraversal(path)) {
4763
+ log2(`DENY resource read: ${uri} \u2014 path traversal in file URI`);
4764
+ throw new Error("Resource URI blocked: path traversal detected");
4765
+ }
4766
+ }
4767
+ if (/^https?:\/\//i.test(uri) && detectSSRF(uri)) {
4768
+ log2(`DENY resource read: ${uri} \u2014 SSRF pattern`);
4769
+ throw new Error("Resource URI blocked: internal/metadata URL not allowed");
4770
+ }
4771
+ log2(`Resource read: ${uri}`);
4772
+ return await this.client.readResource({ uri });
4336
4773
  });
4337
4774
  this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
4338
4775
  if (!this.client) return { resourceTemplates: [] };
@@ -4352,9 +4789,19 @@ var SolonGateProxy = class {
4352
4789
  });
4353
4790
  this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
4354
4791
  if (!this.client) throw new Error("Upstream client disconnected");
4792
+ const args = request.params.arguments;
4793
+ if (args && typeof args === "object") {
4794
+ const argsCheck = sanitizeInput("prompt.arguments", args);
4795
+ if (!argsCheck.safe) {
4796
+ const threats = argsCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
4797
+ log2(`DENY prompt get: ${request.params.name} \u2014 ${threats}`);
4798
+ throw new Error(`Prompt arguments blocked by security policy: ${threats}`);
4799
+ }
4800
+ }
4801
+ log2(`Prompt get: ${request.params.name}`);
4355
4802
  return await this.client.getPrompt({
4356
4803
  name: request.params.name,
4357
- arguments: request.params.arguments
4804
+ arguments: args
4358
4805
  });
4359
4806
  });
4360
4807
  }
package/dist/init.js CHANGED
@@ -67,18 +67,14 @@ function isAlreadyProtected(server) {
67
67
  function wrapServer(server, policy) {
68
68
  const env = { ...server.env ?? {} };
69
69
  env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
70
+ const proxyArgs = ["-y", "@solongate/proxy"];
71
+ if (policy) {
72
+ proxyArgs.push("--policy", policy);
73
+ }
74
+ proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
70
75
  return {
71
76
  command: "npx",
72
- args: [
73
- "-y",
74
- "@solongate/proxy",
75
- "--policy",
76
- policy,
77
- "--verbose",
78
- "--",
79
- server.command,
80
- ...server.args ?? []
81
- ],
77
+ args: proxyArgs,
82
78
  env
83
79
  };
84
80
  }
@@ -127,8 +123,7 @@ USAGE
127
123
 
128
124
  OPTIONS
129
125
  --config <path> Path to MCP config file (default: auto-detect)
130
- --policy <file> Custom policy JSON file (default: restricted)
131
- Auto-detects policy.json in current directory
126
+ --policy <file> Custom policy JSON file (auto-detects policy.json)
132
127
  --api-key <key> SolonGate API key (sg_live_... or sg_test_...)
133
128
  --all Protect all servers without prompting
134
129
  -h, --help Show this help message
@@ -685,7 +680,7 @@ async function main() {
685
680
  console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
686
681
  process.exit(1);
687
682
  }
688
- let policyValue = "restricted";
683
+ let policyValue;
689
684
  if (options.policy) {
690
685
  const policyPath = resolve(options.policy);
691
686
  if (existsSync(policyPath)) {
@@ -701,7 +696,7 @@ async function main() {
701
696
  policyValue = "./policy.json";
702
697
  console.log(` Policy: ${defaultPolicy} (auto-detected)`);
703
698
  } else {
704
- console.log(` Policy: restricted (default)`);
699
+ console.log(` Policy: cloud-managed (fetched via API key)`);
705
700
  }
706
701
  }
707
702
  await sleep(300);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solongate/proxy",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "MCP security proxy — protect any MCP server with customizable policies, path/command constraints, rate limiting, and audit logging. Zero code changes required.",
5
5
  "type": "module",
6
6
  "bin": {