@upstash/context7-mcp 4.0.3 → 4.0.4
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/dist/index.js +5 -2
- package/dist/lib/encryption.js +56 -16
- package/package.json +1 -1
- package/dist/lib/client-ip.js +0 -60
package/dist/index.js
CHANGED
|
@@ -12,7 +12,6 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
13
|
import { SERVER_VERSION, RESOURCE_URL, OAUTH_AUTH_SERVER_URL, EMA_ISSUER, OPENAI_APPS_CHALLENGE_TOKEN, } from "./lib/constants.js";
|
|
14
14
|
import { maybeElicitAuthSignIn } from "./lib/auth/auth-prompt.js";
|
|
15
|
-
import { getClientIp } from "./lib/client-ip.js";
|
|
16
15
|
/** Default HTTP server port */
|
|
17
16
|
const DEFAULT_PORT = 3000;
|
|
18
17
|
// Parse CLI arguments using commander
|
|
@@ -242,6 +241,9 @@ async function main() {
|
|
|
242
241
|
if (TRANSPORT_TYPE === "http") {
|
|
243
242
|
const initialPort = CLI_PORT ?? DEFAULT_PORT;
|
|
244
243
|
const app = express();
|
|
244
|
+
// Only private/local infrastructure may supply forwarding headers. Express
|
|
245
|
+
// then walks the chain right-to-left and ignores attacker-added prefixes.
|
|
246
|
+
app.set("trust proxy", ["loopback", "linklocal", "uniquelocal", "100.64.0.0/10"]);
|
|
245
247
|
app.use(express.json());
|
|
246
248
|
app.use((req, res, next) => {
|
|
247
249
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -273,6 +275,7 @@ async function main() {
|
|
|
273
275
|
};
|
|
274
276
|
const extractApiKey = (req) => {
|
|
275
277
|
return (extractBearerToken(req.headers.authorization) ||
|
|
278
|
+
extractHeaderValue(req.headers["x-context7-api-key"]) ||
|
|
276
279
|
extractHeaderValue(req.headers["context7-api-key"]) ||
|
|
277
280
|
extractHeaderValue(req.headers["x-api-key"]) ||
|
|
278
281
|
extractHeaderValue(req.headers["context7_api_key"]) ||
|
|
@@ -336,7 +339,7 @@ async function main() {
|
|
|
336
339
|
}
|
|
337
340
|
}
|
|
338
341
|
const context = {
|
|
339
|
-
clientIp:
|
|
342
|
+
clientIp: req.ip,
|
|
340
343
|
apiKey: apiKey,
|
|
341
344
|
clientInfo: extractClientInfoFromUserAgent(req.headers["user-agent"]),
|
|
342
345
|
transport: "http",
|
package/dist/lib/encryption.js
CHANGED
|
@@ -1,27 +1,59 @@
|
|
|
1
1
|
import { createCipheriv, randomBytes } from "crypto";
|
|
2
|
+
import { isIP } from "node:net";
|
|
2
3
|
import { SERVER_VERSION } from "./constants.js";
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
4
|
+
const LEGACY_ALGORITHM = "aes-256-cbc";
|
|
5
|
+
const ASSERTION_ALGORITHM = "aes-256-gcm";
|
|
6
|
+
const ASSERTION_VERSION = "v1";
|
|
7
|
+
let reportedInvalidAssertionKey = false;
|
|
6
8
|
function validateEncryptionKey(key) {
|
|
7
9
|
// Must be exactly 64 hex characters (32 bytes)
|
|
8
10
|
return /^[0-9a-fA-F]{64}$/.test(key);
|
|
9
11
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
function encryptionKey(name) {
|
|
13
|
+
const key = process.env[name];
|
|
14
|
+
return key && validateEncryptionKey(key) ? Buffer.from(key, "hex") : null;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Temporary compatibility header for API deployments that predate authenticated assertions.
|
|
18
|
+
* This header is ignored by patched API deployments. Removal is tracked by CTX7-2536.
|
|
19
|
+
*/
|
|
20
|
+
function encryptLegacyClientIp(clientIp, key) {
|
|
15
21
|
try {
|
|
16
22
|
const iv = randomBytes(16);
|
|
17
|
-
const cipher = createCipheriv(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
23
|
+
const cipher = createCipheriv(LEGACY_ALGORITHM, key, iv);
|
|
24
|
+
const encrypted = Buffer.concat([cipher.update(clientIp, "utf8"), cipher.final()]);
|
|
25
|
+
return `${iv.toString("hex")}:${encrypted.toString("hex")}`;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a short-lived, authenticated client-IP assertion.
|
|
33
|
+
* Format: v1:<unix timestamp seconds>:<12-byte nonce hex>:<ciphertext + tag hex>
|
|
34
|
+
*/
|
|
35
|
+
export function createClientIpAssertion(clientIp, nowMs = Date.now(), nonce = randomBytes(12)) {
|
|
36
|
+
const key = encryptionKey("MCP_CLIENT_IP_ASSERTION_KEY");
|
|
37
|
+
if (!key) {
|
|
38
|
+
if (!reportedInvalidAssertionKey) {
|
|
39
|
+
reportedInvalidAssertionKey = true;
|
|
40
|
+
console.error("MCP_CLIENT_IP_ASSERTION_KEY is missing or invalid; client IP assertions are disabled.");
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
if (nonce.length !== 12 || isIP(clientIp) === 0)
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const timestamp = Math.floor(nowMs / 1000).toString();
|
|
48
|
+
const aad = `${ASSERTION_VERSION}:${timestamp}`;
|
|
49
|
+
const cipher = createCipheriv(ASSERTION_ALGORITHM, key, nonce);
|
|
50
|
+
cipher.setAAD(Buffer.from(aad, "utf8"));
|
|
51
|
+
const ciphertext = Buffer.concat([cipher.update(clientIp, "utf8"), cipher.final()]);
|
|
52
|
+
const ciphertextAndTag = Buffer.concat([ciphertext, cipher.getAuthTag()]);
|
|
53
|
+
return `${aad}:${nonce.toString("hex")}:${ciphertextAndTag.toString("hex")}`;
|
|
21
54
|
}
|
|
22
|
-
catch
|
|
23
|
-
|
|
24
|
-
return clientIp; // Fallback to unencrypted
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
25
57
|
}
|
|
26
58
|
}
|
|
27
59
|
/**
|
|
@@ -34,7 +66,15 @@ export function generateHeaders(context) {
|
|
|
34
66
|
"X-Context7-Server-Version": SERVER_VERSION,
|
|
35
67
|
};
|
|
36
68
|
if (context.clientIp) {
|
|
37
|
-
|
|
69
|
+
const assertion = createClientIpAssertion(context.clientIp);
|
|
70
|
+
if (assertion) {
|
|
71
|
+
headers["mcp-client-ip-assertion"] = assertion;
|
|
72
|
+
// Producer-first rollout compatibility. Removal is tracked by CTX7-2536.
|
|
73
|
+
const key = encryptionKey("CLIENT_IP_ENCRYPTION_KEY");
|
|
74
|
+
const legacyValue = key ? encryptLegacyClientIp(context.clientIp, key) : null;
|
|
75
|
+
if (legacyValue)
|
|
76
|
+
headers["mcp-client-ip"] = legacyValue;
|
|
77
|
+
}
|
|
38
78
|
}
|
|
39
79
|
if (context.sessionId) {
|
|
40
80
|
headers["mcp-session-id"] = context.sessionId;
|
package/package.json
CHANGED
package/dist/lib/client-ip.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
function stripIpv4MappedPrefix(ip) {
|
|
2
|
-
return ip.replace(/^::ffff:/i, "");
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* Returns true for RFC1918, CGNAT, loopback, link-local, and IPv6 private ranges.
|
|
6
|
-
*/
|
|
7
|
-
export function isPrivateOrLocalIp(ip) {
|
|
8
|
-
const plainIp = stripIpv4MappedPrefix(ip).toLowerCase();
|
|
9
|
-
if (plainIp.includes(".")) {
|
|
10
|
-
return (plainIp.startsWith("10.") ||
|
|
11
|
-
plainIp.startsWith("192.168.") ||
|
|
12
|
-
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(plainIp) ||
|
|
13
|
-
/^100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\./.test(plainIp) ||
|
|
14
|
-
plainIp.startsWith("127.") ||
|
|
15
|
-
plainIp.startsWith("169.254."));
|
|
16
|
-
}
|
|
17
|
-
// ::1 loopback in any textual form (e.g. "0::1", "0:0:0:0:0:0:0:1")
|
|
18
|
-
if (/^[0:]+1$/.test(plainIp)) {
|
|
19
|
-
return true;
|
|
20
|
-
}
|
|
21
|
-
// First hextets in fe80::/10 and fc00::/7 start with a non-zero digit, so a
|
|
22
|
-
// valid textual form always spells out all 4 digits.
|
|
23
|
-
// fe80::/10 link-local
|
|
24
|
-
if (/^fe[89ab][0-9a-f]:/.test(plainIp)) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
// fc00::/7 unique local
|
|
28
|
-
if (/^f[cd][0-9a-f]{2}:/.test(plainIp)) {
|
|
29
|
-
return true;
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
function pickClientIpFromForwardedList(ipList) {
|
|
34
|
-
for (const ip of ipList) {
|
|
35
|
-
const plainIp = stripIpv4MappedPrefix(ip);
|
|
36
|
-
if (!isPrivateOrLocalIp(plainIp)) {
|
|
37
|
-
return plainIp;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
if (ipList.length === 0) {
|
|
41
|
-
return undefined;
|
|
42
|
-
}
|
|
43
|
-
return stripIpv4MappedPrefix(ipList[0]);
|
|
44
|
-
}
|
|
45
|
-
/**
|
|
46
|
-
* Extract client IP address from request headers.
|
|
47
|
-
* Handles X-Forwarded-For header for proxied requests.
|
|
48
|
-
*/
|
|
49
|
-
export function getClientIp(req) {
|
|
50
|
-
const forwardedFor = req.headers["x-forwarded-for"] || req.headers["X-Forwarded-For"];
|
|
51
|
-
if (forwardedFor) {
|
|
52
|
-
const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor;
|
|
53
|
-
const ipList = ips.split(",").map((ip) => ip.trim());
|
|
54
|
-
return pickClientIpFromForwardedList(ipList);
|
|
55
|
-
}
|
|
56
|
-
if (req.socket?.remoteAddress) {
|
|
57
|
-
return stripIpv4MappedPrefix(req.socket.remoteAddress);
|
|
58
|
-
}
|
|
59
|
-
return undefined;
|
|
60
|
-
}
|