@productbrain/mcp 0.0.1-beta.11417 → 0.0.1-beta.11420

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/cli/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/cli/index.ts
4
4
  var subcommand = process.argv[2];
5
5
  if (subcommand === "setup") {
6
- const { runSetup } = await import("../setup-J7JZV6FC.js");
6
+ const { runSetup } = await import("../setup-MDXQKHPZ.js");
7
7
  await runSetup();
8
8
  } else {
9
9
  await import("../index.js");
package/dist/http.js CHANGED
@@ -241,7 +241,8 @@ function keyVerdictGate(verdict, failures, credentialId, now) {
241
241
  var DEFAULT_OAUTH_REDIRECT_URIS = [
242
242
  "https://claude.ai/api/mcp/auth_callback",
243
243
  "https://claude.com/api/mcp/auth_callback",
244
- "https://chatgpt.com/connector_platform_oauth_redirect"
244
+ "https://chatgpt.com/connector_platform_oauth_redirect",
245
+ "https://www.cursor.com/agents/mcp/oauth/callback"
245
246
  ];
246
247
  var CHATGPT_CONNECTOR_CALLBACK = /^https:\/\/chatgpt\.com\/connector\/oauth\/[A-Za-z0-9_-]+$/;
247
248
  var LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
@@ -307,15 +308,19 @@ function checkRedirectUri(redirectUri, allowlist) {
307
308
  return "redirect_uri scheme is not allowed (use https, or http on loopback)";
308
309
  }
309
310
  function checkRegistrationRedirects(redirectUris, allowlist) {
310
- if (!Array.isArray(redirectUris) || redirectUris.length === 0) return "redirect_uris is required";
311
+ const refused = (refusal) => ({ refusal, allowed: [], dropped: [] });
312
+ if (!Array.isArray(redirectUris) || redirectUris.length === 0) return refused("redirect_uris is required");
311
313
  if (redirectUris.length > MAX_REGISTRATION_REDIRECTS) {
312
- return `redirect_uris may hold at most ${MAX_REGISTRATION_REDIRECTS} entries`;
314
+ return refused(`redirect_uris may hold at most ${MAX_REGISTRATION_REDIRECTS} entries`);
313
315
  }
316
+ const allowed = [];
317
+ const dropped = [];
314
318
  for (const uri of redirectUris) {
315
319
  const reason = checkRedirectUri(uri, allowlist);
316
- if (reason) return reason;
320
+ if (reason) dropped.push({ uri, reason });
321
+ else allowed.push(uri);
317
322
  }
318
- return null;
323
+ return { refusal: allowed.length === 0 ? dropped[0].reason : null, allowed, dropped };
319
324
  }
320
325
 
321
326
  // src/lib/oauthAuthCode.ts
@@ -401,45 +406,39 @@ app.get("/.well-known/oauth-authorization-server", (req, res) => {
401
406
  scopes_supported: ["mcp:tools", "mcp:resources"]
402
407
  });
403
408
  });
409
+ function logOAuthDoor(door, status, reason, uris) {
410
+ const list = uris === void 0 ? "" : ` uris=${JSON.stringify(uris.map(String)).slice(0, 600)}`;
411
+ process.stderr.write(`[oauth] door=${door} status=${status} reason=${JSON.stringify(reason)}${list}
412
+ `);
413
+ }
404
414
  var authLimiter = rateLimit({
405
415
  windowMs: 6e4,
406
416
  max: 20,
407
417
  standardHeaders: true,
408
418
  legacyHeaders: false,
409
- message: { error: "Too many auth requests. Try again later." }
419
+ handler: (req, res) => {
420
+ logOAuthDoor(`${req.method} /authorize`, 429, "rate_limited_per_ip");
421
+ res.status(429).json({ error: "Too many auth requests. Try again later." });
422
+ }
410
423
  });
411
- var registeredClients = /* @__PURE__ */ new Map();
412
- var MAX_REGISTERED_CLIENTS = 500;
413
424
  app.post(
414
425
  "/register",
415
- authLimiter,
416
426
  express.json(),
417
427
  (req, res) => {
418
- if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {
419
- res.status(503).json({
420
- error: "server_error",
421
- error_description: "Registration limit reached. Try again later."
422
- });
423
- return;
424
- }
425
- const { redirect_uris, client_name } = req.body;
426
- const refusal = checkRegistrationRedirects(redirect_uris, OAUTH_REDIRECT_ALLOWLIST);
428
+ const { redirect_uris, client_name } = req.body ?? {};
429
+ const { refusal, allowed, dropped } = checkRegistrationRedirects(redirect_uris, OAUTH_REDIRECT_ALLOWLIST);
427
430
  if (refusal) {
431
+ logOAuthDoor("POST /register", 400, refusal, Array.isArray(redirect_uris) ? redirect_uris : [redirect_uris]);
428
432
  res.status(400).json({ error: "invalid_redirect_uri", error_description: refusal });
429
433
  return;
430
434
  }
431
435
  const clientId = `pb_client_${randomUUID()}`;
432
- const client = {
433
- client_id: clientId,
434
- redirect_uris,
435
- client_name,
436
- registeredAt: Date.now()
437
- };
438
- registeredClients.set(clientId, client);
436
+ const droppedUris = dropped.length > 0 ? dropped.map((d) => d.uri) : void 0;
437
+ logOAuthDoor("POST /register", 201, droppedUris ? "dropped_redirects" : "registered", droppedUris);
439
438
  res.status(201).json({
440
439
  client_id: clientId,
441
440
  client_name: client_name ?? "MCP Client",
442
- redirect_uris,
441
+ redirect_uris: allowed,
443
442
  grant_types: ["authorization_code"],
444
443
  response_types: ["code"],
445
444
  token_endpoint_auth_method: "none"
@@ -452,9 +451,6 @@ setInterval(() => {
452
451
  for (const [code, auth] of pendingCodes) {
453
452
  if (now > auth.expiresAt) pendingCodes.delete(code);
454
453
  }
455
- for (const [id, client] of registeredClients) {
456
- if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
457
- }
458
454
  for (const [credentialId, rec] of authFailures) {
459
455
  if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
460
456
  authFailures.delete(credentialId);
@@ -778,11 +774,8 @@ a.btn-primary.success-cta{color:var(--btn-fg);text-decoration:none}
778
774
  <div class="stage">${bodyContent}</div>
779
775
  </body></html>`;
780
776
  }
781
- function providerDisplayName(clientName) {
782
- const name = (clientName ?? "").trim();
783
- if (!name) return "your assistant";
784
- return name.length > 40 ? name.slice(0, 40) + "\u2026" : name;
785
- }
777
+ var PROVIDER_NAME = "your assistant";
778
+ var START_SENTENCE = "Start my Product Brain session.";
786
779
  function successPanelInner(workspaceName, redirectUrl, providerName) {
787
780
  return `
788
781
  <div class="orb-wrap">
@@ -805,8 +798,8 @@ function successPanelInner(workspaceName, redirectUrl, providerName) {
805
798
  <h1 class="ok-title">Product Brain is Live</h1>
806
799
  <p class="ok-lead">Return to your assistant, then say</p>
807
800
  <div class="ok-phrase-row">
808
- <button class="cmd" type="button" data-cmd-pill data-redirect="${esc(redirectUrl)}" aria-label="Copy &quot;Start PB&quot; and return to ${esc(providerName)}">
809
- <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>
801
+ <button class="cmd" type="button" data-cmd-pill data-redirect="${esc(redirectUrl)}" aria-label="Copy &quot;${esc(START_SENTENCE)}&quot; and return to ${esc(providerName)}">
802
+ <span class="cmd-quote-part" aria-hidden="true">&ldquo;</span><span data-cmd-text>${esc(START_SENTENCE)}</span><span class="cmd-quote-part" aria-hidden="true">&rdquo;</span>
810
803
  <span class="cmd-icon" aria-hidden="true">
811
804
  <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>
812
805
  </span>
@@ -850,7 +843,7 @@ var cmdScript = `
850
843
  };
851
844
  try{
852
845
  if(navigator.clipboard&&navigator.clipboard.writeText){
853
- navigator.clipboard.writeText('Start PB').then(done,done);
846
+ navigator.clipboard.writeText(${JSON.stringify(START_SENTENCE)}).then(done,done);
854
847
  }else{done()}
855
848
  }catch(e){done()}
856
849
  });
@@ -860,7 +853,7 @@ var cmdScript = `
860
853
  `;
861
854
  function authorizeFormPage(params) {
862
855
  const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;
863
- const providerName = providerDisplayName(params.client_name);
856
+ const providerName = PROVIDER_NAME;
864
857
  const body = `
865
858
  <!-- \u2500\u2500\u2500 CONNECT \u2500\u2500\u2500 -->
866
859
  <div class="panel" id="p-connect" data-state="connect">
@@ -1019,18 +1012,16 @@ app.get("/authorize", authLimiter, (req, res) => {
1019
1012
  const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.query;
1020
1013
  const refusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST);
1021
1014
  if (refusal) {
1015
+ logOAuthDoor("GET /authorize", 400, refusal);
1022
1016
  res.status(400).json({ error: "invalid_request", error_description: refusal });
1023
1017
  return;
1024
1018
  }
1025
- const cid = String(client_id ?? "");
1026
- const clientName = cid && registeredClients.has(cid) ? registeredClients.get(cid).client_name : void 0;
1027
1019
  res.type("html").send(authorizeFormPage({
1028
1020
  redirect_uri: String(redirect_uri ?? ""),
1029
1021
  code_challenge: String(code_challenge ?? ""),
1030
1022
  code_challenge_method: String(code_challenge_method ?? "S256"),
1031
1023
  state: String(state ?? ""),
1032
- client_id: cid,
1033
- client_name: clientName
1024
+ client_id: String(client_id ?? "")
1034
1025
  }));
1035
1026
  });
1036
1027
  app.post(
@@ -1050,6 +1041,7 @@ app.post(
1050
1041
  }).toString();
1051
1042
  const retryUrl = `/authorize?${retryParams}`;
1052
1043
  function sendError(title, trustedDetailHtml, status = 400) {
1044
+ logOAuthDoor("POST /authorize", status, title);
1053
1045
  if (wantsJson) {
1054
1046
  res.status(status).json({ ok: false, title, detail: trustedDetailHtml });
1055
1047
  } else {
@@ -1075,31 +1067,16 @@ app.post(
1075
1067
  return;
1076
1068
  }
1077
1069
  if (!client_id) {
1070
+ logOAuthDoor("POST /authorize", 400, "client_id is required");
1078
1071
  res.status(400).json({ error: "invalid_request", error_description: "client_id is required" });
1079
1072
  return;
1080
1073
  }
1081
1074
  const redirectRefusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST);
1082
1075
  if (redirectRefusal) {
1076
+ logOAuthDoor("POST /authorize", 400, redirectRefusal);
1083
1077
  res.status(400).json({ error: "invalid_request", error_description: redirectRefusal });
1084
1078
  return;
1085
1079
  }
1086
- if (!registeredClients.has(client_id)) {
1087
- if (registeredClients.size >= MAX_REGISTERED_CLIENTS) {
1088
- res.status(503).json({ error: "server_error", error_description: "Registration limit reached. Try again later." });
1089
- return;
1090
- }
1091
- registeredClients.set(client_id, { client_id, redirect_uris: [redirect_uri], registeredAt: Date.now() });
1092
- process.stderr.write(`[authorize] auto-re-registered stale client_id after restart
1093
- `);
1094
- }
1095
- const client = registeredClients.get(client_id);
1096
- if (!client.redirect_uris.includes(redirect_uri)) {
1097
- res.status(400).json({
1098
- error: "invalid_request",
1099
- error_description: "redirect_uri does not match any registered redirect for this client"
1100
- });
1101
- return;
1102
- }
1103
1080
  let workspaceName = "Your Workspace";
1104
1081
  const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5e3);
1105
1082
  const gate = keyVerdictGate(check.verdict, authFailures, pasted.credentialId, nowTs);
@@ -1130,7 +1107,8 @@ app.post(
1130
1107
  url.searchParams.set("code", code);
1131
1108
  if (state) url.searchParams.set("state", state);
1132
1109
  const redirectUrl = url.toString();
1133
- const providerName = providerDisplayName(client.client_name);
1110
+ const providerName = PROVIDER_NAME;
1111
+ logOAuthDoor("POST /authorize", 200, "code_issued");
1134
1112
  if (wantsJson) {
1135
1113
  res.json({ ok: true, workspaceName, redirectUrl, providerName });
1136
1114
  } else {
@@ -1140,12 +1118,12 @@ app.post(
1140
1118
  );
1141
1119
  app.post(
1142
1120
  "/oauth/token",
1143
- authLimiter,
1144
1121
  express.urlencoded({ extended: false }),
1145
1122
  express.json(),
1146
1123
  (req, res) => {
1147
- const { grant_type, code, code_verifier, redirect_uri } = req.body;
1124
+ const { grant_type, code, code_verifier, redirect_uri } = req.body ?? {};
1148
1125
  if (grant_type !== "authorization_code") {
1126
+ logOAuthDoor("POST /oauth/token", 400, "unsupported_grant_type");
1149
1127
  res.status(400).json({ error: "unsupported_grant_type" });
1150
1128
  return;
1151
1129
  }
@@ -1153,16 +1131,19 @@ app.post(
1153
1131
  const codeRefusal = pendingCodeRefusal(pending, redirect_uri, Date.now());
1154
1132
  if (codeRefusal === "expired") {
1155
1133
  pendingCodes.delete(code);
1134
+ logOAuthDoor("POST /oauth/token", 400, "expired");
1156
1135
  res.status(400).json({ error: "invalid_grant", error_description: "authorization code expired" });
1157
1136
  return;
1158
1137
  }
1159
1138
  if (codeRefusal || !pending) {
1139
+ logOAuthDoor("POST /oauth/token", 400, codeRefusal ?? "unknown_code");
1160
1140
  res.status(400).json({ error: "invalid_grant" });
1161
1141
  return;
1162
1142
  }
1163
1143
  const challenge = createHash("sha256").update(code_verifier ?? "").digest("base64url");
1164
1144
  if (challenge !== pending.codeChallenge) {
1165
1145
  pendingCodes.delete(code);
1146
+ logOAuthDoor("POST /oauth/token", 400, "pkce_failed");
1166
1147
  res.status(400).json({
1167
1148
  error: "invalid_grant",
1168
1149
  error_description: "PKCE verification failed"
@@ -1170,6 +1151,7 @@ app.post(
1170
1151
  return;
1171
1152
  }
1172
1153
  pendingCodes.delete(code);
1154
+ logOAuthDoor("POST /oauth/token", 200, "token_issued");
1173
1155
  res.json(tokenResponse(pending.apiKey));
1174
1156
  }
1175
1157
  );
package/dist/http.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/http.ts","../src/brand/logo-markup.ts","../src/sessionCap.ts","../src/httpErrors.ts","../src/authLockout.ts","../src/lib/oauthRedirectAllowlist.ts","../src/lib/oauthAuthCode.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: https://claude.ai)\n * OAUTH_REDIRECT_HOST_ALLOWLIST — Extra exact https redirect URIs, comma-separated full URIs (see lib/oauthRedirectAllowlist.ts)\n *\n * WP-757 TASK-224 F2 (DEC-1722): the PB_MODULES env var and its one module (gitchain: the\n * `chain` / `chain-review` tools) are deleted along with gitchain itself — the tool surface\n * is flat and always registered now, no opt-in module left to gate.\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, forgetStreamLifecycle } from \"./client.js\"; import { apiKeysUrl } from \"./lib/appUrl.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { appLogoMarkup, appLogoStyles } from \"./brand/logo-markup.js\";\nimport { planKeyAdmission, countKeySessions } from \"./sessionCap.js\";\nimport { send429, buildRateLimitError, buildSessionCapError } from \"./httpErrors.js\";\nimport { AUTH_FAILURE_WINDOW_MS, authLockoutError, recordAuthFailure } from \"./authLockout.js\";\nimport { isCrossSiteFormPost, resolveBearerAuth, resolvePastedKey, keyVerdictGate, type AuthFailureRecord } from \"./authLockout.js\";\nimport { checkRedirectUri, checkRegistrationRedirects, resolveOAuthRedirectAllowlist } from \"./lib/oauthRedirectAllowlist.js\";\nimport { normalisePastedKey, pendingCodeRefusal, tokenResponse, type PendingAuth } from \"./lib/oauthAuthCode.js\";\nimport { checkKeyAgainstCandidates, parseFallbackUrls } from \"./lib/deploymentUrlResolver.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\n/** DEC-789 S2: the deployments a key may live on — primary first, then CONVEX_FALLBACK_URLS. */\nfunction keyCheckCandidates(): string[] {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n return [primaryUrl, ...parseFallbackUrls(process.env.CONVEX_FALLBACK_URLS).map((u) => u.replace(/\\/$/, \"\"))];\n}\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\n// The API key IS the access token, so every redirect_uri is vetted (lib/oauthRedirectAllowlist.ts).\nconst OAUTH_REDIRECT_ALLOWLIST = resolveOAuthRedirectAllowlist({ explicitAllowlist: process.env.OAUTH_REDIRECT_HOST_ALLOWLIST });\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\"], // TEN-4548: no refresh grant (lib/oauthAuthCode.ts tokenResponse)\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 const refusal = checkRegistrationRedirects(redirect_uris, OAUTH_REDIRECT_ALLOWLIST);\n if (refusal) {\n res.status(400).json({ error: \"invalid_redirect_uri\", error_description: refusal });\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\nconst pendingCodes = new Map<string, PendingAuth>();\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 5 — Clean up stale auth failure tracking entries (the size cap is enforced at insert, recordAuthFailure).\n for (const [credentialId, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(credentialId);\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=\"${esc(apiKeysUrl())}\" 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=\"${esc(apiKeysUrl())}\" 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 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 refusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST); // never render the key form for a bad redirect\n if (refusal) { res.status(400).json({ error: \"invalid_request\", error_description: refusal }); return; }\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 { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n const api_key = normalisePastedKey(req.body.api_key); // TASK-668: once here; every later read, pendingCodes too, uses it\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 // A POST from another site's page is refused before any key is read, so no page can drive the form, the lockout\n // map or the kernel through a visitor's browser (isCrossSiteFormPost; own origin = baseUrl).\n if (isCrossSiteFormPost(req.headers[\"origin\"], req.headers[\"sec-fetch-site\"], baseUrl(req))) {\n sendError(\"Request refused\", \"This form only accepts keys sent from its own sign-in page. Open the connect link again.\", 403);\n return;\n }\n // TEN-4601 / TEN-4673: the form is a door a key is presented at, so it shares /mcp's lockout map, keyed on the\n // presented key's hash. A locked key is refused next; a malformed or kernel-rejected key accrues to its own\n // record; an empty field never does (TEN-2606).\n const nowTs = Date.now();\n const pasted = resolvePastedKey(api_key);\n const lockout = authLockoutError(authFailures, pasted, nowTs);\n if (lockout) {\n const retryAfter = lockout.headers[\"Retry-After\"];\n res.setHeader(\"Retry-After\", retryAfter);\n sendError(\"Too many attempts\", `This key was not recognized too many times. Try again in ${retryAfter}s.`, 429);\n return;\n }\n if (pasted.status !== \"ok\") {\n if (pasted.status === \"rejected\") recordAuthFailure(authFailures, pasted.credentialId, nowTs);\n sendError(\"Invalid key format\", \"API keys start with <code>pb_sk_</code>. Check your key and try again.\");\n return;\n }\n\n // The redirect_uri must pass the host allowlist (also for an already-registered client, so a\n // URI stored before the policy or by a stale client is refused) AND match the client's registered\n // URIs. An unknown client_id (server restart wiped the in-memory Map) is auto-re-registered, but\n // only with an allowlisted redirect_uri: the API key is the access token, so an unvetted redirect\n // would hand it to whoever owns that host.\n if (!client_id) {\n res.status(400).json({ error: \"invalid_request\", error_description: \"client_id is required\" });\n return;\n }\n const redirectRefusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST);\n if (redirectRefusal) {\n res.status(400).json({ error: \"invalid_request\", error_description: redirectRefusal });\n return;\n }\n if (!registeredClients.has(client_id)) {\n if (registeredClients.size >= MAX_REGISTERED_CLIENTS) { res.status(503).json({ error: \"server_error\", error_description: \"Registration limit reached. Try again later.\" }); return; }\n registeredClients.set(client_id, { client_id, redirect_uris: [redirect_uri], registeredAt: Date.now() });\n process.stderr.write(`[authorize] auto-re-registered stale client_id after restart\\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 const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5000);\n const gate = keyVerdictGate(check.verdict, authFailures, pasted.credentialId, nowTs); // a rejected key accrues; an unreachable kernel never does\n if (check.verdict === \"ok\") {\n if (check.workspaceName) workspaceName = check.workspaceName;\n getKeyState(api_key).deploymentUrl = check.deploymentUrl;\n } else if (check.verdict === \"rate_limited\") {\n // The key IS valid but its workspace is over its request budget: \"wait\", not \"check your key\".\n sendError(\n \"Rate limited\",\n `This API key is valid but temporarily rate limited. Try again in ${check.retryAfterSeconds ?? 60}s.`,\n 429,\n );\n return;\n } else if (gate === \"unauthorized\") {\n sendError(\"Key not recognized\", \"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.\", 401);\n return;\n } else {\n // No definitive verdict (a candidate down or off-contract): fail-open here, because initialize re-checks the key (TEN-4549).\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// authorization_code is the only grant; the access token is the pb_sk_ key itself, so it\n// survives Railway redeploys with no server-side store (tokenResponse, TEN-4548).\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 } = req.body;\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 const codeRefusal = pendingCodeRefusal(pending, redirect_uri, Date.now());\n if (codeRefusal === \"expired\") {\n pendingCodes.delete(code);\n res.status(400).json({ error: \"invalid_grant\", error_description: \"authorization code expired\" });\n return;\n }\n if (codeRefusal || !pending) {\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(tokenResponse(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// Progressive lockout for failed API key attempts, keyed on the presented key's hash (TEN-4673), on every\n// door that takes a key: /mcp and the sign-in form (POST /authorize) share this one map (TEN-4601). Policy\n// + key classification 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 / TEN-4673 — refuse a presented key whose hash has exceeded the auth failure threshold.\n // TEN-2606 — only a *presented-and-rejected* credential accrues, and an absent one (the OAuth\n // bootstrap probe) is never recorded nor refused, so a locked key can always re-sign-in.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"POST\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 // TEN-4549: ask the kernel once whether this key is real before any session, eviction or\n // server instance exists for it. A rejected key accrues to its own lockout record like any\n // other presented-and-rejected credential; nothing caches the verdict past this request.\n const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5000);\n const gate = keyVerdictGate(check.verdict, authFailures, auth.credentialId, Date.now());\n if (gate === \"unauthorized\") {\n logRequest(\"POST\", \"auth_fail\");\n send401(req, res);\n return;\n }\n if (gate === \"unavailable\") {\n res.status(503).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Product Brain could not verify this API key right now. Retry shortly.\" },\n id: req.body?.id ?? null,\n });\n return;\n }\n if (check.verdict === \"ok\" || check.verdict === \"rate_limited\") getKeyState(apiKey).deploymentUrl = check.deploymentUrl;\n\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; forgetStreamLifecycle(keyH, s); // TASK-583 (WP-801, DEC-1907): the stream's session lifecycle leaves with it\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 / TEN-4673 — refuse a locked presented key; TEN-2606 — an absent credential is never recorded nor refused.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"GET\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 / TEN-4673 — refuse a locked presented key; TEN-2606 — an absent credential is never recorded nor refused.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"DELETE\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 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: Error) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n","// GENERATED by scripts/generate-mcp-embedded-copies.mjs from src/lib/brand/logo-markup.ts.\n// DO NOT EDIT. Run `npm run mcp:embedded-copies` to regenerate.\n// WP-488 Slice 3 (FEAT-1377) / WP-480 S4: Railway's isolated Docker build for\n// packages/mcp-server cannot see the repo-root src/ — this file must physically\n// exist here. See scripts/generate-mcp-embedded-copies.mjs for the full rationale.\n\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\n// The product's name lives in the kernel so the web app, the invite email and MCP read one constant\n// (TASK-797; TEN-4824; RULE-664). The MCP copy's import is rewritten by the generator.\nimport { PRODUCT_NAME } from \"../lib/platformGlossary.js\";\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_NAME}</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\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-presented-key auth-failure lockout 429 (source: authLockoutError in authLockout.ts, TEN-4673).\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","// Progressive lockout for failed API-key auth attempts, keyed on the hash of the PRESENTED\n// credential, plus the classification that feeds it. Extracted from http.ts so the policy is\n// unit-testable in isolation (http.ts binds a live server on import).\n//\n// TEN-4673 (WP-827, TASK-698): the counter keys on hashKey(presented credential), the thing being\n// guessed, never the raw key and never the client IP. claude.ai and other hosted clients reach\n// /mcp from shared egress addresses, so a per-IP counter let ten bad keys from one stale connector\n// lock every user of that platform out. Now ten rejected presentations of key A lock key A on every\n// door and from every address, and a valid key B from the same address keeps working. No path\n// counts per IP: an absent credential never accrues (TEN-2606) and the form's cross-site refusal\n// answers 403 before any key is read. The lockout is defence in depth and load shedding; the\n// 128-bit key itself is the guard against guessing.\n//\n// TEN-2606: the lockout penalizes only a *presented-and-rejected* credential, never an *absent*\n// one, and never answers 429 to one. Absent-credential /mcp calls are the spec-mandated OAuth\n// bootstrap probe (the client hits the resource with no token to receive the 401 +\n// WWW-Authenticate challenge), so a client whose key is locked can always start a fresh sign-in.\n//\n// TEN-4549: the shape check alone let any `pb_sk_<anything>` open an MCP session without ever\n// accruing toward the lockout. keyVerdictGate() is where the kernel's verdict on the key lands:\n// a key the kernel rejects is a presented-and-rejected credential like any other.\n//\n// TEN-4601 (TASK-684): the lockout guards every door a key is presented at, /mcp (POST, GET,\n// DELETE) and the connector's sign-in form (POST /authorize), over ONE map in http.ts, so a key\n// rejected on one door is locked on the other. resolvePastedKey() is the form's sibling of\n// resolveBearerAuth(); both hash the key string itself, so the two doors share one record per key.\n\nimport { hashKey } from \"./auth.js\";\nimport type { KeyCheckResult } from \"./lib/deploymentUrlResolver.js\";\nimport { buildAuthLockoutError, type Built429 } from \"./httpErrors.js\";\n\nexport interface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\n// Outcome of classifying a presented credential. `credentialId` is hashKey() of what was\n// presented (RULE-232: the map never holds a raw key) and is the lockout map's key.\n// - \"ok\": a pb_sk_-shaped key; initialize and the sign-in form ask the kernel whether it is real.\n// - \"absent\": no credential was presented: never penalized, never locked out.\n// - \"rejected\": a credential was presented but can never authenticate: penalized.\nexport type BearerAuth =\n | { status: \"ok\"; apiKey: string; credentialId: string }\n | { status: \"absent\" }\n | { status: \"rejected\"; credentialId: string };\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\nfunction rejected(presented: string): BearerAuth {\n return { status: \"rejected\", credentialId: hashKey(presented) };\n}\n\n/**\n * Classify a request's Authorization header into absent / rejected / ok.\n *\n * The only credential is a pb_sk_ API key: the OAuth token door hands the key itself back as\n * the access token (DEC-7, lib/oauthAuthCode.ts tokenResponse; TEN-4548 deleted the never-written\n * pb_at_ opaque-token store).\n */\nexport function resolveBearerAuth(authorizationHeader: unknown): BearerAuth {\n if (typeof authorizationHeader !== \"string\" || authorizationHeader.trim() === \"\") {\n // No Authorization header at all: the OAuth bootstrap probe. This is the ONLY case that\n // escapes the lockout: a client hitting the resource with no credential to fetch the\n // 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 authenticate, so it\n // is a rejected attempt; classifying it \"absent\" would let a caller bypass the guard.\n return rejected(authorizationHeader.trim());\n }\n const token = authorizationHeader.slice(7).trim();\n // \"Bearer \" with an empty token is a presented, unusable credential, not the header-less probe;\n // a token of unknown shape is garbage that was still presented.\n if (!token.startsWith(\"pb_sk_\")) return rejected(token);\n return { status: \"ok\", apiKey: token, credentialId: hashKey(token) };\n}\n\n/**\n * Classify the key pasted into the sign-in form (POST /authorize, already normalisePastedKey'd).\n * An empty field is absent: nothing was presented, never penalized (TEN-2606). Any other text that\n * is not pb_sk_-shaped is a presented key that can never authenticate, so it accrues like a\n * kernel-rejected one; a pb_sk_-shaped key goes to the kernel (keyVerdictGate).\n */\nexport function resolvePastedKey(apiKey: string): BearerAuth {\n if (apiKey === \"\") return { status: \"absent\" };\n if (!apiKey.startsWith(\"pb_sk_\")) return rejected(apiKey);\n return { status: \"ok\", apiKey, credentialId: hashKey(apiKey) };\n}\n\n/**\n * True when a sign-in form POST came from another site's page. The form is a cookie-less urlencoded\n * POST, which any page can make a visitor's browser send; the refusal keeps such a page from\n * driving the form, the failure map or the kernel at all. /mcp is not exposed: its key rides the\n * Authorization header, which a cross-site simple request cannot set.\n * - Sec-Fetch-Site \"cross-site\" → refused; \"same-origin\", \"same-site\", \"none\" or absent → not by this.\n * - Origin present and not `ownOrigin` (the server's own base, as the OAuth metadata advertises it;\n * an unparseable or \"null\" Origin is not ours) → refused. No Origin (curl, non-browser) → allowed.\n */\nexport function isCrossSiteFormPost(origin: unknown, secFetchSite: unknown, ownOrigin: string): boolean {\n if (secFetchSite === \"cross-site\") return true;\n if (typeof origin !== \"string\" || origin === \"\") return false;\n try {\n return new URL(origin).origin !== new URL(ownOrigin).origin;\n } catch {\n return true;\n }\n}\n\n/** True if `credentialId` is currently within an active lockout block. */\nexport function checkAuthBlock(\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): boolean {\n const rec = failures.get(credentialId);\n if (!rec) return false;\n return rec.blockedUntil > now;\n}\n\n/**\n * The lockout 429 for the presented credential (Retry-After = seconds left in the block), or null\n * when it is not blocked or no credential was presented (TEN-2606: the bootstrap probe is never\n * refused). Every door that takes a key asks this first: /mcp sends it as is; the sign-in form\n * reuses its Retry-After on its own error page. `id` mirrors the JSON-RPC request id when known.\n */\nexport function authLockoutError(\n failures: Map<string, AuthFailureRecord>,\n auth: BearerAuth,\n now: number,\n id: string | number | null = null,\n): Built429 | null {\n if (auth.status === \"absent\") return null;\n const rec = failures.get(auth.credentialId);\n if (!rec || !checkAuthBlock(failures, auth.credentialId, now)) return null;\n const retryAfterSeconds = Math.max(0, Math.ceil((rec.blockedUntil - now) / 1000));\n return buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec.count, id });\n}\n\n/**\n * Record one presented-and-rejected auth failure for `credentialId`. Callers MUST NOT call this\n * for an absent credential (TEN-2606). Tips the credential into a block once AUTH_FAILURE_MAX\n * failures land inside AUTH_FAILURE_WINDOW_MS; a failure past the window resets the count rather\n * than accumulating across windows. The map holds at most MAX_AUTH_FAILURE_ENTRIES records: a new\n * credential past the cap evicts the oldest-inserted one (a memory bound, not a guard; see header).\n */\nexport function recordAuthFailure(\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): void {\n const rec = failures.get(credentialId);\n\n if (!rec) {\n if (failures.size >= MAX_AUTH_FAILURE_ENTRIES) {\n const oldest = failures.keys().next().value;\n if (oldest !== undefined) failures.delete(oldest);\n }\n failures.set(credentialId, { 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\n/**\n * TEN-4549 / TEN-4601: what a door does with the kernel's verdict on a presented pb_sk_ key\n * (lib/deploymentUrlResolver.ts checkKeyAgainstCandidates, asked once per MCP initialize and once\n * per sign-in form submit; no cache of accepted keys outlives the session).\n * - ok / rate_limited → \"admit\": the key is real (a throttled workspace is still a real key).\n * - rejected → \"unauthorized\", and the failure accrues to that key's lockout record.\n * - unreachable → \"unavailable\": never penalize the caller for the kernel being down\n * (initialize fails closed with no session; the form proceeds, because\n * initialize re-checks the key).\n */\nexport function keyVerdictGate(\n verdict: KeyCheckResult[\"verdict\"],\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): \"admit\" | \"unauthorized\" | \"unavailable\" {\n if (verdict === \"ok\" || verdict === \"rate_limited\") return \"admit\";\n if (verdict === \"rejected\") {\n recordAuthFailure(failures, credentialId, now);\n return \"unauthorized\";\n }\n return \"unavailable\";\n}\n","/**\n * OAuth redirect_uri policy for the remote MCP HTTP server (POST /register, GET+POST /authorize).\n *\n * Why this exists: /oauth/token returns the caller's raw `pb_sk_*` API key as the OAuth access\n * token (intentional: it survives Railway restarts). Before this policy, /register stored any\n * redirect_uris and /authorize auto-registered any https redirect_uri for an unknown client_id,\n * so a link on our real domain could send a victim's pasted key to an attacker:\n * 1. attacker builds /authorize?client_id=x&redirect_uri=https://evil.example/cb&code_challenge=...\n * 2. victim pastes their pb_sk_* key into our real page\n * 3. the one-time code lands at evil.example; the attacker redeems it at /oauth/token for the key.\n *\n * The policy, applied at every entry point:\n * - https redirect_uris are accepted only as an EXACT redirect URI (scheme + host + path) on the\n * allowlist: the platforms' own documented callbacks below, plus OAUTH_REDIRECT_HOST_ALLOWLIST\n * entries. A host alone is not enough: an allowed host can serve a callback path that belongs\n * to someone else (a custom GPT's `https://chatgpt.com/aip/{g-ID}/oauth/callback` exchanges the\n * code at a token URL its builder chose). The host compares case-insensitively; the path\n * compares exactly, so a trailing slash or an extra segment is refused. CORS_ORIGINS is\n * deliberately NOT included: a CORS origin is a browser-fetch permission, not a place we send\n * an API key.\n * - http redirect_uris are accepted only on a loopback host (localhost, 127.0.0.1, [::1]), any\n * port and path. This is the RFC 8252 section 7.3 native-app exception (Claude Code and other\n * local MCP clients use it) and is on in production: the browser follows the redirect on the\n * victim's OWN machine, so an attacker cannot receive the code unless they already run code there.\n * - https must use the default port (empty or 443) and carry no query; loopback keeps any port.\n * - no credentials in the URI, no fragment, at most 2048 characters.\n * Every check returns a plain-language refusal reason, or null when the URI is allowed. Callers\n * respond 400 with that reason; nothing here ever fails open.\n */\n\n/**\n * Built-in exact https redirect URIs (origin + path).\n * - https://claude.ai/api/mcp/auth_callback: the hosted Claude apps' one callback,\n * https://claude.com/docs/connectors/building/authentication (\"Callback URLs\").\n * - https://claude.com/api/mcp/auth_callback: the announced move of that callback to claude.com.\n * Anthropic's current page above names only claude.ai; the claude.com form is listed by\n * https://docs.strata.io/guides/ai-identity/connect/claude. Same owner, same path.\n * - https://chatgpt.com/connector_platform_oauth_redirect: ChatGPT's stable connector callback,\n * https://developers.openai.com/plugins/build/auth.\n */\nexport const DEFAULT_OAUTH_REDIRECT_URIS: readonly string[] = [\n \"https://claude.ai/api/mcp/auth_callback\",\n \"https://claude.com/api/mcp/auth_callback\",\n \"https://chatgpt.com/connector_platform_oauth_redirect\",\n];\n\n/**\n * ChatGPT's per-connection callback `https://chatgpt.com/connector/oauth/{callback_id}`\n * (https://developers.openai.com/plugins/build/auth). Exactly one id segment, nothing after it.\n */\nconst CHATGPT_CONNECTOR_CALLBACK = /^https:\\/\\/chatgpt\\.com\\/connector\\/oauth\\/[A-Za-z0-9_-]+$/;\n\n/** Hostnames as `URL.hostname` reports them (IPv6 keeps its brackets). */\nconst LOOPBACK_HOSTS: readonly string[] = [\"localhost\", \"127.0.0.1\", \"[::1]\"];\n\nconst MAX_REDIRECT_URI_LENGTH = 2048;\nconst MAX_REGISTRATION_REDIRECTS = 10;\n\nconst OPERATOR_ENTRY_FORM =\n \"OAUTH_REDIRECT_HOST_ALLOWLIST entries must be full https redirect URIs such as https://partner.example/oauth/callback (no bare host, no query, no fragment)\";\n\n/** The effective https redirect policy. A non-null `configError` refuses every redirect_uri. */\nexport interface OAuthRedirectAllowlist {\n uris: readonly string[];\n configError: string | null;\n}\n\n/** `origin + pathname` of an absolute URL: the form every allowlist entry is compared in. */\nfunction exactForm(url: URL): string {\n return `${url.origin}${url.pathname}`;\n}\n\n/**\n * Parse OAUTH_REDIRECT_HOST_ALLOWLIST (comma-separated). Every entry must be a full https redirect\n * URI with a path; one bare host or malformed entry makes the whole value a config error, so OAuth\n * fails closed until the operator fixes it (the error names the required form).\n */\nexport function parseRedirectUriAllowlist(raw: string | undefined): { uris: string[]; configError: string | null } {\n if (!raw) return { uris: [], configError: null };\n const uris: string[] = [];\n for (const part of raw.split(\",\")) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n let url: URL | null = null;\n try {\n url = trimmed.includes(\"://\") ? new URL(trimmed) : null;\n } catch {\n url = null;\n }\n const valid =\n url !== null &&\n url.protocol === \"https:\" &&\n !url.username && !url.password && !url.search && !url.hash &&\n (url.port === \"\" || url.port === \"443\") &&\n url.pathname !== \"/\";\n if (!valid || url === null) {\n return { uris: [], configError: `${OPERATOR_ENTRY_FORM}; got \"${trimmed}\"` };\n }\n uris.push(exactForm(url));\n }\n return { uris, configError: null };\n}\n\n/** Effective allowlist: built-in exact URIs + OAUTH_REDIRECT_HOST_ALLOWLIST, nothing else. */\nexport function resolveOAuthRedirectAllowlist(opts: { explicitAllowlist?: string } = {}): OAuthRedirectAllowlist {\n const parsed = parseRedirectUriAllowlist(opts.explicitAllowlist);\n return {\n uris: [...new Set<string>([...DEFAULT_OAUTH_REDIRECT_URIS, ...parsed.uris])],\n configError: parsed.configError,\n };\n}\n\n/**\n * Check one redirect_uri. Returns the refusal reason, or null when it is allowed.\n */\nexport function checkRedirectUri(redirectUri: unknown, allowlist: OAuthRedirectAllowlist): string | null {\n if (allowlist.configError) return `OAuth is misconfigured: ${allowlist.configError}`;\n if (typeof redirectUri !== \"string\" || redirectUri.length === 0) {\n return \"redirect_uri is required\";\n }\n if (redirectUri.length > MAX_REDIRECT_URI_LENGTH) {\n return `redirect_uri is longer than ${MAX_REDIRECT_URI_LENGTH} characters`;\n }\n let url: URL;\n try {\n url = new URL(redirectUri);\n } catch {\n return \"redirect_uri is not a valid absolute URL\";\n }\n if (url.username || url.password) return \"redirect_uri must not contain credentials\";\n if (url.hash) return \"redirect_uri must not contain a fragment\";\n\n const host = url.hostname.toLowerCase();\n if (url.protocol === \"http:\") {\n return LOOPBACK_HOSTS.includes(host)\n ? null\n : \"redirect_uri must use https (http is allowed only for localhost, 127.0.0.1 or [::1])\";\n }\n if (url.protocol === \"https:\") {\n if (url.port && url.port !== \"443\") return \"redirect_uri must use the default https port (443)\";\n if (url.search) return \"redirect_uri must not contain a query\";\n const exact = exactForm(url);\n return allowlist.uris.includes(exact) || CHATGPT_CONNECTOR_CALLBACK.test(exact)\n ? null\n : \"redirect_uri is not an allowed callback (an operator can add the full URI with OAUTH_REDIRECT_HOST_ALLOWLIST)\";\n }\n return \"redirect_uri scheme is not allowed (use https, or http on loopback)\";\n}\n\n/**\n * Check a whole registration `redirect_uris` value. Refuses the request if ANY entry is refused\n * (RFC 7591 invalid_redirect_uri) rather than silently registering a shorter list.\n */\nexport function checkRegistrationRedirects(\n redirectUris: unknown,\n allowlist: OAuthRedirectAllowlist,\n): string | null {\n if (!Array.isArray(redirectUris) || redirectUris.length === 0) return \"redirect_uris is required\";\n if (redirectUris.length > MAX_REGISTRATION_REDIRECTS) {\n return `redirect_uris may hold at most ${MAX_REGISTRATION_REDIRECTS} entries`;\n }\n for (const uri of redirectUris) {\n const reason = checkRedirectUri(uri, allowlist);\n if (reason) return reason;\n }\n return null;\n}\n","/**\n * The one-time authorization code minted by POST /authorize and redeemed at POST /oauth/token.\n * The token door decides here whether a stored code may still be redeemed, so expiry is enforced\n * at redemption and not only by the 60-second sweep in http.ts.\n */\nexport interface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\n/**\n * The api_key field POST /authorize received, as the key the person meant (TASK-668, TEN-4580).\n * Onboarding shows the key inside JSON, so people paste it with its quotes. Trim, then strip one\n * matching pair of ' \" ` quotes and trim again. Input hygiene at the door (RULE-632: not a\n * fallback for old data). A missing or non-string field (a repeated form field arrives as an\n * array) reads as empty, which the prefix check then refuses.\n */\nexport function normalisePastedKey(raw: unknown): string {\n if (typeof raw !== \"string\") return \"\";\n const trimmed = raw.trim();\n const first = trimmed[0];\n if (trimmed.length >= 2 && (first === '\"' || first === \"'\" || first === \"`\") && trimmed.endsWith(first)) {\n return trimmed.slice(1, -1).trim();\n }\n return trimmed;\n}\n\n/** Why a code cannot be redeemed, or null when it can (PKCE is checked separately). */\nexport function pendingCodeRefusal(\n pending: PendingAuth | undefined,\n redirectUri: unknown,\n now: number,\n): \"unknown_code\" | \"redirect_mismatch\" | \"expired\" | null {\n if (!pending) return \"unknown_code\";\n if (pending.redirectUri !== redirectUri) return \"redirect_mismatch\";\n if (now > pending.expiresAt) return \"expired\";\n return null;\n}\n\n/**\n * The POST /oauth/token response body (DEC-7: the access token IS the caller's pb_sk_ key).\n *\n * TEN-4548: no refresh_token. The key is valid until revoked in Cortex, so a refresh grant could\n * only re-issue the same key; the old pb_rt_ token carried that key base64url-readable and was\n * never revoked, a second place the key leaked for no security gain. The MCP authorization spec\n * makes refresh tokens optional, so a client re-authorizes when the key is rejected.\n */\nexport function tokenResponse(apiKey: string): {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n} {\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1 year: validity is enforced by Convex on every call, not by this clock.\n expires_in: 365 * 24 * 3600,\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAwBA,SAAS,YAAY,kBAAkB;AACvC,OAAO,aAAa;AACpB,SAAS,qCAAqC;AAC9C,SAAS,2BAA2B;AACpC,OAAO,eAAe;;;ACFtB,IAAM,eAA4C;AAAA,EAChD,IAAI;AAAA,EACJ,IAAI;AACN;AAMO,IAAM,gBAAgB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAqBtB,SAAS,cAAc,OAAuB,CAAC,GAAW;AAC/D,QAAM,OAAO,KAAK,QAAQ;AAC1B,QAAM,eAAe,KAAK,gBAAgB;AAC1C,QAAM,MAAM,CAAC,WAAW,aAAa,IAAI,GAAG,KAAK,SAAS,EAAE,OAAO,OAAO,EAAE,KAAK,GAAG;AACpF,QAAM,WAAW,eAAe,+BAA+B,YAAY,YAAY;AACvF,SAAO,gBAAgB,GAAG,2EAA2E,QAAQ;AAC/G;;;ACcO,SAAS,iBACdA,WACA,SACA,KACA,KACA,MACe;AACf,MAAI,MAAM,GAAG;AACX,UAAM,IAAI,WAAW,yBAAyB,GAAG,EAAE;AAAA,EACrD;AAEA,QAAM,EAAE,OAAO,QAAQ,IAAI;AAI3B,QAAM,cAAc,CAAC,MACnB,EAAE,WAAW,KAAK,MAAM,EAAE,aAAa;AAIzC,QAAM,aAAqC,CAAC;AAC5C,aAAW,CAAC,IAAI,OAAO,KAAKA,WAAU;AACpC,QAAI,QAAQ,YAAY,SAAS;AAC/B,iBAAW,KAAK,CAAC,IAAI,OAAO,CAAC;AAAA,IAC/B;AAAA,EACF;AAGA,QAAM,WAAW,oBAAI,IAAY;AACjC,aAAW,CAAC,IAAI,OAAO,KAAK,YAAY;AACtC,QAAI,CAAC,YAAY,OAAO,KAAK,MAAM,QAAQ,cAAc,OAAO;AAC9D,eAAS,IAAI,EAAE;AAAA,IACjB;AAAA,EACF;AAKA,MAAI,YAAY,WAAW,OAAO,CAAC,CAAC,EAAE,MAAM,CAAC,SAAS,IAAI,EAAE,CAAC;AAE7D,MAAI,UAAU,UAAU,KAAK;AAG3B,UAAM,gBAAgB,UACnB,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,EAEjC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU;AAErD,eAAW,CAAC,EAAE,KAAK,eAAe;AAChC,UAAI,UAAU,SAAS,IAAK;AAC5B,eAAS,IAAI,EAAE;AAEf,kBAAY,UAAU,OAAO,CAAC,CAAC,GAAG,MAAM,QAAQ,EAAE;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,QAAQ,MAAM,KAAK,QAAQ;AAGjC,QAAM,QAAQ,UAAU,SAAS;AAGjC,MAAI,oBAAoB;AACxB,MAAI,cAAc;AAClB,MAAI,CAAC,OAAO;AAEV,UAAM,gBAAgB,UAAU,OAAO,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,CAAC;AAClE,QAAI,cAAc,SAAS,GAAG;AAE5B,YAAM,gBAAgB,KAAK,IAAI,GAAG,cAAc,IAAI,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,CAAC;AAC5E,YAAM,cAAc,WAAW,MAAM;AACrC,0BAAoB,KAAK,IAAI,GAAG,KAAK,KAAK,cAAc,GAAI,CAAC;AAAA,IAC/D,OAAO;AAIL,0BAAoB;AACpB,oBAAc;AAAA,IAChB;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,OAAO,mBAAmB,YAAY;AACxD;AAUO,SAAS,iBACdA,WACA,SACQ;AACR,MAAI,QAAQ;AACZ,aAAW,WAAWA,UAAS,OAAO,GAAG;AACvC,QAAI,QAAQ,YAAY,QAAS;AAAA,EACnC;AACA,SAAO;AACT;;;ACjJA,SAAS,aAAa,GAAmB;AACvC,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,CAAC,CAAC;AAClC;AAeA,SAAS,MACP,QACA,SACA,GACU;AACV,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,SAAS,EAAE,eAAe,OAAO,OAAO,EAAE;AAAA,IAC1C,MAAM;AAAA,MACJ,SAAS;AAAA,MACT,IAAI,EAAE,MAAM;AAAA,MACZ,OAAO;AAAA,QACL,MAAM;AAAA,QACN;AAAA,QACA,MAAM;AAAA,UACJ;AAAA,UACA,mBAAmB;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,SAAS,EAAE;AAAA,QACb;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAUO,SAAS,oBAAoB,GAA0B;AAC5D,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,SAAO;AAAA,IACL;AAAA,IACA,wCAAmC,OAAO;AAAA,IAC1C;AAAA,EACF;AACF;AAMO,SAAS,sBAAsB,GAA0B;AAC9D,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,SAAO;AAAA,IACL;AAAA,IACA,gEAA2D,OAAO;AAAA,IAClE;AAAA,EACF;AACF;AAYO,SAAS,qBACd,GACU;AACV,QAAM,UAAU,aAAa,EAAE,iBAAiB;AAChD,QAAM,UAAU,EAAE,OACd,2DACA,4DAAuD,OAAO;AAClE,SAAO,MAAM,eAAe,SAAS,CAAC;AACxC;AAuBO,SAAS,QAAQ,KAAsB,OAAuB;AACnE,MAAI,IAAI,YAAa;AACrB,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,MAAM,OAAO,GAAG;AAClD,QAAI,UAAU,GAAG,CAAC;AAAA,EACpB;AACA,MAAI,OAAO,MAAM,MAAM,EAAE,KAAK,MAAM,IAAI;AAC1C;;;ACzGO,IAAM,mBAAmB;AACzB,IAAM,yBAAyB,IAAI;AACnC,IAAM,yBAAyB,KAAK;AACpC,IAAM,2BAA2B;AAExC,SAAS,SAAS,WAA+B;AAC/C,SAAO,EAAE,QAAQ,YAAY,cAAc,QAAQ,SAAS,EAAE;AAChE;AASO,SAAS,kBAAkB,qBAA0C;AAC1E,MAAI,OAAO,wBAAwB,YAAY,oBAAoB,KAAK,MAAM,IAAI;AAIhF,WAAO,EAAE,QAAQ,SAAS;AAAA,EAC5B;AACA,MAAI,CAAC,oBAAoB,WAAW,SAAS,GAAG;AAG9C,WAAO,SAAS,oBAAoB,KAAK,CAAC;AAAA,EAC5C;AACA,QAAM,QAAQ,oBAAoB,MAAM,CAAC,EAAE,KAAK;AAGhD,MAAI,CAAC,MAAM,WAAW,QAAQ,EAAG,QAAO,SAAS,KAAK;AACtD,SAAO,EAAE,QAAQ,MAAM,QAAQ,OAAO,cAAc,QAAQ,KAAK,EAAE;AACrE;AAQO,SAAS,iBAAiB,QAA4B;AAC3D,MAAI,WAAW,GAAI,QAAO,EAAE,QAAQ,SAAS;AAC7C,MAAI,CAAC,OAAO,WAAW,QAAQ,EAAG,QAAO,SAAS,MAAM;AACxD,SAAO,EAAE,QAAQ,MAAM,QAAQ,cAAc,QAAQ,MAAM,EAAE;AAC/D;AAWO,SAAS,oBAAoB,QAAiB,cAAuB,WAA4B;AACtG,MAAI,iBAAiB,aAAc,QAAO;AAC1C,MAAI,OAAO,WAAW,YAAY,WAAW,GAAI,QAAO;AACxD,MAAI;AACF,WAAO,IAAI,IAAI,MAAM,EAAE,WAAW,IAAI,IAAI,SAAS,EAAE;AAAA,EACvD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGO,SAAS,eACd,UACA,cACA,KACS;AACT,QAAM,MAAM,SAAS,IAAI,YAAY;AACrC,MAAI,CAAC,IAAK,QAAO;AACjB,SAAO,IAAI,eAAe;AAC5B;AAQO,SAAS,iBACd,UACA,MACA,KACA,KAA6B,MACZ;AACjB,MAAI,KAAK,WAAW,SAAU,QAAO;AACrC,QAAM,MAAM,SAAS,IAAI,KAAK,YAAY;AAC1C,MAAI,CAAC,OAAO,CAAC,eAAe,UAAU,KAAK,cAAc,GAAG,EAAG,QAAO;AACtE,QAAM,oBAAoB,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,eAAe,OAAO,GAAI,CAAC;AAChF,SAAO,sBAAsB,EAAE,mBAAmB,OAAO,kBAAkB,SAAS,IAAI,OAAO,GAAG,CAAC;AACrG;AASO,SAAS,kBACd,UACA,cACA,KACM;AACN,QAAM,MAAM,SAAS,IAAI,YAAY;AAErC,MAAI,CAAC,KAAK;AACR,QAAI,SAAS,QAAQ,0BAA0B;AAC7C,YAAM,SAAS,SAAS,KAAK,EAAE,KAAK,EAAE;AACtC,UAAI,WAAW,OAAW,UAAS,OAAO,MAAM;AAAA,IAClD;AACA,aAAS,IAAI,cAAc,EAAE,OAAO,GAAG,cAAc,KAAK,cAAc,EAAE,CAAC;AAC3E;AAAA,EACF;AAGA,MAAI,MAAM,IAAI,eAAe,wBAAwB;AACnD,QAAI,QAAQ;AACZ,QAAI,eAAe;AACnB,QAAI,eAAe;AAAA,EACrB,OAAO;AACL,QAAI;AACJ,QAAI,IAAI,SAAS,kBAAkB;AACjC,UAAI,eAAe,MAAM;AAAA,IAC3B;AAAA,EACF;AACF;AAYO,SAAS,eACd,SACA,UACA,cACA,KAC0C;AAC1C,MAAI,YAAY,QAAQ,YAAY,eAAgB,QAAO;AAC3D,MAAI,YAAY,YAAY;AAC1B,sBAAkB,UAAU,cAAc,GAAG;AAC7C,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ACjKO,IAAM,8BAAiD;AAAA,EAC5D;AAAA,EACA;AAAA,EACA;AACF;AAMA,IAAM,6BAA6B;AAGnC,IAAM,iBAAoC,CAAC,aAAa,aAAa,OAAO;AAE5E,IAAM,0BAA0B;AAChC,IAAM,6BAA6B;AAEnC,IAAM,sBACJ;AASF,SAAS,UAAU,KAAkB;AACnC,SAAO,GAAG,IAAI,MAAM,GAAG,IAAI,QAAQ;AACrC;AAOO,SAAS,0BAA0B,KAAyE;AACjH,MAAI,CAAC,IAAK,QAAO,EAAE,MAAM,CAAC,GAAG,aAAa,KAAK;AAC/C,QAAM,OAAiB,CAAC;AACxB,aAAW,QAAQ,IAAI,MAAM,GAAG,GAAG;AACjC,UAAM,UAAU,KAAK,KAAK;AAC1B,QAAI,CAAC,QAAS;AACd,QAAI,MAAkB;AACtB,QAAI;AACF,YAAM,QAAQ,SAAS,KAAK,IAAI,IAAI,IAAI,OAAO,IAAI;AAAA,IACrD,QAAQ;AACN,YAAM;AAAA,IACR;AACA,UAAM,QACJ,QAAQ,QACR,IAAI,aAAa,YACjB,CAAC,IAAI,YAAY,CAAC,IAAI,YAAY,CAAC,IAAI,UAAU,CAAC,IAAI,SACrD,IAAI,SAAS,MAAM,IAAI,SAAS,UACjC,IAAI,aAAa;AACnB,QAAI,CAAC,SAAS,QAAQ,MAAM;AAC1B,aAAO,EAAE,MAAM,CAAC,GAAG,aAAa,GAAG,mBAAmB,UAAU,OAAO,IAAI;AAAA,IAC7E;AACA,SAAK,KAAK,UAAU,GAAG,CAAC;AAAA,EAC1B;AACA,SAAO,EAAE,MAAM,aAAa,KAAK;AACnC;AAGO,SAAS,8BAA8B,OAAuC,CAAC,GAA2B;AAC/G,QAAM,SAAS,0BAA0B,KAAK,iBAAiB;AAC/D,SAAO;AAAA,IACL,MAAM,CAAC,GAAG,oBAAI,IAAY,CAAC,GAAG,6BAA6B,GAAG,OAAO,IAAI,CAAC,CAAC;AAAA,IAC3E,aAAa,OAAO;AAAA,EACtB;AACF;AAKO,SAAS,iBAAiB,aAAsB,WAAkD;AACvG,MAAI,UAAU,YAAa,QAAO,2BAA2B,UAAU,WAAW;AAClF,MAAI,OAAO,gBAAgB,YAAY,YAAY,WAAW,GAAG;AAC/D,WAAO;AAAA,EACT;AACA,MAAI,YAAY,SAAS,yBAAyB;AAChD,WAAO,+BAA+B,uBAAuB;AAAA,EAC/D;AACA,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,WAAW;AAAA,EAC3B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,IAAI,YAAY,IAAI,SAAU,QAAO;AACzC,MAAI,IAAI,KAAM,QAAO;AAErB,QAAM,OAAO,IAAI,SAAS,YAAY;AACtC,MAAI,IAAI,aAAa,SAAS;AAC5B,WAAO,eAAe,SAAS,IAAI,IAC/B,OACA;AAAA,EACN;AACA,MAAI,IAAI,aAAa,UAAU;AAC7B,QAAI,IAAI,QAAQ,IAAI,SAAS,MAAO,QAAO;AAC3C,QAAI,IAAI,OAAQ,QAAO;AACvB,UAAM,QAAQ,UAAU,GAAG;AAC3B,WAAO,UAAU,KAAK,SAAS,KAAK,KAAK,2BAA2B,KAAK,KAAK,IAC1E,OACA;AAAA,EACN;AACA,SAAO;AACT;AAMO,SAAS,2BACd,cACA,WACe;AACf,MAAI,CAAC,MAAM,QAAQ,YAAY,KAAK,aAAa,WAAW,EAAG,QAAO;AACtE,MAAI,aAAa,SAAS,4BAA4B;AACpD,WAAO,kCAAkC,0BAA0B;AAAA,EACrE;AACA,aAAW,OAAO,cAAc;AAC9B,UAAM,SAAS,iBAAiB,KAAK,SAAS;AAC9C,QAAI,OAAQ,QAAO;AAAA,EACrB;AACA,SAAO;AACT;;;ACnJO,SAAS,mBAAmB,KAAsB;AACvD,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,UAAU,IAAI,KAAK;AACzB,QAAM,QAAQ,QAAQ,CAAC;AACvB,MAAI,QAAQ,UAAU,MAAM,UAAU,OAAO,UAAU,OAAO,UAAU,QAAQ,QAAQ,SAAS,KAAK,GAAG;AACvG,WAAO,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AAAA,EACnC;AACA,SAAO;AACT;AAGO,SAAS,mBACd,SACA,aACA,KACyD;AACzD,MAAI,CAAC,QAAS,QAAO;AACrB,MAAI,QAAQ,gBAAgB,YAAa,QAAO;AAChD,MAAI,MAAM,QAAQ,UAAW,QAAO;AACpC,SAAO;AACT;AAUO,SAAS,cAAc,QAI5B;AACA,SAAO;AAAA,IACL,cAAc;AAAA,IACd,YAAY;AAAA;AAAA,IAEZ,YAAY,MAAM,KAAK;AAAA,EACzB;AACF;;;ANhBA,cAAc;AAEd,IAAM,OAAO,SAAS,QAAQ,IAAI,QAAQ,QAAQ,IAAI,YAAY,QAAQ,EAAE;AAG5E,SAAS,qBAA+B;AACtC,QAAM,cAAc,QAAQ,IAAI,mBAAmB,mBAAmB,QAAQ,OAAO,EAAE;AACvF,SAAO,CAAC,YAAY,GAAG,kBAAkB,QAAQ,IAAI,oBAAoB,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC,CAAC;AAC7G;AAEA,SAAS,QAAQ,KAAkB;AACjC,QAAM,QAAQ,IAAI,QAAQ,mBAAmB,KAAK,IAAI,YAAY;AAClE,QAAM,OAAO,IAAI,QAAQ,QAAQ,aAAa,IAAI;AAClD,SAAO,GAAG,KAAK,MAAM,IAAI;AAC3B;AAIA,IAAM,MAAM,QAAQ;AAGpB,IAAI,IAAI,eAAe,CAAC;AACxB,IAAI,IAAI,QAAQ,KAAK,CAAC;AAGtB,IAAM,mBAAmB,QAAQ,IAAI,gBAAgB,qBAClD,MAAM,GAAG,EACT,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EACnB,OAAO,OAAO;AAGjB,IAAM,2BAA2B,8BAA8B,EAAE,mBAAmB,QAAQ,IAAI,8BAA8B,CAAC;AAE/H,IAAI,IAAI,CAAC,MAAW,KAAU,SAAc;AAC1C,QAAM,SAAS,KAAK,QAAQ;AAC5B,MAAI,UAAU,gBAAgB,SAAS,MAAM,GAAG;AAC9C,QAAI,UAAU,+BAA+B,MAAM;AAAA,EACrD;AACA,MAAI,UAAU,gCAAgC,4BAA4B;AAC1E,MAAI;AAAA,IACF;AAAA,IACA;AAAA,EACF;AAGA,MAAI,UAAU,iCAAiC,6BAA6B;AAC5E,MAAI,KAAK,WAAW,WAAW;AAC7B,QAAI,OAAO,GAAG,EAAE,IAAI;AACpB;AAAA,EACF;AACA,OAAK;AACP,CAAC;AAKD,IAAI,IAAI,yCAAyC,CAAC,KAAU,QAAa;AACvE,QAAM,OAAO,QAAQ,GAAG;AACxB,MAAI,KAAK;AAAA,IACP,UAAU;AAAA,IACV,uBAAuB,CAAC,IAAI;AAAA,IAC5B,kBAAkB,CAAC,aAAa,eAAe;AAAA,IAC/C,0BAA0B,CAAC,QAAQ;AAAA,EACrC,CAAC;AACH,CAAC;AAKD,IAAI,IAAI,2CAA2C,CAAC,KAAU,QAAa;AACzE,QAAM,OAAO,QAAQ,GAAG;AACxB,MAAI,KAAK;AAAA,IACP,QAAQ;AAAA,IACR,wBAAwB,GAAG,IAAI;AAAA,IAC/B,gBAAgB,GAAG,IAAI;AAAA,IACvB,uBAAuB,GAAG,IAAI;AAAA,IAC9B,0BAA0B,CAAC,MAAM;AAAA,IACjC,uBAAuB,CAAC,oBAAoB;AAAA;AAAA,IAC5C,kCAAkC,CAAC,MAAM;AAAA,IACzC,uCAAuC,CAAC,MAAM;AAAA,IAC9C,kBAAkB,CAAC,aAAa,eAAe;AAAA,EACjD,CAAC;AACH,CAAC;AAMD,IAAM,cAAc,UAAU;AAAA,EAC5B,UAAU;AAAA,EACV,KAAK;AAAA,EACL,iBAAiB;AAAA,EACjB,eAAe;AAAA,EACf,SAAS,EAAE,OAAO,2CAA2C;AAC/D,CAAC;AAYD,IAAM,oBAAoB,oBAAI,IAA8B;AAE5D,IAAM,yBAAyB;AAE/B,IAAI;AAAA,EACF;AAAA,EACA;AAAA,EACA,QAAQ,KAAK;AAAA,EACb,CAAC,KAAU,QAAa;AAEtB,QAAI,kBAAkB,QAAQ,wBAAwB;AACpD,UAAI,OAAO,GAAG,EAAE,KAAK;AAAA,QACnB,OAAO;AAAA,QACP,mBAAmB;AAAA,MACrB,CAAC;AACD;AAAA,IACF;AAEA,UAAM,EAAE,eAAe,YAAY,IAAI,IAAI;AAE3C,UAAM,UAAU,2BAA2B,eAAe,wBAAwB;AAClF,QAAI,SAAS;AACX,UAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,wBAAwB,mBAAmB,QAAQ,CAAC;AAClF;AAAA,IACF;AAEA,UAAM,WAAW,aAAa,WAAW,CAAC;AAC1C,UAAM,SAA2B;AAAA,MAC/B,WAAW;AAAA,MACX;AAAA,MACA;AAAA,MACA,cAAc,KAAK,IAAI;AAAA,IACzB;AACA,sBAAkB,IAAI,UAAU,MAAM;AAEtC,QAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MACnB,WAAW;AAAA,MACX,aAAa,eAAe;AAAA,MAC5B;AAAA,MACA,aAAa,CAAC,oBAAoB;AAAA,MAClC,gBAAgB,CAAC,MAAM;AAAA,MACvB,4BAA4B;AAAA,IAC9B,CAAC;AAAA,EACH;AACF;AAKA,IAAM,eAAe,oBAAI,IAAyB;AAElD,YAAY,MAAM;AAChB,QAAM,MAAM,KAAK,IAAI;AACrB,aAAW,CAAC,MAAM,IAAI,KAAK,cAAc;AACvC,QAAI,MAAM,KAAK,UAAW,cAAa,OAAO,IAAI;AAAA,EACpD;AACA,aAAW,CAAC,IAAI,MAAM,KAAK,mBAAmB;AAC5C,QAAI,MAAM,OAAO,eAAe,KAAK,KAAK,IAAQ,mBAAkB,OAAO,EAAE;AAAA,EAC/E;AAEA,aAAW,CAAC,cAAc,GAAG,KAAK,cAAc;AAC9C,QAAI,IAAI,eAAe,OAAO,IAAI,eAAe,yBAAyB,KAAK;AAC7E,mBAAa,OAAO,YAAY;AAAA,IAClC;AAAA,EACF;AACF,GAAG,GAAM;AAET,SAAS,IAAI,GAAoB;AAC/B,SAAO,OAAO,KAAK,EAAE,EAAE;AAAA,IAAQ;AAAA,IAAY,CAAC,OACzC,EAAE,KAAK,SAAS,KAAK,UAAU,KAAK,SAAS,KAAK,QAAQ,KAAK,OAAO,GAAG,CAAC;AAAA,EAC7E;AACF;AAQA,SAAS,cAAc,OAAe,aAAqB,YAAY,IAAY;AACjF,SAAO;AAAA;AAAA;AAAA,SAGA,IAAI,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA,EAIjB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BT,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBA6QS,cAAc,EAAE,MAAM,KAAK,CAAC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","names":["sessions","sid"]}
1
+ {"version":3,"sources":["../src/http.ts","../src/brand/logo-markup.ts","../src/sessionCap.ts","../src/httpErrors.ts","../src/authLockout.ts","../src/lib/oauthRedirectAllowlist.ts","../src/lib/oauthAuthCode.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: https://claude.ai)\n * OAUTH_REDIRECT_HOST_ALLOWLIST — Extra exact https redirect URIs, comma-separated full URIs (see lib/oauthRedirectAllowlist.ts)\n *\n * WP-757 TASK-224 F2 (DEC-1722): the PB_MODULES env var and its one module (gitchain: the\n * `chain` / `chain-review` tools) are deleted along with gitchain itself — the tool surface\n * is flat and always registered now, no opt-in module left to gate.\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, forgetStreamLifecycle } from \"./client.js\"; import { apiKeysUrl } from \"./lib/appUrl.js\";\nimport { runWithAuth, hashKey, getKeyState } from \"./auth.js\";\nimport { createProductBrainServer, SERVER_VERSION } from \"./server.js\";\nimport { appLogoMarkup, appLogoStyles } from \"./brand/logo-markup.js\";\nimport { planKeyAdmission, countKeySessions } from \"./sessionCap.js\";\nimport { send429, buildRateLimitError, buildSessionCapError } from \"./httpErrors.js\";\nimport { AUTH_FAILURE_WINDOW_MS, authLockoutError, recordAuthFailure } from \"./authLockout.js\";\nimport { isCrossSiteFormPost, resolveBearerAuth, resolvePastedKey, keyVerdictGate, type AuthFailureRecord } from \"./authLockout.js\";\nimport { checkRedirectUri, checkRegistrationRedirects, resolveOAuthRedirectAllowlist } from \"./lib/oauthRedirectAllowlist.js\";\nimport { normalisePastedKey, pendingCodeRefusal, tokenResponse, type PendingAuth } from \"./lib/oauthAuthCode.js\";\nimport { checkKeyAgainstCandidates, parseFallbackUrls } from \"./lib/deploymentUrlResolver.js\";\n\n// ── Bootstrap ───────────────────────────────────────────────────────────\n\nbootstrapHttp();\n\nconst PORT = parseInt(process.env.PORT ?? process.env.MCP_PORT ?? \"3002\", 10);\n\n/** DEC-789 S2: the deployments a key may live on — primary first, then CONVEX_FALLBACK_URLS. */\nfunction keyCheckCandidates(): string[] {\n const primaryUrl = (process.env.CONVEX_SITE_URL ?? DEFAULT_CLOUD_URL).replace(/\\/$/, \"\");\n return [primaryUrl, ...parseFallbackUrls(process.env.CONVEX_FALLBACK_URLS).map((u) => u.replace(/\\/$/, \"\"))];\n}\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\n// The API key IS the access token, so every redirect_uri is vetted (lib/oauthRedirectAllowlist.ts).\nconst OAUTH_REDIRECT_ALLOWLIST = resolveOAuthRedirectAllowlist({ explicitAllowlist: process.env.OAUTH_REDIRECT_HOST_ALLOWLIST });\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\"], // TEN-4548: no refresh grant (lib/oauthAuthCode.ts tokenResponse)\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: one stderr line per refusal on every door, plus each success of /register, POST /authorize\n// and /oauth/token; GET /authorize 200 writes none (TASK-1005). The kill line (shape-S1b.md) reads these.\n// Never a key or a code; refused or dropped URIs only for /register (DEC-2203), JSON-quoted.\n\nfunction logOAuthDoor(door: string, status: number, reason: string, uris?: unknown[]): void {\n const list = uris === undefined ? \"\" : ` uris=${JSON.stringify(uris.map(String)).slice(0, 600)}`;\n process.stderr.write(`[oauth] door=${door} status=${status} reason=${JSON.stringify(reason)}${list}\\n`);\n}\n\n// ── OAuth: Rate Limiting ─────────────────────────────────────────────────\n// 20 a minute keyed on the client IP, on GET and POST /authorize only. There the IP is the person's\n// own browser, and the limiter bounds the in-memory pendingCodes POST /authorize mints. It is off\n// /register (stateless, stores nothing) and /oauth/token (a code is single-use, PKCE-bound and dies\n// in 5 minutes), where the caller is a vendor's server and every claude.ai user shares Anthropic's\n// egress IP (TEN-4678; DEC-2002 keeps the key lockout per credential for the same reason).\n\nconst authLimiter = rateLimit({\n windowMs: 60_000,\n max: 20,\n standardHeaders: true,\n legacyHeaders: false,\n handler: (req: any, res: any) => {\n logOAuthDoor(`${req.method} /authorize`, 429, \"rate_limited_per_ip\");\n res.status(429).json({ error: \"Too many auth requests. Try again later.\" });\n },\n});\n\n// ── OAuth: Dynamic Client Registration (RFC 7591) ────────────────────────\n// Step 3: the client registers before starting the auth flow. Stateless (TASK-1005): every\n// redirect_uri is checked against the exact-URI allowlist and an opaque id comes back; nothing is\n// stored, so a restart or a deploy forgets nothing and there is no cap to hit. The guards are the\n// allowlist at /authorize and the code bound to its redirect_uri and PKCE challenge at /oauth/token\n// (lib/oauthAuthCode.ts pendingCodeRefusal); RFC 7591 does not require server-side state for a\n// public client (token_endpoint_auth_method \"none\").\n\napp.post(\n \"/register\",\n express.json(),\n (req: any, res: any) => {\n const { redirect_uris, client_name } = req.body ?? {};\n\n const { refusal, allowed, dropped } = checkRegistrationRedirects(redirect_uris, OAUTH_REDIRECT_ALLOWLIST); // DEC-2203\n if (refusal) {\n logOAuthDoor(\"POST /register\", 400, refusal, Array.isArray(redirect_uris) ? redirect_uris : [redirect_uris]);\n res.status(400).json({ error: \"invalid_redirect_uri\", error_description: refusal });\n return;\n }\n\n const clientId = `pb_client_${randomUUID()}`;\n const droppedUris = dropped.length > 0 ? dropped.map((d) => d.uri) : undefined;\n logOAuthDoor(\"POST /register\", 201, droppedUris ? \"dropped_redirects\" : \"registered\", droppedUris);\n res.status(201).json({\n client_id: clientId,\n client_name: client_name ?? \"MCP Client\",\n redirect_uris: allowed,\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\nconst pendingCodes = new Map<string, PendingAuth>();\n\nsetInterval(() => {\n const now = Date.now();\n for (const [code, auth] of pendingCodes) {\n if (now > auth.expiresAt) pendingCodes.delete(code);\n }\n // Fix 5 — Clean up stale auth failure tracking entries (the size cap is enforced at insert, recordAuthFailure).\n for (const [credentialId, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(credentialId);\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 says \"your assistant\", never \"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: /register stores nothing, so no client_name reaches this page (TASK-1005).\nconst PROVIDER_NAME = \"your assistant\";\n\n// The one sentence a person says to start. Literal until TASK-1006 lands the kernel PASTE_NOW constant.\nconst START_SENTENCE = \"Start my Product Brain session.\";\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;${esc(START_SENTENCE)}&quot; and return to ${esc(providerName)}\">\n <span class=\"cmd-quote-part\" aria-hidden=\"true\">&ldquo;</span><span data-cmd-text>${esc(START_SENTENCE)}</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=\"${esc(apiKeysUrl())}\" 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(${JSON.stringify(START_SENTENCE)}).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}): string {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = params;\n const providerName = PROVIDER_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=\"${esc(apiKeysUrl())}\" 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 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 refusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST); // never render the key form for a bad redirect\n if (refusal) {\n logOAuthDoor(\"GET /authorize\", 400, refusal);\n res.status(400).json({ error: \"invalid_request\", error_description: refusal });\n return;\n }\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: String(client_id ?? \"\"),\n }));\n});\n\napp.post(\n \"/authorize\",\n authLimiter,\n express.urlencoded({ extended: false }),\n async (req: any, res: any) => {\n const { redirect_uri, code_challenge, code_challenge_method, state, client_id } = req.body;\n const api_key = normalisePastedKey(req.body.api_key); // TASK-668: once here; every later read, pendingCodes too, uses it\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 logOAuthDoor(\"POST /authorize\", status, title);\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 // A POST from another site's page is refused before any key is read, so no page can drive the form, the lockout\n // map or the kernel through a visitor's browser (isCrossSiteFormPost; own origin = baseUrl).\n if (isCrossSiteFormPost(req.headers[\"origin\"], req.headers[\"sec-fetch-site\"], baseUrl(req))) {\n sendError(\"Request refused\", \"This form only accepts keys sent from its own sign-in page. Open the connect link again.\", 403);\n return;\n }\n // TEN-4601 / TEN-4673: the form is a door a key is presented at, so it shares /mcp's lockout map, keyed on the\n // presented key's hash. A locked key is refused next; a malformed or kernel-rejected key accrues to its own\n // record; an empty field never does (TEN-2606).\n const nowTs = Date.now();\n const pasted = resolvePastedKey(api_key);\n const lockout = authLockoutError(authFailures, pasted, nowTs);\n if (lockout) {\n const retryAfter = lockout.headers[\"Retry-After\"];\n res.setHeader(\"Retry-After\", retryAfter);\n sendError(\"Too many attempts\", `This key was not recognized too many times. Try again in ${retryAfter}s.`, 429);\n return;\n }\n if (pasted.status !== \"ok\") {\n if (pasted.status === \"rejected\") recordAuthFailure(authFailures, pasted.credentialId, nowTs);\n sendError(\"Invalid key format\", \"API keys start with <code>pb_sk_</code>. Check your key and try again.\");\n return;\n }\n\n // Any client_id is accepted (TASK-1005: /register stores nothing); the redirect_uri must pass the\n // exact-URI allowlist, because the API key is the access token and an unvetted redirect would\n // hand it to whoever owns that host. The code minted below is bound to this redirect_uri.\n if (!client_id) {\n logOAuthDoor(\"POST /authorize\", 400, \"client_id is required\");\n res.status(400).json({ error: \"invalid_request\", error_description: \"client_id is required\" });\n return;\n }\n const redirectRefusal = checkRedirectUri(redirect_uri, OAUTH_REDIRECT_ALLOWLIST);\n if (redirectRefusal) {\n logOAuthDoor(\"POST /authorize\", 400, redirectRefusal);\n res.status(400).json({ error: \"invalid_request\", error_description: redirectRefusal });\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 const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5000);\n const gate = keyVerdictGate(check.verdict, authFailures, pasted.credentialId, nowTs); // a rejected key accrues; an unreachable kernel never does\n if (check.verdict === \"ok\") {\n if (check.workspaceName) workspaceName = check.workspaceName;\n getKeyState(api_key).deploymentUrl = check.deploymentUrl;\n } else if (check.verdict === \"rate_limited\") {\n // The key IS valid but its workspace is over its request budget: \"wait\", not \"check your key\".\n sendError(\n \"Rate limited\",\n `This API key is valid but temporarily rate limited. Try again in ${check.retryAfterSeconds ?? 60}s.`,\n 429,\n );\n return;\n } else if (gate === \"unauthorized\") {\n sendError(\"Key not recognized\", \"This API key wasn't found in Product Brain. Check your API Keys in Cortex and try again.\", 401);\n return;\n } else {\n // No definitive verdict (a candidate down or off-contract): fail-open here, because initialize re-checks the key (TEN-4549).\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 = PROVIDER_NAME;\n logOAuthDoor(\"POST /authorize\", 200, \"code_issued\");\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// authorization_code is the only grant; the access token is the pb_sk_ key itself, so it\n// survives Railway redeploys with no server-side store (tokenResponse, TEN-4548).\n\napp.post(\n \"/oauth/token\",\n express.urlencoded({ extended: false }),\n express.json(),\n (req: any, res: any) => {\n const { grant_type, code, code_verifier, redirect_uri } = req.body ?? {};\n\n if (grant_type !== \"authorization_code\") {\n logOAuthDoor(\"POST /oauth/token\", 400, \"unsupported_grant_type\");\n res.status(400).json({ error: \"unsupported_grant_type\" });\n return;\n }\n\n // client_id is not read: the code is bound to its redirect_uri and PKCE challenge (TASK-1005).\n const pending = pendingCodes.get(code);\n const codeRefusal = pendingCodeRefusal(pending, redirect_uri, Date.now());\n if (codeRefusal === \"expired\") {\n pendingCodes.delete(code);\n logOAuthDoor(\"POST /oauth/token\", 400, \"expired\");\n res.status(400).json({ error: \"invalid_grant\", error_description: \"authorization code expired\" });\n return;\n }\n if (codeRefusal || !pending) {\n logOAuthDoor(\"POST /oauth/token\", 400, codeRefusal ?? \"unknown_code\");\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 logOAuthDoor(\"POST /oauth/token\", 400, \"pkce_failed\");\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 logOAuthDoor(\"POST /oauth/token\", 200, \"token_issued\");\n res.json(tokenResponse(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// Progressive lockout for failed API key attempts, keyed on the presented key's hash (TEN-4673), on every\n// door that takes a key: /mcp and the sign-in form (POST /authorize) share this one map (TEN-4601). Policy\n// + key classification 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 / TEN-4673 — refuse a presented key whose hash has exceeded the auth failure threshold.\n // TEN-2606 — only a *presented-and-rejected* credential accrues, and an absent one (the OAuth\n // bootstrap probe) is never recorded nor refused, so a locked key can always re-sign-in.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"POST\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 // TEN-4549: ask the kernel once whether this key is real before any session, eviction or\n // server instance exists for it. A rejected key accrues to its own lockout record like any\n // other presented-and-rejected credential; nothing caches the verdict past this request.\n const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5000);\n const gate = keyVerdictGate(check.verdict, authFailures, auth.credentialId, Date.now());\n if (gate === \"unauthorized\") {\n logRequest(\"POST\", \"auth_fail\");\n send401(req, res);\n return;\n }\n if (gate === \"unavailable\") {\n res.status(503).json({\n jsonrpc: \"2.0\",\n error: { code: -32000, message: \"Product Brain could not verify this API key right now. Retry shortly.\" },\n id: req.body?.id ?? null,\n });\n return;\n }\n if (check.verdict === \"ok\" || check.verdict === \"rate_limited\") getKeyState(apiKey).deploymentUrl = check.deploymentUrl;\n\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; forgetStreamLifecycle(keyH, s); // TASK-583 (WP-801, DEC-1907): the stream's session lifecycle leaves with it\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 / TEN-4673 — refuse a locked presented key; TEN-2606 — an absent credential is never recorded nor refused.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"GET\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 / TEN-4673 — refuse a locked presented key; TEN-2606 — an absent credential is never recorded nor refused.\n const nowTs = Date.now();\n const auth = resolveBearerAuth(req.headers?.authorization);\n const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n if (auth.status !== \"ok\") {\n logRequest(\"DELETE\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, auth.credentialId, 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, mcpSessionId: sessionId }, 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 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: Error) => {\n console.error(`[MCP HTTP] Server error: ${err.message}`);\n process.exit(1);\n});\n\nprocess.on(\"SIGINT\", gracefulShutdown);\nprocess.on(\"SIGTERM\", gracefulShutdown);\n","// GENERATED by scripts/generate-mcp-embedded-copies.mjs from src/lib/brand/logo-markup.ts.\n// DO NOT EDIT. Run `npm run mcp:embedded-copies` to regenerate.\n// WP-488 Slice 3 (FEAT-1377) / WP-480 S4: Railway's isolated Docker build for\n// packages/mcp-server cannot see the repo-root src/ — this file must physically\n// exist here. See scripts/generate-mcp-embedded-copies.mjs for the full rationale.\n\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\n// The product's name lives in the kernel so the web app, the invite email and MCP read one constant\n// (TASK-797; TEN-4824; RULE-664). The MCP copy's import is rewritten by the generator.\nimport { PRODUCT_NAME } from \"../lib/platformGlossary.js\";\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_NAME}</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\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-presented-key auth-failure lockout 429 (source: authLockoutError in authLockout.ts, TEN-4673).\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","// Progressive lockout for failed API-key auth attempts, keyed on the hash of the PRESENTED\n// credential, plus the classification that feeds it. Extracted from http.ts so the policy is\n// unit-testable in isolation (http.ts binds a live server on import).\n//\n// TEN-4673 (WP-827, TASK-698): the counter keys on hashKey(presented credential), the thing being\n// guessed, never the raw key and never the client IP. claude.ai and other hosted clients reach\n// /mcp from shared egress addresses, so a per-IP counter let ten bad keys from one stale connector\n// lock every user of that platform out. Now ten rejected presentations of key A lock key A on every\n// door and from every address, and a valid key B from the same address keeps working. No path\n// counts per IP: an absent credential never accrues (TEN-2606) and the form's cross-site refusal\n// answers 403 before any key is read. The lockout is defence in depth and load shedding; the\n// 128-bit key itself is the guard against guessing.\n//\n// TEN-2606: the lockout penalizes only a *presented-and-rejected* credential, never an *absent*\n// one, and never answers 429 to one. Absent-credential /mcp calls are the spec-mandated OAuth\n// bootstrap probe (the client hits the resource with no token to receive the 401 +\n// WWW-Authenticate challenge), so a client whose key is locked can always start a fresh sign-in.\n//\n// TEN-4549: the shape check alone let any `pb_sk_<anything>` open an MCP session without ever\n// accruing toward the lockout. keyVerdictGate() is where the kernel's verdict on the key lands:\n// a key the kernel rejects is a presented-and-rejected credential like any other.\n//\n// TEN-4601 (TASK-684): the lockout guards every door a key is presented at, /mcp (POST, GET,\n// DELETE) and the connector's sign-in form (POST /authorize), over ONE map in http.ts, so a key\n// rejected on one door is locked on the other. resolvePastedKey() is the form's sibling of\n// resolveBearerAuth(); both hash the key string itself, so the two doors share one record per key.\n\nimport { hashKey } from \"./auth.js\";\nimport type { KeyCheckResult } from \"./lib/deploymentUrlResolver.js\";\nimport { buildAuthLockoutError, type Built429 } from \"./httpErrors.js\";\n\nexport interface AuthFailureRecord {\n count: number;\n firstFailure: number;\n blockedUntil: number;\n}\n\n// Outcome of classifying a presented credential. `credentialId` is hashKey() of what was\n// presented (RULE-232: the map never holds a raw key) and is the lockout map's key.\n// - \"ok\": a pb_sk_-shaped key; initialize and the sign-in form ask the kernel whether it is real.\n// - \"absent\": no credential was presented: never penalized, never locked out.\n// - \"rejected\": a credential was presented but can never authenticate: penalized.\nexport type BearerAuth =\n | { status: \"ok\"; apiKey: string; credentialId: string }\n | { status: \"absent\" }\n | { status: \"rejected\"; credentialId: string };\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\nfunction rejected(presented: string): BearerAuth {\n return { status: \"rejected\", credentialId: hashKey(presented) };\n}\n\n/**\n * Classify a request's Authorization header into absent / rejected / ok.\n *\n * The only credential is a pb_sk_ API key: the OAuth token door hands the key itself back as\n * the access token (DEC-7, lib/oauthAuthCode.ts tokenResponse; TEN-4548 deleted the never-written\n * pb_at_ opaque-token store).\n */\nexport function resolveBearerAuth(authorizationHeader: unknown): BearerAuth {\n if (typeof authorizationHeader !== \"string\" || authorizationHeader.trim() === \"\") {\n // No Authorization header at all: the OAuth bootstrap probe. This is the ONLY case that\n // escapes the lockout: a client hitting the resource with no credential to fetch the\n // 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 authenticate, so it\n // is a rejected attempt; classifying it \"absent\" would let a caller bypass the guard.\n return rejected(authorizationHeader.trim());\n }\n const token = authorizationHeader.slice(7).trim();\n // \"Bearer \" with an empty token is a presented, unusable credential, not the header-less probe;\n // a token of unknown shape is garbage that was still presented.\n if (!token.startsWith(\"pb_sk_\")) return rejected(token);\n return { status: \"ok\", apiKey: token, credentialId: hashKey(token) };\n}\n\n/**\n * Classify the key pasted into the sign-in form (POST /authorize, already normalisePastedKey'd).\n * An empty field is absent: nothing was presented, never penalized (TEN-2606). Any other text that\n * is not pb_sk_-shaped is a presented key that can never authenticate, so it accrues like a\n * kernel-rejected one; a pb_sk_-shaped key goes to the kernel (keyVerdictGate).\n */\nexport function resolvePastedKey(apiKey: string): BearerAuth {\n if (apiKey === \"\") return { status: \"absent\" };\n if (!apiKey.startsWith(\"pb_sk_\")) return rejected(apiKey);\n return { status: \"ok\", apiKey, credentialId: hashKey(apiKey) };\n}\n\n/**\n * True when a sign-in form POST came from another site's page. The form is a cookie-less urlencoded\n * POST, which any page can make a visitor's browser send; the refusal keeps such a page from\n * driving the form, the failure map or the kernel at all. /mcp is not exposed: its key rides the\n * Authorization header, which a cross-site simple request cannot set.\n * - Sec-Fetch-Site \"cross-site\" → refused; \"same-origin\", \"same-site\", \"none\" or absent → not by this.\n * - Origin present and not `ownOrigin` (the server's own base, as the OAuth metadata advertises it;\n * an unparseable or \"null\" Origin is not ours) → refused. No Origin (curl, non-browser) → allowed.\n */\nexport function isCrossSiteFormPost(origin: unknown, secFetchSite: unknown, ownOrigin: string): boolean {\n if (secFetchSite === \"cross-site\") return true;\n if (typeof origin !== \"string\" || origin === \"\") return false;\n try {\n return new URL(origin).origin !== new URL(ownOrigin).origin;\n } catch {\n return true;\n }\n}\n\n/** True if `credentialId` is currently within an active lockout block. */\nexport function checkAuthBlock(\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): boolean {\n const rec = failures.get(credentialId);\n if (!rec) return false;\n return rec.blockedUntil > now;\n}\n\n/**\n * The lockout 429 for the presented credential (Retry-After = seconds left in the block), or null\n * when it is not blocked or no credential was presented (TEN-2606: the bootstrap probe is never\n * refused). Every door that takes a key asks this first: /mcp sends it as is; the sign-in form\n * reuses its Retry-After on its own error page. `id` mirrors the JSON-RPC request id when known.\n */\nexport function authLockoutError(\n failures: Map<string, AuthFailureRecord>,\n auth: BearerAuth,\n now: number,\n id: string | number | null = null,\n): Built429 | null {\n if (auth.status === \"absent\") return null;\n const rec = failures.get(auth.credentialId);\n if (!rec || !checkAuthBlock(failures, auth.credentialId, now)) return null;\n const retryAfterSeconds = Math.max(0, Math.ceil((rec.blockedUntil - now) / 1000));\n return buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec.count, id });\n}\n\n/**\n * Record one presented-and-rejected auth failure for `credentialId`. Callers MUST NOT call this\n * for an absent credential (TEN-2606). Tips the credential into a block once AUTH_FAILURE_MAX\n * failures land inside AUTH_FAILURE_WINDOW_MS; a failure past the window resets the count rather\n * than accumulating across windows. The map holds at most MAX_AUTH_FAILURE_ENTRIES records: a new\n * credential past the cap evicts the oldest-inserted one (a memory bound, not a guard; see header).\n */\nexport function recordAuthFailure(\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): void {\n const rec = failures.get(credentialId);\n\n if (!rec) {\n if (failures.size >= MAX_AUTH_FAILURE_ENTRIES) {\n const oldest = failures.keys().next().value;\n if (oldest !== undefined) failures.delete(oldest);\n }\n failures.set(credentialId, { 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\n/**\n * TEN-4549 / TEN-4601: what a door does with the kernel's verdict on a presented pb_sk_ key\n * (lib/deploymentUrlResolver.ts checkKeyAgainstCandidates, asked once per MCP initialize and once\n * per sign-in form submit; no cache of accepted keys outlives the session).\n * - ok / rate_limited → \"admit\": the key is real (a throttled workspace is still a real key).\n * - rejected → \"unauthorized\", and the failure accrues to that key's lockout record.\n * - unreachable → \"unavailable\": never penalize the caller for the kernel being down\n * (initialize fails closed with no session; the form proceeds, because\n * initialize re-checks the key).\n */\nexport function keyVerdictGate(\n verdict: KeyCheckResult[\"verdict\"],\n failures: Map<string, AuthFailureRecord>,\n credentialId: string,\n now: number,\n): \"admit\" | \"unauthorized\" | \"unavailable\" {\n if (verdict === \"ok\" || verdict === \"rate_limited\") return \"admit\";\n if (verdict === \"rejected\") {\n recordAuthFailure(failures, credentialId, now);\n return \"unauthorized\";\n }\n return \"unavailable\";\n}\n","/**\n * OAuth redirect_uri policy for the remote MCP HTTP server (POST /register, GET+POST /authorize).\n *\n * Why this exists: /oauth/token returns the caller's raw `pb_sk_*` API key as the OAuth access\n * token (intentional: it survives Railway restarts). Before this policy, /register stored any\n * redirect_uris and /authorize auto-registered any https redirect_uri for an unknown client_id,\n * so a link on our real domain could send a victim's pasted key to an attacker:\n * 1. attacker builds /authorize?client_id=x&redirect_uri=https://evil.example/cb&code_challenge=...\n * 2. victim pastes their pb_sk_* key into our real page\n * 3. the one-time code lands at evil.example; the attacker redeems it at /oauth/token for the key.\n *\n * The policy, applied at every entry point:\n * - https redirect_uris are accepted only as an EXACT redirect URI (scheme + host + path) on the\n * allowlist: the platforms' own documented callbacks below, plus OAUTH_REDIRECT_HOST_ALLOWLIST\n * entries. A host alone is not enough: an allowed host can serve a callback path that belongs\n * to someone else (a custom GPT's `https://chatgpt.com/aip/{g-ID}/oauth/callback` exchanges the\n * code at a token URL its builder chose). The host compares case-insensitively; the path\n * compares exactly, so a trailing slash or an extra segment is refused. CORS_ORIGINS is\n * deliberately NOT included: a CORS origin is a browser-fetch permission, not a place we send\n * an API key.\n * - http redirect_uris are accepted only on a loopback host (localhost, 127.0.0.1, [::1]), any\n * port and path. This is the RFC 8252 section 7.3 native-app exception (Claude Code and other\n * local MCP clients use it) and is on in production: the browser follows the redirect on the\n * victim's OWN machine, so an attacker cannot receive the code unless they already run code there.\n * - https must use the default port (empty or 443) and carry no query; loopback keeps any port.\n * - no credentials in the URI, no fragment, at most 2048 characters.\n * Every check returns a plain-language refusal reason, or null when the URI is allowed. Callers\n * respond 400 with that reason; nothing here ever fails open.\n */\n\n/**\n * Built-in exact https redirect URIs (origin + path).\n * - https://claude.ai/api/mcp/auth_callback: the hosted Claude apps' one callback,\n * https://claude.com/docs/connectors/building/authentication (\"Callback URLs\").\n * - https://claude.com/api/mcp/auth_callback: the announced move of that callback to claude.com.\n * Anthropic's current page above names only claude.ai; the claude.com form is listed by\n * https://docs.strata.io/guides/ai-identity/connect/claude. Same owner, same path.\n * - https://chatgpt.com/connector_platform_oauth_redirect: ChatGPT's stable connector callback,\n * https://developers.openai.com/plugins/build/auth.\n * - https://www.cursor.com/agents/mcp/oauth/callback: Cursor web and Cursor agents' one callback,\n * https://cursor.com/docs/context/mcp (TASK-1005). Cursor desktop uses loopback, allowed below;\n * its cursor:// scheme stays refused. Every MCP server a person connects in Cursor shares this\n * URL and is told apart by state alone (TEN-5409, open).\n */\nexport const DEFAULT_OAUTH_REDIRECT_URIS: readonly string[] = [\n \"https://claude.ai/api/mcp/auth_callback\",\n \"https://claude.com/api/mcp/auth_callback\",\n \"https://chatgpt.com/connector_platform_oauth_redirect\",\n \"https://www.cursor.com/agents/mcp/oauth/callback\",\n];\n\n/**\n * ChatGPT's per-connection callback `https://chatgpt.com/connector/oauth/{callback_id}`\n * (https://developers.openai.com/plugins/build/auth). Exactly one id segment, nothing after it.\n */\nconst CHATGPT_CONNECTOR_CALLBACK = /^https:\\/\\/chatgpt\\.com\\/connector\\/oauth\\/[A-Za-z0-9_-]+$/;\n\n/** Hostnames as `URL.hostname` reports them (IPv6 keeps its brackets). */\nconst LOOPBACK_HOSTS: readonly string[] = [\"localhost\", \"127.0.0.1\", \"[::1]\"];\n\nconst MAX_REDIRECT_URI_LENGTH = 2048;\nconst MAX_REGISTRATION_REDIRECTS = 10;\n\nconst OPERATOR_ENTRY_FORM =\n \"OAUTH_REDIRECT_HOST_ALLOWLIST entries must be full https redirect URIs such as https://partner.example/oauth/callback (no bare host, no query, no fragment)\";\n\n/** The effective https redirect policy. A non-null `configError` refuses every redirect_uri. */\nexport interface OAuthRedirectAllowlist {\n uris: readonly string[];\n configError: string | null;\n}\n\n/** `origin + pathname` of an absolute URL: the form every allowlist entry is compared in. */\nfunction exactForm(url: URL): string {\n return `${url.origin}${url.pathname}`;\n}\n\n/**\n * Parse OAUTH_REDIRECT_HOST_ALLOWLIST (comma-separated). Every entry must be a full https redirect\n * URI with a path; one bare host or malformed entry makes the whole value a config error, so OAuth\n * fails closed until the operator fixes it (the error names the required form).\n */\nexport function parseRedirectUriAllowlist(raw: string | undefined): { uris: string[]; configError: string | null } {\n if (!raw) return { uris: [], configError: null };\n const uris: string[] = [];\n for (const part of raw.split(\",\")) {\n const trimmed = part.trim();\n if (!trimmed) continue;\n let url: URL | null = null;\n try {\n url = trimmed.includes(\"://\") ? new URL(trimmed) : null;\n } catch {\n url = null;\n }\n const valid =\n url !== null &&\n url.protocol === \"https:\" &&\n !url.username && !url.password && !url.search && !url.hash &&\n (url.port === \"\" || url.port === \"443\") &&\n url.pathname !== \"/\";\n if (!valid || url === null) {\n return { uris: [], configError: `${OPERATOR_ENTRY_FORM}; got \"${trimmed}\"` };\n }\n uris.push(exactForm(url));\n }\n return { uris, configError: null };\n}\n\n/** Effective allowlist: built-in exact URIs + OAUTH_REDIRECT_HOST_ALLOWLIST, nothing else. */\nexport function resolveOAuthRedirectAllowlist(opts: { explicitAllowlist?: string } = {}): OAuthRedirectAllowlist {\n const parsed = parseRedirectUriAllowlist(opts.explicitAllowlist);\n return {\n uris: [...new Set<string>([...DEFAULT_OAUTH_REDIRECT_URIS, ...parsed.uris])],\n configError: parsed.configError,\n };\n}\n\n/**\n * Check one redirect_uri. Returns the refusal reason, or null when it is allowed.\n */\nexport function checkRedirectUri(redirectUri: unknown, allowlist: OAuthRedirectAllowlist): string | null {\n if (allowlist.configError) return `OAuth is misconfigured: ${allowlist.configError}`;\n if (typeof redirectUri !== \"string\" || redirectUri.length === 0) {\n return \"redirect_uri is required\";\n }\n if (redirectUri.length > MAX_REDIRECT_URI_LENGTH) {\n return `redirect_uri is longer than ${MAX_REDIRECT_URI_LENGTH} characters`;\n }\n let url: URL;\n try {\n url = new URL(redirectUri);\n } catch {\n return \"redirect_uri is not a valid absolute URL\";\n }\n if (url.username || url.password) return \"redirect_uri must not contain credentials\";\n if (url.hash) return \"redirect_uri must not contain a fragment\";\n\n const host = url.hostname.toLowerCase();\n if (url.protocol === \"http:\") {\n return LOOPBACK_HOSTS.includes(host)\n ? null\n : \"redirect_uri must use https (http is allowed only for localhost, 127.0.0.1 or [::1])\";\n }\n if (url.protocol === \"https:\") {\n if (url.port && url.port !== \"443\") return \"redirect_uri must use the default https port (443)\";\n if (url.search) return \"redirect_uri must not contain a query\";\n const exact = exactForm(url);\n return allowlist.uris.includes(exact) || CHATGPT_CONNECTOR_CALLBACK.test(exact)\n ? null\n : \"redirect_uri is not an allowed callback (an operator can add the full URI with OAUTH_REDIRECT_HOST_ALLOWLIST)\";\n }\n return \"redirect_uri scheme is not allowed (use https, or http on loopback)\";\n}\n\n/** A registration's redirect_uris split by the policy; `refusal` is set when the request must be refused. */\nexport interface RegistrationRedirects {\n refusal: string | null;\n allowed: string[];\n dropped: { uri: unknown; reason: string }[];\n}\n\n/**\n * Check a whole registration `redirect_uris` value (DEC-2203). The registration is accepted when at\n * least one entry passes, and the 201 lists only the passing entries (RFC 7591 lets the server\n * return altered metadata); it is refused (invalid_redirect_uri) only when none passes, or when the\n * value is not a 1-to-10 entry array. Cursor sends cursor://, its web callback and a loopback URI in\n * one array. /authorize re-checks the redirect on every request, so dropping refuses nothing extra.\n */\nexport function checkRegistrationRedirects(redirectUris: unknown, allowlist: OAuthRedirectAllowlist): RegistrationRedirects {\n const refused = (refusal: string): RegistrationRedirects => ({ refusal, allowed: [], dropped: [] });\n if (!Array.isArray(redirectUris) || redirectUris.length === 0) return refused(\"redirect_uris is required\");\n if (redirectUris.length > MAX_REGISTRATION_REDIRECTS) {\n return refused(`redirect_uris may hold at most ${MAX_REGISTRATION_REDIRECTS} entries`);\n }\n const allowed: string[] = [];\n const dropped: { uri: unknown; reason: string }[] = [];\n for (const uri of redirectUris) {\n const reason = checkRedirectUri(uri, allowlist);\n if (reason) dropped.push({ uri, reason });\n else allowed.push(uri as string);\n }\n return { refusal: allowed.length === 0 ? dropped[0].reason : null, allowed, dropped };\n}\n","/**\n * The one-time authorization code minted by POST /authorize and redeemed at POST /oauth/token.\n * The token door decides here whether a stored code may still be redeemed, so expiry is enforced\n * at redemption and not only by the 60-second sweep in http.ts.\n */\nexport interface PendingAuth {\n apiKey: string;\n codeChallenge: string;\n redirectUri: string;\n expiresAt: number;\n}\n\n/**\n * The api_key field POST /authorize received, as the key the person meant (TASK-668, TEN-4580).\n * Onboarding shows the key inside JSON, so people paste it with its quotes. Trim, then strip one\n * matching pair of ' \" ` quotes and trim again. Input hygiene at the door (RULE-632: not a\n * fallback for old data). A missing or non-string field (a repeated form field arrives as an\n * array) reads as empty, which the prefix check then refuses.\n */\nexport function normalisePastedKey(raw: unknown): string {\n if (typeof raw !== \"string\") return \"\";\n const trimmed = raw.trim();\n const first = trimmed[0];\n if (trimmed.length >= 2 && (first === '\"' || first === \"'\" || first === \"`\") && trimmed.endsWith(first)) {\n return trimmed.slice(1, -1).trim();\n }\n return trimmed;\n}\n\n/** Why a code cannot be redeemed, or null when it can (PKCE is checked separately). */\nexport function pendingCodeRefusal(\n pending: PendingAuth | undefined,\n redirectUri: unknown,\n now: number,\n): \"unknown_code\" | \"redirect_mismatch\" | \"expired\" | null {\n if (!pending) return \"unknown_code\";\n if (pending.redirectUri !== redirectUri) return \"redirect_mismatch\";\n if (now > pending.expiresAt) return \"expired\";\n return null;\n}\n\n/**\n * The POST /oauth/token response body (DEC-7: the access token IS the caller's pb_sk_ key).\n *\n * TEN-4548: no refresh_token. The key is valid until revoked in Cortex, so a refresh grant could\n * only re-issue the same key; the old pb_rt_ token carried that key base64url-readable and was\n * never revoked, a second place the key leaked for no security gain. The MCP authorization spec\n * makes refresh tokens optional, so a client re-authorizes when the key is rejected.\n */\nexport function tokenResponse(apiKey: string): {\n access_token: string;\n token_type: \"Bearer\";\n expires_in: number;\n} {\n return {\n access_token: apiKey,\n token_type: \"Bearer\",\n // 1 year: validity is enforced by Convex on every call, not by this clock.\n expires_in: 365 * 24 * 3600,\n };\n}\n"],"mappings":"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AAA;AAAA;AAAA,SAGA,IAAI,KAAK,CAAC;AAAA;AAAA;AAAA;AAAA,EAIjB,SAAS;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EA8BT,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA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","names":["sessions","sid"]}
@@ -135,7 +135,7 @@ async function runSetup() {
135
135
  if (choice === 0) printDeeplink(apiKey);
136
136
  log("");
137
137
  log(
138
- ` ${green("\u2713")} Done! Restart your AI assistant and try: ${bold('"Start PB"')}`
138
+ ` ${green("\u2713")} Done! Restart your AI assistant and try: ${bold('"Start my Product Brain session."')}`
139
139
  );
140
140
  printHelpLink();
141
141
  }
@@ -196,4 +196,4 @@ export {
196
196
  resolveSetupSiteUrl,
197
197
  runSetup
198
198
  };
199
- //# sourceMappingURL=setup-J7JZV6FC.js.map
199
+ //# sourceMappingURL=setup-MDXQKHPZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/setup.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * `npx @productbrain/mcp@beta setup`\n *\n * Guided onboarding: get API key from the app, paste it, write MCP config.\n */\n\nimport { execSync } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport { resolveClient, writeClientConfig, MCP_NPX_PACKAGE, type McpClientInfo } from \"./config-writer.js\";\nimport { DEFAULT_CLOUD_URL } from \"../client.js\";\nimport { apiKeysUrl } from \"../lib/appUrl.js\";\nimport { warnOnProdFallthrough } from \"../prod-fallthrough.js\";\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nfunction bold(s: string) {\n return `\\x1b[1m${s}\\x1b[0m`;\n}\nfunction green(s: string) {\n return `\\x1b[32m${s}\\x1b[0m`;\n}\nfunction dim(s: string) {\n return `\\x1b[2m${s}\\x1b[0m`;\n}\nfunction orange(s: string) {\n return `\\x1b[33m${s}\\x1b[0m`;\n}\n\nfunction log(msg: string) {\n process.stdout.write(`${msg}\\n`);\n}\n\nfunction openBrowser(url: string) {\n const platform = process.platform;\n try {\n if (platform === \"darwin\") execSync(`open \"${url}\"`);\n else if (platform === \"win32\") execSync(`start \"\" \"${url}\"`);\n else execSync(`xdg-open \"${url}\"`);\n } catch {\n log(dim(` Could not open browser automatically.`));\n log(` Open this URL manually: ${url}`);\n }\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nfunction promptChoice(question: string, choices: string[]): Promise<number> {\n return new Promise((resolve) => {\n log(\"\");\n log(bold(question));\n choices.forEach((c, i) => log(` ${i + 1}) ${c}`));\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(`\\n ${dim(\"Choice [1]:\")} `, (line) => {\n rl.close();\n const n = parseInt(line.trim(), 10);\n if (isNaN(n) || n < 1 || n > choices.length) {\n resolve(0);\n } else {\n resolve(n - 1);\n }\n });\n });\n}\n\n// ── Workspace Verification ───────────────────────────────────────────────\n\n/**\n * Resolve the gateway URL the setup flow probes. TEN-2382: setup is an\n * independent entry point (no bootstrap), so it warns on silent prod\n * fall-through here. DEFAULT_CLOUD_URL is imported from client.ts (the\n * in-package SSOT) — no local re-declare.\n */\nexport function resolveSetupSiteUrl(): string {\n const explicit = process.env.CONVEX_SITE_URL ?? process.env.PRODUCTBRAIN_URL;\n const url = explicit ?? DEFAULT_CLOUD_URL;\n warnOnProdFallthrough(url, { explicit: explicit != null });\n return url;\n}\n\nasync function verifyWorkspace(\n apiKey: string,\n): Promise<{ name: string; slug: string } | null> {\n const siteUrl = resolveSetupSiteUrl();\n\n try {\n const res = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/aki`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ fn: \"resolveWorkspace\", args: {} }),\n });\n\n if (!res.ok) return null;\n const json = (await res.json()) as {\n data?: { name: string; slug: string } | null;\n error?: string;\n };\n return json.data ?? null;\n } catch {\n return null;\n }\n}\n\n// ── Main ────────────────────────────────────────────────────────────────\n\nexport async function runSetup() {\n log(\"\");\n log(bold(` Product${orange(\"Brain\")} Setup`));\n log(dim(\" Connect your AI assistant to your chain\\n\"));\n\n const keysUrl = apiKeysUrl();\n\n log(` ${dim(\"1. Get your API key from Settings → API Keys\")}`);\n log(` ${dim(keysUrl)}\\n`);\n\n const openNow = await prompt(` Open this URL in your browser? [Y/n]: `);\n if (openNow.toLowerCase() !== \"n\" && openNow.toLowerCase() !== \"no\") {\n openBrowser(keysUrl);\n }\n\n log(\"\");\n log(` ${dim(\"2. Generate a key (if you don't have one), then copy it.\\n\")}`);\n\n const apiKey = await prompt(` Paste your API key (pb_sk_...): `);\n\n if (!apiKey || !apiKey.startsWith(\"pb_sk_\")) {\n log(` ${orange(\"!\")} Invalid key format. Keys start with pb_sk_.`);\n log(` Get one at ${keysUrl}\\n`);\n process.exit(1);\n }\n\n log(` ${green(\"✓\")} Key received`);\n\n const workspace = await verifyWorkspace(apiKey);\n if (workspace) {\n log(` ${green(\"✓\")} Connected to workspace: ${bold(workspace.name)} ${dim(`(${workspace.slug})`)}`);\n } else {\n log(` ${orange(\"!\")} Could not verify workspace. Check your key at ${keysUrl}`);\n }\n log(\"\");\n\n const CLIENT_NAMES = [\"Cursor\", \"Claude Desktop\"] as const;\n const options = [...CLIENT_NAMES, \"Other\"];\n\n const choice = await promptChoice(\"Where do you want to set up Product Brain?\", options);\n\n if (choice === 2) {\n printConfigSnippet(apiKey);\n } else {\n const client = resolveClient(CLIENT_NAMES[choice]);\n if (client) {\n await writeConfig(client, apiKey);\n } else {\n log(` ${orange(\"!\")} ${CLIENT_NAMES[choice]} config path not available on this platform.`);\n printConfigSnippet(apiKey);\n }\n }\n\n // TASK-670 (WP-820, DEC-1863): no Cursor rule file and no CLAUDE.md snippet; the connector's\n // session start teaches the agent how to work here, one source.\n if (choice === 0) printDeeplink(apiKey);\n\n log(\"\");\n log(\n ` ${green(\"✓\")} Done! Restart your AI assistant and try: ${bold('\"Start my Product Brain session.\"')}`,\n );\n printHelpLink();\n}\n\nasync function writeConfig(\n client: McpClientInfo,\n apiKey: string,\n): Promise<void> {\n try {\n const wrote = await writeClientConfig(client, apiKey);\n if (wrote) {\n log(` ${green(\"✓\")} Wrote config to ${dim(client.configPath)}`);\n } else {\n log(` ${dim(\"ℹ\")} ${client.name} already configured — skipped`);\n }\n } catch (err: any) {\n log(` ${orange(\"!\")} Could not write ${client.name} config: ${err.message}`);\n printConfigSnippet(apiKey);\n }\n}\n\nfunction printHelpLink() {\n log(` ${dim(`Need help? See ${apiKeysUrl()}`)}`);\n log(\"\");\n}\n\nfunction printConfigSnippet(apiKey: string) {\n log(\"\");\n log(bold(\" Add this to your MCP client config:\\n\"));\n const snippet = JSON.stringify(\n {\n mcpServers: {\n \"Product Brain\": {\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n },\n },\n },\n null,\n 2,\n );\n for (const line of snippet.split(\"\\n\")) {\n log(` ${line}`);\n }\n log(\"\");\n}\n\nfunction buildDeeplink(apiKey: string): string {\n const config = JSON.stringify({\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n });\n const encoded = Buffer.from(config).toString(\"base64url\");\n return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(\"Product Brain\")}&config=${encoded}`;\n}\n\nfunction printDeeplink(apiKey: string): void {\n const link = buildDeeplink(apiKey);\n log(\"\");\n log(` ${dim(\"One-click install for Cursor (paste in browser):\")}`);\n log(` ${link}`);\n}\n\n"],"mappings":";;;;;;;;;;;AAQA,SAAS,gBAAgB;AACzB,SAAS,uBAAuB;AAQhC,SAAS,KAAK,GAAW;AACvB,SAAO,UAAU,CAAC;AACpB;AACA,SAAS,MAAM,GAAW;AACxB,SAAO,WAAW,CAAC;AACrB;AACA,SAAS,IAAI,GAAW;AACtB,SAAO,UAAU,CAAC;AACpB;AACA,SAAS,OAAO,GAAW;AACzB,SAAO,WAAW,CAAC;AACrB;AAEA,SAAS,IAAI,KAAa;AACxB,UAAQ,OAAO,MAAM,GAAG,GAAG;AAAA,CAAI;AACjC;AAEA,SAAS,YAAY,KAAa;AAChC,QAAM,WAAW,QAAQ;AACzB,MAAI;AACF,QAAI,aAAa,SAAU,UAAS,SAAS,GAAG,GAAG;AAAA,aAC1C,aAAa,QAAS,UAAS,aAAa,GAAG,GAAG;AAAA,QACtD,UAAS,aAAa,GAAG,GAAG;AAAA,EACnC,QAAQ;AACN,QAAI,IAAI,yCAAyC,CAAC;AAClD,QAAI,6BAA6B,GAAG,EAAE;AAAA,EACxC;AACF;AAEA,SAAS,OAAO,UAAmC;AACjD,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,OAAG,SAAS,UAAU,CAAC,WAAW;AAChC,SAAG,MAAM;AACT,cAAQ,OAAO,KAAK,CAAC;AAAA,IACvB,CAAC;AAAA,EACH,CAAC;AACH;AAEA,SAAS,aAAa,UAAkB,SAAoC;AAC1E,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI,EAAE;AACN,QAAI,KAAK,QAAQ,CAAC;AAClB,YAAQ,QAAQ,CAAC,GAAG,MAAM,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;AACjD,UAAM,KAAK,gBAAgB,EAAE,OAAO,QAAQ,OAAO,QAAQ,QAAQ,OAAO,CAAC;AAC3E,OAAG,SAAS;AAAA,IAAO,IAAI,aAAa,CAAC,KAAK,CAAC,SAAS;AAClD,SAAG,MAAM;AACT,YAAM,IAAI,SAAS,KAAK,KAAK,GAAG,EAAE;AAClC,UAAI,MAAM,CAAC,KAAK,IAAI,KAAK,IAAI,QAAQ,QAAQ;AAC3C,gBAAQ,CAAC;AAAA,MACX,OAAO;AACL,gBAAQ,IAAI,CAAC;AAAA,MACf;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAUO,SAAS,sBAA8B;AAC5C,QAAM,WAAW,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;AAC5D,QAAM,MAAM,YAAY;AACxB,wBAAsB,KAAK,EAAE,UAAU,YAAY,KAAK,CAAC;AACzD,SAAO;AACT;AAEA,eAAe,gBACb,QACgD;AAChD,QAAM,UAAU,oBAAoB;AAEpC,MAAI;AACF,UAAM,MAAM,MAAM,MAAM,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC,YAAY;AAAA,MAC/D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,QAChB,eAAe,UAAU,MAAM;AAAA,MACjC;AAAA,MACA,MAAM,KAAK,UAAU,EAAE,IAAI,oBAAoB,MAAM,CAAC,EAAE,CAAC;AAAA,IAC3D,CAAC;AAED,QAAI,CAAC,IAAI,GAAI,QAAO;AACpB,UAAM,OAAQ,MAAM,IAAI,KAAK;AAI7B,WAAO,KAAK,QAAQ;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIA,eAAsB,WAAW;AAC/B,MAAI,EAAE;AACN,MAAI,KAAK,YAAY,OAAO,OAAO,CAAC,QAAQ,CAAC;AAC7C,MAAI,IAAI,6CAA6C,CAAC;AAEtD,QAAM,UAAU,WAAW;AAE3B,MAAI,KAAK,IAAI,mDAA8C,CAAC,EAAE;AAC9D,MAAI,QAAQ,IAAI,OAAO,CAAC;AAAA,CAAI;AAE5B,QAAM,UAAU,MAAM,OAAO,0CAA0C;AACvE,MAAI,QAAQ,YAAY,MAAM,OAAO,QAAQ,YAAY,MAAM,MAAM;AACnE,gBAAY,OAAO;AAAA,EACrB;AAEA,MAAI,EAAE;AACN,MAAI,KAAK,IAAI,4DAA4D,CAAC,EAAE;AAE5E,QAAM,SAAS,MAAM,OAAO,oCAAoC;AAEhE,MAAI,CAAC,UAAU,CAAC,OAAO,WAAW,QAAQ,GAAG;AAC3C,QAAI,KAAK,OAAO,GAAG,CAAC,8CAA8C;AAClE,QAAI,gBAAgB,OAAO;AAAA,CAAI;AAC/B,YAAQ,KAAK,CAAC;AAAA,EAChB;AAEA,MAAI,KAAK,MAAM,QAAG,CAAC,eAAe;AAElC,QAAM,YAAY,MAAM,gBAAgB,MAAM;AAC9C,MAAI,WAAW;AACb,QAAI,KAAK,MAAM,QAAG,CAAC,4BAA4B,KAAK,UAAU,IAAI,CAAC,IAAI,IAAI,IAAI,UAAU,IAAI,GAAG,CAAC,EAAE;AAAA,EACrG,OAAO;AACL,QAAI,KAAK,OAAO,GAAG,CAAC,kDAAkD,OAAO,EAAE;AAAA,EACjF;AACA,MAAI,EAAE;AAEN,QAAM,eAAe,CAAC,UAAU,gBAAgB;AAChD,QAAM,UAAU,CAAC,GAAG,cAAc,OAAO;AAEzC,QAAM,SAAS,MAAM,aAAa,8CAA8C,OAAO;AAEvF,MAAI,WAAW,GAAG;AAChB,uBAAmB,MAAM;AAAA,EAC3B,OAAO;AACL,UAAM,SAAS,cAAc,aAAa,MAAM,CAAC;AACjD,QAAI,QAAQ;AACV,YAAM,YAAY,QAAQ,MAAM;AAAA,IAClC,OAAO;AACL,UAAI,KAAK,OAAO,GAAG,CAAC,IAAI,aAAa,MAAM,CAAC,8CAA8C;AAC1F,yBAAmB,MAAM;AAAA,IAC3B;AAAA,EACF;AAIA,MAAI,WAAW,EAAG,eAAc,MAAM;AAEtC,MAAI,EAAE;AACN;AAAA,IACE,KAAK,MAAM,QAAG,CAAC,6CAA6C,KAAK,mCAAmC,CAAC;AAAA,EACvG;AACA,gBAAc;AAChB;AAEA,eAAe,YACb,QACA,QACe;AACf,MAAI;AACF,UAAM,QAAQ,MAAM,kBAAkB,QAAQ,MAAM;AACpD,QAAI,OAAO;AACT,UAAI,KAAK,MAAM,QAAG,CAAC,oBAAoB,IAAI,OAAO,UAAU,CAAC,EAAE;AAAA,IACjE,OAAO;AACL,UAAI,KAAK,IAAI,QAAG,CAAC,IAAI,OAAO,IAAI,oCAA+B;AAAA,IACjE;AAAA,EACF,SAAS,KAAU;AACjB,QAAI,KAAK,OAAO,GAAG,CAAC,oBAAoB,OAAO,IAAI,YAAY,IAAI,OAAO,EAAE;AAC5E,uBAAmB,MAAM;AAAA,EAC3B;AACF;AAEA,SAAS,gBAAgB;AACvB,MAAI,KAAK,IAAI,kBAAkB,WAAW,CAAC,EAAE,CAAC,EAAE;AAChD,MAAI,EAAE;AACR;AAEA,SAAS,mBAAmB,QAAgB;AAC1C,MAAI,EAAE;AACN,MAAI,KAAK,yCAAyC,CAAC;AACnD,QAAM,UAAU,KAAK;AAAA,IACnB;AAAA,MACE,YAAY;AAAA,QACV,iBAAiB;AAAA,UACf,SAAS;AAAA,UACT,MAAM,CAAC,MAAM,eAAe;AAAA,UAC5B,KAAK,EAAE,sBAAsB,OAAO;AAAA,QACtC;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACtC,QAAI,OAAO,IAAI,EAAE;AAAA,EACnB;AACA,MAAI,EAAE;AACR;AAEA,SAAS,cAAc,QAAwB;AAC7C,QAAM,SAAS,KAAK,UAAU;AAAA,IAC5B,SAAS;AAAA,IACT,MAAM,CAAC,MAAM,eAAe;AAAA,IAC5B,KAAK,EAAE,sBAAsB,OAAO;AAAA,EACtC,CAAC;AACD,QAAM,UAAU,OAAO,KAAK,MAAM,EAAE,SAAS,WAAW;AACxD,SAAO,uDAAuD,mBAAmB,eAAe,CAAC,WAAW,OAAO;AACrH;AAEA,SAAS,cAAc,QAAsB;AAC3C,QAAM,OAAO,cAAc,MAAM;AACjC,MAAI,EAAE;AACN,MAAI,KAAK,IAAI,kDAAkD,CAAC,EAAE;AAClE,MAAI,KAAK,IAAI,EAAE;AACjB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productbrain/mcp",
3
- "version": "0.0.1-beta.11417",
3
+ "version": "0.0.1-beta.11420",
4
4
  "description": "Product Brain — MCP server for AI-assisted product knowledge management",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli/setup.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * `npx @productbrain/mcp@beta setup`\n *\n * Guided onboarding: get API key from the app, paste it, write MCP config.\n */\n\nimport { execSync } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport { resolveClient, writeClientConfig, MCP_NPX_PACKAGE, type McpClientInfo } from \"./config-writer.js\";\nimport { DEFAULT_CLOUD_URL } from \"../client.js\";\nimport { apiKeysUrl } from \"../lib/appUrl.js\";\nimport { warnOnProdFallthrough } from \"../prod-fallthrough.js\";\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nfunction bold(s: string) {\n return `\\x1b[1m${s}\\x1b[0m`;\n}\nfunction green(s: string) {\n return `\\x1b[32m${s}\\x1b[0m`;\n}\nfunction dim(s: string) {\n return `\\x1b[2m${s}\\x1b[0m`;\n}\nfunction orange(s: string) {\n return `\\x1b[33m${s}\\x1b[0m`;\n}\n\nfunction log(msg: string) {\n process.stdout.write(`${msg}\\n`);\n}\n\nfunction openBrowser(url: string) {\n const platform = process.platform;\n try {\n if (platform === \"darwin\") execSync(`open \"${url}\"`);\n else if (platform === \"win32\") execSync(`start \"\" \"${url}\"`);\n else execSync(`xdg-open \"${url}\"`);\n } catch {\n log(dim(` Could not open browser automatically.`));\n log(` Open this URL manually: ${url}`);\n }\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer) => {\n rl.close();\n resolve(answer.trim());\n });\n });\n}\n\nfunction promptChoice(question: string, choices: string[]): Promise<number> {\n return new Promise((resolve) => {\n log(\"\");\n log(bold(question));\n choices.forEach((c, i) => log(` ${i + 1}) ${c}`));\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(`\\n ${dim(\"Choice [1]:\")} `, (line) => {\n rl.close();\n const n = parseInt(line.trim(), 10);\n if (isNaN(n) || n < 1 || n > choices.length) {\n resolve(0);\n } else {\n resolve(n - 1);\n }\n });\n });\n}\n\n// ── Workspace Verification ───────────────────────────────────────────────\n\n/**\n * Resolve the gateway URL the setup flow probes. TEN-2382: setup is an\n * independent entry point (no bootstrap), so it warns on silent prod\n * fall-through here. DEFAULT_CLOUD_URL is imported from client.ts (the\n * in-package SSOT) — no local re-declare.\n */\nexport function resolveSetupSiteUrl(): string {\n const explicit = process.env.CONVEX_SITE_URL ?? process.env.PRODUCTBRAIN_URL;\n const url = explicit ?? DEFAULT_CLOUD_URL;\n warnOnProdFallthrough(url, { explicit: explicit != null });\n return url;\n}\n\nasync function verifyWorkspace(\n apiKey: string,\n): Promise<{ name: string; slug: string } | null> {\n const siteUrl = resolveSetupSiteUrl();\n\n try {\n const res = await fetch(`${siteUrl.replace(/\\/$/, \"\")}/api/aki`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ fn: \"resolveWorkspace\", args: {} }),\n });\n\n if (!res.ok) return null;\n const json = (await res.json()) as {\n data?: { name: string; slug: string } | null;\n error?: string;\n };\n return json.data ?? null;\n } catch {\n return null;\n }\n}\n\n// ── Main ────────────────────────────────────────────────────────────────\n\nexport async function runSetup() {\n log(\"\");\n log(bold(` Product${orange(\"Brain\")} Setup`));\n log(dim(\" Connect your AI assistant to your chain\\n\"));\n\n const keysUrl = apiKeysUrl();\n\n log(` ${dim(\"1. Get your API key from Settings → API Keys\")}`);\n log(` ${dim(keysUrl)}\\n`);\n\n const openNow = await prompt(` Open this URL in your browser? [Y/n]: `);\n if (openNow.toLowerCase() !== \"n\" && openNow.toLowerCase() !== \"no\") {\n openBrowser(keysUrl);\n }\n\n log(\"\");\n log(` ${dim(\"2. Generate a key (if you don't have one), then copy it.\\n\")}`);\n\n const apiKey = await prompt(` Paste your API key (pb_sk_...): `);\n\n if (!apiKey || !apiKey.startsWith(\"pb_sk_\")) {\n log(` ${orange(\"!\")} Invalid key format. Keys start with pb_sk_.`);\n log(` Get one at ${keysUrl}\\n`);\n process.exit(1);\n }\n\n log(` ${green(\"✓\")} Key received`);\n\n const workspace = await verifyWorkspace(apiKey);\n if (workspace) {\n log(` ${green(\"✓\")} Connected to workspace: ${bold(workspace.name)} ${dim(`(${workspace.slug})`)}`);\n } else {\n log(` ${orange(\"!\")} Could not verify workspace. Check your key at ${keysUrl}`);\n }\n log(\"\");\n\n const CLIENT_NAMES = [\"Cursor\", \"Claude Desktop\"] as const;\n const options = [...CLIENT_NAMES, \"Other\"];\n\n const choice = await promptChoice(\"Where do you want to set up Product Brain?\", options);\n\n if (choice === 2) {\n printConfigSnippet(apiKey);\n } else {\n const client = resolveClient(CLIENT_NAMES[choice]);\n if (client) {\n await writeConfig(client, apiKey);\n } else {\n log(` ${orange(\"!\")} ${CLIENT_NAMES[choice]} config path not available on this platform.`);\n printConfigSnippet(apiKey);\n }\n }\n\n // TASK-670 (WP-820, DEC-1863): no Cursor rule file and no CLAUDE.md snippet; the connector's\n // session start teaches the agent how to work here, one source.\n if (choice === 0) printDeeplink(apiKey);\n\n log(\"\");\n log(\n ` ${green(\"✓\")} Done! Restart your AI assistant and try: ${bold('\"Start PB\"')}`,\n );\n printHelpLink();\n}\n\nasync function writeConfig(\n client: McpClientInfo,\n apiKey: string,\n): Promise<void> {\n try {\n const wrote = await writeClientConfig(client, apiKey);\n if (wrote) {\n log(` ${green(\"✓\")} Wrote config to ${dim(client.configPath)}`);\n } else {\n log(` ${dim(\"ℹ\")} ${client.name} already configured — skipped`);\n }\n } catch (err: any) {\n log(` ${orange(\"!\")} Could not write ${client.name} config: ${err.message}`);\n printConfigSnippet(apiKey);\n }\n}\n\nfunction printHelpLink() {\n log(` ${dim(`Need help? See ${apiKeysUrl()}`)}`);\n log(\"\");\n}\n\nfunction printConfigSnippet(apiKey: string) {\n log(\"\");\n log(bold(\" Add this to your MCP client config:\\n\"));\n const snippet = JSON.stringify(\n {\n mcpServers: {\n \"Product Brain\": {\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n },\n },\n },\n null,\n 2,\n );\n for (const line of snippet.split(\"\\n\")) {\n log(` ${line}`);\n }\n log(\"\");\n}\n\nfunction buildDeeplink(apiKey: string): string {\n const config = JSON.stringify({\n command: \"npx\",\n args: [\"-y\", MCP_NPX_PACKAGE],\n env: { PRODUCTBRAIN_API_KEY: apiKey },\n });\n const encoded = Buffer.from(config).toString(\"base64url\");\n return `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodeURIComponent(\"Product Brain\")}&config=${encoded}`;\n}\n\nfunction printDeeplink(apiKey: string): void {\n const link = buildDeeplink(apiKey);\n log(\"\");\n log(` ${dim(\"One-click install for Cursor (paste in browser):\")}`);\n log(` ${link}`);\n}\n\n"],"mappings":"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","names":[]}