yikart/AiToEarn · error · AppException

ChannelAuthRefreshTokenMissing

ChannelAuthRefreshTokenMissing

Error message

ChannelAuthRefreshTokenMissing

What it means

Twitter credential refresh requires a stored refresh token; refresh() throws ChannelAuthRefreshTokenMissing when input.refreshToken is missing. Twitter OAuth2 (PKCE) refresh tokens can also be rotated/invalidated, so accounts may legitimately lack a usable one.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/twitter/twitter-auth.provider.ts:65

    }
  }

  async exchangeCode(input: AuthCallbackInput): Promise<CredentialResult> {
    const callback = parseOAuthCallback(input)
    const codeVerifier = parseAuthCodeVerifier(input)
    const result = await this.twitterService.exchangeCode(callback.code, codeVerifier)

    return {
      accessToken: result.accessToken,
      refreshToken: result.refreshToken,
      expiresAt: result.expiresAt,
      scope: result.scope,
    }
  }

  async refresh(input: RefreshCredentialInput): Promise<CredentialResult> {
    if (!input.refreshToken) {
      throw new AppException(ResponseCode.ChannelAuthRefreshTokenMissing)
    }

    const result = await this.twitterService.refreshAccessToken(input.refreshToken)

    return {
      accessToken: result.accessToken,
      refreshToken: result.refreshToken,
      expiresAt: result.expiresAt,
      scope: result.scope,
    }
  }

  async revoke(input: RevokeCredentialInput): Promise<void> {
    await this.twitterService.revokeToken(input.accessToken)
  }

  async getProfile(input: CredentialContext): Promise<PlatformAccountProfile> {
    const userInfo = await this.twitterService.getUserInfo(input.accessToken)

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Check the stored credentials for the Twitter account — if refreshToken is empty, re-run the Twitter OAuth connect flow (with offline_access scope) to get and persist one.
  2. Fix the token-exchange/refresh path so every new refresh_token returned by Twitter (rotations included) is written back to storage.
  3. Ensure the initial authorize request includes the offline_access scope; otherwise no refresh token is issued.
  4. Have the refresh job mark such accounts as needing re-authorization instead of throwing per account.

Example fix

// before
await authProvider.refresh({ refreshToken: account.credentials.refreshToken })
// after
if (!account.credentials.refreshToken) {
  await markAccountNeedsReauth(account.id)
} else {
  const result = await authProvider.refresh({ refreshToken: account.credentials.refreshToken })
  await saveCredentials(account.id, result) // persist rotated refresh token
}
Defensive patterns

Strategy: validation

Validate before calling

function canRefreshTwitter(creds: TwitterCredentials): boolean {
  return typeof creds.refreshToken === 'string' && creds.refreshToken.length > 0
}
if (!canRefreshTwitter(account.credentials)) await markAccountNeedsReauth(account.id)

Type guard

function hasRefreshToken(c: { refreshToken?: string | null }): c is { refreshToken: string } {
  return typeof c.refreshToken === 'string' && c.refreshToken.length > 0
}

Try / catch

try {
  await authProvider.refresh({ refreshToken })
} catch (e) {
  if (e instanceof AppException && e.code === ResponseCode.ChannelAuthRefreshTokenMissing) {
    await markAccountNeedsReauth(accountId)
    return
  }
  throw e
}

Prevention

When it happens

Trigger: Calling twitterAuthProvider.refresh({ refreshToken: undefined }) — account connected without persisting refresh_token, credential record cleared, or refresh token consumed during a previous rotation and not re-saved.

Common situations: Refresh job processing an account whose Twitter connect flow never stored refresh_token; refresh-token rotation in Twitter OAuth2 invalidated the stored value and the new one wasn't saved; DB cleanup/migration dropped the field; offline_access scope missing at connect time so Twitter never issued a refresh token.

Related errors


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/4578f4a3d04072da. Report an issue: GitHub.