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
- Request a fresh magic link and use the newest code, whole and unmodified
- Copy-paste the code rather than retyping; strip surrounding whitespace
- Complete verification promptly after the email arrives
- 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
- Trim OTPs on paste; verify immediately after receiving the email
- Invalidate previously shown codes in the UI whenever a new link is requested
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)