{"record":{"id":"4b648eb1247baf30","repo":"toeverything/AFFiNE","slug":"email-token-not-found","errorCode":"email_token_not_found","errorMessage":"The email token provided is not found.","messagePattern":"The email token provided is not found\\.","errorType":"exception","errorClass":"EmailTokenNotFound","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/core/auth/controller.ts","lineNumber":339,"sourceCode":"    this.assertSessionMutationAuthorized(req, session);\n    await this.authSessions.revoke(\n      parsedSessionId.data,\n      'user_action',\n      user.id\n    );\n    return {};\n  }\n\n  @Public()\n  @UseNamedGuard('version')\n  @Post('/magic-link')\n  async magicLinkSignIn(\n    @Req() req: Request,\n    @Res() res: Response,\n    @Body() body?: unknown\n  ) {\n    const credential = MagicLinkBodySchema.safeParse(body);\n    if (!credential.success) throw new EmailTokenNotFound();\n    const { email, token: otp, client_nonce: clientNonce } = credential.data;\n    if (!email) throw new EmailTokenNotFound();\n    validators.assertValidEmail(email);\n    const identity = await this.magicLink.verify(email, otp, clientNonce);\n    const { exchangeCode } = await this.sessionIssuer.issue(req, res, identity);\n    res.send({ id: identity.userId, exchangeCode });\n  }\n\n  @UseNamedGuard('version')\n  @Throttle('default', { limit: 1200 })\n  @Public()\n  @Get('/session')\n  @Header('Cache-Control', 'no-store')\n  async currentSessionUser(@CurrentUser() user?: CurrentUser) {\n    return { user };\n  }\n\n  private assertSessionMutationAuthorized(","sourceCodeStart":321,"sourceCodeEnd":357,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/controller.ts#L321-L357","documentation":"Thrown by `POST /api/auth/magic-link` when the body fails `MagicLinkBodySchema.safeParse`. The schema is `{ email, token, client_nonce? }.strict()` with `token` being a 1–512 char string, so a missing/empty token, missing email, extra fields, or wrong types all fail. HTTP 400, reported as `email_token_not_found` rather than leaking which field is wrong.","triggerScenarios":"Posting a magic-link completion without the `token` (OTP), without `email`, with an empty token, or with extra fields the strict schema rejects.","commonSituations":"User clicks the magic link but the OTP query param was stripped, the deep-link handler drops the token, or a client sends `{ otp }` instead of `{ token }`.","solutions":["Send `{ email, token }` where `token` is the OTP from the magic link (1–512 chars).","Ensure the magic-link URL preserves the OTP/query params through the redirect chain.","If the token was consumed or expired (>10 min), request a new magic link."],"exampleFix":"// before\nfetch('/api/auth/magic-link', { method: 'POST', body: JSON.stringify({ email }) });\n\n// after\nfetch('/api/auth/magic-link', {\n  method: 'POST',\n  headers: { 'content-type': 'application/json' },\n  body: JSON.stringify({ email, token: otp }),\n});","handlingStrategy":"validation","validationCode":"import { z } from 'zod';\nconst Body = z.object({ email: z.string().min(1).max(320), token: z.string().min(1).max(512) }).strict();\nconst parsed = Body.safeParse(payload);\nif (!parsed.success) throw new Error('email and token required');\nawait fetch('/api/auth/magic-link', {\n  method: 'POST',\n  headers: { 'content-type': 'application/json' },\n  body: JSON.stringify(parsed.data),\n});","typeGuard":"function isMagicLinkBody(v: unknown): v is { email: string; token: string } {\n  const o = v as any;\n  return !!o && typeof o.email === 'string' && o.email.length >= 1 && o.email.length <= 320 &&\n    typeof o.token === 'string' && o.token.length >= 1 && o.token.length <= 512;\n}","tryCatchPattern":null,"preventionTips":["Preserve the OTP/token through the magic-link redirect chain.","Match the strict schema exactly — only `email` and `token` (optional `client_nonce`).","Request a new magic link if the token is older than its TTL."],"tags":["validation","magic-link","input","email"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}