@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-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
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 (
|
|
1008
|
-
|
|
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)) {
|