{"record":{"id":"35bff87d8ac94fb3","repo":"koala73/worldmonitor","slug":"invalid-user-id-35bff8","errorCode":"invalid-user-id","errorMessage":"Invalid userId for Pro MCP token issue","messagePattern":"Invalid userId for Pro MCP token issue","errorType":"exception","errorClass":"ProMcpIssueFailed","httpStatus":400,"severity":"error","filePath":"server/_shared/pro-mcp-token.ts","lineNumber":224,"sourceCode":"      'network',\n      `Convex issue request failed: ${err instanceof Error ? err.message : String(err)}`,\n    );\n  }\n\n  if (resp.ok) {\n    const data = (await resp.json().catch(() => null)) as ProMcpIssueResult | null;\n    if (!data || typeof data.tokenId !== 'string' || !data.tokenId) {\n      throw new ProMcpIssueFailed('network', 'Convex issue response missing tokenId', resp.status);\n    }\n    return { tokenId: data.tokenId };\n  }\n\n  // Map Convex error responses (see convex/http.ts /api/internal-issue-pro-mcp-token).\n  if (resp.status === 403) {\n    throw new ProMcpIssueFailed('pro-required', 'Pro entitlement required to issue MCP token', 403);\n  }\n  if (resp.status === 400) {\n    throw new ProMcpIssueFailed('invalid-user-id', 'Invalid userId for Pro MCP token issue', 400);\n  }\n  // 401 (shared-secret mismatch) and 5xx and any other status → network/transient.\n  throw new ProMcpIssueFailed(\n    'network',\n    `Convex issue returned HTTP ${resp.status}`,\n    resp.status,\n  );\n}\n\n/**\n * Validate a Pro MCP token by tokenId — discriminated-union variant.\n *\n * Returns `{ok:'valid', userId}` if the row exists and is not revoked.\n * Returns `{ok:'revoked'}` if Convex authoritatively returned null\n * (row missing, revoked, or malformed-id). Returns `{ok:'transient'}` on\n * Convex 5xx / network error / timeout / non-JSON — caller can decide\n * whether to fail-closed (per-request validate) or preserve the refresh\n * token (refresh-grant path) instead of consuming it.","sourceCodeStart":206,"sourceCodeEnd":242,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/server/_shared/pro-mcp-token.ts#L206-L242","documentation":"Convex answered HTTP 400 (INVALID_USER_ID) for the internal token issue action: the userId in the request body was empty, missing, or not a usable user reference. issueProMcpTokenForUser maps this to ProMcpIssueFailed kind='invalid-user-id'. It is a caller bug, deterministic for the same input — retrying with the same userId will fail identically.","triggerScenarios":"issueProMcpTokenForUser('') or issueProMcpTokenForUser(undefined as any) — e.g. the Clerk session resolved without a subject; the caller passed an internal/clerk-prefixed ID where Convex expects its own user table reference; JSON.stringify body dropped userId because the variable was never assigned.","commonSituations":"OAuth handler reads userId from a session that expired mid-flow, yielding an empty string; a refactor changed the userId source and TypeScript did not catch the undefined because of an any cast; the Convex user row was deleted between grant and issue.","solutions":["Log and inspect the exact userId being sent in the request body at the call site","Guard the call: only invoke issueProMcpTokenForUser after confirming userId is a non-empty string","Check how the caller derives userId (Clerk subject vs Convex user table ID) and align it with what convex/http.ts validates"],"exampleFix":"// before — session hiccup silently produces an empty userId\nconst userId = session?.user?.id ?? '';\nawait issueProMcpTokenForUser(userId);\n\n// after — fail fast before hitting Convex\nconst userId = session?.user?.id;\nif (!userId) return new Response('unauthenticated', { status: 401 });\nawait issueProMcpTokenForUser(userId);","handlingStrategy":"validation","validationCode":"function isValidIssueUserId(userId: unknown): userId is string {\n  return typeof userId === 'string' && userId.trim().length > 0;\n}\n\nif (!isValidIssueUserId(userId)) return new Response('missing user', { status: 401 });","typeGuard":"function isInvalidUserId(e: unknown): e is ProMcpIssueFailed {\n  return e instanceof ProMcpIssueFailed && e.kind === 'invalid-user-id';\n}","tryCatchPattern":"try {\n  await issueProMcpTokenForUser(userId);\n} catch (err) {\n  if (isInvalidUserId(err)) return new Response('invalid user id', { status: 400 }); // fix input, do not retry\n  throw err;\n}","preventionTips":["Never default userId to '' — treat an unresolved session subject as unauthenticated","Type the userId parameter as string (not string | undefined) so the compiler enforces the guard","Log the userId shape (not contents) when the OAuth flow starts to catch mapping regressions early"],"tags":["convex","mcp","validation","user-id","oauth"],"backgroundTag":"invalid-user-id","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}