@productbrain/mcp 0.0.1-beta.9450 → 0.0.1-beta.9452
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/{chunk-BCYOMOD2.js → chunk-NCTCZQHK.js} +2 -2
- package/dist/{chunk-DKJ4X2D7.js → chunk-Q77SFDBZ.js} +1 -1
- package/dist/chunk-Q77SFDBZ.js.map +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/http.js +22 -107
- package/dist/http.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{setup-ZT5VSESE.js → setup-6UYW6XKO.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-DKJ4X2D7.js.map +0 -1
- /package/dist/{chunk-BCYOMOD2.js.map → chunk-NCTCZQHK.js.map} +0 -0
- /package/dist/{setup-ZT5VSESE.js.map → setup-6UYW6XKO.js.map} +0 -0
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-NCTCZQHK.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_CLOUD_URL,
|
|
8
8
|
apiKeysUrl,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
initAnalytics,
|
|
15
15
|
runWithAuth,
|
|
16
16
|
shutdownAnalytics
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-Q77SFDBZ.js";
|
|
18
18
|
|
|
19
19
|
// src/http.ts
|
|
20
|
-
import { createHash, randomUUID
|
|
20
|
+
import { createHash, randomUUID } from "crypto";
|
|
21
21
|
import express from "express";
|
|
22
22
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
23
23
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -56,63 +56,6 @@ function appLogoMarkup(opts = {}) {
|
|
|
56
56
|
return `<span class="${cls}"><span class="pb-logo__mark"><span class="pb-logo__core"></span></span>${wordmark}</span>`;
|
|
57
57
|
}
|
|
58
58
|
|
|
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
59
|
// src/sessionCap.ts
|
|
117
60
|
function planKeyAdmission(sessions2, keyHash, cap, now, opts) {
|
|
118
61
|
if (cap < 1) {
|
|
@@ -225,7 +168,7 @@ var AUTH_FAILURE_MAX = 10;
|
|
|
225
168
|
var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
|
|
226
169
|
var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
|
|
227
170
|
var MAX_AUTH_FAILURE_ENTRIES = 1e4;
|
|
228
|
-
function resolveBearerAuth(authorizationHeader
|
|
171
|
+
function resolveBearerAuth(authorizationHeader) {
|
|
229
172
|
if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
|
|
230
173
|
return { status: "absent" };
|
|
231
174
|
}
|
|
@@ -239,15 +182,6 @@ function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenT
|
|
|
239
182
|
if (token.startsWith("pb_sk_")) {
|
|
240
183
|
return { status: "ok", apiKey: token };
|
|
241
184
|
}
|
|
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
185
|
return { status: "rejected" };
|
|
252
186
|
}
|
|
253
187
|
function checkAuthBlock(failures, ip, now) {
|
|
@@ -361,6 +295,14 @@ function pendingCodeRefusal(pending, redirectUri, now) {
|
|
|
361
295
|
if (now > pending.expiresAt) return "expired";
|
|
362
296
|
return null;
|
|
363
297
|
}
|
|
298
|
+
function tokenResponse(apiKey) {
|
|
299
|
+
return {
|
|
300
|
+
access_token: apiKey,
|
|
301
|
+
token_type: "Bearer",
|
|
302
|
+
// 1 year: validity is enforced by Convex on every call, not by this clock.
|
|
303
|
+
expires_in: 365 * 24 * 3600
|
|
304
|
+
};
|
|
305
|
+
}
|
|
364
306
|
|
|
365
307
|
// src/http.ts
|
|
366
308
|
bootstrapHttp();
|
|
@@ -411,7 +353,8 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
|
|
|
411
353
|
token_endpoint: `${base}/oauth/token`,
|
|
412
354
|
registration_endpoint: `${base}/register`,
|
|
413
355
|
response_types_supported: ["code"],
|
|
414
|
-
grant_types_supported: ["authorization_code"
|
|
356
|
+
grant_types_supported: ["authorization_code"],
|
|
357
|
+
// TEN-4548: no refresh grant (lib/oauthAuthCode.ts tokenResponse)
|
|
415
358
|
code_challenge_methods_supported: ["S256"],
|
|
416
359
|
token_endpoint_auth_methods_supported: ["none"],
|
|
417
360
|
scopes_supported: ["mcp:tools", "mcp:resources"]
|
|
@@ -444,7 +387,7 @@ app.post(
|
|
|
444
387
|
res.status(400).json({ error: "invalid_redirect_uri", error_description: refusal });
|
|
445
388
|
return;
|
|
446
389
|
}
|
|
447
|
-
const clientId = `pb_client_${
|
|
390
|
+
const clientId = `pb_client_${randomUUID()}`;
|
|
448
391
|
const client = {
|
|
449
392
|
client_id: clientId,
|
|
450
393
|
redirect_uris,
|
|
@@ -463,9 +406,6 @@ app.post(
|
|
|
463
406
|
}
|
|
464
407
|
);
|
|
465
408
|
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
409
|
setInterval(() => {
|
|
470
410
|
const now = Date.now();
|
|
471
411
|
for (const [code, auth] of pendingCodes) {
|
|
@@ -474,9 +414,6 @@ setInterval(() => {
|
|
|
474
414
|
for (const [id, client] of registeredClients) {
|
|
475
415
|
if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
|
|
476
416
|
}
|
|
477
|
-
for (const [token, entry] of accessTokens) {
|
|
478
|
-
if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);
|
|
479
|
-
}
|
|
480
417
|
for (const [ip, rec] of authFailures) {
|
|
481
418
|
if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
|
|
482
419
|
authFailures.delete(ip);
|
|
@@ -1172,7 +1109,7 @@ app.post(
|
|
|
1172
1109
|
} catch {
|
|
1173
1110
|
process.stderr.write("[authorize] key-check unavailable \u2014 proceeding without validation\n");
|
|
1174
1111
|
}
|
|
1175
|
-
const code =
|
|
1112
|
+
const code = randomUUID();
|
|
1176
1113
|
pendingCodes.set(code, {
|
|
1177
1114
|
apiKey: api_key,
|
|
1178
1115
|
codeChallenge: code_challenge,
|
|
@@ -1191,35 +1128,13 @@ app.post(
|
|
|
1191
1128
|
}
|
|
1192
1129
|
}
|
|
1193
1130
|
);
|
|
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
1131
|
app.post(
|
|
1205
1132
|
"/oauth/token",
|
|
1206
1133
|
authLimiter,
|
|
1207
1134
|
express.urlencoded({ extended: false }),
|
|
1208
1135
|
express.json(),
|
|
1209
1136
|
(req, res) => {
|
|
1210
|
-
const { grant_type, code, code_verifier, redirect_uri
|
|
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
|
-
}
|
|
1137
|
+
const { grant_type, code, code_verifier, redirect_uri } = req.body;
|
|
1223
1138
|
if (grant_type !== "authorization_code") {
|
|
1224
1139
|
res.status(400).json({ error: "unsupported_grant_type" });
|
|
1225
1140
|
return;
|
|
@@ -1245,7 +1160,7 @@ app.post(
|
|
|
1245
1160
|
return;
|
|
1246
1161
|
}
|
|
1247
1162
|
pendingCodes.delete(code);
|
|
1248
|
-
res.json(
|
|
1163
|
+
res.json(tokenResponse(pending.apiKey));
|
|
1249
1164
|
}
|
|
1250
1165
|
);
|
|
1251
1166
|
var mcpLimiter = rateLimit({
|
|
@@ -1339,7 +1254,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1339
1254
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1340
1255
|
return;
|
|
1341
1256
|
}
|
|
1342
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1257
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1343
1258
|
if (auth.status !== "ok") {
|
|
1344
1259
|
logRequest("POST", "auth_fail");
|
|
1345
1260
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|
|
@@ -1396,7 +1311,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1396
1311
|
}));
|
|
1397
1312
|
return;
|
|
1398
1313
|
}
|
|
1399
|
-
const sid =
|
|
1314
|
+
const sid = randomUUID();
|
|
1400
1315
|
let initialized = false;
|
|
1401
1316
|
const transport = new StreamableHTTPServerTransport({
|
|
1402
1317
|
sessionIdGenerator: () => sid,
|
|
@@ -1479,7 +1394,7 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1479
1394
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1480
1395
|
return;
|
|
1481
1396
|
}
|
|
1482
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1397
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1483
1398
|
if (auth.status !== "ok") {
|
|
1484
1399
|
logRequest("GET", "auth_fail");
|
|
1485
1400
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|
|
@@ -1528,7 +1443,7 @@ app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1528
1443
|
send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
|
|
1529
1444
|
return;
|
|
1530
1445
|
}
|
|
1531
|
-
const auth = resolveBearerAuth(req.headers?.authorization
|
|
1446
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1532
1447
|
if (auth.status !== "ok") {
|
|
1533
1448
|
logRequest("DELETE", "auth_fail");
|
|
1534
1449
|
if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
|