veryfront 0.1.80 → 0.1.81
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/esm/deno.js +1 -1
- package/esm/src/mcp/server.d.ts.map +1 -1
- package/esm/src/mcp/server.js +45 -9
- package/esm/src/proxy/handler.d.ts.map +1 -1
- package/esm/src/proxy/handler.js +54 -3
- package/package.json +1 -1
- package/src/deno.js +1 -1
- package/src/src/mcp/server.ts +57 -9
- package/src/src/proxy/handler.ts +63 -2
package/esm/deno.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAG/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAMlD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../../src/src/mcp/server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAG/C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAMlD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAOzE,KAAK,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EAAE,CAAC;AAOzD,UAAU,cAAc;IACtB,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,KAAK,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,uBAAuB;IACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,GAAG,SAAS,CAAC,CAAC;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,iBAAiB,CAAC,CAA0B;IACpD,OAAO,CAAC,kBAAkB,CAAS;gBAEvB,MAAM,EAAE,eAAe;IAQnC;;;OAGG;IACH,oBAAoB,CAAC,MAAM,EAAE,uBAAuB,GAAG,IAAI;IAK3D,aAAa,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,eAAe,CAAC;IAsBhG,OAAO,CAAC,QAAQ;IA8BhB,OAAO,CAAC,UAAU;YAYJ,SAAS;IA2BvB,OAAO,CAAC,QAAQ;IAmChB,OAAO,CAAC,aAAa;IAgBrB,OAAO,CAAC,YAAY;IA6CpB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,SAAS;IAuCjB,iBAAiB,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC;IA+E5E,OAAO,CAAC,qBAAqB;YAkBf,YAAY;IAoB1B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,cAAc;YAqBR,oBAAoB;CA0BnC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAElE"}
|
package/esm/src/mcp/server.js
CHANGED
|
@@ -9,6 +9,9 @@ import { withSpan } from "../observability/tracing/otlp-setup.js";
|
|
|
9
9
|
import { VERSION } from "../utils/version.js";
|
|
10
10
|
import { validateContentType } from "../security/input-validation/limits.js";
|
|
11
11
|
import { VeryfrontError } from "../security/input-validation/errors.js";
|
|
12
|
+
import { logger as baseLogger } from "../utils/index.js";
|
|
13
|
+
const logger = baseLogger.component("mcp-server");
|
|
14
|
+
const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
|
|
12
15
|
function asParamsRecord(params) {
|
|
13
16
|
if (!params || Array.isArray(params))
|
|
14
17
|
return {};
|
|
@@ -20,6 +23,9 @@ export class MCPServer {
|
|
|
20
23
|
integrationsLoaded = false;
|
|
21
24
|
constructor(config) {
|
|
22
25
|
this.config = config;
|
|
26
|
+
if (!config.auth || config.auth.type === "none") {
|
|
27
|
+
logger.warn("MCP server has no authentication configured — all requests will be accepted");
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
/**
|
|
25
31
|
* Configure integration tools to be lazily loaded on first tools/list call.
|
|
@@ -207,13 +213,23 @@ export class MCPServer {
|
|
|
207
213
|
}
|
|
208
214
|
createHTTPHandler() {
|
|
209
215
|
return async (request) => {
|
|
216
|
+
const requestOrigin = request.headers.get("Origin");
|
|
210
217
|
if (request.method === "OPTIONS")
|
|
211
|
-
return this.handleCORS();
|
|
218
|
+
return this.handleCORS(requestOrigin);
|
|
212
219
|
if (this.config.auth?.type && this.config.auth.type !== "none") {
|
|
213
220
|
const authorized = await this.validateAuth(request);
|
|
214
221
|
if (!authorized)
|
|
215
222
|
return new dntShim.Response("Unauthorized", { status: 401 });
|
|
216
223
|
}
|
|
224
|
+
// Enforce request body size limit (fast path via Content-Length header)
|
|
225
|
+
const contentLength = request.headers.get("content-length");
|
|
226
|
+
if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
|
|
227
|
+
return new dntShim.Response(JSON.stringify({
|
|
228
|
+
jsonrpc: "2.0",
|
|
229
|
+
id: null,
|
|
230
|
+
error: { code: -32600, message: "Request body too large" },
|
|
231
|
+
}), { status: 413, headers: { "Content-Type": "application/json" } });
|
|
232
|
+
}
|
|
217
233
|
try {
|
|
218
234
|
validateContentType(request, "application/json");
|
|
219
235
|
}
|
|
@@ -227,7 +243,15 @@ export class MCPServer {
|
|
|
227
243
|
}
|
|
228
244
|
let rpcRequest;
|
|
229
245
|
try {
|
|
230
|
-
|
|
246
|
+
const bodyText = await request.text();
|
|
247
|
+
if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
|
|
248
|
+
return new dntShim.Response(JSON.stringify({
|
|
249
|
+
jsonrpc: "2.0",
|
|
250
|
+
id: null,
|
|
251
|
+
error: { code: -32600, message: "Request body too large" },
|
|
252
|
+
}), { status: 413, headers: { "Content-Type": "application/json" } });
|
|
253
|
+
}
|
|
254
|
+
rpcRequest = JSON.parse(bodyText);
|
|
231
255
|
}
|
|
232
256
|
catch (_) {
|
|
233
257
|
// expected: malformed JSON in request body
|
|
@@ -246,7 +270,7 @@ export class MCPServer {
|
|
|
246
270
|
return new dntShim.Response(JSON.stringify(rpcResponse), {
|
|
247
271
|
headers: {
|
|
248
272
|
"Content-Type": "application/json",
|
|
249
|
-
...this.getCORSHeaders(),
|
|
273
|
+
...this.getCORSHeaders(requestOrigin),
|
|
250
274
|
},
|
|
251
275
|
});
|
|
252
276
|
};
|
|
@@ -275,21 +299,33 @@ export class MCPServer {
|
|
|
275
299
|
if (auth.type !== "bearer")
|
|
276
300
|
return false;
|
|
277
301
|
const token = authHeader.replace("Bearer ", "");
|
|
278
|
-
|
|
302
|
+
// When bearer auth is configured without a validate function, reject all requests
|
|
303
|
+
if (!auth.validate) {
|
|
304
|
+
logger.warn("Bearer auth configured without validate function — rejecting request");
|
|
279
305
|
return false;
|
|
306
|
+
}
|
|
280
307
|
return await auth.validate(token);
|
|
281
308
|
}
|
|
282
|
-
handleCORS() {
|
|
283
|
-
return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders() });
|
|
309
|
+
handleCORS(requestOrigin) {
|
|
310
|
+
return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders(requestOrigin) });
|
|
284
311
|
}
|
|
285
|
-
getCORSHeaders() {
|
|
312
|
+
getCORSHeaders(requestOrigin) {
|
|
286
313
|
if (!this.config.cors?.enabled)
|
|
287
314
|
return {};
|
|
288
|
-
const
|
|
315
|
+
const origins = this.config.cors.origins;
|
|
316
|
+
if (!origins || origins.length === 0)
|
|
317
|
+
return {};
|
|
318
|
+
// Match request origin against the configured origins list
|
|
319
|
+
const matchedOrigin = requestOrigin && origins.includes(requestOrigin)
|
|
320
|
+
? requestOrigin
|
|
321
|
+
: undefined;
|
|
322
|
+
if (!matchedOrigin)
|
|
323
|
+
return {};
|
|
289
324
|
return {
|
|
290
|
-
"Access-Control-Allow-Origin":
|
|
325
|
+
"Access-Control-Allow-Origin": matchedOrigin,
|
|
291
326
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
292
327
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-End-User-Id, X-Project-Id",
|
|
328
|
+
"Vary": "Origin",
|
|
293
329
|
};
|
|
294
330
|
}
|
|
295
331
|
async loadIntegrationTools(config) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,kCAAkC,CAAC;AACzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAQnD,eAAO,MAAM,sBAAsB,0MAYzB,CAAC;
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../../src/src/proxy/handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,KAAK,YAAY,EAAsB,MAAM,kCAAkC,CAAC;AACzF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAQnD,eAAO,MAAM,sBAAsB,0MAYzB,CAAC;AAoGX,MAAM,WAAW,WAAW;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;IACxB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,SAAS,GAAG,YAAY,CAAC;IACtC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,YAAY,CAAC;IAC3B,cAAc,EAAE,OAAO,CAAC;IACxB,KAAK,CAAC,EAAE;QACN,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;CACH;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC9D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC9E;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AA8ED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;0BAiL1B,OAAO,CAAC,OAAO,KAAG,OAAO,CAAC,YAAY,CAAC;0BA+NvC,OAAO,CAAC,OAAO,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;;;;;;;0BArVrD,MAAM,EAAE;;EAmYpC;AAED,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEjE,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,EAAE,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,OAAO,CAwB7F"}
|
package/esm/src/proxy/handler.js
CHANGED
|
@@ -6,7 +6,7 @@ import { cwd, getEnv } from "../platform/compat/process.js";
|
|
|
6
6
|
import { join } from "../platform/compat/path/index.js";
|
|
7
7
|
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
8
8
|
import { computeContentSourceId } from "../cache/keys.js";
|
|
9
|
-
import { jwtVerify } from "jose";
|
|
9
|
+
import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
10
10
|
export const INTERNAL_PROXY_HEADERS = [
|
|
11
11
|
"x-token",
|
|
12
12
|
"x-project-slug",
|
|
@@ -20,6 +20,26 @@ export const INTERNAL_PROXY_HEADERS = [
|
|
|
20
20
|
"x-branch-id",
|
|
21
21
|
"x-branch-name",
|
|
22
22
|
];
|
|
23
|
+
const remoteJwksByUrl = new Map();
|
|
24
|
+
function getApiJwks(apiBaseUrl, logger) {
|
|
25
|
+
try {
|
|
26
|
+
const normalizedBaseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
|
|
27
|
+
const jwksUrl = new URL(".well-known/jwks.json", normalizedBaseUrl);
|
|
28
|
+
const cacheKey = jwksUrl.toString();
|
|
29
|
+
let jwks = remoteJwksByUrl.get(cacheKey);
|
|
30
|
+
if (!jwks) {
|
|
31
|
+
jwks = createRemoteJWKSet(jwksUrl);
|
|
32
|
+
remoteJwksByUrl.set(cacheKey, jwks);
|
|
33
|
+
}
|
|
34
|
+
return jwks;
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
logger?.error("Invalid API base URL for JWKS lookup", error, {
|
|
38
|
+
apiBaseUrl,
|
|
39
|
+
});
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
23
43
|
async function lookupProjectByDomain(domain, apiBaseUrl, token, logger) {
|
|
24
44
|
return withSpan(ProxySpanNames.PROXY_DOMAIN_LOOKUP, async () => {
|
|
25
45
|
const domainWithoutPort = domain.replace(/:\d+$/, "");
|
|
@@ -70,7 +90,38 @@ function extractUserToken(cookieHeader) {
|
|
|
70
90
|
const match = cookieHeader.match(/(?:^|;\s*)authToken=([^;]+)/);
|
|
71
91
|
return match?.[1] ? decodeURIComponent(match[1]) : undefined;
|
|
72
92
|
}
|
|
73
|
-
async function extractUserIdFromToken(token, log) {
|
|
93
|
+
async function extractUserIdFromToken(token, apiBaseUrl, log) {
|
|
94
|
+
let algorithm;
|
|
95
|
+
try {
|
|
96
|
+
algorithm = decodeProtectedHeader(token).alg;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
log?.debug("Failed to decode JWT header", {
|
|
100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
101
|
+
});
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (algorithm === "RS256") {
|
|
105
|
+
const jwks = getApiJwks(apiBaseUrl, log);
|
|
106
|
+
if (!jwks)
|
|
107
|
+
return undefined;
|
|
108
|
+
try {
|
|
109
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
110
|
+
algorithms: ["RS256"],
|
|
111
|
+
});
|
|
112
|
+
return payload.userId;
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
log?.debug("RS256 JWT verification failed", {
|
|
116
|
+
error: error instanceof Error ? error.message : String(error),
|
|
117
|
+
});
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (algorithm !== "HS256") {
|
|
122
|
+
log?.debug("Unsupported JWT algorithm", { algorithm: algorithm ?? null });
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
74
125
|
const jwtSecret = getEnv("JWT_SECRET");
|
|
75
126
|
if (!jwtSecret) {
|
|
76
127
|
log?.warn("JWT_SECRET not configured — cannot verify user token");
|
|
@@ -184,7 +235,7 @@ export function createProxyHandler(options) {
|
|
|
184
235
|
});
|
|
185
236
|
return { status: 302, message: "Authentication required", redirectUrl };
|
|
186
237
|
}
|
|
187
|
-
const userId = await extractUserIdFromToken(userToken, logger);
|
|
238
|
+
const userId = await extractUserIdFromToken(userToken, config.apiBaseUrl, logger);
|
|
188
239
|
if (!userId) {
|
|
189
240
|
const redirectUrl = makeAuthRedirectUrl(req);
|
|
190
241
|
logger?.info("Could not extract userId from token", {
|
package/package.json
CHANGED
package/src/deno.js
CHANGED
package/src/src/mcp/server.ts
CHANGED
|
@@ -12,6 +12,11 @@ import { VERSION } from "../utils/version.js";
|
|
|
12
12
|
import { validateContentType } from "../security/input-validation/limits.js";
|
|
13
13
|
import { VeryfrontError } from "../security/input-validation/errors.js";
|
|
14
14
|
import type { IntegrationRuntimeConfig } from "../integrations/types.js";
|
|
15
|
+
import { logger as baseLogger } from "../utils/index.js";
|
|
16
|
+
|
|
17
|
+
const logger = baseLogger.component("mcp-server");
|
|
18
|
+
|
|
19
|
+
const MAX_REQUEST_BODY_SIZE = 1_048_576; // 1 MB
|
|
15
20
|
|
|
16
21
|
type JSONRPCParams = Record<string, unknown> | unknown[];
|
|
17
22
|
|
|
@@ -51,6 +56,10 @@ export class MCPServer {
|
|
|
51
56
|
|
|
52
57
|
constructor(config: MCPServerConfig) {
|
|
53
58
|
this.config = config;
|
|
59
|
+
|
|
60
|
+
if (!config.auth || config.auth.type === "none") {
|
|
61
|
+
logger.warn("MCP server has no authentication configured — all requests will be accepted");
|
|
62
|
+
}
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
/**
|
|
@@ -304,13 +313,27 @@ export class MCPServer {
|
|
|
304
313
|
|
|
305
314
|
createHTTPHandler(): (request: dntShim.Request) => Promise<dntShim.Response> {
|
|
306
315
|
return async (request: dntShim.Request) => {
|
|
307
|
-
|
|
316
|
+
const requestOrigin = request.headers.get("Origin");
|
|
317
|
+
if (request.method === "OPTIONS") return this.handleCORS(requestOrigin);
|
|
308
318
|
|
|
309
319
|
if (this.config.auth?.type && this.config.auth.type !== "none") {
|
|
310
320
|
const authorized = await this.validateAuth(request);
|
|
311
321
|
if (!authorized) return new dntShim.Response("Unauthorized", { status: 401 });
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
// Enforce request body size limit (fast path via Content-Length header)
|
|
325
|
+
const contentLength = request.headers.get("content-length");
|
|
326
|
+
if (contentLength && Number(contentLength) > MAX_REQUEST_BODY_SIZE) {
|
|
327
|
+
return new dntShim.Response(
|
|
328
|
+
JSON.stringify({
|
|
329
|
+
jsonrpc: "2.0",
|
|
330
|
+
id: null,
|
|
331
|
+
error: { code: -32600, message: "Request body too large" },
|
|
332
|
+
}),
|
|
333
|
+
{ status: 413, headers: { "Content-Type": "application/json" } },
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
314
337
|
try {
|
|
315
338
|
validateContentType(request, "application/json");
|
|
316
339
|
} catch (error) {
|
|
@@ -327,7 +350,18 @@ export class MCPServer {
|
|
|
327
350
|
|
|
328
351
|
let rpcRequest: JSONRPCRequest;
|
|
329
352
|
try {
|
|
330
|
-
|
|
353
|
+
const bodyText = await request.text();
|
|
354
|
+
if (bodyText.length > MAX_REQUEST_BODY_SIZE) {
|
|
355
|
+
return new dntShim.Response(
|
|
356
|
+
JSON.stringify({
|
|
357
|
+
jsonrpc: "2.0",
|
|
358
|
+
id: null,
|
|
359
|
+
error: { code: -32600, message: "Request body too large" },
|
|
360
|
+
}),
|
|
361
|
+
{ status: 413, headers: { "Content-Type": "application/json" } },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
rpcRequest = JSON.parse(bodyText) as JSONRPCRequest;
|
|
331
365
|
} catch (_) {
|
|
332
366
|
// expected: malformed JSON in request body
|
|
333
367
|
return new dntShim.Response(
|
|
@@ -350,7 +384,7 @@ export class MCPServer {
|
|
|
350
384
|
return new dntShim.Response(JSON.stringify(rpcResponse), {
|
|
351
385
|
headers: {
|
|
352
386
|
"Content-Type": "application/json",
|
|
353
|
-
...this.getCORSHeaders(),
|
|
387
|
+
...this.getCORSHeaders(requestOrigin),
|
|
354
388
|
},
|
|
355
389
|
});
|
|
356
390
|
};
|
|
@@ -384,24 +418,38 @@ export class MCPServer {
|
|
|
384
418
|
if (auth.type !== "bearer") return false;
|
|
385
419
|
|
|
386
420
|
const token = authHeader.replace("Bearer ", "");
|
|
387
|
-
|
|
421
|
+
|
|
422
|
+
// When bearer auth is configured without a validate function, reject all requests
|
|
423
|
+
if (!auth.validate) {
|
|
424
|
+
logger.warn("Bearer auth configured without validate function — rejecting request");
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
388
427
|
|
|
389
428
|
return await auth.validate(token);
|
|
390
429
|
}
|
|
391
430
|
|
|
392
|
-
private handleCORS(): dntShim.Response {
|
|
393
|
-
return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders() });
|
|
431
|
+
private handleCORS(requestOrigin?: string | null): dntShim.Response {
|
|
432
|
+
return new dntShim.Response(null, { status: 204, headers: this.getCORSHeaders(requestOrigin) });
|
|
394
433
|
}
|
|
395
434
|
|
|
396
|
-
private getCORSHeaders(): Record<string, string> {
|
|
435
|
+
private getCORSHeaders(requestOrigin?: string | null): Record<string, string> {
|
|
397
436
|
if (!this.config.cors?.enabled) return {};
|
|
398
437
|
|
|
399
|
-
const
|
|
438
|
+
const origins = this.config.cors.origins;
|
|
439
|
+
if (!origins || origins.length === 0) return {};
|
|
440
|
+
|
|
441
|
+
// Match request origin against the configured origins list
|
|
442
|
+
const matchedOrigin = requestOrigin && origins.includes(requestOrigin)
|
|
443
|
+
? requestOrigin
|
|
444
|
+
: undefined;
|
|
445
|
+
|
|
446
|
+
if (!matchedOrigin) return {};
|
|
400
447
|
|
|
401
448
|
return {
|
|
402
|
-
"Access-Control-Allow-Origin":
|
|
449
|
+
"Access-Control-Allow-Origin": matchedOrigin,
|
|
403
450
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
404
451
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, X-End-User-Id, X-Project-Id",
|
|
452
|
+
"Vary": "Origin",
|
|
405
453
|
};
|
|
406
454
|
}
|
|
407
455
|
|
package/src/src/proxy/handler.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { cwd, getEnv } from "../platform/compat/process.js";
|
|
|
7
7
|
import { join } from "../platform/compat/path/index.js";
|
|
8
8
|
import { injectContext, ProxySpanNames, withSpan } from "./tracing.js";
|
|
9
9
|
import { computeContentSourceId } from "../cache/keys.js";
|
|
10
|
-
import { jwtVerify } from "jose";
|
|
10
|
+
import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify } from "jose";
|
|
11
11
|
|
|
12
12
|
export const INTERNAL_PROXY_HEADERS = [
|
|
13
13
|
"x-token",
|
|
@@ -37,6 +37,29 @@ interface DomainLookupResult {
|
|
|
37
37
|
}>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const remoteJwksByUrl = new Map<string, ReturnType<typeof createRemoteJWKSet>>();
|
|
41
|
+
|
|
42
|
+
function getApiJwks(apiBaseUrl: string, logger?: ProxyLogger) {
|
|
43
|
+
try {
|
|
44
|
+
const normalizedBaseUrl = apiBaseUrl.endsWith("/") ? apiBaseUrl : `${apiBaseUrl}/`;
|
|
45
|
+
const jwksUrl = new URL(".well-known/jwks.json", normalizedBaseUrl);
|
|
46
|
+
const cacheKey = jwksUrl.toString();
|
|
47
|
+
let jwks = remoteJwksByUrl.get(cacheKey);
|
|
48
|
+
|
|
49
|
+
if (!jwks) {
|
|
50
|
+
jwks = createRemoteJWKSet(jwksUrl);
|
|
51
|
+
remoteJwksByUrl.set(cacheKey, jwks);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return jwks;
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger?.error("Invalid API base URL for JWKS lookup", error as Error, {
|
|
57
|
+
apiBaseUrl,
|
|
58
|
+
});
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
async function lookupProjectByDomain(
|
|
41
64
|
domain: string,
|
|
42
65
|
apiBaseUrl: string,
|
|
@@ -154,8 +177,42 @@ function extractUserToken(cookieHeader: string): string | undefined {
|
|
|
154
177
|
|
|
155
178
|
async function extractUserIdFromToken(
|
|
156
179
|
token: string,
|
|
180
|
+
apiBaseUrl: string,
|
|
157
181
|
log?: ProxyLogger,
|
|
158
182
|
): Promise<string | undefined> {
|
|
183
|
+
let algorithm: string | undefined;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
algorithm = decodeProtectedHeader(token).alg;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
log?.debug("Failed to decode JWT header", {
|
|
189
|
+
error: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (algorithm === "RS256") {
|
|
195
|
+
const jwks = getApiJwks(apiBaseUrl, log);
|
|
196
|
+
if (!jwks) return undefined;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
const { payload } = await jwtVerify(token, jwks, {
|
|
200
|
+
algorithms: ["RS256"],
|
|
201
|
+
});
|
|
202
|
+
return (payload as { userId?: string }).userId;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
log?.debug("RS256 JWT verification failed", {
|
|
205
|
+
error: error instanceof Error ? error.message : String(error),
|
|
206
|
+
});
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (algorithm !== "HS256") {
|
|
212
|
+
log?.debug("Unsupported JWT algorithm", { algorithm: algorithm ?? null });
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
159
216
|
const jwtSecret = getEnv("JWT_SECRET");
|
|
160
217
|
|
|
161
218
|
if (!jwtSecret) {
|
|
@@ -304,7 +361,11 @@ export function createProxyHandler(options: ProxyHandlerOptions) {
|
|
|
304
361
|
return { status: 302, message: "Authentication required", redirectUrl };
|
|
305
362
|
}
|
|
306
363
|
|
|
307
|
-
const userId = await extractUserIdFromToken(
|
|
364
|
+
const userId = await extractUserIdFromToken(
|
|
365
|
+
userToken,
|
|
366
|
+
config.apiBaseUrl,
|
|
367
|
+
logger,
|
|
368
|
+
);
|
|
308
369
|
if (!userId) {
|
|
309
370
|
const redirectUrl = makeAuthRedirectUrl(req);
|
|
310
371
|
logger?.info("Could not extract userId from token", {
|