{"record":{"id":"4578f4a3d04072da","repo":"yikart/AiToEarn","slug":"channelauthrefreshtokenmissing-4578f4","errorCode":"ChannelAuthRefreshTokenMissing","errorMessage":"ChannelAuthRefreshTokenMissing","messagePattern":"ChannelAuthRefreshTokenMissing","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/twitter/twitter-auth.provider.ts","lineNumber":65,"sourceCode":"    }\n  }\n\n  async exchangeCode(input: AuthCallbackInput): Promise<CredentialResult> {\n    const callback = parseOAuthCallback(input)\n    const codeVerifier = parseAuthCodeVerifier(input)\n    const result = await this.twitterService.exchangeCode(callback.code, codeVerifier)\n\n    return {\n      accessToken: result.accessToken,\n      refreshToken: result.refreshToken,\n      expiresAt: result.expiresAt,\n      scope: result.scope,\n    }\n  }\n\n  async refresh(input: RefreshCredentialInput): Promise<CredentialResult> {\n    if (!input.refreshToken) {\n      throw new AppException(ResponseCode.ChannelAuthRefreshTokenMissing)\n    }\n\n    const result = await this.twitterService.refreshAccessToken(input.refreshToken)\n\n    return {\n      accessToken: result.accessToken,\n      refreshToken: result.refreshToken,\n      expiresAt: result.expiresAt,\n      scope: result.scope,\n    }\n  }\n\n  async revoke(input: RevokeCredentialInput): Promise<void> {\n    await this.twitterService.revokeToken(input.accessToken)\n  }\n\n  async getProfile(input: CredentialContext): Promise<PlatformAccountProfile> {\n    const userInfo = await this.twitterService.getUserInfo(input.accessToken)","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/twitter/twitter-auth.provider.ts#L47-L83","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Fix the token-exchange/refresh path so every new refresh_token returned by Twitter (rotations included) is written back to storage.","Ensure the initial authorize request includes the offline_access scope; otherwise no refresh token is issued.","Have the refresh job mark such accounts as needing re-authorization instead of throwing per account."],"exampleFix":"// before\nawait authProvider.refresh({ refreshToken: account.credentials.refreshToken })\n// after\nif (!account.credentials.refreshToken) {\n  await markAccountNeedsReauth(account.id)\n} else {\n  const result = await authProvider.refresh({ refreshToken: account.credentials.refreshToken })\n  await saveCredentials(account.id, result) // persist rotated refresh token\n}","handlingStrategy":"validation","validationCode":"function canRefreshTwitter(creds: TwitterCredentials): boolean {\n  return typeof creds.refreshToken === 'string' && creds.refreshToken.length > 0\n}\nif (!canRefreshTwitter(account.credentials)) await markAccountNeedsReauth(account.id)","typeGuard":"function hasRefreshToken(c: { refreshToken?: string | null }): c is { refreshToken: string } {\n  return typeof c.refreshToken === 'string' && c.refreshToken.length > 0\n}","tryCatchPattern":"try {\n  await authProvider.refresh({ refreshToken })\n} catch (e) {\n  if (e instanceof AppException && e.code === ResponseCode.ChannelAuthRefreshTokenMissing) {\n    await markAccountNeedsReauth(accountId)\n    return\n  }\n  throw e\n}","preventionTips":["Always request offline_access scope so Twitter issues a refresh token.","Persist rotated refresh tokens on every refresh — Twitter rotates them.","Flag accounts missing refreshToken for re-authorization instead of throwing in batch jobs.","Monitor refresh_token_invalid responses; they usually mean rotation was mishandled."],"tags":["oauth","twitter","missing-token","auth"],"backgroundTag":"missing-refresh-token","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}