A better Rust ATProto crate

url rework

Orual 873b85e9 3bf78e2e

-1
Cargo.lock
··· 2506 2506 "serde_html_form", 2507 2507 "serde_json", 2508 2508 "sha2", 2509 - "signature", 2510 2509 "smol_str", 2511 2510 "thiserror 2.0.17", 2512 2511 "tokio",
-1
Cargo.toml
··· 52 52 ipld-core = { version = "0.4.2", features = ["serde"] } 53 53 multihash = "0.19" 54 54 dashmap = "6.1" 55 - moka = "0.12" 56 55 mini-moka = "0.10" 57 56 58 57 # Proc macros
+1 -1
crates/jacquard-common/Cargo.toml
··· 93 93 futures-lite = "2.6" 94 94 95 95 [package.metadata.docs.rs] 96 - features = [ "crypto-k256", "crypto-k256", "crypto-p256", "websocket", "zstd", "service-auth", "reqwest-client", "crypto"] 96 + features = [ "crypto-k256", "crypto-ed22519", "crypto-p256", "websocket", "zstd", "service-auth", "reqwest-client", "crypto"]
-1
crates/jacquard-oauth/Cargo.toml
··· 33 33 serde_html_form = { workspace = true } 34 34 miette = { workspace = true } 35 35 p256 = { workspace = true, features = ["ecdsa"] } 36 - signature = "2" 37 36 jose-jwa = "0.1" 38 37 jose-jwk = { workspace = true, features = ["p256"] } 39 38 chrono.workspace = true
+30 -21
crates/jacquard-oauth/src/atproto.rs
··· 152 152 #[derive(serde::Serialize)] 153 153 struct Parameters<'a> { 154 154 #[serde(skip_serializing_if = "Option::is_none")] 155 - redirect_uri: Option<Vec<Url>>, 155 + redirect_uri: Option<Vec<CowStr<'a>>>, 156 156 #[serde(skip_serializing_if = "Option::is_none")] 157 157 scope: Option<CowStr<'a>>, 158 158 } 159 159 let query = serde_html_form::to_string(Parameters { 160 - redirect_uri: redirect_uris.clone(), 160 + redirect_uri: redirect_uris.as_ref().map(|u| { 161 + u.iter() 162 + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) 163 + .collect() 164 + }), 161 165 scope: scopes 162 166 .as_ref() 163 167 .map(|s| Scope::serialize_multiple(s.as_slice())), 164 168 }) 165 169 .ok(); 166 - let mut client_id = String::from("http://localhost"); 170 + let mut client_id = String::from("http://localhost/"); 167 171 if let Some(query) = query 168 172 && !query.is_empty() 169 173 { ··· 173 177 client_id: Url::parse(&client_id).unwrap(), 174 178 client_uri: None, 175 179 redirect_uris: redirect_uris.unwrap_or(vec![ 176 - Url::from_str("http://127.0.0.1/").unwrap(), 177 - Url::from_str("http://[::1]/").unwrap(), 180 + Url::from_str("http://127.0.0.1").unwrap(), 181 + Url::from_str("http://[::1]").unwrap(), 178 182 ]), 179 183 grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], 180 184 scopes: scopes.unwrap_or(vec![Scope::Atproto]), ··· 216 220 } else { 217 221 (AuthMethod::None, None, None) 218 222 }; 219 - 223 + let client_id = metadata.client_id.as_str().trim_end_matches("/"); 224 + let client_uri = metadata 225 + .client_uri 226 + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()); 227 + let redirect_uris = metadata 228 + .redirect_uris 229 + .iter() 230 + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) 231 + .collect(); 232 + let jwks_uri = jwks_uri.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()); 220 233 Ok(OAuthClientMetadata { 221 - client_id: metadata.client_id.to_cowstr().into_static(), 222 - client_uri: metadata.client_uri.map(|u| u.to_cowstr().into_static()), 223 - redirect_uris: metadata 224 - .redirect_uris 225 - .iter() 226 - .map(|u| u.to_cowstr().into_static()) 227 - .collect(), 234 + client_id: client_id.to_cowstr().into_static(), 235 + client_uri, 236 + redirect_uris, 228 237 token_endpoint_auth_method: Some(auth_method.into()), 229 238 grant_types: if keyset.is_some() { 230 239 Some(metadata.grant_types.into_iter().map(|v| v.into()).collect()) ··· 233 242 }, 234 243 scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())), 235 244 dpop_bound_access_tokens: Some(true), 236 - jwks_uri: jwks_uri.map(|u| u.to_cowstr().into_static()), 245 + jwks_uri, 237 246 jwks, 238 247 token_endpoint_auth_signing_alg: if keyset.is_some() { 239 248 Some(CowStr::new_static("ES256")) ··· 275 284 client_id: CowStr::new_static("http://localhost"), 276 285 client_uri: None, 277 286 redirect_uris: vec![ 278 - CowStr::new_static("http://127.0.0.1/"), 279 - CowStr::new_static("http://[::1]/"), 287 + CowStr::new_static("http://127.0.0.1"), 288 + CowStr::new_static("http://[::1]"), 280 289 ], 281 290 scope: Some(CowStr::new_static("atproto")), 282 291 grant_types: None, ··· 313 322 .expect("failed to convert metadata"), 314 323 OAuthClientMetadata { 315 324 client_id: CowStr::new_static( 316 - "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" 325 + "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" 317 326 ), 318 327 client_uri: None, 319 328 redirect_uris: vec![ ··· 354 363 out, 355 364 OAuthClientMetadata { 356 365 client_id: CowStr::new_static( 357 - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 366 + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1" 358 367 ), 359 368 client_uri: None, 360 369 redirect_uris: vec![CowStr::new_static("http://127.0.0.1")], ··· 385 394 out, 386 395 OAuthClientMetadata { 387 396 client_id: CowStr::new_static( 388 - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F" 397 + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000" 389 398 ), 390 399 client_uri: None, 391 400 redirect_uris: vec![CowStr::new_static("http://127.0.0.1:8000")], ··· 416 425 out, 417 426 OAuthClientMetadata { 418 427 client_id: CowStr::new_static( 419 - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 428 + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1" 420 429 ), 421 430 client_uri: None, 422 - redirect_uris: vec![CowStr::new_static("http://127.0.0.1/")], 431 + redirect_uris: vec![CowStr::new_static("http://127.0.0.1")], 423 432 scope: Some(CowStr::new_static("atproto")), 424 433 grant_types: None, 425 434 token_endpoint_auth_method: Some(AuthMethod::None.into()),
+1 -1
crates/jacquard-oauth/src/client.rs
··· 593 593 594 594 async fn set_base_uri(&self, url: Url) { 595 595 let mut guard = self.data.write().await; 596 - guard.host_url = url.to_cowstr().into_static(); 596 + guard.host_url = url.as_str().trim_end_matches("/").to_cowstr().into_static(); 597 597 } 598 598 599 599 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
+6 -3
crates/jacquard-oauth/src/resolver.rs
··· 5 5 use http::{Request, StatusCode}; 6 6 use jacquard_common::CowStr; 7 7 use jacquard_common::IntoStatic; 8 + use jacquard_common::cowstr::ToCowStr; 8 9 use jacquard_common::types::did_doc::DidDocument; 9 10 use jacquard_common::types::ident::AtIdentifier; 10 11 use jacquard_common::{http_client::HttpClient, types::did::Did}; ··· 423 424 // resolve to a DID) 424 425 Ok(if input.starts_with("https://") { 425 426 let url = Url::parse(input).map_err(|_| ResolverError::not_found())?; 426 - (resolver.resolve_from_service(&url).await?, None) 427 + (resolver.resolve_from_service(&url.to_cowstr()).await?, None) 427 428 } else { 428 429 let (metadata, identity) = resolver.resolve_from_identity(input).await?; 429 430 (metadata, Some(identity)) ··· 491 492 .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; 492 493 let identity = resolver.resolve_ident_owned(&actor).await?; 493 494 if let Some(pds) = &identity.pds_endpoint() { 494 - let metadata = resolver.get_resource_server_metadata(pds).await?; 495 + let metadata = resolver 496 + .get_resource_server_metadata(&pds.to_cowstr()) 497 + .await?; 495 498 Ok((metadata, identity)) 496 499 } else { 497 500 Err(ResolverError::did_document("Did doc lacking pds")) ··· 514 517 issuer: &CowStr<'_>, 515 518 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 516 519 let mut md = resolve_authorization_server(client, issuer).await?; 517 - md.issuer = issuer.into_static(); 520 + md.issuer = issuer.clone().into_static(); 518 521 Ok(md) 519 522 } 520 523
+1 -1
crates/jacquard/Cargo.toml
··· 144 144 bytes.workspace = true 145 145 http.workspace = true 146 146 miette = { workspace = true } 147 - reqwest = { workspace = true, features = ["charset", "json", "gzip"] } 147 + reqwest = { workspace = true, features = ["json", "gzip"] } 148 148 serde.workspace = true 149 149 serde_html_form.workspace = true 150 150 serde_json.workspace = true
+3 -2
crates/jacquard/src/client.rs
··· 254 254 let base_uri = self.base_uri().await; 255 255 let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url"); 256 256 self.resolver 257 - .xrpc(base_uri.clone()) 257 + .xrpc(base_uri) 258 258 .with_options(opts.clone()) 259 259 .send(&request) 260 260 .await ··· 287 287 { 288 288 async move { 289 289 let base_uri = self.base_uri().await; 290 + let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url"); 290 291 self.resolver 291 - .xrpc(base_uri.clone()) 292 + .xrpc(base_uri) 292 293 .with_options(opts.clone()) 293 294 .send(&request) 294 295 .await
+3 -2
crates/jacquard/src/client/credential_session.rs
··· 286 286 } 287 287 // Activate 288 288 *self.key.write().await = Some(key); 289 - *self.endpoint.write().await = Some(pds.to_cowstr().into_static()); 289 + *self.endpoint.write().await = 290 + Some(pds.as_str().trim_end_matches("/").to_cowstr().into_static()); 290 291 291 292 Ok(session) 292 293 } ··· 442 443 443 444 async fn set_base_uri(&self, url: Url) { 444 445 let mut guard = self.endpoint.write().await; 445 - *guard = Some(url.to_cowstr().into_static()); 446 + *guard = Some(url.as_str().trim_end_matches("/").to_cowstr().into_static()); 446 447 } 447 448 448 449 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>>
+2 -5
crates/jacquard/tests/credential_session.rs
··· 169 169 let session = CredentialSession::new(store.clone(), client.clone()); 170 170 171 171 // Before login, default endpoint should be public appview 172 - assert_eq!( 173 - session.endpoint().await.as_str(), 174 - "https://public.bsky.app/" 175 - ); 172 + assert_eq!(session.endpoint().await.as_str(), "https://public.bsky.app"); 176 173 177 174 // Login using handle; resolves to PDS and persists session 178 175 session ··· 187 184 .expect("login ok"); 188 185 189 186 // Endpoint switches to PDS 190 - assert_eq!(session.endpoint().await.as_str(), "https://pds/"); 187 + assert_eq!(session.endpoint().await.as_str(), "https://pds"); 191 188 192 189 // Send a request that will first 401 (ExpiredToken), then refresh, then succeed 193 190 let resp = session
+1 -1
rust-toolchain.toml
··· 1 1 [toolchain] 2 - channel = "stable" 2 + channel = "nightly" 3 3 profile = "default" 4 4 targets = [ "wasm32-unknown-unknown" ]