@productbrain/mcp 0.0.1-beta.8248 → 0.0.1-beta.8252

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/http.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  SERVER_VERSION,
3
3
  createProductBrainServer,
4
4
  initFeatureFlags
5
- } from "./chunk-J5R4OAP5.js";
5
+ } from "./chunk-LV23VXXV.js";
6
6
  import {
7
7
  DEFAULT_CLOUD_URL,
8
8
  bootstrapHttp,
@@ -271,6 +271,72 @@ function recordAuthFailure(failures, ip, now) {
271
271
  }
272
272
  }
273
273
 
274
+ // src/lib/oauthRedirectAllowlist.ts
275
+ var DEFAULT_OAUTH_REDIRECT_HOSTS = [
276
+ "claude.ai",
277
+ "claude.com",
278
+ "chatgpt.com"
279
+ ];
280
+ var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
281
+ var MAX_REDIRECT_URI_LENGTH = 2048;
282
+ var MAX_REGISTRATION_REDIRECTS = 10;
283
+ function parseHostAllowlist(raw) {
284
+ if (!raw) return [];
285
+ const hosts = [];
286
+ for (const part of raw.split(",")) {
287
+ const trimmed = part.trim();
288
+ if (!trimmed) continue;
289
+ try {
290
+ const asUrl = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
291
+ const host = asUrl.hostname.toLowerCase();
292
+ if (host) hosts.push(host);
293
+ } catch {
294
+ }
295
+ }
296
+ return hosts;
297
+ }
298
+ function resolveOAuthRedirectHosts(opts = {}) {
299
+ const hosts = new Set(DEFAULT_OAUTH_REDIRECT_HOSTS);
300
+ for (const h of parseHostAllowlist(opts.explicitAllowlist)) hosts.add(h);
301
+ return [...hosts];
302
+ }
303
+ function checkRedirectUri(redirectUri, allowedHosts) {
304
+ if (typeof redirectUri !== "string" || redirectUri.length === 0) {
305
+ return "redirect_uri is required";
306
+ }
307
+ if (redirectUri.length > MAX_REDIRECT_URI_LENGTH) {
308
+ return `redirect_uri is longer than ${MAX_REDIRECT_URI_LENGTH} characters`;
309
+ }
310
+ let url;
311
+ try {
312
+ url = new URL(redirectUri);
313
+ } catch {
314
+ return "redirect_uri is not a valid absolute URL";
315
+ }
316
+ if (url.username || url.password) return "redirect_uri must not contain credentials";
317
+ if (url.hash) return "redirect_uri must not contain a fragment";
318
+ const host = url.hostname.toLowerCase();
319
+ if (url.protocol === "http:") {
320
+ return LOOPBACK_HOSTS.includes(host) ? null : "redirect_uri must use https (http is allowed only for localhost, 127.0.0.1 or [::1])";
321
+ }
322
+ if (url.protocol === "https:") {
323
+ if (url.port && url.port !== "443") return "redirect_uri must use the default https port (443)";
324
+ return allowedHosts.includes(host) ? null : "redirect_uri host is not allowed (an operator can add it with OAUTH_REDIRECT_HOST_ALLOWLIST)";
325
+ }
326
+ return "redirect_uri scheme is not allowed (use https, or http on loopback)";
327
+ }
328
+ function checkRegistrationRedirects(redirectUris, allowedHosts) {
329
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) return "redirect_uris is required";
330
+ if (redirectUris.length > MAX_REGISTRATION_REDIRECTS) {
331
+ return `redirect_uris may hold at most ${MAX_REGISTRATION_REDIRECTS} entries`;
332
+ }
333
+ for (const uri of redirectUris) {
334
+ const reason = checkRedirectUri(uri, allowedHosts);
335
+ if (reason) return reason;
336
+ }
337
+ return null;
338
+ }
339
+
274
340
  // src/http.ts
275
341
  bootstrapHttp();
276
342
  initAnalytics();
@@ -285,6 +351,7 @@ var app = express();
285
351
  app.set("trust proxy", 1);
286
352
  app.use(express.json());
287
353
  var ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? "https://claude.ai").split(",").map((o) => o.trim()).filter(Boolean);
354
+ var OAUTH_REDIRECT_HOSTS = resolveOAuthRedirectHosts({ explicitAllowlist: process.env.OAUTH_REDIRECT_HOST_ALLOWLIST });
288
355
  app.use((_req, res, next) => {
289
356
  const origin = _req.headers.origin;
290
357
  if (origin && ALLOWED_ORIGINS.includes(origin)) {
@@ -347,11 +414,9 @@ app.post(
347
414
  return;
348
415
  }
349
416
  const { redirect_uris, client_name } = req.body;
350
- if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {
351
- res.status(400).json({
352
- error: "invalid_client_metadata",
353
- error_description: "redirect_uris is required"
354
- });
417
+ const refusal = checkRegistrationRedirects(redirect_uris, OAUTH_REDIRECT_HOSTS);
418
+ if (refusal) {
419
+ res.status(400).json({ error: "invalid_redirect_uri", error_description: refusal });
355
420
  return;
356
421
  }
357
422
  const clientId = `pb_client_${randomUUID2()}`;
@@ -956,6 +1021,11 @@ ${errorPanelInner(title, trustedDetailHtml, retryUrl)}
956
1021
  }
957
1022
  app.get("/authorize", authLimiter, (req, res) => {
958
1023
  const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
1024
+ const refusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_HOSTS);
1025
+ if (refusal) {
1026
+ res.status(400).json({ error: "invalid_request", error_description: refusal });
1027
+ return;
1028
+ }
959
1029
  const cid = String(client_id ?? "");
960
1030
  const clientName = cid && registeredClients.has(cid) ? registeredClients.get(cid).client_name : void 0;
961
1031
  res.type("html").send(authorizeFormPage({
@@ -997,28 +1067,22 @@ app.post(
997
1067
  return;
998
1068
  }
999
1069
  if (!client_id) {
1000
- res.status(400).json({
1001
- error: "invalid_request",
1002
- error_description: "client_id is required"
1003
- });
1070
+ res.status(400).json({ error: "invalid_request", error_description: "client_id is required" });
1071
+ return;
1072
+ }
1073
+ const redirectRefusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_HOSTS);
1074
+ if (redirectRefusal) {
1075
+ res.status(400).json({ error: "invalid_request", error_description: redirectRefusal });
1004
1076
  return;
1005
1077
  }
1006
1078
  if (!registeredClients.has(client_id)) {
1007
- if (typeof redirect_uri === "string" && redirect_uri.startsWith("https://")) {
1008
- registeredClients.set(client_id, {
1009
- client_id,
1010
- redirect_uris: [redirect_uri],
1011
- registeredAt: Date.now()
1012
- });
1013
- process.stderr.write(`[authorize] auto-re-registered stale client_id after restart
1014
- `);
1015
- } else {
1016
- res.status(400).json({
1017
- error: "invalid_request",
1018
- error_description: "Unknown client_id and redirect_uri is not a valid https URL"
1019
- });
1079
+ if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {
1080
+ res.status(503).json({ error: "server_error", error_description: "Registration limit reached. Try again later." });
1020
1081
  return;
1021
1082
  }
1083
+ registeredClients.set(client_id, { client_id, redirect_uris: [redirect_uri], registeredAt: Date.now() });
1084
+ process.stderr.write(`[authorize] auto-re-registered stale client_id after restart
1085
+ `);
1022
1086
  }
1023
1087
  const client = registeredClients.get(client_id);
1024
1088
  if (!client.redirect_uris.includes(redirect_uri)) {