@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.
@@ -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
@@ -9,7 +9,8 @@
9
9
  "npmVersion": "0.4.0",
10
10
  "mcp": {
11
11
  "command": "erp-mcp",
12
- "args": []
12
+ "args": [],
13
+ "passEnv": ["ERP_COMPANY_FILE"]
13
14
  },
14
15
  "credentials": {
15
16
  "fields": [
@@ -5045,8 +5045,9 @@ if (existsSync11(UI_DIST)) {
5045
5045
  });
5046
5046
  });
5047
5047
  }
5048
- app.listen(port, "0.0.0.0", () => {
5049
- console.error(`[Control Plane] Listening on http://0.0.0.0:${port}`);
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);
@@ -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 interpolate = (s) => s.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_m, name) => env[name] ?? process.env[name] ?? "");
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
- // `...process.env` is also how a saved `--ca-file` reaches an
5599
- // integration: NODE_EXTRA_CA_CERTS (read by Node only at process
5600
- // start) is already set on THIS process by bundle/server.js before
5601
- // the MCP server ever starts, so every integration this manager spawns
5602
- // inherits it here for free — no separate handling needed.
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
- ...process.env,
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
- app.get("/sse", async (_req, res) => {
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, "0.0.0.0", () => {
7646
- console.error(`[Suveren MCP] HTTP server listening on http://0.0.0.0:${port}`);
7647
- console.error(`[Suveren MCP] SSE: http://0.0.0.0:${port}/sse`);
7648
- console.error(`[Suveren MCP] Streamable: http://0.0.0.0:${port}/mcp`);
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();