A better Rust ATProto crate

import cleanup and fixing some client metadata failures due to adjusting behaviour earlier

Orual f934cf26 40c7d5ab

Changed files
+30 -39
crates
jacquard-common
src
jacquard-oauth
+1 -1
crates/jacquard-common/src/error.rs
··· 149 149 RefreshFailed, 150 150 151 151 /// Request requires authentication but none was provided 152 - #[error("No authentication provided")] 152 + #[error("No authentication provided, but endpoint requires auth")] 153 153 NotAuthenticated, 154 154 155 155 /// Other authentication error
+18 -17
crates/jacquard-oauth/src/atproto.rs
··· 232 232 Url::from_str("http://127.0.0.1/").unwrap(), 233 233 Url::from_str("http://[::1]/").unwrap(), 234 234 ], 235 - scope: None, 235 + scope: Some(CowStr::new_static("atproto")), 236 236 grant_types: None, 237 237 token_endpoint_auth_method: Some(AuthMethod::None.into()), 238 238 dpop_bound_access_tokens: None, ··· 262 262 .expect("failed to convert metadata"), 263 263 OAuthClientMetadata { 264 264 client_id: Url::from_str( 265 - "http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric" 265 + "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&redirect_uri=http%3A%2F%2F127.0.0.1%2Fcallback&scope=account%3Aemail+atproto+transition%3Ageneric" 266 266 ).unwrap(), 267 267 client_uri: None, 268 268 redirect_uris: vec![ 269 - Url::from_str("http://localhost/callback").unwrap(), 270 - Url::from_str("http://localhost/callback").unwrap(), 269 + Url::from_str("http://127.0.0.1/callback").unwrap(), 270 + // TODO: fix this so that it respects IPv6 271 + Url::from_str("http://127.0.0.1/callback").unwrap(), 271 272 ], 272 - scope: None, 273 + scope: Some(CowStr::new_static("account:email atproto transition:generic")), 273 274 grant_types: None, 274 275 token_endpoint_auth_method: Some(AuthMethod::None.into()), 275 276 dpop_bound_access_tokens: None, ··· 291 292 ), 292 293 &None, 293 294 ) 294 - .expect("should coerce to localhost"); 295 + .expect("should coerce to 127.0.0.1"); 295 296 assert_eq!( 296 297 out, 297 298 OAuthClientMetadata { 298 299 client_id: Url::from_str( 299 - "http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F" 300 + "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 300 301 ) 301 302 .unwrap(), 302 303 client_uri: None, 303 - redirect_uris: vec![Url::from_str("http://localhost/").unwrap()], 304 - scope: None, 304 + redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()], 305 + scope: Some(CowStr::new_static("atproto")), 305 306 grant_types: None, 306 307 token_endpoint_auth_method: Some(AuthMethod::None.into()), 307 308 dpop_bound_access_tokens: None, ··· 319 320 ), 320 321 &None, 321 322 ) 322 - .expect("should coerce to localhost"); 323 + .expect("should coerce to 127.0.0.1"); 323 324 assert_eq!( 324 325 out, 325 326 OAuthClientMetadata { 326 327 client_id: Url::from_str( 327 - "http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F" 328 + "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F" 328 329 ) 329 330 .unwrap(), 330 331 client_uri: None, 331 - redirect_uris: vec![Url::from_str("http://localhost/").unwrap()], 332 - scope: None, 332 + redirect_uris: vec![Url::from_str("http://127.0.0.1:8000/").unwrap()], 333 + scope: Some(CowStr::new_static("atproto")), 333 334 grant_types: None, 334 335 token_endpoint_auth_method: Some(AuthMethod::None.into()), 335 336 dpop_bound_access_tokens: None, ··· 347 348 ), 348 349 &None, 349 350 ) 350 - .expect("should coerce to localhost"); 351 + .expect("should coerce to 127.0.0.1"); 351 352 assert_eq!( 352 353 out, 353 354 OAuthClientMetadata { 354 355 client_id: Url::from_str( 355 - "http://localhost?redirect_uri=http%3A%2F%2Flocalhost%2F" 356 + "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 356 357 ) 357 358 .unwrap(), 358 359 client_uri: None, 359 - redirect_uris: vec![Url::from_str("http://localhost/").unwrap()], 360 - scope: None, 360 + redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()], 361 + scope: Some(CowStr::new_static("atproto")), 361 362 grant_types: None, 362 363 token_endpoint_auth_method: Some(AuthMethod::None.into()), 363 364 dpop_bound_access_tokens: None,
+11 -21
crates/jacquard-oauth/src/loopback.rs
··· 10 10 scopes::Scope, 11 11 types::{AuthorizeOptions, CallbackParams}, 12 12 }; 13 - use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr}; 13 + use jacquard_common::{IntoStatic, cowstr::ToCowStr}; 14 14 use rouille::Server; 15 - use std::{net::SocketAddr, sync::Arc}; 16 - use tokio::{ 17 - net::TcpListener, 18 - sync::{Mutex, mpsc, oneshot}, 19 - }; 15 + use std::net::SocketAddr; 16 + use tokio::sync::mpsc; 20 17 use url::Url; 21 18 22 19 #[derive(Clone, Debug)] ··· 108 105 opts: AuthorizeOptions<'_>, 109 106 cfg: LoopbackConfig, 110 107 ) -> crate::error::Result<super::client::OAuthSession<T, S>> { 111 - // 1) Bind server first to learn effective port 112 108 let port = match cfg.port { 113 109 LoopbackPort::Fixed(p) => p, 114 110 LoopbackPort::Ephemeral => 0, 115 111 }; 116 - // TODO: fix this to it also accepts ipv6 112 + // TODO: fix this to it also accepts ipv6 and properly finds a free port 117 113 let bind_addr: SocketAddr = format!("0.0.0.0:{}", port) 118 114 .parse() 119 115 .expect("invalid loopback host/port"); 120 116 let (local_addr, handle) = one_shot_server(bind_addr); 121 117 println!("Listening on {}", local_addr); 122 - 123 - // 2) Build per-flow metadata with the actual redirect URI 118 + // build redirect uri 124 119 let redirect = Url::parse(&format!( 125 120 "http://{}:{}/oauth/callback", 126 121 cfg.host, ··· 138 133 ), 139 134 }; 140 135 141 - // Build a per-flow client using shared store and resolver 136 + // Build client using store and resolver 142 137 let flow_client = OAuthClient::new_with_shared( 143 138 self.registry.store.clone(), 144 139 self.client.clone(), 145 140 client_data.clone(), 146 141 ); 147 142 148 - // 3) Start auth (persists state) and get authorization URL 143 + // Start auth and get authorization URL 149 144 let auth_url = flow_client.start_auth(input.as_ref(), opts).await?; 150 145 // Print URL for copy/paste 151 - println!("Open this URL to authorize:\n{}\n", auth_url); 146 + println!("To authenticate with your PDS, visit:\n{}\n", auth_url); 152 147 // Optionally open browser 153 148 if cfg.open_browser { 154 149 let _ = try_open_in_browser(&auth_url); 155 150 } 156 151 157 - // 4) Await callback or timeout 152 + // Await callback or timeout 158 153 let mut callback_rx = handle.callback_rx; 159 154 let cb = tokio::time::timeout( 160 155 std::time::Duration::from_millis(cfg.timeout_ms), ··· 163 158 .await; 164 159 // trigger shutdown 165 160 let _ = handle.server_stop.send(()); 166 - if let Err(_) = cb { 167 - return Err(OAuthError::Callback(CallbackError::Timeout)); 168 - } 169 - 170 161 if let Ok(Some(cb)) = cb { 171 - // 5) Continue with callback flow 172 - let session = flow_client.callback(cb).await?; 173 - Ok(session) 162 + // Handle callback and create a session 163 + Ok(flow_client.callback(cb).await?) 174 164 } else { 175 165 Err(OAuthError::Callback(CallbackError::Timeout)) 176 166 }