{"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":352,"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 result = await this.magicLink.complete(\n      email,\n      otp,\n      clientNonce,\n      this.sessionIssuer.target(req)\n    );\n    this.sessionIssuer.apply(res, result);\n    res.send({ id: result.user.id, exchangeCode: result.exchangeCode });\n  }\n\n  @UseNamedGuard('version')\n  @Throttle('default', { limit: 1200 })\n  @Public()\n  @Get('/session')\n  @Header('Cache-Control', 'no-store')","sourceCodeStart":334,"sourceCodeEnd":370,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/2af30773aecd567f09b346e7b72fc69143144057/packages/backend/server/src/core/auth/controller.ts#L334-L370","documentation":"POST /api/auth/magic-link parses the JSON body with MagicLinkBodySchema - a strict object { email: string (max 320), token: string (1..512), client_nonce?: string (1..512) }. Any parse failure is deliberately mapped to EmailTokenNotFound (email_token_not_found) instead of a field-level validation error, to avoid leaking which part of the credential is wrong. Because the schema is .strict(), unknown extra fields also fail.","triggerScenarios":"Missing the token field; sending legacy fields like verifyToken or challenge (rejected by .strict()); a token longer than 512 chars or email longer than 320; a Content-Type that does not parse to a JSON object (form-encoded string, plain text); wrong field name such as code instead of token.","commonSituations":"Older clients still posting captcha fields that were moved to headers; hand-rolled fetch calls with typo'd keys; tests sending urlencoded bodies; copy-pasting a magic-link URL instead of extracting the token parameter.","solutions":["Send exactly { email, token, client_nonce? } as JSON with Content-Type: application/json","Extract token from the actual magic-link URL parameter, not the whole URL","Check field sizes: email <= 320 chars, token 1..512 chars","Remove legacy extra fields (verifyToken, challenge) from the body"],"exampleFix":"// before\nawait fetch('/api/auth/magic-link', {\n  method: 'POST',\n  body: JSON.stringify({ email, code: otp, verifyToken }), // wrong key + legacy field\n});\n\n// after\nawait fetch('/api/auth/magic-link', {\n  method: 'POST',\n  headers: { 'content-type': 'application/json' },\n  body: JSON.stringify({ email, token: otp }), // client_nonce only if you passed one at send time\n});","handlingStrategy":"validation","validationCode":"import { z } from 'zod';\nconst MagicLinkBody = z.object({\n  email: z.string().min(1).max(320),\n  token: z.string().min(1).max(512),\n  client_nonce: z.string().min(1).max(512).optional(),\n}).strict();\n// throws locally with a useful message instead of opaque email_token_not_found\nconst body = MagicLinkBody.parse({ email, token, client_nonce });","typeGuard":null,"tryCatchPattern":"try {\n  await post('/auth/magic-link', body);\n} catch (e) {\n  if (isAffineErrorCode(e, 'email_token_not_found')) {\n    showFormError('The sign-in code is missing or malformed. Open the newest email.');\n  } else throw e;\n}","preventionTips":["Mirror server body schemas (zod) in the client and parse before sending","Send JSON with an explicit content-type header from a shared request helper","Extract the token parameter from the link URL instead of pasting the whole URL"],"tags":["auth","magic-link","validation","zod","http-body"],"backgroundTag":"request-body-validation-failed","analyzedSha":"2af30773aecd567f09b346e7b72fc69143144057","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}