@@ -3,37 +3,28 @@ use anyhow::Result;
33use super :: { OAuthProvider , OAuthToken } ;
44
55/// Trait abstracting per-provider OAuth operations.
6- ///
7- /// Each provider implements login (obtain initial token) and refresh
8- /// (re-validate or refresh an expired token). The trait uses dynamic
9- /// dispatch so providers can be selected at runtime.
106pub trait OAuthProviderHandler : Send + Sync {
11- /// Which provider this handler serves.
127 fn provider ( & self ) -> OAuthProvider ;
138
14- /// Obtain an initial OAuth token (interactive: may open browser, read CLI files, etc.)
159 fn login (
1610 & self ,
1711 profile_name : & str ,
1812 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > ;
1913
20- /// Refresh an existing token. Returns the new token.
2114 fn refresh (
2215 & self ,
2316 profile_name : & str ,
2417 token : & OAuthToken ,
2518 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > ;
2619
27- /// Read token from external CLI files (non-interactive).
28- /// Falls back to keyring if no external CLI is available.
2920 fn read_external_token ( & self ) -> Result < OAuthToken > ;
3021}
3122
3223/// Factory: get the handler for a given provider.
3324pub fn for_provider ( provider : & OAuthProvider ) -> Box < dyn OAuthProviderHandler > {
34- match provider {
25+ match provider. normalize ( ) {
3526 OAuthProvider :: Claude => Box :: new ( ClaudeHandler ) ,
36- OAuthProvider :: Openai => Box :: new ( OpenaiHandler ) ,
27+ OAuthProvider :: Chatgpt | OAuthProvider :: Openai => Box :: new ( ChatgptHandler ) ,
3728 OAuthProvider :: Google => Box :: new ( ExternalCliHandler {
3829 provider : OAuthProvider :: Google ,
3930 } ) ,
@@ -43,13 +34,11 @@ pub fn for_provider(provider: &OAuthProvider) -> Box<dyn OAuthProviderHandler> {
4334 OAuthProvider :: Qwen => Box :: new ( DeviceCodeHandler {
4435 provider : OAuthProvider :: Qwen ,
4536 } ) ,
46- OAuthProvider :: Github => Box :: new ( DeviceCodeHandler {
47- provider : OAuthProvider :: Github ,
48- } ) ,
37+ OAuthProvider :: Github => Box :: new ( GithubHandler ) ,
4938 }
5039}
5140
52- // ── Claude: read ~/.claude/.credentials.json ──
41+ // ── Claude ──
5342
5443struct ClaudeHandler ;
5544
@@ -63,11 +52,8 @@ impl OAuthProviderHandler for ClaudeHandler {
6352 _profile_name : & str ,
6453 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
6554 Box :: pin ( async {
66- println ! ( "Reading Claude credentials from ~/.claude/.credentials.json..." ) ;
67- let token = super :: token:: read_claude_credentials ( )
68- . map_err ( |e| anyhow:: anyhow!( "Failed to read Claude credentials: {e}" ) ) ?;
69- println ! ( "Note: Claude subscription profiles bypass the proxy (Claude Code uses its own OAuth)." ) ;
70- Ok ( token)
55+ let cred = super :: source:: read_claude_credentials ( ) ?;
56+ Ok ( cred. into_oauth_token ( ) )
7157 } )
7258 }
7359
@@ -77,57 +63,32 @@ impl OAuthProviderHandler for ClaudeHandler {
7763 _token : & OAuthToken ,
7864 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
7965 Box :: pin ( async {
80- let token = super :: token:: read_claude_credentials ( ) ?;
81- println ! ( "Refreshed Claude token from ~/.claude/.credentials.json" ) ;
82- Ok ( token)
66+ let cred = super :: source:: read_claude_credentials ( ) ?;
67+ Ok ( cred. into_oauth_token ( ) )
8368 } )
8469 }
8570
8671 fn read_external_token ( & self ) -> Result < OAuthToken > {
87- super :: token :: read_claude_credentials ( )
72+ super :: source :: read_claude_credentials ( ) . map ( |c| c . into_oauth_token ( ) )
8873 }
8974}
9075
91- // ── OpenAI: read Codex CLI + refresh_token ──
76+ // ── ChatGPT (was OpenAI) ──
9277
93- struct OpenaiHandler ;
78+ struct ChatgptHandler ;
9479
95- impl OAuthProviderHandler for OpenaiHandler {
80+ impl OAuthProviderHandler for ChatgptHandler {
9681 fn provider ( & self ) -> OAuthProvider {
97- OAuthProvider :: Openai
82+ OAuthProvider :: Chatgpt
9883 }
9984
10085 fn login (
10186 & self ,
102- profile_name : & str ,
87+ _profile_name : & str ,
10388 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
104- let profile_name = profile_name. to_string ( ) ;
105- Box :: pin ( async move {
106- match super :: token:: read_codex_credentials ( ) {
107- Ok ( token) => {
108- let auth_mode = token
109- . extra
110- . as_ref ( )
111- . and_then ( |e| e. get ( "auth_mode" ) )
112- . and_then ( |v| v. as_str ( ) )
113- . unwrap_or ( "unknown" ) ;
114- println ! ( "Found Codex CLI credentials (auth_mode: {auth_mode})" ) ;
115- println ! ( "Token will be refreshed automatically from ~/.codex/auth.json" ) ;
116- Ok ( token)
117- }
118- Err ( _) => {
119- println ! ( "No Codex CLI credentials found at ~/.codex/auth.json" ) ;
120- println ! ( ) ;
121- println ! ( "To use your ChatGPT subscription with Claudex:" ) ;
122- println ! ( " 1. Install Codex CLI: npm install -g @openai/codex" ) ;
123- println ! ( " 2. Login: codex --login" ) ;
124- println ! (
125- " 3. Re-run: claudex auth login openai --profile {}" ,
126- profile_name
127- ) ;
128- anyhow:: bail!( "no OpenAI credentials available" )
129- }
130- }
89+ Box :: pin ( async {
90+ let cred = super :: source:: read_codex_credentials ( ) ?;
91+ Ok ( cred. into_oauth_token ( ) )
13192 } )
13293 }
13394
@@ -138,18 +99,15 @@ impl OAuthProviderHandler for OpenaiHandler {
13899 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
139100 let refresh_tok = token. refresh_token . clone ( ) ;
140101 Box :: pin ( async move {
141- let refresh_tok = refresh_tok. ok_or_else ( || {
142- anyhow:: anyhow!(
143- "no refresh_token in Codex credentials, please re-login with `codex --login`"
144- )
145- } ) ?;
146- super :: providers:: refresh_openai_token_pub ( & refresh_tok, Some ( refresh_tok. clone ( ) ) )
147- . await
102+ let refresh_tok = refresh_tok
103+ . ok_or_else ( || anyhow:: anyhow!( "no refresh_token, please re-login" ) ) ?;
104+ let client = reqwest:: Client :: new ( ) ;
105+ super :: exchange:: refresh_chatgpt_token ( & client, & refresh_tok) . await
148106 } )
149107 }
150108
151109 fn read_external_token ( & self ) -> Result < OAuthToken > {
152- super :: token :: read_external_token ( & OAuthProvider :: Openai )
110+ super :: source :: read_codex_credentials ( ) . map ( |c| c . into_oauth_token ( ) )
153111 }
154112}
155113
@@ -170,12 +128,8 @@ impl OAuthProviderHandler for ExternalCliHandler {
170128 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
171129 let provider = self . provider . clone ( ) ;
172130 Box :: pin ( async move {
173- println ! (
174- "Reading {} credentials from external CLI..." ,
175- provider. display_name( )
176- ) ;
177- let token = super :: token:: read_external_token ( & provider) ?;
178- Ok ( token)
131+ let cred = super :: source:: load_credential_chain ( & provider) ?;
132+ Ok ( cred. into_oauth_token ( ) )
179133 } )
180134 }
181135
@@ -186,21 +140,72 @@ impl OAuthProviderHandler for ExternalCliHandler {
186140 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
187141 let provider = self . provider . clone ( ) ;
188142 Box :: pin ( async move {
189- let token = super :: token:: read_external_token ( & provider) ?;
190- println ! (
191- "Refreshed {} token from external CLI" ,
192- provider. display_name( )
193- ) ;
194- Ok ( token)
143+ let cred = super :: source:: load_credential_chain ( & provider) ?;
144+ Ok ( cred. into_oauth_token ( ) )
145+ } )
146+ }
147+
148+ fn read_external_token ( & self ) -> Result < OAuthToken > {
149+ super :: source:: load_credential_chain ( & self . provider ) . map ( |c| c. into_oauth_token ( ) )
150+ }
151+ }
152+
153+ // ── GitHub Copilot ──
154+
155+ struct GithubHandler ;
156+
157+ impl OAuthProviderHandler for GithubHandler {
158+ fn provider ( & self ) -> OAuthProvider {
159+ OAuthProvider :: Github
160+ }
161+
162+ fn login (
163+ & self ,
164+ _profile_name : & str ,
165+ ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
166+ Box :: pin ( async {
167+ let cred = super :: source:: load_credential_chain ( & OAuthProvider :: Github ) ?;
168+ let client = reqwest:: Client :: new ( ) ;
169+ let copilot =
170+ super :: exchange:: exchange_github_for_copilot ( & client, & cred. access_token ) . await ?;
171+ Ok ( OAuthToken {
172+ access_token : copilot. token ,
173+ refresh_token : None ,
174+ expires_at : Some ( copilot. expires_at * 1000 ) ,
175+ token_type : Some ( "Bearer" . to_string ( ) ) ,
176+ scopes : None ,
177+ extra : Some ( serde_json:: json!( { "provider" : "copilot" } ) ) ,
178+ } )
179+ } )
180+ }
181+
182+ fn refresh (
183+ & self ,
184+ _profile_name : & str ,
185+ _token : & OAuthToken ,
186+ ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
187+ Box :: pin ( async {
188+ let cred = super :: source:: load_credential_chain ( & OAuthProvider :: Github ) ?;
189+ let client = reqwest:: Client :: new ( ) ;
190+ let copilot =
191+ super :: exchange:: exchange_github_for_copilot ( & client, & cred. access_token ) . await ?;
192+ Ok ( OAuthToken {
193+ access_token : copilot. token ,
194+ refresh_token : None ,
195+ expires_at : Some ( copilot. expires_at * 1000 ) ,
196+ token_type : Some ( "Bearer" . to_string ( ) ) ,
197+ scopes : None ,
198+ extra : Some ( serde_json:: json!( { "provider" : "copilot" } ) ) ,
199+ } )
195200 } )
196201 }
197202
198203 fn read_external_token ( & self ) -> Result < OAuthToken > {
199- super :: token :: read_external_token ( & self . provider )
204+ super :: source :: read_copilot_config ( ) . map ( |c| c . into_oauth_token ( ) )
200205 }
201206}
202207
203- // ── Device Code: GitHub, Qwen ──
208+ // ── Device Code: Qwen ──
204209
205210struct DeviceCodeHandler {
206211 provider : OAuthProvider ,
@@ -217,9 +222,11 @@ impl OAuthProviderHandler for DeviceCodeHandler {
217222 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
218223 let provider = self . provider . clone ( ) ;
219224 Box :: pin ( async move {
220- // Device code login is handled by providers::login_device_code
221- // which requires interactive I/O. Delegate to the existing impl.
222- super :: providers:: login_device_code_pub ( & provider) . await
225+ // Qwen device code login requires interactive I/O
226+ anyhow:: bail!(
227+ "use `claudex auth login {}` for interactive device code flow" ,
228+ provider. display_name( ) . to_lowercase( )
229+ )
223230 } )
224231 }
225232
@@ -228,15 +235,32 @@ impl OAuthProviderHandler for DeviceCodeHandler {
228235 profile_name : & str ,
229236 _token : & OAuthToken ,
230237 ) -> std:: pin:: Pin < Box < dyn std:: future:: Future < Output = Result < OAuthToken > > + Send + ' _ > > {
231- let provider = self . provider . clone ( ) ;
232238 let profile_name = profile_name. to_string ( ) ;
233239 Box :: pin ( async move {
234- super :: providers:: refresh_device_code_pub ( & provider, & profile_name) . await
240+ let token = super :: source:: load_keyring ( & profile_name) ?;
241+ let refresh_token = token
242+ . refresh_token
243+ . as_ref ( )
244+ . ok_or_else ( || anyhow:: anyhow!( "no refresh_token, please re-login" ) ) ?;
245+ let client = reqwest:: Client :: new ( ) ;
246+ let resp = super :: server:: refresh_access_token (
247+ & client,
248+ "https://chat.qwen.ai/api/oauth/token" ,
249+ refresh_token,
250+ "claudex-qwen" ,
251+ )
252+ . await ?;
253+ let mut new_token = OAuthToken :: from_token_response ( & resp)
254+ . ok_or_else ( || anyhow:: anyhow!( "failed to parse refreshed token" ) ) ?;
255+ if new_token. refresh_token . is_none ( ) {
256+ new_token. refresh_token = token. refresh_token ;
257+ }
258+ Ok ( new_token)
235259 } )
236260 }
237261
238262 fn read_external_token ( & self ) -> Result < OAuthToken > {
239- super :: token :: read_external_token ( & self . provider )
263+ anyhow :: bail! ( "Qwen has no external CLI credentials" )
240264 }
241265}
242266
@@ -246,38 +270,29 @@ mod tests {
246270
247271 #[ test]
248272 fn test_factory_returns_correct_provider ( ) {
249- let handler = for_provider ( & OAuthProvider :: Claude ) ;
250- assert_eq ! ( handler. provider( ) , OAuthProvider :: Claude ) ;
251-
252- let handler = for_provider ( & OAuthProvider :: Openai ) ;
253- assert_eq ! ( handler. provider( ) , OAuthProvider :: Openai ) ;
254-
255- let handler = for_provider ( & OAuthProvider :: Google ) ;
256- assert_eq ! ( handler. provider( ) , OAuthProvider :: Google ) ;
257-
258- let handler = for_provider ( & OAuthProvider :: Qwen ) ;
259- assert_eq ! ( handler. provider( ) , OAuthProvider :: Qwen ) ;
260-
261- let handler = for_provider ( & OAuthProvider :: Kimi ) ;
262- assert_eq ! ( handler. provider( ) , OAuthProvider :: Kimi ) ;
263-
264- let handler = for_provider ( & OAuthProvider :: Github ) ;
265- assert_eq ! ( handler. provider( ) , OAuthProvider :: Github ) ;
273+ assert_eq ! ( for_provider( & OAuthProvider :: Claude ) . provider( ) , OAuthProvider :: Claude ) ;
274+ assert_eq ! ( for_provider( & OAuthProvider :: Chatgpt ) . provider( ) , OAuthProvider :: Chatgpt ) ;
275+ // Openai normalizes to Chatgpt handler
276+ assert_eq ! ( for_provider( & OAuthProvider :: Openai ) . provider( ) , OAuthProvider :: Chatgpt ) ;
277+ assert_eq ! ( for_provider( & OAuthProvider :: Google ) . provider( ) , OAuthProvider :: Google ) ;
278+ assert_eq ! ( for_provider( & OAuthProvider :: Qwen ) . provider( ) , OAuthProvider :: Qwen ) ;
279+ assert_eq ! ( for_provider( & OAuthProvider :: Kimi ) . provider( ) , OAuthProvider :: Kimi ) ;
280+ assert_eq ! ( for_provider( & OAuthProvider :: Github ) . provider( ) , OAuthProvider :: Github ) ;
266281 }
267282
268283 #[ test]
269284 fn test_all_providers_have_handler ( ) {
270285 let providers = [
271286 OAuthProvider :: Claude ,
287+ OAuthProvider :: Chatgpt ,
272288 OAuthProvider :: Openai ,
273289 OAuthProvider :: Google ,
274290 OAuthProvider :: Qwen ,
275291 OAuthProvider :: Kimi ,
276292 OAuthProvider :: Github ,
277293 ] ;
278294 for p in & providers {
279- let handler = for_provider ( p) ;
280- assert_eq ! ( & handler. provider( ) , p) ;
295+ let _handler = for_provider ( p) ;
281296 }
282297 }
283298}
0 commit comments