toeverything/AFFiNE · error · InvalidEmailToken

invalid_email_token

invalid_email_token

Error message

An invalid email token provided.

What it means

During MagicLinkAuthService.verify, magicLinkOtp.consume returns ok:false for any reason OTHER than nonce_mismatch. consume fails when the (email, otp) pair is not found, was already consumed (one-time use), or has expired. The server throws InvalidEmailToken rather than InvalidAuthState because the failure is about the credential itself, not the client binding.

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 26c515e050)

Solutions

  1. Request a fresh magic link / OTP and enter it promptly within the 30-minute window.
  2. Make sure the OTP is not consumed elsewhere before this verify (no double-submission).
  3. Copy the code exactly as delivered; avoid whitespace/typing errors.
  4. If delivery is consistently slow, investigate mail transport latency so the code arrives before expiry.
Defensive patterns

Strategy: try-catch

Type guard

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

Try / catch

try {
  await verify(email, otp, nonce);
} catch (err) {
  if (isInvalidEmailToken(err)) {
    showUser('The code is invalid or expired. Request a new one.');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: User submits a wrong, typo'd, or already-used OTP for the given email. The OTP row expired past its otpExpiresAt (30-minute token TTL window). The OTP was consumed by an earlier successful verify attempt.

Common situations: User retried an old code, typed it manually and mistyped, or clicked the same magic link twice (second click finds the OTP already consumed). Slow email delivery means the user enters a code after it expired.

Related errors


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