@suveren/gateway 0.19.1 → 0.19.2
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/bin/suveren-gateway.js
CHANGED
|
@@ -1610,6 +1610,10 @@ Environment:
|
|
|
1610
1610
|
SUVEREN_DATA_DIR Data directory (default ~/.suveren) — overrides a saved \`config set data-dir\`
|
|
1611
1611
|
SUVEREN_AS_URL Authority Server URL — overrides the saved as-url
|
|
1612
1612
|
SUVEREN_SIMULATION 1 to force simulation mode for this run — overrides the saved setting
|
|
1613
|
+
SUVEREN_BIND_HOST Listen address (default 127.0.0.1 — this machine only). Anything
|
|
1614
|
+
else lets other machines connect as your agent, so it needs:
|
|
1615
|
+
SUVEREN_MCP_TOKEN Token an AI assistant must send to open a session
|
|
1616
|
+
(Authorization: Bearer <token>, or ?token=<token>)
|
|
1613
1617
|
HTTP_PROXY / HTTPS_PROXY / NO_PROXY (upper or lower case)
|
|
1614
1618
|
Corporate proxy — honoured for every outbound call to the Authority
|
|
1615
1619
|
Server, the update checker, and a remote AI assistant endpoint. A
|
|
@@ -5045,8 +5045,9 @@ if (existsSync11(UI_DIST)) {
|
|
|
5045
5045
|
});
|
|
5046
5046
|
});
|
|
5047
5047
|
}
|
|
5048
|
-
|
|
5049
|
-
|
|
5048
|
+
var bindHost = process.env.SUVEREN_BIND_HOST?.trim() || "127.0.0.1";
|
|
5049
|
+
app.listen(port, bindHost, () => {
|
|
5050
|
+
console.error(`[Control Plane] Listening on http://${bindHost}:${port}`);
|
|
5050
5051
|
if (process.env.SUVEREN_AUTOSTART === "1" && !vault.isUnlocked()) {
|
|
5051
5052
|
const { title, message, url } = lockedNotification(port);
|
|
5052
5053
|
notify(title, message, process.platform, url);
|
package/dist/mcp-server/http.mjs
CHANGED
|
@@ -5192,6 +5192,60 @@ function safeHost(url) {
|
|
|
5192
5192
|
}
|
|
5193
5193
|
}
|
|
5194
5194
|
|
|
5195
|
+
// src/lib/connector-env.ts
|
|
5196
|
+
var RUNTIME_VARS = [
|
|
5197
|
+
// POSIX
|
|
5198
|
+
"HOME",
|
|
5199
|
+
"USER",
|
|
5200
|
+
"LOGNAME",
|
|
5201
|
+
"SHELL",
|
|
5202
|
+
"TERM",
|
|
5203
|
+
"TMPDIR",
|
|
5204
|
+
"LANG",
|
|
5205
|
+
"LC_ALL",
|
|
5206
|
+
"LC_CTYPE",
|
|
5207
|
+
"TZ",
|
|
5208
|
+
"XDG_CONFIG_HOME",
|
|
5209
|
+
"XDG_DATA_HOME",
|
|
5210
|
+
"XDG_CACHE_HOME",
|
|
5211
|
+
"XDG_RUNTIME_DIR",
|
|
5212
|
+
// Windows (names are case-insensitive there; matched case-insensitively below)
|
|
5213
|
+
"APPDATA",
|
|
5214
|
+
"LOCALAPPDATA",
|
|
5215
|
+
"USERPROFILE",
|
|
5216
|
+
"USERNAME",
|
|
5217
|
+
"HOMEDRIVE",
|
|
5218
|
+
"HOMEPATH",
|
|
5219
|
+
"SYSTEMROOT",
|
|
5220
|
+
"SYSTEMDRIVE",
|
|
5221
|
+
"WINDIR",
|
|
5222
|
+
"COMSPEC",
|
|
5223
|
+
"PATHEXT",
|
|
5224
|
+
"TEMP",
|
|
5225
|
+
"TMP",
|
|
5226
|
+
"PROGRAMFILES",
|
|
5227
|
+
"PROGRAMFILES(X86)",
|
|
5228
|
+
"PROGRAMDATA",
|
|
5229
|
+
"PROCESSOR_ARCHITECTURE",
|
|
5230
|
+
"NUMBER_OF_PROCESSORS"
|
|
5231
|
+
];
|
|
5232
|
+
var NETWORK_VARS = [
|
|
5233
|
+
"HTTP_PROXY",
|
|
5234
|
+
"HTTPS_PROXY",
|
|
5235
|
+
"NO_PROXY",
|
|
5236
|
+
"NODE_EXTRA_CA_CERTS",
|
|
5237
|
+
"SSL_CERT_FILE"
|
|
5238
|
+
];
|
|
5239
|
+
function inheritedConnectorEnv(parent, passEnv = []) {
|
|
5240
|
+
const allowed = new Set([...RUNTIME_VARS, ...NETWORK_VARS, ...passEnv].map((n) => n.toUpperCase()));
|
|
5241
|
+
const out = {};
|
|
5242
|
+
for (const [name, value] of Object.entries(parent)) {
|
|
5243
|
+
if (value === void 0) continue;
|
|
5244
|
+
if (allowed.has(name.toUpperCase())) out[name] = value;
|
|
5245
|
+
}
|
|
5246
|
+
return out;
|
|
5247
|
+
}
|
|
5248
|
+
|
|
5195
5249
|
// src/lib/integration-manager.ts
|
|
5196
5250
|
var DEFAULT_DATA_DIR2 = process.env.SUVEREN_DATA_DIR ?? join14(homedir10(), ".suveren");
|
|
5197
5251
|
var INTEGRATIONS_DIR = process.env.SUVEREN_INTEGRATIONS_DIR ?? join14(DEFAULT_DATA_DIR2, "integrations");
|
|
@@ -5585,7 +5639,8 @@ var IntegrationManager = class {
|
|
|
5585
5639
|
if (envVar) env[envVar] = "simulation";
|
|
5586
5640
|
}
|
|
5587
5641
|
}
|
|
5588
|
-
const
|
|
5642
|
+
const inherited = inheritedConnectorEnv(process.env, getManifest(config.id)?.mcp.passEnv);
|
|
5643
|
+
const interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? inherited[name] ?? "");
|
|
5589
5644
|
const args = config.args.map(interpolate);
|
|
5590
5645
|
const remote = remotePreflightTarget(config.command, args);
|
|
5591
5646
|
if (remote) {
|
|
@@ -5595,13 +5650,13 @@ var IntegrationManager = class {
|
|
|
5595
5650
|
const transport = new StdioClientTransport({
|
|
5596
5651
|
command: config.command,
|
|
5597
5652
|
args,
|
|
5598
|
-
//
|
|
5599
|
-
//
|
|
5600
|
-
//
|
|
5601
|
-
//
|
|
5602
|
-
//
|
|
5653
|
+
// Only an allowlist of the gateway's environment — never its own
|
|
5654
|
+
// secrets (connector-env.ts). The allowlist includes
|
|
5655
|
+
// NODE_EXTRA_CA_CERTS, which is how a saved `--ca-file` reaches an
|
|
5656
|
+
// integration: bundle/server.js sets it on THIS process before the MCP
|
|
5657
|
+
// server starts, and Node reads it only at process start.
|
|
5603
5658
|
env: {
|
|
5604
|
-
...
|
|
5659
|
+
...inherited,
|
|
5605
5660
|
PATH: buildPath(),
|
|
5606
5661
|
HAP_DATA_DIR: DEFAULT_DATA_DIR2,
|
|
5607
5662
|
...config.env,
|
|
@@ -6275,6 +6330,73 @@ async function scopeReportSourcesToStoredWindow(base, stored, opts) {
|
|
|
6275
6330
|
return { ok: true, window, sources: scopedSources(base, window, loadedAt) };
|
|
6276
6331
|
}
|
|
6277
6332
|
|
|
6333
|
+
// src/lib/agent-access.ts
|
|
6334
|
+
import { timingSafeEqual } from "crypto";
|
|
6335
|
+
var DEFAULT_BIND_HOST = "127.0.0.1";
|
|
6336
|
+
function resolveBindHost(env = process.env) {
|
|
6337
|
+
const raw = env.SUVEREN_BIND_HOST?.trim();
|
|
6338
|
+
return raw ? raw : DEFAULT_BIND_HOST;
|
|
6339
|
+
}
|
|
6340
|
+
function isLoopbackBind(host) {
|
|
6341
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
6342
|
+
return h === "localhost" || h === "::1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
6343
|
+
}
|
|
6344
|
+
function resolveAgentToken(env = process.env) {
|
|
6345
|
+
return env.SUVEREN_MCP_TOKEN?.trim() ?? "";
|
|
6346
|
+
}
|
|
6347
|
+
function bindRefusal(env = process.env) {
|
|
6348
|
+
const host = resolveBindHost(env);
|
|
6349
|
+
if (isLoopbackBind(host)) return null;
|
|
6350
|
+
if (resolveAgentToken(env)) return null;
|
|
6351
|
+
if (env.SUVEREN_CONTAINER === "1") return null;
|
|
6352
|
+
return `SUVEREN_BIND_HOST=${host} would let other machines connect as this person's agent, but SUVEREN_MCP_TOKEN is not set. Set a token (long and random), or remove SUVEREN_BIND_HOST to listen on this machine only.`;
|
|
6353
|
+
}
|
|
6354
|
+
function hostnameOf(hostHeader) {
|
|
6355
|
+
const trimmed = hostHeader.trim().toLowerCase();
|
|
6356
|
+
const bracketed = trimmed.match(/^\[([^\]]+)\](?::\d+)?$/);
|
|
6357
|
+
if (bracketed) return bracketed[1];
|
|
6358
|
+
return trimmed.replace(/:\d+$/, "");
|
|
6359
|
+
}
|
|
6360
|
+
function isLocalHost(hostHeader) {
|
|
6361
|
+
if (!hostHeader) return false;
|
|
6362
|
+
const name = hostnameOf(hostHeader);
|
|
6363
|
+
if (name === "localhost" || name.endsWith(".localhost")) return true;
|
|
6364
|
+
if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(name)) return true;
|
|
6365
|
+
if (name.includes(":")) return true;
|
|
6366
|
+
return false;
|
|
6367
|
+
}
|
|
6368
|
+
function presentedToken(req) {
|
|
6369
|
+
const auth = req.headers.authorization;
|
|
6370
|
+
if (typeof auth === "string") {
|
|
6371
|
+
const m = auth.match(/^Bearer\s+(.+)$/i);
|
|
6372
|
+
if (m) return m[1].trim();
|
|
6373
|
+
}
|
|
6374
|
+
const q = req.query.token;
|
|
6375
|
+
return typeof q === "string" ? q : "";
|
|
6376
|
+
}
|
|
6377
|
+
function sameToken(a, b) {
|
|
6378
|
+
const ab = Buffer.from(a);
|
|
6379
|
+
const bb = Buffer.from(b);
|
|
6380
|
+
return ab.length === bb.length && timingSafeEqual(ab, bb);
|
|
6381
|
+
}
|
|
6382
|
+
function agentAccess(token, opensSession) {
|
|
6383
|
+
return (req, res, next) => {
|
|
6384
|
+
if (!token) {
|
|
6385
|
+
if (!isLocalHost(req.headers.host)) {
|
|
6386
|
+
res.status(403).json({ error: "Forbidden host" });
|
|
6387
|
+
return;
|
|
6388
|
+
}
|
|
6389
|
+
next();
|
|
6390
|
+
return;
|
|
6391
|
+
}
|
|
6392
|
+
if (opensSession(req) && !sameToken(presentedToken(req), token)) {
|
|
6393
|
+
res.status(401).json({ error: "Missing or wrong gateway token" });
|
|
6394
|
+
return;
|
|
6395
|
+
}
|
|
6396
|
+
next();
|
|
6397
|
+
};
|
|
6398
|
+
}
|
|
6399
|
+
|
|
6278
6400
|
// src/lib/profile-loader.ts
|
|
6279
6401
|
import { readFileSync as readFileSync16, existsSync as existsSync15 } from "fs";
|
|
6280
6402
|
import { join as join15, resolve as resolve2 } from "path";
|
|
@@ -7008,6 +7130,13 @@ var dataDir = process.env.SUVEREN_DATA_DIR ?? join19(homedir11(), ".suveren");
|
|
|
7008
7130
|
var spUrl = resolveAsUrl(dataDir);
|
|
7009
7131
|
validatePinTlsForUrl(resolvePinTls(dataDir), spUrl);
|
|
7010
7132
|
var port = parseInt(process.env.SUVEREN_MCP_PORT ?? "3430", 10);
|
|
7133
|
+
var bindHost = resolveBindHost();
|
|
7134
|
+
var agentToken = resolveAgentToken();
|
|
7135
|
+
var bindProblem = bindRefusal();
|
|
7136
|
+
if (bindProblem) {
|
|
7137
|
+
console.error(`[Suveren MCP] Refusing to start: ${bindProblem}`);
|
|
7138
|
+
process.exit(1);
|
|
7139
|
+
}
|
|
7011
7140
|
setAsBaseUrl(spUrl);
|
|
7012
7141
|
var state = new SharedState(spUrl, void 0, dataDir);
|
|
7013
7142
|
var reportSources = {
|
|
@@ -7051,16 +7180,6 @@ integrationManager.setOnConfigMigrated((id, updates) => {
|
|
|
7051
7180
|
});
|
|
7052
7181
|
var app = express();
|
|
7053
7182
|
app.use(express.json());
|
|
7054
|
-
app.use((_req, res, next) => {
|
|
7055
|
-
res.header("Access-Control-Allow-Origin", "http://localhost:3000");
|
|
7056
|
-
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
7057
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, mcp-session-id");
|
|
7058
|
-
if (_req.method === "OPTIONS") {
|
|
7059
|
-
res.sendStatus(204);
|
|
7060
|
-
return;
|
|
7061
|
-
}
|
|
7062
|
-
next();
|
|
7063
|
-
});
|
|
7064
7183
|
var INTERNAL_SECRET = process.env.SUVEREN_INTERNAL_SECRET ?? "";
|
|
7065
7184
|
function internalOnly(req, res, next) {
|
|
7066
7185
|
const ip = req.ip ?? req.socket.remoteAddress ?? "";
|
|
@@ -7296,7 +7415,11 @@ app.post("/internal/stop-all-running", internalOnly, async (_req, res) => {
|
|
|
7296
7415
|
}
|
|
7297
7416
|
});
|
|
7298
7417
|
var sseSessions = /* @__PURE__ */ new Map();
|
|
7299
|
-
|
|
7418
|
+
var guardAgent = agentAccess(
|
|
7419
|
+
agentToken,
|
|
7420
|
+
(req) => req.path === "/sse" && req.method === "GET" || req.path === "/mcp" && req.method === "POST" && !req.headers["mcp-session-id"]
|
|
7421
|
+
);
|
|
7422
|
+
app.get("/sse", guardAgent, async (_req, res) => {
|
|
7300
7423
|
const transport = new SSEServerTransport("/messages", res);
|
|
7301
7424
|
const { server, refreshTools, registerProxiedTools } = createMcpServer(state, integrationManager);
|
|
7302
7425
|
const sessionId = transport.sessionId;
|
|
@@ -7308,12 +7431,9 @@ app.get("/sse", async (_req, res) => {
|
|
|
7308
7431
|
activeSessions.delete(sessionId);
|
|
7309
7432
|
console.error(`[Suveren MCP] SSE session ${sessionId} closed`);
|
|
7310
7433
|
});
|
|
7311
|
-
server.registerTool("debug_test_tool", { description: "Debug test" }, async () => ({
|
|
7312
|
-
content: [{ type: "text", text: "debug" }]
|
|
7313
|
-
}));
|
|
7314
7434
|
await server.connect(transport);
|
|
7315
7435
|
});
|
|
7316
|
-
app.post("/messages", async (req, res) => {
|
|
7436
|
+
app.post("/messages", guardAgent, async (req, res) => {
|
|
7317
7437
|
const sessionId = req.query.sessionId;
|
|
7318
7438
|
const transport = sseSessions.get(sessionId);
|
|
7319
7439
|
if (!transport) {
|
|
@@ -7323,7 +7443,7 @@ app.post("/messages", async (req, res) => {
|
|
|
7323
7443
|
await transport.handlePostMessage(req, res, req.body);
|
|
7324
7444
|
});
|
|
7325
7445
|
var streamableSessions = /* @__PURE__ */ new Map();
|
|
7326
|
-
app.all("/mcp", async (req, res) => {
|
|
7446
|
+
app.all("/mcp", guardAgent, async (req, res) => {
|
|
7327
7447
|
const sessionId = req.headers["mcp-session-id"];
|
|
7328
7448
|
if (req.method === "GET" || req.method === "POST" || req.method === "DELETE") {
|
|
7329
7449
|
if (sessionId && streamableSessions.has(sessionId)) {
|
|
@@ -7642,10 +7762,11 @@ process.on("SIGINT", async () => {
|
|
|
7642
7762
|
await integrationManager.shutdown();
|
|
7643
7763
|
process.exit(0);
|
|
7644
7764
|
});
|
|
7645
|
-
app.listen(port,
|
|
7646
|
-
console.error(`[Suveren MCP] HTTP server listening on http
|
|
7647
|
-
console.error(`[Suveren MCP] SSE: http
|
|
7648
|
-
console.error(`[Suveren MCP] Streamable: http
|
|
7765
|
+
app.listen(port, bindHost, () => {
|
|
7766
|
+
console.error(`[Suveren MCP] HTTP server listening on http://${bindHost}:${port}`);
|
|
7767
|
+
console.error(`[Suveren MCP] SSE: http://${bindHost}:${port}/sse`);
|
|
7768
|
+
console.error(`[Suveren MCP] Streamable: http://${bindHost}:${port}/mcp`);
|
|
7769
|
+
console.error(`[Suveren MCP] Agent token: ${agentToken ? "required" : "not set (this machine only)"}`);
|
|
7649
7770
|
console.error(`[Suveren MCP] SP server: ${spUrl}`);
|
|
7650
7771
|
profilesLoaded = loadProfiles();
|
|
7651
7772
|
loadManifests();
|