@solongate/proxy 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create.js +247 -0
- package/dist/index.js +1388 -380
- package/dist/inject.js +339 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -285,315 +285,601 @@ var init_init = __esm({
|
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
-
// src/
|
|
289
|
-
|
|
290
|
-
import {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
300
|
-
const data = await res.json();
|
|
301
|
-
return {
|
|
302
|
-
id: String(data.id ?? "cloud"),
|
|
303
|
-
name: String(data.name ?? "Cloud Policy"),
|
|
304
|
-
version: Number(data._version ?? 1),
|
|
305
|
-
rules: data.rules ?? [],
|
|
306
|
-
createdAt: String(data._created_at ?? ""),
|
|
307
|
-
updatedAt: ""
|
|
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
|
|
308
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;
|
|
309
321
|
}
|
|
310
|
-
|
|
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;
|
|
311
352
|
try {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
"Authorization": `Bearer ${apiKey}`,
|
|
316
|
-
"Content-Type": "application/json"
|
|
317
|
-
},
|
|
318
|
-
body: JSON.stringify(entry)
|
|
319
|
-
});
|
|
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"]);
|
|
320
356
|
} catch {
|
|
357
|
+
return false;
|
|
321
358
|
}
|
|
322
359
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
description: "Block shell execution",
|
|
333
|
-
effect: "DENY",
|
|
334
|
-
priority: 100,
|
|
335
|
-
toolPattern: "*shell*",
|
|
336
|
-
permission: "EXECUTE",
|
|
337
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
338
|
-
enabled: true,
|
|
339
|
-
createdAt: "",
|
|
340
|
-
updatedAt: ""
|
|
341
|
-
},
|
|
342
|
-
{
|
|
343
|
-
id: "deny-exec",
|
|
344
|
-
description: "Block command execution",
|
|
345
|
-
effect: "DENY",
|
|
346
|
-
priority: 101,
|
|
347
|
-
toolPattern: "*exec*",
|
|
348
|
-
permission: "EXECUTE",
|
|
349
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
350
|
-
enabled: true,
|
|
351
|
-
createdAt: "",
|
|
352
|
-
updatedAt: ""
|
|
353
|
-
},
|
|
354
|
-
{
|
|
355
|
-
id: "deny-eval",
|
|
356
|
-
description: "Block code eval",
|
|
357
|
-
effect: "DENY",
|
|
358
|
-
priority: 102,
|
|
359
|
-
toolPattern: "*eval*",
|
|
360
|
-
permission: "EXECUTE",
|
|
361
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
362
|
-
enabled: true,
|
|
363
|
-
createdAt: "",
|
|
364
|
-
updatedAt: ""
|
|
365
|
-
},
|
|
366
|
-
{
|
|
367
|
-
id: "allow-rest",
|
|
368
|
-
description: "Allow all other tools",
|
|
369
|
-
effect: "ALLOW",
|
|
370
|
-
priority: 1e3,
|
|
371
|
-
toolPattern: "*",
|
|
372
|
-
permission: "EXECUTE",
|
|
373
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
374
|
-
enabled: true,
|
|
375
|
-
createdAt: "",
|
|
376
|
-
updatedAt: ""
|
|
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);
|
|
377
369
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
description: "Allow list tools",
|
|
403
|
-
effect: "ALLOW",
|
|
404
|
-
priority: 101,
|
|
405
|
-
toolPattern: "*list*",
|
|
406
|
-
permission: "EXECUTE",
|
|
407
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
408
|
-
enabled: true,
|
|
409
|
-
createdAt: "",
|
|
410
|
-
updatedAt: ""
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
id: "allow-get",
|
|
414
|
-
description: "Allow get tools",
|
|
415
|
-
effect: "ALLOW",
|
|
416
|
-
priority: 102,
|
|
417
|
-
toolPattern: "*get*",
|
|
418
|
-
permission: "EXECUTE",
|
|
419
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
420
|
-
enabled: true,
|
|
421
|
-
createdAt: "",
|
|
422
|
-
updatedAt: ""
|
|
423
|
-
},
|
|
424
|
-
{
|
|
425
|
-
id: "allow-search",
|
|
426
|
-
description: "Allow search tools",
|
|
427
|
-
effect: "ALLOW",
|
|
428
|
-
priority: 103,
|
|
429
|
-
toolPattern: "*search*",
|
|
430
|
-
permission: "EXECUTE",
|
|
431
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
432
|
-
enabled: true,
|
|
433
|
-
createdAt: "",
|
|
434
|
-
updatedAt: ""
|
|
435
|
-
},
|
|
436
|
-
{
|
|
437
|
-
id: "allow-query",
|
|
438
|
-
description: "Allow query tools",
|
|
439
|
-
effect: "ALLOW",
|
|
440
|
-
priority: 104,
|
|
441
|
-
toolPattern: "*query*",
|
|
442
|
-
permission: "EXECUTE",
|
|
443
|
-
minimumTrustLevel: "UNTRUSTED",
|
|
444
|
-
enabled: true,
|
|
445
|
-
createdAt: "",
|
|
446
|
-
updatedAt: ""
|
|
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 {
|
|
447
394
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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");
|
|
469
464
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
},
|
|
474
|
-
"deny-all": {
|
|
475
|
-
id: "deny-all",
|
|
476
|
-
name: "Deny All",
|
|
477
|
-
description: "Blocks all tool calls",
|
|
478
|
-
version: 1,
|
|
479
|
-
rules: [],
|
|
480
|
-
createdAt: "",
|
|
481
|
-
updatedAt: ""
|
|
465
|
+
importReplaced = true;
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
482
468
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (existsSync(filePath)) {
|
|
489
|
-
const content = readFileSync(filePath, "utf-8");
|
|
490
|
-
return JSON.parse(content);
|
|
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");
|
|
491
474
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
)
|
|
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 };
|
|
495
482
|
}
|
|
496
|
-
function
|
|
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) {
|
|
497
641
|
const args = argv.slice(2);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
let
|
|
504
|
-
|
|
505
|
-
let apiKey;
|
|
506
|
-
let apiUrl;
|
|
507
|
-
let separatorIndex = args.indexOf("--");
|
|
508
|
-
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
509
|
-
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
510
|
-
for (let i = 0; i < flags.length; i++) {
|
|
511
|
-
switch (flags[i]) {
|
|
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]) {
|
|
512
649
|
case "--policy":
|
|
513
|
-
|
|
514
|
-
break;
|
|
515
|
-
case "--name":
|
|
516
|
-
name = flags[++i];
|
|
650
|
+
opts.policy = args[++i];
|
|
517
651
|
break;
|
|
518
|
-
case "--
|
|
519
|
-
|
|
520
|
-
break;
|
|
521
|
-
case "--no-input-guard":
|
|
522
|
-
validateInput = false;
|
|
523
|
-
break;
|
|
524
|
-
case "--rate-limit":
|
|
525
|
-
rateLimitPerTool = parseInt(flags[++i], 10);
|
|
526
|
-
break;
|
|
527
|
-
case "--global-rate-limit":
|
|
528
|
-
globalRateLimit = parseInt(flags[++i], 10);
|
|
529
|
-
break;
|
|
530
|
-
case "--config":
|
|
531
|
-
configFile = flags[++i];
|
|
532
|
-
break;
|
|
533
|
-
case "--api-key":
|
|
534
|
-
apiKey = flags[++i];
|
|
652
|
+
case "--no-install":
|
|
653
|
+
opts.noInstall = true;
|
|
535
654
|
break;
|
|
536
|
-
case "--
|
|
537
|
-
|
|
655
|
+
case "--help":
|
|
656
|
+
case "-h":
|
|
657
|
+
printHelp3();
|
|
658
|
+
process.exit(0);
|
|
538
659
|
break;
|
|
660
|
+
default:
|
|
661
|
+
if (!args[i].startsWith("-") && !opts.name) {
|
|
662
|
+
opts.name = args[i];
|
|
663
|
+
}
|
|
539
664
|
}
|
|
540
665
|
}
|
|
541
|
-
if (
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
name: fileConfig.name ?? name,
|
|
552
|
-
verbose: fileConfig.verbose ?? verbose,
|
|
553
|
-
validateInput: fileConfig.validateInput ?? validateInput,
|
|
554
|
-
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
555
|
-
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
556
|
-
apiKey: apiKey ?? fileConfig.apiKey,
|
|
557
|
-
apiUrl: apiUrl ?? fileConfig.apiUrl
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
if (upstreamArgs.length === 0) {
|
|
561
|
-
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"
|
|
563
|
-
);
|
|
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);
|
|
564
676
|
}
|
|
565
|
-
|
|
566
|
-
return {
|
|
567
|
-
upstream: {
|
|
568
|
-
command,
|
|
569
|
-
args: commandArgs,
|
|
570
|
-
env: { ...process.env }
|
|
571
|
-
},
|
|
572
|
-
policy: loadPolicy(policySource),
|
|
573
|
-
name,
|
|
574
|
-
verbose,
|
|
575
|
-
validateInput,
|
|
576
|
-
rateLimitPerTool,
|
|
577
|
-
globalRateLimit,
|
|
578
|
-
apiKey,
|
|
579
|
-
apiUrl
|
|
580
|
-
};
|
|
677
|
+
return opts;
|
|
581
678
|
}
|
|
679
|
+
function printHelp3() {
|
|
680
|
+
log3(`
|
|
681
|
+
SolonGate Create \u2014 Scaffold a secure MCP server in seconds
|
|
582
682
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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 installDeps(dir) {
|
|
824
|
+
log3(" Installing dependencies with npm...");
|
|
825
|
+
try {
|
|
826
|
+
execSync2("npm install", { cwd: dir, stdio: "pipe" });
|
|
827
|
+
} catch {
|
|
828
|
+
log3(" npm install failed \u2014 run it manually.");
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
async function main3() {
|
|
832
|
+
const opts = parseCreateArgs(process.argv);
|
|
833
|
+
const dir = resolve4(opts.name);
|
|
834
|
+
log3("");
|
|
835
|
+
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");
|
|
836
|
+
log3(" \u2551 SolonGate \u2014 Create MCP Server \u2551");
|
|
837
|
+
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");
|
|
838
|
+
log3("");
|
|
839
|
+
if (existsSync4(dir)) {
|
|
840
|
+
log3(` Error: Directory "${opts.name}" already exists.`);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
mkdirSync(dir, { recursive: true });
|
|
844
|
+
log3(` Project: ${opts.name}`);
|
|
845
|
+
log3(` Language: TypeScript`);
|
|
846
|
+
log3(` Policy: ${opts.policy}`);
|
|
847
|
+
log3("");
|
|
848
|
+
createProject(dir, opts.name, opts.policy);
|
|
849
|
+
log3(" Files created:");
|
|
850
|
+
log3(" package.json");
|
|
851
|
+
log3(" tsconfig.json");
|
|
852
|
+
log3(" src/index.ts");
|
|
853
|
+
log3(" .mcp.json");
|
|
854
|
+
log3(" .gitignore");
|
|
855
|
+
log3("");
|
|
856
|
+
if (!opts.noInstall) {
|
|
857
|
+
installDeps(dir);
|
|
858
|
+
log3("");
|
|
859
|
+
}
|
|
860
|
+
log3(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
|
|
861
|
+
log3(" \u2502 Project created! \u2502");
|
|
862
|
+
log3(" \u2502 \u2502");
|
|
863
|
+
log3(` \u2502 cd ${opts.name.padEnd(39)}\u2502`);
|
|
864
|
+
log3(" \u2502 \u2502");
|
|
865
|
+
log3(" \u2502 npm run build # Build \u2502");
|
|
866
|
+
log3(" \u2502 npm run dev # Dev mode (tsx) \u2502");
|
|
867
|
+
log3(" \u2502 npm start # Run built server \u2502");
|
|
868
|
+
log3(" \u2502 \u2502");
|
|
869
|
+
log3(" \u2502 Set your API key: \u2502");
|
|
870
|
+
log3(" \u2502 export SOLONGATE_API_KEY=sg_live_xxx \u2502");
|
|
871
|
+
log3(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
|
|
872
|
+
log3("");
|
|
873
|
+
}
|
|
874
|
+
var init_create = __esm({
|
|
875
|
+
"src/create.ts"() {
|
|
876
|
+
"use strict";
|
|
877
|
+
main3().catch((err) => {
|
|
878
|
+
log3(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
|
|
879
|
+
process.exit(1);
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
});
|
|
597
883
|
|
|
598
884
|
// ../core/dist/index.js
|
|
599
885
|
import { z } from "zod";
|
|
@@ -746,7 +1032,9 @@ var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
|
|
|
746
1032
|
shellInjection: true,
|
|
747
1033
|
wildcardAbuse: true,
|
|
748
1034
|
lengthLimit: 4096,
|
|
749
|
-
entropyLimit: true
|
|
1035
|
+
entropyLimit: true,
|
|
1036
|
+
ssrf: true,
|
|
1037
|
+
sqlInjection: true
|
|
750
1038
|
});
|
|
751
1039
|
var PATH_TRAVERSAL_PATTERNS = [
|
|
752
1040
|
/\.\.\//,
|
|
@@ -772,7 +1060,23 @@ var SENSITIVE_PATHS = [
|
|
|
772
1060
|
/c:\\windows\\system32/i,
|
|
773
1061
|
/c:\\windows\\syswow64/i,
|
|
774
1062
|
/\/root\//i,
|
|
775
|
-
|
|
1063
|
+
/~\//,
|
|
1064
|
+
/\.env(\.|$)/i,
|
|
1065
|
+
// .env, .env.local, .env.production
|
|
1066
|
+
/\.aws\/credentials/i,
|
|
1067
|
+
// AWS credentials
|
|
1068
|
+
/\.ssh\/id_/i,
|
|
1069
|
+
// SSH keys
|
|
1070
|
+
/\.kube\/config/i,
|
|
1071
|
+
// Kubernetes config
|
|
1072
|
+
/wp-config\.php/i,
|
|
1073
|
+
// WordPress config
|
|
1074
|
+
/\.git\/config/i,
|
|
1075
|
+
// Git config
|
|
1076
|
+
/\.npmrc/i,
|
|
1077
|
+
// npm credentials
|
|
1078
|
+
/\.pypirc/i
|
|
1079
|
+
// PyPI credentials
|
|
776
1080
|
];
|
|
777
1081
|
function detectPathTraversal(value) {
|
|
778
1082
|
for (const pattern of PATH_TRAVERSAL_PATTERNS) {
|
|
@@ -802,8 +1106,18 @@ var SHELL_INJECTION_PATTERNS = [
|
|
|
802
1106
|
// eval command
|
|
803
1107
|
/\bexec\b/i,
|
|
804
1108
|
// exec command
|
|
805
|
-
/\bsystem\b/i
|
|
1109
|
+
/\bsystem\b/i,
|
|
806
1110
|
// system call
|
|
1111
|
+
/%0a/i,
|
|
1112
|
+
// URL-encoded newline
|
|
1113
|
+
/%0d/i,
|
|
1114
|
+
// URL-encoded carriage return
|
|
1115
|
+
/%09/i,
|
|
1116
|
+
// URL-encoded tab
|
|
1117
|
+
/\r\n/,
|
|
1118
|
+
// CRLF injection
|
|
1119
|
+
/\n/
|
|
1120
|
+
// Newline (command separator on Unix)
|
|
807
1121
|
];
|
|
808
1122
|
function detectShellInjection(value) {
|
|
809
1123
|
for (const pattern of SHELL_INJECTION_PATTERNS) {
|
|
@@ -818,6 +1132,91 @@ function detectWildcardAbuse(value) {
|
|
|
818
1132
|
if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
|
|
819
1133
|
return false;
|
|
820
1134
|
}
|
|
1135
|
+
var SSRF_PATTERNS = [
|
|
1136
|
+
/^https?:\/\/localhost\b/i,
|
|
1137
|
+
/^https?:\/\/127\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1138
|
+
/^https?:\/\/0\.0\.0\.0/,
|
|
1139
|
+
/^https?:\/\/\[::1\]/,
|
|
1140
|
+
// IPv6 loopback
|
|
1141
|
+
/^https?:\/\/10\.\d{1,3}\.\d{1,3}\.\d{1,3}/,
|
|
1142
|
+
// 10.x.x.x
|
|
1143
|
+
/^https?:\/\/172\.(1[6-9]|2\d|3[01])\./,
|
|
1144
|
+
// 172.16-31.x.x
|
|
1145
|
+
/^https?:\/\/192\.168\./,
|
|
1146
|
+
// 192.168.x.x
|
|
1147
|
+
/^https?:\/\/169\.254\./,
|
|
1148
|
+
// Link-local / AWS metadata
|
|
1149
|
+
/metadata\.google\.internal/i,
|
|
1150
|
+
// GCP metadata
|
|
1151
|
+
/^https?:\/\/metadata\b/i,
|
|
1152
|
+
// Generic metadata endpoint
|
|
1153
|
+
// IPv6 bypass patterns
|
|
1154
|
+
/^https?:\/\/\[fe80:/i,
|
|
1155
|
+
// IPv6 link-local
|
|
1156
|
+
/^https?:\/\/\[fc00:/i,
|
|
1157
|
+
// IPv6 unique local
|
|
1158
|
+
/^https?:\/\/\[fd[0-9a-f]{2}:/i,
|
|
1159
|
+
// IPv6 unique local (fd00::/8)
|
|
1160
|
+
/^https?:\/\/\[::ffff:127\./i,
|
|
1161
|
+
// IPv4-mapped IPv6 loopback
|
|
1162
|
+
/^https?:\/\/\[::ffff:10\./i,
|
|
1163
|
+
// IPv4-mapped IPv6 private
|
|
1164
|
+
/^https?:\/\/\[::ffff:172\.(1[6-9]|2\d|3[01])\./i,
|
|
1165
|
+
// IPv4-mapped IPv6 private
|
|
1166
|
+
/^https?:\/\/\[::ffff:192\.168\./i,
|
|
1167
|
+
// IPv4-mapped IPv6 private
|
|
1168
|
+
/^https?:\/\/\[::ffff:169\.254\./i,
|
|
1169
|
+
// IPv4-mapped IPv6 link-local
|
|
1170
|
+
// Hex IP bypass (e.g., 0x7f000001 = 127.0.0.1)
|
|
1171
|
+
/^https?:\/\/0x[0-9a-f]+\b/i,
|
|
1172
|
+
// Octal IP bypass (e.g., 0177.0.0.1 = 127.0.0.1)
|
|
1173
|
+
/^https?:\/\/0[0-7]{1,3}\./
|
|
1174
|
+
];
|
|
1175
|
+
function detectDecimalIP(value) {
|
|
1176
|
+
const match = value.match(/^https?:\/\/(\d{8,10})(?:[:/]|$)/);
|
|
1177
|
+
if (!match || !match[1]) return false;
|
|
1178
|
+
const decimal = parseInt(match[1], 10);
|
|
1179
|
+
if (isNaN(decimal) || decimal > 4294967295) return false;
|
|
1180
|
+
return decimal >= 2130706432 && decimal <= 2147483647 || // 127.0.0.0/8
|
|
1181
|
+
decimal >= 167772160 && decimal <= 184549375 || // 10.0.0.0/8
|
|
1182
|
+
decimal >= 2886729728 && decimal <= 2887778303 || // 172.16.0.0/12
|
|
1183
|
+
decimal >= 3232235520 && decimal <= 3232301055 || // 192.168.0.0/16
|
|
1184
|
+
decimal >= 2851995648 && decimal <= 2852061183 || // 169.254.0.0/16
|
|
1185
|
+
decimal === 0;
|
|
1186
|
+
}
|
|
1187
|
+
function detectSSRF(value) {
|
|
1188
|
+
for (const pattern of SSRF_PATTERNS) {
|
|
1189
|
+
if (pattern.test(value)) return true;
|
|
1190
|
+
}
|
|
1191
|
+
if (detectDecimalIP(value)) return true;
|
|
1192
|
+
return false;
|
|
1193
|
+
}
|
|
1194
|
+
var SQL_INJECTION_PATTERNS = [
|
|
1195
|
+
/'\s{0,20}(OR|AND)\s{0,20}'.{0,200}'/i,
|
|
1196
|
+
// ' OR '1'='1 — bounded to prevent ReDoS
|
|
1197
|
+
/'\s{0,10};\s{0,10}(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE|EXEC)/i,
|
|
1198
|
+
// '; DROP TABLE
|
|
1199
|
+
/UNION\s+(ALL\s+)?SELECT/i,
|
|
1200
|
+
// UNION SELECT
|
|
1201
|
+
/--\s*$/m,
|
|
1202
|
+
// SQL comment at end of line
|
|
1203
|
+
/\/\*.{0,500}?\*\//,
|
|
1204
|
+
// SQL block comment — bounded + non-greedy
|
|
1205
|
+
/\bSLEEP\s*\(/i,
|
|
1206
|
+
// Time-based injection
|
|
1207
|
+
/\bBENCHMARK\s*\(/i,
|
|
1208
|
+
// MySQL benchmark
|
|
1209
|
+
/\bWAITFOR\s+DELAY/i,
|
|
1210
|
+
// MSSQL delay
|
|
1211
|
+
/\b(LOAD_FILE|INTO\s+OUTFILE|INTO\s+DUMPFILE)\b/i
|
|
1212
|
+
// File operations
|
|
1213
|
+
];
|
|
1214
|
+
function detectSQLInjection(value) {
|
|
1215
|
+
for (const pattern of SQL_INJECTION_PATTERNS) {
|
|
1216
|
+
if (pattern.test(value)) return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
821
1220
|
function checkLengthLimits(value, maxLength = 4096) {
|
|
822
1221
|
return value.length <= maxLength;
|
|
823
1222
|
}
|
|
@@ -841,79 +1240,470 @@ function calculateShannonEntropy(str) {
|
|
|
841
1240
|
entropy -= p * Math.log2(p);
|
|
842
1241
|
}
|
|
843
1242
|
}
|
|
844
|
-
return entropy;
|
|
1243
|
+
return entropy;
|
|
1244
|
+
}
|
|
1245
|
+
function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
|
|
1246
|
+
const threats = [];
|
|
1247
|
+
if (typeof value !== "string") {
|
|
1248
|
+
if (typeof value === "object" && value !== null) {
|
|
1249
|
+
return sanitizeObject(field, value, config);
|
|
1250
|
+
}
|
|
1251
|
+
return { safe: true, threats: [] };
|
|
1252
|
+
}
|
|
1253
|
+
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
1254
|
+
threats.push({
|
|
1255
|
+
type: "PATH_TRAVERSAL",
|
|
1256
|
+
field,
|
|
1257
|
+
value: truncate(value, 100),
|
|
1258
|
+
description: "Path traversal pattern detected"
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
if (config.shellInjection && detectShellInjection(value)) {
|
|
1262
|
+
threats.push({
|
|
1263
|
+
type: "SHELL_INJECTION",
|
|
1264
|
+
field,
|
|
1265
|
+
value: truncate(value, 100),
|
|
1266
|
+
description: "Shell injection pattern detected"
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
1270
|
+
threats.push({
|
|
1271
|
+
type: "WILDCARD_ABUSE",
|
|
1272
|
+
field,
|
|
1273
|
+
value: truncate(value, 100),
|
|
1274
|
+
description: "Wildcard abuse pattern detected"
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
if (!checkLengthLimits(value, config.lengthLimit)) {
|
|
1278
|
+
threats.push({
|
|
1279
|
+
type: "LENGTH_EXCEEDED",
|
|
1280
|
+
field,
|
|
1281
|
+
value: `[${value.length} chars]`,
|
|
1282
|
+
description: `Value exceeds maximum length of ${config.lengthLimit}`
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
if (config.entropyLimit && !checkEntropyLimits(value)) {
|
|
1286
|
+
threats.push({
|
|
1287
|
+
type: "HIGH_ENTROPY",
|
|
1288
|
+
field,
|
|
1289
|
+
value: truncate(value, 100),
|
|
1290
|
+
description: "High entropy string detected - possible encoded payload"
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
if (config.ssrf && detectSSRF(value)) {
|
|
1294
|
+
threats.push({
|
|
1295
|
+
type: "SSRF",
|
|
1296
|
+
field,
|
|
1297
|
+
value: truncate(value, 100),
|
|
1298
|
+
description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
if (config.sqlInjection && detectSQLInjection(value)) {
|
|
1302
|
+
threats.push({
|
|
1303
|
+
type: "SQL_INJECTION",
|
|
1304
|
+
field,
|
|
1305
|
+
value: truncate(value, 100),
|
|
1306
|
+
description: "SQL injection pattern detected"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
return { safe: threats.length === 0, threats };
|
|
1310
|
+
}
|
|
1311
|
+
function sanitizeObject(basePath, obj, config) {
|
|
1312
|
+
const threats = [];
|
|
1313
|
+
if (Array.isArray(obj)) {
|
|
1314
|
+
for (let i = 0; i < obj.length; i++) {
|
|
1315
|
+
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
1316
|
+
threats.push(...result.threats);
|
|
1317
|
+
}
|
|
1318
|
+
} else {
|
|
1319
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1320
|
+
const result = sanitizeInput(`${basePath}.${key}`, val, config);
|
|
1321
|
+
threats.push(...result.threats);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
return { safe: threats.length === 0, threats };
|
|
1325
|
+
}
|
|
1326
|
+
function truncate(str, maxLen) {
|
|
1327
|
+
return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
|
|
1328
|
+
}
|
|
1329
|
+
var DEFAULT_TOKEN_TTL_SECONDS = 30;
|
|
1330
|
+
var TOKEN_ALGORITHM = "HS256";
|
|
1331
|
+
var MIN_SECRET_LENGTH = 32;
|
|
1332
|
+
|
|
1333
|
+
// src/config.ts
|
|
1334
|
+
import { readFileSync, existsSync } from "fs";
|
|
1335
|
+
import { resolve } from "path";
|
|
1336
|
+
async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
|
|
1337
|
+
const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
|
|
1338
|
+
const res = await fetch(url, {
|
|
1339
|
+
headers: { "Authorization": `Bearer ${apiKey}` }
|
|
1340
|
+
});
|
|
1341
|
+
if (!res.ok) {
|
|
1342
|
+
const body = await res.text().catch(() => "");
|
|
1343
|
+
throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
|
|
1344
|
+
}
|
|
1345
|
+
const data = await res.json();
|
|
1346
|
+
return {
|
|
1347
|
+
id: String(data.id ?? "cloud"),
|
|
1348
|
+
name: String(data.name ?? "Cloud Policy"),
|
|
1349
|
+
version: Number(data._version ?? 1),
|
|
1350
|
+
rules: data.rules ?? [],
|
|
1351
|
+
createdAt: String(data._created_at ?? ""),
|
|
1352
|
+
updatedAt: ""
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
async function sendAuditLog(apiKey, apiUrl, entry) {
|
|
1356
|
+
try {
|
|
1357
|
+
await fetch(`${apiUrl}/api/v1/audit-logs`, {
|
|
1358
|
+
method: "POST",
|
|
1359
|
+
headers: {
|
|
1360
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
1361
|
+
"Content-Type": "application/json"
|
|
1362
|
+
},
|
|
1363
|
+
body: JSON.stringify(entry)
|
|
1364
|
+
});
|
|
1365
|
+
} catch {
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
var PRESETS = {
|
|
1369
|
+
restricted: {
|
|
1370
|
+
id: "restricted",
|
|
1371
|
+
name: "Restricted",
|
|
1372
|
+
description: "Blocks dangerous tools (shell, web), allows safe tools",
|
|
1373
|
+
version: 1,
|
|
1374
|
+
rules: [
|
|
1375
|
+
{
|
|
1376
|
+
id: "deny-shell",
|
|
1377
|
+
description: "Block shell execution",
|
|
1378
|
+
effect: "DENY",
|
|
1379
|
+
priority: 100,
|
|
1380
|
+
toolPattern: "*shell*",
|
|
1381
|
+
permission: "EXECUTE",
|
|
1382
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1383
|
+
enabled: true,
|
|
1384
|
+
createdAt: "",
|
|
1385
|
+
updatedAt: ""
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
id: "deny-exec",
|
|
1389
|
+
description: "Block command execution",
|
|
1390
|
+
effect: "DENY",
|
|
1391
|
+
priority: 101,
|
|
1392
|
+
toolPattern: "*exec*",
|
|
1393
|
+
permission: "EXECUTE",
|
|
1394
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1395
|
+
enabled: true,
|
|
1396
|
+
createdAt: "",
|
|
1397
|
+
updatedAt: ""
|
|
1398
|
+
},
|
|
1399
|
+
{
|
|
1400
|
+
id: "deny-eval",
|
|
1401
|
+
description: "Block code eval",
|
|
1402
|
+
effect: "DENY",
|
|
1403
|
+
priority: 102,
|
|
1404
|
+
toolPattern: "*eval*",
|
|
1405
|
+
permission: "EXECUTE",
|
|
1406
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1407
|
+
enabled: true,
|
|
1408
|
+
createdAt: "",
|
|
1409
|
+
updatedAt: ""
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
id: "allow-rest",
|
|
1413
|
+
description: "Allow all other tools",
|
|
1414
|
+
effect: "ALLOW",
|
|
1415
|
+
priority: 1e3,
|
|
1416
|
+
toolPattern: "*",
|
|
1417
|
+
permission: "EXECUTE",
|
|
1418
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1419
|
+
enabled: true,
|
|
1420
|
+
createdAt: "",
|
|
1421
|
+
updatedAt: ""
|
|
1422
|
+
}
|
|
1423
|
+
],
|
|
1424
|
+
createdAt: "",
|
|
1425
|
+
updatedAt: ""
|
|
1426
|
+
},
|
|
1427
|
+
"read-only": {
|
|
1428
|
+
id: "read-only",
|
|
1429
|
+
name: "Read Only",
|
|
1430
|
+
description: "Only allows read operations, blocks writes and execution",
|
|
1431
|
+
version: 1,
|
|
1432
|
+
rules: [
|
|
1433
|
+
{
|
|
1434
|
+
id: "allow-read",
|
|
1435
|
+
description: "Allow read tools",
|
|
1436
|
+
effect: "ALLOW",
|
|
1437
|
+
priority: 100,
|
|
1438
|
+
toolPattern: "*read*",
|
|
1439
|
+
permission: "EXECUTE",
|
|
1440
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1441
|
+
enabled: true,
|
|
1442
|
+
createdAt: "",
|
|
1443
|
+
updatedAt: ""
|
|
1444
|
+
},
|
|
1445
|
+
{
|
|
1446
|
+
id: "allow-list",
|
|
1447
|
+
description: "Allow list tools",
|
|
1448
|
+
effect: "ALLOW",
|
|
1449
|
+
priority: 101,
|
|
1450
|
+
toolPattern: "*list*",
|
|
1451
|
+
permission: "EXECUTE",
|
|
1452
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1453
|
+
enabled: true,
|
|
1454
|
+
createdAt: "",
|
|
1455
|
+
updatedAt: ""
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
id: "allow-get",
|
|
1459
|
+
description: "Allow get tools",
|
|
1460
|
+
effect: "ALLOW",
|
|
1461
|
+
priority: 102,
|
|
1462
|
+
toolPattern: "*get*",
|
|
1463
|
+
permission: "EXECUTE",
|
|
1464
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1465
|
+
enabled: true,
|
|
1466
|
+
createdAt: "",
|
|
1467
|
+
updatedAt: ""
|
|
1468
|
+
},
|
|
1469
|
+
{
|
|
1470
|
+
id: "allow-search",
|
|
1471
|
+
description: "Allow search tools",
|
|
1472
|
+
effect: "ALLOW",
|
|
1473
|
+
priority: 103,
|
|
1474
|
+
toolPattern: "*search*",
|
|
1475
|
+
permission: "EXECUTE",
|
|
1476
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1477
|
+
enabled: true,
|
|
1478
|
+
createdAt: "",
|
|
1479
|
+
updatedAt: ""
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
id: "allow-query",
|
|
1483
|
+
description: "Allow query tools",
|
|
1484
|
+
effect: "ALLOW",
|
|
1485
|
+
priority: 104,
|
|
1486
|
+
toolPattern: "*query*",
|
|
1487
|
+
permission: "EXECUTE",
|
|
1488
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1489
|
+
enabled: true,
|
|
1490
|
+
createdAt: "",
|
|
1491
|
+
updatedAt: ""
|
|
1492
|
+
}
|
|
1493
|
+
],
|
|
1494
|
+
createdAt: "",
|
|
1495
|
+
updatedAt: ""
|
|
1496
|
+
},
|
|
1497
|
+
permissive: {
|
|
1498
|
+
id: "permissive",
|
|
1499
|
+
name: "Permissive",
|
|
1500
|
+
description: "Allows all tool calls (monitoring only)",
|
|
1501
|
+
version: 1,
|
|
1502
|
+
rules: [
|
|
1503
|
+
{
|
|
1504
|
+
id: "allow-all",
|
|
1505
|
+
description: "Allow all",
|
|
1506
|
+
effect: "ALLOW",
|
|
1507
|
+
priority: 1e3,
|
|
1508
|
+
toolPattern: "*",
|
|
1509
|
+
permission: "EXECUTE",
|
|
1510
|
+
minimumTrustLevel: "UNTRUSTED",
|
|
1511
|
+
enabled: true,
|
|
1512
|
+
createdAt: "",
|
|
1513
|
+
updatedAt: ""
|
|
1514
|
+
}
|
|
1515
|
+
],
|
|
1516
|
+
createdAt: "",
|
|
1517
|
+
updatedAt: ""
|
|
1518
|
+
},
|
|
1519
|
+
"deny-all": {
|
|
1520
|
+
id: "deny-all",
|
|
1521
|
+
name: "Deny All",
|
|
1522
|
+
description: "Blocks all tool calls",
|
|
1523
|
+
version: 1,
|
|
1524
|
+
rules: [],
|
|
1525
|
+
createdAt: "",
|
|
1526
|
+
updatedAt: ""
|
|
1527
|
+
}
|
|
1528
|
+
};
|
|
1529
|
+
function loadPolicy(source) {
|
|
1530
|
+
if (typeof source === "object") return source;
|
|
1531
|
+
if (PRESETS[source]) return PRESETS[source];
|
|
1532
|
+
const filePath = resolve(source);
|
|
1533
|
+
if (existsSync(filePath)) {
|
|
1534
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1535
|
+
return JSON.parse(content);
|
|
1536
|
+
}
|
|
1537
|
+
throw new Error(
|
|
1538
|
+
`Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
|
|
1539
|
+
);
|
|
845
1540
|
}
|
|
846
|
-
function
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
1541
|
+
function parseArgs(argv) {
|
|
1542
|
+
const args = argv.slice(2);
|
|
1543
|
+
let policySource = "restricted";
|
|
1544
|
+
let name = "solongate-proxy";
|
|
1545
|
+
let verbose = false;
|
|
1546
|
+
let validateInput = true;
|
|
1547
|
+
let rateLimitPerTool;
|
|
1548
|
+
let globalRateLimit;
|
|
1549
|
+
let configFile;
|
|
1550
|
+
let apiKey;
|
|
1551
|
+
let apiUrl;
|
|
1552
|
+
let upstreamUrl;
|
|
1553
|
+
let upstreamTransport;
|
|
1554
|
+
let port;
|
|
1555
|
+
let separatorIndex = args.indexOf("--");
|
|
1556
|
+
const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
|
|
1557
|
+
const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
|
|
1558
|
+
for (let i = 0; i < flags.length; i++) {
|
|
1559
|
+
switch (flags[i]) {
|
|
1560
|
+
case "--policy":
|
|
1561
|
+
policySource = flags[++i];
|
|
1562
|
+
break;
|
|
1563
|
+
case "--name":
|
|
1564
|
+
name = flags[++i];
|
|
1565
|
+
break;
|
|
1566
|
+
case "--verbose":
|
|
1567
|
+
verbose = true;
|
|
1568
|
+
break;
|
|
1569
|
+
case "--no-input-guard":
|
|
1570
|
+
validateInput = false;
|
|
1571
|
+
break;
|
|
1572
|
+
case "--rate-limit":
|
|
1573
|
+
rateLimitPerTool = parseInt(flags[++i], 10);
|
|
1574
|
+
break;
|
|
1575
|
+
case "--global-rate-limit":
|
|
1576
|
+
globalRateLimit = parseInt(flags[++i], 10);
|
|
1577
|
+
break;
|
|
1578
|
+
case "--config":
|
|
1579
|
+
configFile = flags[++i];
|
|
1580
|
+
break;
|
|
1581
|
+
case "--api-key":
|
|
1582
|
+
apiKey = flags[++i];
|
|
1583
|
+
break;
|
|
1584
|
+
case "--api-url":
|
|
1585
|
+
apiUrl = flags[++i];
|
|
1586
|
+
break;
|
|
1587
|
+
case "--upstream-url":
|
|
1588
|
+
upstreamUrl = flags[++i];
|
|
1589
|
+
break;
|
|
1590
|
+
case "--upstream-transport":
|
|
1591
|
+
upstreamTransport = flags[++i];
|
|
1592
|
+
break;
|
|
1593
|
+
case "--port":
|
|
1594
|
+
port = parseInt(flags[++i], 10);
|
|
1595
|
+
break;
|
|
851
1596
|
}
|
|
852
|
-
return { safe: true, threats: [] };
|
|
853
|
-
}
|
|
854
|
-
if (config.pathTraversal && detectPathTraversal(value)) {
|
|
855
|
-
threats.push({
|
|
856
|
-
type: "PATH_TRAVERSAL",
|
|
857
|
-
field,
|
|
858
|
-
value: truncate(value, 100),
|
|
859
|
-
description: "Path traversal pattern detected"
|
|
860
|
-
});
|
|
861
|
-
}
|
|
862
|
-
if (config.shellInjection && detectShellInjection(value)) {
|
|
863
|
-
threats.push({
|
|
864
|
-
type: "SHELL_INJECTION",
|
|
865
|
-
field,
|
|
866
|
-
value: truncate(value, 100),
|
|
867
|
-
description: "Shell injection pattern detected"
|
|
868
|
-
});
|
|
869
|
-
}
|
|
870
|
-
if (config.wildcardAbuse && detectWildcardAbuse(value)) {
|
|
871
|
-
threats.push({
|
|
872
|
-
type: "WILDCARD_ABUSE",
|
|
873
|
-
field,
|
|
874
|
-
value: truncate(value, 100),
|
|
875
|
-
description: "Wildcard abuse pattern detected"
|
|
876
|
-
});
|
|
877
1597
|
}
|
|
878
|
-
if (!
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1598
|
+
if (!apiKey) {
|
|
1599
|
+
const envKey = process.env.SOLONGATE_API_KEY;
|
|
1600
|
+
if (envKey) {
|
|
1601
|
+
apiKey = envKey;
|
|
1602
|
+
} else {
|
|
1603
|
+
throw new Error(
|
|
1604
|
+
"A valid SolonGate API key is required.\n\nUsage: solongate-proxy --api-key sg_live_xxx -- <command>\n or: set SOLONGATE_API_KEY=sg_live_xxx\n\nGet your API key at https://solongate.com\n"
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
885
1607
|
}
|
|
886
|
-
if (
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
value: truncate(value, 100),
|
|
891
|
-
description: "High entropy string detected - possible encoded payload"
|
|
892
|
-
});
|
|
1608
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
1609
|
+
throw new Error(
|
|
1610
|
+
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'.\nGet your API key at https://solongate.com\n"
|
|
1611
|
+
);
|
|
893
1612
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
|
|
901
|
-
threats.push(...result.threats);
|
|
1613
|
+
if (configFile) {
|
|
1614
|
+
const filePath = resolve(configFile);
|
|
1615
|
+
const content = readFileSync(filePath, "utf-8");
|
|
1616
|
+
const fileConfig = JSON.parse(content);
|
|
1617
|
+
if (!fileConfig.upstream) {
|
|
1618
|
+
throw new Error('Config file must include "upstream" with at least "command" or "url"');
|
|
902
1619
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1620
|
+
if (fileConfig.upstream.url && detectSSRF(fileConfig.upstream.url)) {
|
|
1621
|
+
throw new Error(
|
|
1622
|
+
`Upstream URL blocked: "${fileConfig.upstream.url}" points to an internal or private network address.`
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
return {
|
|
1626
|
+
upstream: fileConfig.upstream,
|
|
1627
|
+
policy: loadPolicy(fileConfig.policy ?? policySource),
|
|
1628
|
+
name: fileConfig.name ?? name,
|
|
1629
|
+
verbose: fileConfig.verbose ?? verbose,
|
|
1630
|
+
validateInput: fileConfig.validateInput ?? validateInput,
|
|
1631
|
+
rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
|
|
1632
|
+
globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit,
|
|
1633
|
+
apiKey: apiKey ?? fileConfig.apiKey,
|
|
1634
|
+
apiUrl: apiUrl ?? fileConfig.apiUrl,
|
|
1635
|
+
port: port ?? fileConfig.port
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
if (upstreamUrl) {
|
|
1639
|
+
if (detectSSRF(upstreamUrl)) {
|
|
1640
|
+
throw new Error(
|
|
1641
|
+
`Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
|
|
1642
|
+
SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
|
|
1643
|
+
);
|
|
907
1644
|
}
|
|
1645
|
+
const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
|
|
1646
|
+
return {
|
|
1647
|
+
upstream: {
|
|
1648
|
+
transport,
|
|
1649
|
+
command: "",
|
|
1650
|
+
// not used for URL-based transports
|
|
1651
|
+
url: upstreamUrl
|
|
1652
|
+
},
|
|
1653
|
+
policy: loadPolicy(policySource),
|
|
1654
|
+
name,
|
|
1655
|
+
verbose,
|
|
1656
|
+
validateInput,
|
|
1657
|
+
rateLimitPerTool,
|
|
1658
|
+
globalRateLimit,
|
|
1659
|
+
apiKey,
|
|
1660
|
+
apiUrl,
|
|
1661
|
+
port
|
|
1662
|
+
};
|
|
908
1663
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
1664
|
+
if (upstreamArgs.length === 0) {
|
|
1665
|
+
throw new Error(
|
|
1666
|
+
"No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --upstream-url http://localhost:3001/mcp\n solongate-proxy --config solongate.json\n"
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
const [command, ...commandArgs] = upstreamArgs;
|
|
1670
|
+
return {
|
|
1671
|
+
upstream: {
|
|
1672
|
+
transport: upstreamTransport ?? "stdio",
|
|
1673
|
+
command,
|
|
1674
|
+
args: commandArgs,
|
|
1675
|
+
env: { ...process.env }
|
|
1676
|
+
},
|
|
1677
|
+
policy: loadPolicy(policySource),
|
|
1678
|
+
name,
|
|
1679
|
+
verbose,
|
|
1680
|
+
validateInput,
|
|
1681
|
+
rateLimitPerTool,
|
|
1682
|
+
globalRateLimit,
|
|
1683
|
+
apiKey,
|
|
1684
|
+
apiUrl,
|
|
1685
|
+
port
|
|
1686
|
+
};
|
|
913
1687
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1688
|
+
|
|
1689
|
+
// src/proxy.ts
|
|
1690
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
1691
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1692
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
1693
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1694
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
1695
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1696
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
1697
|
+
import {
|
|
1698
|
+
ListToolsRequestSchema,
|
|
1699
|
+
CallToolRequestSchema,
|
|
1700
|
+
ListResourcesRequestSchema,
|
|
1701
|
+
ListPromptsRequestSchema,
|
|
1702
|
+
GetPromptRequestSchema,
|
|
1703
|
+
ReadResourceRequestSchema,
|
|
1704
|
+
ListResourceTemplatesRequestSchema
|
|
1705
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
1706
|
+
import { createServer as createHttpServer } from "http";
|
|
917
1707
|
|
|
918
1708
|
// ../policy-engine/dist/index.js
|
|
919
1709
|
import { createHash } from "crypto";
|
|
@@ -1064,8 +1854,51 @@ function trustLevelMeetsMinimum(actual, minimum) {
|
|
|
1064
1854
|
function argumentConstraintsMatch(constraints, args) {
|
|
1065
1855
|
for (const [key, constraint] of Object.entries(constraints)) {
|
|
1066
1856
|
if (!(key in args)) return false;
|
|
1067
|
-
|
|
1068
|
-
|
|
1857
|
+
const argValue = args[key];
|
|
1858
|
+
if (typeof constraint === "string") {
|
|
1859
|
+
if (constraint === "*") continue;
|
|
1860
|
+
if (typeof argValue === "string") {
|
|
1861
|
+
if (argValue !== constraint) return false;
|
|
1862
|
+
} else {
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
continue;
|
|
1866
|
+
}
|
|
1867
|
+
if (typeof constraint === "object" && constraint !== null && !Array.isArray(constraint)) {
|
|
1868
|
+
const ops = constraint;
|
|
1869
|
+
const strValue = typeof argValue === "string" ? argValue : void 0;
|
|
1870
|
+
const numValue = typeof argValue === "number" ? argValue : void 0;
|
|
1871
|
+
if ("$contains" in ops && typeof ops.$contains === "string") {
|
|
1872
|
+
if (!strValue || !strValue.includes(ops.$contains)) return false;
|
|
1873
|
+
}
|
|
1874
|
+
if ("$notContains" in ops && typeof ops.$notContains === "string") {
|
|
1875
|
+
if (strValue && strValue.includes(ops.$notContains)) return false;
|
|
1876
|
+
}
|
|
1877
|
+
if ("$startsWith" in ops && typeof ops.$startsWith === "string") {
|
|
1878
|
+
if (!strValue || !strValue.startsWith(ops.$startsWith)) return false;
|
|
1879
|
+
}
|
|
1880
|
+
if ("$endsWith" in ops && typeof ops.$endsWith === "string") {
|
|
1881
|
+
if (!strValue || !strValue.endsWith(ops.$endsWith)) return false;
|
|
1882
|
+
}
|
|
1883
|
+
if ("$in" in ops && Array.isArray(ops.$in)) {
|
|
1884
|
+
if (!ops.$in.includes(argValue)) return false;
|
|
1885
|
+
}
|
|
1886
|
+
if ("$notIn" in ops && Array.isArray(ops.$notIn)) {
|
|
1887
|
+
if (ops.$notIn.includes(argValue)) return false;
|
|
1888
|
+
}
|
|
1889
|
+
if ("$gt" in ops && typeof ops.$gt === "number") {
|
|
1890
|
+
if (numValue === void 0 || numValue <= ops.$gt) return false;
|
|
1891
|
+
}
|
|
1892
|
+
if ("$lt" in ops && typeof ops.$lt === "number") {
|
|
1893
|
+
if (numValue === void 0 || numValue >= ops.$lt) return false;
|
|
1894
|
+
}
|
|
1895
|
+
if ("$gte" in ops && typeof ops.$gte === "number") {
|
|
1896
|
+
if (numValue === void 0 || numValue < ops.$gte) return false;
|
|
1897
|
+
}
|
|
1898
|
+
if ("$lte" in ops && typeof ops.$lte === "number") {
|
|
1899
|
+
if (numValue === void 0 || numValue > ops.$lte) return false;
|
|
1900
|
+
}
|
|
1901
|
+
continue;
|
|
1069
1902
|
}
|
|
1070
1903
|
}
|
|
1071
1904
|
return true;
|
|
@@ -1098,7 +1931,15 @@ function evaluatePolicy(policySet, request) {
|
|
|
1098
1931
|
matchedRule: null,
|
|
1099
1932
|
reason: "No matching policy rule found. Default action: DENY.",
|
|
1100
1933
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1101
|
-
evaluationTimeMs: endTime - startTime
|
|
1934
|
+
evaluationTimeMs: endTime - startTime,
|
|
1935
|
+
metadata: {
|
|
1936
|
+
evaluatedRules: sortedRules.length,
|
|
1937
|
+
ruleIds: sortedRules.map((r) => r.id),
|
|
1938
|
+
requestContext: {
|
|
1939
|
+
tool: request.toolName,
|
|
1940
|
+
arguments: Object.keys(request.arguments ?? {})
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1102
1943
|
};
|
|
1103
1944
|
}
|
|
1104
1945
|
function validatePolicyRule(input) {
|
|
@@ -1420,6 +2261,7 @@ var PolicyStore = class {
|
|
|
1420
2261
|
|
|
1421
2262
|
// ../sdk-ts/dist/index.js
|
|
1422
2263
|
import { randomUUID, createHmac } from "crypto";
|
|
2264
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1423
2265
|
var DEFAULT_CONFIG = Object.freeze({
|
|
1424
2266
|
validateSchemas: true,
|
|
1425
2267
|
enableLogging: true,
|
|
@@ -1514,7 +2356,7 @@ async function interceptToolCall(params, upstreamCall, options) {
|
|
|
1514
2356
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1515
2357
|
};
|
|
1516
2358
|
options.onDecision?.(result);
|
|
1517
|
-
const reason = options.verboseErrors ? `Input validation failed: ${
|
|
2359
|
+
const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
|
|
1518
2360
|
return createDeniedToolResult(reason);
|
|
1519
2361
|
}
|
|
1520
2362
|
}
|
|
@@ -1832,6 +2674,25 @@ var RateLimiter = class {
|
|
|
1832
2674
|
const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
|
|
1833
2675
|
return { allowed, remaining, resetAt };
|
|
1834
2676
|
}
|
|
2677
|
+
/**
|
|
2678
|
+
* Atomically checks and records a tool call.
|
|
2679
|
+
* Prevents TOCTOU race conditions between check and record.
|
|
2680
|
+
* Returns the rate limit result; if allowed, the call is already recorded.
|
|
2681
|
+
*/
|
|
2682
|
+
checkAndRecord(toolName, limitPerWindow, globalLimit) {
|
|
2683
|
+
const result = this.checkLimit(toolName, limitPerWindow);
|
|
2684
|
+
if (!result.allowed) {
|
|
2685
|
+
return result;
|
|
2686
|
+
}
|
|
2687
|
+
if (globalLimit !== void 0) {
|
|
2688
|
+
const globalResult = this.checkGlobalLimit(globalLimit);
|
|
2689
|
+
if (!globalResult.allowed) {
|
|
2690
|
+
return globalResult;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
this.recordCall(toolName);
|
|
2694
|
+
return result;
|
|
2695
|
+
}
|
|
1835
2696
|
/**
|
|
1836
2697
|
* Records a tool call for rate limiting.
|
|
1837
2698
|
* Call this after successful execution.
|
|
@@ -1887,6 +2748,16 @@ var RateLimiter = class {
|
|
|
1887
2748
|
return active;
|
|
1888
2749
|
}
|
|
1889
2750
|
};
|
|
2751
|
+
var LicenseError = class extends Error {
|
|
2752
|
+
constructor(message) {
|
|
2753
|
+
super(
|
|
2754
|
+
`${message}
|
|
2755
|
+
Get your API key at https://solongate.com
|
|
2756
|
+
Usage: new SolonGate({ name: '...', apiKey: 'sg_live_xxx' })`
|
|
2757
|
+
);
|
|
2758
|
+
this.name = "LicenseError";
|
|
2759
|
+
}
|
|
2760
|
+
};
|
|
1890
2761
|
var SolonGate = class {
|
|
1891
2762
|
policyEngine;
|
|
1892
2763
|
config;
|
|
@@ -1895,7 +2766,19 @@ var SolonGate = class {
|
|
|
1895
2766
|
tokenIssuer;
|
|
1896
2767
|
serverVerifier;
|
|
1897
2768
|
rateLimiter;
|
|
2769
|
+
apiKey;
|
|
2770
|
+
licenseValidated = false;
|
|
1898
2771
|
constructor(options) {
|
|
2772
|
+
const apiKey = options.apiKey || process.env.SOLONGATE_API_KEY || "";
|
|
2773
|
+
if (!apiKey) {
|
|
2774
|
+
throw new LicenseError("A valid SolonGate API key is required.");
|
|
2775
|
+
}
|
|
2776
|
+
if (!apiKey.startsWith("sg_live_") && !apiKey.startsWith("sg_test_")) {
|
|
2777
|
+
throw new LicenseError(
|
|
2778
|
+
"Invalid API key format. Keys must start with 'sg_live_' or 'sg_test_'."
|
|
2779
|
+
);
|
|
2780
|
+
}
|
|
2781
|
+
this.apiKey = apiKey;
|
|
1899
2782
|
const { config, warnings } = resolveConfig(options.config);
|
|
1900
2783
|
this.config = config;
|
|
1901
2784
|
this.configWarnings = warnings;
|
|
@@ -1921,12 +2804,47 @@ var SolonGate = class {
|
|
|
1921
2804
|
this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
|
|
1922
2805
|
this.rateLimiter = new RateLimiter();
|
|
1923
2806
|
}
|
|
2807
|
+
/**
|
|
2808
|
+
* Validate the API key against the SolonGate cloud API.
|
|
2809
|
+
* Called once on first executeToolCall. Throws LicenseError if invalid.
|
|
2810
|
+
* Test keys (sg_test_) skip online validation.
|
|
2811
|
+
*/
|
|
2812
|
+
async validateLicense() {
|
|
2813
|
+
if (this.licenseValidated) return;
|
|
2814
|
+
if (this.apiKey.startsWith("sg_test_")) {
|
|
2815
|
+
this.licenseValidated = true;
|
|
2816
|
+
return;
|
|
2817
|
+
}
|
|
2818
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2819
|
+
try {
|
|
2820
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
2821
|
+
headers: {
|
|
2822
|
+
"X-API-Key": this.apiKey,
|
|
2823
|
+
"Authorization": `Bearer ${this.apiKey}`
|
|
2824
|
+
},
|
|
2825
|
+
signal: AbortSignal.timeout(1e4)
|
|
2826
|
+
});
|
|
2827
|
+
if (res.status === 401) {
|
|
2828
|
+
throw new LicenseError("Invalid or expired API key.");
|
|
2829
|
+
}
|
|
2830
|
+
if (res.status === 403) {
|
|
2831
|
+
throw new LicenseError("Your subscription is inactive. Renew at https://solongate.com");
|
|
2832
|
+
}
|
|
2833
|
+
this.licenseValidated = true;
|
|
2834
|
+
} catch (err) {
|
|
2835
|
+
if (err instanceof LicenseError) throw err;
|
|
2836
|
+
throw new LicenseError(
|
|
2837
|
+
"Unable to reach SolonGate license server. Check your internet connection."
|
|
2838
|
+
);
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
1924
2841
|
/**
|
|
1925
2842
|
* Intercept and evaluate a tool call against the full security pipeline.
|
|
1926
2843
|
* If denied at any stage, returns an error result without calling upstream.
|
|
1927
2844
|
* If allowed, calls upstream and returns the result.
|
|
1928
2845
|
*/
|
|
1929
2846
|
async executeToolCall(params, upstreamCall) {
|
|
2847
|
+
await this.validateLicense();
|
|
1930
2848
|
return interceptToolCall(params, upstreamCall, {
|
|
1931
2849
|
policyEngine: this.policyEngine,
|
|
1932
2850
|
validateSchemas: this.config.validateSchemas,
|
|
@@ -1976,8 +2894,8 @@ var Mutex = class {
|
|
|
1976
2894
|
this.locked = true;
|
|
1977
2895
|
return;
|
|
1978
2896
|
}
|
|
1979
|
-
return new Promise((
|
|
1980
|
-
this.queue.push(
|
|
2897
|
+
return new Promise((resolve5) => {
|
|
2898
|
+
this.queue.push(resolve5);
|
|
1981
2899
|
});
|
|
1982
2900
|
}
|
|
1983
2901
|
release() {
|
|
@@ -2018,9 +2936,31 @@ var SolonGateProxy = class {
|
|
|
2018
2936
|
*/
|
|
2019
2937
|
async start() {
|
|
2020
2938
|
log("Starting SolonGate Proxy...");
|
|
2939
|
+
const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
|
|
2021
2940
|
if (this.config.apiKey) {
|
|
2022
|
-
|
|
2023
|
-
|
|
2941
|
+
log(`Validating license with ${apiUrl}...`);
|
|
2942
|
+
try {
|
|
2943
|
+
const res = await fetch(`${apiUrl}/api/v1/auth/me`, {
|
|
2944
|
+
headers: {
|
|
2945
|
+
"X-API-Key": this.config.apiKey,
|
|
2946
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
2947
|
+
},
|
|
2948
|
+
signal: AbortSignal.timeout(1e4)
|
|
2949
|
+
});
|
|
2950
|
+
if (res.status === 401) {
|
|
2951
|
+
log("ERROR: Invalid or expired API key.");
|
|
2952
|
+
process.exit(1);
|
|
2953
|
+
}
|
|
2954
|
+
if (res.status === 403) {
|
|
2955
|
+
log("ERROR: Your subscription is inactive. Renew at https://solongate.com");
|
|
2956
|
+
process.exit(1);
|
|
2957
|
+
}
|
|
2958
|
+
log("License validated.");
|
|
2959
|
+
} catch (err) {
|
|
2960
|
+
log(`ERROR: Unable to reach SolonGate license server. Check your internet connection.`);
|
|
2961
|
+
log(`Details: ${err instanceof Error ? err.message : String(err)}`);
|
|
2962
|
+
process.exit(1);
|
|
2963
|
+
}
|
|
2024
2964
|
try {
|
|
2025
2965
|
const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
|
|
2026
2966
|
this.config.policy = cloudPolicy;
|
|
@@ -2030,29 +2970,54 @@ var SolonGateProxy = class {
|
|
|
2030
2970
|
}
|
|
2031
2971
|
}
|
|
2032
2972
|
log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
|
|
2033
|
-
|
|
2973
|
+
const transport = this.config.upstream.transport ?? "stdio";
|
|
2974
|
+
if (transport === "stdio") {
|
|
2975
|
+
log(`Upstream: [stdio] ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
|
|
2976
|
+
} else {
|
|
2977
|
+
log(`Upstream: [${transport}] ${this.config.upstream.url}`);
|
|
2978
|
+
}
|
|
2034
2979
|
await this.connectUpstream();
|
|
2035
2980
|
await this.discoverTools();
|
|
2036
2981
|
this.createServer();
|
|
2037
2982
|
await this.serve();
|
|
2038
2983
|
}
|
|
2039
2984
|
/**
|
|
2040
|
-
* Connect to the upstream MCP server
|
|
2985
|
+
* Connect to the upstream MCP server.
|
|
2986
|
+
* Supports stdio (child process), SSE, and StreamableHTTP transports.
|
|
2041
2987
|
*/
|
|
2042
2988
|
async connectUpstream() {
|
|
2043
2989
|
this.client = new Client(
|
|
2044
2990
|
{ name: "solongate-proxy-client", version: "0.1.0" },
|
|
2045
2991
|
{ capabilities: {} }
|
|
2046
2992
|
);
|
|
2047
|
-
const
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2993
|
+
const upstreamTransport = this.config.upstream.transport ?? "stdio";
|
|
2994
|
+
switch (upstreamTransport) {
|
|
2995
|
+
case "sse": {
|
|
2996
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for SSE transport");
|
|
2997
|
+
const transport = new SSEClientTransport(new URL(this.config.upstream.url));
|
|
2998
|
+
await this.client.connect(transport);
|
|
2999
|
+
break;
|
|
3000
|
+
}
|
|
3001
|
+
case "http": {
|
|
3002
|
+
if (!this.config.upstream.url) throw new Error("--upstream-url required for HTTP transport");
|
|
3003
|
+
const transport = new StreamableHTTPClientTransport(new URL(this.config.upstream.url));
|
|
3004
|
+
await this.client.connect(transport);
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
case "stdio":
|
|
3008
|
+
default: {
|
|
3009
|
+
const transport = new StdioClientTransport({
|
|
3010
|
+
command: this.config.upstream.command,
|
|
3011
|
+
args: this.config.upstream.args,
|
|
3012
|
+
env: this.config.upstream.env,
|
|
3013
|
+
cwd: this.config.upstream.cwd,
|
|
3014
|
+
stderr: "pipe"
|
|
3015
|
+
});
|
|
3016
|
+
await this.client.connect(transport);
|
|
3017
|
+
break;
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
log(`Connected to upstream server (${upstreamTransport})`);
|
|
2056
3021
|
}
|
|
2057
3022
|
/**
|
|
2058
3023
|
* Discover tools from the upstream server.
|
|
@@ -2091,8 +3056,17 @@ var SolonGateProxy = class {
|
|
|
2091
3056
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2092
3057
|
return { tools: this.upstreamTools };
|
|
2093
3058
|
});
|
|
3059
|
+
const MAX_ARGUMENT_SIZE = 1024 * 1024;
|
|
2094
3060
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2095
3061
|
const { name, arguments: args } = request.params;
|
|
3062
|
+
const argsSize = JSON.stringify(args ?? {}).length;
|
|
3063
|
+
if (argsSize > MAX_ARGUMENT_SIZE) {
|
|
3064
|
+
log(`DENY: ${name} \u2014 payload size ${argsSize} exceeds limit ${MAX_ARGUMENT_SIZE}`);
|
|
3065
|
+
return {
|
|
3066
|
+
content: [{ type: "text", text: `Request payload too large (${Math.round(argsSize / 1024)}KB > ${Math.round(MAX_ARGUMENT_SIZE / 1024)}KB limit)` }],
|
|
3067
|
+
isError: true
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
2096
3070
|
log(`Tool call: ${name}`);
|
|
2097
3071
|
await this.callMutex.acquire();
|
|
2098
3072
|
const startTime = Date.now();
|
|
@@ -2166,14 +3140,38 @@ var SolonGateProxy = class {
|
|
|
2166
3140
|
});
|
|
2167
3141
|
}
|
|
2168
3142
|
/**
|
|
2169
|
-
* Start serving
|
|
3143
|
+
* Start serving downstream.
|
|
3144
|
+
* If --port is set, serves via StreamableHTTP on that port.
|
|
3145
|
+
* Otherwise, serves on stdio (default for Claude Code / Cursor / etc).
|
|
2170
3146
|
*/
|
|
2171
3147
|
async serve() {
|
|
2172
3148
|
if (!this.server) throw new Error("Server not created");
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
3149
|
+
if (this.config.port) {
|
|
3150
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
3151
|
+
sessionIdGenerator: () => crypto.randomUUID()
|
|
3152
|
+
});
|
|
3153
|
+
await this.server.connect(httpTransport);
|
|
3154
|
+
const httpServer = createHttpServer(async (req, res) => {
|
|
3155
|
+
if (req.url === "/mcp" || req.url?.startsWith("/mcp?")) {
|
|
3156
|
+
await httpTransport.handleRequest(req, res);
|
|
3157
|
+
} else if (req.url === "/health") {
|
|
3158
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3159
|
+
res.end(JSON.stringify({ status: "healthy", proxy: this.config.name ?? "solongate-proxy" }));
|
|
3160
|
+
} else {
|
|
3161
|
+
res.writeHead(404);
|
|
3162
|
+
res.end("Not found. Use /mcp for MCP protocol or /health for health check.");
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
httpServer.listen(this.config.port, () => {
|
|
3166
|
+
log(`Proxy is live on http://localhost:${this.config.port}/mcp`);
|
|
3167
|
+
log("All tool calls are now protected by SolonGate.");
|
|
3168
|
+
});
|
|
3169
|
+
} else {
|
|
3170
|
+
const transport = new StdioServerTransport();
|
|
3171
|
+
await this.server.connect(transport);
|
|
3172
|
+
log("Proxy is live. All tool calls are now protected by SolonGate.");
|
|
3173
|
+
log("Waiting for requests...");
|
|
3174
|
+
}
|
|
2177
3175
|
}
|
|
2178
3176
|
};
|
|
2179
3177
|
|
|
@@ -2190,13 +3188,23 @@ console.error = (...args) => {
|
|
|
2190
3188
|
process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
|
|
2191
3189
|
`);
|
|
2192
3190
|
};
|
|
2193
|
-
async function
|
|
3191
|
+
async function main4() {
|
|
2194
3192
|
const subcommand = process.argv[2];
|
|
2195
3193
|
if (subcommand === "init") {
|
|
2196
3194
|
process.argv.splice(2, 1);
|
|
2197
3195
|
await Promise.resolve().then(() => (init_init(), init_exports));
|
|
2198
3196
|
return;
|
|
2199
3197
|
}
|
|
3198
|
+
if (subcommand === "inject") {
|
|
3199
|
+
process.argv.splice(2, 1);
|
|
3200
|
+
await Promise.resolve().then(() => (init_inject(), inject_exports));
|
|
3201
|
+
return;
|
|
3202
|
+
}
|
|
3203
|
+
if (subcommand === "create") {
|
|
3204
|
+
process.argv.splice(2, 1);
|
|
3205
|
+
await Promise.resolve().then(() => (init_create(), create_exports));
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
2200
3208
|
try {
|
|
2201
3209
|
const config = parseArgs(process.argv);
|
|
2202
3210
|
const proxy = new SolonGateProxy(config);
|
|
@@ -2208,4 +3216,4 @@ async function main2() {
|
|
|
2208
3216
|
process.exit(1);
|
|
2209
3217
|
}
|
|
2210
3218
|
}
|
|
2211
|
-
|
|
3219
|
+
main4();
|