{"record":{"id":"352692859c18c9d5","repo":"toeverything/AFFiNE","slug":"email-token-not-found-352692","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/resolver.ts","lineNumber":256,"sourceCode":"    const url = this.url.safeLink(callbackUrl, { token });\n\n    return await this.auth.sendChangeEmail(\n      user.email,\n      url,\n      this.mailMetadata(context, expiresAt)\n    );\n  }\n\n  @Mutation(() => Boolean)\n  async sendVerifyChangeEmail(\n    @CurrentUser() user: CurrentUser,\n    @Args('token') token: string,\n    @Args('email') email: string,\n    @Args('callbackUrl') callbackUrl: string,\n    @Context() context: GraphqlContext\n  ) {\n    if (!token) {\n      throw new EmailTokenNotFound();\n    }\n\n    validators.assertValidEmail(email);\n    const valid = await this.models.verificationToken.verify(\n      TokenType.ChangeEmail,\n      token,\n      {\n        credential: user.id,\n      }\n    );\n\n    if (!valid) {\n      throw new InvalidEmailToken();\n    }\n\n    const hasRegistered = await this.models.user.getUserByEmail(email);\n\n    if (hasRegistered) {","sourceCodeStart":238,"sourceCodeEnd":274,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/auth/resolver.ts#L238-L274","documentation":"The sendVerifyChangeEmail mutation (step 3 of the change-email flow) requires a non-empty token argument (the ChangeEmail token from step 1, sent to the CURRENT email). If token is falsy, EmailTokenNotFound is thrown before any further validation. This guards the subsequent verify step and gives a distinct code from 'invalid' tokens so the client can prompt correctly.","triggerScenarios":"sendVerifyChangeEmail called with an empty/missing token argument. The user's change-email link was truncated, dropping the token, or the client built the request without forwarding it.","commonSituations":"The change-email link's token param was stripped during navigation/copy. A client form submitted without binding the token. Manually constructed request omitted the token arg.","solutions":["Restart from sendChangeEmail to get a fresh ChangeEmail link with a valid token.","Forward the token from the email link's query string into the mutation arguments.","Validate the landing URL contains a non-empty token before calling the mutation.","Avoid truncating or rewriting the change-email link."],"exampleFix":"// before — token not forwarded\nsendVerifyChangeEmail({ email, callbackUrl })\n// after — pass token from the step-1 email link\nsendVerifyChangeEmail({ token, email, callbackUrl })","handlingStrategy":"validation","validationCode":"function readChangeEmailToken(link: string): string | undefined {\n  try {\n    return new URL(link).searchParams.get('token') ?? undefined;\n  } catch {\n    return undefined;\n  }\n}\n\nconst token = readChangeEmailToken(window.location.href);\nif (!token) {\n  showError('The link is missing its token. Restart the email change flow.');\n}","typeGuard":"function isEmailTokenNotFound(err: unknown): boolean {\n  return (\n    !!err &&\n    typeof err === 'object' &&\n    (err as { code?: string }).code === 'email_token_not_found'\n  );\n}","tryCatchPattern":"try {\n  await sendVerifyChangeEmail({ token, email, callbackUrl });\n} catch (err) {\n  if (isEmailTokenNotFound(err)) {\n    showUser('The link is invalid. Restart the email change flow.');\n    return;\n  }\n  throw err;\n}","preventionTips":["Parse the change-email link and assert token is present before calling the mutation.","Forward the token param unchanged from the email link.","Avoid truncating or rewriting change-email links.","Restart at step 1 when the token is missing."],"tags":["auth","email-change","verification-token","graphql","validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}