@solongate/proxy 0.1.2 → 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.
- package/dist/create.js +263 -0
- package/dist/index.js +1213 -84
- package/dist/inject.js +339 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -285,6 +285,730 @@ var init_init = __esm({
|
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
// src/inject.ts
|
|
289
|
+
var inject_exports = {};
|
|
290
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3, copyFileSync as copyFileSync2 } from "fs";
|
|
291
|
+
import { resolve as resolve3 } from "path";
|
|
292
|
+
import { execSync } from "child_process";
|
|
293
|
+
function parseInjectArgs(argv) {
|
|
294
|
+
const args = argv.slice(2);
|
|
295
|
+
const opts = {
|
|
296
|
+
dryRun: false,
|
|
297
|
+
restore: false,
|
|
298
|
+
skipInstall: false
|
|
299
|
+
};
|
|
300
|
+
for (let i = 0; i < args.length; i++) {
|
|
301
|
+
switch (args[i]) {
|
|
302
|
+
case "--file":
|
|
303
|
+
opts.file = args[++i];
|
|
304
|
+
break;
|
|
305
|
+
case "--dry-run":
|
|
306
|
+
opts.dryRun = true;
|
|
307
|
+
break;
|
|
308
|
+
case "--restore":
|
|
309
|
+
opts.restore = true;
|
|
310
|
+
break;
|
|
311
|
+
case "--skip-install":
|
|
312
|
+
opts.skipInstall = true;
|
|
313
|
+
break;
|
|
314
|
+
case "--help":
|
|
315
|
+
case "-h":
|
|
316
|
+
printHelp2();
|
|
317
|
+
process.exit(0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return opts;
|
|
321
|
+
}
|
|
322
|
+
function printHelp2() {
|
|
323
|
+
log2(`
|
|
324
|
+
SolonGate Inject \u2014 Add security to your MCP server in seconds
|
|
325
|
+
|
|
326
|
+
USAGE
|
|
327
|
+
npx @solongate/proxy inject [options]
|
|
328
|
+
|
|
329
|
+
OPTIONS
|
|
330
|
+
--file <path> Entry file to modify (default: auto-detect)
|
|
331
|
+
--dry-run Preview changes without writing
|
|
332
|
+
--restore Restore original file from backup
|
|
333
|
+
--skip-install Don't install SDK package
|
|
334
|
+
-h, --help Show this help message
|
|
335
|
+
|
|
336
|
+
EXAMPLES
|
|
337
|
+
npx @solongate/proxy inject # Auto-detect and inject
|
|
338
|
+
npx @solongate/proxy inject --file src/main.ts # Specify entry file
|
|
339
|
+
npx @solongate/proxy inject --dry-run # Preview changes
|
|
340
|
+
npx @solongate/proxy inject --restore # Undo injection
|
|
341
|
+
|
|
342
|
+
WHAT IT DOES
|
|
343
|
+
Replaces McpServer with SecureMcpServer (2 lines changed)
|
|
344
|
+
All tool() calls are automatically protected by SolonGate.
|
|
345
|
+
`);
|
|
346
|
+
}
|
|
347
|
+
function log2(msg) {
|
|
348
|
+
process.stderr.write(msg + "\n");
|
|
349
|
+
}
|
|
350
|
+
function detectProject() {
|
|
351
|
+
if (!existsSync3(resolve3("package.json"))) return false;
|
|
352
|
+
try {
|
|
353
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
354
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
355
|
+
return !!(allDeps["@modelcontextprotocol/sdk"] || allDeps["@modelcontextprotocol/server"]);
|
|
356
|
+
} catch {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function findTsEntryFile() {
|
|
361
|
+
try {
|
|
362
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
363
|
+
if (pkg.bin) {
|
|
364
|
+
const binPath = typeof pkg.bin === "string" ? pkg.bin : Object.values(pkg.bin)[0];
|
|
365
|
+
if (typeof binPath === "string") {
|
|
366
|
+
const srcPath = binPath.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
367
|
+
if (existsSync3(resolve3(srcPath))) return resolve3(srcPath);
|
|
368
|
+
if (existsSync3(resolve3(binPath))) return resolve3(binPath);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (pkg.main) {
|
|
372
|
+
const srcPath = pkg.main.replace(/^\.\/dist\//, "./src/").replace(/\.js$/, ".ts");
|
|
373
|
+
if (existsSync3(resolve3(srcPath))) return resolve3(srcPath);
|
|
374
|
+
}
|
|
375
|
+
} catch {
|
|
376
|
+
}
|
|
377
|
+
const candidates = [
|
|
378
|
+
"src/index.ts",
|
|
379
|
+
"src/server.ts",
|
|
380
|
+
"src/main.ts",
|
|
381
|
+
"index.ts",
|
|
382
|
+
"server.ts",
|
|
383
|
+
"main.ts"
|
|
384
|
+
];
|
|
385
|
+
for (const c of candidates) {
|
|
386
|
+
const full = resolve3(c);
|
|
387
|
+
if (existsSync3(full)) {
|
|
388
|
+
try {
|
|
389
|
+
const content = readFileSync3(full, "utf-8");
|
|
390
|
+
if (content.includes("McpServer") || content.includes("McpServer")) {
|
|
391
|
+
return full;
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const c of candidates) {
|
|
398
|
+
if (existsSync3(resolve3(c))) return resolve3(c);
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
function detectPackageManager() {
|
|
403
|
+
if (existsSync3(resolve3("pnpm-lock.yaml"))) return "pnpm";
|
|
404
|
+
if (existsSync3(resolve3("yarn.lock"))) return "yarn";
|
|
405
|
+
return "npm";
|
|
406
|
+
}
|
|
407
|
+
function installSdk() {
|
|
408
|
+
try {
|
|
409
|
+
const pkg = JSON.parse(readFileSync3(resolve3("package.json"), "utf-8"));
|
|
410
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
411
|
+
if (allDeps["@solongate/sdk"]) {
|
|
412
|
+
log2(" @solongate/sdk already installed");
|
|
413
|
+
return true;
|
|
414
|
+
}
|
|
415
|
+
} catch {
|
|
416
|
+
}
|
|
417
|
+
const pm = detectPackageManager();
|
|
418
|
+
const cmd = pm === "yarn" ? "yarn add @solongate/sdk" : `${pm} install @solongate/sdk`;
|
|
419
|
+
log2(` Installing @solongate/sdk via ${pm}...`);
|
|
420
|
+
try {
|
|
421
|
+
execSync(cmd, { stdio: "pipe", cwd: process.cwd() });
|
|
422
|
+
return true;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
log2(` Failed to install: ${err instanceof Error ? err.message : String(err)}`);
|
|
425
|
+
log2(" You can install manually: npm install @solongate/sdk");
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function injectTypeScript(filePath) {
|
|
430
|
+
const original = readFileSync3(filePath, "utf-8");
|
|
431
|
+
const changes = [];
|
|
432
|
+
let modified = original;
|
|
433
|
+
if (modified.includes("SecureMcpServer")) {
|
|
434
|
+
return { file: filePath, changes: ["Already injected \u2014 skipping"], original, modified };
|
|
435
|
+
}
|
|
436
|
+
const mcpImportPatterns = [
|
|
437
|
+
// Solo import: import { McpServer } from '...'
|
|
438
|
+
/import\s*\{\s*McpServer\s*\}\s*from\s*['"][^'"]+['"]/,
|
|
439
|
+
// Import with others: import { McpServer, ... } from '...'
|
|
440
|
+
/import\s*\{[^}]*McpServer[^}]*\}\s*from\s*['"][^'"]+['"]/
|
|
441
|
+
];
|
|
442
|
+
let importReplaced = false;
|
|
443
|
+
for (const pattern of mcpImportPatterns) {
|
|
444
|
+
const match = modified.match(pattern);
|
|
445
|
+
if (match) {
|
|
446
|
+
const importLine = match[0];
|
|
447
|
+
const namedImports = importLine.match(/\{([^}]+)\}/)?.[1] ?? "";
|
|
448
|
+
const importNames = namedImports.split(",").map((s) => s.trim()).filter(Boolean);
|
|
449
|
+
if (importNames.length === 1 && importNames[0] === "McpServer") {
|
|
450
|
+
modified = modified.replace(
|
|
451
|
+
importLine,
|
|
452
|
+
`import { SecureMcpServer } from '@solongate/sdk'`
|
|
453
|
+
);
|
|
454
|
+
changes.push("Replaced McpServer import with SecureMcpServer from @solongate/sdk");
|
|
455
|
+
} else {
|
|
456
|
+
const otherImports = importNames.filter((n) => n !== "McpServer");
|
|
457
|
+
const fromModule = importLine.match(/from\s*['"]([^'"]+)['"]/)?.[1] ?? "";
|
|
458
|
+
modified = modified.replace(
|
|
459
|
+
importLine,
|
|
460
|
+
`import { ${otherImports.join(", ")} } from '${fromModule}';
|
|
461
|
+
import { SecureMcpServer } from '@solongate/sdk'`
|
|
462
|
+
);
|
|
463
|
+
changes.push("Removed McpServer from existing import, added SecureMcpServer import from @solongate/sdk");
|
|
464
|
+
}
|
|
465
|
+
importReplaced = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (!importReplaced) {
|
|
470
|
+
const insertPos = findImportInsertPosition(modified);
|
|
471
|
+
modified = modified.slice(0, insertPos) + `import { SecureMcpServer } from '@solongate/sdk';
|
|
472
|
+
` + modified.slice(insertPos);
|
|
473
|
+
changes.push("Added SecureMcpServer import from @solongate/sdk");
|
|
474
|
+
}
|
|
475
|
+
const constructorPattern = /new\s+McpServer\s*\(/g;
|
|
476
|
+
const constructorCount = (modified.match(constructorPattern) || []).length;
|
|
477
|
+
if (constructorCount > 0) {
|
|
478
|
+
modified = modified.replace(constructorPattern, "new SecureMcpServer(");
|
|
479
|
+
changes.push(`Replaced ${constructorCount} McpServer constructor(s) with SecureMcpServer`);
|
|
480
|
+
}
|
|
481
|
+
return { file: filePath, changes, original, modified };
|
|
482
|
+
}
|
|
483
|
+
function findImportInsertPosition(content) {
|
|
484
|
+
let pos = 0;
|
|
485
|
+
if (content.startsWith("#!")) {
|
|
486
|
+
pos = content.indexOf("\n") + 1;
|
|
487
|
+
}
|
|
488
|
+
const lines = content.slice(pos).split("\n");
|
|
489
|
+
let offset = pos;
|
|
490
|
+
for (const line of lines) {
|
|
491
|
+
const trimmed = line.trim();
|
|
492
|
+
if (trimmed === "" || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("*/")) {
|
|
493
|
+
offset += line.length + 1;
|
|
494
|
+
} else {
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const importRegex = /^import\s+.+$/gm;
|
|
499
|
+
let lastImportEnd = offset;
|
|
500
|
+
let importMatch;
|
|
501
|
+
while ((importMatch = importRegex.exec(content)) !== null) {
|
|
502
|
+
lastImportEnd = importMatch.index + importMatch[0].length + 1;
|
|
503
|
+
}
|
|
504
|
+
return lastImportEnd > offset ? lastImportEnd : offset;
|
|
505
|
+
}
|
|
506
|
+
function showDiff(result) {
|
|
507
|
+
if (result.original === result.modified) {
|
|
508
|
+
log2(" No changes needed.");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
const origLines = result.original.split("\n");
|
|
512
|
+
const modLines = result.modified.split("\n");
|
|
513
|
+
log2("");
|
|
514
|
+
log2(` File: ${result.file}`);
|
|
515
|
+
log2(" \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");
|
|
516
|
+
const maxLines = Math.max(origLines.length, modLines.length);
|
|
517
|
+
let diffCount = 0;
|
|
518
|
+
for (let i = 0; i < maxLines; i++) {
|
|
519
|
+
const orig = origLines[i];
|
|
520
|
+
const mod = modLines[i];
|
|
521
|
+
if (orig !== mod) {
|
|
522
|
+
if (diffCount < 30) {
|
|
523
|
+
if (orig !== void 0) log2(` - ${orig}`);
|
|
524
|
+
if (mod !== void 0) log2(` + ${mod}`);
|
|
525
|
+
}
|
|
526
|
+
diffCount++;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (diffCount > 30) {
|
|
530
|
+
log2(` ... and ${diffCount - 30} more line changes`);
|
|
531
|
+
}
|
|
532
|
+
log2("");
|
|
533
|
+
}
|
|
534
|
+
async function main2() {
|
|
535
|
+
const opts = parseInjectArgs(process.argv);
|
|
536
|
+
log2("");
|
|
537
|
+
log2(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
538
|
+
log2(" \u2551 SolonGate \u2014 Inject SDK \u2551");
|
|
539
|
+
log2(" \u2551 Add security to your MCP server \u2551");
|
|
540
|
+
log2(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
541
|
+
log2("");
|
|
542
|
+
if (!detectProject()) {
|
|
543
|
+
log2(" Could not detect a TypeScript MCP server project.");
|
|
544
|
+
log2(" Make sure you are in a project directory with:");
|
|
545
|
+
log2(" package.json + @modelcontextprotocol/sdk in dependencies");
|
|
546
|
+
log2("");
|
|
547
|
+
log2(" To create a new MCP server: npx @solongate/proxy create <name>");
|
|
548
|
+
process.exit(1);
|
|
549
|
+
}
|
|
550
|
+
log2(" Language: TypeScript");
|
|
551
|
+
const entryFile = opts.file ? resolve3(opts.file) : findTsEntryFile();
|
|
552
|
+
if (!entryFile || !existsSync3(entryFile)) {
|
|
553
|
+
log2(` Could not find entry file.${opts.file ? ` File not found: ${opts.file}` : ""}`);
|
|
554
|
+
log2("");
|
|
555
|
+
log2(" Specify it manually: --file <path>");
|
|
556
|
+
log2("");
|
|
557
|
+
log2(" Common entry points:");
|
|
558
|
+
log2(" src/index.ts, src/server.ts, index.ts");
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
log2(` Entry: ${entryFile}`);
|
|
562
|
+
log2("");
|
|
563
|
+
const backupPath = entryFile + ".solongate-backup";
|
|
564
|
+
if (opts.restore) {
|
|
565
|
+
if (!existsSync3(backupPath)) {
|
|
566
|
+
log2(" No backup found. Nothing to restore.");
|
|
567
|
+
process.exit(1);
|
|
568
|
+
}
|
|
569
|
+
copyFileSync2(backupPath, entryFile);
|
|
570
|
+
log2(` Restored original file from backup.`);
|
|
571
|
+
log2(` Backup: ${backupPath}`);
|
|
572
|
+
process.exit(0);
|
|
573
|
+
}
|
|
574
|
+
if (!opts.skipInstall && !opts.dryRun) {
|
|
575
|
+
installSdk();
|
|
576
|
+
log2("");
|
|
577
|
+
}
|
|
578
|
+
const result = injectTypeScript(entryFile);
|
|
579
|
+
log2(` Changes (${result.changes.length}):`);
|
|
580
|
+
for (const change of result.changes) {
|
|
581
|
+
log2(` - ${change}`);
|
|
582
|
+
}
|
|
583
|
+
if (result.changes.length === 1 && result.changes[0].includes("Already injected")) {
|
|
584
|
+
log2("");
|
|
585
|
+
log2(" Your MCP server is already protected by SolonGate!");
|
|
586
|
+
process.exit(0);
|
|
587
|
+
}
|
|
588
|
+
if (result.original === result.modified) {
|
|
589
|
+
log2("");
|
|
590
|
+
log2(" No changes were made. The file may not contain recognizable MCP patterns.");
|
|
591
|
+
log2(" See docs: https://solongate.com/docs/integration");
|
|
592
|
+
process.exit(0);
|
|
593
|
+
}
|
|
594
|
+
if (opts.dryRun) {
|
|
595
|
+
log2("");
|
|
596
|
+
log2(" --- DRY RUN (no changes written) ---");
|
|
597
|
+
showDiff(result);
|
|
598
|
+
log2(" To apply: npx @solongate/proxy inject");
|
|
599
|
+
process.exit(0);
|
|
600
|
+
}
|
|
601
|
+
if (!existsSync3(backupPath)) {
|
|
602
|
+
copyFileSync2(entryFile, backupPath);
|
|
603
|
+
log2("");
|
|
604
|
+
log2(` Backup: ${backupPath}`);
|
|
605
|
+
}
|
|
606
|
+
writeFileSync2(entryFile, result.modified);
|
|
607
|
+
log2("");
|
|
608
|
+
log2(" \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");
|
|
609
|
+
log2(" \u2502 SolonGate SDK injected successfully! \u2502");
|
|
610
|
+
log2(" \u2502 \u2502");
|
|
611
|
+
log2(" \u2502 McpServer \u2192 SecureMcpServer \u2502");
|
|
612
|
+
log2(" \u2502 All tool() calls are now auto-protected. \u2502");
|
|
613
|
+
log2(" \u2502 \u2502");
|
|
614
|
+
log2(" \u2502 Set your API key: \u2502");
|
|
615
|
+
log2(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
|
|
616
|
+
log2(" \u2502 \u2502");
|
|
617
|
+
log2(" \u2502 To undo: \u2502");
|
|
618
|
+
log2(" \u2502 npx @solongate/proxy inject --restore \u2502");
|
|
619
|
+
log2(" \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");
|
|
620
|
+
log2("");
|
|
621
|
+
}
|
|
622
|
+
var init_inject = __esm({
|
|
623
|
+
"src/inject.ts"() {
|
|
624
|
+
"use strict";
|
|
625
|
+
main2().catch((err) => {
|
|
626
|
+
log2(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
627
|
+
process.exit(1);
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// src/create.ts
|
|
633
|
+
var create_exports = {};
|
|
634
|
+
import { mkdirSync, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
635
|
+
import { resolve as resolve4, join as join2 } from "path";
|
|
636
|
+
import { execSync as execSync2 } from "child_process";
|
|
637
|
+
function log3(msg) {
|
|
638
|
+
process.stderr.write(msg + "\n");
|
|
639
|
+
}
|
|
640
|
+
function parseCreateArgs(argv) {
|
|
641
|
+
const args = argv.slice(2);
|
|
642
|
+
const opts = {
|
|
643
|
+
name: "",
|
|
644
|
+
policy: "restricted",
|
|
645
|
+
noInstall: false
|
|
646
|
+
};
|
|
647
|
+
for (let i = 0; i < args.length; i++) {
|
|
648
|
+
switch (args[i]) {
|
|
649
|
+
case "--policy":
|
|
650
|
+
opts.policy = args[++i];
|
|
651
|
+
break;
|
|
652
|
+
case "--no-install":
|
|
653
|
+
opts.noInstall = true;
|
|
654
|
+
break;
|
|
655
|
+
case "--help":
|
|
656
|
+
case "-h":
|
|
657
|
+
printHelp3();
|
|
658
|
+
process.exit(0);
|
|
659
|
+
break;
|
|
660
|
+
default:
|
|
661
|
+
if (!args[i].startsWith("-") && !opts.name) {
|
|
662
|
+
opts.name = args[i];
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (!opts.name) {
|
|
667
|
+
log3("");
|
|
668
|
+
log3(" Error: Project name required.");
|
|
669
|
+
log3("");
|
|
670
|
+
log3(" Usage: npx @solongate/proxy create <name>");
|
|
671
|
+
log3("");
|
|
672
|
+
log3(" Examples:");
|
|
673
|
+
log3(" npx @solongate/proxy create my-mcp-server");
|
|
674
|
+
log3(" npx @solongate/proxy create weather-api");
|
|
675
|
+
process.exit(1);
|
|
676
|
+
}
|
|
677
|
+
return opts;
|
|
678
|
+
}
|
|
679
|
+
function printHelp3() {
|
|
680
|
+
log3(`
|
|
681
|
+
SolonGate Create \u2014 Scaffold a secure MCP server in seconds
|
|
682
|
+
|
|
683
|
+
USAGE
|
|
684
|
+
npx @solongate/proxy create <name> [options]
|
|
685
|
+
|
|
686
|
+
OPTIONS
|
|
687
|
+
--policy <preset> Policy preset (default: restricted)
|
|
688
|
+
--no-install Skip dependency installation
|
|
689
|
+
-h, --help Show this help message
|
|
690
|
+
|
|
691
|
+
EXAMPLES
|
|
692
|
+
npx @solongate/proxy create my-server
|
|
693
|
+
npx @solongate/proxy create db-tools --policy read-only
|
|
694
|
+
`);
|
|
695
|
+
}
|
|
696
|
+
function createProject(dir, name, _policy) {
|
|
697
|
+
writeFileSync3(
|
|
698
|
+
join2(dir, "package.json"),
|
|
699
|
+
JSON.stringify(
|
|
700
|
+
{
|
|
701
|
+
name,
|
|
702
|
+
version: "0.1.0",
|
|
703
|
+
type: "module",
|
|
704
|
+
private: true,
|
|
705
|
+
bin: { [name]: "./dist/index.js" },
|
|
706
|
+
scripts: {
|
|
707
|
+
build: "tsup src/index.ts --format esm",
|
|
708
|
+
dev: "tsx src/index.ts",
|
|
709
|
+
start: "node dist/index.js"
|
|
710
|
+
},
|
|
711
|
+
dependencies: {
|
|
712
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
713
|
+
"@solongate/sdk": "latest",
|
|
714
|
+
zod: "^3.25.0"
|
|
715
|
+
},
|
|
716
|
+
devDependencies: {
|
|
717
|
+
tsup: "^8.3.0",
|
|
718
|
+
tsx: "^4.19.0",
|
|
719
|
+
typescript: "^5.7.0"
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
null,
|
|
723
|
+
2
|
|
724
|
+
) + "\n"
|
|
725
|
+
);
|
|
726
|
+
writeFileSync3(
|
|
727
|
+
join2(dir, "tsconfig.json"),
|
|
728
|
+
JSON.stringify(
|
|
729
|
+
{
|
|
730
|
+
compilerOptions: {
|
|
731
|
+
target: "ES2022",
|
|
732
|
+
module: "ESNext",
|
|
733
|
+
moduleResolution: "bundler",
|
|
734
|
+
esModuleInterop: true,
|
|
735
|
+
strict: true,
|
|
736
|
+
outDir: "dist",
|
|
737
|
+
rootDir: "src",
|
|
738
|
+
declaration: true,
|
|
739
|
+
skipLibCheck: true
|
|
740
|
+
},
|
|
741
|
+
include: ["src"]
|
|
742
|
+
},
|
|
743
|
+
null,
|
|
744
|
+
2
|
|
745
|
+
) + "\n"
|
|
746
|
+
);
|
|
747
|
+
mkdirSync(join2(dir, "src"), { recursive: true });
|
|
748
|
+
writeFileSync3(
|
|
749
|
+
join2(dir, "src", "index.ts"),
|
|
750
|
+
`#!/usr/bin/env node
|
|
751
|
+
|
|
752
|
+
// MCP uses stdout for JSON-RPC \u2014 redirect console to stderr
|
|
753
|
+
console.log = (...args: unknown[]) => {
|
|
754
|
+
process.stderr.write(args.map(String).join(' ') + '\\n');
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
import { SecureMcpServer } from '@solongate/sdk';
|
|
758
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
759
|
+
import { z } from 'zod';
|
|
760
|
+
|
|
761
|
+
// Create a secure MCP server (API key from SOLONGATE_API_KEY env var)
|
|
762
|
+
const server = new SecureMcpServer({
|
|
763
|
+
name: '${name}',
|
|
764
|
+
version: '0.1.0',
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// \u2500\u2500 Register your tools below \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
|
|
768
|
+
|
|
769
|
+
server.tool(
|
|
770
|
+
'hello',
|
|
771
|
+
'Say hello to someone',
|
|
772
|
+
{ name: z.string().describe('Name of the person to greet') },
|
|
773
|
+
async ({ name }) => ({
|
|
774
|
+
content: [{ type: 'text', text: \`Hello, \${name}! Welcome to ${name}.\` }],
|
|
775
|
+
}),
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
// Example: Add more tools here
|
|
779
|
+
// server.tool(
|
|
780
|
+
// 'read_data',
|
|
781
|
+
// 'Read data from a source',
|
|
782
|
+
// { query: z.string().describe('What to read') },
|
|
783
|
+
// async ({ query }) => ({
|
|
784
|
+
// content: [{ type: 'text', text: \`Result for: \${query}\` }],
|
|
785
|
+
// }),
|
|
786
|
+
// );
|
|
787
|
+
|
|
788
|
+
// \u2500\u2500 Start the server \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
789
|
+
|
|
790
|
+
const transport = new StdioServerTransport();
|
|
791
|
+
await server.connect(transport);
|
|
792
|
+
console.log('${name} is running');
|
|
793
|
+
`
|
|
794
|
+
);
|
|
795
|
+
writeFileSync3(
|
|
796
|
+
join2(dir, ".mcp.json"),
|
|
797
|
+
JSON.stringify(
|
|
798
|
+
{
|
|
799
|
+
mcpServers: {
|
|
800
|
+
[name]: {
|
|
801
|
+
command: "node",
|
|
802
|
+
args: ["dist/index.js"],
|
|
803
|
+
env: {
|
|
804
|
+
SOLONGATE_API_KEY: "sg_test_e4460d32_replace_with_your_key"
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
null,
|
|
810
|
+
2
|
|
811
|
+
) + "\n"
|
|
812
|
+
);
|
|
813
|
+
writeFileSync3(
|
|
814
|
+
join2(dir, ".gitignore"),
|
|
815
|
+
`node_modules/
|
|
816
|
+
dist/
|
|
817
|
+
*.solongate-backup
|
|
818
|
+
.env
|
|
819
|
+
.env.local
|
|
820
|
+
`
|
|
821
|
+
);
|
|
822
|
+
}
|
|
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);
|
|
829
|
+
try {
|
|
830
|
+
fn();
|
|
831
|
+
clearInterval(id);
|
|
832
|
+
process.stderr.write(`\r \u2713 ${message}
|
|
833
|
+
`);
|
|
834
|
+
} catch {
|
|
835
|
+
clearInterval(id);
|
|
836
|
+
process.stderr.write(`\r \u2717 ${message} \u2014 failed
|
|
837
|
+
`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
function installDeps(dir) {
|
|
841
|
+
withSpinner("Installing dependencies...", () => {
|
|
842
|
+
execSync2("npm install", { cwd: dir, stdio: "pipe" });
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
async function main3() {
|
|
846
|
+
const opts = parseCreateArgs(process.argv);
|
|
847
|
+
const dir = resolve4(opts.name);
|
|
848
|
+
log3("");
|
|
849
|
+
log3(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
850
|
+
log3(" \u2551 SolonGate \u2014 Create MCP Server \u2551");
|
|
851
|
+
log3(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
852
|
+
log3("");
|
|
853
|
+
if (existsSync4(dir)) {
|
|
854
|
+
log3(` Error: Directory "${opts.name}" already exists.`);
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
mkdirSync(dir, { recursive: true });
|
|
858
|
+
log3(` Project: ${opts.name}`);
|
|
859
|
+
log3(` Language: TypeScript`);
|
|
860
|
+
log3(` Policy: ${opts.policy}`);
|
|
861
|
+
log3("");
|
|
862
|
+
createProject(dir, opts.name, opts.policy);
|
|
863
|
+
log3(" Files created:");
|
|
864
|
+
log3(" package.json");
|
|
865
|
+
log3(" tsconfig.json");
|
|
866
|
+
log3(" src/index.ts");
|
|
867
|
+
log3(" .mcp.json");
|
|
868
|
+
log3(" .gitignore");
|
|
869
|
+
log3("");
|
|
870
|
+
if (!opts.noInstall) {
|
|
871
|
+
installDeps(dir);
|
|
872
|
+
log3("");
|
|
873
|
+
}
|
|
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`);
|
|
888
|
+
log3("");
|
|
889
|
+
}
|
|
890
|
+
var init_create = __esm({
|
|
891
|
+
"src/create.ts"() {
|
|
892
|
+
"use strict";
|
|
893
|
+
main3().catch((err) => {
|
|
894
|
+
log3(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
895
|
+
process.exit(1);
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
// ../core/dist/index.js
|
|
901
|
+
import { z } from "zod";
|
|
902
|
+
var Permission = {
|
|
903
|
+
READ: "READ",
|
|
904
|
+
WRITE: "WRITE",
|
|
905
|
+
EXECUTE: "EXECUTE"
|
|
906
|
+
};
|
|
907
|
+
var PermissionSchema = z.enum(["READ", "WRITE", "EXECUTE"]);
|
|
908
|
+
var NO_PERMISSIONS = Object.freeze(
|
|
909
|
+
/* @__PURE__ */ new Set()
|
|
910
|
+
);
|
|
911
|
+
var READ_ONLY = Object.freeze(
|
|
912
|
+
/* @__PURE__ */ new Set([Permission.READ])
|
|
913
|
+
);
|
|
914
|
+
var PolicyRuleSchema = z.object({
|
|
915
|
+
id: z.string().min(1).max(256),
|
|
916
|
+
description: z.string().max(1024),
|
|
917
|
+
effect: z.enum(["ALLOW", "DENY"]),
|
|
918
|
+
priority: z.number().int().min(0).max(1e4).default(1e3),
|
|
919
|
+
toolPattern: z.string().min(1).max(512),
|
|
920
|
+
permission: z.enum(["READ", "WRITE", "EXECUTE"]),
|
|
921
|
+
minimumTrustLevel: z.enum(["UNTRUSTED", "VERIFIED", "TRUSTED"]),
|
|
922
|
+
argumentConstraints: z.record(z.unknown()).optional(),
|
|
923
|
+
pathConstraints: z.object({
|
|
924
|
+
allowed: z.array(z.string()).optional(),
|
|
925
|
+
denied: z.array(z.string()).optional(),
|
|
926
|
+
rootDirectory: z.string().optional(),
|
|
927
|
+
allowSymlinks: z.boolean().optional()
|
|
928
|
+
}).optional(),
|
|
929
|
+
enabled: z.boolean().default(true),
|
|
930
|
+
createdAt: z.string().datetime(),
|
|
931
|
+
updatedAt: z.string().datetime()
|
|
932
|
+
});
|
|
933
|
+
var PolicySetSchema = z.object({
|
|
934
|
+
id: z.string().min(1).max(256),
|
|
935
|
+
name: z.string().min(1).max(256),
|
|
936
|
+
description: z.string().max(2048),
|
|
937
|
+
version: z.number().int().min(0),
|
|
938
|
+
rules: z.array(PolicyRuleSchema),
|
|
939
|
+
createdAt: z.string().datetime(),
|
|
940
|
+
updatedAt: z.string().datetime()
|
|
941
|
+
});
|
|
942
|
+
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
943
|
+
var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
944
|
+
pathTraversal: true,
|
|
945
|
+
shellInjection: true,
|
|
946
|
+
wildcardAbuse: true,
|
|
947
|
+
lengthLimit: 4096,
|
|
948
|
+
entropyLimit: true,
|
|
949
|
+
ssrf: true,
|
|
950
|
+
sqlInjection: true
|
|
951
|
+
});
|
|
952
|
+
var SSRF_PATTERNS = [
|
|
953
|
+
/^https?:\/\/localhost\b/i,
|
|
954
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
955
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
956
|
+
/^https?:\/\/\[::1\]/,
|
|
957
|
+
// IPv6 loopback
|
|
958
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
959
|
+
// 10.x.x.x
|
|
960
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
961
|
+
// 172.16-31.x.x
|
|
962
|
+
/^https?:\/\/192\.168\./,
|
|
963
|
+
// 192.168.x.x
|
|
964
|
+
/^https?:\/\/169\.254\./,
|
|
965
|
+
// Link-local / AWS metadata
|
|
966
|
+
/metadata\.google\.internal/i,
|
|
967
|
+
// GCP metadata
|
|
968
|
+
/^https?:\/\/metadata\b/i,
|
|
969
|
+
// Generic metadata endpoint
|
|
970
|
+
// IPv6 bypass patterns
|
|
971
|
+
/^https?:\/\/\[fe80:/i,
|
|
972
|
+
// IPv6 link-local
|
|
973
|
+
/^https?:\/\/\[fc00:/i,
|
|
974
|
+
// IPv6 unique local
|
|
975
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
976
|
+
// IPv6 unique local (fd00::/8)
|
|
977
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
978
|
+
// IPv4-mapped IPv6 loopback
|
|
979
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
980
|
+
// IPv4-mapped IPv6 private
|
|
981
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
982
|
+
// IPv4-mapped IPv6 private
|
|
983
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
984
|
+
// IPv4-mapped IPv6 private
|
|
985
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
986
|
+
// IPv4-mapped IPv6 link-local
|
|
987
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
988
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
989
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
990
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
991
|
+
];
|
|
992
|
+
function detectDecimalIP(value) {
|
|
993
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
994
|
+
if (!match || !match[1]) return false;
|
|
995
|
+
const decimal = parseInt(match[1], 10);
|
|
996
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
997
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
998
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
999
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
1000
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
1001
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
1002
|
+
decimal === 0;
|
|
1003
|
+
}
|
|
1004
|
+
function detectSSRF(value) {
|
|
1005
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
1006
|
+
if (pattern.test(value)) return true;
|
|
1007
|
+
}
|
|
1008
|
+
if (detectDecimalIP(value)) return true;
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
288
1012
|
// src/config.ts
|
|
289
1013
|
import { readFileSync, existsSync } from "fs";
|
|
290
1014
|
import { resolve } from "path";
|
|
@@ -504,6 +1228,9 @@ function parseArgs(argv) {
|
|
|
504
1228
|
let configFile;
|
|
505
1229
|
let apiKey;
|
|
506
1230
|
let apiUrl;
|
|
1231
|
+
let upstreamUrl;
|
|
1232
|
+
let upstreamTransport;
|
|
1233
|
+
let port;
|
|
507
1234
|
let separatorIndex = args.indexOf("--");
|
|
508
1235
|
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
509
1236
|
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
@@ -536,14 +1263,43 @@ function parseArgs(argv) {
|
|
|
536
1263
|
case "--api-url":
|
|
537
1264
|
apiUrl = flags[++i];
|
|
538
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
|
+
);
|
|
539
1285
|
}
|
|
540
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
|
+
}
|
|
541
1292
|
if (configFile) {
|
|
542
1293
|
const filePath = resolve(configFile);
|
|
543
1294
|
const content = readFileSync(filePath, "utf-8");
|
|
544
1295
|
const fileConfig = JSON.parse(content);
|
|
545
1296
|
if (!fileConfig.upstream) {
|
|
546
|
-
throw new Error('Config file must include "upstream" with at least "command"');
|
|
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
|
+
);
|
|
547
1303
|
}
|
|
548
1304
|
return {
|
|
549
1305
|
upstream: fileConfig.upstream,
|
|
@@ -554,17 +1310,45 @@ function parseArgs(argv) {
|
|
|
554
1310
|
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
555
1311
|
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
556
1312
|
apiKey: apiKey ?? fileConfig.apiKey,
|
|
557
|
-
apiUrl: apiUrl ?? fileConfig.apiUrl
|
|
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
|
|
558
1341
|
};
|
|
559
1342
|
}
|
|
560
1343
|
if (upstreamArgs.length === 0) {
|
|
561
1344
|
throw new Error(
|
|
562
|
-
"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 --config solongate.json\n"
|
|
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"
|
|
563
1346
|
);
|
|
564
1347
|
}
|
|
565
1348
|
const [command, ...commandArgs] = upstreamArgs;
|
|
566
1349
|
return {
|
|
567
1350
|
upstream: {
|
|
1351
|
+
transport: upstreamTransport ?? "stdio",
|
|
568
1352
|
command,
|
|
569
1353
|
args: commandArgs,
|
|
570
1354
|
env: { ...process.env }
|
|
@@ -576,15 +1360,19 @@ function parseArgs(argv) {
|
|
|
576
1360
|
rateLimitPerTool,
|
|
577
1361
|
globalRateLimit,
|
|
578
1362
|
apiKey,
|
|
579
|
-
apiUrl
|
|
1363
|
+
apiUrl,
|
|
1364
|
+
port
|
|
580
1365
|
};
|
|
581
1366
|
}
|
|
582
1367
|
|
|
583
1368
|
// src/proxy.ts
|
|
584
1369
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
585
1370
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1371
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
586
1372
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
587
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";
|
|
588
1376
|
import {
|
|
589
1377
|
ListToolsRequestSchema,
|
|
590
1378
|
CallToolRequestSchema,
|
|
@@ -594,9 +1382,12 @@ import {
|
|
|
594
1382
|
ReadResourceRequestSchema,
|
|
595
1383
|
ListResourceTemplatesRequestSchema
|
|
596
1384
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1385
|
+
import { createServer as createHttpServer } from "http";
|
|
597
1386
|
|
|
598
|
-
// ../
|
|
599
|
-
import { z } from "zod";
|
|
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";
|
|
600
1391
|
var SolonGateError = class extends Error {
|
|
601
1392
|
code;
|
|
602
1393
|
timestamp;
|
|
@@ -658,49 +1449,49 @@ var TrustLevel = {
|
|
|
658
1449
|
VERIFIED: "VERIFIED",
|
|
659
1450
|
TRUSTED: "TRUSTED"
|
|
660
1451
|
};
|
|
661
|
-
var
|
|
1452
|
+
var Permission2 = {
|
|
662
1453
|
READ: "READ",
|
|
663
1454
|
WRITE: "WRITE",
|
|
664
1455
|
EXECUTE: "EXECUTE"
|
|
665
1456
|
};
|
|
666
|
-
|
|
667
|
-
|
|
1457
|
+
z2.enum(["READ", "WRITE", "EXECUTE"]);
|
|
1458
|
+
Object.freeze(
|
|
668
1459
|
/* @__PURE__ */ new Set()
|
|
669
1460
|
);
|
|
670
|
-
|
|
671
|
-
/* @__PURE__ */ new Set([
|
|
1461
|
+
Object.freeze(
|
|
1462
|
+
/* @__PURE__ */ new Set([Permission2.READ])
|
|
672
1463
|
);
|
|
673
1464
|
var PolicyEffect = {
|
|
674
1465
|
ALLOW: "ALLOW",
|
|
675
1466
|
DENY: "DENY"
|
|
676
1467
|
};
|
|
677
|
-
var
|
|
678
|
-
id:
|
|
679
|
-
description:
|
|
680
|
-
effect:
|
|
681
|
-
priority:
|
|
682
|
-
toolPattern:
|
|
683
|
-
permission:
|
|
684
|
-
minimumTrustLevel:
|
|
685
|
-
argumentConstraints:
|
|
686
|
-
pathConstraints:
|
|
687
|
-
allowed:
|
|
688
|
-
denied:
|
|
689
|
-
rootDirectory:
|
|
690
|
-
allowSymlinks:
|
|
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()
|
|
691
1482
|
}).optional(),
|
|
692
|
-
enabled:
|
|
693
|
-
createdAt:
|
|
694
|
-
updatedAt:
|
|
1483
|
+
enabled: z2.boolean().default(true),
|
|
1484
|
+
createdAt: z2.string().datetime(),
|
|
1485
|
+
updatedAt: z2.string().datetime()
|
|
695
1486
|
});
|
|
696
|
-
var
|
|
697
|
-
id:
|
|
698
|
-
name:
|
|
699
|
-
description:
|
|
700
|
-
version:
|
|
701
|
-
rules:
|
|
702
|
-
createdAt:
|
|
703
|
-
updatedAt:
|
|
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()
|
|
704
1495
|
});
|
|
705
1496
|
function createSecurityContext(params) {
|
|
706
1497
|
return {
|
|
@@ -714,14 +1505,12 @@ function createSecurityContext(params) {
|
|
|
714
1505
|
}
|
|
715
1506
|
var DEFAULT_POLICY_EFFECT = "DENY";
|
|
716
1507
|
var MAX_RULES_PER_POLICY_SET = 1e3;
|
|
717
|
-
var SECURITY_CONTEXT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
718
1508
|
var POLICY_EVALUATION_TIMEOUT_MS = 100;
|
|
719
1509
|
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
720
1510
|
var RATE_LIMIT_MAX_ENTRIES = 1e4;
|
|
721
1511
|
var UNSAFE_CONFIGURATION_WARNINGS = {
|
|
722
1512
|
WILDCARD_ALLOW: "Wildcard ALLOW rules grant permission to ALL tools. This bypasses the default-deny model.",
|
|
723
1513
|
TRUSTED_LEVEL_EXTERNAL: "Setting trust level to TRUSTED for external requests bypasses all security checks.",
|
|
724
|
-
WRITE_WITHOUT_READ: "Granting WRITE without READ is unusual and may indicate a misconfiguration.",
|
|
725
1514
|
EXECUTE_WITHOUT_REVIEW: "EXECUTE permission allows tools to perform arbitrary actions. Review carefully.",
|
|
726
1515
|
RATE_LIMIT_ZERO: "A rate limit of 0 means unlimited calls. This removes protection against runaway loops.",
|
|
727
1516
|
DISABLED_VALIDATION: "Disabling schema validation removes input sanitization protections."
|
|
@@ -741,12 +1530,14 @@ function createDeniedToolResult(reason) {
|
|
|
741
1530
|
isError: true
|
|
742
1531
|
};
|
|
743
1532
|
}
|
|
744
|
-
var
|
|
1533
|
+
var DEFAULT_INPUT_GUARD_CONFIG2 = Object.freeze({
|
|
745
1534
|
pathTraversal: true,
|
|
746
1535
|
shellInjection: true,
|
|
747
1536
|
wildcardAbuse: true,
|
|
748
1537
|
lengthLimit: 4096,
|
|
749
|
-
entropyLimit: true
|
|
1538
|
+
entropyLimit: true,
|
|
1539
|
+
ssrf: true,
|
|
1540
|
+
sqlInjection: true
|
|
750
1541
|
});
|
|
751
1542
|
var PATH_TRAVERSAL_PATTERNS = [
|
|
752
1543
|
/\.\.\//,
|
|
@@ -772,7 +1563,23 @@ var SENSITIVE_PATHS = [
|
|
|
772
1563
|
/c:\\windows\\system32/i,
|
|
773
1564
|
/c:\\windows\\syswow64/i,
|
|
774
1565
|
/\/root\//i,
|
|
775
|
-
|
|
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
|
|
776
1583
|
];
|
|
777
1584
|
function detectPathTraversal(value) {
|
|
778
1585
|
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
@@ -802,8 +1609,18 @@ var SHELL_INJECTION_PATTERNS = [
|
|
|
802
1609
|
// eval command
|
|
803
1610
|
/\bexec\b/i,
|
|
804
1611
|
// exec command
|
|
805
|
-
/\bsystem\b/i
|
|
1612
|
+
/\bsystem\b/i,
|
|
806
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)
|
|
807
1624
|
];
|
|
808
1625
|
function detectShellInjection(value) {
|
|
809
1626
|
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
@@ -818,6 +1635,91 @@ function detectWildcardAbuse(value) {
|
|
|
818
1635
|
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
819
1636
|
return false;
|
|
820
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
|
+
}
|
|
821
1723
|
function checkLengthLimits(value, maxLength = 4096) {
|
|
822
1724
|
return value.length <= maxLength;
|
|
823
1725
|
}
|
|
@@ -843,7 +1745,7 @@ function calculateShannonEntropy(str) {
|
|
|
843
1745
|
}
|
|
844
1746
|
return entropy;
|
|
845
1747
|
}
|
|
846
|
-
function sanitizeInput(field, value, config =
|
|
1748
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG2) {
|
|
847
1749
|
const threats = [];
|
|
848
1750
|
if (typeof value !== "string") {
|
|
849
1751
|
if (typeof value === "object" && value !== null) {
|
|
@@ -891,6 +1793,22 @@ function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
|
|
|
891
1793
|
description: "High entropy string detected - possible encoded payload"
|
|
892
1794
|
});
|
|
893
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
|
+
}
|
|
894
1812
|
return { safe: threats.length === 0, threats };
|
|
895
1813
|
}
|
|
896
1814
|
function sanitizeObject(basePath, obj, config) {
|
|
@@ -914,9 +1832,6 @@ function truncate(str, maxLen) {
|
|
|
914
1832
|
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
915
1833
|
var TOKEN_ALGORITHM = "HS256";
|
|
916
1834
|
var MIN_SECRET_LENGTH = 32;
|
|
917
|
-
|
|
918
|
-
// ../policy-engine/dist/index.js
|
|
919
|
-
import { createHash } from "crypto";
|
|
920
1835
|
function normalizePath(path) {
|
|
921
1836
|
let normalized = path.replace(/\\/g, "/");
|
|
922
1837
|
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
@@ -1064,8 +1979,51 @@ function trustLevelMeetsMinimum(actual, minimum) {
|
|
|
1064
1979
|
function argumentConstraintsMatch(constraints, args) {
|
|
1065
1980
|
for (const [key, constraint] of Object.entries(constraints)) {
|
|
1066
1981
|
if (!(key in args)) return false;
|
|
1067
|
-
|
|
1068
|
-
|
|
1982
|
+
const argValue = args[key];
|
|
1983
|
+
if (typeof constraint === "string") {
|
|
1984
|
+
if (constraint === "*") continue;
|
|
1985
|
+
if (typeof argValue === "string") {
|
|
1986
|
+
if (argValue !== constraint) return false;
|
|
1987
|
+
} else {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
continue;
|
|
1991
|
+
}
|
|
1992
|
+
if (typeof constraint === "object" && constraint !== null && !Array.isArray(constraint)) {
|
|
1993
|
+
const ops = constraint;
|
|
1994
|
+
const strValue = typeof argValue === "string" ? argValue : void 0;
|
|
1995
|
+
const numValue = typeof argValue === "number" ? argValue : void 0;
|
|
1996
|
+
if ("$contains" in ops && typeof ops.$contains === "string") {
|
|
1997
|
+
if (!strValue || !strValue.includes(ops.$contains)) return false;
|
|
1998
|
+
}
|
|
1999
|
+
if ("$notContains" in ops && typeof ops.$notContains === "string") {
|
|
2000
|
+
if (strValue && strValue.includes(ops.$notContains)) return false;
|
|
2001
|
+
}
|
|
2002
|
+
if ("$startsWith" in ops && typeof ops.$startsWith === "string") {
|
|
2003
|
+
if (!strValue || !strValue.startsWith(ops.$startsWith)) return false;
|
|
2004
|
+
}
|
|
2005
|
+
if ("$endsWith" in ops && typeof ops.$endsWith === "string") {
|
|
2006
|
+
if (!strValue || !strValue.endsWith(ops.$endsWith)) return false;
|
|
2007
|
+
}
|
|
2008
|
+
if ("$in" in ops && Array.isArray(ops.$in)) {
|
|
2009
|
+
if (!ops.$in.includes(argValue)) return false;
|
|
2010
|
+
}
|
|
2011
|
+
if ("$notIn" in ops && Array.isArray(ops.$notIn)) {
|
|
2012
|
+
if (ops.$notIn.includes(argValue)) return false;
|
|
2013
|
+
}
|
|
2014
|
+
if ("$gt" in ops && typeof ops.$gt === "number") {
|
|
2015
|
+
if (numValue === void 0 || numValue <= ops.$gt) return false;
|
|
2016
|
+
}
|
|
2017
|
+
if ("$lt" in ops && typeof ops.$lt === "number") {
|
|
2018
|
+
if (numValue === void 0 || numValue >= ops.$lt) return false;
|
|
2019
|
+
}
|
|
2020
|
+
if ("$gte" in ops && typeof ops.$gte === "number") {
|
|
2021
|
+
if (numValue === void 0 || numValue < ops.$gte) return false;
|
|
2022
|
+
}
|
|
2023
|
+
if ("$lte" in ops && typeof ops.$lte === "number") {
|
|
2024
|
+
if (numValue === void 0 || numValue > ops.$lte) return false;
|
|
2025
|
+
}
|
|
2026
|
+
continue;
|
|
1069
2027
|
}
|
|
1070
2028
|
}
|
|
1071
2029
|
return true;
|
|
@@ -1098,13 +2056,21 @@ function evaluatePolicy(policySet, request) {
|
|
|
1098
2056
|
matchedRule: null,
|
|
1099
2057
|
reason: "No matching policy rule found. Default action: DENY.",
|
|
1100
2058
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1101
|
-
evaluationTimeMs: endTime - startTime
|
|
2059
|
+
evaluationTimeMs: endTime - startTime,
|
|
2060
|
+
metadata: {
|
|
2061
|
+
evaluatedRules: sortedRules.length,
|
|
2062
|
+
ruleIds: sortedRules.map((r) => r.id),
|
|
2063
|
+
requestContext: {
|
|
2064
|
+
tool: request.toolName,
|
|
2065
|
+
arguments: Object.keys(request.arguments ?? {})
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1102
2068
|
};
|
|
1103
2069
|
}
|
|
1104
2070
|
function validatePolicyRule(input) {
|
|
1105
2071
|
const errors = [];
|
|
1106
2072
|
const warnings = [];
|
|
1107
|
-
const result =
|
|
2073
|
+
const result = PolicyRuleSchema2.safeParse(input);
|
|
1108
2074
|
if (!result.success) {
|
|
1109
2075
|
return {
|
|
1110
2076
|
valid: false,
|
|
@@ -1129,7 +2095,7 @@ function validatePolicyRule(input) {
|
|
|
1129
2095
|
function validatePolicySet(input) {
|
|
1130
2096
|
const errors = [];
|
|
1131
2097
|
const warnings = [];
|
|
1132
|
-
const result =
|
|
2098
|
+
const result = PolicySetSchema2.safeParse(input);
|
|
1133
2099
|
if (!result.success) {
|
|
1134
2100
|
return {
|
|
1135
2101
|
valid: false,
|
|
@@ -1223,7 +2189,7 @@ function createDefaultDenyPolicySet() {
|
|
|
1223
2189
|
effect: PolicyEffect.DENY,
|
|
1224
2190
|
priority: 1e4,
|
|
1225
2191
|
toolPattern: "*",
|
|
1226
|
-
permission:
|
|
2192
|
+
permission: Permission2.EXECUTE,
|
|
1227
2193
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
1228
2194
|
enabled: true,
|
|
1229
2195
|
createdAt: now,
|
|
@@ -1235,7 +2201,7 @@ function createDefaultDenyPolicySet() {
|
|
|
1235
2201
|
effect: PolicyEffect.DENY,
|
|
1236
2202
|
priority: 1e4,
|
|
1237
2203
|
toolPattern: "*",
|
|
1238
|
-
permission:
|
|
2204
|
+
permission: Permission2.WRITE,
|
|
1239
2205
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
1240
2206
|
enabled: true,
|
|
1241
2207
|
createdAt: now,
|
|
@@ -1247,7 +2213,7 @@ function createDefaultDenyPolicySet() {
|
|
|
1247
2213
|
effect: PolicyEffect.DENY,
|
|
1248
2214
|
priority: 1e4,
|
|
1249
2215
|
toolPattern: "*",
|
|
1250
|
-
permission:
|
|
2216
|
+
permission: Permission2.READ,
|
|
1251
2217
|
minimumTrustLevel: TrustLevel.UNTRUSTED,
|
|
1252
2218
|
enabled: true,
|
|
1253
2219
|
createdAt: now,
|
|
@@ -1417,9 +2383,6 @@ var PolicyStore = class {
|
|
|
1417
2383
|
return createHash("sha256").update(serialized).digest("hex");
|
|
1418
2384
|
}
|
|
1419
2385
|
};
|
|
1420
|
-
|
|
1421
|
-
// ../sdk-ts/dist/index.js
|
|
1422
|
-
import { randomUUID, createHmac } from "crypto";
|
|
1423
2386
|
var DEFAULT_CONFIG = Object.freeze({
|
|
1424
2387
|
validateSchemas: true,
|
|
1425
2388
|
enableLogging: true,
|
|
@@ -1429,7 +2392,7 @@ var DEFAULT_CONFIG = Object.freeze({
|
|
|
1429
2392
|
globalRateLimitPerMinute: 600,
|
|
1430
2393
|
rateLimitPerTool: 60,
|
|
1431
2394
|
tokenTtlSeconds: 30,
|
|
1432
|
-
inputGuardConfig:
|
|
2395
|
+
inputGuardConfig: DEFAULT_INPUT_GUARD_CONFIG2,
|
|
1433
2396
|
enableVersionedPolicies: true
|
|
1434
2397
|
});
|
|
1435
2398
|
function resolveConfig(userConfig) {
|
|
@@ -1462,7 +2425,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1462
2425
|
toolName: params.name,
|
|
1463
2426
|
serverName: "default",
|
|
1464
2427
|
arguments: params.arguments ?? {},
|
|
1465
|
-
requiredPermission:
|
|
2428
|
+
requiredPermission: Permission2.EXECUTE,
|
|
1466
2429
|
timestamp
|
|
1467
2430
|
};
|
|
1468
2431
|
if (options.rateLimiter) {
|
|
@@ -1501,7 +2464,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1501
2464
|
}
|
|
1502
2465
|
}
|
|
1503
2466
|
if (options.validateSchemas && params.arguments) {
|
|
1504
|
-
const guardConfig = options.inputGuardConfig ??
|
|
2467
|
+
const guardConfig = options.inputGuardConfig ?? DEFAULT_INPUT_GUARD_CONFIG2;
|
|
1505
2468
|
const sanitization = sanitizeInput("arguments", params.arguments, guardConfig);
|
|
1506
2469
|
if (!sanitization.safe) {
|
|
1507
2470
|
const threatDescriptions = sanitization.threats.map(
|
|
@@ -1514,7 +2477,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1514
2477
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1515
2478
|
};
|
|
1516
2479
|
options.onDecision?.(result);
|
|
1517
|
-
const reason = options.verboseErrors ? `Input validation failed: ${
|
|
2480
|
+
const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
|
|
1518
2481
|
return createDeniedToolResult(reason);
|
|
1519
2482
|
}
|
|
1520
2483
|
}
|
|
@@ -1534,7 +2497,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1534
2497
|
if (options.tokenIssuer) {
|
|
1535
2498
|
capabilityToken = options.tokenIssuer.issue(
|
|
1536
2499
|
requestId,
|
|
1537
|
-
[
|
|
2500
|
+
[Permission2.EXECUTE],
|
|
1538
2501
|
[params.name]
|
|
1539
2502
|
);
|
|
1540
2503
|
}
|
|
@@ -1832,6 +2795,25 @@ var RateLimiter = class {
|
|
|
1832
2795
|
const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
|
|
1833
2796
|
return { allowed, remaining, resetAt };
|
|
1834
2797
|
}
|
|
2798
|
+
/**
|
|
2799
|
+
* Atomically checks and records a tool call.
|
|
2800
|
+
* Prevents TOCTOU race conditions between check and record.
|
|
2801
|
+
* Returns the rate limit result; if allowed, the call is already recorded.
|
|
2802
|
+
*/
|
|
2803
|
+
checkAndRecord(toolName, limitPerWindow, globalLimit) {
|
|
2804
|
+
const result = this.checkLimit(toolName, limitPerWindow);
|
|
2805
|
+
if (!result.allowed) {
|
|
2806
|
+
return result;
|
|
2807
|
+
}
|
|
2808
|
+
if (globalLimit !== void 0) {
|
|
2809
|
+
const globalResult = this.checkGlobalLimit(globalLimit);
|
|
2810
|
+
if (!globalResult.allowed) {
|
|
2811
|
+
return globalResult;
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
this.recordCall(toolName);
|
|
2815
|
+
return result;
|
|
2816
|
+
}
|
|
1835
2817
|
/**
|
|
1836
2818
|
* Records a tool call for rate limiting.
|
|
1837
2819
|
* Call this after successful execution.
|
|
@@ -1887,6 +2869,16 @@ var RateLimiter = class {
|
|
|
1887
2869
|
return active;
|
|
1888
2870
|
}
|
|
1889
2871
|
};
|
|
2872
|
+
var LicenseError = class extends Error {
|
|
2873
|
+
constructor(message) {
|
|
2874
|
+
super(
|
|
2875
|
+
`${message}
|
|
2876
|
+
Get your API key at https://solongate.com
|
|
2877
|
+
Usage: new SolonGate({ name: '...', apiKey: 'sg_live_xxx' })`
|
|
2878
|
+
);
|
|
2879
|
+
this.name = "LicenseError";
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
1890
2882
|
var SolonGate = class {
|
|
1891
2883
|
policyEngine;
|
|
1892
2884
|
config;
|
|
@@ -1895,7 +2887,19 @@ var SolonGate = class {
|
|
|
1895
2887
|
tokenIssuer;
|
|
1896
2888
|
serverVerifier;
|
|
1897
2889
|
rateLimiter;
|
|
2890
|
+
apiKey;
|
|
2891
|
+
licenseValidated = false;
|
|
1898
2892
|
constructor(options) {
|
|
2893
|
+
const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
2894
|
+
if (!apiKey) {
|
|
2895
|
+
throw new LicenseError("A valid SolonGate API key is required.");
|
|
2896
|
+
}
|
|
2897
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
2898
|
+
throw new LicenseError(
|
|
2899
|
+
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'."
|
|
2900
|
+
);
|
|
2901
|
+
}
|
|
2902
|
+
this.apiKey = apiKey;
|
|
1899
2903
|
const { config, warnings } = resolveConfig(options.config);
|
|
1900
2904
|
this.config = config;
|
|
1901
2905
|
this.configWarnings = warnings;
|
|
@@ -1921,12 +2925,47 @@ var SolonGate = class {
|
|
|
1921
2925
|
this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
|
|
1922
2926
|
this.rateLimiter = new RateLimiter();
|
|
1923
2927
|
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Validate the API key against the SolonGate cloud API.
|
|
2930
|
+
* Called once on first executeToolCall. Throws LicenseError if invalid.
|
|
2931
|
+
* Test keys (sg_test_) skip online validation.
|
|
2932
|
+
*/
|
|
2933
|
+
async validateLicense() {
|
|
2934
|
+
if (this.licenseValidated) return;
|
|
2935
|
+
if (this.apiKey.startsWith("sg_test_")) {
|
|
2936
|
+
this.licenseValidated = true;
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2940
|
+
try {
|
|
2941
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
2942
|
+
headers: {
|
|
2943
|
+
"X-API-Key": this.apiKey,
|
|
2944
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2945
|
+
},
|
|
2946
|
+
signal: AbortSignal.timeout(1e4)
|
|
2947
|
+
});
|
|
2948
|
+
if (res.status === 401) {
|
|
2949
|
+
throw new LicenseError("Invalid or expired API key.");
|
|
2950
|
+
}
|
|
2951
|
+
if (res.status === 403) {
|
|
2952
|
+
throw new LicenseError("Your subscription is inactive. Renew at https://solongate.com");
|
|
2953
|
+
}
|
|
2954
|
+
this.licenseValidated = true;
|
|
2955
|
+
} catch (err) {
|
|
2956
|
+
if (err instanceof LicenseError) throw err;
|
|
2957
|
+
throw new LicenseError(
|
|
2958
|
+
"Unable to reach SolonGate license server. Check your internet connection."
|
|
2959
|
+
);
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
1924
2962
|
/**
|
|
1925
2963
|
* Intercept and evaluate a tool call against the full security pipeline.
|
|
1926
2964
|
* If denied at any stage, returns an error result without calling upstream.
|
|
1927
2965
|
* If allowed, calls upstream and returns the result.
|
|
1928
2966
|
*/
|
|
1929
2967
|
async executeToolCall(params, upstreamCall) {
|
|
2968
|
+
await this.validateLicense();
|
|
1930
2969
|
return interceptToolCall(params, upstreamCall, {
|
|
1931
2970
|
policyEngine: this.policyEngine,
|
|
1932
2971
|
validateSchemas: this.config.validateSchemas,
|
|
@@ -1976,8 +3015,8 @@ var Mutex = class {
|
|
|
1976
3015
|
this.locked = true;
|
|
1977
3016
|
return;
|
|
1978
3017
|
}
|
|
1979
|
-
return new Promise((
|
|
1980
|
-
this.queue.push(
|
|
3018
|
+
return new Promise((resolve5) => {
|
|
3019
|
+
this.queue.push(resolve5);
|
|
1981
3020
|
});
|
|
1982
3021
|
}
|
|
1983
3022
|
release() {
|
|
@@ -2018,9 +3057,31 @@ var SolonGateProxy = class {
|
|
|
2018
3057
|
*/
|
|
2019
3058
|
async start() {
|
|
2020
3059
|
log("Starting SolonGate Proxy...");
|
|
3060
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2021
3061
|
if (this.config.apiKey) {
|
|
2022
|
-
|
|
2023
|
-
|
|
3062
|
+
log(`Validating license with ${apiUrl}...`);
|
|
3063
|
+
try {
|
|
3064
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
3065
|
+
headers: {
|
|
3066
|
+
"X-API-Key": this.config.apiKey,
|
|
3067
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
3068
|
+
},
|
|
3069
|
+
signal: AbortSignal.timeout(1e4)
|
|
3070
|
+
});
|
|
3071
|
+
if (res.status === 401) {
|
|
3072
|
+
log("ERROR: Invalid or expired API key.");
|
|
3073
|
+
process.exit(1);
|
|
3074
|
+
}
|
|
3075
|
+
if (res.status === 403) {
|
|
3076
|
+
log("ERROR: Your subscription is inactive. Renew at https://solongate.com");
|
|
3077
|
+
process.exit(1);
|
|
3078
|
+
}
|
|
3079
|
+
log("License validated.");
|
|
3080
|
+
} catch (err) {
|
|
3081
|
+
log(`ERROR: Unable to reach SolonGate license server. Check your internet connection.`);
|
|
3082
|
+
log(`Details: ${err instanceof Error ? err.message : String(err)}`);
|
|
3083
|
+
process.exit(1);
|
|
3084
|
+
}
|
|
2024
3085
|
try {
|
|
2025
3086
|
const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
|
|
2026
3087
|
this.config.policy = cloudPolicy;
|
|
@@ -2030,29 +3091,54 @@ var SolonGateProxy = class {
|
|
|
2030
3091
|
}
|
|
2031
3092
|
}
|
|
2032
3093
|
log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
|
|
2033
|
-
|
|
3094
|
+
const transport = this.config.upstream.transport ?? "stdio";
|
|
3095
|
+
if (transport === "stdio") {
|
|
3096
|
+
log(`Upstream: [stdio] ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
|
|
3097
|
+
} else {
|
|
3098
|
+
log(`Upstream: [${transport}] ${this.config.upstream.url}`);
|
|
3099
|
+
}
|
|
2034
3100
|
await this.connectUpstream();
|
|
2035
3101
|
await this.discoverTools();
|
|
2036
3102
|
this.createServer();
|
|
2037
3103
|
await this.serve();
|
|
2038
3104
|
}
|
|
2039
3105
|
/**
|
|
2040
|
-
* Connect to the upstream MCP server
|
|
3106
|
+
* Connect to the upstream MCP server.
|
|
3107
|
+
* Supports stdio (child process), SSE, and StreamableHTTP transports.
|
|
2041
3108
|
*/
|
|
2042
3109
|
async connectUpstream() {
|
|
2043
3110
|
this.client = new Client(
|
|
2044
3111
|
{ name: "solongate-proxy-client", version: "0.1.0" },
|
|
2045
3112
|
{ capabilities: {} }
|
|
2046
3113
|
);
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
3114
|
+
const upstreamTransport = this.config.upstream.transport ?? "stdio";
|
|
3115
|
+
switch (upstreamTransport) {
|
|
3116
|
+
case "sse": {
|
|
3117
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for SSE transport");
|
|
3118
|
+
const transport = new SSEClientTransport(new URL(this.config.upstream.url));
|
|
3119
|
+
await this.client.connect(transport);
|
|
3120
|
+
break;
|
|
3121
|
+
}
|
|
3122
|
+
case "http": {
|
|
3123
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for HTTP transport");
|
|
3124
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.config.upstream.url));
|
|
3125
|
+
await this.client.connect(transport);
|
|
3126
|
+
break;
|
|
3127
|
+
}
|
|
3128
|
+
case "stdio":
|
|
3129
|
+
default: {
|
|
3130
|
+
const transport = new StdioClientTransport({
|
|
3131
|
+
command: this.config.upstream.command,
|
|
3132
|
+
args: this.config.upstream.args,
|
|
3133
|
+
env: this.config.upstream.env,
|
|
3134
|
+
cwd: this.config.upstream.cwd,
|
|
3135
|
+
stderr: "pipe"
|
|
3136
|
+
});
|
|
3137
|
+
await this.client.connect(transport);
|
|
3138
|
+
break;
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
log(`Connected to upstream server (${upstreamTransport})`);
|
|
2056
3142
|
}
|
|
2057
3143
|
/**
|
|
2058
3144
|
* Discover tools from the upstream server.
|
|
@@ -2091,8 +3177,17 @@ var SolonGateProxy = class {
|
|
|
2091
3177
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2092
3178
|
return { tools: this.upstreamTools };
|
|
2093
3179
|
});
|
|
3180
|
+
const MAX_ARGUMENT_SIZE = 1024 * 1024;
|
|
2094
3181
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2095
3182
|
const { name, arguments: args } = request.params;
|
|
3183
|
+
const argsSize = JSON.stringify(args ?? {}).length;
|
|
3184
|
+
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
3185
|
+
log(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
3186
|
+
return {
|
|
3187
|
+
content: [{ type: "text", text: `Request payload too large (${Math.round(argsSize / 1024)}KB > ${Math.round(MAX_ARGUMENT_SIZE / 1024)}KB limit)` }],
|
|
3188
|
+
isError: true
|
|
3189
|
+
};
|
|
3190
|
+
}
|
|
2096
3191
|
log(`Tool call: ${name}`);
|
|
2097
3192
|
await this.callMutex.acquire();
|
|
2098
3193
|
const startTime = Date.now();
|
|
@@ -2166,14 +3261,38 @@ var SolonGateProxy = class {
|
|
|
2166
3261
|
});
|
|
2167
3262
|
}
|
|
2168
3263
|
/**
|
|
2169
|
-
* Start serving
|
|
3264
|
+
* Start serving downstream.
|
|
3265
|
+
* If --port is set, serves via StreamableHTTP on that port.
|
|
3266
|
+
* Otherwise, serves on stdio (default for Claude Code / Cursor / etc).
|
|
2170
3267
|
*/
|
|
2171
3268
|
async serve() {
|
|
2172
3269
|
if (!this.server) throw new Error("Server not created");
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
3270
|
+
if (this.config.port) {
|
|
3271
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
3272
|
+
sessionIdGenerator: () => crypto.randomUUID()
|
|
3273
|
+
});
|
|
3274
|
+
await this.server.connect(httpTransport);
|
|
3275
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
3276
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
3277
|
+
await httpTransport.handleRequest(req, res);
|
|
3278
|
+
} else if (req.url === "/health") {
|
|
3279
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3280
|
+
res.end(JSON.stringify({ status: "healthy", proxy: this.config.name ?? "solongate-proxy" }));
|
|
3281
|
+
} else {
|
|
3282
|
+
res.writeHead(404);
|
|
3283
|
+
res.end("Not found. Use /mcp for MCP protocol or /health for health check.");
|
|
3284
|
+
}
|
|
3285
|
+
});
|
|
3286
|
+
httpServer.listen(this.config.port, () => {
|
|
3287
|
+
log(`Proxy is live on http://localhost:${this.config.port}/mcp`);
|
|
3288
|
+
log("All tool calls are now protected by SolonGate.");
|
|
3289
|
+
});
|
|
3290
|
+
} else {
|
|
3291
|
+
const transport = new StdioServerTransport();
|
|
3292
|
+
await this.server.connect(transport);
|
|
3293
|
+
log("Proxy is live. All tool calls are now protected by SolonGate.");
|
|
3294
|
+
log("Waiting for requests...");
|
|
3295
|
+
}
|
|
2177
3296
|
}
|
|
2178
3297
|
};
|
|
2179
3298
|
|
|
@@ -2190,13 +3309,23 @@ console.error = (...args) => {
|
|
|
2190
3309
|
process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
|
|
2191
3310
|
`);
|
|
2192
3311
|
};
|
|
2193
|
-
async function
|
|
3312
|
+
async function main4() {
|
|
2194
3313
|
const subcommand = process.argv[2];
|
|
2195
3314
|
if (subcommand === "init") {
|
|
2196
3315
|
process.argv.splice(2, 1);
|
|
2197
3316
|
await Promise.resolve().then(() => (init_init(), init_exports));
|
|
2198
3317
|
return;
|
|
2199
3318
|
}
|
|
3319
|
+
if (subcommand === "inject") {
|
|
3320
|
+
process.argv.splice(2, 1);
|
|
3321
|
+
await Promise.resolve().then(() => (init_inject(), inject_exports));
|
|
3322
|
+
return;
|
|
3323
|
+
}
|
|
3324
|
+
if (subcommand === "create") {
|
|
3325
|
+
process.argv.splice(2, 1);
|
|
3326
|
+
await Promise.resolve().then(() => (init_create(), create_exports));
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
2200
3329
|
try {
|
|
2201
3330
|
const config = parseArgs(process.argv);
|
|
2202
3331
|
const proxy = new SolonGateProxy(config);
|
|
@@ -2208,4 +3337,4 @@ async function main2() {
|
|
|
2208
3337
|
process.exit(1);
|
|
2209
3338
|
}
|
|
2210
3339
|
}
|
|
2211
|
-
|
|
3340
|
+
main4();
|