toeverything/AFFiNE · error · EmailTokenNotFound

email_token_not_found

email_token_not_found

Error message

The email token provided is not found.

What it means

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.

Source

Thrown at packages/backend/server/src/core/auth/resolver.ts:256

    const url = this.url.safeLink(callbackUrl, { token });

    return await this.auth.sendChangeEmail(
      user.email,
      url,
      this.mailMetadata(context, expiresAt)
    );
  }

  @Mutation(() => Boolean)
  async sendVerifyChangeEmail(
    @CurrentUser() user: CurrentUser,
    @Args('token') token: string,
    @Args('email') email: string,
    @Args('callbackUrl') callbackUrl: string,
    @Context() context: GraphqlContext
  ) {
    if (!token) {
      throw new EmailTokenNotFound();
    }

    validators.assertValidEmail(email);
    const valid = await this.models.verificationToken.verify(
      TokenType.ChangeEmail,
      token,
      {
        credential: user.id,
      }
    );

    if (!valid) {
      throw new InvalidEmailToken();
    }

    const hasRegistered = await this.models.user.getUserByEmail(email);

    if (hasRegistered) {

View on GitHub (pinned to 26c515e050)

Solutions

  1. Restart from sendChangeEmail to get a fresh ChangeEmail link with a valid token.
  2. Forward the token from the email link's query string into the mutation arguments.
  3. Validate the landing URL contains a non-empty token before calling the mutation.
  4. Avoid truncating or rewriting the change-email link.

Example fix

// before — token not forwarded
sendVerifyChangeEmail({ email, callbackUrl })
// after — pass token from the step-1 email link
sendVerifyChangeEmail({ token, email, callbackUrl })
Defensive patterns

Strategy: validation

Validate before calling

function readChangeEmailToken(link: string): string | undefined {
  try {
    return new URL(link).searchParams.get('token') ?? undefined;
  } catch {
    return undefined;
  }
}

const token = readChangeEmailToken(window.location.href);
if (!token) {
  showError('The link is missing its token. Restart the email change flow.');
}

Type guard

function isEmailTokenNotFound(err: unknown): boolean {
  return (
    !!err &&
    typeof err === 'object' &&
    (err as { code?: string }).code === 'email_token_not_found'
  );
}

Try / catch

try {
  await sendVerifyChangeEmail({ token, email, callbackUrl });
} catch (err) {
  if (isEmailTokenNotFound(err)) {
    showUser('The link is invalid. Restart the email change flow.');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of toeverything/AFFiNE@26c515e050 (2026-08-12). Data as JSON: /api/errors/352692859c18c9d5. Report an issue: GitHub.