toeverything/AFFiNE · error · InvalidEmailToken

invalid_email_token

invalid_email_token

Error message

An invalid email token provided.

What it means

During magic-link verification, magicLinkOtp.consume fails for a reason other than nonce_mismatch - the OTP itself is wrong, expired, or has no record - and the service throws InvalidEmailToken (invalid_email_token). The OTP is a separate short code from the URL token (upserted at send time with its own expiry), so a valid-looking link can still fail here.

Solutions

  1. Request a fresh magic link and use the newest code, whole and unmodified
  2. Copy-paste the code rather than retyping; strip surrounding whitespace
  3. Complete verification promptly after the email arrives
  4. If codes keep expiring, check for delayed email delivery rather than resubmitting the same code

Example fix

// before
await verifyMagicLink(email, otpFromOldestEmail);

// after
const { otp } = await requestNewMagicLink(email); // newest email
await verifyMagicLink(email, otp.trim());
Defensive patterns

Strategy: try-catch

Type guard

function isInvalidEmailToken(e: unknown): boolean {
  return typeof e === 'object' && e !== null && (e as { code?: string }).code === 'invalid_email_token';
}

Try / catch

try {
  await verifyMagicLink(email, otp.trim());
} catch (e) {
  if (isInvalidEmailToken(e)) {
    await resendMagicLink(email); // never resubmit the same otp
  } else throw e;
}

Prevention

When it happens

Trigger: Typing/entering an OTP that does not match the one generated at send time; OTP past its expiry window; retrying an OTP that was already consumed (single-use); requesting a new link but submitting the old email's OTP; whitespace or truncation when copying the code.

Common situations: User finds an older email and uses its code; multiple magic-link requests in flight and codes crossed; slow email delivery pushing verification past expiry; OCR/copy errors on mobile.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/9be0d000290d87db. Report an issue: GitHub.

Appendix: source

Thrown at packages/backend/server/src/core/auth/magic-link.ts:113

  }

  async verify(
    email: string,
    otp: string,
    clientNonce?: string
  ): Promise<VerifiedIdentity> {
    validators.assertValidEmail(email);

    const consumed = await this.models.magicLinkOtp.consume(
      email,
      otp,
      clientNonce
    );
    if (!consumed.ok) {
      if (consumed.reason === 'nonce_mismatch') {
        throw new InvalidAuthState();
      }
      throw new InvalidEmailToken();
    }

    const tokenRecord = await this.models.verificationToken.verify(
      TokenType.SignIn,
      consumed.token,
      {
        credential: email,
      }
    );

    if (!tokenRecord) {
      throw new InvalidEmailToken();
    }

    const user = await this.models.user.fulfill(email);

    return { userId: user.id, method: 'magic_link' };
  }

View on GitHub (pinned to b4c8548c09)