{"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/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/magic-link.ts#L95-L131","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Request a fresh magic link / OTP and enter it promptly within the 30-minute window.","Make sure the OTP is not consumed elsewhere before this verify (no double-submission).","Copy the code exactly as delivered; avoid whitespace/typing errors.","If delivery is consistently slow, investigate mail transport latency so the code arrives before expiry."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isInvalidEmailToken(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'invalid_email_token'\n  );\n}","tryCatchPattern":"try {\n  await verify(email, otp, nonce);\n} catch (err) {\n  if (isInvalidEmailToken(err)) {\n    showUser('The code is invalid or expired. Request a new one.');\n    return;\n  }\n  throw err;\n}","preventionTips":["Enter the OTP exactly as delivered, within the 30-minute window.","Prevent double-submit of the same OTP (disable the button after first submit).","Re-request a new magic link rather than retrying the same expired code.","Track delivery latency to ensure codes arrive before expiry."],"tags":["auth","magic-link","otp","expired-token","validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}