@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-
|
|
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
|
|
181
|
+
return rejected(authorizationHeader.trim());
|
|
179
182
|
}
|
|
180
183
|
const token = authorizationHeader.slice(7).trim();
|
|
181
|
-
if (token
|
|
182
|
-
|
|
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
|
-
|
|
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,
|
|
203
|
-
const rec = failures.get(
|
|
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,
|
|
208
|
-
|
|
209
|
-
|
|
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,
|
|
214
|
-
const rec = failures.get(
|
|
213
|
+
function recordAuthFailure(failures, credentialId, now) {
|
|
214
|
+
const rec = failures.get(credentialId);
|
|
215
215
|
if (!rec) {
|
|
216
|
-
failures.
|
|
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,
|
|
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,
|
|
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 [
|
|
463
|
+
for (const [credentialId, rec] of authFailures) {
|
|
460
464
|
if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
|
|
461
|
-
authFailures.delete(
|
|
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
|
|
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", `
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
1474
|
+
if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
|
|
1481
1475
|
send401(req, res);
|
|
1482
1476
|
return;
|
|
1483
1477
|
}
|