@productbrain/mcp 0.0.1-beta.1774 → 0.0.1-beta.1777

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
@@ -218,6 +218,59 @@ function send429(res, built) {
218
218
  res.status(built.status).json(built.body);
219
219
  }
220
220
 
221
+ // src/authLockout.ts
222
+ var AUTH_FAILURE_MAX = 10;
223
+ var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
224
+ var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
225
+ var MAX_AUTH_FAILURE_ENTRIES = 1e4;
226
+ function resolveBearerAuth(authorizationHeader, accessTokens2, now, accessTokenTtlMs) {
227
+ if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
228
+ return { status: "absent" };
229
+ }
230
+ if (!authorizationHeader.startsWith("Bearer ")) {
231
+ return { status: "rejected" };
232
+ }
233
+ const token = authorizationHeader.slice(7).trim();
234
+ if (token === "") {
235
+ return { status: "rejected" };
236
+ }
237
+ if (token.startsWith("pb_sk_")) {
238
+ return { status: "ok", apiKey: token };
239
+ }
240
+ if (token.startsWith("pb_at_")) {
241
+ const entry = accessTokens2.get(token);
242
+ if (!entry) return { status: "rejected" };
243
+ if (now - entry.createdAt > accessTokenTtlMs) {
244
+ accessTokens2.delete(token);
245
+ return { status: "rejected" };
246
+ }
247
+ return { status: "ok", apiKey: entry.apiKey };
248
+ }
249
+ return { status: "rejected" };
250
+ }
251
+ function checkAuthBlock(failures, ip, now) {
252
+ const rec = failures.get(ip);
253
+ if (!rec) return false;
254
+ return rec.blockedUntil > now;
255
+ }
256
+ function recordAuthFailure(failures, ip, now) {
257
+ const rec = failures.get(ip);
258
+ if (!rec) {
259
+ failures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
260
+ return;
261
+ }
262
+ if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
263
+ rec.count = 1;
264
+ rec.firstFailure = now;
265
+ rec.blockedUntil = 0;
266
+ } else {
267
+ rec.count++;
268
+ if (rec.count >= AUTH_FAILURE_MAX) {
269
+ rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
270
+ }
271
+ }
272
+ }
273
+
221
274
  // src/http.ts
222
275
  bootstrapHttp();
223
276
  initAnalytics();
@@ -1105,33 +1158,6 @@ var mcpLimiter = rateLimit({
1105
1158
  }
1106
1159
  });
1107
1160
  var authFailures = /* @__PURE__ */ new Map();
1108
- var AUTH_FAILURE_MAX = 10;
1109
- var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
1110
- var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
1111
- var MAX_AUTH_FAILURE_ENTRIES = 1e4;
1112
- function checkAuthBlock(ip) {
1113
- const rec = authFailures.get(ip);
1114
- if (!rec) return false;
1115
- return rec.blockedUntil > Date.now();
1116
- }
1117
- function recordAuthFailure(ip) {
1118
- const now = Date.now();
1119
- const rec = authFailures.get(ip);
1120
- if (!rec) {
1121
- authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });
1122
- return;
1123
- }
1124
- if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
1125
- rec.count = 1;
1126
- rec.firstFailure = now;
1127
- rec.blockedUntil = 0;
1128
- } else {
1129
- rec.count++;
1130
- if (rec.count >= AUTH_FAILURE_MAX) {
1131
- rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;
1132
- }
1133
- }
1134
- }
1135
1161
  app.get("/health", (_req, res) => {
1136
1162
  res.json({ status: "ok", version: SERVER_VERSION, transport: "http" });
1137
1163
  });
@@ -1176,25 +1202,6 @@ function evictStaleSessions() {
1176
1202
  }
1177
1203
  }
1178
1204
  setInterval(evictStaleSessions, 6e4);
1179
- function extractBearerKey(req) {
1180
- const header = req.headers?.authorization;
1181
- if (typeof header !== "string" || !header.startsWith("Bearer ")) return null;
1182
- const token = header.slice(7).trim();
1183
- if (token.startsWith("pb_sk_")) {
1184
- return token;
1185
- }
1186
- if (token.startsWith("pb_at_")) {
1187
- const entry = accessTokens.get(token);
1188
- if (!entry) return null;
1189
- const now = Date.now();
1190
- if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {
1191
- accessTokens.delete(token);
1192
- return null;
1193
- }
1194
- return entry.apiKey;
1195
- }
1196
- return null;
1197
- }
1198
1205
  function send401(req, res) {
1199
1206
  const base = baseUrl(req);
1200
1207
  res.status(401).set(
@@ -1217,19 +1224,21 @@ function logSessionLifecycle(event, sessionId, reason) {
1217
1224
  }
1218
1225
  app.post("/mcp", mcpLimiter, async (req, res) => {
1219
1226
  const reqIp = req.ip ?? "unknown";
1220
- if (checkAuthBlock(reqIp)) {
1227
+ const nowTs = Date.now();
1228
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1221
1229
  const rec = authFailures.get(reqIp);
1222
- const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1e3)) : 0;
1230
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1223
1231
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1224
1232
  return;
1225
1233
  }
1226
- const apiKey = extractBearerKey(req);
1227
- if (!apiKey) {
1234
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1235
+ if (auth.status !== "ok") {
1228
1236
  logRequest("POST", "auth_fail");
1229
- recordAuthFailure(reqIp);
1237
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1230
1238
  send401(req, res);
1231
1239
  return;
1232
1240
  }
1241
+ const apiKey = auth.apiKey;
1233
1242
  const sessionId = req.headers["mcp-session-id"];
1234
1243
  const reqStart = Date.now();
1235
1244
  try {
@@ -1354,19 +1363,21 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
1354
1363
  });
1355
1364
  app.get("/mcp", mcpLimiter, async (req, res) => {
1356
1365
  const reqIp = req.ip ?? "unknown";
1357
- if (checkAuthBlock(reqIp)) {
1366
+ const nowTs = Date.now();
1367
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1358
1368
  const rec = authFailures.get(reqIp);
1359
- const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1e3)) : 0;
1369
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1360
1370
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1361
1371
  return;
1362
1372
  }
1363
- const apiKey = extractBearerKey(req);
1364
- if (!apiKey) {
1373
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1374
+ if (auth.status !== "ok") {
1365
1375
  logRequest("GET", "auth_fail");
1366
- recordAuthFailure(reqIp);
1376
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1367
1377
  send401(req, res);
1368
1378
  return;
1369
1379
  }
1380
+ const apiKey = auth.apiKey;
1370
1381
  const sessionId = req.headers["mcp-session-id"];
1371
1382
  if (!sessionId) {
1372
1383
  res.status(400).send("Missing Mcp-Session-Id header");
@@ -1401,19 +1412,21 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
1401
1412
  });
1402
1413
  app.delete("/mcp", mcpLimiter, async (req, res) => {
1403
1414
  const reqIp = req.ip ?? "unknown";
1404
- if (checkAuthBlock(reqIp)) {
1415
+ const nowTs = Date.now();
1416
+ if (checkAuthBlock(authFailures, reqIp, nowTs)) {
1405
1417
  const rec = authFailures.get(reqIp);
1406
- const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1e3)) : 0;
1418
+ const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1e3)) : 0;
1407
1419
  send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));
1408
1420
  return;
1409
1421
  }
1410
- const apiKey = extractBearerKey(req);
1411
- if (!apiKey) {
1422
+ const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);
1423
+ if (auth.status !== "ok") {
1412
1424
  logRequest("DELETE", "auth_fail");
1413
- recordAuthFailure(reqIp);
1425
+ if (auth.status === "rejected") recordAuthFailure(authFailures, reqIp, nowTs);
1414
1426
  send401(req, res);
1415
1427
  return;
1416
1428
  }
1429
+ const apiKey = auth.apiKey;
1417
1430
  const sessionId = req.headers["mcp-session-id"];
1418
1431
  if (!sessionId) {
1419
1432
  res.status(400).send("Missing Mcp-Session-Id header");
package/dist/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/http.ts","../src/brand/logo-markup.ts","../src/lib/refresh-token.ts","../src/sessionCap.ts","../src/httpErrors.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp, DEFAULT_CLOUD_URL } from \"./client.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\nimport { appLogoMarkup, appLogoStyles } from \"./brand/logo-markup.js\";\nimport {\n signRefreshToken,\n verifyRefreshToken,\n} from \"./lib/refresh-token.js\";\nimport { planKeyAdmission, countKeySessions } from \"./sessionCap.js\";\nimport {\n send429,\n buildRateLimitError,\n buildAuthLockoutError,\n buildSessionCapError,\n} from \"./httpErrors.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — defaults to https://claude.ai (per REMOTE.md). Override via CORS_ORIGINS env var.\nconst ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? \"https://claude.ai\")\n .split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n // Retry-After is exposed so browser MCP clients (default origin https://claude.ai)\n // can read the structured-429 retry timing from JS, not just from the JSON body.\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id, Retry-After\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Rate Limiting (Fix 2) ─────────────────────────────────────────\n// Separate, stricter limiter for auth endpoints to prevent brute-force and\n// enumeration attacks on the OAuth flow.\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many auth requests. Try again later.\" },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n// Fix 4 — Cap client registrations at 500 to prevent unbounded memory growth.\nconst MAX_REGISTERED_CLIENTS = 500;\n\napp.post(\n \"/register\",\n authLimiter,\n express.json(),\n (req: any, res: any) => {\n // Fix 4 — Reject registration when cap is reached.\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {\n res.status(503).json({\n error: \"server_error\",\n error_description: \"Registration limit reached. Try again later.\",\n });\n return;\n }\n\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh tokens are now stateless HMAC-signed (see lib/refresh-token.ts).\n// No in-memory store, no cleanup loop, no per-key caps — they survive\n// Railway redeploys, which fixes TEN-1661.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\n\n// Fix 1 — Opaque access token store.\n// Maps pb_at_<uuid> → { apiKey, createdAt } so the raw pb_sk_* key is never\n// exposed through the OAuth flow. Capped at 1000 entries with LRU eviction.\ninterface AccessTokenEntry {\n apiKey: string;\n createdAt: number;\n}\nconst accessTokens = new Map<string, AccessTokenEntry>();\nconst MAX_ACCESS_TOKENS = 1000;\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n // Fix 1 — Evict expired opaque access tokens.\n for (const [token, entry] of accessTokens) {\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);\n }\n // Fix 5 — Clean up stale auth failure tracking entries.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&amp;\", '\"': \"&quot;\", \"'\": \"&#39;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Parchment-dark OAuth pages — brand tokens from DEC-419 (brand-tokens.json).\n// Monochrome-first per DEC-417, warm temperature per DEC-418.\n// Provider-agnostic: copy interpolates registered client_name (RFC 7591),\n// never hardcodes \"Claude\" — any MCP client may reach this page.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"parchment-dark\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${esc(title)} — Product Brain</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap\">\n${headExtra}\n<style>\n:root{\n --bg:#1a1917;--bg-warm:#201f1c;--surface:#262521;\n --fg1:#e4e0d8;--fg2:#c4bfb4;--fg3:#9a9589;--fg4:#6a6560;--fg-bright:#ffffff;\n --border:rgba(255,255,255,0.07);--border-light:rgba(255,255,255,0.04);\n --accent:#c9b99a;\n --btn-bg:#ffffff;--btn-fg:#1a1917;--btn-hover:#e4e0d8;\n --green:#4ade80;--rose:#ef4444;\n --ghost:rgba(38,37,33,0.55);\n --radius-md:7px;--radius-lg:10px;\n --font-display:\"Source Serif 4\",ui-serif,Georgia,serif;\n --font-body:\"IBM Plex Sans\",ui-sans-serif,system-ui,sans-serif;\n --font-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace;\n}\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%}\nbody{\n font-family:var(--font-body);font-size:13px;line-height:1.45;\n color:var(--fg1);background:var(--bg);\n min-height:100vh;display:grid;place-items:center;padding:24px;\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n position:relative;overflow:hidden;\n}\nbody::before{\n content:\"\";position:fixed;inset:0;\n background:radial-gradient(900px 600px at 50% 50%,rgba(228,224,216,0.025),transparent 60%);\n pointer-events:none;z-index:0;\n}\n.top-mark{position:fixed;top:22px;left:24px;z-index:5;opacity:0.7}\n${appLogoStyles}\n.stage{\n width:100%;max-width:460px;text-align:center;\n position:relative;z-index:1;\n display:grid;\n}\n.panel{\n grid-area:1/1;\n transition:opacity 280ms ease-out,transform 380ms cubic-bezier(.2,.6,.2,1),filter 380ms ease-out;\n}\n.panel[hidden]{\n display:block !important;\n opacity:0;transform:scale(0.96) translateY(-2px);filter:blur(6px);\n pointer-events:none;\n}\n.panel:not([hidden]){opacity:1;transform:scale(1);filter:none;pointer-events:auto}\n\n/* eyebrows */\n.eyebrow{\n font-family:var(--font-mono);font-size:10px;font-weight:700;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg4);\n margin-bottom:24px;display:inline-flex;align-items:center;gap:7px;\n}\n.eyebrow .dot{width:5px;height:5px;border-radius:50%;background:currentColor}\n.eyebrow.danger{color:var(--rose)}\n.eyebrow.success{color:var(--green)}\n.eyebrow.success .dot{box-shadow:0 0 0 3px rgba(74,222,128,0.18)}\n\n/* form input */\n.input-wrap{\n display:flex;align-items:center;background:rgba(0,0,0,0.22);\n border:1px solid var(--border);border-radius:var(--radius-md);\n transition:border-color 200ms ease-out,box-shadow 200ms ease-out;\n}\n.input-wrap:focus-within{\n border-color:rgba(228,224,216,0.45);\n box-shadow:0 0 0 3px rgba(228,224,216,0.10);\n}\n.input-wrap.has-error{\n border-color:rgba(239,68,68,0.55);\n box-shadow:0 0 0 3px rgba(239,68,68,0.12);\n animation:shake 360ms cubic-bezier(.36,.07,.19,.97);\n}\n@keyframes shake{\n 10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}\n 30%,50%,70%{transform:translateX(-4px)}40%,60%{transform:translateX(4px)}\n}\n.input{\n flex:1;min-width:0;background:transparent;border:0;outline:none;\n padding:16px;\n font-family:var(--font-mono);font-size:14px;color:var(--fg1);letter-spacing:0.02em;\n}\n.input::placeholder{color:var(--fg4)}\n.hint{\n margin-top:10px;font-family:var(--font-mono);font-size:10.5px;\n letter-spacing:0.16em;text-transform:uppercase;\n text-align:left;padding-left:4px;height:14px;color:var(--fg4);\n transition:color 160ms ease-out;\n}\n.hint.is-error{color:var(--rose)}\n\n/* primary button */\n.btn-primary{\n width:100%;height:48px;margin-top:14px;\n border:0;border-radius:var(--radius-md);\n background:var(--btn-bg);color:var(--btn-fg);\n font-family:var(--font-body);font-size:14.5px;font-weight:600;letter-spacing:-0.005em;\n cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:10px;\n transition:background 140ms ease-out,transform 80ms ease-out,opacity 200ms ease-out;\n position:relative;overflow:hidden;\n}\n.btn-primary:hover:not([disabled]){background:var(--btn-hover)}\n.btn-primary:active:not([disabled]){transform:translateY(1px)}\n.btn-primary[disabled]{opacity:0.45;cursor:default}\n.spin{\n width:14px;height:14px;border-radius:50%;\n border:1.5px solid currentColor;border-top-color:transparent;\n animation:spin 700ms linear infinite;opacity:0.85;\n}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n/* secondary link */\n.small-link{\n margin-top:18px;font-family:var(--font-mono);font-size:11px;\n letter-spacing:0.18em;text-transform:uppercase;color:var(--fg4);\n}\n.small-link a{\n color:var(--fg3);text-decoration:none;border-bottom:1px dotted currentColor;\n padding-bottom:1px;transition:color 140ms;\n}\n.small-link a:hover{color:var(--fg1)}\n\n/* orb */\n.orb-wrap{\n position:relative;width:160px;height:160px;margin:0 auto 36px;\n display:grid;place-items:center;\n}\n.orb-ring{position:absolute;border-radius:50%}\n.orb-ring.r1{inset:0;border:1px solid rgba(228,224,216,0.06);animation:drift1 40s linear infinite}\n.orb-ring.r2{inset:18px;border:1px dashed rgba(228,224,216,0.10);animation:drift2 28s linear infinite}\n.orb-ring.r3{inset:38px;border:1px solid rgba(74,222,128,0.20);animation:ringPulse 3.4s ease-in-out infinite}\n@keyframes drift1{to{transform:rotate(360deg)}}\n@keyframes drift2{to{transform:rotate(-360deg)}}\n@keyframes ringPulse{0%,100%{opacity:0.6}50%{opacity:1}}\n\n.orb-wrap.is-verifying .orb-ring.r3{\n border-color:rgba(228,224,216,0.20);\n animation:ringPulse 1.1s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-core{\n box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06);\n animation:corePulseNeutral 1.6s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-dot{background:var(--fg3);box-shadow:0 0 10px rgba(228,224,216,0.4)}\n@keyframes corePulseNeutral{\n 0%,100%{box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(228,224,216,0.07),0 0 32px rgba(228,224,216,0.18),inset 0 0 22px rgba(228,224,216,0.10)}\n}\n\n.orb-wrap.is-error .orb-ring.r3{border-color:rgba(239,68,68,0.30);animation:none;opacity:1}\n.orb-wrap.is-error .orb-ring.r1,.orb-wrap.is-error .orb-ring.r2{animation-play-state:paused}\n.orb-wrap.is-error .orb-core{\n box-shadow:0 0 0 6px rgba(239,68,68,0.05),0 0 22px rgba(239,68,68,0.20),inset 0 0 16px rgba(239,68,68,0.08);\n animation:none;\n}\n.orb-wrap.is-error .orb-dot{background:var(--rose);box-shadow:0 0 10px rgba(239,68,68,0.6)}\n\n.sat-orbit{position:absolute;inset:0;pointer-events:none}\n.sat-orbit.o1{animation:drift1 40s linear infinite}\n.sat-orbit.o2{animation:drift2 56s linear infinite}\n.sat{\n position:absolute;top:50%;left:50%;\n font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;\n background:var(--bg);padding:2px 6px;border-radius:3px;\n border:1px solid var(--border-light);\n transform-origin:0 0;opacity:0;\n}\n.panel:not([hidden])[data-state=\"connected\"] .sat{animation:satIn 600ms ease-out forwards}\n@keyframes satIn{from{opacity:0}to{opacity:1}}\n.sat span{display:inline-block;animation:counter 40s linear infinite}\n.sat-orbit.o2 .sat span{animation:counter2 56s linear infinite}\n@keyframes counter{to{transform:rotate(-360deg)}}\n@keyframes counter2{to{transform:rotate(360deg)}}\n\n.orb-core{\n position:relative;width:60px;height:60px;border-radius:50%;\n background:radial-gradient(circle at 50% 45%,#1a1a1a 0%,#0c0c0c 60%,#050505 100%);\n border:1px solid rgba(255,255,255,0.06);\n display:grid;place-items:center;\n box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 28px rgba(74,222,128,0.20),inset 0 0 18px rgba(74,222,128,0.08);\n animation:corePulse 3.4s ease-in-out infinite;\n}\n@keyframes corePulse{\n 0%,100%{box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 24px rgba(74,222,128,0.18),inset 0 0 16px rgba(74,222,128,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(74,222,128,0.06),0 0 40px rgba(74,222,128,0.32),inset 0 0 22px rgba(74,222,128,0.14)}\n}\n.orb-dot{width:10px;height:10px;border-radius:50%;background:#4ade80;box-shadow:0 0 12px rgba(74,222,128,0.7)}\n\n.core-shockwave{\n position:absolute;inset:0;border-radius:50%;\n border:1px solid rgba(74,222,128,0.6);\n opacity:0;pointer-events:none;\n}\n.panel:not([hidden])[data-state=\"connected\"] .core-shockwave{animation:shock 1100ms ease-out 200ms}\n@keyframes shock{\n 0%{opacity:0.7;transform:scale(0.4);border-width:2px}\n 100%{opacity:0;transform:scale(2.4);border-width:1px}\n}\n\n/* titles */\n.ok-title{\n font-family:var(--font-display);font-weight:600;\n font-size:38px;line-height:1.05;letter-spacing:-0.025em;\n color:var(--fg-bright);opacity:0;\n}\n.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}\n.ok-lead{\n margin:18px auto 0;max-width:22em;font-size:16px;line-height:1.55;color:var(--fg2);\n opacity:0;\n}\n.panel:not([hidden]) .ok-lead{animation:rise 600ms ease-out 500ms forwards}\n.ok-phrase-row{\n margin-top:14px;display:flex;align-items:center;justify-content:center;\n opacity:0;\n}\n.panel:not([hidden]) .ok-phrase-row{animation:rise 600ms ease-out 620ms forwards}\n.cmd.is-copied .cmd-quote-part{display:none}\n.success-cta-wrap{\n margin-top:48px;width:100%;opacity:0;\n}\n.panel:not([hidden]) .success-cta-wrap{animation:rise 600ms ease-out 780ms forwards}\na.btn-primary.success-cta{color:var(--btn-fg);text-decoration:none}\n\n.cmd{\n display:inline-flex;align-items:center;gap:8px;vertical-align:middle;\n font-family:var(--font-mono);font-size:15px;font-weight:500;color:var(--accent);\n background:rgba(201,185,154,0.08);border:1px solid rgba(201,185,154,0.30);\n padding:5px 10px 5px 12px;border-radius:6px;letter-spacing:0.02em;\n cursor:pointer;user-select:none;\n transition:background 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;\n}\n.cmd:hover{background:rgba(201,185,154,0.14);border-color:rgba(201,185,154,0.50);color:#dcc9a4}\n.cmd.is-copied{color:var(--green);border-color:rgba(74,222,128,0.35);background:rgba(74,222,128,0.08)}\n.cmd .cmd-icon{\n width:12px;height:12px;color:currentColor;opacity:0.75;\n display:inline-grid;place-items:center;\n transition:opacity 140ms;\n}\n.cmd:hover .cmd-icon{opacity:1}\n.cmd.is-copied .cmd-icon{color:var(--green);opacity:1}\n.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}\n\n/* error specifics */\n.err-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:12px;\n}\n.err-msg{\n font-size:14.5px;line-height:1.55;color:var(--fg3);\n margin-bottom:28px;\n}\n.err-msg code{\n font-family:var(--font-mono);font-size:12px;\n background:rgba(255,255,255,0.06);padding:1px 5px;border-radius:4px;color:var(--fg2);\n}\n.err-actions{display:flex;gap:8px}\n.btn-secondary{\n flex:1;height:44px;border-radius:var(--radius-md);\n background:transparent;color:var(--fg2);\n border:1px solid var(--border);\n font-family:var(--font-body);font-size:13.5px;font-weight:500;\n cursor:pointer;text-decoration:none;\n display:inline-flex;align-items:center;justify-content:center;\n transition:background 140ms,color 140ms,border-color 140ms;\n}\n.btn-secondary:hover{background:rgba(255,255,255,0.04);color:var(--fg1);border-color:rgba(255,255,255,0.14)}\n\n/* verifying */\n.verifying-eyebrow{\n font-family:var(--font-mono);font-size:10px;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg3);\n font-weight:700;margin-bottom:14px;\n}\n.verifying-title{\n font-family:var(--font-display);font-weight:600;\n font-size:28px;line-height:1.1;color:var(--fg1);margin-bottom:8px;\n letter-spacing:-0.02em;\n}\n.verifying-sub{font-size:13px;color:var(--fg3);min-height:1.45em}\n\n/* form heading */\n.form-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:10px;\n}\n.form-sub{font-size:13.5px;color:var(--fg3);margin-bottom:28px;line-height:1.55}\n\n@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n\n@media (prefers-reduced-motion: reduce){\n *,*::before,*::after{\n animation-duration:0.01ms !important;animation-iteration-count:1 !important;\n transition-duration:0.01ms !important;\n }\n}\n</style>\n</head><body>\n<div class=\"top-mark\">${appLogoMarkup({ size: \"sm\" })}</div>\n<div class=\"stage\">${bodyContent}</div>\n</body></html>`;\n}\n\n// Provider-agnostic display name. Trims and clamps to keep page chrome stable.\nfunction providerDisplayName(clientName: string | undefined): string {\n const name = (clientName ?? \"\").trim();\n if (!name) return \"your assistant\";\n return name.length > 40 ? name.slice(0, 40) + \"…\" : name;\n}\n\n// Inner HTML for the success panel. Used both by the JSON path (cloned client-side\n// from a <template>) and by the no-JS server-rendered fallback page.\nfunction successPanelInner(workspaceName: string, redirectUrl: string, providerName: string): string {\n return `\n<div class=\"orb-wrap\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"sat-orbit o1\">\n <span class=\"sat\" style=\"transform:rotate(20deg) translate(78px) rotate(-20deg);color:#4ade80;animation-delay:600ms\"><span>DEC</span></span>\n <span class=\"sat\" style=\"transform:rotate(140deg) translate(78px) rotate(-140deg);color:#c9b99a;animation-delay:720ms\"><span>WP</span></span>\n <span class=\"sat\" style=\"transform:rotate(260deg) translate(78px) rotate(-260deg);color:#f59e0b;animation-delay:840ms\"><span>TEN</span></span>\n </div>\n <div class=\"sat-orbit o2\">\n <span class=\"sat\" style=\"transform:rotate(80deg) translate(54px) rotate(-80deg);color:#60a5fa;animation-delay:960ms\"><span>STD</span></span>\n <span class=\"sat\" style=\"transform:rotate(220deg) translate(54px) rotate(-220deg);color:#a78bfa;animation-delay:1080ms\"><span>INS</span></span>\n </div>\n <div class=\"core-shockwave\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow success\"><span class=\"dot\"></span>Connected</div>\n<h1 class=\"ok-title\">Product Brain is Live</h1>\n<p class=\"ok-lead\">Return to your assistant, then say</p>\n<div class=\"ok-phrase-row\">\n <button class=\"cmd\" type=\"button\" data-cmd-pill data-redirect=\"${esc(redirectUrl)}\" aria-label=\"Copy &quot;Start PB&quot; and return to ${esc(providerName)}\">\n <span class=\"cmd-quote-part\" aria-hidden=\"true\">&ldquo;</span><span data-cmd-text>Start PB</span><span class=\"cmd-quote-part\" aria-hidden=\"true\">&rdquo;</span>\n <span class=\"cmd-icon\" aria-hidden=\"true\">\n <svg data-cmd-svg viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V6a2 2 0 0 1 2-2h9\"/></svg>\n </span>\n </button>\n</div>\n<div class=\"success-cta-wrap\">\n <a class=\"btn-primary success-cta\" href=\"${esc(redirectUrl)}\">Continue in ${esc(providerName)}</a>\n</div>\n<!-- workspace name retained as data hook for tests, hidden from view -->\n<span hidden data-field=\"ws-name\">${esc(workspaceName)}</span>`;\n}\n\nfunction errorPanelInner(title: string, trustedDetailHtml: string, retryUrl: string): string {\n return `\n<div class=\"orb-wrap is-error\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow danger\"><span class=\"dot\"></span>Couldn't connect</div>\n<h2 class=\"err-title\" data-field=\"err-title\">${esc(title)}</h2>\n<p class=\"err-msg\" data-field=\"err-msg\">${trustedDetailHtml}</p>\n<div class=\"err-actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-secondary\" data-retry-link>&larr; Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key &rarr;</a>\n</div>`;\n}\n\nconst cmdScript = `\n(function(){\n function bindCmd(pill){\n if(!pill||pill.__bound)return;pill.__bound=true;\n var textEl=pill.querySelector('[data-cmd-text]');\n var svgEl=pill.querySelector('[data-cmd-svg]');\n var redirectUrl=pill.getAttribute('data-redirect')||'';\n pill.addEventListener('click',function(){\n var done=function(){\n pill.classList.add('is-copied');\n if(textEl)textEl.textContent='Copied';\n if(svgEl)svgEl.innerHTML='<polyline points=\"4 12 10 18 20 6\"/>';\n setTimeout(function(){if(redirectUrl)window.location.assign(redirectUrl)},900);\n };\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){\n navigator.clipboard.writeText('Start PB').then(done,done);\n }else{done()}\n }catch(e){done()}\n });\n }\n document.querySelectorAll('[data-cmd-pill]').forEach(bindCmd);\n})();\n`;\n\nfunction authorizeFormPage(params: {\n redirect_uri: string;\n code_challenge: string;\n code_challenge_method: string;\n state: string;\n client_id: string;\n client_name?: string;\n}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const providerName = providerDisplayName(params.client_name);\n const body = `\n<!-- ─── CONNECT ─── -->\n<div class=\"panel\" id=\"p-connect\" data-state=\"connect\">\n <div class=\"eyebrow\">Connect Product Brain</div>\n <h1 class=\"form-title\">Paste your API key</h1>\n <p class=\"form-sub\">Give <span data-provider>${esc(providerName)}</span> access to your workspace memory.</p>\n <form method=\"POST\" action=\"/authorize\" id=\"f\" autocomplete=\"off\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\n <div class=\"input-wrap\" id=\"iw\">\n <input type=\"password\" id=\"k\" name=\"api_key\" class=\"input input-full\" placeholder=\"pb_sk_…\" required autofocus spellcheck=\"false\">\n </div>\n <div class=\"hint\" id=\"hint\" hidden></div>\n <button type=\"submit\" class=\"btn-primary\" id=\"sb\" disabled><span id=\"bt\">Connect</span></button>\n </form>\n <div class=\"small-link\"><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">No key? Generate one &rarr;</a></div>\n</div>\n\n<!-- ─── VERIFYING ─── -->\n<div class=\"panel\" id=\"p-verifying\" data-state=\"verifying\" hidden>\n <div class=\"orb-wrap is-verifying\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n </div>\n <div class=\"verifying-eyebrow\">Handshake</div>\n <h2 class=\"verifying-title\">Verifying key…</h2>\n <p class=\"verifying-sub\" id=\"verify-sub\">Checking workspace · …</p>\n</div>\n\n<!-- ─── CONNECTED (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-connected\" data-state=\"connected\" hidden></div>\n\n<!-- ─── ERROR (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-error\" data-state=\"error\" hidden></div>\n\n<template id=\"tpl-connected\">${successPanelInner(\"__WS__\", \"__URL__\", \"__PROVIDER__\")}</template>\n<template id=\"tpl-error\">${errorPanelInner(\"__TITLE__\", \"__DETAIL__\", \"__RETRY__\")}</template>\n\n<script>\n${cmdScript}\n(function(){\n var f=document.getElementById('f'),k=document.getElementById('k'),iw=document.getElementById('iw'),hint=document.getElementById('hint'),sb=document.getElementById('sb'),bt=document.getElementById('bt');\n var pConnect=document.getElementById('p-connect'),pVerify=document.getElementById('p-verifying'),pOk=document.getElementById('p-connected'),pErr=document.getElementById('p-error');\n var verifySub=document.getElementById('verify-sub');\n\n function show(panel){\n [pConnect,pVerify,pOk,pErr].forEach(function(p){\n if(p===panel){p.removeAttribute('hidden')}else{p.setAttribute('hidden','')}\n });\n }\n\n function syncInput(){\n sb.disabled=!k.value.trim();\n iw.classList.remove('has-error');\n hint.classList.remove('is-error');\n hint.textContent='';\n hint.setAttribute('hidden','');\n }\n k.addEventListener('input',syncInput);\n k.addEventListener('keydown',function(e){\n if(e.key==='Escape'){k.value='';syncInput()}\n });\n\n function showError(title,detailHtml){\n var tpl=document.getElementById('tpl-error');\n var html=tpl.innerHTML\n .replace('__TITLE__',title.replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]}))\n .replace('__DETAIL__',detailHtml)\n .replace('__RETRY__','#');\n pErr.innerHTML=html;\n var retry=pErr.querySelector('[data-retry-link]');\n if(retry){retry.addEventListener('click',function(e){e.preventDefault();show(pConnect);k.focus();k.select()})}\n show(pErr);\n }\n\n function showSuccess(workspaceName,redirectUrl,providerName){\n var tpl=document.getElementById('tpl-connected');\n var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]});\n var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'&quot;').replace(/[<>]/g,function(c){return{'<':'&lt;','>':'&gt;'}[c]});\n var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);\n pOk.innerHTML=html;\n pOk.querySelectorAll('[data-cmd-pill]').forEach(function(pill){\n pill.__bound=false;\n });\n // Re-run binder\n var s=document.createElement('script');s.textContent=${JSON.stringify(cmdScript)};document.body.appendChild(s);s.remove();\n show(pOk);\n }\n\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var v=k.value.trim();\n if(!v){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Paste your key first';hint.removeAttribute('hidden');return}\n if(v.indexOf('pb_sk_')!==0){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Key must start with pb_sk_';hint.removeAttribute('hidden');return}\n sb.disabled=true;bt.textContent='Verifying';\n show(pVerify);\n\n var steps=['Checking workspace · …','Loading chain · …','Establishing memory · …'];\n var i=0;verifySub.textContent=steps[0];\n var ti=setInterval(function(){i++;if(i>=steps.length){clearInterval(ti);return}verifySub.textContent=steps[i]},900);\n\n var minDelay=new Promise(function(r){setTimeout(r,2800)});\n var fd=new FormData(f);\n var body=new URLSearchParams();\n fd.forEach(function(val,key){body.append(key,String(val))});\n\n var req=fetch('/authorize',{\n method:'POST',\n headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'},\n body:body.toString(),\n credentials:'same-origin'\n }).then(function(r){return r.json().then(function(j){return{status:r.status,body:j}})});\n\n Promise.all([req,minDelay]).then(function(arr){\n clearInterval(ti);\n var res=arr[0];\n if(res.body&&res.body.ok){\n showSuccess(res.body.workspaceName,res.body.redirectUrl,res.body.providerName);\n }else{\n showError(res.body&&res.body.title||'Couldn\\\\'t connect',res.body&&res.body.detail||'Try again, or generate a new key.');\n sb.disabled=false;bt.textContent='Connect';\n }\n }).catch(function(){\n clearInterval(ti);\n showError('Network error','We couldn\\\\'t reach Product Brain. Check your connection and try again.');\n sb.disabled=false;bt.textContent='Connect';\n });\n });\n\n k.focus();\n})();\n</script>`;\n return authPageShell(\"Connect Product Brain\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string, providerName: string): string {\n const body = `\n<div class=\"panel\" data-state=\"connected\">\n${successPanelInner(workspaceName, redirectUrl, providerName)}\n</div>\n<script>${cmdScript}</script>`;\n return authPageShell(\"Connected\", body);\n}\n\n// security: trustedDetailHtml must be a hardcoded literal — never pass user-controlled data.\nfunction authorizeErrorPage(title: string, trustedDetailHtml: string, retryUrl: string): string {\n const body = `\n<div class=\"panel\" data-state=\"error\">\n${errorPanelInner(title, trustedDetailHtml, retryUrl)}\n</div>`;\n return authPageShell(\"Connection error\", body);\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;\n const cid = String(client_id ?? \"\");\n const clientName = cid && registeredClients.has(cid)\n ? registeredClients.get(cid)!.client_name\n : undefined;\n res.type(\"html\").send(authorizeFormPage({\n redirect_uri: String(redirect_uri ?? \"\"),\n code_challenge: String(code_challenge ?? \"\"),\n code_challenge_method: String(code_challenge_method ?? \"S256\"),\n state: String(state ?? \"\"),\n client_id: cid,\n client_name: clientName,\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n\n // Negotiate response shape: fetch path sets Accept: application/json,\n // form-post no-JS path gets the standard rebranded HTML page.\n const wantsJson = String(req.headers[\"accept\"] ?? \"\").includes(\"application/json\");\n\n // Build \"retry\" URL so error pages can link back to the form.\n const retryParams = new URLSearchParams({\n redirect_uri: redirect_uri ?? \"\",\n code_challenge: code_challenge ?? \"\",\n code_challenge_method: code_challenge_method ?? \"S256\",\n ...(state ? { state } : {}),\n ...(client_id ? { client_id } : {}),\n }).toString();\n const retryUrl = `/authorize?${retryParams}`;\n\n function sendError(title: string, trustedDetailHtml: string, status = 400): void {\n if (wantsJson) {\n res.status(status).json({ ok: false, title, detail: trustedDetailHtml });\n } else {\n res.status(status).type(\"html\").send(authorizeErrorPage(title, trustedDetailHtml, retryUrl));\n }\n }\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n sendError(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n );\n return;\n }\n\n // Validate redirect_uri against the registered client's allowed redirects.\n // Open redirect prevention: never trust a request-supplied redirect_uri without\n // checking it was pre-registered during dynamic client registration (RFC 7591).\n //\n // When client_id is provided but unknown (e.g. server restart wiped the in-memory Map\n // after claude.ai cached its client_id from a previous session), auto-re-register using\n // the supplied redirect_uri rather than hard-rejecting. The API key validation below is\n // the primary security gate; basic HTTPS validation guards against degenerate redirect URIs.\n if (!client_id) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"client_id is required\",\n });\n return;\n }\n if (!registeredClients.has(client_id)) {\n if (typeof redirect_uri === \"string\" && redirect_uri.startsWith(\"https://\")) {\n registeredClients.set(client_id, {\n client_id,\n redirect_uris: [redirect_uri],\n registeredAt: Date.now(),\n });\n process.stderr.write(`[authorize] auto-re-registered stale client_id after restart\\n`);\n } else {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown client_id and redirect_uri is not a valid https URL\",\n });\n return;\n }\n }\n\n const client = registeredClients.get(client_id)!;\n if (!client.redirect_uris.includes(redirect_uri)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"redirect_uri does not match any registered redirect for this client\",\n });\n return;\n }\n\n // Validate key against Convex before issuing the code.\n // DEC-789 S2: probe primary then fallback URLs so DEV keys work against PROD Railway MCP.\n let workspaceName = \"Your Workspace\";\n try {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? \"\")\n .split(\",\").map((u) => u.trim().replace(/\\/$/, \"\")).filter(Boolean);\n const candidates = [primaryUrl, ...fallbackUrls];\n\n let foundUrl: string | undefined;\n let anyDefinitiveReject = false;\n\n for (const url of candidates) {\n let checkData: { ok: boolean; workspaceName?: string; deploymentUrl?: string } | null = null;\n try {\n const checkRes = await fetch(`${url}/api/key-check`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${api_key}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n checkData = await checkRes.json() as { ok: boolean; workspaceName?: string; deploymentUrl?: string };\n } catch {\n // This candidate unreachable — try next.\n continue;\n }\n if (checkData.ok) {\n if (checkData.workspaceName) workspaceName = checkData.workspaceName;\n foundUrl = checkData.deploymentUrl ?? url;\n break;\n }\n anyDefinitiveReject = true;\n }\n\n if (!foundUrl) {\n if (anyDefinitiveReject) {\n sendError(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.\",\n 401,\n );\n return;\n }\n // All candidates unreachable (network errors only) — fail-open.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n } else {\n getKeyState(api_key).deploymentUrl = foundUrl;\n }\n } catch {\n // Outer guard: fail-open so auth works even if the entire block throws.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n const redirectUrl = url.toString();\n const providerName = providerDisplayName(client.client_name);\n\n if (wantsJson) {\n res.json({ ok: true, workspaceName, redirectUrl, providerName });\n } else {\n // No-JS path: server-rendered success page. User clicks the pill or the\n // Primary CTA + copy pill both complete the OAuth redirect — no auto-bounce.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl, providerName));\n }\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n // Return the pb_sk_* key directly as the access_token so connections\n // survive server restarts. Railway deploys on every git push, wiping\n // in-memory Maps. pb_sk_* keys are long-lived (valid until explicitly\n // revoked in Cortex); actual validity is enforced by Convex per tool call.\n // extractBearerKey() already handles pb_sk_* with zero Map lookup.\n //\n // The refresh token is HMAC-signed and self-contained (TEN-1661, lib/\n // refresh-token.ts) so it also survives redeploys. No server-side store.\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1-year TTL: actual validity enforced by Convex, not by expiry clock.\n // Long TTL prevents unnecessary refresh cycles after restarts.\n expires_in: 365 * 24 * 3600,\n refresh_token: signRefreshToken(apiKey),\n };\n}\n\napp.post(\n \"/oauth/token\",\n authLimiter,\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const verified = verifyRefreshToken(refresh_token);\n if (!verified) {\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"Invalid or expired refresh token\",\n });\n return;\n }\n // Rotation = re-issuance. With stateless tokens there is no Map entry\n // to revoke; the new pair simply takes over from the next request.\n res.json(issueTokens(verified.apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n handler: (req: any, res: any) => {\n const r = req.rateLimit;\n const reset = r?.resetTime?.getTime?.() ?? Date.now() + 60_000;\n send429(res, buildRateLimitError({\n retryAfterSeconds: Math.max(1, Math.ceil((reset - Date.now()) / 1000)),\n limit: r?.limit ?? 120,\n current: r?.used ?? 0,\n id: req.body?.id ?? null, // mirror the JSON-RPC id when a POST body is present\n }));\n },\n});\n\n// ── Auth Failure Backoff (Fix 5) ──────────────────────────────────────────\n// Per-IP progressive lockout for failed API key auth attempts to prevent\n// brute-force attacks through the MCP endpoints.\n\ninterface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\nconst authFailures = new Map<string, AuthFailureRecord>();\nconst AUTH_FAILURE_MAX = 10;\nconst AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nconst AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nconst MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\nfunction checkAuthBlock(ip: string): boolean {\n const rec = authFailures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > Date.now();\n}\n\nfunction recordAuthFailure(ip: string): void {\n const now = Date.now();\n const rec = authFailures.get(ip);\n\n if (!rec) {\n authFailures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n // Number of POST tool-call requests currently being handled. Incremented at\n // POST start, decremented in finally. A session with inFlight > 0 is never\n // torn by planKeyAdmission; the only exception is the evictStaleSessions TTL\n // backstop, which force-evicts past SESSION_TTL_MS to self-heal a leaked\n // counter (logged as reason=inflight_leak). GET SSE and DELETE do NOT touch\n // this — see design §3.1.\n inFlight: number;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Grace window: a session touched within this many ms is \"recently active\" and\n// is never evicted by the per-key planner (covers the gap between back-to-back\n// POST calls). See planKeyAdmission in sessionCap.ts.\nconst EVICT_GRACE_MS = 60_000;\n// Fix 6 — MAX_SESSIONS_PER_KEY is an in-process memory/working-set guard, NOT a\n// concurrency policy. Raising MCP_MAX_SESSIONS_PER_KEY does NOT grant more\n// concurrent agents — it only widens the idle-session reclaim window. It must\n// never be the thing that limits how many agents can work; fleet concurrency is\n// a kernel concern (TEN-2229), not a per-key surface cap.\n// Clamped to [1, MAX_SESSIONS - 1]: a value < 1 would make planKeyAdmission\n// throw on every init (RangeError); the MAX_SESSIONS - 1 ceiling guarantees a\n// single key can never alone fill the entire global pool (always leaves >= 1\n// slot), so a bad env degrades to a safe bound rather than breaking init or\n// disabling the cross-key fairness guard. Default 25 is far below this ceiling.\nconst MAX_SESSIONS_PER_KEY = Math.max(\n 1,\n Math.min(MAX_SESSIONS - 1, Number(process.env.MCP_MAX_SESSIONS_PER_KEY) || 25),\n);\n\n// The other eviction site is planKeyAdmission (sessionCap.ts), which runs\n// per-key on init. Keep both coherent — a change to one may affect the other.\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n // TTL branch: evict regardless of inFlight once past SESSION_TTL_MS. GET is\n // no longer inFlight-guarded after the round-3 fix, so an unconditional skip\n // on inFlight > 0 could make a session with a *leaked* counter immortal. The\n // hard-age backstop turns that leak into a self-healing, observable event.\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n // Force-evicting an inFlight > 0 session here means the counter leaked\n // (an unexpected throw outside the POST try/finally). Log it as such.\n logSessionLifecycle(\n \"session_deleted\",\n id,\n entry.inFlight > 0 ? \"inflight_leak\" : \"ttl\",\n );\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n // Global memory-pressure branch: SKIP inFlight > 0 so we can't drop a live\n // POST call mid-flight.\n const sorted = [...sessions.entries()]\n .filter(([, e]) => e.inFlight === 0)\n .sort((a, b) => a[1].lastAccess - b[1].lastAccess);\n const overflow = sessions.size - MAX_SESSIONS;\n for (let i = 0; i < Math.min(overflow, sorted.length); i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n // Observability: if every session is inFlight (sorted is empty / too short)\n // the loop above could not bring us under MAX_SESSIONS. We deliberately do\n // NOT tear live POSTs — the TTL backstop reclaims them at SESSION_TTL_MS —\n // but a sustained breach is worth surfacing rather than silently exceeding\n // the global memory ceiling.\n if (sessions.size > MAX_SESSIONS) {\n const ts = new Date().toISOString();\n process.stderr.write(\n `[HTTP] ${ts} session_cap_breach size=${sessions.size} max=${MAX_SESSIONS} ` +\n `reason=all_inflight (no idle sessions to evict; TTL backstop will reclaim)\\n`,\n );\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n\nfunction extractBearerKey(req: any): string | null {\n const header = req.headers?.authorization;\n if (typeof header !== \"string\" || !header.startsWith(\"Bearer \")) return null;\n const token = header.slice(7).trim();\n\n // Fix 1 — Support both direct API keys (stdio/backward compat) and opaque\n // OAuth access tokens issued by issueTokens().\n if (token.startsWith(\"pb_sk_\")) {\n // Direct API key — accepted for stdio and backward compatibility.\n return token;\n }\n if (token.startsWith(\"pb_at_\")) {\n // Opaque OAuth access token — resolve to the underlying API key.\n const entry = accessTokens.get(token);\n if (!entry) return null;\n const now = Date.now();\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) {\n // Expired — remove and reject.\n accessTokens.delete(token);\n return null;\n }\n return entry.apiKey;\n }\n return null;\n}\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\" | \"lru_evict\" | \"inflight_leak\" | \"init_rejected\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"POST\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n // inFlight guard (design §3.3): the increment is the FIRST statement\n // inside the try so nothing throwable sits between it and the guarded\n // region; finally decrements and re-stamps lastAccess so a long tool\n // call leaves a fresh stamp (never looks idle mid-flight).\n try {\n entry.inFlight++;\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } finally {\n entry.inFlight--;\n entry.lastAccess = Date.now();\n }\n } else if (!sessionId && isInitializeRequest(req.body)) {\n // Fix 6 — per-key session cap with recency-guarded LRU eviction.\n const keyH = hashKey(apiKey);\n const plan = planKeyAdmission(sessions, keyH, MAX_SESSIONS_PER_KEY, Date.now(),\n { ttlMs: SESSION_TTL_MS, graceMs: EVICT_GRACE_MS });\n for (const sid of plan.evict) {\n const victim = sessions.get(sid);\n sessions.delete(sid); // delete before close; onclose guards on membership\n victim?.transport.close().catch(() => {});\n logSessionLifecycle(\"session_deleted\", sid, \"lru_evict\");\n }\n if (!plan.admit) {\n // poll-vs-deadline is decided by the planner from the same snapshot it\n // evicted against (plan.retryIsPoll) — no second scan / clock read here.\n send429(res, buildSessionCapError({\n retryAfterSeconds: plan.retryAfterSeconds,\n limit: MAX_SESSIONS_PER_KEY,\n current: countKeySessions(sessions, keyH),\n poll: plan.retryIsPoll,\n id: req.body?.id ?? null,\n }));\n return;\n }\n const sid = randomUUID();\n let initialized = false; // set true once the SDK confirms the session initialized\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => sid,\n onsessioninitialized: (s: string) => { initialized = true; logSessionLifecycle(\"session_created\", s); }, // log only — no map write\n });\n const reserved: SessionEntry = { transport, lastAccess: Date.now(), keyHash: keyH, inFlight: 1 };\n sessions.set(sid, reserved); // synchronous single-writer reservation (closes TOCTOU)\n transport.onclose = () => {\n const s = transport.sessionId ?? sid;\n if (sessions.has(s)) { sessions.delete(s); logSessionLifecycle(\"session_deleted\", s, \"onclose\"); }\n };\n try {\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } catch (e) {\n transport.onclose = undefined as any; // detach → no phantom delete log on failure\n sessions.delete(sid);\n throw e;\n } finally {\n // handleRequest can reject an init-shaped POST with a 4xx WITHOUT throwing\n // (e.g. missing \"Accept: application/json, text/event-stream\", bad content\n // type, invalid batch init) — onsessioninitialized never fires and no\n // session id reaches the client, so the reservation would otherwise linger\n // until grace/TTL and let a broken/malicious client pin per-key slots.\n // Drop the orphan reservation immediately when init did not complete.\n if (!initialized) {\n transport.onclose = undefined as any;\n if (sessions.delete(sid)) logSessionLifecycle(\"session_deleted\", sid, \"init_rejected\");\n } else {\n const e = sessions.get(sid); if (e) { e.inFlight--; e.lastAccess = Date.now(); }\n }\n }\n } else if (sessionId) {\n // Stale Mcp-Session-Id (client cached one from before a Railway restart\n // or after TTL eviction). Per MCP Streamable HTTP spec § Session\n // Management clause 3, the server MUST respond with HTTP 404 Not Found\n // so the client (clause 4) re-initialises with a fresh InitializeRequest\n // without prompting OAuth re-auth. Returning 400 here was the root cause\n // of \"Claude.ai re-auths after every deploy\".\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale sessionId=${sessionId} (likely server restart — instructing client to re-initialise)\\n`,\n );\n res.status(404).json({\n jsonrpc: \"2.0\",\n error: { code: -32001, message: \"Session not found — re-initialise\" },\n id: null,\n });\n } else {\n // Non-initialise request with no Mcp-Session-Id — per spec clause 2,\n // servers that require a session ID SHOULD respond with HTTP 400.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"GET\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale GET sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n // NOT inFlight-guarded (design §3.1, round-3 review): the SSE\n // handleRequest stays open for the stream's whole life, so guarding it\n // would pin every active session → permanent 429 lockout. Re-stamp\n // lastAccess on stream open; the recency guard alone governs evictability.\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n if (checkAuthBlock(reqIp)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - Date.now()) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n const apiKey = extractBearerKey(req);\n if (!apiKey) {\n logRequest(\"DELETE\", \"auth_fail\");\n // Fix 5 — Record the auth failure for progressive lockout.\n recordAuthFailure(reqIp);\n send401(req, res);\n return;\n }\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale DELETE sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n","// SSOT for the Product Brain APP logo (wordmark style).\n// Used by: src/lib/brand/AppLogo.svelte (Cortex UI) AND\n// packages/mcp-server (mirrored via prebuild) for /authorize and other agent surfaces.\n//\n// NOT used by marketing pages (they have their own brand treatment).\n//\n// To change the logo, edit ONLY this file. Both surfaces re-render automatically.\n\nexport type AppLogoSize = \"sm\" | \"md\";\n\nexport interface AppLogoOptions {\n size?: AppLogoSize;\n showWordmark?: boolean;\n className?: string;\n}\n\nconst SIZE_CLASSES: Record<AppLogoSize, string> = {\n sm: \"pb-logo--sm\",\n md: \"pb-logo--md\",\n};\n\n// Self-contained CSS for environments that can't load Svelte styles\n// (the MCP authorize page, embedded HTML responses, etc.).\n// Cortex UI consumers can rely on AppLogo.svelte's scoped styles instead;\n// the inline class hooks are designed not to collide.\nexport const appLogoStyles = `\n.pb-logo{display:inline-flex;align-items:center;gap:8px;color:inherit}\n.pb-logo__mark{\n border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);\n display:inline-grid;place-items:center;flex-shrink:0;\n}\n.pb-logo__core{border-radius:50%;background:var(--accent,#c9b99a)}\n.pb-logo__name{\n font-family:var(--font-mono,\"IBM Plex Mono\",ui-monospace,monospace);\n font-weight:500;text-transform:uppercase;\n letter-spacing:0.22em;color:var(--fg4,#6a6560);\n}\n.pb-logo--sm .pb-logo__mark{width:16px;height:16px}\n.pb-logo--sm .pb-logo__core{width:5px;height:5px}\n.pb-logo--sm .pb-logo__name{font-size:10.5px}\n.pb-logo--md .pb-logo__mark{width:24px;height:24px;border-radius:6px}\n.pb-logo--md .pb-logo__core{width:8px;height:8px}\n.pb-logo--md .pb-logo__name{font-size:13px}\n`;\n\nexport function appLogoMarkup(opts: AppLogoOptions = {}): string {\n const size = opts.size ?? \"sm\";\n const showWordmark = opts.showWordmark ?? true;\n const cls = [\"pb-logo\", SIZE_CLASSES[size], opts.className].filter(Boolean).join(\" \");\n const wordmark = showWordmark ? `<span class=\"pb-logo__name\">Product Brain</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\n}\n","/**\n * Stateless HMAC-signed OAuth refresh tokens.\n *\n * Why this exists (TEN-1661, DEC-783):\n * Railway redeploys on every git push, wiping in-memory Maps. The previous\n * `refreshTokens` Map made claude.ai's proactive refresh calls fail with\n * `invalid_grant`, forcing OAuth re-auth after every deploy. DEC-783 fixed\n * the access-token side by returning the underlying pb_sk_* key directly.\n * This module is the refresh-token equivalent: a self-contained,\n * HMAC-signed token format that needs no server-side state.\n *\n * Format: pb_rt_<base64url(payload)>.<base64url(hmac-sha256(payload))>\n * Payload: { k: apiKey, i: iat-ms, j: jti }\n *\n * The jti (j) field is reserved for future revocation/rotation work and is\n * not yet checked. Signature comparison uses crypto.timingSafeEqual.\n */\n\nimport { createHmac, randomBytes, randomUUID, timingSafeEqual } from \"node:crypto\";\n\nexport const REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\nconst PREFIX = \"pb_rt_\";\n\ninterface RefreshPayload {\n k: string; // apiKey\n i: number; // iat (ms since epoch)\n j: string; // jti\n}\n\n// Read once at module load. In production we warn rather than crash so the\n// server keeps working — degrades gracefully to current Railway-lifetime\n// behaviour if the secret is missing.\nconst secret: Buffer = (() => {\n const fromEnv = process.env.MCP_REFRESH_SECRET;\n if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, \"utf8\");\n if (process.env.NODE_ENV === \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[HTTP] WARNING MCP_REFRESH_SECRET not set — refresh tokens will not survive restart\",\n );\n }\n return randomBytes(32);\n})();\n\nfunction sign(payloadB64: string): Buffer {\n return createHmac(\"sha256\", secret).update(payloadB64).digest();\n}\n\nexport function signRefreshToken(apiKey: string): string {\n const payload: RefreshPayload = {\n k: apiKey,\n i: Date.now(),\n j: randomUUID(),\n };\n const payloadB64 = Buffer.from(JSON.stringify(payload), \"utf8\").toString(\"base64url\");\n const sigB64 = sign(payloadB64).toString(\"base64url\");\n return `${PREFIX}${payloadB64}.${sigB64}`;\n}\n\nexport function verifyRefreshToken(token: string): { apiKey: string } | null {\n if (typeof token !== \"string\" || !token.startsWith(PREFIX)) return null;\n const body = token.slice(PREFIX.length);\n const dot = body.indexOf(\".\");\n if (dot <= 0 || dot === body.length - 1) return null;\n const payloadB64 = body.slice(0, dot);\n const sigB64 = body.slice(dot + 1);\n\n let providedSig: Buffer;\n try {\n providedSig = Buffer.from(sigB64, \"base64url\");\n } catch {\n return null;\n }\n const expectedSig = sign(payloadB64);\n if (providedSig.length !== expectedSig.length) return null;\n if (!timingSafeEqual(providedSig, expectedSig)) return null;\n\n let payload: RefreshPayload;\n try {\n const json = Buffer.from(payloadB64, \"base64url\").toString(\"utf8\");\n payload = JSON.parse(json);\n } catch {\n return null;\n }\n if (\n !payload ||\n typeof payload.k !== \"string\" ||\n typeof payload.i !== \"number\" ||\n typeof payload.j !== \"string\"\n ) {\n return null;\n }\n if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;\n\n return { apiKey: payload.k };\n}\n","/**\n * sessionCap.ts — Pure, side-effect-free session cap planner.\n *\n * Used by http.ts init branch to decide which sessions to evict before\n * admitting a new one for the presenting API key.\n *\n * Two eviction sites in the codebase:\n * 1. planKeyAdmission (here) — per-key, on init, targets only the presenting key.\n * 2. evictStaleSessions (http.ts) — global TTL sweep, runs every 60 s.\n * Keep both coherent; a change to one may affect the other.\n */\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface CapSession {\n /** Unix-ms timestamp of last request start (or completion). */\n lastAccess: number;\n /** Hash of the API key that owns this session. */\n keyHash: string;\n /**\n * Number of POST tool-call requests currently being handled.\n * Incremented at POST start; decremented in finally.\n * GET SSE handlers and DELETE are NOT counted here — see design §3.1.\n */\n inFlight: number;\n}\n\nexport interface AdmissionPlan {\n /** IDs of sessions the caller should evict before admitting the new one. */\n evict: string[];\n /** True if the new session can be admitted after evictions. */\n admit: boolean;\n /**\n * Seconds the caller should suggest as a retry delay when admit=false.\n * 0 when admit=true.\n */\n retryAfterSeconds: number;\n /**\n * True when admit=false AND every blocking same-key session has inFlight > 0\n * — there is no grace deadline to wait out, so retryAfterSeconds is a fixed\n * poll-hint rather than a guarantee. The caller surfaces this as a \"retry\n * shortly\" message instead of a hard deadline. Always false when admit=true.\n * This is the single source of truth for poll-vs-deadline — callers must not\n * re-derive it from the live session map (avoids a second clock read).\n */\n retryIsPoll: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// planKeyAdmission\n// ---------------------------------------------------------------------------\n\n/**\n * Plan how the presenting key admits ONE new session under `cap`.\n *\n * Protected = inFlight > 0 OR (now - lastAccess) < graceMs. Never evicted.\n *\n * 1. Throw RangeError if cap < 1.\n * 2. Collect entries belonging to `keyHash`.\n * 3. Mark the key's TTL-expired-AND-not-protected sessions for eviction (G2).\n * (inFlight===0 && now-lastAccess >= ttlMs)\n * 4. If survivors (key ids not in evict) >= cap, additionally evict the key's\n * idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n * ascending by lastAccess (Map-insertion-order stable sort resolves ties)\n * until survivors < cap or none remain.\n * 5. admit = survivors.length < cap.\n * 6. retryAfterSeconds: 0 when admit; else computed from the oldest\n * grace-blocker (smallest lastAccess), or 5 if every blocker is inFlight\n * (retryIsPoll=true in that all-inFlight case).\n * 7. Return { evict, admit, retryAfterSeconds, retryIsPoll }.\n *\n * Only the presenting key's sessions are ever touched.\n * Pure; total over empty maps. Ties in lastAccess resolve by Map insertion order.\n */\nexport function planKeyAdmission(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n cap: number,\n now: number,\n opts: { ttlMs: number; graceMs: number },\n): AdmissionPlan {\n if (cap < 1) {\n throw new RangeError(`cap must be >= 1, got ${cap}`);\n }\n\n const { ttlMs, graceMs } = opts;\n\n // Helper: a session is \"protected\" if it has an active POST call OR was\n // recently accessed (within the grace window).\n const isProtected = (s: CapSession): boolean =>\n s.inFlight > 0 || now - s.lastAccess < graceMs;\n\n // Step 2: collect the presenting key's entries as an ordered array.\n // Map iteration order equals insertion order (ES2015+; V8 stable).\n const keyEntries: [string, CapSession][] = [];\n for (const [id, session] of sessions) {\n if (session.keyHash === keyHash) {\n keyEntries.push([id, session]);\n }\n }\n\n // Step 3: always evict TTL-expired AND non-protected sessions (G2).\n const evictSet = new Set<string>();\n for (const [id, session] of keyEntries) {\n if (!isProtected(session) && now - session.lastAccess >= ttlMs) {\n evictSet.add(id);\n }\n }\n\n // Step 4: if survivors (key sessions not in evict) >= cap, additionally\n // evict idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n // ascending by lastAccess until survivors < cap.\n let survivors = keyEntries.filter(([id]) => !evictSet.has(id));\n\n if (survivors.length >= cap) {\n // Collect idle (evictable) survivors, preserving insertion order for\n // stable sort — Array.sort is stable in V8 and spec-required ES2019+.\n const idleSurvivors = survivors\n .filter(([, s]) => !isProtected(s))\n // Ascending by lastAccess (oldest first); equal values keep insertion order.\n .sort(([, a], [, b]) => a.lastAccess - b.lastAccess);\n\n for (const [id] of idleSurvivors) {\n if (survivors.length < cap) break;\n evictSet.add(id);\n // Recompute survivors in-place without the evicted id.\n survivors = survivors.filter(([sid]) => sid !== id);\n }\n }\n\n const evict = Array.from(evictSet);\n\n // Step 5.\n const admit = survivors.length < cap;\n\n // Step 6.\n let retryAfterSeconds = 0;\n let retryIsPoll = false;\n if (!admit) {\n // Grace-blockers: surviving sessions with inFlight===0 (blocked by grace only).\n const graceBlockers = survivors.filter(([, s]) => s.inFlight === 0);\n if (graceBlockers.length > 0) {\n // Target the oldest (smallest lastAccess) — crosses graceMs soonest.\n const minLastAccess = Math.min(...graceBlockers.map(([, s]) => s.lastAccess));\n const msRemaining = graceMs - (now - minLastAccess);\n retryAfterSeconds = Math.max(1, Math.ceil(msRemaining / 1000));\n } else {\n // All blockers have inFlight > 0; completion time is unpredictable. The\n // 5 s value is a poll-hint fallback, not a guarantee — flag it so the\n // caller's 429 reads as \"retry shortly\" rather than a hard deadline.\n retryAfterSeconds = 5;\n retryIsPoll = true;\n }\n }\n\n return { evict, admit, retryAfterSeconds, retryIsPoll };\n}\n\n// ---------------------------------------------------------------------------\n// countKeySessions\n// ---------------------------------------------------------------------------\n\n/**\n * Count how many sessions in the map belong to `keyHash`.\n * Pure; returns 0 for an empty map.\n */\nexport function countKeySessions(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n): number {\n let count = 0;\n for (const session of sessions.values()) {\n if (session.keyHash === keyHash) count++;\n }\n return count;\n}\n","/**\n * httpErrors.ts — pure, import-safe builders for structured 429 responses.\n *\n * All three 429 sources (rate limit, auth lockout, session cap) return a\n * JSON-RPC envelope so the consuming MCP client can tell them apart.\n *\n * Rule — id mirrors the JSON-RPC request id when known\n * ---------------------------------------------------------------------------\n * JSON-RPC requires the response id to mirror the request id so the client can\n * correlate. All three call-sites run AFTER the global `express.json()`\n * middleware, so a POST body is already parsed and callers SHOULD thread\n * `req.body?.id` through `BuildParams.id` (the POST `/mcp` rate-limit,\n * auth-lockout, and session-cap gates all do). Requests with no parsable body\n * (GET/DELETE, or a malformed POST) have no id to mirror — those omit `id` and\n * it defaults to `null`, which is the correct JSON-RPC value for \"unknown id\".\n * ---------------------------------------------------------------------------\n */\n\nexport interface Built429 {\n status: 429;\n headers: Record<string, string>;\n body: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Clamp to non-negative, round to integer — used for both the Retry-After\n * header value and the `retryAfterSeconds` field in `data`.\n */\nfunction clampSeconds(s: number): number {\n return Math.max(0, Math.round(s));\n}\n\ntype Reason = \"rate_limited\" | \"auth_lockout\" | \"session_cap\";\n\n/** JSON-RPC request/response id. `null` is the spec value for \"unknown id\". */\ntype JsonRpcId = string | number | null;\n\ninterface BuildParams {\n retryAfterSeconds: number;\n limit: number;\n current: number;\n /** Request id to mirror; defaults to null when the caller has no body id. */\n id?: JsonRpcId;\n}\n\nfunction build(\n reason: Reason,\n message: string,\n p: BuildParams,\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return {\n status: 429,\n headers: { \"Retry-After\": String(seconds) },\n body: {\n jsonrpc: \"2.0\",\n id: p.id ?? null,\n error: {\n code: -32000,\n message,\n data: {\n reason,\n retryAfterSeconds: seconds,\n limit: p.limit,\n current: p.current,\n },\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public builders\n// ---------------------------------------------------------------------------\n\n/**\n * Per-IP rate-limit 429 (source: mcpLimiter middleware).\n * reason = \"rate_limited\"\n */\nexport function buildRateLimitError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"rate_limited\",\n `Too many requests — retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-IP auth-failure lockout 429 (source: checkAuthBlock).\n * reason = \"auth_lockout\"\n */\nexport function buildAuthLockoutError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"auth_lockout\",\n `Too many failed auth attempts — locked out, retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-key session-cap 429 (source: planKeyAdmission !admit fallback).\n * reason = \"session_cap\"\n *\n * @param p.poll - true when ALL blockers have inFlight > 0 (no grace deadline\n * can be predicted; the 5 s value is a fixed fallback, not a guarantee).\n * The message reads as a poll-hint rather than a hard deadline.\n * false (default) when at least one blocker is grace-protected; the\n * retryAfterSeconds is a real guarantee and the message names it.\n */\nexport function buildSessionCapError(\n p: BuildParams & { poll?: boolean },\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n const message = p.poll\n ? \"Server busy — all sessions active; retry shortly.\"\n : `Too many active sessions for this key — retry after ${seconds} s.`;\n return build(\"session_cap\", message, p);\n}\n\n// ---------------------------------------------------------------------------\n// Sender\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal structural shape of the response object send429 needs. Any\n * Express/Hono response satisfies it; the explicit interface (rather than\n * `any`) keeps a wrong argument from compiling and silently swallowing a 429.\n */\nexport interface Http429Response {\n headersSent: boolean;\n setHeader(name: string, value: string): unknown;\n status(code: number): { json(body: unknown): unknown };\n}\n\n/**\n * Write a Built429 to an Express/Hono-compatible response object.\n *\n * Guards `res.headersSent` first so calling this on an already-flushed\n * response is a safe no-op (e.g. when a middleware has already sent a body).\n */\nexport function send429(res: Http429Response, built: Built429): void {\n if (res.headersSent) return;\n for (const [k, v] of Object.entries(built.headers)) {\n res.setHeader(k, v);\n }\n res.status(built.status).json(built.body);\n}\n"],"mappings":"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","names":["randomUUID","sessions","randomUUID","url","randomUUID","sid"]}
1
+ {"version":3,"sources":["../src/http.ts","../src/brand/logo-markup.ts","../src/lib/refresh-token.ts","../src/sessionCap.ts","../src/httpErrors.ts","../src/authLockout.ts"],"sourcesContent":["/**\n * HTTP transport entry point for Product Brain MCP.\n *\n * Serves the MCP protocol over Streamable HTTP for web clients\n * (Claude web app, API consumers) that can't spawn local processes.\n *\n * Implements the full MCP OAuth 2.1 spec (Nov 2025):\n * 1. Protected Resource Metadata (/.well-known/oauth-protected-resource)\n * 2. Authorization Server Metadata (/.well-known/oauth-authorization-server)\n * 3. Dynamic Client Registration (POST /register)\n * 4. Authorization Code + PKCE (GET/POST /authorize)\n * 5. Token Exchange (POST /oauth/token)\n *\n * Env:\n * CONVEX_SITE_URL — Convex deployment URL (defaults to cloud)\n * PORT / MCP_PORT — Listen port (default 3000)\n * CORS_ORIGINS — Comma-separated allowed origins (default: all)\n * PB_MODULES — Comma-separated modules (default: core,gitchain,arch)\n */\n\nimport { createHash, randomUUID } from \"node:crypto\";\nimport express from \"express\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { isInitializeRequest } from \"@modelcontextprotocol/sdk/types.js\";\nimport rateLimit from \"express-rate-limit\";\n\nimport { bootstrapHttp, DEFAULT_CLOUD_URL } from \"./client.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.js\";\nimport { appLogoMarkup, appLogoStyles } from \"./brand/logo-markup.js\";\nimport {\n signRefreshToken,\n verifyRefreshToken,\n} from \"./lib/refresh-token.js\";\nimport { planKeyAdmission, countKeySessions } from \"./sessionCap.js\";\nimport {\n send429,\n buildRateLimitError,\n buildAuthLockoutError,\n buildSessionCapError,\n} from \"./httpErrors.js\";\nimport {\n AUTH_FAILURE_MAX,\n AUTH_FAILURE_WINDOW_MS,\n MAX_AUTH_FAILURE_ENTRIES,\n checkAuthBlock,\n recordAuthFailure,\n resolveBearerAuth,\n} from \"./authLockout.js\";\nimport type { AuthFailureRecord } from \"./authLockout.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\nfunction baseUrl(req: any): string {\n const proto = req.headers[\"x-forwarded-proto\"] ?? req.protocol ?? \"http\";\n const host = req.headers.host ?? `localhost:${PORT}`;\n return `${proto}://${host}`;\n}\n\n// ── Express App ─────────────────────────────────────────────────────────\n\nconst app = express();\n// Required when behind a reverse proxy (e.g. Railway): rate limiter uses X-Forwarded-For\n// and throws ERR_ERL_UNEXPECTED_X_FORWARDED_FOR if trust proxy is false.\napp.set(\"trust proxy\", 1);\napp.use(express.json());\n\n// CORS — defaults to https://claude.ai (per REMOTE.md). Override via CORS_ORIGINS env var.\nconst ALLOWED_ORIGINS = (process.env.CORS_ORIGINS ?? \"https://claude.ai\")\n .split(\",\")\n .map((o) => o.trim())\n .filter(Boolean);\n\napp.use((_req: any, res: any, next: any) => {\n const origin = _req.headers.origin;\n if (origin && ALLOWED_ORIGINS.includes(origin)) {\n res.setHeader(\"Access-Control-Allow-Origin\", origin);\n }\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\");\n res.setHeader(\n \"Access-Control-Allow-Headers\",\n \"Content-Type, Authorization, Mcp-Session-Id, Last-Event-Id\",\n );\n // Retry-After is exposed so browser MCP clients (default origin https://claude.ai)\n // can read the structured-429 retry timing from JS, not just from the JSON body.\n res.setHeader(\"Access-Control-Expose-Headers\", \"Mcp-Session-Id, Retry-After\");\n if (_req.method === \"OPTIONS\") {\n res.status(204).end();\n return;\n }\n next();\n});\n\n// ── OAuth: Protected Resource Metadata (RFC 9728) ────────────────────────\n// Step 1 of MCP auth: Claude fetches this to discover the authorization server.\n\napp.get(\"/.well-known/oauth-protected-resource\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n resource: base,\n authorization_servers: [base],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n bearer_methods_supported: [\"header\"],\n });\n});\n\n// ── OAuth: Authorization Server Metadata (RFC 8414) ──────────────────────\n// Step 2: Claude fetches this to discover authorize, token, and register endpoints.\n\napp.get(\"/.well-known/oauth-authorization-server\", (req: any, res: any) => {\n const base = baseUrl(req);\n res.json({\n issuer: base,\n authorization_endpoint: `${base}/authorize`,\n token_endpoint: `${base}/oauth/token`,\n registration_endpoint: `${base}/register`,\n response_types_supported: [\"code\"],\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"],\n scopes_supported: [\"mcp:tools\", \"mcp:resources\"],\n });\n});\n\n// ── OAuth: Rate Limiting (Fix 2) ─────────────────────────────────────────\n// Separate, stricter limiter for auth endpoints to prevent brute-force and\n// enumeration attacks on the OAuth flow.\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n message: { error: \"Too many auth requests. Try again later.\" },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: Claude registers itself as a client before starting the auth flow.\n\ninterface RegisteredClient {\n client_id: string;\n redirect_uris: string[];\n client_name?: string;\n registeredAt: number;\n}\n\nconst registeredClients = new Map<string, RegisteredClient>();\n// Fix 4 — Cap client registrations at 500 to prevent unbounded memory growth.\nconst MAX_REGISTERED_CLIENTS = 500;\n\napp.post(\n \"/register\",\n authLimiter,\n express.json(),\n (req: any, res: any) => {\n // Fix 4 — Reject registration when cap is reached.\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {\n res.status(503).json({\n error: \"server_error\",\n error_description: \"Registration limit reached. Try again later.\",\n });\n return;\n }\n\n const { redirect_uris, client_name } = req.body;\n\n if (!Array.isArray(redirect_uris) || redirect_uris.length === 0) {\n res.status(400).json({\n error: \"invalid_client_metadata\",\n error_description: \"redirect_uris is required\",\n });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const client: RegisteredClient = {\n client_id: clientId,\n redirect_uris,\n client_name,\n registeredAt: Date.now(),\n };\n registeredClients.set(clientId, client);\n\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris,\n grant_types: [\"authorization_code\"],\n response_types: [\"code\"],\n token_endpoint_auth_method: \"none\",\n });\n },\n);\n\n// ── OAuth: Authorization Code + PKCE ─────────────────────────────────────\n// Step 4: User enters their pb_sk_* key, server generates a one-time code.\n\ninterface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\nconst pendingCodes = new Map<string, PendingAuth>();\n\n// Refresh tokens are now stateless HMAC-signed (see lib/refresh-token.ts).\n// No in-memory store, no cleanup loop, no per-key caps — they survive\n// Railway redeploys, which fixes TEN-1661.\nconst ACCESS_TOKEN_TTL = 3600; // 1 hour\nconst ACCESS_TOKEN_TTL_MS = ACCESS_TOKEN_TTL * 1000;\n\n// Fix 1 — Opaque access token store.\n// Maps pb_at_<uuid> → { apiKey, createdAt } so the raw pb_sk_* key is never\n// exposed through the OAuth flow. Capped at 1000 entries with LRU eviction.\ninterface AccessTokenEntry {\n apiKey: string;\n createdAt: number;\n}\nconst accessTokens = new Map<string, AccessTokenEntry>();\nconst MAX_ACCESS_TOKENS = 1000;\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n for (const [id, client] of registeredClients) {\n if (now - client.registeredAt > 24 * 60 * 60_000) registeredClients.delete(id);\n }\n // Fix 1 — Evict expired opaque access tokens.\n for (const [token, entry] of accessTokens) {\n if (now - entry.createdAt > ACCESS_TOKEN_TTL_MS) accessTokens.delete(token);\n }\n // Fix 5 — Clean up stale auth failure tracking entries.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&amp;\", '\"': \"&quot;\", \"'\": \"&#39;\", \"<\": \"&lt;\", \">\": \"&gt;\" })[c]!,\n );\n}\n\n// ── Authorize Page Templates ─────────────────────────────────────────────\n// Parchment-dark OAuth pages — brand tokens from DEC-419 (brand-tokens.json).\n// Monochrome-first per DEC-417, warm temperature per DEC-418.\n// Provider-agnostic: copy interpolates registered client_name (RFC 7591),\n// never hardcodes \"Claude\" — any MCP client may reach this page.\n\nfunction authPageShell(title: string, bodyContent: string, headExtra = \"\"): string {\n return `<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"parchment-dark\"><head>\n<meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">\n<title>${esc(title)} — Product Brain</title>\n<link rel=\"preconnect\" href=\"https://fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,600&family=IBM+Plex+Sans:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;700&display=swap\">\n${headExtra}\n<style>\n:root{\n --bg:#1a1917;--bg-warm:#201f1c;--surface:#262521;\n --fg1:#e4e0d8;--fg2:#c4bfb4;--fg3:#9a9589;--fg4:#6a6560;--fg-bright:#ffffff;\n --border:rgba(255,255,255,0.07);--border-light:rgba(255,255,255,0.04);\n --accent:#c9b99a;\n --btn-bg:#ffffff;--btn-fg:#1a1917;--btn-hover:#e4e0d8;\n --green:#4ade80;--rose:#ef4444;\n --ghost:rgba(38,37,33,0.55);\n --radius-md:7px;--radius-lg:10px;\n --font-display:\"Source Serif 4\",ui-serif,Georgia,serif;\n --font-body:\"IBM Plex Sans\",ui-sans-serif,system-ui,sans-serif;\n --font-mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace;\n}\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\nhtml,body{height:100%}\nbody{\n font-family:var(--font-body);font-size:13px;line-height:1.45;\n color:var(--fg1);background:var(--bg);\n min-height:100vh;display:grid;place-items:center;padding:24px;\n -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;\n position:relative;overflow:hidden;\n}\nbody::before{\n content:\"\";position:fixed;inset:0;\n background:radial-gradient(900px 600px at 50% 50%,rgba(228,224,216,0.025),transparent 60%);\n pointer-events:none;z-index:0;\n}\n.top-mark{position:fixed;top:22px;left:24px;z-index:5;opacity:0.7}\n${appLogoStyles}\n.stage{\n width:100%;max-width:460px;text-align:center;\n position:relative;z-index:1;\n display:grid;\n}\n.panel{\n grid-area:1/1;\n transition:opacity 280ms ease-out,transform 380ms cubic-bezier(.2,.6,.2,1),filter 380ms ease-out;\n}\n.panel[hidden]{\n display:block !important;\n opacity:0;transform:scale(0.96) translateY(-2px);filter:blur(6px);\n pointer-events:none;\n}\n.panel:not([hidden]){opacity:1;transform:scale(1);filter:none;pointer-events:auto}\n\n/* eyebrows */\n.eyebrow{\n font-family:var(--font-mono);font-size:10px;font-weight:700;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg4);\n margin-bottom:24px;display:inline-flex;align-items:center;gap:7px;\n}\n.eyebrow .dot{width:5px;height:5px;border-radius:50%;background:currentColor}\n.eyebrow.danger{color:var(--rose)}\n.eyebrow.success{color:var(--green)}\n.eyebrow.success .dot{box-shadow:0 0 0 3px rgba(74,222,128,0.18)}\n\n/* form input */\n.input-wrap{\n display:flex;align-items:center;background:rgba(0,0,0,0.22);\n border:1px solid var(--border);border-radius:var(--radius-md);\n transition:border-color 200ms ease-out,box-shadow 200ms ease-out;\n}\n.input-wrap:focus-within{\n border-color:rgba(228,224,216,0.45);\n box-shadow:0 0 0 3px rgba(228,224,216,0.10);\n}\n.input-wrap.has-error{\n border-color:rgba(239,68,68,0.55);\n box-shadow:0 0 0 3px rgba(239,68,68,0.12);\n animation:shake 360ms cubic-bezier(.36,.07,.19,.97);\n}\n@keyframes shake{\n 10%,90%{transform:translateX(-1px)}20%,80%{transform:translateX(2px)}\n 30%,50%,70%{transform:translateX(-4px)}40%,60%{transform:translateX(4px)}\n}\n.input{\n flex:1;min-width:0;background:transparent;border:0;outline:none;\n padding:16px;\n font-family:var(--font-mono);font-size:14px;color:var(--fg1);letter-spacing:0.02em;\n}\n.input::placeholder{color:var(--fg4)}\n.hint{\n margin-top:10px;font-family:var(--font-mono);font-size:10.5px;\n letter-spacing:0.16em;text-transform:uppercase;\n text-align:left;padding-left:4px;height:14px;color:var(--fg4);\n transition:color 160ms ease-out;\n}\n.hint.is-error{color:var(--rose)}\n\n/* primary button */\n.btn-primary{\n width:100%;height:48px;margin-top:14px;\n border:0;border-radius:var(--radius-md);\n background:var(--btn-bg);color:var(--btn-fg);\n font-family:var(--font-body);font-size:14.5px;font-weight:600;letter-spacing:-0.005em;\n cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:10px;\n transition:background 140ms ease-out,transform 80ms ease-out,opacity 200ms ease-out;\n position:relative;overflow:hidden;\n}\n.btn-primary:hover:not([disabled]){background:var(--btn-hover)}\n.btn-primary:active:not([disabled]){transform:translateY(1px)}\n.btn-primary[disabled]{opacity:0.45;cursor:default}\n.spin{\n width:14px;height:14px;border-radius:50%;\n border:1.5px solid currentColor;border-top-color:transparent;\n animation:spin 700ms linear infinite;opacity:0.85;\n}\n@keyframes spin{to{transform:rotate(360deg)}}\n\n/* secondary link */\n.small-link{\n margin-top:18px;font-family:var(--font-mono);font-size:11px;\n letter-spacing:0.18em;text-transform:uppercase;color:var(--fg4);\n}\n.small-link a{\n color:var(--fg3);text-decoration:none;border-bottom:1px dotted currentColor;\n padding-bottom:1px;transition:color 140ms;\n}\n.small-link a:hover{color:var(--fg1)}\n\n/* orb */\n.orb-wrap{\n position:relative;width:160px;height:160px;margin:0 auto 36px;\n display:grid;place-items:center;\n}\n.orb-ring{position:absolute;border-radius:50%}\n.orb-ring.r1{inset:0;border:1px solid rgba(228,224,216,0.06);animation:drift1 40s linear infinite}\n.orb-ring.r2{inset:18px;border:1px dashed rgba(228,224,216,0.10);animation:drift2 28s linear infinite}\n.orb-ring.r3{inset:38px;border:1px solid rgba(74,222,128,0.20);animation:ringPulse 3.4s ease-in-out infinite}\n@keyframes drift1{to{transform:rotate(360deg)}}\n@keyframes drift2{to{transform:rotate(-360deg)}}\n@keyframes ringPulse{0%,100%{opacity:0.6}50%{opacity:1}}\n\n.orb-wrap.is-verifying .orb-ring.r3{\n border-color:rgba(228,224,216,0.20);\n animation:ringPulse 1.1s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-core{\n box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06);\n animation:corePulseNeutral 1.6s ease-in-out infinite;\n}\n.orb-wrap.is-verifying .orb-dot{background:var(--fg3);box-shadow:0 0 10px rgba(228,224,216,0.4)}\n@keyframes corePulseNeutral{\n 0%,100%{box-shadow:0 0 0 6px rgba(228,224,216,0.04),0 0 22px rgba(228,224,216,0.10),inset 0 0 16px rgba(228,224,216,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(228,224,216,0.07),0 0 32px rgba(228,224,216,0.18),inset 0 0 22px rgba(228,224,216,0.10)}\n}\n\n.orb-wrap.is-error .orb-ring.r3{border-color:rgba(239,68,68,0.30);animation:none;opacity:1}\n.orb-wrap.is-error .orb-ring.r1,.orb-wrap.is-error .orb-ring.r2{animation-play-state:paused}\n.orb-wrap.is-error .orb-core{\n box-shadow:0 0 0 6px rgba(239,68,68,0.05),0 0 22px rgba(239,68,68,0.20),inset 0 0 16px rgba(239,68,68,0.08);\n animation:none;\n}\n.orb-wrap.is-error .orb-dot{background:var(--rose);box-shadow:0 0 10px rgba(239,68,68,0.6)}\n\n.sat-orbit{position:absolute;inset:0;pointer-events:none}\n.sat-orbit.o1{animation:drift1 40s linear infinite}\n.sat-orbit.o2{animation:drift2 56s linear infinite}\n.sat{\n position:absolute;top:50%;left:50%;\n font-family:var(--font-mono);font-size:9px;font-weight:700;letter-spacing:0.14em;\n background:var(--bg);padding:2px 6px;border-radius:3px;\n border:1px solid var(--border-light);\n transform-origin:0 0;opacity:0;\n}\n.panel:not([hidden])[data-state=\"connected\"] .sat{animation:satIn 600ms ease-out forwards}\n@keyframes satIn{from{opacity:0}to{opacity:1}}\n.sat span{display:inline-block;animation:counter 40s linear infinite}\n.sat-orbit.o2 .sat span{animation:counter2 56s linear infinite}\n@keyframes counter{to{transform:rotate(-360deg)}}\n@keyframes counter2{to{transform:rotate(360deg)}}\n\n.orb-core{\n position:relative;width:60px;height:60px;border-radius:50%;\n background:radial-gradient(circle at 50% 45%,#1a1a1a 0%,#0c0c0c 60%,#050505 100%);\n border:1px solid rgba(255,255,255,0.06);\n display:grid;place-items:center;\n box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 28px rgba(74,222,128,0.20),inset 0 0 18px rgba(74,222,128,0.08);\n animation:corePulse 3.4s ease-in-out infinite;\n}\n@keyframes corePulse{\n 0%,100%{box-shadow:0 0 0 6px rgba(74,222,128,0.04),0 0 24px rgba(74,222,128,0.18),inset 0 0 16px rgba(74,222,128,0.06)}\n 50%{box-shadow:0 0 0 9px rgba(74,222,128,0.06),0 0 40px rgba(74,222,128,0.32),inset 0 0 22px rgba(74,222,128,0.14)}\n}\n.orb-dot{width:10px;height:10px;border-radius:50%;background:#4ade80;box-shadow:0 0 12px rgba(74,222,128,0.7)}\n\n.core-shockwave{\n position:absolute;inset:0;border-radius:50%;\n border:1px solid rgba(74,222,128,0.6);\n opacity:0;pointer-events:none;\n}\n.panel:not([hidden])[data-state=\"connected\"] .core-shockwave{animation:shock 1100ms ease-out 200ms}\n@keyframes shock{\n 0%{opacity:0.7;transform:scale(0.4);border-width:2px}\n 100%{opacity:0;transform:scale(2.4);border-width:1px}\n}\n\n/* titles */\n.ok-title{\n font-family:var(--font-display);font-weight:600;\n font-size:38px;line-height:1.05;letter-spacing:-0.025em;\n color:var(--fg-bright);opacity:0;\n}\n.panel:not([hidden]) .ok-title{animation:rise 600ms ease-out 380ms forwards}\n.ok-lead{\n margin:18px auto 0;max-width:22em;font-size:16px;line-height:1.55;color:var(--fg2);\n opacity:0;\n}\n.panel:not([hidden]) .ok-lead{animation:rise 600ms ease-out 500ms forwards}\n.ok-phrase-row{\n margin-top:14px;display:flex;align-items:center;justify-content:center;\n opacity:0;\n}\n.panel:not([hidden]) .ok-phrase-row{animation:rise 600ms ease-out 620ms forwards}\n.cmd.is-copied .cmd-quote-part{display:none}\n.success-cta-wrap{\n margin-top:48px;width:100%;opacity:0;\n}\n.panel:not([hidden]) .success-cta-wrap{animation:rise 600ms ease-out 780ms forwards}\na.btn-primary.success-cta{color:var(--btn-fg);text-decoration:none}\n\n.cmd{\n display:inline-flex;align-items:center;gap:8px;vertical-align:middle;\n font-family:var(--font-mono);font-size:15px;font-weight:500;color:var(--accent);\n background:rgba(201,185,154,0.08);border:1px solid rgba(201,185,154,0.30);\n padding:5px 10px 5px 12px;border-radius:6px;letter-spacing:0.02em;\n cursor:pointer;user-select:none;\n transition:background 140ms ease-out,border-color 140ms ease-out,color 140ms ease-out;\n}\n.cmd:hover{background:rgba(201,185,154,0.14);border-color:rgba(201,185,154,0.50);color:#dcc9a4}\n.cmd.is-copied{color:var(--green);border-color:rgba(74,222,128,0.35);background:rgba(74,222,128,0.08)}\n.cmd .cmd-icon{\n width:12px;height:12px;color:currentColor;opacity:0.75;\n display:inline-grid;place-items:center;\n transition:opacity 140ms;\n}\n.cmd:hover .cmd-icon{opacity:1}\n.cmd.is-copied .cmd-icon{color:var(--green);opacity:1}\n.cmd .cmd-icon svg{width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}\n\n/* error specifics */\n.err-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:12px;\n}\n.err-msg{\n font-size:14.5px;line-height:1.55;color:var(--fg3);\n margin-bottom:28px;\n}\n.err-msg code{\n font-family:var(--font-mono);font-size:12px;\n background:rgba(255,255,255,0.06);padding:1px 5px;border-radius:4px;color:var(--fg2);\n}\n.err-actions{display:flex;gap:8px}\n.btn-secondary{\n flex:1;height:44px;border-radius:var(--radius-md);\n background:transparent;color:var(--fg2);\n border:1px solid var(--border);\n font-family:var(--font-body);font-size:13.5px;font-weight:500;\n cursor:pointer;text-decoration:none;\n display:inline-flex;align-items:center;justify-content:center;\n transition:background 140ms,color 140ms,border-color 140ms;\n}\n.btn-secondary:hover{background:rgba(255,255,255,0.04);color:var(--fg1);border-color:rgba(255,255,255,0.14)}\n\n/* verifying */\n.verifying-eyebrow{\n font-family:var(--font-mono);font-size:10px;\n letter-spacing:0.28em;text-transform:uppercase;color:var(--fg3);\n font-weight:700;margin-bottom:14px;\n}\n.verifying-title{\n font-family:var(--font-display);font-weight:600;\n font-size:28px;line-height:1.1;color:var(--fg1);margin-bottom:8px;\n letter-spacing:-0.02em;\n}\n.verifying-sub{font-size:13px;color:var(--fg3);min-height:1.45em}\n\n/* form heading */\n.form-title{\n font-family:var(--font-display);font-weight:600;\n font-size:32px;line-height:1.1;letter-spacing:-0.02em;\n color:var(--fg1);margin-bottom:10px;\n}\n.form-sub{font-size:13.5px;color:var(--fg3);margin-bottom:28px;line-height:1.55}\n\n@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}\n\n@media (prefers-reduced-motion: reduce){\n *,*::before,*::after{\n animation-duration:0.01ms !important;animation-iteration-count:1 !important;\n transition-duration:0.01ms !important;\n }\n}\n</style>\n</head><body>\n<div class=\"top-mark\">${appLogoMarkup({ size: \"sm\" })}</div>\n<div class=\"stage\">${bodyContent}</div>\n</body></html>`;\n}\n\n// Provider-agnostic display name. Trims and clamps to keep page chrome stable.\nfunction providerDisplayName(clientName: string | undefined): string {\n const name = (clientName ?? \"\").trim();\n if (!name) return \"your assistant\";\n return name.length > 40 ? name.slice(0, 40) + \"…\" : name;\n}\n\n// Inner HTML for the success panel. Used both by the JSON path (cloned client-side\n// from a <template>) and by the no-JS server-rendered fallback page.\nfunction successPanelInner(workspaceName: string, redirectUrl: string, providerName: string): string {\n return `\n<div class=\"orb-wrap\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"sat-orbit o1\">\n <span class=\"sat\" style=\"transform:rotate(20deg) translate(78px) rotate(-20deg);color:#4ade80;animation-delay:600ms\"><span>DEC</span></span>\n <span class=\"sat\" style=\"transform:rotate(140deg) translate(78px) rotate(-140deg);color:#c9b99a;animation-delay:720ms\"><span>WP</span></span>\n <span class=\"sat\" style=\"transform:rotate(260deg) translate(78px) rotate(-260deg);color:#f59e0b;animation-delay:840ms\"><span>TEN</span></span>\n </div>\n <div class=\"sat-orbit o2\">\n <span class=\"sat\" style=\"transform:rotate(80deg) translate(54px) rotate(-80deg);color:#60a5fa;animation-delay:960ms\"><span>STD</span></span>\n <span class=\"sat\" style=\"transform:rotate(220deg) translate(54px) rotate(-220deg);color:#a78bfa;animation-delay:1080ms\"><span>INS</span></span>\n </div>\n <div class=\"core-shockwave\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow success\"><span class=\"dot\"></span>Connected</div>\n<h1 class=\"ok-title\">Product Brain is Live</h1>\n<p class=\"ok-lead\">Return to your assistant, then say</p>\n<div class=\"ok-phrase-row\">\n <button class=\"cmd\" type=\"button\" data-cmd-pill data-redirect=\"${esc(redirectUrl)}\" aria-label=\"Copy &quot;Start PB&quot; and return to ${esc(providerName)}\">\n <span class=\"cmd-quote-part\" aria-hidden=\"true\">&ldquo;</span><span data-cmd-text>Start PB</span><span class=\"cmd-quote-part\" aria-hidden=\"true\">&rdquo;</span>\n <span class=\"cmd-icon\" aria-hidden=\"true\">\n <svg data-cmd-svg viewBox=\"0 0 24 24\"><rect x=\"9\" y=\"9\" width=\"11\" height=\"11\" rx=\"2\"/><path d=\"M5 15V6a2 2 0 0 1 2-2h9\"/></svg>\n </span>\n </button>\n</div>\n<div class=\"success-cta-wrap\">\n <a class=\"btn-primary success-cta\" href=\"${esc(redirectUrl)}\">Continue in ${esc(providerName)}</a>\n</div>\n<!-- workspace name retained as data hook for tests, hidden from view -->\n<span hidden data-field=\"ws-name\">${esc(workspaceName)}</span>`;\n}\n\nfunction errorPanelInner(title: string, trustedDetailHtml: string, retryUrl: string): string {\n return `\n<div class=\"orb-wrap is-error\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n</div>\n<div class=\"eyebrow danger\"><span class=\"dot\"></span>Couldn't connect</div>\n<h2 class=\"err-title\" data-field=\"err-title\">${esc(title)}</h2>\n<p class=\"err-msg\" data-field=\"err-msg\">${trustedDetailHtml}</p>\n<div class=\"err-actions\">\n <a href=\"${esc(retryUrl)}\" class=\"btn-secondary\" data-retry-link>&larr; Try again</a>\n <a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key &rarr;</a>\n</div>`;\n}\n\nconst cmdScript = `\n(function(){\n function bindCmd(pill){\n if(!pill||pill.__bound)return;pill.__bound=true;\n var textEl=pill.querySelector('[data-cmd-text]');\n var svgEl=pill.querySelector('[data-cmd-svg]');\n var redirectUrl=pill.getAttribute('data-redirect')||'';\n pill.addEventListener('click',function(){\n var done=function(){\n pill.classList.add('is-copied');\n if(textEl)textEl.textContent='Copied';\n if(svgEl)svgEl.innerHTML='<polyline points=\"4 12 10 18 20 6\"/>';\n setTimeout(function(){if(redirectUrl)window.location.assign(redirectUrl)},900);\n };\n try{\n if(navigator.clipboard&&navigator.clipboard.writeText){\n navigator.clipboard.writeText('Start PB').then(done,done);\n }else{done()}\n }catch(e){done()}\n });\n }\n document.querySelectorAll('[data-cmd-pill]').forEach(bindCmd);\n})();\n`;\n\nfunction authorizeFormPage(params: {\n redirect_uri: string;\n code_challenge: string;\n code_challenge_method: string;\n state: string;\n client_id: string;\n client_name?: string;\n}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const providerName = providerDisplayName(params.client_name);\n const body = `\n<!-- ─── CONNECT ─── -->\n<div class=\"panel\" id=\"p-connect\" data-state=\"connect\">\n <div class=\"eyebrow\">Connect Product Brain</div>\n <h1 class=\"form-title\">Paste your API key</h1>\n <p class=\"form-sub\">Give <span data-provider>${esc(providerName)}</span> access to your workspace memory.</p>\n <form method=\"POST\" action=\"/authorize\" id=\"f\" autocomplete=\"off\">\n <input type=\"hidden\" name=\"redirect_uri\" value=\"${esc(redirect_uri)}\">\n <input type=\"hidden\" name=\"code_challenge\" value=\"${esc(code_challenge)}\">\n <input type=\"hidden\" name=\"code_challenge_method\" value=\"${esc(code_challenge_method)}\">\n <input type=\"hidden\" name=\"state\" value=\"${esc(state)}\">\n <input type=\"hidden\" name=\"client_id\" value=\"${esc(client_id)}\">\n <div class=\"input-wrap\" id=\"iw\">\n <input type=\"password\" id=\"k\" name=\"api_key\" class=\"input input-full\" placeholder=\"pb_sk_…\" required autofocus spellcheck=\"false\">\n </div>\n <div class=\"hint\" id=\"hint\" hidden></div>\n <button type=\"submit\" class=\"btn-primary\" id=\"sb\" disabled><span id=\"bt\">Connect</span></button>\n </form>\n <div class=\"small-link\"><a href=\"https://productbrain.io\" target=\"_blank\" rel=\"noopener noreferrer\">No key? Generate one &rarr;</a></div>\n</div>\n\n<!-- ─── VERIFYING ─── -->\n<div class=\"panel\" id=\"p-verifying\" data-state=\"verifying\" hidden>\n <div class=\"orb-wrap is-verifying\">\n <div class=\"orb-ring r1\"></div>\n <div class=\"orb-ring r2\"></div>\n <div class=\"orb-ring r3\"></div>\n <div class=\"orb-core\"><div class=\"orb-dot\"></div></div>\n </div>\n <div class=\"verifying-eyebrow\">Handshake</div>\n <h2 class=\"verifying-title\">Verifying key…</h2>\n <p class=\"verifying-sub\" id=\"verify-sub\">Checking workspace · …</p>\n</div>\n\n<!-- ─── CONNECTED (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-connected\" data-state=\"connected\" hidden></div>\n\n<!-- ─── ERROR (filled by JS from JSON response) ─── -->\n<div class=\"panel\" id=\"p-error\" data-state=\"error\" hidden></div>\n\n<template id=\"tpl-connected\">${successPanelInner(\"__WS__\", \"__URL__\", \"__PROVIDER__\")}</template>\n<template id=\"tpl-error\">${errorPanelInner(\"__TITLE__\", \"__DETAIL__\", \"__RETRY__\")}</template>\n\n<script>\n${cmdScript}\n(function(){\n var f=document.getElementById('f'),k=document.getElementById('k'),iw=document.getElementById('iw'),hint=document.getElementById('hint'),sb=document.getElementById('sb'),bt=document.getElementById('bt');\n var pConnect=document.getElementById('p-connect'),pVerify=document.getElementById('p-verifying'),pOk=document.getElementById('p-connected'),pErr=document.getElementById('p-error');\n var verifySub=document.getElementById('verify-sub');\n\n function show(panel){\n [pConnect,pVerify,pOk,pErr].forEach(function(p){\n if(p===panel){p.removeAttribute('hidden')}else{p.setAttribute('hidden','')}\n });\n }\n\n function syncInput(){\n sb.disabled=!k.value.trim();\n iw.classList.remove('has-error');\n hint.classList.remove('is-error');\n hint.textContent='';\n hint.setAttribute('hidden','');\n }\n k.addEventListener('input',syncInput);\n k.addEventListener('keydown',function(e){\n if(e.key==='Escape'){k.value='';syncInput()}\n });\n\n function showError(title,detailHtml){\n var tpl=document.getElementById('tpl-error');\n var html=tpl.innerHTML\n .replace('__TITLE__',title.replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]}))\n .replace('__DETAIL__',detailHtml)\n .replace('__RETRY__','#');\n pErr.innerHTML=html;\n var retry=pErr.querySelector('[data-retry-link]');\n if(retry){retry.addEventListener('click',function(e){e.preventDefault();show(pConnect);k.focus();k.select()})}\n show(pErr);\n }\n\n function showSuccess(workspaceName,redirectUrl,providerName){\n var tpl=document.getElementById('tpl-connected');\n var safeWs=String(workspaceName||'').replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]});\n var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'&lt;','>':'&gt;','&':'&amp;'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'&quot;').replace(/[<>]/g,function(c){return{'<':'&lt;','>':'&gt;'}[c]});\n var html=tpl.innerHTML.split('__WS__').join(safeWs).split('__URL__').join(safeUrl).split('__PROVIDER__').join(safeProv);\n pOk.innerHTML=html;\n pOk.querySelectorAll('[data-cmd-pill]').forEach(function(pill){\n pill.__bound=false;\n });\n // Re-run binder\n var s=document.createElement('script');s.textContent=${JSON.stringify(cmdScript)};document.body.appendChild(s);s.remove();\n show(pOk);\n }\n\n f.addEventListener('submit',function(e){\n e.preventDefault();\n var v=k.value.trim();\n if(!v){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Paste your key first';hint.removeAttribute('hidden');return}\n if(v.indexOf('pb_sk_')!==0){iw.classList.add('has-error');hint.classList.add('is-error');hint.textContent='Key must start with pb_sk_';hint.removeAttribute('hidden');return}\n sb.disabled=true;bt.textContent='Verifying';\n show(pVerify);\n\n var steps=['Checking workspace · …','Loading chain · …','Establishing memory · …'];\n var i=0;verifySub.textContent=steps[0];\n var ti=setInterval(function(){i++;if(i>=steps.length){clearInterval(ti);return}verifySub.textContent=steps[i]},900);\n\n var minDelay=new Promise(function(r){setTimeout(r,2800)});\n var fd=new FormData(f);\n var body=new URLSearchParams();\n fd.forEach(function(val,key){body.append(key,String(val))});\n\n var req=fetch('/authorize',{\n method:'POST',\n headers:{'Accept':'application/json','Content-Type':'application/x-www-form-urlencoded'},\n body:body.toString(),\n credentials:'same-origin'\n }).then(function(r){return r.json().then(function(j){return{status:r.status,body:j}})});\n\n Promise.all([req,minDelay]).then(function(arr){\n clearInterval(ti);\n var res=arr[0];\n if(res.body&&res.body.ok){\n showSuccess(res.body.workspaceName,res.body.redirectUrl,res.body.providerName);\n }else{\n showError(res.body&&res.body.title||'Couldn\\\\'t connect',res.body&&res.body.detail||'Try again, or generate a new key.');\n sb.disabled=false;bt.textContent='Connect';\n }\n }).catch(function(){\n clearInterval(ti);\n showError('Network error','We couldn\\\\'t reach Product Brain. Check your connection and try again.');\n sb.disabled=false;bt.textContent='Connect';\n });\n });\n\n k.focus();\n})();\n</script>`;\n return authPageShell(\"Connect Product Brain\", body);\n}\n\nfunction authorizeSuccessPage(workspaceName: string, redirectUrl: string, providerName: string): string {\n const body = `\n<div class=\"panel\" data-state=\"connected\">\n${successPanelInner(workspaceName, redirectUrl, providerName)}\n</div>\n<script>${cmdScript}</script>`;\n return authPageShell(\"Connected\", body);\n}\n\n// security: trustedDetailHtml must be a hardcoded literal — never pass user-controlled data.\nfunction authorizeErrorPage(title: string, trustedDetailHtml: string, retryUrl: string): string {\n const body = `\n<div class=\"panel\" data-state=\"error\">\n${errorPanelInner(title, trustedDetailHtml, retryUrl)}\n</div>`;\n return authPageShell(\"Connection error\", body);\n}\n\napp.get(\"/authorize\", authLimiter, (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;\n const cid = String(client_id ?? \"\");\n const clientName = cid && registeredClients.has(cid)\n ? registeredClients.get(cid)!.client_name\n : undefined;\n res.type(\"html\").send(authorizeFormPage({\n redirect_uri: String(redirect_uri ?? \"\"),\n code_challenge: String(code_challenge ?? \"\"),\n code_challenge_method: String(code_challenge_method ?? \"S256\"),\n state: String(state ?? \"\"),\n client_id: cid,\n client_name: clientName,\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { api_key, redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n\n // Negotiate response shape: fetch path sets Accept: application/json,\n // form-post no-JS path gets the standard rebranded HTML page.\n const wantsJson = String(req.headers[\"accept\"] ?? \"\").includes(\"application/json\");\n\n // Build \"retry\" URL so error pages can link back to the form.\n const retryParams = new URLSearchParams({\n redirect_uri: redirect_uri ?? \"\",\n code_challenge: code_challenge ?? \"\",\n code_challenge_method: code_challenge_method ?? \"S256\",\n ...(state ? { state } : {}),\n ...(client_id ? { client_id } : {}),\n }).toString();\n const retryUrl = `/authorize?${retryParams}`;\n\n function sendError(title: string, trustedDetailHtml: string, status = 400): void {\n if (wantsJson) {\n res.status(status).json({ ok: false, title, detail: trustedDetailHtml });\n } else {\n res.status(status).type(\"html\").send(authorizeErrorPage(title, trustedDetailHtml, retryUrl));\n }\n }\n\n if (!api_key?.startsWith(\"pb_sk_\")) {\n sendError(\n \"Invalid key format\",\n \"API keys start with <code>pb_sk_</code>. Check your key and try again.\",\n );\n return;\n }\n\n // Validate redirect_uri against the registered client's allowed redirects.\n // Open redirect prevention: never trust a request-supplied redirect_uri without\n // checking it was pre-registered during dynamic client registration (RFC 7591).\n //\n // When client_id is provided but unknown (e.g. server restart wiped the in-memory Map\n // after claude.ai cached its client_id from a previous session), auto-re-register using\n // the supplied redirect_uri rather than hard-rejecting. The API key validation below is\n // the primary security gate; basic HTTPS validation guards against degenerate redirect URIs.\n if (!client_id) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"client_id is required\",\n });\n return;\n }\n if (!registeredClients.has(client_id)) {\n if (typeof redirect_uri === \"string\" && redirect_uri.startsWith(\"https://\")) {\n registeredClients.set(client_id, {\n client_id,\n redirect_uris: [redirect_uri],\n registeredAt: Date.now(),\n });\n process.stderr.write(`[authorize] auto-re-registered stale client_id after restart\\n`);\n } else {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"Unknown client_id and redirect_uri is not a valid https URL\",\n });\n return;\n }\n }\n\n const client = registeredClients.get(client_id)!;\n if (!client.redirect_uris.includes(redirect_uri)) {\n res.status(400).json({\n error: \"invalid_request\",\n error_description: \"redirect_uri does not match any registered redirect for this client\",\n });\n return;\n }\n\n // Validate key against Convex before issuing the code.\n // DEC-789 S2: probe primary then fallback URLs so DEV keys work against PROD Railway MCP.\n let workspaceName = \"Your Workspace\";\n try {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n const fallbackUrls = (process.env.CONVEX_FALLBACK_URLS ?? \"\")\n .split(\",\").map((u) => u.trim().replace(/\\/$/, \"\")).filter(Boolean);\n const candidates = [primaryUrl, ...fallbackUrls];\n\n let foundUrl: string | undefined;\n let anyDefinitiveReject = false;\n\n for (const url of candidates) {\n let checkData: { ok: boolean; workspaceName?: string; deploymentUrl?: string } | null = null;\n try {\n const checkRes = await fetch(`${url}/api/key-check`, {\n method: \"POST\",\n headers: { \"Authorization\": `Bearer ${api_key}`, \"Content-Type\": \"application/json\" },\n signal: AbortSignal.timeout(5000),\n });\n checkData = await checkRes.json() as { ok: boolean; workspaceName?: string; deploymentUrl?: string };\n } catch {\n // This candidate unreachable — try next.\n continue;\n }\n if (checkData.ok) {\n if (checkData.workspaceName) workspaceName = checkData.workspaceName;\n foundUrl = checkData.deploymentUrl ?? url;\n break;\n }\n anyDefinitiveReject = true;\n }\n\n if (!foundUrl) {\n if (anyDefinitiveReject) {\n sendError(\n \"Key not recognized\",\n \"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.\",\n 401,\n );\n return;\n }\n // All candidates unreachable (network errors only) — fail-open.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n } else {\n getKeyState(api_key).deploymentUrl = foundUrl;\n }\n } catch {\n // Outer guard: fail-open so auth works even if the entire block throws.\n process.stderr.write(\"[authorize] key-check unavailable — proceeding without validation\\n\");\n }\n\n const code = randomUUID();\n pendingCodes.set(code, {\n apiKey: api_key,\n codeChallenge: code_challenge,\n redirectUri: redirect_uri,\n expiresAt: Date.now() + 5 * 60_000,\n });\n\n const url = new URL(redirect_uri);\n url.searchParams.set(\"code\", code);\n if (state) url.searchParams.set(\"state\", state);\n const redirectUrl = url.toString();\n const providerName = providerDisplayName(client.client_name);\n\n if (wantsJson) {\n res.json({ ok: true, workspaceName, redirectUrl, providerName });\n } else {\n // No-JS path: server-rendered success page. User clicks the pill or the\n // Primary CTA + copy pill both complete the OAuth redirect — no auto-bounce.\n res.type(\"html\").send(authorizeSuccessPage(workspaceName, redirectUrl, providerName));\n }\n },\n);\n\n// ── OAuth: Token Exchange ────────────────────────────────────────────────\n// Step 5: Claude exchanges the authorization code (with PKCE verifier) for a token.\n// Supports both authorization_code and refresh_token grants.\n\nfunction issueTokens(apiKey: string): object {\n // Return the pb_sk_* key directly as the access_token so connections\n // survive server restarts. Railway deploys on every git push, wiping\n // in-memory Maps. pb_sk_* keys are long-lived (valid until explicitly\n // revoked in Cortex); actual validity is enforced by Convex per tool call.\n // resolveBearerAuth() already handles pb_sk_* with zero Map lookup.\n //\n // The refresh token is HMAC-signed and self-contained (TEN-1661, lib/\n // refresh-token.ts) so it also survives redeploys. No server-side store.\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1-year TTL: actual validity enforced by Convex, not by expiry clock.\n // Long TTL prevents unnecessary refresh cycles after restarts.\n expires_in: 365 * 24 * 3600,\n refresh_token: signRefreshToken(apiKey),\n };\n}\n\napp.post(\n \"/oauth/token\",\n authLimiter,\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri, refresh_token } =\n req.body;\n\n if (grant_type === \"refresh_token\") {\n const verified = verifyRefreshToken(refresh_token);\n if (!verified) {\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"Invalid or expired refresh token\",\n });\n return;\n }\n // Rotation = re-issuance. With stateless tokens there is no Map entry\n // to revoke; the new pair simply takes over from the next request.\n res.json(issueTokens(verified.apiKey));\n return;\n }\n\n if (grant_type !== \"authorization_code\") {\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n const pending = pendingCodes.get(code);\n if (!pending || pending.redirectUri !== redirect_uri) {\n res.status(400).json({ error: \"invalid_grant\" });\n return;\n }\n\n // PKCE S256 validation\n const challenge = createHash(\"sha256\")\n .update(code_verifier ?? \"\")\n .digest(\"base64url\");\n if (challenge !== pending.codeChallenge) {\n pendingCodes.delete(code);\n res.status(400).json({\n error: \"invalid_grant\",\n error_description: \"PKCE verification failed\",\n });\n return;\n }\n\n pendingCodes.delete(code);\n res.json(issueTokens(pending.apiKey));\n },\n);\n\n// ── Rate Limiting ────────────────────────────────────────────────────────\n\nconst mcpLimiter = rateLimit({\n windowMs: 60_000,\n max: 120,\n standardHeaders: true,\n legacyHeaders: false,\n handler: (req: any, res: any) => {\n const r = req.rateLimit;\n const reset = r?.resetTime?.getTime?.() ?? Date.now() + 60_000;\n send429(res, buildRateLimitError({\n retryAfterSeconds: Math.max(1, Math.ceil((reset - Date.now()) / 1000)),\n limit: r?.limit ?? 120,\n current: r?.used ?? 0,\n id: req.body?.id ?? null, // mirror the JSON-RPC id when a POST body is present\n }));\n },\n});\n\n// ── Auth Failure Backoff (Fix 5) ──────────────────────────────────────────\n// Per-IP progressive lockout for failed API key auth attempts to prevent\n// brute-force attacks through the MCP endpoints. Policy + Bearer classification\n// live in ./authLockout.ts (unit-tested); the state map stays here.\nconst authFailures = new Map<string, AuthFailureRecord>();\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n // Number of POST tool-call requests currently being handled. Incremented at\n // POST start, decremented in finally. A session with inFlight > 0 is never\n // torn by planKeyAdmission; the only exception is the evictStaleSessions TTL\n // backstop, which force-evicts past SESSION_TTL_MS to self-heal a leaked\n // counter (logged as reason=inflight_leak). GET SSE and DELETE do NOT touch\n // this — see design §3.1.\n inFlight: number;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Grace window: a session touched within this many ms is \"recently active\" and\n// is never evicted by the per-key planner (covers the gap between back-to-back\n// POST calls). See planKeyAdmission in sessionCap.ts.\nconst EVICT_GRACE_MS = 60_000;\n// Fix 6 — MAX_SESSIONS_PER_KEY is an in-process memory/working-set guard, NOT a\n// concurrency policy. Raising MCP_MAX_SESSIONS_PER_KEY does NOT grant more\n// concurrent agents — it only widens the idle-session reclaim window. It must\n// never be the thing that limits how many agents can work; fleet concurrency is\n// a kernel concern (TEN-2229), not a per-key surface cap.\n// Clamped to [1, MAX_SESSIONS - 1]: a value < 1 would make planKeyAdmission\n// throw on every init (RangeError); the MAX_SESSIONS - 1 ceiling guarantees a\n// single key can never alone fill the entire global pool (always leaves >= 1\n// slot), so a bad env degrades to a safe bound rather than breaking init or\n// disabling the cross-key fairness guard. Default 25 is far below this ceiling.\nconst MAX_SESSIONS_PER_KEY = Math.max(\n 1,\n Math.min(MAX_SESSIONS - 1, Number(process.env.MCP_MAX_SESSIONS_PER_KEY) || 25),\n);\n\n// The other eviction site is planKeyAdmission (sessionCap.ts), which runs\n// per-key on init. Keep both coherent — a change to one may affect the other.\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n // TTL branch: evict regardless of inFlight once past SESSION_TTL_MS. GET is\n // no longer inFlight-guarded after the round-3 fix, so an unconditional skip\n // on inFlight > 0 could make a session with a *leaked* counter immortal. The\n // hard-age backstop turns that leak into a self-healing, observable event.\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n // Force-evicting an inFlight > 0 session here means the counter leaked\n // (an unexpected throw outside the POST try/finally). Log it as such.\n logSessionLifecycle(\n \"session_deleted\",\n id,\n entry.inFlight > 0 ? \"inflight_leak\" : \"ttl\",\n );\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n // Global memory-pressure branch: SKIP inFlight > 0 so we can't drop a live\n // POST call mid-flight.\n const sorted = [...sessions.entries()]\n .filter(([, e]) => e.inFlight === 0)\n .sort((a, b) => a[1].lastAccess - b[1].lastAccess);\n const overflow = sessions.size - MAX_SESSIONS;\n for (let i = 0; i < Math.min(overflow, sorted.length); i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n // Observability: if every session is inFlight (sorted is empty / too short)\n // the loop above could not bring us under MAX_SESSIONS. We deliberately do\n // NOT tear live POSTs — the TTL backstop reclaims them at SESSION_TTL_MS —\n // but a sustained breach is worth surfacing rather than silently exceeding\n // the global memory ceiling.\n if (sessions.size > MAX_SESSIONS) {\n const ts = new Date().toISOString();\n process.stderr.write(\n `[HTTP] ${ts} session_cap_breach size=${sessions.size} max=${MAX_SESSIONS} ` +\n `reason=all_inflight (no idle sessions to evict; TTL backstop will reclaim)\\n`,\n );\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n// Bearer classification (absent vs. rejected vs. ok) lives in ./authLockout.ts\n// via resolveBearerAuth(); handlers call it directly so that only a\n// presented-and-rejected credential accrues toward the lockout (TEN-2606).\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\" | \"lru_evict\" | \"inflight_leak\" | \"init_rejected\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n if (checkAuthBlock(authFailures, reqIp, nowTs)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n // TEN-2606 — only a *presented-and-rejected* credential accrues toward the\n // lockout; an absent credential is the OAuth bootstrap probe and is never\n // recorded, so a legitimate re-bootstrap can never self-lock its IP.\n const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);\n if (auth.status !== \"ok\") {\n logRequest(\"POST\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey }, async () => {\n if (sessionId && sessions.has(sessionId)) {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n // inFlight guard (design §3.3): the increment is the FIRST statement\n // inside the try so nothing throwable sits between it and the guarded\n // region; finally decrements and re-stamps lastAccess so a long tool\n // call leaves a fresh stamp (never looks idle mid-flight).\n try {\n entry.inFlight++;\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", sessionId, Date.now() - reqStart);\n } finally {\n entry.inFlight--;\n entry.lastAccess = Date.now();\n }\n } else if (!sessionId && isInitializeRequest(req.body)) {\n // Fix 6 — per-key session cap with recency-guarded LRU eviction.\n const keyH = hashKey(apiKey);\n const plan = planKeyAdmission(sessions, keyH, MAX_SESSIONS_PER_KEY, Date.now(),\n { ttlMs: SESSION_TTL_MS, graceMs: EVICT_GRACE_MS });\n for (const sid of plan.evict) {\n const victim = sessions.get(sid);\n sessions.delete(sid); // delete before close; onclose guards on membership\n victim?.transport.close().catch(() => {});\n logSessionLifecycle(\"session_deleted\", sid, \"lru_evict\");\n }\n if (!plan.admit) {\n // poll-vs-deadline is decided by the planner from the same snapshot it\n // evicted against (plan.retryIsPoll) — no second scan / clock read here.\n send429(res, buildSessionCapError({\n retryAfterSeconds: plan.retryAfterSeconds,\n limit: MAX_SESSIONS_PER_KEY,\n current: countKeySessions(sessions, keyH),\n poll: plan.retryIsPoll,\n id: req.body?.id ?? null,\n }));\n return;\n }\n const sid = randomUUID();\n let initialized = false; // set true once the SDK confirms the session initialized\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => sid,\n onsessioninitialized: (s: string) => { initialized = true; logSessionLifecycle(\"session_created\", s); }, // log only — no map write\n });\n const reserved: SessionEntry = { transport, lastAccess: Date.now(), keyHash: keyH, inFlight: 1 };\n sessions.set(sid, reserved); // synchronous single-writer reservation (closes TOCTOU)\n transport.onclose = () => {\n const s = transport.sessionId ?? sid;\n if (sessions.has(s)) { sessions.delete(s); logSessionLifecycle(\"session_deleted\", s, \"onclose\"); }\n };\n try {\n const server = createProductBrainServer();\n await server.connect(transport);\n await transport.handleRequest(req, res, req.body);\n logRequest(\"POST\", \"ok\", transport.sessionId ?? undefined, Date.now() - reqStart);\n } catch (e) {\n transport.onclose = undefined as any; // detach → no phantom delete log on failure\n sessions.delete(sid);\n throw e;\n } finally {\n // handleRequest can reject an init-shaped POST with a 4xx WITHOUT throwing\n // (e.g. missing \"Accept: application/json, text/event-stream\", bad content\n // type, invalid batch init) — onsessioninitialized never fires and no\n // session id reaches the client, so the reservation would otherwise linger\n // until grace/TTL and let a broken/malicious client pin per-key slots.\n // Drop the orphan reservation immediately when init did not complete.\n if (!initialized) {\n transport.onclose = undefined as any;\n if (sessions.delete(sid)) logSessionLifecycle(\"session_deleted\", sid, \"init_rejected\");\n } else {\n const e = sessions.get(sid); if (e) { e.inFlight--; e.lastAccess = Date.now(); }\n }\n }\n } else if (sessionId) {\n // Stale Mcp-Session-Id (client cached one from before a Railway restart\n // or after TTL eviction). Per MCP Streamable HTTP spec § Session\n // Management clause 3, the server MUST respond with HTTP 404 Not Found\n // so the client (clause 4) re-initialises with a fresh InitializeRequest\n // without prompting OAuth re-auth. Returning 400 here was the root cause\n // of \"Claude.ai re-auths after every deploy\".\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale sessionId=${sessionId} (likely server restart — instructing client to re-initialise)\\n`,\n );\n res.status(404).json({\n jsonrpc: \"2.0\",\n error: { code: -32001, message: \"Session not found — re-initialise\" },\n id: null,\n });\n } else {\n // Non-initialise request with no Mcp-Session-Id — per spec clause 2,\n // servers that require a session ID SHOULD respond with HTTP 400.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_invalid no valid session ID (client may have omitted Mcp-Session-Id)\\n`,\n );\n res.status(400).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Bad Request: no valid session ID provided\" },\n id: null,\n });\n }\n });\n } catch (err: any) {\n logRequest(\"POST\", \"error\", sessionId, Date.now() - reqStart);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n }\n});\n\napp.get(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n if (checkAuthBlock(authFailures, reqIp, nowTs)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n // TEN-2606 — absent credential (OAuth bootstrap probe) is never recorded.\n const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);\n if (auth.status !== \"ok\") {\n logRequest(\"GET\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale GET sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n // NOT inFlight-guarded (design §3.1, round-3 review): the SSE\n // handleRequest stays open for the stream's whole life, so guarding it\n // would pin every active session → permanent 429 lockout. Re-stamp\n // lastAccess on stream open; the recency guard alone governs evictability.\n entry.lastAccess = Date.now();\n await entry.transport.handleRequest(req, res);\n logRequest(\"GET\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"GET\", \"error\", sessionId);\n }\n});\n\napp.delete(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n if (checkAuthBlock(authFailures, reqIp, nowTs)) {\n const rec = authFailures.get(reqIp);\n const retryAfterSeconds = rec ? Math.max(0, Math.ceil((rec.blockedUntil - nowTs) / 1000)) : 0;\n send429(res, buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec?.count ?? AUTH_FAILURE_MAX, id: req.body?.id ?? null }));\n return;\n }\n\n // TEN-2606 — absent credential (OAuth bootstrap probe) is never recorded.\n const auth = resolveBearerAuth(req.headers?.authorization, accessTokens, nowTs, ACCESS_TOKEN_TTL_MS);\n if (auth.status !== \"ok\") {\n logRequest(\"DELETE\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale DELETE sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey }, async () => {\n const entry = sessions.get(sessionId)!;\n // Fix 3 — Verify the session belongs to the presenting key.\n if (entry.keyHash !== hashKey(apiKey)) {\n res.status(403).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Session key mismatch\" },\n id: null,\n });\n return;\n }\n await entry.transport.handleRequest(req, res);\n logRequest(\"DELETE\", \"ok\", sessionId);\n });\n } catch {\n logRequest(\"DELETE\", \"error\", sessionId);\n }\n});\n\n// ── Start ───────────────────────────────────────────────────────────────\n\nprocess.on(\"unhandledRejection\", (reason) => {\n const msg = reason instanceof Error ? reason.message : String(reason);\n console.error(`[MCP HTTP] Unhandled rejection: ${msg}`);\n});\n\nprocess.on(\"uncaughtException\", (err) => {\n console.error(`[MCP HTTP] Uncaught exception: ${err.stack ?? err.message}`);\n gracefulShutdown();\n});\n\nlet shuttingDown = false;\nasync function gracefulShutdown() {\n if (shuttingDown) return;\n shuttingDown = true;\n setTimeout(() => process.exit(1), 3_000).unref();\n console.log(\"Shutting down...\");\n for (const [, entry] of sessions) {\n await entry.transport.close().catch(() => {});\n }\n try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n","// SSOT for the Product Brain APP logo (wordmark style).\n// Used by: src/lib/brand/AppLogo.svelte (Cortex UI) AND\n// packages/mcp-server (mirrored via prebuild) for /authorize and other agent surfaces.\n//\n// NOT used by marketing pages (they have their own brand treatment).\n//\n// To change the logo, edit ONLY this file. Both surfaces re-render automatically.\n\nexport type AppLogoSize = \"sm\" | \"md\";\n\nexport interface AppLogoOptions {\n size?: AppLogoSize;\n showWordmark?: boolean;\n className?: string;\n}\n\nconst SIZE_CLASSES: Record<AppLogoSize, string> = {\n sm: \"pb-logo--sm\",\n md: \"pb-logo--md\",\n};\n\n// Self-contained CSS for environments that can't load Svelte styles\n// (the MCP authorize page, embedded HTML responses, etc.).\n// Cortex UI consumers can rely on AppLogo.svelte's scoped styles instead;\n// the inline class hooks are designed not to collide.\nexport const appLogoStyles = `\n.pb-logo{display:inline-flex;align-items:center;gap:8px;color:inherit}\n.pb-logo__mark{\n border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);\n display:inline-grid;place-items:center;flex-shrink:0;\n}\n.pb-logo__core{border-radius:50%;background:var(--accent,#c9b99a)}\n.pb-logo__name{\n font-family:var(--font-mono,\"IBM Plex Mono\",ui-monospace,monospace);\n font-weight:500;text-transform:uppercase;\n letter-spacing:0.22em;color:var(--fg4,#6a6560);\n}\n.pb-logo--sm .pb-logo__mark{width:16px;height:16px}\n.pb-logo--sm .pb-logo__core{width:5px;height:5px}\n.pb-logo--sm .pb-logo__name{font-size:10.5px}\n.pb-logo--md .pb-logo__mark{width:24px;height:24px;border-radius:6px}\n.pb-logo--md .pb-logo__core{width:8px;height:8px}\n.pb-logo--md .pb-logo__name{font-size:13px}\n`;\n\nexport function appLogoMarkup(opts: AppLogoOptions = {}): string {\n const size = opts.size ?? \"sm\";\n const showWordmark = opts.showWordmark ?? true;\n const cls = [\"pb-logo\", SIZE_CLASSES[size], opts.className].filter(Boolean).join(\" \");\n const wordmark = showWordmark ? `<span class=\"pb-logo__name\">Product Brain</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\n}\n","/**\n * Stateless HMAC-signed OAuth refresh tokens.\n *\n * Why this exists (TEN-1661, DEC-783):\n * Railway redeploys on every git push, wiping in-memory Maps. The previous\n * `refreshTokens` Map made claude.ai's proactive refresh calls fail with\n * `invalid_grant`, forcing OAuth re-auth after every deploy. DEC-783 fixed\n * the access-token side by returning the underlying pb_sk_* key directly.\n * This module is the refresh-token equivalent: a self-contained,\n * HMAC-signed token format that needs no server-side state.\n *\n * Format: pb_rt_<base64url(payload)>.<base64url(hmac-sha256(payload))>\n * Payload: { k: apiKey, i: iat-ms, j: jti }\n *\n * The jti (j) field is reserved for future revocation/rotation work and is\n * not yet checked. Signature comparison uses crypto.timingSafeEqual.\n */\n\nimport { createHmac, randomBytes, randomUUID, timingSafeEqual } from \"node:crypto\";\n\nexport const REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60_000; // 90 days\nconst PREFIX = \"pb_rt_\";\n\ninterface RefreshPayload {\n k: string; // apiKey\n i: number; // iat (ms since epoch)\n j: string; // jti\n}\n\n// Read once at module load. In production we warn rather than crash so the\n// server keeps working — degrades gracefully to current Railway-lifetime\n// behaviour if the secret is missing.\nconst secret: Buffer = (() => {\n const fromEnv = process.env.MCP_REFRESH_SECRET;\n if (fromEnv && fromEnv.length > 0) return Buffer.from(fromEnv, \"utf8\");\n if (process.env.NODE_ENV === \"production\") {\n // eslint-disable-next-line no-console\n console.warn(\n \"[HTTP] WARNING MCP_REFRESH_SECRET not set — refresh tokens will not survive restart\",\n );\n }\n return randomBytes(32);\n})();\n\nfunction sign(payloadB64: string): Buffer {\n return createHmac(\"sha256\", secret).update(payloadB64).digest();\n}\n\nexport function signRefreshToken(apiKey: string): string {\n const payload: RefreshPayload = {\n k: apiKey,\n i: Date.now(),\n j: randomUUID(),\n };\n const payloadB64 = Buffer.from(JSON.stringify(payload), \"utf8\").toString(\"base64url\");\n const sigB64 = sign(payloadB64).toString(\"base64url\");\n return `${PREFIX}${payloadB64}.${sigB64}`;\n}\n\nexport function verifyRefreshToken(token: string): { apiKey: string } | null {\n if (typeof token !== \"string\" || !token.startsWith(PREFIX)) return null;\n const body = token.slice(PREFIX.length);\n const dot = body.indexOf(\".\");\n if (dot <= 0 || dot === body.length - 1) return null;\n const payloadB64 = body.slice(0, dot);\n const sigB64 = body.slice(dot + 1);\n\n let providedSig: Buffer;\n try {\n providedSig = Buffer.from(sigB64, \"base64url\");\n } catch {\n return null;\n }\n const expectedSig = sign(payloadB64);\n if (providedSig.length !== expectedSig.length) return null;\n if (!timingSafeEqual(providedSig, expectedSig)) return null;\n\n let payload: RefreshPayload;\n try {\n const json = Buffer.from(payloadB64, \"base64url\").toString(\"utf8\");\n payload = JSON.parse(json);\n } catch {\n return null;\n }\n if (\n !payload ||\n typeof payload.k !== \"string\" ||\n typeof payload.i !== \"number\" ||\n typeof payload.j !== \"string\"\n ) {\n return null;\n }\n if (Date.now() - payload.i > REFRESH_TOKEN_TTL_MS) return null;\n\n return { apiKey: payload.k };\n}\n","/**\n * sessionCap.ts — Pure, side-effect-free session cap planner.\n *\n * Used by http.ts init branch to decide which sessions to evict before\n * admitting a new one for the presenting API key.\n *\n * Two eviction sites in the codebase:\n * 1. planKeyAdmission (here) — per-key, on init, targets only the presenting key.\n * 2. evictStaleSessions (http.ts) — global TTL sweep, runs every 60 s.\n * Keep both coherent; a change to one may affect the other.\n */\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface CapSession {\n /** Unix-ms timestamp of last request start (or completion). */\n lastAccess: number;\n /** Hash of the API key that owns this session. */\n keyHash: string;\n /**\n * Number of POST tool-call requests currently being handled.\n * Incremented at POST start; decremented in finally.\n * GET SSE handlers and DELETE are NOT counted here — see design §3.1.\n */\n inFlight: number;\n}\n\nexport interface AdmissionPlan {\n /** IDs of sessions the caller should evict before admitting the new one. */\n evict: string[];\n /** True if the new session can be admitted after evictions. */\n admit: boolean;\n /**\n * Seconds the caller should suggest as a retry delay when admit=false.\n * 0 when admit=true.\n */\n retryAfterSeconds: number;\n /**\n * True when admit=false AND every blocking same-key session has inFlight > 0\n * — there is no grace deadline to wait out, so retryAfterSeconds is a fixed\n * poll-hint rather than a guarantee. The caller surfaces this as a \"retry\n * shortly\" message instead of a hard deadline. Always false when admit=true.\n * This is the single source of truth for poll-vs-deadline — callers must not\n * re-derive it from the live session map (avoids a second clock read).\n */\n retryIsPoll: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// planKeyAdmission\n// ---------------------------------------------------------------------------\n\n/**\n * Plan how the presenting key admits ONE new session under `cap`.\n *\n * Protected = inFlight > 0 OR (now - lastAccess) < graceMs. Never evicted.\n *\n * 1. Throw RangeError if cap < 1.\n * 2. Collect entries belonging to `keyHash`.\n * 3. Mark the key's TTL-expired-AND-not-protected sessions for eviction (G2).\n * (inFlight===0 && now-lastAccess >= ttlMs)\n * 4. If survivors (key ids not in evict) >= cap, additionally evict the key's\n * idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n * ascending by lastAccess (Map-insertion-order stable sort resolves ties)\n * until survivors < cap or none remain.\n * 5. admit = survivors.length < cap.\n * 6. retryAfterSeconds: 0 when admit; else computed from the oldest\n * grace-blocker (smallest lastAccess), or 5 if every blocker is inFlight\n * (retryIsPoll=true in that all-inFlight case).\n * 7. Return { evict, admit, retryAfterSeconds, retryIsPoll }.\n *\n * Only the presenting key's sessions are ever touched.\n * Pure; total over empty maps. Ties in lastAccess resolve by Map insertion order.\n */\nexport function planKeyAdmission(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n cap: number,\n now: number,\n opts: { ttlMs: number; graceMs: number },\n): AdmissionPlan {\n if (cap < 1) {\n throw new RangeError(`cap must be >= 1, got ${cap}`);\n }\n\n const { ttlMs, graceMs } = opts;\n\n // Helper: a session is \"protected\" if it has an active POST call OR was\n // recently accessed (within the grace window).\n const isProtected = (s: CapSession): boolean =>\n s.inFlight > 0 || now - s.lastAccess < graceMs;\n\n // Step 2: collect the presenting key's entries as an ordered array.\n // Map iteration order equals insertion order (ES2015+; V8 stable).\n const keyEntries: [string, CapSession][] = [];\n for (const [id, session] of sessions) {\n if (session.keyHash === keyHash) {\n keyEntries.push([id, session]);\n }\n }\n\n // Step 3: always evict TTL-expired AND non-protected sessions (G2).\n const evictSet = new Set<string>();\n for (const [id, session] of keyEntries) {\n if (!isProtected(session) && now - session.lastAccess >= ttlMs) {\n evictSet.add(id);\n }\n }\n\n // Step 4: if survivors (key sessions not in evict) >= cap, additionally\n // evict idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n // ascending by lastAccess until survivors < cap.\n let survivors = keyEntries.filter(([id]) => !evictSet.has(id));\n\n if (survivors.length >= cap) {\n // Collect idle (evictable) survivors, preserving insertion order for\n // stable sort — Array.sort is stable in V8 and spec-required ES2019+.\n const idleSurvivors = survivors\n .filter(([, s]) => !isProtected(s))\n // Ascending by lastAccess (oldest first); equal values keep insertion order.\n .sort(([, a], [, b]) => a.lastAccess - b.lastAccess);\n\n for (const [id] of idleSurvivors) {\n if (survivors.length < cap) break;\n evictSet.add(id);\n // Recompute survivors in-place without the evicted id.\n survivors = survivors.filter(([sid]) => sid !== id);\n }\n }\n\n const evict = Array.from(evictSet);\n\n // Step 5.\n const admit = survivors.length < cap;\n\n // Step 6.\n let retryAfterSeconds = 0;\n let retryIsPoll = false;\n if (!admit) {\n // Grace-blockers: surviving sessions with inFlight===0 (blocked by grace only).\n const graceBlockers = survivors.filter(([, s]) => s.inFlight === 0);\n if (graceBlockers.length > 0) {\n // Target the oldest (smallest lastAccess) — crosses graceMs soonest.\n const minLastAccess = Math.min(...graceBlockers.map(([, s]) => s.lastAccess));\n const msRemaining = graceMs - (now - minLastAccess);\n retryAfterSeconds = Math.max(1, Math.ceil(msRemaining / 1000));\n } else {\n // All blockers have inFlight > 0; completion time is unpredictable. The\n // 5 s value is a poll-hint fallback, not a guarantee — flag it so the\n // caller's 429 reads as \"retry shortly\" rather than a hard deadline.\n retryAfterSeconds = 5;\n retryIsPoll = true;\n }\n }\n\n return { evict, admit, retryAfterSeconds, retryIsPoll };\n}\n\n// ---------------------------------------------------------------------------\n// countKeySessions\n// ---------------------------------------------------------------------------\n\n/**\n * Count how many sessions in the map belong to `keyHash`.\n * Pure; returns 0 for an empty map.\n */\nexport function countKeySessions(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n): number {\n let count = 0;\n for (const session of sessions.values()) {\n if (session.keyHash === keyHash) count++;\n }\n return count;\n}\n","/**\n * httpErrors.ts — pure, import-safe builders for structured 429 responses.\n *\n * All three 429 sources (rate limit, auth lockout, session cap) return a\n * JSON-RPC envelope so the consuming MCP client can tell them apart.\n *\n * Rule — id mirrors the JSON-RPC request id when known\n * ---------------------------------------------------------------------------\n * JSON-RPC requires the response id to mirror the request id so the client can\n * correlate. All three call-sites run AFTER the global `express.json()`\n * middleware, so a POST body is already parsed and callers SHOULD thread\n * `req.body?.id` through `BuildParams.id` (the POST `/mcp` rate-limit,\n * auth-lockout, and session-cap gates all do). Requests with no parsable body\n * (GET/DELETE, or a malformed POST) have no id to mirror — those omit `id` and\n * it defaults to `null`, which is the correct JSON-RPC value for \"unknown id\".\n * ---------------------------------------------------------------------------\n */\n\nexport interface Built429 {\n status: 429;\n headers: Record<string, string>;\n body: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Clamp to non-negative, round to integer — used for both the Retry-After\n * header value and the `retryAfterSeconds` field in `data`.\n */\nfunction clampSeconds(s: number): number {\n return Math.max(0, Math.round(s));\n}\n\ntype Reason = \"rate_limited\" | \"auth_lockout\" | \"session_cap\";\n\n/** JSON-RPC request/response id. `null` is the spec value for \"unknown id\". */\ntype JsonRpcId = string | number | null;\n\ninterface BuildParams {\n retryAfterSeconds: number;\n limit: number;\n current: number;\n /** Request id to mirror; defaults to null when the caller has no body id. */\n id?: JsonRpcId;\n}\n\nfunction build(\n reason: Reason,\n message: string,\n p: BuildParams,\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return {\n status: 429,\n headers: { \"Retry-After\": String(seconds) },\n body: {\n jsonrpc: \"2.0\",\n id: p.id ?? null,\n error: {\n code: -32000,\n message,\n data: {\n reason,\n retryAfterSeconds: seconds,\n limit: p.limit,\n current: p.current,\n },\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public builders\n// ---------------------------------------------------------------------------\n\n/**\n * Per-IP rate-limit 429 (source: mcpLimiter middleware).\n * reason = \"rate_limited\"\n */\nexport function buildRateLimitError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"rate_limited\",\n `Too many requests — retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-IP auth-failure lockout 429 (source: checkAuthBlock).\n * reason = \"auth_lockout\"\n */\nexport function buildAuthLockoutError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"auth_lockout\",\n `Too many failed auth attempts — locked out, retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-key session-cap 429 (source: planKeyAdmission !admit fallback).\n * reason = \"session_cap\"\n *\n * @param p.poll - true when ALL blockers have inFlight > 0 (no grace deadline\n * can be predicted; the 5 s value is a fixed fallback, not a guarantee).\n * The message reads as a poll-hint rather than a hard deadline.\n * false (default) when at least one blocker is grace-protected; the\n * retryAfterSeconds is a real guarantee and the message names it.\n */\nexport function buildSessionCapError(\n p: BuildParams & { poll?: boolean },\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n const message = p.poll\n ? \"Server busy — all sessions active; retry shortly.\"\n : `Too many active sessions for this key — retry after ${seconds} s.`;\n return build(\"session_cap\", message, p);\n}\n\n// ---------------------------------------------------------------------------\n// Sender\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal structural shape of the response object send429 needs. Any\n * Express/Hono response satisfies it; the explicit interface (rather than\n * `any`) keeps a wrong argument from compiling and silently swallowing a 429.\n */\nexport interface Http429Response {\n headersSent: boolean;\n setHeader(name: string, value: string): unknown;\n status(code: number): { json(body: unknown): unknown };\n}\n\n/**\n * Write a Built429 to an Express/Hono-compatible response object.\n *\n * Guards `res.headersSent` first so calling this on an already-flushed\n * response is a safe no-op (e.g. when a middleware has already sent a body).\n */\nexport function send429(res: Http429Response, built: Built429): void {\n if (res.headersSent) return;\n for (const [k, v] of Object.entries(built.headers)) {\n res.setHeader(k, v);\n }\n res.status(built.status).json(built.body);\n}\n","// Per-IP progressive lockout for failed API-key auth attempts, plus the Bearer\n// credential classification that feeds it. Extracted from http.ts so the policy\n// is unit-testable in isolation (http.ts binds a live server on import).\n//\n// TEN-2606: the lockout must penalize only a *presented-and-rejected* Bearer\n// credential — never an *absent* one. Absent-credential /mcp calls are the\n// spec-mandated OAuth bootstrap probe (the client hits the resource with no\n// token to receive the 401 + WWW-Authenticate challenge). A client re-bootstraps\n// on every connect, and in a burst right after a Railway restart wipes the\n// in-memory token/session maps — so counting those probes as attacks self-locks\n// a legitimate IP. resolveBearerAuth() distinguishes the two; the handlers only\n// call recordAuthFailure() on the \"rejected\" branch.\n\nexport interface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\n// Minimal shape of an opaque access-token store entry (http.ts's accessTokens).\nexport interface AccessTokenLike {\n apiKey: string;\n createdAt: number;\n}\n\n// Outcome of classifying a request's Authorization header.\n// - \"ok\": a valid credential; apiKey is the resolved pb_sk_ key.\n// - \"absent\": no Bearer credential was presented — never penalized.\n// - \"rejected\": a Bearer credential was presented but is invalid — penalized.\nexport type BearerAuth =\n | { status: \"ok\"; apiKey: string }\n | { status: \"absent\" }\n | { status: \"rejected\" };\n\nexport const AUTH_FAILURE_MAX = 10;\nexport const AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nexport const AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nexport const MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\n/**\n * Classify a request's Authorization header into absent / rejected / ok.\n *\n * Supports both direct pb_sk_ API keys (stdio / backward compat) and opaque\n * pb_at_ OAuth access tokens issued by issueTokens(). Expired opaque tokens are\n * evicted from `accessTokens` as a side effect. `now` is injected (never\n * Date.now()) so callers and tests share a single clock.\n */\nexport function resolveBearerAuth(\n authorizationHeader: unknown,\n accessTokens: Map<string, AccessTokenLike>,\n now: number,\n accessTokenTtlMs: number,\n): BearerAuth {\n if (typeof authorizationHeader !== \"string\" || authorizationHeader.trim() === \"\") {\n // No Authorization header at all — the OAuth bootstrap probe. This is the\n // ONLY case that escapes the lockout: a client hitting the resource with no\n // credential to fetch the 401 + WWW-Authenticate challenge.\n return { status: \"absent\" };\n }\n if (!authorizationHeader.startsWith(\"Bearer \")) {\n // A credential of a wrong scheme (e.g. Basic) was presented — it can never\n // authenticate, so treat it as a rejected attempt that accrues toward the\n // lockout. Classifying it \"absent\" would let a caller bypass the guard by\n // always sending a non-Bearer header.\n return { status: \"rejected\" };\n }\n const token = authorizationHeader.slice(7).trim();\n if (token === \"\") {\n // \"Bearer \" scheme presented with an empty token — a presented, unusable\n // credential, not the (header-less) bootstrap probe.\n return { status: \"rejected\" };\n }\n\n if (token.startsWith(\"pb_sk_\")) {\n // Direct API key — accepted for stdio and backward compatibility.\n return { status: \"ok\", apiKey: token };\n }\n if (token.startsWith(\"pb_at_\")) {\n // Opaque OAuth access token — resolve to the underlying API key.\n const entry = accessTokens.get(token);\n if (!entry) return { status: \"rejected\" };\n if (now - entry.createdAt > accessTokenTtlMs) {\n // Expired — evict and reject.\n accessTokens.delete(token);\n return { status: \"rejected\" };\n }\n return { status: \"ok\", apiKey: entry.apiKey };\n }\n // A Bearer credential of unknown shape was presented — treat as a rejected\n // attempt so brute-forcing garbage tokens still accrues toward the lockout.\n return { status: \"rejected\" };\n}\n\n/** True if `ip` is currently within an active lockout block. */\nexport function checkAuthBlock(\n failures: Map<string, AuthFailureRecord>,\n ip: string,\n now: number,\n): boolean {\n const rec = failures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > now;\n}\n\n/**\n * Record one presented-and-rejected auth failure for `ip`. Callers MUST NOT\n * call this for an absent credential (TEN-2606). Tips the IP into a block once\n * AUTH_FAILURE_MAX failures land inside AUTH_FAILURE_WINDOW_MS; a failure past\n * the window resets the count rather than accumulating across windows.\n */\nexport function recordAuthFailure(\n failures: Map<string, AuthFailureRecord>,\n ip: string,\n now: number,\n): void {\n const rec = failures.get(ip);\n\n if (!rec) {\n failures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset the window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n"],"mappings":"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","names":["randomUUID","sessions","accessTokens","randomUUID","url","randomUUID","sid"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productbrain/mcp",
3
- "version": "0.0.1-beta.1774",
3
+ "version": "0.0.1-beta.1777",
4
4
  "description": "Product Brain — MCP server for AI-assisted product knowledge management",
5
5
  "type": "module",
6
6
  "engines": {