@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 +1 -1
- package/src/mcpServer.js +18 -24
- package/src/oauthHandler.js +18 -20
package/package.json
CHANGED
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
|
-
|
|
653
|
-
|
|
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
|
|
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
|
|
1141
|
+
return sid;
|
|
1140
1142
|
}
|
|
1141
|
-
return
|
|
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
|
-
|
|
1162
|
-
|
|
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
|
|
package/src/oauthHandler.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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();
|