{"record":{"id":"9be0d000290d87db","repo":"toeverything/AFFiNE","slug":"invalid-email-token","errorCode":"invalid_email_token","errorMessage":"An invalid email token provided.","messagePattern":"An invalid email token provided\\.","errorType":"exception","errorClass":"InvalidEmailToken","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/magic-link.ts","lineNumber":113,"sourceCode":"  }\n\n  async verify(\n    email: string,\n    otp: string,\n    clientNonce?: string\n  ): Promise<VerifiedIdentity> {\n    validators.assertValidEmail(email);\n\n    const consumed = await this.models.magicLinkOtp.consume(\n      email,\n      otp,\n      clientNonce\n    );\n    if (!consumed.ok) {\n      if (consumed.reason === 'nonce_mismatch') {\n        throw new InvalidAuthState();\n      }\n      throw new InvalidEmailToken();\n    }\n\n    const tokenRecord = await this.models.verificationToken.verify(\n      TokenType.SignIn,\n      consumed.token,\n      {\n        credential: email,\n      }\n    );\n\n    if (!tokenRecord) {\n      throw new InvalidEmailToken();\n    }\n\n    const user = await this.models.user.fulfill(email);\n\n    return { userId: user.id, method: 'magic_link' };\n  }","sourceCodeStart":95,"sourceCodeEnd":131,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/auth/magic-link.ts#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait verifyMagicLink(email, otpFromOldestEmail);\n\n// after\nconst { otp } = await requestNewMagicLink(email); // newest email\nawait verifyMagicLink(email, otp.trim());","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isInvalidEmailToken(e: unknown): boolean {\n  return typeof e === 'object' && e !== null && (e as { code?: string }).code === 'invalid_email_token';\n}","tryCatchPattern":"try {\n  await verifyMagicLink(email, otp.trim());\n} catch (e) {\n  if (isInvalidEmailToken(e)) {\n    await resendMagicLink(email); // never resubmit the same otp\n  } else throw e;\n}","preventionTips":["Trim OTPs on paste; verify immediately after receiving the email","Invalidate previously shown codes in the UI whenever a new link is requested"],"tags":["auth","magic-link","otp","expiry"],"backgroundTag":"otp-invalid-or-expired","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}