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 CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -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;AAEzE,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;IAInC;;;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;IAsD5E,OAAO,CAAC,qBAAqB;YAkBf,YAAY;IAe1B,OAAO,CAAC,UAAU;IAIlB,OAAO,CAAC,cAAc;YAYR,oBAAoB;CA0BnC;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,SAAS,CAElE"}
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"}
@@ -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
- rpcRequest = await request.json();
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
- if (!auth.validate)
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 origin = this.config.cors.origins?.[0] ?? "*";
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": 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;AA6EX,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;AA4CD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB;0BA6K1B,OAAO,CAAC,OAAO,KAAG,OAAO,CAAC,YAAY,CAAC;0BA+NvC,OAAO,CAAC,OAAO,KAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;;;;;;;;0BAjVrD,MAAM,EAAE;;EA+XpC;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"}
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"}
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",
package/src/deno.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export default {
2
2
  "name": "veryfront",
3
- "version": "0.1.80",
3
+ "version": "0.1.81",
4
4
  "license": "Apache-2.0",
5
5
  "nodeModulesDir": "auto",
6
6
  "exclude": [
@@ -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
- if (request.method === "OPTIONS") return this.handleCORS();
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
- rpcRequest = await request.json();
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
- if (!auth.validate) return false;
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 origin = this.config.cors.origins?.[0] ?? "*";
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": 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
 
@@ -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(userToken, logger);
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", {