{"record":{"id":"4e569a45fe4ceb3e","repo":"koala73/worldmonitor","slug":"invite-email-mismatch","errorCode":"INVITE_EMAIL_MISMATCH","errorMessage":"INVITE_EMAIL_MISMATCH","messagePattern":"INVITE_EMAIL_MISMATCH","errorType":"exception","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":478,"sourceCode":"      throw new ConvexError({ kind: \"INVITEE_EMAIL_UNAVAILABLE\" });\n    }\n\n    const grant = await ctx.db.get(args.grantId);\n    if (!grant) {\n      throw new ConvexError({ kind: \"GRANT_NOT_FOUND\" });\n    }\n    if (grant.status !== \"pending\") {\n      throw new ConvexError({ kind: \"INVITE_ALREADY_USED\" });\n    }\n    const now = Date.now();\n    if (grant.expiresAt <= now) {\n      throw new ConvexError({ kind: \"INVITE_EXPIRED\" });\n    }\n    if (!(await verifyBusinessInviteToken(args.grantId, args.token))) {\n      throw new ConvexError({ kind: \"INVALID_INVITE_TOKEN\" });\n    }\n    if (grant.inviteeEmail !== inviteeEmail) {\n      throw new ConvexError({ kind: \"INVITE_EMAIL_MISMATCH\" });\n    }\n    if (!sameDomain(grant.inviteeEmail, inviteeEmail)) {\n      throw new ConvexError({ kind: \"INVITE_EMAIL_MISMATCH\" });\n    }\n    if (!isCorporateDomain(inviteeEmail)) {\n      throw new ConvexError({ kind: \"INVITEE_DOMAIN_NOT_CORPORATE\" });\n    }\n\n    const businessSub = await ctx.db\n      .query(\"subscriptions\")\n      .withIndex(\"by_dodoSubscriptionId\", (q) =>\n        q.eq(\"dodoSubscriptionId\", grant.businessSubscriptionId),\n      )\n      .unique();\n    if (!businessSub || businessSub.planKey !== \"api_business\" || !isCoveringAt(businessSub, now)) {\n      throw new ConvexError({ kind: \"BUSINESS_NOT_ACTIVE\" });\n    }\n","sourceCodeStart":460,"sourceCodeEnd":496,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L460-L496","documentation":"After token verification, `acceptBusinessInvite` requires the signed-in Clerk email to exactly match (case-insensitively, after trim+lowercase) the `inviteeEmail` stored on the grant. An exact mismatch throws INVITE_EMAIL_MISMATCH. This is the primary identity-binding check — the invite is personal to the invited address.","triggerScenarios":"Accepting an invite sent to alice@corp.com while signed in as alice.alt@corp.com or any other distinct address. The token may be valid, but the signed-in identity differs from the invited one.","commonSituations":"Invite sent to one alias (e.g., a distribution list or HR address) but the user logs in with their personal corporate alias; plus-addressing differences (alice+pro@corp.com vs alice@corp.com).","solutions":["Sign in with the exact email address the invite was sent to","Ask the owner to re-invite the address you actually sign in with"],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Before accepting, ensure the signed-in email matches the invited one.\nconst invited = grant.inviteeEmail;\nconst signedIn = (clerk.user?.primaryEmailAddress?.emailAddress ?? '').trim().toLowerCase();\nif (signedIn !== invited) {\n  // prompt user to sign in with the invited address\n}","typeGuard":"function emailMatchesInvite(invited: string, signedIn: string | null | undefined): boolean {\n  return Boolean(signedIn) && signedIn!.trim().toLowerCase() === invited.trim().toLowerCase();\n}","tryCatchPattern":"try {\n  await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });\n} catch (err) {\n  if (err.data?.kind === 'INVITE_EMAIL_MISMATCH') {\n    // prompt to sign in with the exact invited address\n  } else { throw err; }\n}","preventionTips":["Show which email the invite was sent to and require signing in with that exact address","Avoid plus-addressing differences between invite and login","Normalize (trim+lowercase) emails before comparison on the client"],"tags":["payments","business-seats","email","invites","identity"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}