@mhingston5/conduit 1.1.7 → 1.1.8

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/README.md CHANGED
@@ -108,6 +108,7 @@ npx conduit auth \
108
108
  --auth-url <url> \
109
109
  --token-url <url> \
110
110
  --scopes <scopes>
111
+ ```
111
112
 
112
113
  For Atlassian (3LO), include `offline_access` and set the audience:
113
114
 
@@ -119,7 +120,6 @@ npx conduit auth \
119
120
  --token-url "https://auth.atlassian.com/oauth/token" \
120
121
  --scopes "offline_access,read:me"
121
122
  ```
122
- ```
123
123
 
124
124
  This will start a temporary local server, open your browser for authorization, and print the generated `credentials` block for your `conduit.yaml`.
125
125
 
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
+ import { createRequire } from "module";
5
6
 
6
7
  // src/core/config.service.ts
7
8
  import { z } from "zod";
@@ -1083,6 +1084,9 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
1083
1084
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
1084
1085
  import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
1085
1086
  import { z as z2 } from "zod";
1087
+ import dns from "dns";
1088
+ import net2 from "net";
1089
+ import { Agent } from "undici";
1086
1090
  var UpstreamClient = class {
1087
1091
  logger;
1088
1092
  info;
@@ -1091,6 +1095,9 @@ var UpstreamClient = class {
1091
1095
  mcpClient;
1092
1096
  transport;
1093
1097
  connected = false;
1098
+ // Pinned-IP dispatchers per upstream origin (defends against DNS rebinding)
1099
+ dispatcherCache = /* @__PURE__ */ new Map();
1100
+ pinned;
1094
1101
  constructor(logger, info, authService, urlValidator) {
1095
1102
  this.logger = logger.child({ upstreamId: info.id });
1096
1103
  this.info = info;
@@ -1116,7 +1123,9 @@ var UpstreamClient = class {
1116
1123
  return;
1117
1124
  }
1118
1125
  if (this.info.type === "streamableHttp") {
1119
- this.transport = new StreamableHTTPClientTransport(new URL(this.info.url), {
1126
+ const upstreamUrl = new URL(this.info.url);
1127
+ this.pinned = { origin: upstreamUrl.origin, hostname: upstreamUrl.hostname };
1128
+ this.transport = new StreamableHTTPClientTransport(upstreamUrl, {
1120
1129
  fetch: this.createAuthedFetch()
1121
1130
  });
1122
1131
  this.mcpClient = new Client({
@@ -1128,6 +1137,8 @@ var UpstreamClient = class {
1128
1137
  return;
1129
1138
  }
1130
1139
  if (this.info.type === "sse") {
1140
+ const upstreamUrl = new URL(this.info.url);
1141
+ this.pinned = { origin: upstreamUrl.origin, hostname: upstreamUrl.hostname };
1131
1142
  this.mcpClient = new Client({
1132
1143
  name: "conduit-gateway",
1133
1144
  version: "1.0.0"
@@ -1136,16 +1147,65 @@ var UpstreamClient = class {
1136
1147
  });
1137
1148
  }
1138
1149
  }
1150
+ getDispatcher(origin, hostname, resolvedIp) {
1151
+ const existing = this.dispatcherCache.get(origin);
1152
+ if (existing && existing.resolvedIp === resolvedIp) {
1153
+ return existing.agent;
1154
+ }
1155
+ if (existing) {
1156
+ try {
1157
+ existing.agent.close();
1158
+ } catch {
1159
+ }
1160
+ }
1161
+ const agent = new Agent({
1162
+ connect: {
1163
+ lookup: (lookupHostname, options, callback) => {
1164
+ if (lookupHostname === hostname) {
1165
+ callback(null, resolvedIp, net2.isIP(resolvedIp));
1166
+ return;
1167
+ }
1168
+ dns.lookup(lookupHostname, options, callback);
1169
+ }
1170
+ }
1171
+ });
1172
+ this.dispatcherCache.set(origin, { resolvedIp, agent });
1173
+ return agent;
1174
+ }
1139
1175
  createAuthedFetch() {
1140
1176
  const creds = this.info.credentials;
1141
- if (!creds) return fetch;
1177
+ const pinned = this.pinned;
1178
+ const baseFetch = fetch;
1142
1179
  return async (input, init = {}) => {
1143
- const headers = new Headers(init.headers || {});
1144
- const authHeaders = await this.authService.getAuthHeaders(creds);
1145
- for (const [k, v] of Object.entries(authHeaders)) {
1146
- headers.set(k, v);
1180
+ const requestUrlStr = (() => {
1181
+ if (typeof input === "string") return input;
1182
+ if (input instanceof URL) return input.toString();
1183
+ if (input instanceof Request) return input.url;
1184
+ return String(input);
1185
+ })();
1186
+ const requestUrl = pinned ? new URL(requestUrlStr, pinned.origin) : new URL(requestUrlStr);
1187
+ if (pinned && requestUrl.origin !== pinned.origin) {
1188
+ throw new Error(`Forbidden upstream redirect/origin: ${requestUrl.origin}`);
1147
1189
  }
1148
- return fetch(input, { ...init, headers });
1190
+ if (pinned && !pinned.resolvedIp) {
1191
+ const securityResult = await this.urlValidator.validateUrl(pinned.origin);
1192
+ if (!securityResult.valid) {
1193
+ throw new Error(securityResult.message || "Forbidden URL");
1194
+ }
1195
+ pinned.resolvedIp = securityResult.resolvedIp;
1196
+ }
1197
+ const headers = new Headers((input instanceof Request ? input.headers : void 0) || void 0);
1198
+ const initHeaders = new Headers(init.headers || {});
1199
+ for (const [k, v] of initHeaders.entries()) headers.set(k, v);
1200
+ if (creds) {
1201
+ const authHeaders = await this.authService.getAuthHeaders(creds);
1202
+ for (const [k, v] of Object.entries(authHeaders)) {
1203
+ headers.set(k, v);
1204
+ }
1205
+ }
1206
+ const request = input instanceof Request ? new Request(input, { ...init, headers, redirect: init.redirect ?? "manual" }) : new Request(requestUrl.toString(), { ...init, headers, redirect: init.redirect ?? "manual" });
1207
+ const dispatcher = pinned && pinned.resolvedIp ? this.getDispatcher(pinned.origin, pinned.hostname, pinned.resolvedIp) : void 0;
1208
+ return baseFetch(request, dispatcher ? { dispatcher } : void 0);
1149
1209
  };
1150
1210
  }
1151
1211
  async ensureConnected() {
@@ -1166,6 +1226,9 @@ var UpstreamClient = class {
1166
1226
  this.logger.error({ url: this.info.url }, "Blocked upstream URL (SSRF)");
1167
1227
  throw new Error(securityResult.message || "Forbidden URL");
1168
1228
  }
1229
+ if (this.pinned) {
1230
+ this.pinned.resolvedIp = securityResult.resolvedIp;
1231
+ }
1169
1232
  }
1170
1233
  try {
1171
1234
  this.logger.debug("Connecting to upstream transport...");
@@ -1935,8 +1998,8 @@ var GatewayService = class {
1935
1998
  };
1936
1999
 
1937
2000
  // src/core/network.policy.service.ts
1938
- import dns from "dns/promises";
1939
- import net2 from "net";
2001
+ import dns2 from "dns/promises";
2002
+ import net3 from "net";
1940
2003
  import { LRUCache as LRUCache2 } from "lru-cache";
1941
2004
  var NetworkPolicyService = class {
1942
2005
  logger;
@@ -1977,9 +2040,9 @@ var NetworkPolicyService = class {
1977
2040
  return { valid: false, message: "Access denied: private network access forbidden" };
1978
2041
  }
1979
2042
  }
1980
- if (!net2.isIP(hostname)) {
2043
+ if (!net3.isIP(hostname)) {
1981
2044
  try {
1982
- const lookup = await dns.lookup(hostname, { all: true });
2045
+ const lookup = await dns2.lookup(hostname, { all: true });
1983
2046
  const resolvedIps = [];
1984
2047
  for (const address of lookup) {
1985
2048
  let ip = address.address;
@@ -2080,13 +2143,14 @@ var SecurityService = class {
2080
2143
  checkRateLimit(key) {
2081
2144
  return this.networkPolicy.checkRateLimit(key);
2082
2145
  }
2083
- validateIpcToken(token) {
2084
- if (!this.ipcToken) {
2085
- return true;
2086
- }
2146
+ isMasterToken(token) {
2147
+ if (!this.ipcToken) return true;
2087
2148
  const expected = Buffer.from(this.ipcToken);
2088
- const actual = Buffer.from(token);
2089
- if (expected.length === actual.length && crypto2.timingSafeEqual(expected, actual)) {
2149
+ const actual = Buffer.from(token || "");
2150
+ return expected.length === actual.length && crypto2.timingSafeEqual(expected, actual);
2151
+ }
2152
+ validateIpcToken(token) {
2153
+ if (this.isMasterToken(token)) {
2090
2154
  return true;
2091
2155
  }
2092
2156
  return !!this.sessionManager.getSession(token);
@@ -2709,26 +2773,30 @@ var IsolateExecutor = class {
2709
2773
  const jail = ctx.global;
2710
2774
  let currentLogBytes = 0;
2711
2775
  let currentErrorBytes = 0;
2776
+ let totalLogEntries = 0;
2712
2777
  await jail.set("__log", new ivm.Callback((msg) => {
2778
+ if (totalLogEntries + 1 > limits.maxLogEntries) {
2779
+ throw new Error("[LIMIT_LOG_ENTRIES]");
2780
+ }
2713
2781
  if (currentLogBytes + msg.length + 1 > limits.maxOutputBytes) {
2714
2782
  throw new Error("[LIMIT_LOG]");
2715
2783
  }
2716
- if (currentLogBytes < limits.maxOutputBytes) {
2717
- logs.push(msg);
2718
- currentLogBytes += msg.length + 1;
2719
- }
2784
+ totalLogEntries++;
2785
+ logs.push(msg);
2786
+ currentLogBytes += msg.length + 1;
2720
2787
  }));
2721
2788
  await jail.set("__error", new ivm.Callback((msg) => {
2789
+ if (totalLogEntries + 1 > limits.maxLogEntries) {
2790
+ throw new Error("[LIMIT_LOG_ENTRIES]");
2791
+ }
2722
2792
  if (currentErrorBytes + msg.length + 1 > limits.maxOutputBytes) {
2723
2793
  throw new Error("[LIMIT_OUTPUT]");
2724
2794
  }
2725
- if (currentErrorBytes < limits.maxOutputBytes) {
2726
- errors.push(msg);
2727
- currentErrorBytes += msg.length + 1;
2728
- }
2795
+ totalLogEntries++;
2796
+ errors.push(msg);
2797
+ currentErrorBytes += msg.length + 1;
2729
2798
  }));
2730
2799
  let requestIdCounter = 0;
2731
- const pendingToolCalls = /* @__PURE__ */ new Map();
2732
2800
  await jail.set("__dispatchToolCall", new ivm.Callback((nameStr, argsStr) => {
2733
2801
  const requestId = ++requestIdCounter;
2734
2802
  const name = nameStr;
@@ -2864,6 +2932,28 @@ var IsolateExecutor = class {
2864
2932
  }
2865
2933
  };
2866
2934
  }
2935
+ if (message.includes("[LIMIT_LOG_ENTRIES]")) {
2936
+ return {
2937
+ stdout: logs.join("\n"),
2938
+ stderr: errors.join("\n"),
2939
+ exitCode: null,
2940
+ error: {
2941
+ code: -32014 /* LogLimitExceeded */,
2942
+ message: "Log entry limit exceeded"
2943
+ }
2944
+ };
2945
+ }
2946
+ if (message.includes("[LIMIT_LOG]") || message.includes("[LIMIT_OUTPUT]")) {
2947
+ return {
2948
+ stdout: logs.join("\n"),
2949
+ stderr: errors.join("\n"),
2950
+ exitCode: null,
2951
+ error: {
2952
+ code: -32013 /* OutputLimitExceeded */,
2953
+ message: "Output limit exceeded"
2954
+ }
2955
+ };
2956
+ }
2867
2957
  this.logger.error({ err }, "Isolate execution failed");
2868
2958
  return {
2869
2959
  stdout: logs.join("\n"),
@@ -3276,13 +3366,13 @@ var ExecutionService = class {
3276
3366
  const packages = await this.gatewayService.listToolPackages();
3277
3367
  const allBindings = [];
3278
3368
  this.logger.debug({ packageCount: packages.length, packages: packages.map((p) => p.id) }, "Fetching tool bindings");
3279
- for (const pkg of packages) {
3369
+ for (const pkg2 of packages) {
3280
3370
  try {
3281
- const stubs = await this.gatewayService.listToolStubs(pkg.id, context);
3282
- this.logger.debug({ packageId: pkg.id, stubCount: stubs.length }, "Got stubs from package");
3371
+ const stubs = await this.gatewayService.listToolStubs(pkg2.id, context);
3372
+ this.logger.debug({ packageId: pkg2.id, stubCount: stubs.length }, "Got stubs from package");
3283
3373
  allBindings.push(...stubs.map((s) => toToolBinding(s.id, void 0, s.description)));
3284
3374
  } catch (err) {
3285
- this.logger.warn({ packageId: pkg.id, err: err.message }, "Failed to list stubs for package");
3375
+ this.logger.warn({ packageId: pkg2.id, err: err.message }, "Failed to list stubs for package");
3286
3376
  }
3287
3377
  }
3288
3378
  this.logger.info({ totalBindings: allBindings.length }, "Tool bindings ready for SDK generation");
@@ -3377,8 +3467,7 @@ var AuthMiddleware = class {
3377
3467
  }
3378
3468
  async handle(request, context, next) {
3379
3469
  const providedToken = request.auth?.bearerToken || "";
3380
- const masterToken = this.securityService.getIpcToken();
3381
- const isMaster = !masterToken || providedToken === masterToken;
3470
+ const isMaster = this.securityService.isMasterToken(providedToken);
3382
3471
  const isSession = !isMaster && this.securityService.validateIpcToken(providedToken);
3383
3472
  if (!isMaster && !isSession) {
3384
3473
  return {
@@ -3639,7 +3728,9 @@ async function handleAuth(options) {
3639
3728
 
3640
3729
  // src/index.ts
3641
3730
  var program = new Command();
3642
- program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version("1.0.0");
3731
+ var require2 = createRequire(import.meta.url);
3732
+ var pkg = require2("../package.json");
3733
+ program.name("conduit").description("A secure Code Mode execution substrate for MCP agents").version(pkg.version || "0.0.0");
3643
3734
  program.command("serve", { isDefault: true }).description("Start the Conduit server").option("--stdio", "Use stdio transport").option("--config <path>", "Path to config file").action(async (options) => {
3644
3735
  try {
3645
3736
  await startServer(options);