@productbrain/mcp 0.0.1-beta.9756 → 0.0.1-beta.9775

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-ETBTCCMU.js";
5
+ } from "./chunk-ER5AXQN2.js";
6
6
  import {
7
7
  DEFAULT_CLOUD_URL,
8
8
  apiKeysUrl,
@@ -170,25 +170,24 @@ var AUTH_FAILURE_MAX = 10;
170
170
  var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
171
171
  var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
172
172
  var MAX_AUTH_FAILURE_ENTRIES = 1e4;
173
+ function rejected(presented) {
174
+ return { status: "rejected", credentialId: hashKey(presented) };
175
+ }
173
176
  function resolveBearerAuth(authorizationHeader) {
174
177
  if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
175
178
  return { status: "absent" };
176
179
  }
177
180
  if (!authorizationHeader.startsWith("Bearer ")) {
178
- return { status: "rejected" };
181
+ return rejected(authorizationHeader.trim());
179
182
  }
180
183
  const token = authorizationHeader.slice(7).trim();
181
- if (token === "") {
182
- return { status: "rejected" };
183
- }
184
- if (token.startsWith("pb_sk_")) {
185
- return { status: "ok", apiKey: token };
186
- }
187
- return { status: "rejected" };
184
+ if (!token.startsWith("pb_sk_")) return rejected(token);
185
+ return { status: "ok", apiKey: token, credentialId: hashKey(token) };
188
186
  }
189
187
  function resolvePastedKey(apiKey) {
190
188
  if (apiKey === "") return { status: "absent" };
191
- return apiKey.startsWith("pb_sk_") ? { status: "ok", apiKey } : { status: "rejected" };
189
+ if (!apiKey.startsWith("pb_sk_")) return rejected(apiKey);
190
+ return { status: "ok", apiKey, credentialId: hashKey(apiKey) };
192
191
  }
193
192
  function isCrossSiteFormPost(origin, secFetchSite, ownOrigin) {
194
193
  if (secFetchSite === "cross-site") return true;
@@ -199,21 +198,26 @@ function isCrossSiteFormPost(origin, secFetchSite, ownOrigin) {
199
198
  return true;
200
199
  }
201
200
  }
202
- function checkAuthBlock(failures, ip, now) {
203
- const rec = failures.get(ip);
201
+ function checkAuthBlock(failures, credentialId, now) {
202
+ const rec = failures.get(credentialId);
204
203
  if (!rec) return false;
205
204
  return rec.blockedUntil > now;
206
205
  }
207
- function authLockoutError(failures, ip, now, id = null) {
208
- const rec = failures.get(ip);
209
- if (!rec || !checkAuthBlock(failures, ip, now)) return null;
206
+ function authLockoutError(failures, auth, now, id = null) {
207
+ if (auth.status === "absent") return null;
208
+ const rec = failures.get(auth.credentialId);
209
+ if (!rec || !checkAuthBlock(failures, auth.credentialId, now)) return null;
210
210
  const retryAfterSeconds = Math.max(0, Math.ceil((rec.blockedUntil - now) / 1e3));
211
211
  return buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec.count, id });
212
212
  }
213
- function recordAuthFailure(failures, ip, now) {
214
- const rec = failures.get(ip);
213
+ function recordAuthFailure(failures, credentialId, now) {
214
+ const rec = failures.get(credentialId);
215
215
  if (!rec) {
216
- failures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
216
+ if (failures.size >= MAX_AUTH_FAILURE_ENTRIES) {
217
+ const oldest = failures.keys().next().value;
218
+ if (oldest !== void 0) failures.delete(oldest);
219
+ }
220
+ failures.set(credentialId, { count: 1, firstFailure: now, blockedUntil: 0 });
217
221
  return;
218
222
  }
219
223
  if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
@@ -227,10 +231,10 @@ function recordAuthFailure(failures, ip, now) {
227
231
  }
228
232
  }
229
233
  }
230
- function keyVerdictGate(verdict, failures, ip, now) {
234
+ function keyVerdictGate(verdict, failures, credentialId, now) {
231
235
  if (verdict === "ok" || verdict === "rate_limited") return "admit";
232
236
  if (verdict === "rejected") {
233
- recordAuthFailure(failures, ip, now);
237
+ recordAuthFailure(failures, credentialId, now);
234
238
  return "unauthorized";
235
239
  }
236
240
  return "unavailable";
@@ -456,15 +460,9 @@ setInterval(() => {
456
460
  for (const [id, client] of registeredClients) {
457
461
  if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
458
462
  }
459
- for (const [ip, rec] of authFailures) {
463
+ for (const [credentialId, rec] of authFailures) {
460
464
  if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
461
- authFailures.delete(ip);
462
- }
463
- }
464
- if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {
465
- const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);
466
- for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {
467
- authFailures.delete(sorted[i][0]);
465
+ authFailures.delete(credentialId);
468
466
  }
469
467
  }
470
468
  }, 6e4);
@@ -1067,18 +1065,17 @@ app.post(
1067
1065
  sendError("Request refused", "This form only accepts keys sent from its own sign-in page. Open the connect link again.", 403);
1068
1066
  return;
1069
1067
  }
1070
- const reqIp = req.ip ?? "unknown";
1071
1068
  const nowTs = Date.now();
1072
- const lockout = authLockoutError(authFailures, reqIp, nowTs);
1069
+ const pasted = resolvePastedKey(api_key);
1070
+ const lockout = authLockoutError(authFailures, pasted, nowTs);
1073
1071
  if (lockout) {
1074
1072
  const retryAfter = lockout.headers["Retry-After"];
1075
1073
  res.setHeader("Retry-After", retryAfter);
1076
- sendError("Too many attempts", `Too many keys from this network were not recognized. Try again in ${retryAfter}s.`, 429);
1074
+ sendError("Too many attempts", `This key was not recognized too many times. Try again in ${retryAfter}s.`, 429);
1077
1075
  return;
1078
1076
  }
1079
- const pasted = resolvePastedKey(api_key);
1080
1077
  if (pasted.status !== "ok") {
1081
- if (pasted.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1078
+ if (pasted.status === "rejected") recordAuthFailure(authFailures, pasted.credentialId, nowTs);
1082
1079
  sendError("Invalid key format", "API keys start with <code>pb_sk_</code>. Check your key and try again.");
1083
1080
  return;
1084
1081
  }
@@ -1110,7 +1107,7 @@ app.post(
1110
1107
  }
1111
1108
  let workspaceName = "Your Workspace";
1112
1109
  const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5e3);
1113
- const gate = keyVerdictGate(check.verdict, authFailures, reqIp, nowTs);
1110
+ const gate = keyVerdictGate(check.verdict, authFailures, pasted.credentialId, nowTs);
1114
1111
  if (check.verdict === "ok") {
1115
1112
  if (check.workspaceName) workspaceName = check.workspaceName;
1116
1113
  getKeyState(api_key).deploymentUrl = check.deploymentUrl;
@@ -1264,17 +1261,16 @@ function logSessionLifecycle(event, sessionId, reason) {
1264
1261
  `);
1265
1262
  }
1266
1263
  app.post("/mcp", mcpLimiter, async (req, res) => {
1267
- const reqIp = req.ip ?? "unknown";
1268
1264
  const nowTs = Date.now();
1269
- const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);
1265
+ const auth = resolveBearerAuth(req.headers?.authorization);
1266
+ const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
1270
1267
  if (lockout) {
1271
1268
  send429(res, lockout);
1272
1269
  return;
1273
1270
  }
1274
- const auth = resolveBearerAuth(req.headers?.authorization);
1275
1271
  if (auth.status !== "ok") {
1276
1272
  logRequest("POST", "auth_fail");
1277
- if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1273
+ if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
1278
1274
  send401(req, res);
1279
1275
  return;
1280
1276
  }
@@ -1304,7 +1300,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1304
1300
  }
1305
1301
  } else if (!sessionId && isInitializeRequest(req.body)) {
1306
1302
  const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5e3);
1307
- const gate = keyVerdictGate(check.verdict, authFailures, reqIp, Date.now());
1303
+ const gate = keyVerdictGate(check.verdict, authFailures, auth.credentialId, Date.now());
1308
1304
  if (gate === "unauthorized") {
1309
1305
  logRequest("POST", "auth_fail");
1310
1306
  send401(req, res);
@@ -1419,17 +1415,16 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1419
1415
  }
1420
1416
  });
1421
1417
  app.get("/mcp", mcpLimiter, async (req, res) => {
1422
- const reqIp = req.ip ?? "unknown";
1423
1418
  const nowTs = Date.now();
1424
- const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);
1419
+ const auth = resolveBearerAuth(req.headers?.authorization);
1420
+ const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
1425
1421
  if (lockout) {
1426
1422
  send429(res, lockout);
1427
1423
  return;
1428
1424
  }
1429
- const auth = resolveBearerAuth(req.headers?.authorization);
1430
1425
  if (auth.status !== "ok") {
1431
1426
  logRequest("GET", "auth_fail");
1432
- if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1427
+ if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
1433
1428
  send401(req, res);
1434
1429
  return;
1435
1430
  }
@@ -1467,17 +1462,16 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
1467
1462
  }
1468
1463
  });
1469
1464
  app.delete("/mcp", mcpLimiter, async (req, res) => {
1470
- const reqIp = req.ip ?? "unknown";
1471
1465
  const nowTs = Date.now();
1472
- const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);
1466
+ const auth = resolveBearerAuth(req.headers?.authorization);
1467
+ const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
1473
1468
  if (lockout) {
1474
1469
  send429(res, lockout);
1475
1470
  return;
1476
1471
  }
1477
- const auth = resolveBearerAuth(req.headers?.authorization);
1478
1472
  if (auth.status !== "ok") {
1479
1473
  logRequest("DELETE", "auth_fail");
1480
- if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1474
+ if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
1481
1475
  send401(req, res);
1482
1476
  return;
1483
1477
  }