{"record":{"id":"1d8a82175514fa38","repo":"koala73/worldmonitor","slug":"email-ownership-required","errorCode":"EMAIL_OWNERSHIP_REQUIRED","errorMessage":"Connect your verified account email to receive notifications.","messagePattern":"Connect your verified account email to receive notifications\\.","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/lib/notificationEmail.ts","lineNumber":7,"sourceCode":"import { ConvexError } from \"convex/values\";\n\n/** Only the authenticated identity or the server's Clerk lookup supplies proof. */\nexport function requireVerifiedAccountEmail(requested: string | undefined, verifiedEmail: string | undefined): string {\n  const email = verifiedEmail?.trim();\n  if (!email || typeof requested !== \"string\" || requested.trim().toLowerCase() !== email.toLowerCase()) {\n    throw new ConvexError({ code: \"EMAIL_OWNERSHIP_REQUIRED\", message: \"Connect your verified account email to receive notifications.\" });\n  }\n  return email;\n}\n\nexport async function lookupVerifiedAccountEmail(userId: string): Promise<string | undefined> {\n  const secret = process.env.CLERK_SECRET_KEY;\n  if (!secret) throw new Error(\"EMAIL_VERIFICATION_UNAVAILABLE\");\n  const response = await fetch(`https://api.clerk.com/v1/users/${encodeURIComponent(userId)}`, {\n    headers: { Authorization: `Bearer ${secret}`, \"User-Agent\": \"worldmonitor-convex/1.0\" },\n    signal: AbortSignal.timeout(5_000),\n  });\n  if (!response.ok) throw new Error(\"EMAIL_VERIFICATION_UNAVAILABLE\");\n  const user = await response.json() as {\n    id?: string;\n    primary_email_address_id?: string;\n    email_addresses?: Array<{ id: string; email_address: string; verification?: { status?: string } }>;\n  };\n  if (user.id !== userId) throw new Error(\"EMAIL_VERIFICATION_UNAVAILABLE\");","sourceCodeStart":1,"sourceCodeEnd":25,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/convex/lib/notificationEmail.ts#L1-L25","documentation":"requireVerifiedAccountEmail throws EMAIL_OWNERSHIP_REQUIRED when the email a caller wants notifications sent to cannot be proven to belong to the authenticated account. Proof comes only from the authenticated identity or the server's Clerk lookup — never from client-supplied strings. The requested email must exactly match (case-insensitively) the verified email on the account; otherwise the function refuses to return any recipient.","triggerScenarios":"Calling a mutation/query (verifiedAccountEmail, recipient, email paths) that passes a `requested` email which is undefined, empty after trim, or differs from the Clerk-verified email (different case is tolerated, but any character difference is not). Typical calls: registering a notification recipient with a personal email while the Clerk account has a different verified address, or passing no email at all when the identity has none.","commonSituations":"User signed up with Google SSO so no email was passed through the identity; developer forwards a user-typed email from a form instead of the verified one; user changed their Clerk primary email but the client caches the old one; email has stray whitespace or a plus-address variant that fails the strict lowercase comparison.","solutions":["Send the email exactly as it appears on the Clerk account, or omit it so the server uses lookupVerifiedAccountEmail.","Have the user verify the address in Clerk (verification email) before using it as a notification recipient.","Check case/whitespace: the comparison trims and lowercases, but any other character difference (e.g. plus-alias) fails.","Ensure CLERK_SECRET_KEY is set so lookupVerifiedAccountEmail can resolve the verified email server-side."],"exampleFix":"// before\nawait api.notifications.addRecipient({ email: formData.email });\n// after\n// use the verified email from the auth identity, or omit to let the server resolve it\nawait api.notifications.addRecipient({ email: clerkUser.primaryEmailAddress.emailAddress });","handlingStrategy":"try-catch","validationCode":"const requested = (email ?? \"\").trim().toLowerCase();\nconst verified = (clerkUser.primaryEmailAddress?.emailAddress ?? \"\").trim().toLowerCase();\nif (!verified || requested !== verified) throw new Error(\"recipient must be the verified account email\");","typeGuard":null,"tryCatchPattern":"try {\n  await addRecipient({ email });\n} catch (e) {\n  if (isConvexError(e) && e.data?.code === \"EMAIL_OWNERSHIP_REQUIRED\") {\n    promptUserToVerifyEmail();\n  }\n}","preventionTips":["Always source the recipient email from the Clerk identity, never free-form user input.","Prompt users to verify their email in Clerk before enabling email notifications.","Show the verified email in the UI instead of an editable field."],"tags":["email","authentication","convex","ownership-check"],"backgroundTag":"authentication-required","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-09-15T16:44:39.439Z","contentChangedAt":"2026-09-15T16:44:39.439Z","schemaVersion":2},"datasetVersion":"2026-09-15T18:17:12.389Z"}