@productbrain/mcp 0.0.1-beta.9760 → 0.0.1-beta.9775
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +40 -46
- package/dist/http.js.map +1 -1
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -170,25 +170,24 @@ var AUTH_FAILURE_MAX = 10;
|
|
|
170
170
|
var AUTH_FAILURE_WINDOW_MS = 5 * 6e4;
|
|
171
171
|
var AUTH_BLOCK_DURATION_MS = 15 * 6e4;
|
|
172
172
|
var MAX_AUTH_FAILURE_ENTRIES = 1e4;
|
|
173
|
+
function rejected(presented) {
|
|
174
|
+
return { status: "rejected", credentialId: hashKey(presented) };
|
|
175
|
+
}
|
|
173
176
|
function resolveBearerAuth(authorizationHeader) {
|
|
174
177
|
if (typeof authorizationHeader !== "string" || authorizationHeader.trim() === "") {
|
|
175
178
|
return { status: "absent" };
|
|
176
179
|
}
|
|
177
180
|
if (!authorizationHeader.startsWith("Bearer ")) {
|
|
178
|
-
return
|
|
181
|
+
return rejected(authorizationHeader.trim());
|
|
179
182
|
}
|
|
180
183
|
const token = authorizationHeader.slice(7).trim();
|
|
181
|
-
if (token
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
if (token.startsWith("pb_sk_")) {
|
|
185
|
-
return { status: "ok", apiKey: token };
|
|
186
|
-
}
|
|
187
|
-
return { status: "rejected" };
|
|
184
|
+
if (!token.startsWith("pb_sk_")) return rejected(token);
|
|
185
|
+
return { status: "ok", apiKey: token, credentialId: hashKey(token) };
|
|
188
186
|
}
|
|
189
187
|
function resolvePastedKey(apiKey) {
|
|
190
188
|
if (apiKey === "") return { status: "absent" };
|
|
191
|
-
|
|
189
|
+
if (!apiKey.startsWith("pb_sk_")) return rejected(apiKey);
|
|
190
|
+
return { status: "ok", apiKey, credentialId: hashKey(apiKey) };
|
|
192
191
|
}
|
|
193
192
|
function isCrossSiteFormPost(origin, secFetchSite, ownOrigin) {
|
|
194
193
|
if (secFetchSite === "cross-site") return true;
|
|
@@ -199,21 +198,26 @@ function isCrossSiteFormPost(origin, secFetchSite, ownOrigin) {
|
|
|
199
198
|
return true;
|
|
200
199
|
}
|
|
201
200
|
}
|
|
202
|
-
function checkAuthBlock(failures,
|
|
203
|
-
const rec = failures.get(
|
|
201
|
+
function checkAuthBlock(failures, credentialId, now) {
|
|
202
|
+
const rec = failures.get(credentialId);
|
|
204
203
|
if (!rec) return false;
|
|
205
204
|
return rec.blockedUntil > now;
|
|
206
205
|
}
|
|
207
|
-
function authLockoutError(failures,
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
function authLockoutError(failures, auth, now, id = null) {
|
|
207
|
+
if (auth.status === "absent") return null;
|
|
208
|
+
const rec = failures.get(auth.credentialId);
|
|
209
|
+
if (!rec || !checkAuthBlock(failures, auth.credentialId, now)) return null;
|
|
210
210
|
const retryAfterSeconds = Math.max(0, Math.ceil((rec.blockedUntil - now) / 1e3));
|
|
211
211
|
return buildAuthLockoutError({ retryAfterSeconds, limit: AUTH_FAILURE_MAX, current: rec.count, id });
|
|
212
212
|
}
|
|
213
|
-
function recordAuthFailure(failures,
|
|
214
|
-
const rec = failures.get(
|
|
213
|
+
function recordAuthFailure(failures, credentialId, now) {
|
|
214
|
+
const rec = failures.get(credentialId);
|
|
215
215
|
if (!rec) {
|
|
216
|
-
failures.
|
|
216
|
+
if (failures.size >= MAX_AUTH_FAILURE_ENTRIES) {
|
|
217
|
+
const oldest = failures.keys().next().value;
|
|
218
|
+
if (oldest !== void 0) failures.delete(oldest);
|
|
219
|
+
}
|
|
220
|
+
failures.set(credentialId, { count: 1, firstFailure: now, blockedUntil: 0 });
|
|
217
221
|
return;
|
|
218
222
|
}
|
|
219
223
|
if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {
|
|
@@ -227,10 +231,10 @@ function recordAuthFailure(failures, ip, now) {
|
|
|
227
231
|
}
|
|
228
232
|
}
|
|
229
233
|
}
|
|
230
|
-
function keyVerdictGate(verdict, failures,
|
|
234
|
+
function keyVerdictGate(verdict, failures, credentialId, now) {
|
|
231
235
|
if (verdict === "ok" || verdict === "rate_limited") return "admit";
|
|
232
236
|
if (verdict === "rejected") {
|
|
233
|
-
recordAuthFailure(failures,
|
|
237
|
+
recordAuthFailure(failures, credentialId, now);
|
|
234
238
|
return "unauthorized";
|
|
235
239
|
}
|
|
236
240
|
return "unavailable";
|
|
@@ -456,15 +460,9 @@ setInterval(() => {
|
|
|
456
460
|
for (const [id, client] of registeredClients) {
|
|
457
461
|
if (now - client.registeredAt > 24 * 60 * 6e4) registeredClients.delete(id);
|
|
458
462
|
}
|
|
459
|
-
for (const [
|
|
463
|
+
for (const [credentialId, rec] of authFailures) {
|
|
460
464
|
if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {
|
|
461
|
-
authFailures.delete(
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {
|
|
465
|
-
const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);
|
|
466
|
-
for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {
|
|
467
|
-
authFailures.delete(sorted[i][0]);
|
|
465
|
+
authFailures.delete(credentialId);
|
|
468
466
|
}
|
|
469
467
|
}
|
|
470
468
|
}, 6e4);
|
|
@@ -1067,18 +1065,17 @@ app.post(
|
|
|
1067
1065
|
sendError("Request refused", "This form only accepts keys sent from its own sign-in page. Open the connect link again.", 403);
|
|
1068
1066
|
return;
|
|
1069
1067
|
}
|
|
1070
|
-
const reqIp = req.ip ?? "unknown";
|
|
1071
1068
|
const nowTs = Date.now();
|
|
1072
|
-
const
|
|
1069
|
+
const pasted = resolvePastedKey(api_key);
|
|
1070
|
+
const lockout = authLockoutError(authFailures, pasted, nowTs);
|
|
1073
1071
|
if (lockout) {
|
|
1074
1072
|
const retryAfter = lockout.headers["Retry-After"];
|
|
1075
1073
|
res.setHeader("Retry-After", retryAfter);
|
|
1076
|
-
sendError("Too many attempts", `
|
|
1074
|
+
sendError("Too many attempts", `This key was not recognized too many times. Try again in ${retryAfter}s.`, 429);
|
|
1077
1075
|
return;
|
|
1078
1076
|
}
|
|
1079
|
-
const pasted = resolvePastedKey(api_key);
|
|
1080
1077
|
if (pasted.status !== "ok") {
|
|
1081
|
-
if (pasted.status === "rejected") recordAuthFailure(authFailures,
|
|
1078
|
+
if (pasted.status === "rejected") recordAuthFailure(authFailures, pasted.credentialId, nowTs);
|
|
1082
1079
|
sendError("Invalid key format", "API keys start with <code>pb_sk_</code>. Check your key and try again.");
|
|
1083
1080
|
return;
|
|
1084
1081
|
}
|
|
@@ -1110,7 +1107,7 @@ app.post(
|
|
|
1110
1107
|
}
|
|
1111
1108
|
let workspaceName = "Your Workspace";
|
|
1112
1109
|
const check = await checkKeyAgainstCandidates(keyCheckCandidates(), api_key, 5e3);
|
|
1113
|
-
const gate = keyVerdictGate(check.verdict, authFailures,
|
|
1110
|
+
const gate = keyVerdictGate(check.verdict, authFailures, pasted.credentialId, nowTs);
|
|
1114
1111
|
if (check.verdict === "ok") {
|
|
1115
1112
|
if (check.workspaceName) workspaceName = check.workspaceName;
|
|
1116
1113
|
getKeyState(api_key).deploymentUrl = check.deploymentUrl;
|
|
@@ -1264,17 +1261,16 @@ function logSessionLifecycle(event, sessionId, reason) {
|
|
|
1264
1261
|
`);
|
|
1265
1262
|
}
|
|
1266
1263
|
app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
1267
|
-
const reqIp = req.ip ?? "unknown";
|
|
1268
1264
|
const nowTs = Date.now();
|
|
1269
|
-
const
|
|
1265
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1266
|
+
const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
|
|
1270
1267
|
if (lockout) {
|
|
1271
1268
|
send429(res, lockout);
|
|
1272
1269
|
return;
|
|
1273
1270
|
}
|
|
1274
|
-
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1275
1271
|
if (auth.status !== "ok") {
|
|
1276
1272
|
logRequest("POST", "auth_fail");
|
|
1277
|
-
if (auth.status === "rejected") recordAuthFailure(authFailures,
|
|
1273
|
+
if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
|
|
1278
1274
|
send401(req, res);
|
|
1279
1275
|
return;
|
|
1280
1276
|
}
|
|
@@ -1304,7 +1300,7 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1304
1300
|
}
|
|
1305
1301
|
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
1306
1302
|
const check = await checkKeyAgainstCandidates(keyCheckCandidates(), apiKey, 5e3);
|
|
1307
|
-
const gate = keyVerdictGate(check.verdict, authFailures,
|
|
1303
|
+
const gate = keyVerdictGate(check.verdict, authFailures, auth.credentialId, Date.now());
|
|
1308
1304
|
if (gate === "unauthorized") {
|
|
1309
1305
|
logRequest("POST", "auth_fail");
|
|
1310
1306
|
send401(req, res);
|
|
@@ -1419,17 +1415,16 @@ app.post("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1419
1415
|
}
|
|
1420
1416
|
});
|
|
1421
1417
|
app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
1422
|
-
const reqIp = req.ip ?? "unknown";
|
|
1423
1418
|
const nowTs = Date.now();
|
|
1424
|
-
const
|
|
1419
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1420
|
+
const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
|
|
1425
1421
|
if (lockout) {
|
|
1426
1422
|
send429(res, lockout);
|
|
1427
1423
|
return;
|
|
1428
1424
|
}
|
|
1429
|
-
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1430
1425
|
if (auth.status !== "ok") {
|
|
1431
1426
|
logRequest("GET", "auth_fail");
|
|
1432
|
-
if (auth.status === "rejected") recordAuthFailure(authFailures,
|
|
1427
|
+
if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
|
|
1433
1428
|
send401(req, res);
|
|
1434
1429
|
return;
|
|
1435
1430
|
}
|
|
@@ -1467,17 +1462,16 @@ app.get("/mcp", mcpLimiter, async (req, res) => {
|
|
|
1467
1462
|
}
|
|
1468
1463
|
});
|
|
1469
1464
|
app.delete("/mcp", mcpLimiter, async (req, res) => {
|
|
1470
|
-
const reqIp = req.ip ?? "unknown";
|
|
1471
1465
|
const nowTs = Date.now();
|
|
1472
|
-
const
|
|
1466
|
+
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1467
|
+
const lockout = authLockoutError(authFailures, auth, nowTs, req.body?.id ?? null);
|
|
1473
1468
|
if (lockout) {
|
|
1474
1469
|
send429(res, lockout);
|
|
1475
1470
|
return;
|
|
1476
1471
|
}
|
|
1477
|
-
const auth = resolveBearerAuth(req.headers?.authorization);
|
|
1478
1472
|
if (auth.status !== "ok") {
|
|
1479
1473
|
logRequest("DELETE", "auth_fail");
|
|
1480
|
-
if (auth.status === "rejected") recordAuthFailure(authFailures,
|
|
1474
|
+
if (auth.status === "rejected") recordAuthFailure(authFailures, auth.credentialId, nowTs);
|
|
1481
1475
|
send401(req, res);
|
|
1482
1476
|
return;
|
|
1483
1477
|
}
|
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 { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.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, MAX_AUTH_FAILURE_ENTRIES, 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();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\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.\n for (const [ip, rec] of authFailures) {\n if (rec.blockedUntil < now && rec.firstFailure + AUTH_FAILURE_WINDOW_MS < now) {\n authFailures.delete(ip);\n }\n }\n // Cap authFailures map size.\n if (authFailures.size > MAX_AUTH_FAILURE_ENTRIES) {\n const sorted = [...authFailures.entries()].sort((a, b) => a[1].firstFailure - b[1].firstFailure);\n for (let i = 0; i < sorted.length - MAX_AUTH_FAILURE_ENTRIES; i++) {\n authFailures.delete(sorted[i][0]);\n }\n }\n}, 60_000);\n\nfunction esc(s: unknown): string {\n return String(s ?? \"\").replace(/[&\"'<>]/g, (c) =>\n ({ \"&\": \"&\", '\"': \""\", \"'\": \"'\", \"<\": \"<\", \">\": \">\" })[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 "Start PB" and return to ${esc(providerName)}\">\n <span class=\"cmd-quote-part\" aria-hidden=\"true\">“</span><span data-cmd-text>Start PB</span><span class=\"cmd-quote-part\" aria-hidden=\"true\">”</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>← Try again</a>\n <a href=\"${esc(apiKeysUrl())}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key →</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 →</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{'<':'<','>':'>','&':'&'}[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{'<':'<','>':'>','&':'&'}[c]});\n var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[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 it touches the lockout map or the kernel, so no page can\n // lock a visitor's IP out by posting garbage keys through their 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: the form is a door a key is presented at, so it shares /mcp's per-IP lockout map. A blocked IP is\n // refused next; a malformed or kernel-rejected key accrues; an empty field never does (TEN-2606).\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n const lockout = authLockoutError(authFailures, reqIp, nowTs);\n if (lockout) {\n const retryAfter = lockout.headers[\"Retry-After\"];\n res.setHeader(\"Retry-After\", retryAfter);\n sendError(\"Too many attempts\", `Too many keys from this network were not recognized. Try again in ${retryAfter}s.`, 429);\n return;\n }\n const pasted = resolvePastedKey(api_key);\n if (pasted.status !== \"ok\") {\n if (pasted.status === \"rejected\") recordAuthFailure(authFailures, reqIp, 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, reqIp, 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// Per-IP progressive lockout for failed API key attempts on every door that takes a key: /mcp and the\n// sign-in form (POST /authorize) share this one map (TEN-4601). Policy + key classification\n// live in ./authLockout.ts (unit-tested); the state map stays here.\nconst authFailures = new Map<string, AuthFailureRecord>();\n\n// ── Health Check ─────────────────────────────────────────────────────────\n\napp.get(\"/health\", (_req: any, res: any) => {\n res.json({ status: \"ok\", version: SERVER_VERSION, transport: \"http\" });\n});\n\n// ── Session Management ──────────────────────────────────────────────────\n\ninterface SessionEntry {\n transport: StreamableHTTPServerTransport;\n lastAccess: number;\n // Fix 3 — short hash of the API key that created this session. Used to\n // detect session hijacking when subsequent requests arrive with a different key.\n keyHash: string;\n // Number of POST tool-call requests currently being handled. Incremented at\n // POST start, decremented in finally. A session with inFlight > 0 is never\n // torn by planKeyAdmission; the only exception is the evictStaleSessions TTL\n // backstop, which force-evicts past SESSION_TTL_MS to self-heal a leaked\n // counter (logged as reason=inflight_leak). GET SSE and DELETE do NOT touch\n // this — see design §3.1.\n inFlight: number;\n}\n\nconst sessions = new Map<string, SessionEntry>();\nconst SESSION_TTL_MS = 30 * 60 * 1000;\nconst MAX_SESSIONS = 200;\n// Grace window: a session touched within this many ms is \"recently active\" and\n// is never evicted by the per-key planner (covers the gap between back-to-back\n// POST calls). See planKeyAdmission in sessionCap.ts.\nconst EVICT_GRACE_MS = 60_000;\n// Fix 6 — MAX_SESSIONS_PER_KEY is an in-process memory/working-set guard, NOT a\n// concurrency policy. Raising MCP_MAX_SESSIONS_PER_KEY does NOT grant more\n// concurrent agents — it only widens the idle-session reclaim window. It must\n// never be the thing that limits how many agents can work; fleet concurrency is\n// a kernel concern (TEN-2229), not a per-key surface cap.\n// Clamped to [1, MAX_SESSIONS - 1]: a value < 1 would make planKeyAdmission\n// throw on every init (RangeError); the MAX_SESSIONS - 1 ceiling guarantees a\n// single key can never alone fill the entire global pool (always leaves >= 1\n// slot), so a bad env degrades to a safe bound rather than breaking init or\n// disabling the cross-key fairness guard. Default 25 is far below this ceiling.\nconst MAX_SESSIONS_PER_KEY = Math.max(\n 1,\n Math.min(MAX_SESSIONS - 1, Number(process.env.MCP_MAX_SESSIONS_PER_KEY) || 25),\n);\n\n// The other eviction site is planKeyAdmission (sessionCap.ts), which runs\n// per-key on init. Keep both coherent — a change to one may affect the other.\nfunction evictStaleSessions(): void {\n const now = Date.now();\n for (const [id, entry] of sessions) {\n // TTL branch: evict regardless of inFlight once past SESSION_TTL_MS. GET is\n // no longer inFlight-guarded after the round-3 fix, so an unconditional skip\n // on inFlight > 0 could make a session with a *leaked* counter immortal. The\n // hard-age backstop turns that leak into a self-healing, observable event.\n if (now - entry.lastAccess > SESSION_TTL_MS) {\n // Force-evicting an inFlight > 0 session here means the counter leaked\n // (an unexpected throw outside the POST try/finally). Log it as such.\n logSessionLifecycle(\n \"session_deleted\",\n id,\n entry.inFlight > 0 ? \"inflight_leak\" : \"ttl\",\n );\n entry.transport.close().catch(() => {});\n sessions.delete(id);\n }\n }\n if (sessions.size > MAX_SESSIONS) {\n // Global memory-pressure branch: SKIP inFlight > 0 so we can't drop a live\n // POST call mid-flight.\n const sorted = [...sessions.entries()]\n .filter(([, e]) => e.inFlight === 0)\n .sort((a, b) => a[1].lastAccess - b[1].lastAccess);\n const overflow = sessions.size - MAX_SESSIONS;\n for (let i = 0; i < Math.min(overflow, sorted.length); i++) {\n logSessionLifecycle(\"session_deleted\", sorted[i][0], \"eviction\");\n sorted[i][1].transport.close().catch(() => {});\n sessions.delete(sorted[i][0]);\n }\n // Observability: if every session is inFlight (sorted is empty / too short)\n // the loop above could not bring us under MAX_SESSIONS. We deliberately do\n // NOT tear live POSTs — the TTL backstop reclaims them at SESSION_TTL_MS —\n // but a sustained breach is worth surfacing rather than silently exceeding\n // the global memory ceiling.\n if (sessions.size > MAX_SESSIONS) {\n const ts = new Date().toISOString();\n process.stderr.write(\n `[HTTP] ${ts} session_cap_breach size=${sessions.size} max=${MAX_SESSIONS} ` +\n `reason=all_inflight (no idle sessions to evict; TTL backstop will reclaim)\\n`,\n );\n }\n }\n}\n\nsetInterval(evictStaleSessions, 60_000);\n\n// ── Auth Helpers ─────────────────────────────────────────────────────────\n// Bearer classification (absent vs. rejected vs. ok) lives in ./authLockout.ts\n// via resolveBearerAuth(); handlers call it directly so that only a\n// presented-and-rejected credential accrues toward the lockout (TEN-2606).\n\nfunction send401(req: any, res: any): void {\n const base = baseUrl(req);\n res\n .status(401)\n .set(\n \"WWW-Authenticate\",\n `Bearer resource_metadata=\"${base}/.well-known/oauth-protected-resource\"`,\n )\n .json({ error: \"unauthorized\" });\n}\n\nfunction logRequest(\n method: string,\n outcome: \"ok\" | \"auth_fail\" | \"error\",\n sessionId?: string,\n durationMs?: number,\n): void {\n const ts = new Date().toISOString();\n const sid = sessionId ? ` session=${sessionId}` : \"\";\n const dur = durationMs != null ? ` duration=${durationMs}ms` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${method} ${outcome}${sid}${dur}\\n`);\n}\n\nfunction logSessionLifecycle(\n event: \"session_created\" | \"session_deleted\",\n sessionId: string,\n reason?: \"ttl\" | \"eviction\" | \"onclose\" | \"lru_evict\" | \"inflight_leak\" | \"init_rejected\",\n): void {\n const ts = new Date().toISOString();\n const r = reason ? ` reason=${reason}` : \"\";\n process.stderr.write(`[HTTP] ${ts} ${event} session=${sessionId}${r}\\n`);\n}\n\n// ── MCP Handlers ────────────────────────────────────────────────────────\n\napp.post(\"/mcp\", mcpLimiter, async (req: any, res: any) => {\n // Fix 5 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n\n // TEN-2606 — only a *presented-and-rejected* credential accrues toward the\n // lockout; an absent credential is the OAuth bootstrap probe and is never\n // recorded, so a legitimate re-bootstrap can never self-lock its IP.\n const auth = resolveBearerAuth(req.headers?.authorization);\n if (auth.status !== \"ok\") {\n logRequest(\"POST\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n const reqStart = Date.now();\n\n try {\n await runWithAuth({ apiKey, 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 the IP lockout like any other\n // 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, reqIp, 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 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n\n // TEN-2606 — absent credential (OAuth bootstrap probe) is never recorded.\n const auth = resolveBearerAuth(req.headers?.authorization);\n if (auth.status !== \"ok\") {\n logRequest(\"GET\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale GET sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey, 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 — Block IPs that have exceeded the auth failure threshold.\n const reqIp: string = req.ip ?? \"unknown\";\n const nowTs = Date.now();\n const lockout = authLockoutError(authFailures, reqIp, nowTs, req.body?.id ?? null);\n if (lockout) { send429(res, lockout); return; }\n\n // TEN-2606 — absent credential (OAuth bootstrap probe) is never recorded.\n const auth = resolveBearerAuth(req.headers?.authorization);\n if (auth.status !== \"ok\") {\n logRequest(\"DELETE\", \"auth_fail\");\n if (auth.status === \"rejected\") recordAuthFailure(authFailures, reqIp, nowTs);\n send401(req, res);\n return;\n }\n const apiKey = auth.apiKey;\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n if (!sessionId) {\n res.status(400).send(\"Missing Mcp-Session-Id header\");\n return;\n }\n if (!sessions.has(sessionId)) {\n // Stale session — per MCP spec § Session Management clauses 3-4, return 404\n // so the client re-initialises without re-authing. Survives Railway restarts.\n process.stderr.write(\n `[HTTP] ${new Date().toISOString()} session_stale DELETE sessionId=${sessionId}\\n`,\n );\n res.status(404).send(\"Session not found — re-initialise\");\n return;\n }\n\n try {\n await runWithAuth({ apiKey, 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 try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err: 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\nexport type AppLogoSize = \"sm\" | \"md\";\n\nexport interface AppLogoOptions {\n size?: AppLogoSize;\n showWordmark?: boolean;\n className?: string;\n}\n\nconst SIZE_CLASSES: Record<AppLogoSize, string> = {\n sm: \"pb-logo--sm\",\n md: \"pb-logo--md\",\n};\n\n// Self-contained CSS for environments that can't load Svelte styles\n// (the MCP authorize page, embedded HTML responses, etc.).\n// Cortex UI consumers can rely on AppLogo.svelte's scoped styles instead;\n// the inline class hooks are designed not to collide.\nexport const appLogoStyles = `\n.pb-logo{display:inline-flex;align-items:center;gap:8px;color:inherit}\n.pb-logo__mark{\n border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);\n display:inline-grid;place-items:center;flex-shrink:0;\n}\n.pb-logo__core{border-radius:50%;background:var(--accent,#c9b99a)}\n.pb-logo__name{\n font-family:var(--font-mono,\"IBM Plex Mono\",ui-monospace,monospace);\n font-weight:500;text-transform:uppercase;\n letter-spacing:0.22em;color:var(--fg4,#6a6560);\n}\n.pb-logo--sm .pb-logo__mark{width:16px;height:16px}\n.pb-logo--sm .pb-logo__core{width:5px;height:5px}\n.pb-logo--sm .pb-logo__name{font-size:10.5px}\n.pb-logo--md .pb-logo__mark{width:24px;height:24px;border-radius:6px}\n.pb-logo--md .pb-logo__core{width:8px;height:8px}\n.pb-logo--md .pb-logo__name{font-size:13px}\n`;\n\nexport function appLogoMarkup(opts: AppLogoOptions = {}): string {\n const size = opts.size ?? \"sm\";\n const showWordmark = opts.showWordmark ?? true;\n const cls = [\"pb-logo\", SIZE_CLASSES[size], opts.className].filter(Boolean).join(\" \");\n const wordmark = showWordmark ? `<span class=\"pb-logo__name\">Product Brain</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\n}\n","/**\n * sessionCap.ts — Pure, side-effect-free session cap planner.\n *\n * Used by http.ts init branch to decide which sessions to evict before\n * admitting a new one for the presenting API key.\n *\n * Two eviction sites in the codebase:\n * 1. planKeyAdmission (here) — per-key, on init, targets only the presenting key.\n * 2. evictStaleSessions (http.ts) — global TTL sweep, runs every 60 s.\n * Keep both coherent; a change to one may affect the other.\n */\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\nexport interface CapSession {\n /** Unix-ms timestamp of last request start (or completion). */\n lastAccess: number;\n /** Hash of the API key that owns this session. */\n keyHash: string;\n /**\n * Number of POST tool-call requests currently being handled.\n * Incremented at POST start; decremented in finally.\n * GET SSE handlers and DELETE are NOT counted here — see design §3.1.\n */\n inFlight: number;\n}\n\nexport interface AdmissionPlan {\n /** IDs of sessions the caller should evict before admitting the new one. */\n evict: string[];\n /** True if the new session can be admitted after evictions. */\n admit: boolean;\n /**\n * Seconds the caller should suggest as a retry delay when admit=false.\n * 0 when admit=true.\n */\n retryAfterSeconds: number;\n /**\n * True when admit=false AND every blocking same-key session has inFlight > 0\n * — there is no grace deadline to wait out, so retryAfterSeconds is a fixed\n * poll-hint rather than a guarantee. The caller surfaces this as a \"retry\n * shortly\" message instead of a hard deadline. Always false when admit=true.\n * This is the single source of truth for poll-vs-deadline — callers must not\n * re-derive it from the live session map (avoids a second clock read).\n */\n retryIsPoll: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// planKeyAdmission\n// ---------------------------------------------------------------------------\n\n/**\n * Plan how the presenting key admits ONE new session under `cap`.\n *\n * Protected = inFlight > 0 OR (now - lastAccess) < graceMs. Never evicted.\n *\n * 1. Throw RangeError if cap < 1.\n * 2. Collect entries belonging to `keyHash`.\n * 3. Mark the key's TTL-expired-AND-not-protected sessions for eviction (G2).\n * (inFlight===0 && now-lastAccess >= ttlMs)\n * 4. If survivors (key ids not in evict) >= cap, additionally evict the key's\n * idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n * ascending by lastAccess (Map-insertion-order stable sort resolves ties)\n * until survivors < cap or none remain.\n * 5. admit = survivors.length < cap.\n * 6. retryAfterSeconds: 0 when admit; else computed from the oldest\n * grace-blocker (smallest lastAccess), or 5 if every blocker is inFlight\n * (retryIsPoll=true in that all-inFlight case).\n * 7. Return { evict, admit, retryAfterSeconds, retryIsPoll }.\n *\n * Only the presenting key's sessions are ever touched.\n * Pure; total over empty maps. Ties in lastAccess resolve by Map insertion order.\n */\nexport function planKeyAdmission(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n cap: number,\n now: number,\n opts: { ttlMs: number; graceMs: number },\n): AdmissionPlan {\n if (cap < 1) {\n throw new RangeError(`cap must be >= 1, got ${cap}`);\n }\n\n const { ttlMs, graceMs } = opts;\n\n // Helper: a session is \"protected\" if it has an active POST call OR was\n // recently accessed (within the grace window).\n const isProtected = (s: CapSession): boolean =>\n s.inFlight > 0 || now - s.lastAccess < graceMs;\n\n // Step 2: collect the presenting key's entries as an ordered array.\n // Map iteration order equals insertion order (ES2015+; V8 stable).\n const keyEntries: [string, CapSession][] = [];\n for (const [id, session] of sessions) {\n if (session.keyHash === keyHash) {\n keyEntries.push([id, session]);\n }\n }\n\n // Step 3: always evict TTL-expired AND non-protected sessions (G2).\n const evictSet = new Set<string>();\n for (const [id, session] of keyEntries) {\n if (!isProtected(session) && now - session.lastAccess >= ttlMs) {\n evictSet.add(id);\n }\n }\n\n // Step 4: if survivors (key sessions not in evict) >= cap, additionally\n // evict idle sessions (inFlight===0 && now-lastAccess >= graceMs) sorted\n // ascending by lastAccess until survivors < cap.\n let survivors = keyEntries.filter(([id]) => !evictSet.has(id));\n\n if (survivors.length >= cap) {\n // Collect idle (evictable) survivors, preserving insertion order for\n // stable sort — Array.sort is stable in V8 and spec-required ES2019+.\n const idleSurvivors = survivors\n .filter(([, s]) => !isProtected(s))\n // Ascending by lastAccess (oldest first); equal values keep insertion order.\n .sort(([, a], [, b]) => a.lastAccess - b.lastAccess);\n\n for (const [id] of idleSurvivors) {\n if (survivors.length < cap) break;\n evictSet.add(id);\n // Recompute survivors in-place without the evicted id.\n survivors = survivors.filter(([sid]) => sid !== id);\n }\n }\n\n const evict = Array.from(evictSet);\n\n // Step 5.\n const admit = survivors.length < cap;\n\n // Step 6.\n let retryAfterSeconds = 0;\n let retryIsPoll = false;\n if (!admit) {\n // Grace-blockers: surviving sessions with inFlight===0 (blocked by grace only).\n const graceBlockers = survivors.filter(([, s]) => s.inFlight === 0);\n if (graceBlockers.length > 0) {\n // Target the oldest (smallest lastAccess) — crosses graceMs soonest.\n const minLastAccess = Math.min(...graceBlockers.map(([, s]) => s.lastAccess));\n const msRemaining = graceMs - (now - minLastAccess);\n retryAfterSeconds = Math.max(1, Math.ceil(msRemaining / 1000));\n } else {\n // All blockers have inFlight > 0; completion time is unpredictable. The\n // 5 s value is a poll-hint fallback, not a guarantee — flag it so the\n // caller's 429 reads as \"retry shortly\" rather than a hard deadline.\n retryAfterSeconds = 5;\n retryIsPoll = true;\n }\n }\n\n return { evict, admit, retryAfterSeconds, retryIsPoll };\n}\n\n// ---------------------------------------------------------------------------\n// countKeySessions\n// ---------------------------------------------------------------------------\n\n/**\n * Count how many sessions in the map belong to `keyHash`.\n * Pure; returns 0 for an empty map.\n */\nexport function countKeySessions(\n sessions: ReadonlyMap<string, CapSession>,\n keyHash: string,\n): number {\n let count = 0;\n for (const session of sessions.values()) {\n if (session.keyHash === keyHash) count++;\n }\n return count;\n}\n","/**\n * httpErrors.ts — pure, import-safe builders for structured 429 responses.\n *\n * All three 429 sources (rate limit, auth lockout, session cap) return a\n * JSON-RPC envelope so the consuming MCP client can tell them apart.\n *\n * Rule — id mirrors the JSON-RPC request id when known\n * ---------------------------------------------------------------------------\n * JSON-RPC requires the response id to mirror the request id so the client can\n * correlate. All three call-sites run AFTER the global `express.json()`\n * middleware, so a POST body is already parsed and callers SHOULD thread\n * `req.body?.id` through `BuildParams.id` (the POST `/mcp` rate-limit,\n * auth-lockout, and session-cap gates all do). Requests with no parsable body\n * (GET/DELETE, or a malformed POST) have no id to mirror — those omit `id` and\n * it defaults to `null`, which is the correct JSON-RPC value for \"unknown id\".\n * ---------------------------------------------------------------------------\n */\n\nexport interface Built429 {\n status: 429;\n headers: Record<string, string>;\n body: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Clamp to non-negative, round to integer — used for both the Retry-After\n * header value and the `retryAfterSeconds` field in `data`.\n */\nfunction clampSeconds(s: number): number {\n return Math.max(0, Math.round(s));\n}\n\ntype Reason = \"rate_limited\" | \"auth_lockout\" | \"session_cap\";\n\n/** JSON-RPC request/response id. `null` is the spec value for \"unknown id\". */\ntype JsonRpcId = string | number | null;\n\ninterface BuildParams {\n retryAfterSeconds: number;\n limit: number;\n current: number;\n /** Request id to mirror; defaults to null when the caller has no body id. */\n id?: JsonRpcId;\n}\n\nfunction build(\n reason: Reason,\n message: string,\n p: BuildParams,\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return {\n status: 429,\n headers: { \"Retry-After\": String(seconds) },\n body: {\n jsonrpc: \"2.0\",\n id: p.id ?? null,\n error: {\n code: -32000,\n message,\n data: {\n reason,\n retryAfterSeconds: seconds,\n limit: p.limit,\n current: p.current,\n },\n },\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public builders\n// ---------------------------------------------------------------------------\n\n/**\n * Per-IP rate-limit 429 (source: mcpLimiter middleware).\n * reason = \"rate_limited\"\n */\nexport function buildRateLimitError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"rate_limited\",\n `Too many requests — retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-IP auth-failure lockout 429 (source: authLockoutError in authLockout.ts).\n * reason = \"auth_lockout\"\n */\nexport function buildAuthLockoutError(p: BuildParams): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n return build(\n \"auth_lockout\",\n `Too many failed auth attempts — locked out, retry after ${seconds} s.`,\n p,\n );\n}\n\n/**\n * Per-key session-cap 429 (source: planKeyAdmission !admit fallback).\n * reason = \"session_cap\"\n *\n * @param p.poll - true when ALL blockers have inFlight > 0 (no grace deadline\n * can be predicted; the 5 s value is a fixed fallback, not a guarantee).\n * The message reads as a poll-hint rather than a hard deadline.\n * false (default) when at least one blocker is grace-protected; the\n * retryAfterSeconds is a real guarantee and the message names it.\n */\nexport function buildSessionCapError(\n p: BuildParams & { poll?: boolean },\n): Built429 {\n const seconds = clampSeconds(p.retryAfterSeconds);\n const message = p.poll\n ? \"Server busy — all sessions active; retry shortly.\"\n : `Too many active sessions for this key — retry after ${seconds} s.`;\n return build(\"session_cap\", message, p);\n}\n\n// ---------------------------------------------------------------------------\n// Sender\n// ---------------------------------------------------------------------------\n\n/**\n * Minimal structural shape of the response object send429 needs. Any\n * Express/Hono response satisfies it; the explicit interface (rather than\n * `any`) keeps a wrong argument from compiling and silently swallowing a 429.\n */\nexport interface Http429Response {\n headersSent: boolean;\n setHeader(name: string, value: string): unknown;\n status(code: number): { json(body: unknown): unknown };\n}\n\n/**\n * Write a Built429 to an Express/Hono-compatible response object.\n *\n * Guards `res.headersSent` first so calling this on an already-flushed\n * response is a safe no-op (e.g. when a middleware has already sent a body).\n */\nexport function send429(res: Http429Response, built: Built429): void {\n if (res.headersSent) return;\n for (const [k, v] of Object.entries(built.headers)) {\n res.setHeader(k, v);\n }\n res.status(built.status).json(built.body);\n}\n","// Per-IP progressive lockout for failed API-key auth attempts, plus the Bearer\n// credential classification that feeds it. Extracted from http.ts so the policy\n// is unit-testable in isolation (http.ts binds a live server on import).\n//\n// TEN-2606: the lockout must penalize only a *presented-and-rejected* Bearer\n// credential — never an *absent* one. Absent-credential /mcp calls are the\n// spec-mandated OAuth bootstrap probe (the client hits the resource with no\n// token to receive the 401 + WWW-Authenticate challenge). A client re-bootstraps\n// on every connect, and in a burst right after a Railway restart wipes the\n// in-memory session map — so counting those probes as attacks self-locks\n// a legitimate IP. resolveBearerAuth() distinguishes the two; the handlers only\n// call recordAuthFailure() on the \"rejected\" branch.\n//\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 per-IP map in http.ts, so\n// bad keys on one door lock the other. resolvePastedKey() is the form's sibling of resolveBearerAuth().\n\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 request's Authorization header.\n// - \"ok\": a pb_sk_-shaped key; initializeKeyGate() asks the kernel whether it is real.\n// - \"absent\": no Bearer credential was presented — never penalized.\n// - \"rejected\": a Bearer credential was presented but is invalid — penalized.\nexport type BearerAuth =\n | { status: \"ok\"; apiKey: string }\n | { status: \"absent\" }\n | { status: \"rejected\" };\n\nexport const AUTH_FAILURE_MAX = 10;\nexport const AUTH_FAILURE_WINDOW_MS = 5 * 60_000; // 5 minutes\nexport const AUTH_BLOCK_DURATION_MS = 15 * 60_000; // 15 minutes\nexport const MAX_AUTH_FAILURE_ENTRIES = 10_000;\n\n/**\n * Classify a request's Authorization header into absent / rejected / ok.\n *\n * 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\n // ONLY case that escapes the lockout: a client hitting the resource with no\n // credential to fetch the 401 + WWW-Authenticate challenge.\n return { status: \"absent\" };\n }\n if (!authorizationHeader.startsWith(\"Bearer \")) {\n // A credential of a wrong scheme (e.g. Basic) was presented — it can never\n // authenticate, so treat it as a rejected attempt that accrues toward the\n // lockout. Classifying it \"absent\" would let a caller bypass the guard by\n // always sending a non-Bearer header.\n return { status: \"rejected\" };\n }\n const token = authorizationHeader.slice(7).trim();\n if (token === \"\") {\n // \"Bearer \" scheme presented with an empty token — a presented, unusable\n // credential, not the (header-less) bootstrap probe.\n return { status: \"rejected\" };\n }\n\n if (token.startsWith(\"pb_sk_\")) {\n return { status: \"ok\", apiKey: token };\n }\n // A Bearer credential of unknown shape was presented — treat as a rejected\n // attempt so brute-forcing garbage tokens still accrues toward the lockout.\n return { status: \"rejected\" };\n}\n\n/**\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 return apiKey.startsWith(\"pb_sk_\") ? { status: \"ok\", apiKey } : { status: \"rejected\" };\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; without this, AUTH_FAILURE_MAX garbage keys\n * posted that way would lock the visitor's IP out of the form and /mcp. /mcp is not exposed: its key\n * rides the 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 `ip` is currently within an active lockout block. */\nexport function checkAuthBlock(\n failures: Map<string, AuthFailureRecord>,\n ip: string,\n now: number,\n): boolean {\n const rec = failures.get(ip);\n if (!rec) return false;\n return rec.blockedUntil > now;\n}\n\n/**\n * The lockout 429 for `ip` (Retry-After = seconds left in the block), or null when `ip` is not\n * blocked. 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 ip: string,\n now: number,\n id: string | number | null = null,\n): Built429 | null {\n const rec = failures.get(ip);\n if (!rec || !checkAuthBlock(failures, ip, 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 `ip`. Callers MUST NOT\n * call this for an absent credential (TEN-2606). Tips the IP into a block once\n * AUTH_FAILURE_MAX failures land inside AUTH_FAILURE_WINDOW_MS; a failure past\n * the window resets the count rather than accumulating across windows.\n */\nexport function recordAuthFailure(\n failures: Map<string, AuthFailureRecord>,\n ip: string,\n now: number,\n): void {\n const rec = failures.get(ip);\n\n if (!rec) {\n failures.set(ip, { count: 1, firstFailure: now, blockedUntil: 0 });\n return;\n }\n\n // Reset the window if the first failure is outside the tracking window.\n if (now - rec.firstFailure > AUTH_FAILURE_WINDOW_MS) {\n rec.count = 1;\n rec.firstFailure = now;\n rec.blockedUntil = 0;\n } else {\n rec.count++;\n if (rec.count >= AUTH_FAILURE_MAX) {\n rec.blockedUntil = now + AUTH_BLOCK_DURATION_MS;\n }\n }\n}\n\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 `ip`'s lockout.\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 ip: string,\n now: number,\n): \"admit\" | \"unauthorized\" | \"unavailable\" {\n if (verdict === \"ok\" || verdict === \"rate_limited\") return \"admit\";\n if (verdict === \"rejected\") {\n recordAuthFailure(failures, ip, 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":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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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 { initAnalytics, shutdownAnalytics, getPostHogClient } from \"./analytics.js\";\nimport { initFeatureFlags } from \"./featureFlags.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();\ninitAnalytics();\ninitFeatureFlags(getPostHogClient());\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 ({ \"&\": \"&\", '\"': \""\", \"'\": \"'\", \"<\": \"<\", \">\": \">\" })[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 "Start PB" and return to ${esc(providerName)}\">\n <span class=\"cmd-quote-part\" aria-hidden=\"true\">“</span><span data-cmd-text>Start PB</span><span class=\"cmd-quote-part\" aria-hidden=\"true\">”</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>← Try again</a>\n <a href=\"${esc(apiKeysUrl())}\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"btn-secondary\">Get an API key →</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 →</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{'<':'<','>':'>','&':'&'}[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{'<':'<','>':'>','&':'&'}[c]});\n var safeProv=String(providerName||'your assistant').replace(/[<>&]/g,function(c){return{'<':'<','>':'>','&':'&'}[c]});\n var safeUrl=String(redirectUrl||'').replace(/\"/g,'"').replace(/[<>]/g,function(c){return{'<':'<','>':'>'}[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 try {\n await shutdownAnalytics();\n } catch {\n /* best-effort */\n }\n process.exit(0);\n}\n\n// Bind all interfaces — Railway/Cloudflare reach the container on its non-loopback IP.\n// Loopback-only (127.0.0.1) causes edge 502: the proxy never connects to localhost inside the pod.\nconst LISTEN_HOST = \"0.0.0.0\";\nconst httpServer = app.listen(PORT, LISTEN_HOST, () => {\n console.log(\n `Product Brain MCP HTTP server v${SERVER_VERSION} listening on ${LISTEN_HOST}:${PORT}`,\n );\n});\nhttpServer.on(\"error\", (err: 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\nexport type AppLogoSize = \"sm\" | \"md\";\n\nexport interface AppLogoOptions {\n size?: AppLogoSize;\n showWordmark?: boolean;\n className?: string;\n}\n\nconst SIZE_CLASSES: Record<AppLogoSize, string> = {\n sm: \"pb-logo--sm\",\n md: \"pb-logo--md\",\n};\n\n// Self-contained CSS for environments that can't load Svelte styles\n// (the MCP authorize page, embedded HTML responses, etc.).\n// Cortex UI consumers can rely on AppLogo.svelte's scoped styles instead;\n// the inline class hooks are designed not to collide.\nexport const appLogoStyles = `\n.pb-logo{display:inline-flex;align-items:center;gap:8px;color:inherit}\n.pb-logo__mark{\n border-radius:4px;background:#1c1e24;\n border:1px solid rgba(255,255,255,0.06);\n display:inline-grid;place-items:center;flex-shrink:0;\n}\n.pb-logo__core{border-radius:50%;background:var(--accent,#c9b99a)}\n.pb-logo__name{\n font-family:var(--font-mono,\"IBM Plex Mono\",ui-monospace,monospace);\n font-weight:500;text-transform:uppercase;\n letter-spacing:0.22em;color:var(--fg4,#6a6560);\n}\n.pb-logo--sm .pb-logo__mark{width:16px;height:16px}\n.pb-logo--sm .pb-logo__core{width:5px;height:5px}\n.pb-logo--sm .pb-logo__name{font-size:10.5px}\n.pb-logo--md .pb-logo__mark{width:24px;height:24px;border-radius:6px}\n.pb-logo--md .pb-logo__core{width:8px;height:8px}\n.pb-logo--md .pb-logo__name{font-size:13px}\n`;\n\nexport function appLogoMarkup(opts: AppLogoOptions = {}): string {\n const size = opts.size ?? \"sm\";\n const showWordmark = opts.showWordmark ?? true;\n const cls = [\"pb-logo\", SIZE_CLASSES[size], opts.className].filter(Boolean).join(\" \");\n const wordmark = showWordmark ? `<span class=\"pb-logo__name\">Product Brain</span>` : \"\";\n return `<span class=\"${cls}\"><span class=\"pb-logo__mark\"><span class=\"pb-logo__core\"></span></span>${wordmark}</span>`;\n}\n","/**\n * 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":"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A;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,wBA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","names":["sessions","sid"]}
|