@productbrain/mcp 0.0.1-beta.9450 → 0.0.1-beta.9464

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,22 +2,24 @@ import {
2
2
  SERVER_VERSION,
3
3
  createProductBrainServer,
4
4
  initFeatureFlags
5
- } from "./chunk-BCYOMOD2.js";
5
+ } from "./chunk-BCQ7KBLE.js";
6
6
  import {
7
7
  DEFAULT_CLOUD_URL,
8
8
  apiKeysUrl,
9
9
  bootstrapHttp,
10
+ checkKeyAgainstCandidates,
10
11
  forgetStreamLifecycle,
11
12
  getKeyState,
12
13
  getPostHogClient,
13
14
  hashKey,
14
15
  initAnalytics,
16
+ parseFallbackUrls,
15
17
  runWithAuth,
16
18
  shutdownAnalytics
17
- } from "./chunk-DKJ4X2D7.js";
19
+ } from "./chunk-FVH3B7MF.js";
18
20
 
19
21
  // src/http.ts
20
- import { createHash, randomUUID as randomUUID2 } from "crypto";
22
+ import { createHash, randomUUID } from "crypto";
21
23
  import express from "express";
22
24
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
23
25
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
@@ -56,63 +58,6 @@ function appLogoMarkup(opts = {}) {
56
58
  return `<span class="${cls}"><span class="pb-logo__mark"><span class="pb-logo__core"></span></span>${wordmark}</span>`;
57
59
  }
58
60
 
59
- // src/lib/refresh-token.ts
60
- import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "crypto";
61
- var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 6e4;
62
- var PREFIX = "pb_rt_";
63
- var secret = (() => {
64
- const fromEnv = process.env.MCP_REFRESH_SECRET;
65
- if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, "utf8");
66
- if (process.env.NODE_ENV === "production") {
67
- console.warn(
68
- "[HTTP] WARNING MCP_REFRESH_SECRET not set \u2014 refresh tokens will not survive restart"
69
- );
70
- }
71
- return randomBytes(32);
72
- })();
73
- function sign(payloadB64) {
74
- return createHmac("sha256", secret).update(payloadB64).digest();
75
- }
76
- function signRefreshToken(apiKey) {
77
- const payload = {
78
- k: apiKey,
79
- i: Date.now(),
80
- j: randomUUID()
81
- };
82
- const payloadB64 = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
83
- const sigB64 = sign(payloadB64).toString("base64url");
84
- return `${PREFIX}${payloadB64}.${sigB64}`;
85
- }
86
- function verifyRefreshToken(token) {
87
- if (typeof token !== "string" || !token.startsWith(PREFIX)) return null;
88
- const body = token.slice(PREFIX.length);
89
- const dot = body.indexOf(".");
90
- if (dot <= 0 || dot === body.length - 1) return null;
91
- const payloadB64 = body.slice(0, dot);
92
- const sigB64 = body.slice(dot + 1);
93
- let providedSig;
94
- try {
95
- providedSig = Buffer.from(sigB64, "base64url");
96
- } catch {
97
- return null;
98
- }
99
- const expectedSig = sign(payloadB64);
100
- if (providedSig.length !== expectedSig.length) return null;
101
- if (!timingSafeEqual(providedSig, expectedSig)) return null;
102
- let payload;
103
- try {
104
- const json = Buffer.from(payloadB64, "base64url").toString("utf8");
105
- payload = JSON.parse(json);
106
- } catch {
107
- return null;
108
- }
109
- if (!payload || typeof payload.k !== "string" || typeof payload.i !== "number" || typeof payload.j !== "string") {
110
- return null;
111
- }
112
- if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;
113
- return { apiKey: payload.k };
114
- }
115
-
116
61
  // src/sessionCap.ts
117
62
  function planKeyAdmission(sessions2, keyHash, cap, now, opts) {
118
63
  if (cap < 1) {
@@ -225,7 +170,7 @@ var AUTH_FAILURE_MAX = 10;
225
170
  var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
226
171
  var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
227
172
  var MAX_AUTH_FAILURE_ENTRIES = 1e4;
228
- function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenTtlMs) {
173
+ function resolveBearerAuth(authorizationHeader) {
229
174
  if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
230
175
  return { status: "absent" };
231
176
  }
@@ -239,15 +184,6 @@ function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenT
239
184
  if (token.startsWith("pb_sk_")) {
240
185
  return { status: "ok", apiKey: token };
241
186
  }
242
- if (token.startsWith("pb_at_")) {
243
- const entry = accessTokens2.get(token);
244
- if (!entry) return { status: "rejected" };
245
- if (now - entry.createdAt > accessTokenTtlMs) {
246
- accessTokens2.delete(token);
247
- return { status: "rejected" };
248
- }
249
- return { status: "ok", apiKey: entry.apiKey };
250
- }
251
187
  return { status: "rejected" };
252
188
  }
253
189
  function checkAuthBlock(failures, ip, now) {
@@ -272,6 +208,14 @@ function recordAuthFailure(failures, ip, now) {
272
208
  }
273
209
  }
274
210
  }
211
+ function initializeKeyGate(verdict, failures, ip, now) {
212
+ if (verdict === "ok" || verdict === "rate_limited") return "admit";
213
+ if (verdict === "rejected") {
214
+ recordAuthFailure(failures, ip, now);
215
+ return "unauthorized";
216
+ }
217
+ return "unavailable";
218
+ }
275
219
 
276
220
  // src/lib/oauthRedirectAllowlist.ts
277
221
  var DEFAULT_OAUTH_REDIRECT_URIS = [
@@ -361,12 +305,24 @@ function pendingCodeRefusal(pending, redirectUri, now) {
361
305
  if (now > pending.expiresAt) return "expired";
362
306
  return null;
363
307
  }
308
+ function tokenResponse(apiKey) {
309
+ return {
310
+ access_token: apiKey,
311
+ token_type: "Bearer",
312
+ // 1 year: validity is enforced by Convex on every call, not by this clock.
313
+ expires_in: 365 * 24 * 3600
314
+ };
315
+ }
364
316
 
365
317
  // src/http.ts
366
318
  bootstrapHttp();
367
319
  initAnalytics();
368
320
  initFeatureFlags(getPostHogClient());
369
321
  var PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? "3002", 10);
322
+ function keyCheckCandidates() {
323
+ const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\/$/, "");
324
+ return [primaryUrl, ...parseFallbackUrls(process.env.CONVEX_FALLBACK_URLS).map((u) => u.replace(/\/$/, ""))];
325
+ }
370
326
  function baseUrl(req) {
371
327
  const proto = req.headers["x-forwarded-proto"] ?? req.protocol ?? "http";
372
328
  const host = req.headers.host ?? `localhost:${PORT}`;
@@ -411,7 +367,8 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
411
367
  token_endpoint: `${base}/oauth/token`,
412
368
  registration_endpoint: `${base}/register`,
413
369
  response_types_supported: ["code"],
414
- grant_types_supported: ["authorization_code", "refresh_token"],
370
+ grant_types_supported: ["authorization_code"],
371
+ // TEN-4548: no refresh grant (lib/oauthAuthCode.ts tokenResponse)
415
372
  code_challenge_methods_supported: ["S256"],
416
373
  token_endpoint_auth_methods_supported: ["none"],
417
374
  scopes_supported: ["mcp:tools", "mcp:resources"]
@@ -444,7 +401,7 @@ app.post(
444
401
  res.status(400).json({ error: "invalid_redirect_uri", error_description: refusal });
445
402
  return;
446
403
  }
447
- const clientId = `pb_client_${randomUUID2()}`;
404
+ const clientId = `pb_client_${randomUUID()}`;
448
405
  const client = {
449
406
  client_id: clientId,
450
407
  redirect_uris,
@@ -463,9 +420,6 @@ app.post(
463
420
  }
464
421
  );
465
422
  var pendingCodes = /* @__PURE__ */ new Map();
466
- var ACCESS_TOKEN_TTL = 3600;
467
- var ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1e3;
468
- var accessTokens = /* @__PURE__ */ new Map();
469
423
  setInterval(() => {
470
424
  const now = Date.now();
471
425
  for (const [code, auth] of pendingCodes) {
@@ -474,9 +428,6 @@ setInterval(() => {
474
428
  for (const [id, client] of registeredClients) {
475
429
  if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
476
430
  }
477
- for (const [token, entry] of accessTokens) {
478
- if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
479
- }
480
431
  for (const [ip, rec] of authFailures) {
481
432
  if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
482
433
  authFailures.delete(ip);
@@ -1118,61 +1069,28 @@ app.post(
1118
1069
  return;
1119
1070
  }
1120
1071
  let workspaceName = "Your Workspace";
1121
- try {
1122
- const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\/$/, "");
1123
- const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? "").split(",").map((u) => u.trim().replace(/\/$/, "")).filter(Boolean);
1124
- const candidates = [primaryUrl, ...fallbackUrls];
1125
- let foundUrl;
1126
- let anyDefinitiveReject = false;
1127
- let rateLimitedRetryAfter;
1128
- for (const url2 of candidates) {
1129
- let checkData = null;
1130
- try {
1131
- const checkRes = await fetch(`${url2}/api/key-check`, {
1132
- method: "POST",
1133
- headers: { "Authorization": `Bearer ${api_key}`, "Content-Type": "application/json" },
1134
- signal: AbortSignal.timeout(5e3)
1135
- });
1136
- checkData = await checkRes.json();
1137
- } catch {
1138
- continue;
1139
- }
1140
- if (checkData.ok) {
1141
- if (checkData.workspaceName) workspaceName = checkData.workspaceName;
1142
- foundUrl = checkData.deploymentUrl ?? url2;
1143
- break;
1144
- }
1145
- if (checkData.error === "rate_limited") {
1146
- rateLimitedRetryAfter = checkData.retryAfterSeconds;
1147
- continue;
1148
- }
1149
- anyDefinitiveReject = true;
1150
- }
1151
- if (!foundUrl) {
1152
- if (rateLimitedRetryAfter != null && !anyDefinitiveReject) {
1153
- sendError(
1154
- "Rate limited",
1155
- `This API key is valid but temporarily rate limited. Try again in ${rateLimitedRetryAfter}s.`,
1156
- 429
1157
- );
1158
- return;
1159
- }
1160
- if (anyDefinitiveReject) {
1161
- sendError(
1162
- "Key not recognized",
1163
- "This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.",
1164
- 401
1165
- );
1166
- return;
1167
- }
1168
- process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
1169
- } else {
1170
- getKeyState(api_key).deploymentUrl = foundUrl;
1171
- }
1172
- } catch {
1072
+ const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5e3);
1073
+ if (check.verdict === "ok") {
1074
+ if (check.workspaceName) workspaceName = check.workspaceName;
1075
+ getKeyState(api_key).deploymentUrl = check.deploymentUrl;
1076
+ } else if (check.verdict === "rate_limited") {
1077
+ sendError(
1078
+ "Rate limited",
1079
+ `This API key is valid but temporarily rate limited. Try again in ${check.retryAfterSeconds ?? 60}s.`,
1080
+ 429
1081
+ );
1082
+ return;
1083
+ } else if (check.verdict === "rejected") {
1084
+ sendError(
1085
+ "Key not recognized",
1086
+ "This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.",
1087
+ 401
1088
+ );
1089
+ return;
1090
+ } else {
1173
1091
  process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
1174
1092
  }
1175
- const code = randomUUID2();
1093
+ const code = randomUUID();
1176
1094
  pendingCodes.set(code, {
1177
1095
  apiKey: api_key,
1178
1096
  codeChallenge: code_challenge,
@@ -1191,35 +1109,13 @@ app.post(
1191
1109
  }
1192
1110
  }
1193
1111
  );
1194
- function issueTokens(apiKey) {
1195
- return {
1196
- access_token: apiKey,
1197
- token_type: "Bearer",
1198
- // 1-year TTL: actual validity enforced by Convex, not by expiry clock.
1199
- // Long TTL prevents unnecessary refresh cycles after restarts.
1200
- expires_in: 365 * 24 * 3600,
1201
- refresh_token: signRefreshToken(apiKey)
1202
- };
1203
- }
1204
1112
  app.post(
1205
1113
  "/oauth/token",
1206
1114
  authLimiter,
1207
1115
  express.urlencoded({ extended: false }),
1208
1116
  express.json(),
1209
1117
  (req, res) => {
1210
- const { grant_type, code, code_verifier, redirect_uri, refresh_token } = req.body;
1211
- if (grant_type === "refresh_token") {
1212
- const verified = verifyRefreshToken(refresh_token);
1213
- if (!verified) {
1214
- res.status(400).json({
1215
- error: "invalid_grant",
1216
- error_description: "Invalid or expired refresh token"
1217
- });
1218
- return;
1219
- }
1220
- res.json(issueTokens(verified.apiKey));
1221
- return;
1222
- }
1118
+ const { grant_type, code, code_verifier, redirect_uri } = req.body;
1223
1119
  if (grant_type !== "authorization_code") {
1224
1120
  res.status(400).json({ error: "unsupported_grant_type" });
1225
1121
  return;
@@ -1245,7 +1141,7 @@ app.post(
1245
1141
  return;
1246
1142
  }
1247
1143
  pendingCodes.delete(code);
1248
- res.json(issueTokens(pending.apiKey));
1144
+ res.json(tokenResponse(pending.apiKey));
1249
1145
  }
1250
1146
  );
1251
1147
  var mcpLimiter = rateLimit({
@@ -1339,7 +1235,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1339
1235
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1340
1236
  return;
1341
1237
  }
1342
- const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1238
+ const auth = resolveBearerAuth(req.headers?.authorization);
1343
1239
  if (auth.status !== "ok") {
1344
1240
  logRequest("POST", "auth_fail");
1345
1241
  if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
@@ -1371,6 +1267,22 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1371
1267
  entry.lastAccess = Date.now();
1372
1268
  }
1373
1269
  } else if (!sessionId && isInitializeRequest(req.body)) {
1270
+ const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5e3);
1271
+ const gate = initializeKeyGate(check.verdict, authFailures, reqIp, Date.now());
1272
+ if (gate === "unauthorized") {
1273
+ logRequest("POST", "auth_fail");
1274
+ send401(req, res);
1275
+ return;
1276
+ }
1277
+ if (gate === "unavailable") {
1278
+ res.status(503).json({
1279
+ jsonrpc: "2.0",
1280
+ error: { code: -32e3, message: "Product Brain could not verify this API key right now. Retry shortly." },
1281
+ id: req.body?.id ?? null
1282
+ });
1283
+ return;
1284
+ }
1285
+ if (check.verdict === "ok" || check.verdict === "rate_limited") getKeyState(apiKey).deploymentUrl = check.deploymentUrl;
1374
1286
  const keyH = hashKey(apiKey);
1375
1287
  const plan = planKeyAdmission(
1376
1288
  sessions,
@@ -1396,7 +1308,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1396
1308
  }));
1397
1309
  return;
1398
1310
  }
1399
- const sid = randomUUID2();
1311
+ const sid = randomUUID();
1400
1312
  let initialized = false;
1401
1313
  const transport = new StreamableHTTPServerTransport({
1402
1314
  sessionIdGenerator: () => sid,
@@ -1479,7 +1391,7 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
1479
1391
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1480
1392
  return;
1481
1393
  }
1482
- const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1394
+ const auth = resolveBearerAuth(req.headers?.authorization);
1483
1395
  if (auth.status !== "ok") {
1484
1396
  logRequest("GET", "auth_fail");
1485
1397
  if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
@@ -1528,7 +1440,7 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
1528
1440
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1529
1441
  return;
1530
1442
  }
1531
- const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1443
+ const auth = resolveBearerAuth(req.headers?.authorization);
1532
1444
  if (auth.status !== "ok") {
1533
1445
  logRequest("DELETE", "auth_fail");
1534
1446
  if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);