MCPcopy
hub / github.com/codeaashu/claude-code / refreshOAuthToken

Function refreshOAuthToken

src/services/oauth/client.ts:146–274  ·  view source on GitHub ↗
(
  refreshToken: string,
  { scopes: requestedScopes }: { scopes?: string[] } = {},
)

Source from the content-addressed store, hash-verified

144}
145
146export async function refreshOAuthToken(
147 refreshToken: string,
148 { scopes: requestedScopes }: { scopes?: string[] } = {},
149): Promise<OAuthTokens> {
150 const requestBody = {
151 grant_type: 'refresh_token',
152 refresh_token: refreshToken,
153 client_id: getOauthConfig().CLIENT_ID,
154 // Request specific scopes, defaulting to the full Claude AI set. The
155 // backend's refresh-token grant allows scope expansion beyond what the
156 // initial authorize granted (see ALLOWED_SCOPE_EXPANSIONS), so this is
157 // safe even for tokens issued before scopes were added to the app's
158 // registered oauth_scope.
159 scope: (requestedScopes?.length
160 ? requestedScopes
161 : CLAUDE_AI_OAUTH_SCOPES
162 ).join(' '),
163 }
164
165 try {
166 const response = await axios.post(getOauthConfig().TOKEN_URL, requestBody, {
167 headers: { 'Content-Type': 'application/json' },
168 timeout: 15000,
169 })
170
171 if (response.status !== 200) {
172 throw new Error(`Token refresh failed: ${response.statusText}`)
173 }
174
175 const data = response.data as OAuthTokenExchangeResponse
176 const {
177 access_token: accessToken,
178 refresh_token: newRefreshToken = refreshToken,
179 expires_in: expiresIn,
180 } = data
181
182 const expiresAt = Date.now() + expiresIn * 1000
183 const scopes = parseScopes(data.scope)
184
185 logEvent('tengu_oauth_token_refresh_success', {})
186
187 // Skip the extra /api/oauth/profile round-trip when we already have both
188 // the global-config profile fields AND the secure-storage subscription data.
189 // Routine refreshes satisfy both, so we cut ~7M req/day fleet-wide.
190 //
191 // Checking secure storage (not just config) matters for the
192 // CLAUDE_CODE_OAUTH_REFRESH_TOKEN re-login path: installOAuthTokens runs
193 // performLogout() AFTER we return, wiping secure storage. If we returned
194 // null for subscriptionType here, saveOAuthTokensIfNeeded would persist
195 // null ?? (wiped) ?? null = null, and every future refresh would see the
196 // config guard fields satisfied and skip again, permanently losing the
197 // subscription type for paying users. By passing through existing values,
198 // the re-login path writes cached ?? wiped ?? null = cached; and if secure
199 // storage was already empty we fall through to the fetch.
200 const config = getGlobalConfig()
201 const existing = getClaudeAIOAuthTokens()
202 const haveProfileAlready =
203 config.oauthAccount?.billingType !== undefined &&

Callers 2

authLoginFunction · 0.85

Calls 8

getOauthConfigFunction · 0.85
parseScopesFunction · 0.85
logEventFunction · 0.85
getGlobalConfigFunction · 0.85
fetchProfileInfoFunction · 0.85
saveGlobalConfigFunction · 0.85
postMethod · 0.80
keysMethod · 0.80

Tested by

no test coverage detected