@context-engine-bridge/context-engine-mcp-bridge 0.0.85 → 0.0.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-engine-bridge/context-engine-mcp-bridge",
3
- "version": "0.0.85",
3
+ "version": "0.0.87",
4
4
  "description": "Context Engine MCP bridge (http/stdio proxy combining indexer + memory servers)",
5
5
  "bin": {
6
6
  "ctxce": "bin/ctxce.js",
package/src/mcpServer.js CHANGED
@@ -13,7 +13,6 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/
13
13
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
14
  import {
15
15
  clearResolvedCollection,
16
- loadAnyAuthEntry,
17
16
  loadAuthEntry,
18
17
  loadResolvedCollection,
19
18
  readConfig,
@@ -649,15 +648,8 @@ function resolveAuthBackendContext(indexerUrl, memoryUrl) {
649
648
  }
650
649
  }
651
650
 
652
- try {
653
- const any = loadAnyAuthEntry();
654
- const stored = normalizeBackendUrl(any?.backendUrl || "");
655
- if (stored) {
656
- return { backendUrl: stored, source: "auth_entry" };
657
- }
658
- } catch {
659
- // ignore auth config read failures
660
- }
651
+ // Fail-closed: do NOT fall back to loadAnyAuthEntry() here.
652
+ // Borrowing a session from an unrelated backend is a cross-account leak.
661
653
  return { backendUrl: "", source: "" };
662
654
  }
663
655
 
@@ -1120,10 +1112,20 @@ async function createBridgeServer(options) {
1120
1112
  let sessionId = explicitSession;
1121
1113
 
1122
1114
  function sessionFromEntry(entry) {
1123
- if (!entry || typeof entry.sessionId !== "string" || !entry.sessionId) {
1115
+ if (!entry || typeof entry !== "object") {
1116
+ return "";
1117
+ }
1118
+ // Support both camelCase (sessionId/expiresAt) and snake_case (session_id/expires_at)
1119
+ const sid = (typeof entry.sessionId === "string" && entry.sessionId)
1120
+ ? entry.sessionId
1121
+ : (typeof entry.session_id === "string" && entry.session_id)
1122
+ ? entry.session_id
1123
+ : "";
1124
+ if (!sid) {
1124
1125
  return "";
1125
1126
  }
1126
- const expiresAt = entry.expiresAt;
1127
+ const expiresAt = (typeof entry.expiresAt === "number") ? entry.expiresAt
1128
+ : (typeof entry.expires_at === "number") ? entry.expires_at : undefined;
1127
1129
  if (
1128
1130
  typeof expiresAt === "number" &&
1129
1131
  Number.isFinite(expiresAt) &&
@@ -1136,9 +1138,9 @@ async function createBridgeServer(options) {
1136
1138
  return "";
1137
1139
  }
1138
1140
  debugLog("[ctxce] Session expired but within 5min grace period; using it (server will validate).");
1139
- return entry.sessionId;
1141
+ return sid;
1140
1142
  }
1141
- return entry.sessionId;
1143
+ return sid;
1142
1144
  }
1143
1145
 
1144
1146
  function findSavedSession(backends) {
@@ -1158,16 +1160,8 @@ async function createBridgeServer(options) {
1158
1160
  // ignore lookup failures
1159
1161
  }
1160
1162
  }
1161
- try {
1162
- const any = loadAnyAuthEntry();
1163
- const session = any ? sessionFromEntry(any.entry) : "";
1164
- if (session && any?.backendUrl) {
1165
- backendHint = any.backendUrl;
1166
- return session;
1167
- }
1168
- } catch {
1169
- // ignore lookup failures
1170
- }
1163
+ // Fail-closed: do NOT fall back to loadAnyAuthEntry().
1164
+ // Borrowing an unrelated account's session is a cross-account leak.
1171
1165
  return "";
1172
1166
  }
1173
1167
 
@@ -560,8 +560,7 @@ export function handleOAuthToken(req, res) {
560
560
  const code = data.get("code");
561
561
  const redirectUri = data.get("redirect_uri");
562
562
  const clientId = data.get("client_id");
563
- // PKCE code_verifier - extracted but not validated yet (local bridge, trusted)
564
- data.get("code_verifier");
563
+ const codeVerifier = data.get("code_verifier");
565
564
  const grantType = data.get("grant_type");
566
565
 
567
566
  res.setHeader("Content-Type", "application/json");
@@ -605,24 +604,23 @@ export function handleOAuthToken(req, res) {
605
604
  return;
606
605
  }
607
606
 
608
- // TODO: PKCE validation - disabled for now, no clients implement it yet
609
- // if (pendingData.codeChallenge && pendingData.codeChallengeMethod === "S256") {
610
- // const codeVerifier = data.get("code_verifier");
611
- // if (!codeVerifier) {
612
- // pendingCodes.delete(code);
613
- // res.statusCode = 400;
614
- // res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier required for PKCE" }));
615
- // return;
616
- // }
617
- // const crypto = await import("node:crypto");
618
- // const expectedChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
619
- // if (expectedChallenge !== pendingData.codeChallenge) {
620
- // pendingCodes.delete(code);
621
- // res.statusCode = 400;
622
- // res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier validation failed" }));
623
- // return;
624
- // }
625
- // }
607
+ // PKCE validation (RFC 7636)
608
+ if (pendingData.codeChallenge && pendingData.codeChallengeMethod === "S256") {
609
+ if (!codeVerifier) {
610
+ pendingCodes.delete(code);
611
+ res.statusCode = 400;
612
+ res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier required for PKCE" }));
613
+ return;
614
+ }
615
+ const crypto = await import("node:crypto");
616
+ const expectedChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
617
+ if (expectedChallenge !== pendingData.codeChallenge) {
618
+ pendingCodes.delete(code);
619
+ res.statusCode = 400;
620
+ res.end(JSON.stringify({ error: "invalid_grant", error_description: "code_verifier validation failed" }));
621
+ return;
622
+ }
623
+ }
626
624
 
627
625
  // Clean up expired tokens periodically to prevent unbounded growth
628
626
  cleanupExpiredTokens();