Skip to content

Commit 85d9c10

Browse files
committed
fix: restore removed comments in provider-handler, token-refresh-manager, and connect
1 parent 52fcb94 commit 85d9c10

6 files changed

Lines changed: 54 additions & 7 deletions

File tree

src/oclif/commands/providers/connect.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ export default class ProviderConnect extends Command {
159159
throw new Error(`Provider "${providerId}" does not support OAuth. Use --api-key instead.`)
160160
}
161161

162+
// --code is only valid for code-paste providers (e.g., Anthropic).
163+
// Browser-callback providers like OpenAI handle the code exchange automatically.
162164
if (code && provider.oauthCallbackMode !== 'code-paste') {
163165
throw new Error(
164166
`Provider "${providerId}" uses browser-based OAuth and does not accept --code.\n` +
@@ -185,6 +187,7 @@ export default class ProviderConnect extends Command {
185187
throw new Error(startResponse.error ?? 'Failed to start OAuth flow')
186188
}
187189

190+
// Always print auth URL (user's machine may not support browser launch)
188191
if (startResponse.callbackMode === 'device') {
189192
onProgress?.(`\nOpen this URL and enter the code below:`)
190193
onProgress?.(` ${startResponse.verificationUri ?? startResponse.authUrl}`)
@@ -194,6 +197,7 @@ export default class ProviderConnect extends Command {
194197
onProgress?.(`\nOpen this URL to authenticate:\n ${startResponse.authUrl}\n`)
195198
}
196199

200+
// 3. Handle based on callback mode
197201
if (startResponse.callbackMode === 'auto' || startResponse.callbackMode === 'device') {
198202
if (startResponse.callbackMode === 'auto') {
199203
onProgress?.('Waiting for authentication in browser...')
@@ -211,6 +215,7 @@ export default class ProviderConnect extends Command {
211215
return {providerName: provider.name, showInstructions: false}
212216
}
213217

218+
// code-paste mode: print instructions and exit
214219
onProgress?.('Copy the authorization code from the browser and run:')
215220
onProgress?.(` brv providers connect ${providerId} --oauth --code <code>`)
216221
return {providerName: provider.name, showInstructions: true}

src/server/infra/http/provider-model-fetchers.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -545,16 +545,14 @@ export class ChatBasedModelFetcher implements IProviderModelFetcher {
545545
}
546546
}
547547

548-
// ============================================================================
549-
// OpenRouter Model Fetcher (wraps existing client)
550-
// ============================================================================
551-
552-
import {getOpenRouterApiClient, type NormalizedModel} from './openrouter-api-client.js'
553-
554548
// ============================================================================
555549
// Copilot Model Fetcher
556550
// ============================================================================
557551

552+
/**
553+
* Model fetcher for GitHub Copilot.
554+
* Queries the Copilot API for available models using the session token.
555+
*/
558556
export class CopilotModelFetcher implements IProviderModelFetcher {
559557
private cache: ModelCache | undefined
560558
private readonly cacheTtlMs: number
@@ -636,6 +634,13 @@ export class CopilotModelFetcher implements IProviderModelFetcher {
636634
// ============================================================================
637635
// OpenRouter Model Fetcher (wraps existing client)
638636
// ============================================================================
637+
638+
import {getOpenRouterApiClient, type NormalizedModel} from './openrouter-api-client.js'
639+
640+
/**
641+
* Model fetcher that wraps the existing OpenRouterApiClient.
642+
* Adapts NormalizedModel to ProviderModelInfo.
643+
*/
639644
export class OpenRouterModelFetcher implements IProviderModelFetcher {
640645
async fetchModels(apiKey: string, options?: FetchModelsOptions): Promise<ProviderModelInfo[]> {
641646
const client = getOpenRouterApiClient()

src/server/infra/provider-oauth/token-refresh-manager.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ export class TokenRefreshManager implements ITokenRefreshManager {
117117
const contentType: TokenRequestContentType =
118118
oauthConfig.tokenContentType === 'form' ? 'application/x-www-form-urlencoded' : 'application/json'
119119

120+
// 5. Attempt refresh
120121
try {
121122
const tokens = await this.exchangeRefreshToken({
122123
clientId: oauthConfig.clientId,
@@ -125,6 +126,7 @@ export class TokenRefreshManager implements ITokenRefreshManager {
125126
tokenUrl: oauthConfig.tokenUrl,
126127
})
127128

129+
// 6. Success: update keychain + encrypted store
128130
await this.deps.providerKeychainStore.setApiKey(providerId, tokens.access_token)
129131

130132
const newExpiresAt = tokens.expires_in ? computeExpiresAt(tokens.expires_in) : tokenRecord.expiresAt
@@ -142,6 +144,7 @@ export class TokenRefreshManager implements ITokenRefreshManager {
142144
}
143145

144146
private async handleRefreshError(providerId: string, error: unknown): Promise<boolean> {
147+
// 7. Permanent failure (token revoked, client invalid): disconnect provider, clean up
145148
if (isPermanentOAuthError(error)) {
146149
await this.deps.providerConfigStore.disconnectProvider(providerId).catch(() => {})
147150
await this.deps.providerOAuthTokenStore.delete(providerId).catch(() => {})
@@ -150,6 +153,9 @@ export class TokenRefreshManager implements ITokenRefreshManager {
150153
return false
151154
}
152155

156+
// Transient errors (network timeout, 5xx): keep credentials intact.
157+
// Return true so the caller uses the existing access token from the keychain,
158+
// which may still be valid until it actually expires.
153159
processLog(
154160
`[TokenRefreshManager] Transient refresh error for ${providerId}: ${error instanceof Error ? error.message : String(error)}`,
155161
)

src/server/infra/transport/handlers/provider-handler.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,20 @@ type OAuthFlowState = {
7777
export interface ProviderHandlerDeps {
7878
authStateStore: IAuthStateStore
7979
browserLauncher: IBrowserLauncher
80+
/** Factory for creating callback servers (injectable for testing) */
8081
createCallbackServer?: (options: {callbackPath?: string; port: number}) => ProviderCallbackServer
82+
/** Token exchange function (injectable for testing) */
8183
exchangeCodeForTokens?: (params: TokenExchangeParams) => Promise<ProviderTokenResponse>
84+
/** Copilot token exchange function (injectable for testing) */
8285
exchangeForCopilotToken?: (githubToken: string) => Promise<CopilotTokenResponse>
86+
/** PKCE generator function (injectable for testing) */
8387
generatePkce?: () => PkceParameters
88+
/** Device flow polling function (injectable for testing) */
8489
pollForAccessToken?: (params: PollForAccessTokenParams) => Promise<string>
8590
providerConfigStore: IProviderConfigStore
8691
providerKeychainStore: IProviderKeychainStore
8792
providerOAuthTokenStore: IProviderOAuthTokenStore
93+
/** Device code request function (injectable for testing) */
8894
requestDeviceCode?: (params: RequestDeviceCodeParams) => Promise<DeviceCodeResponse>
8995
transport: ITransportServer
9096
}
@@ -165,18 +171,23 @@ export class ProviderHandler {
165171
tokenUrl: oauthConfig.tokenUrl,
166172
})
167173

174+
// Parse JWT id_token for account ID
168175
const oauthAccountId = tokens.id_token ? parseAccountIdFromIdToken(tokens.id_token) : undefined
169176

177+
// Store access token as the "API key" in keychain
170178
await this.providerKeychainStore.setApiKey(providerId, tokens.access_token)
171179

180+
// Store refresh token + expiry in encrypted OAuth token store
172181
if (tokens.refresh_token) {
173-
const expiresAt = tokens.expires_in ? computeExpiresAt(tokens.expires_in) : computeExpiresAt(3600)
182+
const expiresAt = tokens.expires_in ? computeExpiresAt(tokens.expires_in) : computeExpiresAt(3600) // 1-hour default when provider omits expires_in
174183
await this.providerOAuthTokenStore.set(providerId, {
175184
expiresAt,
176185
refreshToken: tokens.refresh_token,
177186
})
178187
}
179188

189+
// Connect provider — secrets stored in keychain + encrypted token store, not config
190+
// OAuth providers may define their own default model (e.g., Codex for OpenAI OAuth)
180191
const defaultModel = oauthConfig.defaultModel ?? providerDef.defaultModel
181192
await this.providerConfigStore.connectProvider(providerId, {
182193
activeModel: defaultModel,
@@ -266,6 +277,7 @@ export class ProviderHandler {
266277

267278
return {error: getErrorMessage(error), success: false}
268279
} finally {
280+
// Only clean up if this is still the same flow (guard against concurrent START_OAUTH)
269281
if (this.oauthFlows.get(data.providerId) === flow) {
270282
await flow.callbackServer?.stop().catch(() => {})
271283
this.oauthFlows.delete(data.providerId)
@@ -429,8 +441,10 @@ export class ProviderHandler {
429441
return await this.startDeviceFlow(data.providerId, oauthConfig, clientId)
430442
}
431443

444+
// Generate PKCE parameters
432445
const pkce = this.generatePkce()
433446

447+
// Build auth URL
434448
const mode = oauthConfig.modes.find((m) => m.id === (data.mode ?? 'default')) ?? oauthConfig.modes[0]
435449
const params = new URLSearchParams({
436450
client_id: oauthConfig.clientId,
@@ -442,6 +456,7 @@ export class ProviderHandler {
442456
state: pkce.state,
443457
})
444458

459+
// Provider-specific extra params (e.g. OpenAI's codex_cli_simplified_flow)
445460
if (oauthConfig.extraParams) {
446461
for (const [key, value] of Object.entries(oauthConfig.extraParams)) {
447462
params.set(key, value)
@@ -450,19 +465,22 @@ export class ProviderHandler {
450465

451466
const authUrl = `${mode.authUrl}?${params.toString()}`
452467

468+
// Start callback server for auto mode
453469
let callbackServer: ProviderCallbackServer | undefined
454470
if (oauthConfig.callbackMode === 'auto' && oauthConfig.callbackPort) {
455471
callbackServer = this.createCallbackServer({port: oauthConfig.callbackPort})
456472
await callbackServer.start()
457473
}
458474

475+
// Store flow state
459476
this.oauthFlows.set(data.providerId, {
460477
callbackServer,
461478
clientId,
462479
codeVerifier: pkce.codeVerifier,
463480
state: pkce.state,
464481
})
465482

483+
// Open browser (non-fatal on failure)
466484
try {
467485
await this.browserLauncher.open(authUrl)
468486
} catch {
@@ -471,6 +489,7 @@ export class ProviderHandler {
471489

472490
return {authUrl, callbackMode: oauthConfig.callbackMode, success: true}
473491
} catch (error) {
492+
// Clean up callback server if it was started but flow setup failed
474493
const partialFlow = this.oauthFlows.get(data.providerId)
475494
if (partialFlow?.callbackServer) {
476495
await partialFlow.callbackServer.stop().catch(() => {})

test/unit/infra/http/copilot-model-fetcher.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* eslint-disable camelcase */
2+
import axios from 'axios'
23
import {expect} from 'chai'
34
import nock from 'nock'
45
import {restore, stub} from 'sinon'
@@ -158,5 +159,15 @@ describe('CopilotModelFetcher', () => {
158159

159160
expect(result.isValid).to.be.false
160161
})
162+
163+
it('should return isValid false with error message for non-Axios errors', async () => {
164+
stub(axios, 'get').rejects(new TypeError('Cannot read properties of undefined'))
165+
166+
const fetcher = new CopilotModelFetcher()
167+
const result = await fetcher.validateApiKey('token')
168+
169+
expect(result.isValid).to.be.false
170+
expect(result.error).to.equal('Cannot read properties of undefined')
171+
})
161172
})
162173
})

test/unit/infra/provider-oauth/token-refresh-manager.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
createMockTransportServer,
2323
} from '../../../helpers/mock-factories.js'
2424

25+
// Helper to create a provider config with OAuth
2526
function oauthConfig(providerId: string): ProviderConfig {
2627
return ProviderConfig.createDefault().withProviderConnected(providerId, {
2728
authMethod: 'oauth',

0 commit comments

Comments
 (0)