koala73/worldmonitor · error · ConvexError
INVITEE_EMAIL_UNAVAILABLE
INVITEE_EMAIL_UNAVAILABLE
Error message
INVITEE_EMAIL_UNAVAILABLE
What it means
`acceptBusinessInvite` resolves the invitee's email from the Clerk identity via `resolveUserIdentity`. If the signed-in user has no email claim, the accept cannot match the invited address and is rejected. This is rare auth drift that the code intentionally surfaces as a structured error for Sentry.
Source
Thrown at convex/payments/businessSeats.ts:460
return { ok: true as const, status: "revoked" as const };
},
});
/**
* Invitee redeems an HMAC token to accept a Business Pro seat invite. Verifies
* the token, matches the signed-in Clerk email against the invited address,
* checks the underlying Business subscription is still covering, flips the
* grant to `accepted`, stamps `inviteeUserId`, and recomputes the invitee's
* entitlement. Single-use: accepted/revoked/expired tokens are rejected.
*/
export const acceptBusinessInvite = mutation({
args: { grantId: v.id("businessProGrants"), token: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const identity = await resolveUserIdentity(ctx);
const inviteeEmail = identity?.email?.trim().toLowerCase();
if (!inviteeEmail) {
throw new ConvexError({ kind: "INVITEE_EMAIL_UNAVAILABLE" });
}
const grant = await ctx.db.get(args.grantId);
if (!grant) {
throw new ConvexError({ kind: "GRANT_NOT_FOUND" });
}
if (grant.status !== "pending") {
throw new ConvexError({ kind: "INVITE_ALREADY_USED" });
}
const now = Date.now();
if (grant.expiresAt <= now) {
throw new ConvexError({ kind: "INVITE_EXPIRED" });
}
if (!(await verifyBusinessInviteToken(args.grantId, args.token))) {
throw new ConvexError({ kind: "INVALID_INVITE_TOKEN" });
}
if (grant.inviteeEmail !== inviteeEmail) {
throw new ConvexError({ kind: "INVITE_EMAIL_MISMATCH" });View on GitHub (pinned to ffec79ac33)
Solutions
- Ensure the Clerk user has a verified email address on the account before accepting
- Add an email to the Clerk account and re-authenticate so the claim is present
- Verify the OAuth provider includes email scope in the Clerk JWT
Defensive patterns
Strategy: validation
Validate before calling
// Ensure the Clerk session exposes an email before allowing the accept flow.
const email = clerk.user?.primaryEmailAddress?.emailAddress;
if (!email) {
// prompt user to add/verify an email before navigating to accept
} Type guard
function hasEmailClaim(identity: { email?: string } | null): identity is { email: string } {
return Boolean(identity && typeof identity.email === 'string' && identity.email.trim().length > 0);
} Prevention
- Require email scope on all OAuth providers in Clerk
- Prompt users without an email to add one before showing the accept-invite UI
- Re-authenticate after adding an email so the claim is refreshed
When it happens
Trigger: Calling `acceptBusinessInvite` while the Clerk JWT carries no `email` claim — e.g., an OAuth login without email scope, or a phone-only/anonymous account.
Common situations: GitHub or other social login granted without email permissions; account provisioned without a verified email; Clerk session missing the email claim after a provider config change.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/73d7624fd04ef4d2.
Report an issue: GitHub.