{"record":{"id":"689bb6d74e91dddf","repo":"yikart/AiToEarn","slug":"responsecode-channelaccountnotauthorized","errorCode":"ResponseCode.ChannelAccountNotAuthorized","errorMessage":"ChannelAccountNotAuthorized","messagePattern":"ChannelAccountNotAuthorized","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts","lineNumber":629,"sourceCode":"      accessToken: refreshed.accessToken,\n      refreshToken: refreshed.refreshToken,\n      expiresAt: refreshed.expiresAt,\n      scope: refreshed.scope,\n    }\n  }\n\n  private async getCredentialAccount(accountId: string, userId?: string) {\n    const account = userId\n      ? await this.accountRepo.getByIdAndUserId(accountId, userId)\n      : await this.accountRepo.getAccountById(accountId)\n    if (!account) {\n      throw new AppException(ResponseCode.AccountNotFound)\n    }\n    if (account.relayAccountRef) {\n      throw new RelayAccountException(account.relayAccountRef, accountId)\n    }\n    if (account.status === AccountStatus.ABNORMAL) {\n      throw new AppException(ResponseCode.ChannelAccountNotAuthorized)\n    }\n    return account\n  }\n\n  async markAccountOfflineForCredentialFailure(\n    accountId: string,\n    error: unknown,\n    reason = 'platform_auth_failed',\n  ): Promise<boolean> {\n    if (!this.isCredentialFailure(error)) {\n      return false\n    }\n    try {\n      await this.markAccountOffline(accountId, reason)\n      return true\n    }\n    catch (markError) {\n      this.logger.warn(error, `Credential failure for account ${accountId}`)","sourceCodeStart":611,"sourceCodeEnd":647,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/auth/auth.service.ts#L611-L647","documentation":"getCredentialAccount throws ChannelAccountNotAuthorized (AppException, ResponseCode.ChannelAccountNotAuthorized) when the account exists and is not a Relay account but its status is AccountStatus.ABNORMAL. ABNORMAL status is set after non-retryable credential failures (e.g. markAccountOfflineForCredentialFailure), meaning the stored token is known-bad and the account must be re-authorized before use.","triggerScenarios":"Calling refreshCredential or any credential-consuming flow on an account that was previously marked offline/ABNORMAL after a platform auth failure; using an account after the user revoked access or the refresh token expired and the failure handler flagged it.","commonSituations":"Retry loops re-processing an account already marked offline; schedulers not filtering by account status; users trying to publish content with a disconnected channel without re-linking it.","solutions":["Re-run the platform OAuth flow to reconnect the account; this resets status to NORMAL with fresh tokens.","Check account.status before credential calls and short-circuit with a 'reconnect required' user prompt instead of invoking the service.","Filter scheduled/batch jobs to exclude ABNORMAL accounts to avoid systematic failures.","Investigate the original failure (logs of markAccountOfflineForCredentialFailure) to confirm why the account went ABNORMAL before reconnecting."],"exampleFix":"// before\nawait authService.refreshCredential(accountId, userId)\n// after\nconst account = await accountRepo.getAccountById(accountId)\nif (account?.status === AccountStatus.ABNORMAL) {\n  throw new AppException(ResponseCode.ChannelAccountNotAuthorized, { accountId })\n}\nawait authService.refreshCredential(accountId, userId)","handlingStrategy":"validation","validationCode":"const account = await accountRepo.getAccountById(accountId)\nif (account?.status === AccountStatus.ABNORMAL) {\n  throw new AppException(ResponseCode.ChannelAccountNotAuthorized, { accountId })\n}","typeGuard":"function isUsableAccount(a: { status: AccountStatus } | null): a is { status: AccountStatus } {\n  return !!a && a.status === AccountStatus.NORMAL\n}","tryCatchPattern":"try {\n  await authService.refreshCredential(accountId, userId)\n} catch (e) {\n  if (e instanceof AppException && e.code === ResponseCode.ChannelAccountNotAuthorized) {\n    return res.status(403).json({ message: 'Reconnect this channel to continue', accountId })\n  }\n  throw e\n}","preventionTips":["Filter batch jobs and schedulers to exclude ABNORMAL accounts.","Surface account status in the UI so users reconnect before operations fail.","Reset status to NORMAL only via a completed OAuth re-authorization.","Alert when the number of ABNORMAL accounts spikes (platform-wide auth outage)."],"tags":["authorization","oauth","account-status","channels"],"backgroundTag":"account-not-authorized","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}