{"record":{"id":"fe2ce99445e7f076","repo":"koala73/worldmonitor","slug":"invalid-invite-token","errorCode":"INVALID_INVITE_TOKEN","errorMessage":"INVALID_INVITE_TOKEN","messagePattern":"INVALID_INVITE_TOKEN","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/payments/businessSeats.ts","lineNumber":475,"sourceCode":"    const identity = await resolveUserIdentity(ctx);\n    const inviteeEmail = identity?.email?.trim().toLowerCase();\n    if (!inviteeEmail) {\n      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)) {","sourceCodeStart":457,"sourceCodeEnd":493,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/payments/businessSeats.ts#L457-L493","documentation":"The accept URL embeds an HMAC token bound to the grantId via `signBusinessInviteToken`. `acceptBusinessInvite` verifies it with `verifyBusinessInviteToken`; any mismatch raises INVALID_INVITE_TOKEN. This guards against forged or tampered accept links and binds the token to the specific grant row.","triggerScenarios":"The token in the URL does not verify against the grantId: edited/truncated token, grantId reused with a stale token, or the HMAC signing secret was rotated after the invite was issued.","commonSituations":"Email client wrapped or truncated the long accept URL; user hand-edited the link; the identity-signing secret rotated and old tokens no longer verify.","solutions":["Use the exact accept link from the original invite email without modification","If the signing secret was rotated, have the owner resend the invite so a fresh token is minted","If the URL was truncated by a mail client, request a resend"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });\n} catch (err) {\n  if (err.data?.kind === 'INVALID_INVITE_TOKEN') {\n    // the link is corrupt/tampered — request the owner resend the invite\n  } else { throw err; }\n}","preventionTips":["Use the exact accept link from the invite email without editing","After a signing-secret rotation, reissue invites so tokens re-verify","Beware mail clients that wrap/truncate long URLs"],"tags":["payments","business-seats","security","hmac","invites"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}