@solongate/proxy 0.1.1 → 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/index.js CHANGED
@@ -1,270 +1,885 @@
1
1
  #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
2
6
 
3
- // src/config.ts
4
- import { readFileSync, existsSync } from "fs";
5
- import { resolve } from "path";
6
- var PRESETS = {
7
- restricted: {
8
- id: "restricted",
9
- name: "Restricted",
10
- description: "Blocks dangerous tools (shell, web), allows safe tools",
11
- version: 1,
12
- rules: [
13
- {
14
- id: "deny-shell",
15
- description: "Block shell execution",
16
- effect: "DENY",
17
- priority: 100,
18
- toolPattern: "*shell*",
19
- permission: "EXECUTE",
20
- minimumTrustLevel: "UNTRUSTED",
21
- enabled: true,
22
- createdAt: "",
23
- updatedAt: ""
24
- },
25
- {
26
- id: "deny-exec",
27
- description: "Block command execution",
28
- effect: "DENY",
29
- priority: 101,
30
- toolPattern: "*exec*",
31
- permission: "EXECUTE",
32
- minimumTrustLevel: "UNTRUSTED",
33
- enabled: true,
34
- createdAt: "",
35
- updatedAt: ""
36
- },
37
- {
38
- id: "deny-eval",
39
- description: "Block code eval",
40
- effect: "DENY",
41
- priority: 102,
42
- toolPattern: "*eval*",
43
- permission: "EXECUTE",
44
- minimumTrustLevel: "UNTRUSTED",
45
- enabled: true,
46
- createdAt: "",
47
- updatedAt: ""
48
- },
49
- {
50
- id: "allow-rest",
51
- description: "Allow all other tools",
52
- effect: "ALLOW",
53
- priority: 1e3,
54
- toolPattern: "*",
55
- permission: "EXECUTE",
56
- minimumTrustLevel: "UNTRUSTED",
57
- enabled: true,
58
- createdAt: "",
59
- updatedAt: ""
60
- }
61
- ],
62
- createdAt: "",
63
- updatedAt: ""
64
- },
65
- "read-only": {
66
- id: "read-only",
67
- name: "Read Only",
68
- description: "Only allows read operations, blocks writes and execution",
69
- version: 1,
70
- rules: [
71
- {
72
- id: "allow-read",
73
- description: "Allow read tools",
74
- effect: "ALLOW",
75
- priority: 100,
76
- toolPattern: "*read*",
77
- permission: "EXECUTE",
78
- minimumTrustLevel: "UNTRUSTED",
79
- enabled: true,
80
- createdAt: "",
81
- updatedAt: ""
82
- },
83
- {
84
- id: "allow-list",
85
- description: "Allow list tools",
86
- effect: "ALLOW",
87
- priority: 101,
88
- toolPattern: "*list*",
89
- permission: "EXECUTE",
90
- minimumTrustLevel: "UNTRUSTED",
91
- enabled: true,
92
- createdAt: "",
93
- updatedAt: ""
94
- },
95
- {
96
- id: "allow-get",
97
- description: "Allow get tools",
98
- effect: "ALLOW",
99
- priority: 102,
100
- toolPattern: "*get*",
101
- permission: "EXECUTE",
102
- minimumTrustLevel: "UNTRUSTED",
103
- enabled: true,
104
- createdAt: "",
105
- updatedAt: ""
106
- },
107
- {
108
- id: "allow-search",
109
- description: "Allow search tools",
110
- effect: "ALLOW",
111
- priority: 103,
112
- toolPattern: "*search*",
113
- permission: "EXECUTE",
114
- minimumTrustLevel: "UNTRUSTED",
115
- enabled: true,
116
- createdAt: "",
117
- updatedAt: ""
118
- },
119
- {
120
- id: "allow-query",
121
- description: "Allow query tools",
122
- effect: "ALLOW",
123
- priority: 104,
124
- toolPattern: "*query*",
125
- permission: "EXECUTE",
126
- minimumTrustLevel: "UNTRUSTED",
127
- enabled: true,
128
- createdAt: "",
129
- updatedAt: ""
130
- }
131
- ],
132
- createdAt: "",
133
- updatedAt: ""
134
- },
135
- permissive: {
136
- id: "permissive",
137
- name: "Permissive",
138
- description: "Allows all tool calls (monitoring only)",
139
- version: 1,
140
- rules: [
141
- {
142
- id: "allow-all",
143
- description: "Allow all",
144
- effect: "ALLOW",
145
- priority: 1e3,
146
- toolPattern: "*",
147
- permission: "EXECUTE",
148
- minimumTrustLevel: "UNTRUSTED",
149
- enabled: true,
150
- createdAt: "",
151
- updatedAt: ""
152
- }
153
- ],
154
- createdAt: "",
155
- updatedAt: ""
156
- },
157
- "deny-all": {
158
- id: "deny-all",
159
- name: "Deny All",
160
- description: "Blocks all tool calls",
161
- version: 1,
162
- rules: [],
163
- createdAt: "",
164
- updatedAt: ""
7
+ // src/init.ts
8
+ var init_exports = {};
9
+ import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, copyFileSync } from "fs";
10
+ import { resolve as resolve2, join, dirname } from "path";
11
+ import { createInterface } from "readline";
12
+ function findProxyPath() {
13
+ const candidates = [
14
+ resolve2(dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1")), "index.js"),
15
+ resolve2("node_modules", "@solongate", "proxy", "dist", "index.js"),
16
+ resolve2("packages", "proxy", "dist", "index.js")
17
+ ];
18
+ for (const p of candidates) {
19
+ if (existsSync2(p)) return p.replace(/\\/g, "/");
20
+ }
21
+ return "solongate-proxy";
22
+ }
23
+ function findConfigFile(explicitPath) {
24
+ if (explicitPath) {
25
+ if (existsSync2(explicitPath)) {
26
+ return { path: resolve2(explicitPath), type: "mcp" };
27
+ }
28
+ return null;
165
29
  }
166
- };
167
- function loadPolicy(source) {
168
- if (typeof source === "object") return source;
169
- if (PRESETS[source]) return PRESETS[source];
170
- const filePath = resolve(source);
171
- if (existsSync(filePath)) {
172
- const content = readFileSync(filePath, "utf-8");
173
- return JSON.parse(content);
30
+ for (const searchPath of SEARCH_PATHS) {
31
+ const full = resolve2(searchPath);
32
+ if (existsSync2(full)) return { path: full, type: "mcp" };
174
33
  }
175
- throw new Error(
176
- `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
177
- );
34
+ for (const desktopPath of CLAUDE_DESKTOP_PATHS) {
35
+ if (existsSync2(desktopPath)) return { path: desktopPath, type: "claude-desktop" };
36
+ }
37
+ return null;
178
38
  }
179
- function parseArgs(argv) {
39
+ function readConfig(filePath) {
40
+ const content = readFileSync2(filePath, "utf-8");
41
+ const parsed = JSON.parse(content);
42
+ if (parsed.mcpServers) return parsed;
43
+ throw new Error(`Unrecognized config format in ${filePath}`);
44
+ }
45
+ function isAlreadyProtected(server) {
46
+ const cmdStr = [server.command, ...server.args ?? []].join(" ");
47
+ return cmdStr.includes("solongate") || cmdStr.includes("solongate-proxy");
48
+ }
49
+ function wrapServer(server, policy, proxyPath) {
50
+ return {
51
+ command: "node",
52
+ args: [
53
+ proxyPath,
54
+ "--policy",
55
+ policy,
56
+ "--verbose",
57
+ "--",
58
+ server.command,
59
+ ...server.args ?? []
60
+ ],
61
+ env: server.env
62
+ };
63
+ }
64
+ async function prompt(question) {
65
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
66
+ return new Promise((res) => {
67
+ rl.question(question, (answer) => {
68
+ rl.close();
69
+ res(answer.trim());
70
+ });
71
+ });
72
+ }
73
+ function parseInitArgs(argv) {
180
74
  const args = argv.slice(2);
181
- let policySource = "restricted";
182
- let name = "solongate-proxy";
183
- let verbose = false;
184
- let validateInput = true;
185
- let rateLimitPerTool;
186
- let globalRateLimit;
187
- let configFile;
188
- let separatorIndex = args.indexOf("--");
189
- const flags = separatorIndex >= 0 ? args.slice(0, separatorIndex) : args;
190
- const upstreamArgs = separatorIndex >= 0 ? args.slice(separatorIndex + 1) : [];
191
- for (let i = 0; i < flags.length; i++) {
192
- switch (flags[i]) {
193
- case "--policy":
194
- policySource = flags[++i];
195
- break;
196
- case "--name":
197
- name = flags[++i];
198
- break;
199
- case "--verbose":
200
- verbose = true;
75
+ const options = {
76
+ policy: "restricted",
77
+ all: false,
78
+ dryRun: false,
79
+ restore: false
80
+ };
81
+ for (let i = 0; i < args.length; i++) {
82
+ switch (args[i]) {
83
+ case "--config":
84
+ options.configPath = args[++i];
201
85
  break;
202
- case "--no-input-guard":
203
- validateInput = false;
86
+ case "--policy":
87
+ options.policy = args[++i];
204
88
  break;
205
- case "--rate-limit":
206
- rateLimitPerTool = parseInt(flags[++i], 10);
89
+ case "--all":
90
+ options.all = true;
207
91
  break;
208
- case "--global-rate-limit":
209
- globalRateLimit = parseInt(flags[++i], 10);
92
+ case "--dry-run":
93
+ options.dryRun = true;
210
94
  break;
211
- case "--config":
212
- configFile = flags[++i];
95
+ case "--restore":
96
+ options.restore = true;
213
97
  break;
98
+ case "--help":
99
+ case "-h":
100
+ printHelp();
101
+ process.exit(0);
214
102
  }
215
103
  }
216
- if (configFile) {
217
- const filePath = resolve(configFile);
218
- const content = readFileSync(filePath, "utf-8");
219
- const fileConfig = JSON.parse(content);
220
- if (!fileConfig.upstream) {
221
- throw new Error('Config file must include "upstream" with at least "command"');
104
+ if (!POLICY_PRESETS.includes(options.policy)) {
105
+ if (!existsSync2(resolve2(options.policy))) {
106
+ console.error(`Unknown policy: ${options.policy}`);
107
+ console.error(`Available presets: ${POLICY_PRESETS.join(", ")}`);
108
+ process.exit(1);
222
109
  }
223
- return {
224
- upstream: fileConfig.upstream,
225
- policy: loadPolicy(fileConfig.policy ?? policySource),
226
- name: fileConfig.name ?? name,
227
- verbose: fileConfig.verbose ?? verbose,
228
- validateInput: fileConfig.validateInput ?? validateInput,
229
- rateLimitPerTool: fileConfig.rateLimitPerTool ?? rateLimitPerTool,
230
- globalRateLimit: fileConfig.globalRateLimit ?? globalRateLimit
231
- };
232
- }
233
- if (upstreamArgs.length === 0) {
234
- throw new Error(
235
- "No upstream server command provided.\n\nUsage: solongate-proxy [options] -- <command> [args...]\n\nExamples:\n solongate-proxy -- node my-server.js\n solongate-proxy --policy restricted -- npx @openclaw/server\n solongate-proxy --config solongate.json\n"
236
- );
237
110
  }
238
- const [command, ...commandArgs] = upstreamArgs;
239
- return {
240
- upstream: {
241
- command,
242
- args: commandArgs,
243
- env: { ...process.env }
244
- },
245
- policy: loadPolicy(policySource),
246
- name,
247
- verbose,
248
- validateInput,
249
- rateLimitPerTool,
250
- globalRateLimit
251
- };
111
+ return options;
252
112
  }
113
+ function printHelp() {
114
+ const help = `
115
+ SolonGate Init \u2014 Protect your MCP servers in seconds
253
116
 
254
- // src/proxy.ts
255
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
256
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
257
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
258
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
259
- import {
260
- ListToolsRequestSchema,
261
- CallToolRequestSchema,
262
- ListResourcesRequestSchema,
263
- ListPromptsRequestSchema,
264
- GetPromptRequestSchema,
265
- ReadResourceRequestSchema,
266
- ListResourceTemplatesRequestSchema
267
- } from "@modelcontextprotocol/sdk/types.js";
117
+ USAGE
118
+ solongate-init [options]
119
+
120
+ OPTIONS
121
+ --config <path> Path to MCP config file (default: auto-detect)
122
+ --policy <preset> Policy preset or JSON file (default: restricted)
123
+ Presets: ${POLICY_PRESETS.join(", ")}
124
+ --all Protect all servers without prompting
125
+ --dry-run Preview changes without writing
126
+ --restore Restore original config from backup
127
+ -h, --help Show this help message
128
+
129
+ EXAMPLES
130
+ solongate-init # Interactive setup
131
+ solongate-init --all # Protect everything
132
+ solongate-init --policy read-only # Use read-only policy
133
+ solongate-init --dry-run # Preview changes
134
+ solongate-init --restore # Undo protection
135
+
136
+ POLICY PRESETS
137
+ restricted Block shell/exec/eval, allow reads and writes (recommended)
138
+ read-only Only allow read/list/get/search/query operations
139
+ permissive Allow everything (monitoring + audit only)
140
+ deny-all Block all tool calls
141
+ `;
142
+ console.error(help);
143
+ }
144
+ async function main() {
145
+ const options = parseInitArgs(process.argv);
146
+ console.error("");
147
+ console.error(" \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");
148
+ console.error(" \u2551 SolonGate \u2014 Init Setup \u2551");
149
+ console.error(" \u2551 Secure your MCP servers in seconds \u2551");
150
+ console.error(" \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");
151
+ console.error("");
152
+ const configInfo = findConfigFile(options.configPath);
153
+ if (!configInfo) {
154
+ console.error(" No MCP config file found.");
155
+ console.error(" Searched: .mcp.json, mcp.json, Claude Desktop config");
156
+ console.error("");
157
+ console.error(" Create a .mcp.json file first, or specify --config <path>");
158
+ process.exit(1);
159
+ }
160
+ console.error(` Config: ${configInfo.path}`);
161
+ console.error(` Type: ${configInfo.type === "claude-desktop" ? "Claude Desktop" : "MCP JSON"}`);
162
+ console.error("");
163
+ const backupPath = configInfo.path + ".solongate-backup";
164
+ if (options.restore) {
165
+ if (!existsSync2(backupPath)) {
166
+ console.error(" No backup found. Nothing to restore.");
167
+ process.exit(1);
168
+ }
169
+ copyFileSync(backupPath, configInfo.path);
170
+ console.error(" Restored original config from backup.");
171
+ process.exit(0);
172
+ }
173
+ const config = readConfig(configInfo.path);
174
+ const serverNames = Object.keys(config.mcpServers);
175
+ if (serverNames.length === 0) {
176
+ console.error(" No MCP servers found in config.");
177
+ process.exit(1);
178
+ }
179
+ console.error(` Found ${serverNames.length} MCP server(s):`);
180
+ console.error("");
181
+ const toProtect = [];
182
+ const alreadyProtected = [];
183
+ const skipped = [];
184
+ for (const name of serverNames) {
185
+ const server = config.mcpServers[name];
186
+ if (isAlreadyProtected(server)) {
187
+ alreadyProtected.push(name);
188
+ console.error(` [protected] ${name}`);
189
+ } else {
190
+ console.error(` [exposed] ${name} \u2192 ${server.command} ${(server.args ?? []).join(" ")}`);
191
+ if (options.all) {
192
+ toProtect.push(name);
193
+ }
194
+ }
195
+ }
196
+ console.error("");
197
+ if (alreadyProtected.length === serverNames.length) {
198
+ console.error(" All servers are already protected by SolonGate!");
199
+ process.exit(0);
200
+ }
201
+ if (!options.all) {
202
+ const exposed = serverNames.filter((n) => !alreadyProtected.includes(n));
203
+ for (const name of exposed) {
204
+ const answer = await prompt(` Protect "${name}"? [Y/n] `);
205
+ if (answer === "" || answer.toLowerCase() === "y" || answer.toLowerCase() === "yes") {
206
+ toProtect.push(name);
207
+ } else {
208
+ skipped.push(name);
209
+ }
210
+ }
211
+ }
212
+ if (toProtect.length === 0) {
213
+ console.error(" No servers selected for protection.");
214
+ process.exit(0);
215
+ }
216
+ console.error("");
217
+ console.error(` Policy: ${options.policy}`);
218
+ console.error(` Protecting: ${toProtect.join(", ")}`);
219
+ console.error("");
220
+ const proxyPath = findProxyPath();
221
+ const newConfig = { mcpServers: {} };
222
+ for (const name of serverNames) {
223
+ if (toProtect.includes(name)) {
224
+ newConfig.mcpServers[name] = wrapServer(config.mcpServers[name], options.policy, proxyPath);
225
+ } else {
226
+ newConfig.mcpServers[name] = config.mcpServers[name];
227
+ }
228
+ }
229
+ if (options.dryRun) {
230
+ console.error(" --- DRY RUN (no changes written) ---");
231
+ console.error("");
232
+ console.error(" New config:");
233
+ console.error(JSON.stringify(newConfig, null, 2));
234
+ process.exit(0);
235
+ }
236
+ if (!existsSync2(backupPath)) {
237
+ copyFileSync(configInfo.path, backupPath);
238
+ console.error(` Backup: ${backupPath}`);
239
+ }
240
+ if (configInfo.type === "claude-desktop") {
241
+ const original = JSON.parse(readFileSync2(configInfo.path, "utf-8"));
242
+ original.mcpServers = newConfig.mcpServers;
243
+ writeFileSync(configInfo.path, JSON.stringify(original, null, 2) + "\n");
244
+ } else {
245
+ writeFileSync(configInfo.path, JSON.stringify(newConfig, null, 2) + "\n");
246
+ }
247
+ console.error(" Config updated!");
248
+ console.error("");
249
+ console.error(" \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\u2510");
250
+ console.error(" \u2502 Your MCP servers are now protected by \u2502");
251
+ console.error(" \u2502 SolonGate security policies. \u2502");
252
+ console.error(" \u2502 \u2502");
253
+ console.error(" \u2502 Restart your MCP client (Claude Code \u2502");
254
+ console.error(" \u2502 or Claude Desktop) to apply changes. \u2502");
255
+ console.error(" \u2502 \u2502");
256
+ console.error(" \u2502 To undo: solongate-init --restore \u2502");
257
+ console.error(" \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\u2518");
258
+ console.error("");
259
+ for (const name of toProtect) {
260
+ console.error(` \u2713 ${name} \u2014 protected (${options.policy})`);
261
+ }
262
+ for (const name of alreadyProtected) {
263
+ console.error(` \u25CF ${name} \u2014 already protected`);
264
+ }
265
+ for (const name of skipped) {
266
+ console.error(` \u25CB ${name} \u2014 skipped`);
267
+ }
268
+ console.error("");
269
+ }
270
+ var POLICY_PRESETS, SEARCH_PATHS, CLAUDE_DESKTOP_PATHS;
271
+ var init_init = __esm({
272
+ "src/init.ts"() {
273
+ "use strict";
274
+ POLICY_PRESETS = ["restricted", "read-only", "permissive", "deny-all"];
275
+ SEARCH_PATHS = [
276
+ ".mcp.json",
277
+ "mcp.json",
278
+ ".claude/mcp.json"
279
+ ];
280
+ CLAUDE_DESKTOP_PATHS = process.platform === "win32" ? [join(process.env["APPDATA"] ?? "", "Claude", "claude_desktop_config.json")] : process.platform === "darwin" ? [join(process.env["HOME"] ?? "", "Library", "Application Support", "Claude", "claude_desktop_config.json")] : [join(process.env["HOME"] ?? "", ".config", "claude", "claude_desktop_config.json")];
281
+ main().catch((err) => {
282
+ console.error(`Fatal: ${err instanceof Error ? err.message : String(err)}`);
283
+ process.exit(1);
284
+ });
285
+ }
286
+ });
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 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
+ });
268
883
 
269
884
  // ../core/dist/index.js
270
885
  import { z } from "zod";
@@ -417,7 +1032,9 @@ var DEFAULT_INPUT_GUARD_CONFIG = Object.freeze({
417
1032
  shellInjection: true,
418
1033
  wildcardAbuse: true,
419
1034
  lengthLimit: 4096,
420
- entropyLimit: true
1035
+ entropyLimit: true,
1036
+ ssrf: true,
1037
+ sqlInjection: true
421
1038
  });
422
1039
  var PATH_TRAVERSAL_PATTERNS = [
423
1040
  /\.\.\//,
@@ -443,7 +1060,23 @@ var SENSITIVE_PATHS = [
443
1060
  /c:\\windows\\system32/i,
444
1061
  /c:\\windows\\syswow64/i,
445
1062
  /\/root\//i,
446
- /~\//
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
447
1080
  ];
448
1081
  function detectPathTraversal(value) {
449
1082
  for (const pattern of PATH_TRAVERSAL_PATTERNS) {
@@ -473,8 +1106,18 @@ var SHELL_INJECTION_PATTERNS = [
473
1106
  // eval command
474
1107
  /\bexec\b/i,
475
1108
  // exec command
476
- /\bsystem\b/i
1109
+ /\bsystem\b/i,
477
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)
478
1121
  ];
479
1122
  function detectShellInjection(value) {
480
1123
  for (const pattern of SHELL_INJECTION_PATTERNS) {
@@ -489,6 +1132,91 @@ function detectWildcardAbuse(value) {
489
1132
  if (wildcardCount > MAX_WILDCARDS_PER_VALUE) return true;
490
1133
  return false;
491
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
+ }
492
1220
  function checkLengthLimits(value, maxLength = 4096) {
493
1221
  return value.length <= maxLength;
494
1222
  }
@@ -504,87 +1232,478 @@ function calculateShannonEntropy(str) {
504
1232
  for (const char of str) {
505
1233
  freq.set(char, (freq.get(char) ?? 0) + 1);
506
1234
  }
507
- let entropy = 0;
508
- const len = str.length;
509
- for (const count of freq.values()) {
510
- const p = count / len;
511
- if (p > 0) {
512
- entropy -= p * Math.log2(p);
513
- }
1235
+ let entropy = 0;
1236
+ const len = str.length;
1237
+ for (const count of freq.values()) {
1238
+ const p = count / len;
1239
+ if (p > 0) {
1240
+ entropy -= p * Math.log2(p);
1241
+ }
1242
+ }
1243
+ return entropy;
1244
+ }
1245
+ function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
1246
+ const threats = [];
1247
+ if (typeof value !== "string") {
1248
+ if (typeof value === "object" && value !== null) {
1249
+ return sanitizeObject(field, value, config);
1250
+ }
1251
+ return { safe: true, threats: [] };
1252
+ }
1253
+ if (config.pathTraversal && detectPathTraversal(value)) {
1254
+ threats.push({
1255
+ type: "PATH_TRAVERSAL",
1256
+ field,
1257
+ value: truncate(value, 100),
1258
+ description: "Path traversal pattern detected"
1259
+ });
1260
+ }
1261
+ if (config.shellInjection && detectShellInjection(value)) {
1262
+ threats.push({
1263
+ type: "SHELL_INJECTION",
1264
+ field,
1265
+ value: truncate(value, 100),
1266
+ description: "Shell injection pattern detected"
1267
+ });
1268
+ }
1269
+ if (config.wildcardAbuse && detectWildcardAbuse(value)) {
1270
+ threats.push({
1271
+ type: "WILDCARD_ABUSE",
1272
+ field,
1273
+ value: truncate(value, 100),
1274
+ description: "Wildcard abuse pattern detected"
1275
+ });
1276
+ }
1277
+ if (!checkLengthLimits(value, config.lengthLimit)) {
1278
+ threats.push({
1279
+ type: "LENGTH_EXCEEDED",
1280
+ field,
1281
+ value: `[${value.length} chars]`,
1282
+ description: `Value exceeds maximum length of ${config.lengthLimit}`
1283
+ });
1284
+ }
1285
+ if (config.entropyLimit && !checkEntropyLimits(value)) {
1286
+ threats.push({
1287
+ type: "HIGH_ENTROPY",
1288
+ field,
1289
+ value: truncate(value, 100),
1290
+ description: "High entropy string detected - possible encoded payload"
1291
+ });
1292
+ }
1293
+ if (config.ssrf && detectSSRF(value)) {
1294
+ threats.push({
1295
+ type: "SSRF",
1296
+ field,
1297
+ value: truncate(value, 100),
1298
+ description: "Server-side request forgery pattern detected \u2014 internal/metadata URL blocked"
1299
+ });
1300
+ }
1301
+ if (config.sqlInjection && detectSQLInjection(value)) {
1302
+ threats.push({
1303
+ type: "SQL_INJECTION",
1304
+ field,
1305
+ value: truncate(value, 100),
1306
+ description: "SQL injection pattern detected"
1307
+ });
1308
+ }
1309
+ return { safe: threats.length === 0, threats };
1310
+ }
1311
+ function sanitizeObject(basePath, obj, config) {
1312
+ const threats = [];
1313
+ if (Array.isArray(obj)) {
1314
+ for (let i = 0; i < obj.length; i++) {
1315
+ const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
1316
+ threats.push(...result.threats);
1317
+ }
1318
+ } else {
1319
+ for (const [key, val] of Object.entries(obj)) {
1320
+ const result = sanitizeInput(`${basePath}.${key}`, val, config);
1321
+ threats.push(...result.threats);
1322
+ }
1323
+ }
1324
+ return { safe: threats.length === 0, threats };
1325
+ }
1326
+ function truncate(str, maxLen) {
1327
+ return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
1328
+ }
1329
+ var DEFAULT_TOKEN_TTL_SECONDS = 30;
1330
+ var TOKEN_ALGORITHM = "HS256";
1331
+ var MIN_SECRET_LENGTH = 32;
1332
+
1333
+ // src/config.ts
1334
+ import { readFileSync, existsSync } from "fs";
1335
+ import { resolve } from "path";
1336
+ async function fetchCloudPolicy(apiKey, apiUrl, policyId) {
1337
+ const url = `${apiUrl}/api/v1/policies/${policyId ?? "default"}`;
1338
+ const res = await fetch(url, {
1339
+ headers: { "Authorization": `Bearer ${apiKey}` }
1340
+ });
1341
+ if (!res.ok) {
1342
+ const body = await res.text().catch(() => "");
1343
+ throw new Error(`Failed to fetch policy from cloud (${res.status}): ${body}`);
1344
+ }
1345
+ const data = await res.json();
1346
+ return {
1347
+ id: String(data.id ?? "cloud"),
1348
+ name: String(data.name ?? "Cloud Policy"),
1349
+ version: Number(data._version ?? 1),
1350
+ rules: data.rules ?? [],
1351
+ createdAt: String(data._created_at ?? ""),
1352
+ updatedAt: ""
1353
+ };
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);
514
1536
  }
515
- return entropy;
1537
+ throw new Error(
1538
+ `Unknown policy "${source}". Use a preset (${Object.keys(PRESETS).join(", ")}), a JSON file path, or a PolicySet object.`
1539
+ );
516
1540
  }
517
- function sanitizeInput(field, value, config = DEFAULT_INPUT_GUARD_CONFIG) {
518
- const threats = [];
519
- if (typeof value !== "string") {
520
- if (typeof value === "object" && value !== null) {
521
- return sanitizeObject(field, value, config);
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;
522
1596
  }
523
- return { safe: true, threats: [] };
524
- }
525
- if (config.pathTraversal && detectPathTraversal(value)) {
526
- threats.push({
527
- type: "PATH_TRAVERSAL",
528
- field,
529
- value: truncate(value, 100),
530
- description: "Path traversal pattern detected"
531
- });
532
- }
533
- if (config.shellInjection && detectShellInjection(value)) {
534
- threats.push({
535
- type: "SHELL_INJECTION",
536
- field,
537
- value: truncate(value, 100),
538
- description: "Shell injection pattern detected"
539
- });
540
- }
541
- if (config.wildcardAbuse && detectWildcardAbuse(value)) {
542
- threats.push({
543
- type: "WILDCARD_ABUSE",
544
- field,
545
- value: truncate(value, 100),
546
- description: "Wildcard abuse pattern detected"
547
- });
548
1597
  }
549
- if (!checkLengthLimits(value, config.lengthLimit)) {
550
- threats.push({
551
- type: "LENGTH_EXCEEDED",
552
- field,
553
- value: `[${value.length} chars]`,
554
- description: `Value exceeds maximum length of ${config.lengthLimit}`
555
- });
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
+ }
556
1607
  }
557
- if (config.entropyLimit && !checkEntropyLimits(value)) {
558
- threats.push({
559
- type: "HIGH_ENTROPY",
560
- field,
561
- value: truncate(value, 100),
562
- description: "High entropy string detected - possible encoded payload"
563
- });
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
+ );
564
1612
  }
565
- return { safe: threats.length === 0, threats };
566
- }
567
- function sanitizeObject(basePath, obj, config) {
568
- const threats = [];
569
- if (Array.isArray(obj)) {
570
- for (let i = 0; i < obj.length; i++) {
571
- const result = sanitizeInput(`${basePath}[${i}]`, obj[i], config);
572
- 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"');
573
1619
  }
574
- } else {
575
- for (const [key, val] of Object.entries(obj)) {
576
- const result = sanitizeInput(`${basePath}.${key}`, val, config);
577
- threats.push(...result.threats);
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
+ );
578
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
+ };
579
1637
  }
580
- return { safe: threats.length === 0, threats };
581
- }
582
- function truncate(str, maxLen) {
583
- return str.length > maxLen ? str.slice(0, maxLen) + "..." : str;
1638
+ if (upstreamUrl) {
1639
+ if (detectSSRF(upstreamUrl)) {
1640
+ throw new Error(
1641
+ `Upstream URL blocked: "${upstreamUrl}" points to an internal or private network address.
1642
+ SSRF protection prevents connecting to localhost, private IPs, or cloud metadata endpoints.`
1643
+ );
1644
+ }
1645
+ const transport = upstreamTransport ?? (upstreamUrl.includes("/sse") ? "sse" : "http");
1646
+ return {
1647
+ upstream: {
1648
+ transport,
1649
+ command: "",
1650
+ // not used for URL-based transports
1651
+ url: upstreamUrl
1652
+ },
1653
+ policy: loadPolicy(policySource),
1654
+ name,
1655
+ verbose,
1656
+ validateInput,
1657
+ rateLimitPerTool,
1658
+ globalRateLimit,
1659
+ apiKey,
1660
+ apiUrl,
1661
+ port
1662
+ };
1663
+ }
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
+ };
584
1687
  }
585
- var DEFAULT_TOKEN_TTL_SECONDS = 30;
586
- var TOKEN_ALGORITHM = "HS256";
587
- var MIN_SECRET_LENGTH = 32;
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";
588
1707
 
589
1708
  // ../policy-engine/dist/index.js
590
1709
  import { createHash } from "crypto";
@@ -735,8 +1854,51 @@ function trustLevelMeetsMinimum(actual, minimum) {
735
1854
  function argumentConstraintsMatch(constraints, args) {
736
1855
  for (const [key, constraint] of Object.entries(constraints)) {
737
1856
  if (!(key in args)) return false;
738
- if (typeof constraint === "string" && typeof args[key] === "string") {
739
- if (constraint !== "*" && args[key] !== constraint) return false;
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;
740
1902
  }
741
1903
  }
742
1904
  return true;
@@ -769,7 +1931,15 @@ function evaluatePolicy(policySet, request) {
769
1931
  matchedRule: null,
770
1932
  reason: "No matching policy rule found. Default action: DENY.",
771
1933
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
772
- 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
+ }
773
1943
  };
774
1944
  }
775
1945
  function validatePolicyRule(input) {
@@ -1091,6 +2261,7 @@ var PolicyStore = class {
1091
2261
 
1092
2262
  // ../sdk-ts/dist/index.js
1093
2263
  import { randomUUID, createHmac } from "crypto";
2264
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1094
2265
  var DEFAULT_CONFIG = Object.freeze({
1095
2266
  validateSchemas: true,
1096
2267
  enableLogging: true,
@@ -1185,7 +2356,7 @@ async function interceptToolCall(params, upstreamCall, options) {
1185
2356
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
1186
2357
  };
1187
2358
  options.onDecision?.(result);
1188
- const reason = options.verboseErrors ? `Input validation failed: ${threatDescriptions.join("; ")}` : "Input validation failed.";
2359
+ const reason = options.verboseErrors ? `Input validation failed: ${sanitization.threats.length} threat(s) detected` : "Input validation failed.";
1189
2360
  return createDeniedToolResult(reason);
1190
2361
  }
1191
2362
  }
@@ -1503,6 +2674,25 @@ var RateLimiter = class {
1503
2674
  const resetAt = this.globalRecords.length > 0 ? this.globalRecords[0].timestamp + this.windowMs : now + this.windowMs;
1504
2675
  return { allowed, remaining, resetAt };
1505
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
+ }
1506
2696
  /**
1507
2697
  * Records a tool call for rate limiting.
1508
2698
  * Call this after successful execution.
@@ -1558,6 +2748,16 @@ var RateLimiter = class {
1558
2748
  return active;
1559
2749
  }
1560
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
+ };
1561
2761
  var SolonGate = class {
1562
2762
  policyEngine;
1563
2763
  config;
@@ -1566,7 +2766,19 @@ var SolonGate = class {
1566
2766
  tokenIssuer;
1567
2767
  serverVerifier;
1568
2768
  rateLimiter;
2769
+ apiKey;
2770
+ licenseValidated = false;
1569
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;
1570
2782
  const { config, warnings } = resolveConfig(options.config);
1571
2783
  this.config = config;
1572
2784
  this.configWarnings = warnings;
@@ -1592,12 +2804,47 @@ var SolonGate = class {
1592
2804
  this.serverVerifier = config.gatewaySecret ? new ServerVerifier({ gatewaySecret: config.gatewaySecret }) : null;
1593
2805
  this.rateLimiter = new RateLimiter();
1594
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
+ }
1595
2841
  /**
1596
2842
  * Intercept and evaluate a tool call against the full security pipeline.
1597
2843
  * If denied at any stage, returns an error result without calling upstream.
1598
2844
  * If allowed, calls upstream and returns the result.
1599
2845
  */
1600
2846
  async executeToolCall(params, upstreamCall) {
2847
+ await this.validateLicense();
1601
2848
  return interceptToolCall(params, upstreamCall, {
1602
2849
  policyEngine: this.policyEngine,
1603
2850
  validateSchemas: this.config.validateSchemas,
@@ -1647,8 +2894,8 @@ var Mutex = class {
1647
2894
  this.locked = true;
1648
2895
  return;
1649
2896
  }
1650
- return new Promise((resolve2) => {
1651
- this.queue.push(resolve2);
2897
+ return new Promise((resolve5) => {
2898
+ this.queue.push(resolve5);
1652
2899
  });
1653
2900
  }
1654
2901
  release() {
@@ -1689,30 +2936,88 @@ var SolonGateProxy = class {
1689
2936
  */
1690
2937
  async start() {
1691
2938
  log("Starting SolonGate Proxy...");
2939
+ const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
2940
+ if (this.config.apiKey) {
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
+ }
2964
+ try {
2965
+ const cloudPolicy = await fetchCloudPolicy(this.config.apiKey, apiUrl);
2966
+ this.config.policy = cloudPolicy;
2967
+ log(`Loaded cloud policy: ${cloudPolicy.name} (${cloudPolicy.rules.length} rules)`);
2968
+ } catch (err) {
2969
+ log(`Cloud policy fetch failed, using local policy: ${err instanceof Error ? err.message : String(err)}`);
2970
+ }
2971
+ }
1692
2972
  log(`Policy: ${this.config.policy.name} (${this.config.policy.rules.length} rules)`);
1693
- log(`Upstream: ${this.config.upstream.command} ${(this.config.upstream.args ?? []).join(" ")}`);
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
+ }
1694
2979
  await this.connectUpstream();
1695
2980
  await this.discoverTools();
1696
2981
  this.createServer();
1697
2982
  await this.serve();
1698
2983
  }
1699
2984
  /**
1700
- * Connect to the upstream MCP server by spawning it as a child process.
2985
+ * Connect to the upstream MCP server.
2986
+ * Supports stdio (child process), SSE, and StreamableHTTP transports.
1701
2987
  */
1702
2988
  async connectUpstream() {
1703
2989
  this.client = new Client(
1704
2990
  { name: "solongate-proxy-client", version: "0.1.0" },
1705
2991
  { capabilities: {} }
1706
2992
  );
1707
- const transport = new StdioClientTransport({
1708
- command: this.config.upstream.command,
1709
- args: this.config.upstream.args,
1710
- env: this.config.upstream.env,
1711
- cwd: this.config.upstream.cwd,
1712
- stderr: "pipe"
1713
- });
1714
- await this.client.connect(transport);
1715
- log("Connected to upstream server");
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})`);
1716
3021
  }
1717
3022
  /**
1718
3023
  * Discover tools from the upstream server.
@@ -1751,10 +3056,20 @@ var SolonGateProxy = class {
1751
3056
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1752
3057
  return { tools: this.upstreamTools };
1753
3058
  });
3059
+ const MAX_ARGUMENT_SIZE = 1024 * 1024;
1754
3060
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1755
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
+ }
1756
3070
  log(`Tool call: ${name}`);
1757
3071
  await this.callMutex.acquire();
3072
+ const startTime = Date.now();
1758
3073
  try {
1759
3074
  const result = await this.gate.executeToolCall(
1760
3075
  { name, arguments: args ?? {} },
@@ -1767,7 +3082,19 @@ var SolonGateProxy = class {
1767
3082
  return upstreamResult;
1768
3083
  }
1769
3084
  );
1770
- log(`Result: ${result.isError ? "DENIED/ERROR" : "ALLOWED"}`);
3085
+ const decision = result.isError ? "DENY" : "ALLOW";
3086
+ const evaluationTimeMs = Date.now() - startTime;
3087
+ log(`Result: ${decision} (${evaluationTimeMs}ms)`);
3088
+ if (this.config.apiKey) {
3089
+ const apiUrl = this.config.apiUrl ?? "https://api.solongate.com";
3090
+ sendAuditLog(this.config.apiKey, apiUrl, {
3091
+ tool: name,
3092
+ arguments: args ?? {},
3093
+ decision,
3094
+ reason: result.isError ? result.content[0]?.text ?? "denied" : "allowed",
3095
+ evaluationTimeMs
3096
+ });
3097
+ }
1771
3098
  return {
1772
3099
  content: [...result.content],
1773
3100
  isError: result.isError
@@ -1813,14 +3140,38 @@ var SolonGateProxy = class {
1813
3140
  });
1814
3141
  }
1815
3142
  /**
1816
- * Start serving on stdio (downstream to Claude).
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).
1817
3146
  */
1818
3147
  async serve() {
1819
3148
  if (!this.server) throw new Error("Server not created");
1820
- const transport = new StdioServerTransport();
1821
- await this.server.connect(transport);
1822
- log("Proxy is live. All tool calls are now protected by SolonGate.");
1823
- log("Waiting for requests...");
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
+ }
1824
3175
  }
1825
3176
  };
1826
3177
 
@@ -1837,7 +3188,23 @@ console.error = (...args) => {
1837
3188
  process.stderr.write(`[SolonGate ERROR] ${args.map(String).join(" ")}
1838
3189
  `);
1839
3190
  };
1840
- async function main() {
3191
+ async function main4() {
3192
+ const subcommand = process.argv[2];
3193
+ if (subcommand === "init") {
3194
+ process.argv.splice(2, 1);
3195
+ await Promise.resolve().then(() => (init_init(), init_exports));
3196
+ return;
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
+ }
1841
3208
  try {
1842
3209
  const config = parseArgs(process.argv);
1843
3210
  const proxy = new SolonGateProxy(config);
@@ -1849,4 +3216,4 @@ async function main() {
1849
3216
  process.exit(1);
1850
3217
  }
1851
3218
  }
1852
- main();
3219
+ main4();