yikart/AiToEarn · error · AppException

ChannelAuthRefreshTokenMissing

ChannelAuthRefreshTokenMissing

Error message

ChannelAuthRefreshTokenMissing

What it means

Google Business credential refresh was attempted without a stored refresh token. OAuth refresh requires the long-lived refresh token obtained at initial consent; without it the provider cannot mint a new access token.

Source

Thrown at project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/google-business/google-business-auth.provider.ts:46

    return { url, state: input.state, redirectUri: this.config.redirectUri }
  }

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

    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.googleBusinessService.refreshAccessToken(input.refreshToken)

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

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

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

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Re-run the Google OAuth connect flow with access_type=offline&prompt=consent to obtain and store a refresh token
  2. Check the credential storage/migration didn't drop the refreshToken field
  3. Ensure the auth callback persists result.refreshToken before responding
  4. Verify the Google Cloud app is in production (or the user is a test user) so refresh tokens aren't expiring in 7 days

Example fix

// before
await credsRepo.save({ accessToken: result.accessToken })
// after
await credsRepo.save({ accessToken: result.accessToken, refreshToken: result.refreshToken })
Defensive patterns

Strategy: validation

Validate before calling

if (!credential.refreshToken) {
  throw new Error('No Google refresh token stored; user must reconnect the Google Business channel')
}

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 provider.refresh(input)
} catch (e) {
  if (e.code === 'ChannelAuthRefreshTokenMissing') {
    await markChannelNeedsReauth(accountId, 'google-business')
  } else throw e
}

Prevention

When it happens

Trigger: refresh() is called with input.refreshToken undefined/empty — the credential record was saved without offline access or the token field was lost.

Common situations: Initial OAuth consent omitted access_type=offline / prompt=consent so no refresh token was issued; stored credential was partially migrated or overwritten; Google only returns a refresh token on first consent and it was never persisted.

Related errors


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