{"record":{"id":"b262b41a39f479c0","repo":"yikart/AiToEarn","slug":"channelauthrefreshtokenmissing-b262b4","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/google-business/google-business-auth.provider.ts","lineNumber":46,"sourceCode":"\n    return { url, state: input.state, redirectUri: this.config.redirectUri }\n  }\n\n  async exchangeCode(input: AuthCallbackInput): Promise<CredentialResult> {\n    const callback = parseOAuthCallback(input)\n    const result = await this.googleBusinessService.exchangeCode(callback.code)\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.googleBusinessService.refreshAccessToken(input.refreshToken)\n\n    return {\n      accessToken: result.accessToken,\n      refreshToken: result.refreshToken ?? input.refreshToken,\n      expiresAt: result.expiresAt,\n      scope: result.scope,\n    }\n  }\n\n  async revoke(input: RevokeCredentialInput): Promise<void> {\n    await this.googleBusinessService.revokeToken(input.accessToken)\n  }\n\n  async getProfile(input: CredentialContext): Promise<PlatformAccountProfile> {\n    const userInfo = await this.googleBusinessService.getUserInfo(input.accessToken)","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/google-business/google-business-auth.provider.ts#L28-L64","documentation":"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.","triggerScenarios":"refresh() is called with input.refreshToken undefined/empty — the credential record was saved without offline access or the token field was lost.","commonSituations":"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.","solutions":["Re-run the Google OAuth connect flow with access_type=offline&prompt=consent to obtain and store a refresh token","Check the credential storage/migration didn't drop the refreshToken field","Ensure the auth callback persists result.refreshToken before responding","Verify the Google Cloud app is in production (or the user is a test user) so refresh tokens aren't expiring in 7 days"],"exampleFix":"// before\nawait credsRepo.save({ accessToken: result.accessToken })\n// after\nawait credsRepo.save({ accessToken: result.accessToken, refreshToken: result.refreshToken })","handlingStrategy":"validation","validationCode":"if (!credential.refreshToken) {\n  throw new Error('No Google refresh token stored; user must reconnect the Google Business channel')\n}","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 provider.refresh(input)\n} catch (e) {\n  if (e.code === 'ChannelAuthRefreshTokenMissing') {\n    await markChannelNeedsReauth(accountId, 'google-business')\n  } else throw e\n}","preventionTips":["Always request access_type=offline&prompt=consent in the Google OAuth URL","Persist refreshToken in the same transaction as accessToken","Audit credential records for null refreshToken columns","Remember Google omits refresh_token on repeat consents unless prompt=consent"],"tags":["google-business","oauth","refresh-token","missing-data"],"backgroundTag":"missing-refresh-token","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}