@solongate/proxy 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -63,21 +63,48 @@ async function sendAuditLog(apiKey, apiUrl, entry) {
63
63
  `);
64
64
  }
65
65
  }
66
+ function ensureCatchAllAllow(policy) {
67
+ const hasCatchAllAllow = policy.rules.some(
68
+ (r) => r.effect === "ALLOW" && r.toolPattern === "*" && r.enabled !== false
69
+ );
70
+ if (hasCatchAllAllow) return policy;
71
+ const now = (/* @__PURE__ */ new Date()).toISOString();
72
+ return {
73
+ ...policy,
74
+ rules: [
75
+ ...policy.rules,
76
+ {
77
+ id: "_solongate-catch-all-allow",
78
+ description: "Auto-added: allow everything not explicitly denied",
79
+ effect: "ALLOW",
80
+ priority: 9999,
81
+ toolPattern: "*",
82
+ minimumTrustLevel: "UNTRUSTED",
83
+ enabled: true,
84
+ createdAt: now,
85
+ updatedAt: now
86
+ }
87
+ ]
88
+ };
89
+ }
66
90
  function loadPolicy(source) {
67
- if (typeof source === "object") return source;
68
- if (PRESETS[source]) return PRESETS[source];
69
- const filePath = resolve(source);
70
- if (existsSync(filePath)) {
71
- const content = readFileSync(filePath, "utf-8");
72
- return JSON.parse(content);
91
+ let policy;
92
+ if (typeof source === "object") {
93
+ policy = source;
94
+ } else {
95
+ const filePath = resolve(source);
96
+ if (existsSync(filePath)) {
97
+ const content = readFileSync(filePath, "utf-8");
98
+ policy = JSON.parse(content);
99
+ } else {
100
+ return DEFAULT_POLICY;
101
+ }
73
102
  }
74
- throw new Error(
75
- `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
76
- );
103
+ return ensureCatchAllAllow(policy);
77
104
  }
78
105
  function parseArgs(argv) {
79
106
  const args = argv.slice(2);
80
- let policySource = "restricted";
107
+ let policySource;
81
108
  let name = "solongate-proxy";
82
109
  let verbose = false;
83
110
  let rateLimitPerTool;
@@ -159,7 +186,7 @@ function parseArgs(argv) {
159
186
  "Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
160
187
  );
161
188
  }
162
- const resolvedPolicyPath = resolvePolicyPath(policySource);
189
+ const resolvedPolicyPath = policySource ? resolvePolicyPath(policySource) : null;
163
190
  if (configFile) {
164
191
  const filePath = resolve(configFile);
165
192
  const content = readFileSync(filePath, "utf-8");
@@ -167,7 +194,7 @@ function parseArgs(argv) {
167
194
  if (!fileConfig.upstream) {
168
195
  throw new Error('Config file must include "upstream" with at least "command" or "url"');
169
196
  }
170
- const cfgPolicySource = fileConfig.policy ?? policySource;
197
+ const cfgPolicySource = fileConfig.policy ?? policySource ?? "policy.json";
171
198
  return {
172
199
  upstream: fileConfig.upstream,
173
200
  policy: loadPolicy(cfgPolicySource),
@@ -191,7 +218,7 @@ function parseArgs(argv) {
191
218
  // not used for URL-based transports
192
219
  url: upstreamUrl
193
220
  },
194
- policy: loadPolicy(policySource),
221
+ policy: loadPolicy(policySource ?? "policy.json"),
195
222
  name,
196
223
  verbose,
197
224
  rateLimitPerTool,
@@ -205,7 +232,7 @@ function parseArgs(argv) {
205
232
  }
206
233
  if (upstreamArgs.length === 0) {
207
234
  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"
235
+ "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
236
  );
210
237
  }
211
238
  const [command, ...commandArgs] = upstreamArgs;
@@ -216,7 +243,7 @@ function parseArgs(argv) {
216
243
  args: commandArgs,
217
244
  env: { ...process.env }
218
245
  },
219
- policy: loadPolicy(policySource),
246
+ policy: loadPolicy(policySource ?? "policy.json"),
220
247
  name,
221
248
  verbose,
222
249
  rateLimitPerTool,
@@ -229,303 +256,22 @@ function parseArgs(argv) {
229
256
  };
230
257
  }
231
258
  function resolvePolicyPath(source) {
232
- if (PRESETS[source]) return null;
233
259
  const filePath = resolve(source);
234
260
  if (existsSync(filePath)) return filePath;
235
261
  return null;
236
262
  }
237
- var PRESETS;
263
+ var DEFAULT_POLICY;
238
264
  var init_config = __esm({
239
265
  "src/config.ts"() {
240
266
  "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
- }
267
+ DEFAULT_POLICY = {
268
+ id: "default",
269
+ name: "Default (Deny All)",
270
+ description: "Default-deny policy. Create rules in the dashboard or push a policy.json.",
271
+ version: 1,
272
+ rules: [],
273
+ createdAt: "",
274
+ updatedAt: ""
529
275
  };
530
276
  }
531
277
  });
@@ -591,18 +337,14 @@ function isAlreadyProtected(server) {
591
337
  function wrapServer(server, policy) {
592
338
  const env = { ...server.env ?? {} };
593
339
  env.SOLONGATE_API_KEY = "${SOLONGATE_API_KEY}";
340
+ const proxyArgs = ["-y", "@solongate/proxy"];
341
+ if (policy) {
342
+ proxyArgs.push("--policy", policy);
343
+ }
344
+ proxyArgs.push("--verbose", "--", server.command, ...server.args ?? []);
594
345
  return {
595
346
  command: "npx",
596
- args: [
597
- "-y",
598
- "@solongate/proxy",
599
- "--policy",
600
- policy,
601
- "--verbose",
602
- "--",
603
- server.command,
604
- ...server.args ?? []
605
- ],
347
+ args: proxyArgs,
606
348
  env
607
349
  };
608
350
  }
@@ -651,8 +393,7 @@ USAGE
651
393
 
652
394
  OPTIONS
653
395
  --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
396
+ --policy <file> Custom policy JSON file (auto-detects policy.json)
656
397
  --api-key <key> SolonGate API key (sg_live_... or sg_test_...)
657
398
  --all Protect all servers without prompting
658
399
  -h, --help Show this help message
@@ -870,7 +611,7 @@ async function main() {
870
611
  console.log(" Invalid API key format. Must start with sg_live_ or sg_test_");
871
612
  process.exit(1);
872
613
  }
873
- let policyValue = "restricted";
614
+ let policyValue;
874
615
  if (options.policy) {
875
616
  const policyPath = resolve2(options.policy);
876
617
  if (existsSync3(policyPath)) {
@@ -886,7 +627,7 @@ async function main() {
886
627
  policyValue = "./policy.json";
887
628
  console.log(` Policy: ${defaultPolicy} (auto-detected)`);
888
629
  } else {
889
- console.log(` Policy: restricted (default)`);
630
+ console.log(` Policy: cloud-managed (fetched via API key)`);
890
631
  }
891
632
  }
892
633
  await sleep(300);
@@ -1726,7 +1467,7 @@ function parseCreateArgs(argv) {
1726
1467
  const args = argv.slice(2);
1727
1468
  const opts = {
1728
1469
  name: "",
1729
- policy: "restricted",
1470
+ policy: "",
1730
1471
  noInstall: false
1731
1472
  };
1732
1473
  for (let i = 0; i < args.length; i++) {
@@ -1769,13 +1510,13 @@ USAGE
1769
1510
  npx @solongate/proxy create <name> [options]
1770
1511
 
1771
1512
  OPTIONS
1772
- --policy <preset> Policy preset (default: restricted)
1513
+ --policy <file> Policy JSON file (default: cloud-managed)
1773
1514
  --no-install Skip dependency installation
1774
1515
  -h, --help Show this help message
1775
1516
 
1776
1517
  EXAMPLES
1777
1518
  npx @solongate/proxy create my-server
1778
- npx @solongate/proxy create db-tools --policy read-only
1519
+ npx @solongate/proxy create db-tools --policy ./policy.json
1779
1520
  `);
1780
1521
  }
1781
1522
  function createProject(dir, name, _policy) {
@@ -2440,6 +2181,14 @@ var PolicyRuleSchema = z.object({
2440
2181
  allowed: z.array(z.string()).optional(),
2441
2182
  denied: z.array(z.string()).optional()
2442
2183
  }).optional(),
2184
+ filenameConstraints: z.object({
2185
+ allowed: z.array(z.string()).optional(),
2186
+ denied: z.array(z.string()).optional()
2187
+ }).optional(),
2188
+ urlConstraints: z.object({
2189
+ allowed: z.array(z.string()).optional(),
2190
+ denied: z.array(z.string()).optional()
2191
+ }).optional(),
2443
2192
  enabled: z.boolean().default(true),
2444
2193
  createdAt: z.string().datetime(),
2445
2194
  updatedAt: z.string().datetime()
@@ -2625,11 +2374,48 @@ function matchSegmentGlob(segment, pattern) {
2625
2374
  }
2626
2375
  return segment === pattern;
2627
2376
  }
2377
+ var PATH_FIELDS = /* @__PURE__ */ new Set([
2378
+ "path",
2379
+ "file",
2380
+ "file_path",
2381
+ "filepath",
2382
+ "filename",
2383
+ "directory",
2384
+ "dir",
2385
+ "folder",
2386
+ "source",
2387
+ "destination",
2388
+ "dest",
2389
+ "target",
2390
+ "input",
2391
+ "output",
2392
+ "cwd",
2393
+ "root",
2394
+ "notebook_path"
2395
+ ]);
2628
2396
  function extractPathArguments(args) {
2629
2397
  const paths = [];
2630
- for (const value of Object.values(args)) {
2631
- if (typeof value === "string" && (value.includes("/") || value.includes("\\"))) {
2632
- paths.push(value);
2398
+ const seen = /* @__PURE__ */ new Set();
2399
+ function addPath(value) {
2400
+ const trimmed = value.trim();
2401
+ if (trimmed && !seen.has(trimmed)) {
2402
+ seen.add(trimmed);
2403
+ paths.push(trimmed);
2404
+ }
2405
+ }
2406
+ for (const [key, value] of Object.entries(args)) {
2407
+ if (typeof value !== "string") continue;
2408
+ if (PATH_FIELDS.has(key.toLowerCase())) {
2409
+ addPath(value);
2410
+ continue;
2411
+ }
2412
+ if (value.includes("/") || value.includes("\\")) {
2413
+ addPath(value);
2414
+ continue;
2415
+ }
2416
+ if (value.startsWith(".")) {
2417
+ addPath(value);
2418
+ continue;
2633
2419
  }
2634
2420
  }
2635
2421
  return paths;
@@ -2643,15 +2429,62 @@ var COMMAND_FIELDS = /* @__PURE__ */ new Set([
2643
2429
  "shell",
2644
2430
  "exec",
2645
2431
  "sql",
2646
- "expression"
2432
+ "expression",
2433
+ "function"
2647
2434
  ]);
2435
+ var COMMAND_HEURISTICS = [
2436
+ /^(sh|bash|cmd|powershell|zsh|fish)\s+-c\s+/i,
2437
+ // shell -c "..."
2438
+ /^(sudo|doas)\s+/i,
2439
+ // privilege escalation
2440
+ /^\w+\s+&&\s+/,
2441
+ // cmd1 && cmd2
2442
+ /^\w+\s*\|\s*\w+/,
2443
+ // cmd1 | cmd2
2444
+ /^\w+\s*;\s*\w+/,
2445
+ // cmd1; cmd2
2446
+ /^(curl|wget|nc|ncat)\s+/i,
2447
+ // network commands
2448
+ /^(rm|del|rmdir)\s+/i,
2449
+ // destructive commands
2450
+ /^(cat|type|more|less)\s+.*[/\\]/i
2451
+ // file read commands with paths
2452
+ ];
2648
2453
  function extractCommandArguments(args) {
2649
2454
  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);
2455
+ const seen = /* @__PURE__ */ new Set();
2456
+ function addCommand(value) {
2457
+ const trimmed = value.trim();
2458
+ if (trimmed && !seen.has(trimmed)) {
2459
+ seen.add(trimmed);
2460
+ commands.push(trimmed);
2461
+ }
2462
+ }
2463
+ function scanValue(key, value) {
2464
+ if (typeof value === "string") {
2465
+ if (COMMAND_FIELDS.has(key.toLowerCase())) {
2466
+ addCommand(value);
2467
+ return;
2468
+ }
2469
+ for (const pattern of COMMAND_HEURISTICS) {
2470
+ if (pattern.test(value)) {
2471
+ addCommand(value);
2472
+ return;
2473
+ }
2474
+ }
2654
2475
  }
2476
+ if (Array.isArray(value)) {
2477
+ for (const item of value) {
2478
+ scanValue(key, item);
2479
+ }
2480
+ } else if (typeof value === "object" && value !== null) {
2481
+ for (const [k, v] of Object.entries(value)) {
2482
+ scanValue(k, v);
2483
+ }
2484
+ }
2485
+ }
2486
+ for (const [key, value] of Object.entries(args)) {
2487
+ scanValue(key, value);
2655
2488
  }
2656
2489
  return commands;
2657
2490
  }
@@ -2697,6 +2530,224 @@ function isCommandAllowed(command, constraints) {
2697
2530
  }
2698
2531
  return true;
2699
2532
  }
2533
+ function extractFilenames(args) {
2534
+ const filenames = [];
2535
+ const seen = /* @__PURE__ */ new Set();
2536
+ function addFilename(name) {
2537
+ const trimmed = name.trim();
2538
+ if (trimmed && !seen.has(trimmed)) {
2539
+ seen.add(trimmed);
2540
+ filenames.push(trimmed);
2541
+ }
2542
+ }
2543
+ function scanValue(value) {
2544
+ if (typeof value === "string") {
2545
+ const trimmed = value.trim();
2546
+ if (!trimmed) return;
2547
+ if (/^https?:\/\//i.test(trimmed)) return;
2548
+ if (trimmed.includes("/") || trimmed.includes("\\")) {
2549
+ const normalized = trimmed.replace(/\\/g, "/");
2550
+ const parts = normalized.split("/");
2551
+ const basename = parts[parts.length - 1];
2552
+ if (basename && basename.length > 0) {
2553
+ addFilename(basename);
2554
+ }
2555
+ return;
2556
+ }
2557
+ if (trimmed.includes(" ")) {
2558
+ for (const token of trimmed.split(/\s+/)) {
2559
+ if (token.includes("/") || token.includes("\\")) {
2560
+ const parts = token.replace(/\\/g, "/").split("/");
2561
+ const basename = parts[parts.length - 1];
2562
+ if (basename && looksLikeFilename(basename)) addFilename(basename);
2563
+ } else if (looksLikeFilename(token)) {
2564
+ addFilename(token);
2565
+ }
2566
+ }
2567
+ return;
2568
+ }
2569
+ if (looksLikeFilename(trimmed)) {
2570
+ addFilename(trimmed);
2571
+ }
2572
+ return;
2573
+ }
2574
+ if (Array.isArray(value)) {
2575
+ for (const item of value) {
2576
+ scanValue(item);
2577
+ }
2578
+ } else if (typeof value === "object" && value !== null) {
2579
+ for (const v of Object.values(value)) {
2580
+ scanValue(v);
2581
+ }
2582
+ }
2583
+ }
2584
+ for (const value of Object.values(args)) {
2585
+ scanValue(value);
2586
+ }
2587
+ return filenames;
2588
+ }
2589
+ function looksLikeFilename(s) {
2590
+ if (s.startsWith(".")) return true;
2591
+ if (/\.\w+$/.test(s)) return true;
2592
+ const knownFiles = /* @__PURE__ */ new Set([
2593
+ "id_rsa",
2594
+ "id_dsa",
2595
+ "id_ecdsa",
2596
+ "id_ed25519",
2597
+ "authorized_keys",
2598
+ "known_hosts",
2599
+ "makefile",
2600
+ "dockerfile",
2601
+ "vagrantfile",
2602
+ "gemfile",
2603
+ "rakefile",
2604
+ "procfile"
2605
+ ]);
2606
+ if (knownFiles.has(s.toLowerCase())) return true;
2607
+ return false;
2608
+ }
2609
+ function matchFilenamePattern(filename, pattern) {
2610
+ if (pattern === "*") return true;
2611
+ const normalizedFilename = filename.toLowerCase();
2612
+ const normalizedPattern = pattern.toLowerCase();
2613
+ if (normalizedFilename === normalizedPattern) return true;
2614
+ const startsWithStar = normalizedPattern.startsWith("*");
2615
+ const endsWithStar = normalizedPattern.endsWith("*");
2616
+ if (startsWithStar && endsWithStar) {
2617
+ const infix = normalizedPattern.slice(1, -1);
2618
+ return infix.length > 0 && normalizedFilename.includes(infix);
2619
+ }
2620
+ if (startsWithStar) {
2621
+ const suffix = normalizedPattern.slice(1);
2622
+ return normalizedFilename.endsWith(suffix);
2623
+ }
2624
+ if (endsWithStar) {
2625
+ const prefix = normalizedPattern.slice(0, -1);
2626
+ return normalizedFilename.startsWith(prefix);
2627
+ }
2628
+ const starIdx = normalizedPattern.indexOf("*");
2629
+ if (starIdx !== -1) {
2630
+ const prefix = normalizedPattern.slice(0, starIdx);
2631
+ const suffix = normalizedPattern.slice(starIdx + 1);
2632
+ return normalizedFilename.startsWith(prefix) && normalizedFilename.endsWith(suffix) && normalizedFilename.length >= prefix.length + suffix.length;
2633
+ }
2634
+ return false;
2635
+ }
2636
+ function isFilenameAllowed(filename, constraints) {
2637
+ if (constraints.denied && constraints.denied.length > 0) {
2638
+ for (const pattern of constraints.denied) {
2639
+ if (matchFilenamePattern(filename, pattern)) {
2640
+ return false;
2641
+ }
2642
+ }
2643
+ }
2644
+ if (constraints.allowed && constraints.allowed.length > 0) {
2645
+ let matchesAllowed = false;
2646
+ for (const pattern of constraints.allowed) {
2647
+ if (matchFilenamePattern(filename, pattern)) {
2648
+ matchesAllowed = true;
2649
+ break;
2650
+ }
2651
+ }
2652
+ if (!matchesAllowed) return false;
2653
+ }
2654
+ return true;
2655
+ }
2656
+ var URL_FIELDS = /* @__PURE__ */ new Set([
2657
+ "url",
2658
+ "href",
2659
+ "uri",
2660
+ "endpoint",
2661
+ "link",
2662
+ "src",
2663
+ "source",
2664
+ "target",
2665
+ "redirect",
2666
+ "callback",
2667
+ "webhook"
2668
+ ]);
2669
+ function extractUrlArguments(args) {
2670
+ const urls = [];
2671
+ const seen = /* @__PURE__ */ new Set();
2672
+ function addUrl(value) {
2673
+ const trimmed = value.trim();
2674
+ if (trimmed && !seen.has(trimmed)) {
2675
+ seen.add(trimmed);
2676
+ urls.push(trimmed);
2677
+ }
2678
+ }
2679
+ function scanValue(key, value) {
2680
+ if (typeof value === "string") {
2681
+ const lower = key.toLowerCase();
2682
+ if (URL_FIELDS.has(lower)) {
2683
+ addUrl(value);
2684
+ return;
2685
+ }
2686
+ if (/https?:\/\//i.test(value)) {
2687
+ addUrl(value);
2688
+ return;
2689
+ }
2690
+ if (/^[a-zA-Z0-9]([a-zA-Z0-9-]*\.)+[a-zA-Z]{2,}(\/.*)?$/.test(value)) {
2691
+ addUrl(value);
2692
+ return;
2693
+ }
2694
+ }
2695
+ if (Array.isArray(value)) {
2696
+ for (const item of value) {
2697
+ scanValue(key, item);
2698
+ }
2699
+ } else if (typeof value === "object" && value !== null) {
2700
+ for (const [k, v] of Object.entries(value)) {
2701
+ scanValue(k, v);
2702
+ }
2703
+ }
2704
+ }
2705
+ for (const [key, value] of Object.entries(args)) {
2706
+ scanValue(key, value);
2707
+ }
2708
+ return urls;
2709
+ }
2710
+ function matchUrlPattern(url, pattern) {
2711
+ if (pattern === "*") return true;
2712
+ const normalizedUrl = url.trim().toLowerCase();
2713
+ const normalizedPattern = pattern.trim().toLowerCase();
2714
+ if (normalizedPattern === normalizedUrl) return true;
2715
+ const startsWithStar = normalizedPattern.startsWith("*");
2716
+ const endsWithStar = normalizedPattern.endsWith("*");
2717
+ if (startsWithStar && endsWithStar) {
2718
+ const infix = normalizedPattern.slice(1, -1);
2719
+ return infix.length > 0 && normalizedUrl.includes(infix);
2720
+ }
2721
+ if (endsWithStar) {
2722
+ const prefix = normalizedPattern.slice(0, -1);
2723
+ return normalizedUrl.startsWith(prefix);
2724
+ }
2725
+ if (startsWithStar) {
2726
+ const suffix = normalizedPattern.slice(1);
2727
+ return normalizedUrl.endsWith(suffix);
2728
+ }
2729
+ return false;
2730
+ }
2731
+ function isUrlAllowed(url, constraints) {
2732
+ if (constraints.denied && constraints.denied.length > 0) {
2733
+ for (const pattern of constraints.denied) {
2734
+ if (matchUrlPattern(url, pattern)) {
2735
+ return false;
2736
+ }
2737
+ }
2738
+ }
2739
+ if (constraints.allowed && constraints.allowed.length > 0) {
2740
+ let matchesAllowed = false;
2741
+ for (const pattern of constraints.allowed) {
2742
+ if (matchUrlPattern(url, pattern)) {
2743
+ matchesAllowed = true;
2744
+ break;
2745
+ }
2746
+ }
2747
+ if (!matchesAllowed) return false;
2748
+ }
2749
+ return true;
2750
+ }
2700
2751
  function ruleMatchesRequest(rule, request) {
2701
2752
  if (!rule.enabled) return false;
2702
2753
  if (rule.permission !== request.requiredPermission) return false;
@@ -2725,6 +2776,22 @@ function ruleMatchesRequest(rule, request) {
2725
2776
  if (!satisfied) return false;
2726
2777
  }
2727
2778
  }
2779
+ if (rule.filenameConstraints) {
2780
+ const satisfied = filenameConstraintsMatch(rule.filenameConstraints, request.arguments);
2781
+ if (rule.effect === "DENY") {
2782
+ if (satisfied) return false;
2783
+ } else {
2784
+ if (!satisfied) return false;
2785
+ }
2786
+ }
2787
+ if (rule.urlConstraints) {
2788
+ const satisfied = urlConstraintsMatch(rule.urlConstraints, request.arguments);
2789
+ if (rule.effect === "DENY") {
2790
+ if (satisfied) return false;
2791
+ } else {
2792
+ if (!satisfied) return false;
2793
+ }
2794
+ }
2728
2795
  return true;
2729
2796
  }
2730
2797
  function toolPatternMatches(pattern, toolName) {
@@ -2815,6 +2882,16 @@ function commandConstraintsMatch(constraints, args) {
2815
2882
  if (commands.length === 0) return true;
2816
2883
  return commands.every((cmd) => isCommandAllowed(cmd, constraints));
2817
2884
  }
2885
+ function filenameConstraintsMatch(constraints, args) {
2886
+ const filenames = extractFilenames(args);
2887
+ if (filenames.length === 0) return true;
2888
+ return filenames.every((name) => isFilenameAllowed(name, constraints));
2889
+ }
2890
+ function urlConstraintsMatch(constraints, args) {
2891
+ const urls = extractUrlArguments(args);
2892
+ if (urls.length === 0) return true;
2893
+ return urls.every((url) => isUrlAllowed(url, constraints));
2894
+ }
2818
2895
  function evaluatePolicy(policySet, request) {
2819
2896
  const startTime = performance.now();
2820
2897
  const sortedRules = [...policySet.rules].sort(
@@ -3195,6 +3272,11 @@ function resolveConfig(userConfig) {
3195
3272
  "Token secret is shorter than 32 characters. Use a longer secret for production."
3196
3273
  );
3197
3274
  }
3275
+ if (config.apiUrl && config.apiUrl.startsWith("http://") && !config.apiUrl.startsWith("http://localhost") && !config.apiUrl.startsWith("http://127.0.0.1")) {
3276
+ warnings.push(
3277
+ "API URL uses plaintext HTTP. API keys will be sent unencrypted. Use HTTPS in production."
3278
+ );
3279
+ }
3198
3280
  return { config, warnings };
3199
3281
  }
3200
3282
  async function interceptToolCall(params, upstreamCall, options) {
@@ -3700,6 +3782,12 @@ var SolonGate = class {
3700
3782
  async validateLicense() {
3701
3783
  if (this.licenseValidated) return;
3702
3784
  if (this.apiKey.startsWith("sg_test_")) {
3785
+ const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : "";
3786
+ if (nodeEnv === "production") {
3787
+ throw new LicenseError(
3788
+ "Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key instead."
3789
+ );
3790
+ }
3703
3791
  this.licenseValidated = true;
3704
3792
  return;
3705
3793
  }
@@ -3866,6 +3954,361 @@ var SolonGate = class {
3866
3954
  }
3867
3955
  };
3868
3956
 
3957
+ // ../core/dist/index.js
3958
+ import { z as z2 } from "zod";
3959
+ var Permission2 = {
3960
+ READ: "READ",
3961
+ WRITE: "WRITE",
3962
+ EXECUTE: "EXECUTE"
3963
+ };
3964
+ var PermissionSchema = z2.enum(["READ", "WRITE", "EXECUTE"]);
3965
+ var NO_PERMISSIONS = Object.freeze(
3966
+ /* @__PURE__ */ new Set()
3967
+ );
3968
+ var READ_ONLY = Object.freeze(
3969
+ /* @__PURE__ */ new Set([Permission2.READ])
3970
+ );
3971
+ var PolicyRuleSchema2 = z2.object({
3972
+ id: z2.string().min(1).max(256),
3973
+ description: z2.string().max(1024),
3974
+ effect: z2.enum(["ALLOW", "DENY"]),
3975
+ priority: z2.number().int().min(0).max(1e4).default(1e3),
3976
+ toolPattern: z2.string().min(1).max(512),
3977
+ permission: z2.enum(["READ", "WRITE", "EXECUTE"]).optional(),
3978
+ minimumTrustLevel: z2.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
3979
+ argumentConstraints: z2.record(z2.unknown()).optional(),
3980
+ pathConstraints: z2.object({
3981
+ allowed: z2.array(z2.string()).optional(),
3982
+ denied: z2.array(z2.string()).optional(),
3983
+ rootDirectory: z2.string().optional(),
3984
+ allowSymlinks: z2.boolean().optional()
3985
+ }).optional(),
3986
+ commandConstraints: z2.object({
3987
+ allowed: z2.array(z2.string()).optional(),
3988
+ denied: z2.array(z2.string()).optional()
3989
+ }).optional(),
3990
+ filenameConstraints: z2.object({
3991
+ allowed: z2.array(z2.string()).optional(),
3992
+ denied: z2.array(z2.string()).optional()
3993
+ }).optional(),
3994
+ urlConstraints: z2.object({
3995
+ allowed: z2.array(z2.string()).optional(),
3996
+ denied: z2.array(z2.string()).optional()
3997
+ }).optional(),
3998
+ enabled: z2.boolean().default(true),
3999
+ createdAt: z2.string().datetime(),
4000
+ updatedAt: z2.string().datetime()
4001
+ });
4002
+ var PolicySetSchema2 = z2.object({
4003
+ id: z2.string().min(1).max(256),
4004
+ name: z2.string().min(1).max(256),
4005
+ description: z2.string().max(2048),
4006
+ version: z2.number().int().min(0),
4007
+ rules: z2.array(PolicyRuleSchema2),
4008
+ createdAt: z2.string().datetime(),
4009
+ updatedAt: z2.string().datetime()
4010
+ });
4011
+ var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
4012
+ var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
4013
+ pathTraversal: true,
4014
+ shellInjection: true,
4015
+ wildcardAbuse: true,
4016
+ lengthLimit: 4096,
4017
+ entropyLimit: true,
4018
+ ssrf: true,
4019
+ sqlInjection: true
4020
+ });
4021
+ var PATH_TRAVERSAL_PATTERNS = [
4022
+ /\.\.\//,
4023
+ // ../
4024
+ /\.\.\\/,
4025
+ // ..\
4026
+ /%2e%2e/i,
4027
+ // URL-encoded ..
4028
+ /%2e\./i,
4029
+ // partial URL-encoded
4030
+ /\.%2e/i,
4031
+ // partial URL-encoded
4032
+ /%252e%252e/i,
4033
+ // double URL-encoded
4034
+ /\.\.\0/
4035
+ // null byte variant
4036
+ ];
4037
+ var SENSITIVE_PATHS = [
4038
+ /\/etc\/passwd/i,
4039
+ /\/etc\/shadow/i,
4040
+ /\/proc\//i,
4041
+ /\/dev\//i,
4042
+ /c:\\windows\\system32/i,
4043
+ /c:\\windows\\syswow64/i,
4044
+ /\/root\//i,
4045
+ /~\//,
4046
+ /\.env(\.|$)/i,
4047
+ // .env, .env.local, .env.production
4048
+ /\.aws\/credentials/i,
4049
+ // AWS credentials
4050
+ /\.ssh\/id_/i,
4051
+ // SSH keys
4052
+ /\.kube\/config/i,
4053
+ // Kubernetes config
4054
+ /wp-config\.php/i,
4055
+ // WordPress config
4056
+ /\.git\/config/i,
4057
+ // Git config
4058
+ /\.npmrc/i,
4059
+ // npm credentials
4060
+ /\.pypirc/i
4061
+ // PyPI credentials
4062
+ ];
4063
+ function detectPathTraversal(value) {
4064
+ for (const pattern of PATH_TRAVERSAL_PATTERNS) {
4065
+ if (pattern.test(value)) return true;
4066
+ }
4067
+ for (const pattern of SENSITIVE_PATHS) {
4068
+ if (pattern.test(value)) return true;
4069
+ }
4070
+ return false;
4071
+ }
4072
+ var SHELL_INJECTION_PATTERNS = [
4073
+ /[;|&`]/,
4074
+ // Command separators and backtick execution
4075
+ /\$\(/,
4076
+ // Command substitution $(...)
4077
+ /\$\{/,
4078
+ // Variable expansion ${...}
4079
+ />\s*/,
4080
+ // Output redirect
4081
+ /<\s*/,
4082
+ // Input redirect
4083
+ /&&/,
4084
+ // AND chaining
4085
+ /\|\|/,
4086
+ // OR chaining
4087
+ /\beval\b/i,
4088
+ // eval command
4089
+ /\bexec\b/i,
4090
+ // exec command
4091
+ /\bsystem\b/i,
4092
+ // system call
4093
+ /%0a/i,
4094
+ // URL-encoded newline
4095
+ /%0d/i,
4096
+ // URL-encoded carriage return
4097
+ /%09/i,
4098
+ // URL-encoded tab
4099
+ /\r\n/,
4100
+ // CRLF injection
4101
+ /\n/
4102
+ // Newline (command separator on Unix)
4103
+ ];
4104
+ function detectShellInjection(value) {
4105
+ for (const pattern of SHELL_INJECTION_PATTERNS) {
4106
+ if (pattern.test(value)) return true;
4107
+ }
4108
+ return false;
4109
+ }
4110
+ var MAX_WILDCARDS_PER_VALUE = 3;
4111
+ function detectWildcardAbuse(value) {
4112
+ if (value.includes("**")) return true;
4113
+ const wildcardCount = (value.match(/\*/g) || []).length;
4114
+ if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
4115
+ return false;
4116
+ }
4117
+ var SSRF_PATTERNS = [
4118
+ /^https?:\/\/localhost\b/i,
4119
+ /^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
4120
+ /^https?:\/\/0\.0\.0\.0/,
4121
+ /^https?:\/\/\[::1\]/,
4122
+ // IPv6 loopback
4123
+ /^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
4124
+ // 10.x.x.x
4125
+ /^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
4126
+ // 172.16-31.x.x
4127
+ /^https?:\/\/192\.168\./,
4128
+ // 192.168.x.x
4129
+ /^https?:\/\/169\.254\./,
4130
+ // Link-local / AWS metadata
4131
+ /metadata\.google\.internal/i,
4132
+ // GCP metadata
4133
+ /^https?:\/\/metadata\b/i,
4134
+ // Generic metadata endpoint
4135
+ // IPv6 bypass patterns
4136
+ /^https?:\/\/\[fe80:/i,
4137
+ // IPv6 link-local
4138
+ /^https?:\/\/\[fc00:/i,
4139
+ // IPv6 unique local
4140
+ /^https?:\/\/\[fd[0-9a-f]{2}:/i,
4141
+ // IPv6 unique local (fd00::/8)
4142
+ /^https?:\/\/\[::ffff:127\./i,
4143
+ // IPv4-mapped IPv6 loopback
4144
+ /^https?:\/\/\[::ffff:10\./i,
4145
+ // IPv4-mapped IPv6 private
4146
+ /^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
4147
+ // IPv4-mapped IPv6 private
4148
+ /^https?:\/\/\[::ffff:192\.168\./i,
4149
+ // IPv4-mapped IPv6 private
4150
+ /^https?:\/\/\[::ffff:169\.254\./i,
4151
+ // IPv4-mapped IPv6 link-local
4152
+ // Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
4153
+ /^https?:\/\/0x[0-9a-f]+\b/i,
4154
+ // Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
4155
+ /^https?:\/\/0[0-7]{1,3}\./
4156
+ ];
4157
+ function detectDecimalIP(value) {
4158
+ const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
4159
+ if (!match || !match[1]) return false;
4160
+ const decimal = parseInt(match[1], 10);
4161
+ if (isNaN(decimal) || decimal > 4294967295) return false;
4162
+ return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
4163
+ decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
4164
+ decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
4165
+ decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
4166
+ decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
4167
+ decimal === 0;
4168
+ }
4169
+ function detectSSRF(value) {
4170
+ for (const pattern of SSRF_PATTERNS) {
4171
+ if (pattern.test(value)) return true;
4172
+ }
4173
+ if (detectDecimalIP(value)) return true;
4174
+ return false;
4175
+ }
4176
+ var SQL_INJECTION_PATTERNS = [
4177
+ /'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
4178
+ // ' OR '1'='1 — bounded to prevent ReDoS
4179
+ /'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
4180
+ // '; DROP TABLE
4181
+ /UNION\s+(ALL\s+)?SELECT/i,
4182
+ // UNION SELECT
4183
+ /--\s*$/m,
4184
+ // SQL comment at end of line
4185
+ /\/\*.{0,500}?\*\//,
4186
+ // SQL block comment — bounded + non-greedy
4187
+ /\bSLEEP\s*\(/i,
4188
+ // Time-based injection
4189
+ /\bBENCHMARK\s*\(/i,
4190
+ // MySQL benchmark
4191
+ /\bWAITFOR\s+DELAY/i,
4192
+ // MSSQL delay
4193
+ /\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
4194
+ // File operations
4195
+ ];
4196
+ function detectSQLInjection(value) {
4197
+ for (const pattern of SQL_INJECTION_PATTERNS) {
4198
+ if (pattern.test(value)) return true;
4199
+ }
4200
+ return false;
4201
+ }
4202
+ function checkLengthLimits(value, maxLength = 4096) {
4203
+ return value.length <= maxLength;
4204
+ }
4205
+ var ENTROPY_THRESHOLD = 4.5;
4206
+ var MIN_LENGTH_FOR_ENTROPY_CHECK = 32;
4207
+ function checkEntropyLimits(value) {
4208
+ if (value.length < MIN_LENGTH_FOR_ENTROPY_CHECK) return true;
4209
+ const entropy = calculateShannonEntropy(value);
4210
+ return entropy <= ENTROPY_THRESHOLD;
4211
+ }
4212
+ function calculateShannonEntropy(str) {
4213
+ const freq = /* @__PURE__ */ new Map();
4214
+ for (const char of str) {
4215
+ freq.set(char, (freq.get(char) ?? 0) + 1);
4216
+ }
4217
+ let entropy = 0;
4218
+ const len = str.length;
4219
+ for (const count of freq.values()) {
4220
+ const p = count / len;
4221
+ if (p > 0) {
4222
+ entropy -= p * Math.log2(p);
4223
+ }
4224
+ }
4225
+ return entropy;
4226
+ }
4227
+ function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
4228
+ const threats = [];
4229
+ if (typeof value !== "string") {
4230
+ if (typeof value === "object" && value !== null) {
4231
+ return sanitizeObject(field, value, config);
4232
+ }
4233
+ return { safe: true, threats: [] };
4234
+ }
4235
+ if (config.pathTraversal && detectPathTraversal(value)) {
4236
+ threats.push({
4237
+ type: "PATH_TRAVERSAL",
4238
+ field,
4239
+ value: truncate(value, 100),
4240
+ description: "Path traversal pattern detected"
4241
+ });
4242
+ }
4243
+ if (config.shellInjection && detectShellInjection(value)) {
4244
+ threats.push({
4245
+ type: "SHELL_INJECTION",
4246
+ field,
4247
+ value: truncate(value, 100),
4248
+ description: "Shell injection pattern detected"
4249
+ });
4250
+ }
4251
+ if (config.wildcardAbuse && detectWildcardAbuse(value)) {
4252
+ threats.push({
4253
+ type: "WILDCARD_ABUSE",
4254
+ field,
4255
+ value: truncate(value, 100),
4256
+ description: "Wildcard abuse pattern detected"
4257
+ });
4258
+ }
4259
+ if (!checkLengthLimits(value, config.lengthLimit)) {
4260
+ threats.push({
4261
+ type: "LENGTH_EXCEEDED",
4262
+ field,
4263
+ value: `[${value.length} chars]`,
4264
+ description: `Value exceeds maximum length of ${config.lengthLimit}`
4265
+ });
4266
+ }
4267
+ if (config.entropyLimit && !checkEntropyLimits(value)) {
4268
+ threats.push({
4269
+ type: "HIGH_ENTROPY",
4270
+ field,
4271
+ value: truncate(value, 100),
4272
+ description: "High entropy string detected - possible encoded payload"
4273
+ });
4274
+ }
4275
+ if (config.ssrf && detectSSRF(value)) {
4276
+ threats.push({
4277
+ type: "SSRF",
4278
+ field,
4279
+ value: truncate(value, 100),
4280
+ description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
4281
+ });
4282
+ }
4283
+ if (config.sqlInjection && detectSQLInjection(value)) {
4284
+ threats.push({
4285
+ type: "SQL_INJECTION",
4286
+ field,
4287
+ value: truncate(value, 100),
4288
+ description: "SQL injection pattern detected"
4289
+ });
4290
+ }
4291
+ return { safe: threats.length === 0, threats };
4292
+ }
4293
+ function sanitizeObject(basePath, obj, config) {
4294
+ const threats = [];
4295
+ if (Array.isArray(obj)) {
4296
+ for (let i = 0; i < obj.length; i++) {
4297
+ const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
4298
+ threats.push(...result.threats);
4299
+ }
4300
+ } else {
4301
+ for (const [key, val] of Object.entries(obj)) {
4302
+ const result = sanitizeInput(`${basePath}.${key}`, val, config);
4303
+ threats.push(...result.threats);
4304
+ }
4305
+ }
4306
+ return { safe: threats.length === 0, threats };
4307
+ }
4308
+ function truncate(str, maxLen) {
4309
+ return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
4310
+ }
4311
+
3869
4312
  // src/proxy.ts
3870
4313
  init_config();
3871
4314
 
@@ -4131,7 +4574,12 @@ var SolonGateProxy = class {
4131
4574
  const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
4132
4575
  if (this.config.apiKey) {
4133
4576
  if (this.config.apiKey.startsWith("sg_test_")) {
4134
- log2("Using test API key \u2014 skipping online validation.");
4577
+ const nodeEnv = process.env.NODE_ENV ?? "";
4578
+ if (nodeEnv === "production") {
4579
+ log2("ERROR: Test API keys (sg_test_) cannot be used in production. Use a sg_live_ key.");
4580
+ process.exit(1);
4581
+ }
4582
+ log2("Using test API key \u2014 skipping online validation (non-production mode).");
4135
4583
  } else {
4136
4584
  log2(`Validating license with ${apiUrl}...`);
4137
4585
  try {
@@ -4332,7 +4780,26 @@ var SolonGateProxy = class {
4332
4780
  });
4333
4781
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4334
4782
  if (!this.client) throw new Error("Upstream client disconnected");
4335
- return await this.client.readResource({ uri: request.params.uri });
4783
+ const uri = request.params.uri;
4784
+ const uriCheck = sanitizeInput("resource.uri", uri);
4785
+ if (!uriCheck.safe) {
4786
+ const threats = uriCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
4787
+ log2(`DENY resource read: ${uri} \u2014 ${threats}`);
4788
+ throw new Error(`Resource URI blocked by security policy: ${threats}`);
4789
+ }
4790
+ if (/^file:\/\//i.test(uri)) {
4791
+ const path = uri.replace(/^file:\/\/\/?/i, "/");
4792
+ if (detectPathTraversal(path)) {
4793
+ log2(`DENY resource read: ${uri} \u2014 path traversal in file URI`);
4794
+ throw new Error("Resource URI blocked: path traversal detected");
4795
+ }
4796
+ }
4797
+ if (/^https?:\/\//i.test(uri) && detectSSRF(uri)) {
4798
+ log2(`DENY resource read: ${uri} \u2014 SSRF pattern`);
4799
+ throw new Error("Resource URI blocked: internal/metadata URL not allowed");
4800
+ }
4801
+ log2(`Resource read: ${uri}`);
4802
+ return await this.client.readResource({ uri });
4336
4803
  });
4337
4804
  this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
4338
4805
  if (!this.client) return { resourceTemplates: [] };
@@ -4352,9 +4819,19 @@ var SolonGateProxy = class {
4352
4819
  });
4353
4820
  this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
4354
4821
  if (!this.client) throw new Error("Upstream client disconnected");
4822
+ const args = request.params.arguments;
4823
+ if (args && typeof args === "object") {
4824
+ const argsCheck = sanitizeInput("prompt.arguments", args);
4825
+ if (!argsCheck.safe) {
4826
+ const threats = argsCheck.threats.map((t) => `${t.type}: ${t.description}`).join("; ");
4827
+ log2(`DENY prompt get: ${request.params.name} \u2014 ${threats}`);
4828
+ throw new Error(`Prompt arguments blocked by security policy: ${threats}`);
4829
+ }
4830
+ }
4831
+ log2(`Prompt get: ${request.params.name}`);
4355
4832
  return await this.client.getPrompt({
4356
4833
  name: request.params.name,
4357
- arguments: request.params.arguments
4834
+ arguments: args
4358
4835
  });
4359
4836
  });
4360
4837
  }