1- use anyhow:: Result ;
1+ use anyhow:: { Context , Result } ;
22use clap:: Subcommand ;
33use dialoguer:: { Confirm , Password } ;
44use serde_json:: json;
@@ -108,6 +108,11 @@ async fn login(
108108 #[ cfg( feature = "secure-storage" ) ]
109109 if secure {
110110 crate :: keyring:: set_key ( & profile, & key) ?;
111+ let saved = crate :: keyring:: get_key ( & profile) ?
112+ . context ( "API key was written to keyring but could not be read back" ) ?;
113+ if saved != key {
114+ anyhow:: bail!( "API key stored in keyring could not be verified after saving" ) ;
115+ }
111116
112117 if output. is_json ( ) || output. has_template ( ) {
113118 print_json_owned (
@@ -168,8 +173,10 @@ async fn logout(force: bool, output: &OutputOptions) -> Result<()> {
168173 #[ cfg( feature = "secure-storage" ) ]
169174 {
170175 let _ = crate :: keyring:: delete_key ( & profile) ; // Ignore errors (may not exist)
176+ let _ = crate :: keyring:: delete_oauth_tokens ( & profile) ; // Ignore errors (may not exist)
171177 }
172178
179+ config:: clear_oauth_config ( & profile) ?;
173180 config:: workspace_remove ( & profile) ?;
174181
175182 if output. is_json ( ) || output. has_template ( ) {
@@ -197,34 +204,50 @@ async fn status(validate: bool, output: &OutputOptions) -> Result<()> {
197204 . ok ( )
198205 . filter ( |p| !p. is_empty ( ) ) ;
199206
200- let configured = profile
207+ let config_file_configured = profile
201208 . as_ref ( )
202209 . and_then ( |p| config_data. workspaces . get ( p) )
203210 . map ( |w| !w. api_key . is_empty ( ) )
204211 . unwrap_or ( false ) ;
205212
206213 // Check keyring storage
207214 #[ cfg( feature = "secure-storage" ) ]
208- let keyring_configured = profile
215+ let api_key_keyring_configured = profile
209216 . as_ref ( )
210217 . and_then ( |p| crate :: keyring:: get_key ( p) . ok ( ) )
211218 . flatten ( )
212219 . is_some ( ) ;
213220 #[ cfg( not( feature = "secure-storage" ) ) ]
214- let keyring_configured = false ;
221+ let api_key_keyring_configured = false ;
222+
223+ #[ cfg( feature = "secure-storage" ) ]
224+ let oauth_keyring_configured = profile
225+ . as_ref ( )
226+ . and_then ( |p| crate :: keyring:: get_oauth_tokens ( p) . ok ( ) )
227+ . flatten ( )
228+ . is_some ( ) ;
229+ #[ cfg( not( feature = "secure-storage" ) ) ]
230+ let oauth_keyring_configured = false ;
215231
216232 #[ cfg( feature = "secure-storage" ) ]
217233 let keyring_available = crate :: keyring:: is_available ( ) ;
218234 #[ cfg( not( feature = "secure-storage" ) ) ]
219235 let keyring_available = false ;
220236
237+ let keyring_configured = api_key_keyring_configured || oauth_keyring_configured;
238+
221239 // Check OAuth config
240+ let oauth_metadata = profile
241+ . as_ref ( )
242+ . and_then ( |p| config:: get_oauth_metadata ( p) . ok ( ) )
243+ . flatten ( ) ;
244+ let oauth_configured = oauth_metadata. is_some ( ) ;
222245 let oauth_config = profile
223246 . as_ref ( )
224247 . and_then ( |p| config:: get_oauth_config ( p) . ok ( ) )
225248 . flatten ( ) ;
226- let oauth_configured = oauth_config. is_some ( ) ;
227249 let auth_type = if oauth_configured { "oauth" } else { "api_key" } ;
250+ let configured = config_file_configured || keyring_configured || oauth_configured;
228251
229252 let mut validated = None ;
230253 if validate {
@@ -271,11 +294,12 @@ async fn status(validate: bool, output: &OutputOptions) -> Result<()> {
271294 "profile" : profile,
272295 "configured" : configured,
273296 "keyring_configured" : keyring_configured,
297+ "oauth_keyring_configured" : oauth_keyring_configured,
274298 "keyring_available" : keyring_available,
275299 "auth_type" : auth_type,
276300 "oauth_configured" : oauth_configured,
277- "oauth_scopes" : oauth_config . as_ref( ) . map( |o| & o. scopes) ,
278- "oauth_expires_at" : oauth_config . as_ref( ) . and_then( |o| o. expires_at) ,
301+ "oauth_scopes" : oauth_metadata . as_ref( ) . map( |o| & o. scopes) ,
302+ "oauth_expires_at" : oauth_metadata . as_ref( ) . and_then( |o| o. expires_at) ,
279303 "env_api_key" : env_key. is_some( ) ,
280304 "env_profile" : env_profile,
281305 "validated" : validated,
@@ -295,6 +319,10 @@ async fn status(validate: bool, output: &OutputOptions) -> Result<()> {
295319 "Keyring available: {}" ,
296320 if keyring_available { "yes" } else { "no" }
297321 ) ;
322+ println ! (
323+ "OAuth keyring: {}" ,
324+ if oauth_keyring_configured { "yes" } else { "no" }
325+ ) ;
298326 println ! (
299327 "Env API key override: {}" ,
300328 if env_key. is_some( ) { "yes" } else { "no" }
@@ -303,7 +331,7 @@ async fn status(validate: bool, output: &OutputOptions) -> Result<()> {
303331 println ! ( "Validated: {}" , if validated { "yes" } else { "no" } ) ;
304332 }
305333 println ! ( "Auth type: {}" , auth_type) ;
306- if let Some ( ref oauth) = oauth_config {
334+ if let Some ( ref oauth) = oauth_metadata {
307335 println ! ( "OAuth scopes: {:?}" , oauth. scopes) ;
308336 if let Some ( expires) = oauth. expires_at {
309337 let dt = chrono:: DateTime :: from_timestamp ( expires, 0 )
@@ -503,18 +531,9 @@ async fn oauth_login(
503531
504532 #[ cfg( feature = "secure-storage" ) ]
505533 if secure {
506- let json = serde_json:: to_string ( & oauth_config) ?;
507- crate :: keyring:: set_oauth_tokens ( & profile, & json) ?;
508- // Save only metadata in config (no secrets)
509- let metadata_only = config:: OAuthConfig {
510- client_id : oauth_config. client_id . clone ( ) ,
511- access_token : String :: new ( ) ,
512- refresh_token : None ,
513- expires_at : oauth_config. expires_at ,
514- token_type : oauth_config. token_type . clone ( ) ,
515- scopes : oauth_config. scopes . clone ( ) ,
516- } ;
517- config:: save_oauth_config ( & profile, & metadata_only) ?;
534+ config:: save_oauth_config_secure ( & profile, & oauth_config) . context (
535+ "Secure OAuth storage failed. On macOS, locally built or unsigned binaries may not be able to read back Keychain items. Try the official signed release or fall back to plain `linear-cli auth oauth`." ,
536+ ) ?;
518537
519538 if output. is_json ( ) || output. has_template ( ) {
520539 print_json_owned (
0 commit comments