{"record":{"id":"9d8ad81e8d56c4b9","repo":"toeverything/AFFiNE","slug":"invalid-email-token-9d8ad8","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/resolver.ts","lineNumber":122,"sourceCode":"    @Args('token') token: string,\n    @Args('newPassword') newPassword: string,\n    @Args('userId', { type: () => String, nullable: true }) userId?: string\n  ) {\n    if (!userId) {\n      throw new LinkExpired();\n    }\n\n    // NOTE: Set & Change password are using the same token type.\n    const valid = await this.models.verificationToken.verify(\n      TokenType.ChangePassword,\n      token,\n      {\n        credential: userId,\n      }\n    );\n\n    if (!valid) {\n      throw new InvalidEmailToken();\n    }\n\n    await this.auth.changePasswordAndRevokeSessions(userId, newPassword);\n\n    return true;\n  }\n\n  @Mutation(() => UserType)\n  async changeEmail(\n    @CurrentUser() user: CurrentUser,\n    @Args('token') token: string,\n    @Args('email') email: string\n  ) {\n    // @see [sendChangeEmail]\n    const valid = await this.models.verificationToken.verify(\n      TokenType.VerifyEmail,\n      token,\n      {","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/core/auth/resolver.ts#L104-L140","documentation":"Thrown by the changePassword GraphQL mutation when verificationToken.verify(TokenType.ChangePassword, token, { credential: userId }) returns false. The token must be of type ChangePassword, carry the passed userId as its credential, be unexpired, and be unconsumed. Set-password and change-password share one token type, so any earlier use of the token burns it.","triggerScenarios":"Calling changePassword(userId, token, newPassword) with a token whose TTL elapsed, a token already consumed by a previous changePassword call, a token minted for a different user, or a truncated/mangled token string (e.g. URL encoding stripped characters).","commonSituations":"User clicks the reset link twice and the second request reuses the consumed token; a stale email is opened after the password was already changed; the frontend sends the userId of a different signed-in account; token TTL configured shorter than real email delivery latency.","solutions":["Call sendChangePasswordEmail again to mint a fresh token and point the user to the new link","Make the submit single-flight: disable the change-password form while the mutation is in flight and after success","Verify the userId sent with the mutation belongs to the same account that requested the email","If users routinely outlive the TTL, raise the ChangePassword token expiry in the verificationToken model config"],"exampleFix":"// before\nawait client.request(changePasswordMutation, { userId, token, newPassword });\n\n// after\ntry {\n  await client.request(changePasswordMutation, { userId, token, newPassword });\n} catch (e) {\n  if (gqlCode(e) === 'invalid_email_token') {\n    await client.request(sendChangePasswordEmailMutation, { callbackUrl });\n    throw new Error('Reset link expired. A new email has been sent.');\n  }\n  throw e;\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":"function isInvalidEmailToken(e: unknown): boolean {\n  return (\n    typeof e === 'object' &&\n    e !== null &&\n    'extensions' in e &&\n    (e as { extensions?: { code?: string } }).extensions?.code === 'invalid_email_token'\n  );\n}","tryCatchPattern":"Catch the GraphQL error, inspect extensions.code === 'invalid_email_token', and translate it into a user-facing 'link expired' message plus a fresh sendChangePasswordEmail call. Rethrow every other code unchanged.","preventionTips":["Treat reset links as single-use: disable the change-password form after the first submit","Send the userId that matches the account which received the email","Keep the ChangePassword token TTL comfortably larger than worst-case email delivery time"],"tags":["auth","password-reset","email-token","graphql"],"backgroundTag":"verification-token-invalid","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"}