koala73/worldmonitor · error · ConvexError

INVALID_INVITE_TOKEN

INVALID_INVITE_TOKEN

Error message

INVALID_INVITE_TOKEN

What it means

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.

Source

Thrown at convex/payments/businessSeats.ts:475

    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" });
    }
    if (!sameDomain(grant.inviteeEmail, inviteeEmail)) {
      throw new ConvexError({ kind: "INVITE_EMAIL_MISMATCH" });
    }
    if (!isCorporateDomain(inviteeEmail)) {
      throw new ConvexError({ kind: "INVITEE_DOMAIN_NOT_CORPORATE" });
    }

    const businessSub = await ctx.db
      .query("subscriptions")
      .withIndex("by_dodoSubscriptionId", (q) =>
        q.eq("dodoSubscriptionId", grant.businessSubscriptionId),
      )
      .unique();
    if (!businessSub || businessSub.planKey !== "api_business" || !isCoveringAt(businessSub, now)) {

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Use the exact accept link from the original invite email without modification
  2. If the signing secret was rotated, have the owner resend the invite so a fresh token is minted
  3. If the URL was truncated by a mail client, request a resend
Defensive patterns

Strategy: try-catch

Try / catch

try {
  await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });
} catch (err) {
  if (err.data?.kind === 'INVALID_INVITE_TOKEN') {
    // the link is corrupt/tampered — request the owner resend the invite
  } else { throw err; }
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/fe2ce99445e7f076. Report an issue: GitHub.