A better Rust ATProto crate

dealing with url crate issues

Orual 3bf78e2e d853091d

Changed files
+236 -186
crates
+1 -1
crates/jacquard-common/src/xrpc.rs
··· 271 271 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 272 272 pub trait XrpcClient: HttpClient { 273 273 /// Get the base URI for the client. 274 - fn base_uri(&self) -> impl Future<Output = Url>; 274 + fn base_uri(&self) -> impl Future<Output = CowStr<'static>>; 275 275 276 276 /// Set the base URI for the client. 277 277 fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> {
+8 -4
crates/jacquard-common/src/xrpc/subscription.rs
··· 13 13 use std::marker::PhantomData; 14 14 use url::Url; 15 15 16 + use crate::cowstr::ToCowStr; 16 17 use crate::error::DecodeError; 17 18 use crate::stream::StreamError; 18 19 use crate::websocket::{WebSocketClient, WebSocketConnection, WsSink, WsStream}; ··· 792 793 #[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))] 793 794 pub trait SubscriptionClient: WebSocketClient { 794 795 /// Get the base URI for the client. 795 - fn base_uri(&self) -> impl Future<Output = Url>; 796 + fn base_uri(&self) -> impl Future<Output = CowStr<'static>>; 796 797 797 798 /// Get the subscription options for the client. 798 799 fn subscription_opts(&self) -> impl Future<Output = SubscriptionOptions<'_>> { ··· 847 848 /// or when you want to handle auth manually via headers. 848 849 pub struct BasicSubscriptionClient<W: WebSocketClient> { 849 850 client: W, 850 - base_uri: Url, 851 + base_uri: CowStr<'static>, 851 852 opts: SubscriptionOptions<'static>, 852 853 } 853 854 854 855 impl<W: WebSocketClient> BasicSubscriptionClient<W> { 855 856 /// Create a new basic subscription client with the given WebSocket client and base URI. 856 857 pub fn new(client: W, base_uri: Url) -> Self { 858 + let base_uri = base_uri.as_str().trim_end_matches("/"); 857 859 Self { 858 860 client, 859 - base_uri, 861 + base_uri: base_uri.to_cowstr().into_static(), 860 862 opts: SubscriptionOptions::default(), 861 863 } 862 864 } ··· 890 892 } 891 893 892 894 impl<W: WebSocketClient> SubscriptionClient for BasicSubscriptionClient<W> { 893 - async fn base_uri(&self) -> Url { 895 + async fn base_uri(&self) -> CowStr<'static> { 894 896 self.base_uri.clone() 895 897 } 896 898 ··· 934 936 Self: Sync, 935 937 { 936 938 let base = self.base_uri().await; 939 + let base = Url::parse(&base).expect("Failed to parse base URL"); 937 940 self.subscription(base) 938 941 .with_options(opts) 939 942 .subscribe(params) ··· 950 953 Sub: XrpcSubscription + Send + Sync, 951 954 { 952 955 let base = self.base_uri().await; 956 + let base = Url::parse(&base).expect("Failed to parse base URL"); 953 957 self.subscription(base) 954 958 .with_options(opts) 955 959 .subscribe(params)
+50 -43
crates/jacquard-oauth/src/atproto.rs
··· 2 2 3 3 use crate::types::OAuthClientMetadata; 4 4 use crate::{keyset::Keyset, scopes::Scope}; 5 - use jacquard_common::CowStr; 5 + use jacquard_common::cowstr::ToCowStr; 6 + use jacquard_common::{CowStr, IntoStatic}; 6 7 use serde::{Deserialize, Serialize}; 7 8 use smol_str::{SmolStr, ToSmolStr}; 8 9 use thiserror::Error; ··· 217 218 }; 218 219 219 220 Ok(OAuthClientMetadata { 220 - client_id: metadata.client_id, 221 - client_uri: metadata.client_uri, 222 - redirect_uris: metadata.redirect_uris, 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(), 223 228 token_endpoint_auth_method: Some(auth_method.into()), 224 229 grant_types: if keyset.is_some() { 225 230 Some(metadata.grant_types.into_iter().map(|v| v.into()).collect()) ··· 228 233 }, 229 234 scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())), 230 235 dpop_bound_access_tokens: Some(true), 231 - jwks_uri, 236 + jwks_uri: jwks_uri.map(|u| u.to_cowstr().into_static()), 232 237 jwks, 233 238 token_endpoint_auth_signing_alg: if keyset.is_some() { 234 239 Some(CowStr::new_static("ES256")) ··· 236 241 None 237 242 }, 238 243 client_name: metadata.client_name, 239 - logo_uri: metadata.logo_uri, 240 - tos_uri: metadata.tos_uri, 241 - privacy_policy_uri: metadata.privacy_policy_uri, 244 + logo_uri: metadata.logo_uri.map(|u| u.to_cowstr().into_static()), 245 + tos_uri: metadata.tos_uri.map(|u| u.to_cowstr().into_static()), 246 + privacy_policy_uri: metadata 247 + .privacy_policy_uri 248 + .map(|u| u.to_cowstr().into_static()), 242 249 }) 243 250 } 244 251 ··· 265 272 atproto_client_metadata(AtprotoClientMetadata::new_localhost(None, None), &None) 266 273 .unwrap(), 267 274 OAuthClientMetadata { 268 - client_id: Url::from_str("http://localhost").unwrap(), 275 + client_id: CowStr::new_static("http://localhost"), 269 276 client_uri: None, 270 277 redirect_uris: vec![ 271 - Url::from_str("http://127.0.0.1/").unwrap(), 272 - Url::from_str("http://[::1]/").unwrap(), 278 + CowStr::new_static("http://127.0.0.1/"), 279 + CowStr::new_static("http://[::1]/"), 273 280 ], 274 281 scope: Some(CowStr::new_static("atproto")), 275 282 grant_types: None, ··· 289 296 #[test] 290 297 fn test_localhost_client_metadata_custom() { 291 298 assert_eq!( 292 - atproto_client_metadata(AtprotoClientMetadata::new_localhost( 293 - Some(vec![ 294 - Url::from_str("http://127.0.0.1/callback").unwrap(), 295 - Url::from_str("http://[::1]/callback").unwrap(), 296 - ]), 297 - Some( 298 - vec![ 299 + atproto_client_metadata( 300 + AtprotoClientMetadata::new_localhost( 301 + Some(vec![ 302 + Url::parse("http://127.0.0.1/callback").unwrap(), 303 + Url::parse("http://[::1]/callback").unwrap(), 304 + ]), 305 + Some(vec![ 299 306 Scope::Atproto, 300 307 Scope::Transition(TransitionScope::Generic), 301 308 Scope::parse("account:email").unwrap() 302 - ] 303 - ) 304 - ), &None) 309 + ]) 310 + ), 311 + &None 312 + ) 305 313 .expect("failed to convert metadata"), 306 314 OAuthClientMetadata { 307 - client_id: Url::from_str( 315 + client_id: CowStr::new_static( 308 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" 309 - ).unwrap(), 317 + ), 310 318 client_uri: None, 311 319 redirect_uris: vec![ 312 - Url::from_str("http://127.0.0.1/callback").unwrap(), 320 + CowStr::new_static("http://127.0.0.1/callback"), 313 321 // TODO: fix this so that it respects IPv6 314 - Url::from_str("http://127.0.0.1/callback").unwrap(), 322 + CowStr::new_static("http://127.0.0.1/callback"), 315 323 ], 316 - scope: Some(CowStr::new_static("account:email atproto transition:generic")), 324 + scope: Some(CowStr::new_static( 325 + "account:email atproto transition:generic" 326 + )), 317 327 grant_types: None, 318 328 token_endpoint_auth_method: Some(AuthMethod::None.into()), 319 329 dpop_bound_access_tokens: Some(true), ··· 334 344 { 335 345 let out = atproto_client_metadata( 336 346 AtprotoClientMetadata::new_localhost( 337 - Some(vec![Url::from_str("https://127.0.0.1/").unwrap()]), 347 + Some(vec![Url::from_str("https://127.0.0.1").unwrap()]), 338 348 None, 339 349 ), 340 350 &None, ··· 343 353 assert_eq!( 344 354 out, 345 355 OAuthClientMetadata { 346 - client_id: Url::from_str( 356 + client_id: CowStr::new_static( 347 357 "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 348 - ) 349 - .unwrap(), 358 + ), 350 359 client_uri: None, 351 - redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()], 360 + redirect_uris: vec![CowStr::new_static("http://127.0.0.1")], 352 361 scope: Some(CowStr::new_static("atproto")), 353 362 grant_types: None, 354 363 token_endpoint_auth_method: Some(AuthMethod::None.into()), ··· 366 375 { 367 376 let out = atproto_client_metadata( 368 377 AtprotoClientMetadata::new_localhost( 369 - Some(vec![Url::from_str("http://localhost:8000/").unwrap()]), 378 + Some(vec![Url::from_str("http://localhost:8000").unwrap()]), 370 379 None, 371 380 ), 372 381 &None, ··· 375 384 assert_eq!( 376 385 out, 377 386 OAuthClientMetadata { 378 - client_id: Url::from_str( 387 + client_id: CowStr::new_static( 379 388 "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F" 380 - ) 381 - .unwrap(), 389 + ), 382 390 client_uri: None, 383 - redirect_uris: vec![Url::from_str("http://127.0.0.1:8000/").unwrap()], 391 + redirect_uris: vec![CowStr::new_static("http://127.0.0.1:8000")], 384 392 scope: Some(CowStr::new_static("atproto")), 385 393 grant_types: None, 386 394 token_endpoint_auth_method: Some(AuthMethod::None.into()), ··· 407 415 assert_eq!( 408 416 out, 409 417 OAuthClientMetadata { 410 - client_id: Url::from_str( 418 + client_id: CowStr::new_static( 411 419 "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" 412 - ) 413 - .unwrap(), 420 + ), 414 421 client_uri: None, 415 - redirect_uris: vec![Url::from_str("http://127.0.0.1/").unwrap()], 422 + redirect_uris: vec![CowStr::new_static("http://127.0.0.1/")], 416 423 scope: Some(CowStr::new_static("atproto")), 417 424 grant_types: None, 418 425 token_endpoint_auth_method: Some(AuthMethod::None.into()), ··· 465 472 atproto_client_metadata(metadata, &Some(keyset.clone())) 466 473 .expect("failed to convert metadata"), 467 474 OAuthClientMetadata { 468 - client_id: Url::from_str("https://example.com/client_metadata.json").unwrap(), 469 - client_uri: Some(Url::from_str("https://example.com").unwrap()), 470 - redirect_uris: vec![Url::from_str("https://example.com/callback").unwrap()], 475 + client_id: CowStr::new_static("https://example.com/client_metadata.json"), 476 + client_uri: Some(CowStr::new_static("https://example.com")), 477 + redirect_uris: vec![CowStr::new_static("https://example.com/callback")], 471 478 scope: Some(CowStr::new_static("atproto")), 472 479 grant_types: Some(vec![CowStr::new_static("authorization_code")]), 473 480 token_endpoint_auth_method: Some(AuthMethod::PrivateKeyJwt.into()),
+25 -23
crates/jacquard-oauth/src/client.rs
··· 11 11 }; 12 12 use jacquard_common::{ 13 13 AuthorizationToken, CowStr, IntoStatic, 14 + cowstr::ToCowStr, 14 15 error::{AuthError, ClientError, XrpcResult}, 15 16 http_client::HttpClient, 16 17 types::{did::Did, string::Handle}, ··· 29 30 resolver::{DidDocResponse, IdentityError, IdentityResolver, ResolverOptions}, 30 31 }; 31 32 use jose_jwk::JwkSet; 33 + use smol_str::ToSmolStr; 32 34 use std::{future::Future, sync::Arc}; 33 35 use tokio::sync::RwLock; 34 36 use url::Url; ··· 40 42 { 41 43 pub registry: Arc<SessionRegistry<T, S>>, 42 44 pub options: RwLock<CallOptions<'static>>, 43 - pub endpoint: RwLock<Option<Url>>, 45 + pub endpoint: RwLock<Option<CowStr<'static>>>, 44 46 pub client: Arc<T>, 45 47 } 46 48 ··· 192 194 193 195 #[derive(serde::Serialize)] 194 196 struct Parameters<'s> { 195 - client_id: Url, 197 + client_id: CowStr<'s>, 196 198 request_uri: CowStr<'s>, 197 199 } 198 200 Ok(metadata.server_metadata.authorization_endpoint.to_string() 199 201 + "?" 200 202 + &serde_html_form::to_string(Parameters { 201 - client_id: metadata.client_metadata.client_id.clone(), 203 + client_id: metadata.client_metadata.client_id, 202 204 request_uri: auth_req_info.request_uri, 203 205 }) 204 206 .unwrap()) ··· 218 220 219 221 let metadata = self 220 222 .client 221 - .get_authorization_server_metadata(&auth_req_info.authserver_url) 223 + .get_authorization_server_metadata(&auth_req_info.authserver_url.to_cowstr()) 222 224 .await?; 223 225 224 226 if let Some(iss) = params.iss { ··· 262 264 let client_data = ClientSessionData { 263 265 account_did: token_set.sub.clone(), 264 266 session_id: auth_req_info.state, 265 - host_url: Url::parse(&token_set.iss).expect("Failed to parse host URL"), 266 - authserver_url: auth_req_info.authserver_url, 267 + host_url: token_set.iss.clone(), 268 + authserver_url: auth_req_info.authserver_url.to_cowstr(), 267 269 authserver_token_endpoint: auth_req_info.authserver_token_endpoint, 268 270 authserver_revocation_endpoint: auth_req_info.authserver_revocation_endpoint, 269 271 scopes, ··· 347 349 S: ClientAuthStore + Send + Sync + 'static, 348 350 T: OAuthResolver + DpopExt + Send + Sync + 'static, 349 351 { 350 - async fn base_uri(&self) -> Url { 351 - self.endpoint.read().await.clone().unwrap_or( 352 - Url::parse("https://public.api.bsky.app").expect("public appview should be valid url"), 353 - ) 352 + async fn base_uri(&self) -> CowStr<'static> { 353 + self.endpoint 354 + .read() 355 + .await 356 + .clone() 357 + .unwrap_or(CowStr::new_static("https://public.api.bsky.app")) 354 358 } 355 359 356 360 async fn opts(&self) -> CallOptions<'_> { ··· 364 368 365 369 async fn set_base_uri(&self, url: Url) { 366 370 let mut guard = self.endpoint.write().await; 367 - *guard = Some(url); 371 + *guard = Some(url.to_cowstr().into_static()); 368 372 } 369 373 370 374 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> ··· 387 391 { 388 392 let base_uri = self.base_uri().await; 389 393 self.client 390 - .xrpc(base_uri.clone()) 394 + .xrpc(Url::parse(&base_uri).map_err(|e| ClientError::encode(e.to_smolstr()))?) 391 395 .with_options(opts.clone()) 392 396 .send(&request) 393 397 .await ··· 470 474 (data.account_did.clone(), data.session_id.clone()) 471 475 } 472 476 473 - pub async fn endpoint(&self) -> Url { 477 + pub async fn endpoint(&self) -> CowStr<'static> { 474 478 self.data.read().await.host_url.clone() 475 479 } 476 480 ··· 574 578 T: OAuthResolver + DpopExt + XrpcExt + Send + Sync + 'static, 575 579 W: Send + Sync, 576 580 { 577 - async fn base_uri(&self) -> Url { 581 + async fn base_uri(&self) -> CowStr<'static> { 578 582 self.data.read().await.host_url.clone() 579 583 } 580 584 ··· 589 593 590 594 async fn set_base_uri(&self, url: Url) { 591 595 let mut guard = self.data.write().await; 592 - guard.host_url = url; 596 + guard.host_url = url.to_cowstr().into_static(); 593 597 } 594 598 595 599 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> ··· 614 618 opts.auth = Some(self.access_token().await); 615 619 let guard = self.data.read().await; 616 620 let mut dpop = guard.dpop_data.clone(); 621 + let base_uri = Url::parse(&base_uri).map_err(|e| ClientError::transport(e))?; 617 622 let http_response = self 618 623 .client 619 624 .dpop_call(&mut dpop) ··· 718 723 use jacquard_common::StreamError; 719 724 720 725 let base_uri = <Self as XrpcClient>::base_uri(self).await; 726 + let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::protocol(e.to_string()))?; 721 727 let mut opts = self.options.read().await.clone(); 722 728 opts.auth = Some(self.access_token().await); 723 729 let http_request = build_http_request(&base_uri, &request, &opts) ··· 773 779 let mut opts = self.options.read().await.clone(); 774 780 opts.auth = Some(self.access_token().await); 775 781 776 - let mut url = base_uri; 782 + let mut url = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?; 777 783 let mut path = url.path().trim_end_matches('/').to_owned(); 778 784 path.push_str("/xrpc/"); 779 785 path.push_str(<Str::Request as jacquard_common::xrpc::XrpcRequest>::NSID); ··· 919 925 T: OAuthResolver + Send + Sync + 'static, 920 926 W: WebSocketClient + Send + Sync, 921 927 { 922 - async fn base_uri(&self) -> Url { 923 - #[cfg(not(target_arch = "wasm32"))] 924 - if tokio::runtime::Handle::try_current().is_ok() { 925 - return tokio::task::block_in_place(|| self.data.blocking_read().host_url.clone()); 926 - } 927 - 928 - self.data.blocking_read().host_url.clone() 928 + async fn base_uri(&self) -> CowStr<'static> { 929 + self.data.read().await.host_url.clone() 929 930 } 930 931 931 932 async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> { ··· 961 962 { 962 963 use jacquard_common::xrpc::SubscriptionExt; 963 964 let base = self.base_uri().await; 965 + let base = Url::parse(&base).expect("Failed to parse base URL"); 964 966 self.subscription(base) 965 967 .with_options(opts) 966 968 .subscribe(params)
+10 -13
crates/jacquard-oauth/src/request.rs
··· 861 861 use crate::types::{OAuthAuthorizationServerMetadata, OAuthClientMetadata}; 862 862 use bytes::Bytes; 863 863 use http::{Response as HttpResponse, StatusCode}; 864 - use jacquard_common::http_client::HttpClient; 864 + use jacquard_common::{http_client::HttpClient, types::string::Did}; 865 865 use jacquard_identity::resolver::IdentityResolver; 866 866 use std::sync::Arc; 867 867 use tokio::sync::Mutex; ··· 895 895 async fn resolve_handle( 896 896 &self, 897 897 _handle: &jacquard_common::types::string::Handle<'_>, 898 - ) -> std::result::Result< 899 - jacquard_common::types::string::Did<'static>, 900 - jacquard_identity::resolver::IdentityError, 901 - > { 902 - Ok(jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap()) 898 + ) -> std::result::Result<Did<'static>, jacquard_identity::resolver::IdentityError> { 899 + Ok(Did::new_static("did:plc:alice").unwrap()) 903 900 } 904 901 async fn resolve_did_doc( 905 902 &self, 906 - _did: &jacquard_common::types::string::Did<'_>, 903 + _did: &Did<'_>, 907 904 ) -> std::result::Result< 908 905 jacquard_identity::resolver::DidDocResponse, 909 906 jacquard_identity::resolver::IdentityError, ··· 938 935 OAuthMetadata { 939 936 server_metadata: server, 940 937 client_metadata: OAuthClientMetadata { 941 - client_id: url::Url::parse("https://client").unwrap(), 938 + client_id: CowStr::new_static("https://client"), 942 939 client_uri: None, 943 - redirect_uris: vec![url::Url::parse("https://client/cb").unwrap()], 940 + redirect_uris: vec![CowStr::new_static("https://client/cb")], 944 941 scope: Some(CowStr::from("atproto")), 945 942 grant_types: None, 946 943 token_endpoint_auth_method: Some(CowStr::from("none")), ··· 976 973 let client = MockClient::default(); 977 974 let meta = base_metadata(); 978 975 let session = ClientSessionData { 979 - account_did: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(), 976 + account_did: Did::new_static("did:plc:alice").unwrap(), 980 977 session_id: CowStr::from("state"), 981 - host_url: url::Url::parse("https://pds").unwrap(), 982 - authserver_url: url::Url::parse("https://issuer").unwrap(), 978 + host_url: CowStr::new_static("https://pds"), 979 + authserver_url: CowStr::new_static("https://issuer"), 983 980 authserver_token_endpoint: CowStr::from("https://issuer/token"), 984 981 authserver_revocation_endpoint: None, 985 982 scopes: vec![], ··· 990 987 }, 991 988 token_set: crate::types::TokenSet { 992 989 iss: CowStr::from("https://issuer"), 993 - sub: jacquard_common::types::string::Did::new_static("did:plc:alice").unwrap(), 990 + sub: Did::new_static("did:plc:alice").unwrap(), 994 991 aud: CowStr::from("https://pds"), 995 992 scope: None, 996 993 refresh_token: None,
+35 -29
crates/jacquard-oauth/src/resolver.rs
··· 400 400 // when the user forgot their handle, or when the handle does not 401 401 // resolve to a DID) 402 402 Ok(if input.starts_with("https://") { 403 + use jacquard_common::cowstr::ToCowStr; 404 + 403 405 let url = Url::parse(input).map_err(|_| ResolverError::not_found())?; 404 - (resolver.resolve_from_service(&url).await?, None) 406 + (resolver.resolve_from_service(&url.to_cowstr()).await?, None) 405 407 } else { 406 408 let (metadata, identity) = resolver.resolve_from_identity(input).await?; 407 409 (metadata, Some(identity)) ··· 431 433 #[cfg(not(target_arch = "wasm32"))] 432 434 async fn resolve_from_service_impl<T: OAuthResolver + Sync + ?Sized>( 433 435 resolver: &T, 434 - input: &Url, 436 + input: &CowStr<'_>, 435 437 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 436 438 // Assume first that input is a PDS URL (as required by ATPROTO) 437 439 if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { ··· 444 446 #[cfg(target_arch = "wasm32")] 445 447 async fn resolve_from_service_impl<T: OAuthResolver + ?Sized>( 446 448 resolver: &T, 447 - input: &Url, 449 + input: &CowStr<'_>, 448 450 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 449 451 // Assume first that input is a PDS URL (as required by ATPROTO) 450 452 if let Ok(metadata) = resolver.get_resource_server_metadata(input).await { ··· 466 468 .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; 467 469 let identity = resolver.resolve_ident_owned(&actor).await?; 468 470 if let Some(pds) = &identity.pds_endpoint() { 469 - let metadata = resolver.get_resource_server_metadata(pds).await?; 471 + use jacquard_common::cowstr::ToCowStr; 472 + 473 + let metadata = resolver 474 + .get_resource_server_metadata(&pds.to_cowstr()) 475 + .await?; 470 476 Ok((metadata, identity)) 471 477 } else { 472 478 Err(ResolverError::did_document("Did doc lacking pds")) ··· 495 501 #[cfg(not(target_arch = "wasm32"))] 496 502 async fn get_authorization_server_metadata_impl<T: HttpClient + Sync + ?Sized>( 497 503 client: &T, 498 - issuer: &Url, 504 + issuer: &CowStr<'_>, 499 505 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 500 506 let mut md = resolve_authorization_server(client, issuer).await?; 501 - // Normalize issuer string to the input URL representation to avoid slash quirks 502 - md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static(); 507 + md.issuer = issuer.clone().into_static(); 503 508 Ok(md) 504 509 } 505 510 506 511 #[cfg(target_arch = "wasm32")] 507 512 async fn get_authorization_server_metadata_impl<T: HttpClient + ?Sized>( 508 513 client: &T, 509 - issuer: &Url, 514 + issuer: &CowStr<'_>, 510 515 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 511 516 let mut md = resolve_authorization_server(client, issuer).await?; 512 - // Normalize issuer string to the input URL representation to avoid slash quirks 513 - md.issuer = jacquard_common::CowStr::from(issuer.as_str()).into_static(); 517 + md.issuer = issuer.into_static(); 514 518 Ok(md) 515 519 } 516 520 517 521 #[cfg(not(target_arch = "wasm32"))] 518 522 async fn get_resource_server_metadata_impl<T: OAuthResolver + Sync + ?Sized>( 519 523 resolver: &T, 520 - pds: &Url, 524 + pds: &CowStr<'_>, 521 525 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 522 526 let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; 523 527 // ATPROTO requires one, and only one, authorization server entry ··· 575 579 #[cfg(target_arch = "wasm32")] 576 580 async fn get_resource_server_metadata_impl<T: OAuthResolver + ?Sized>( 577 581 resolver: &T, 578 - pds: &Url, 582 + pds: &CowStr<'_>, 579 583 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 580 584 let rs_metadata = resolve_protected_resource_info(resolver, pds).await?; 581 585 // ATPROTO requires one, and only one, authorization server entry ··· 685 689 #[cfg(not(target_arch = "wasm32"))] 686 690 fn resolve_from_service( 687 691 &self, 688 - input: &Url, 692 + input: &CowStr<'_>, 689 693 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 690 694 where 691 695 Self: Sync, ··· 696 700 #[cfg(target_arch = "wasm32")] 697 701 fn resolve_from_service( 698 702 &self, 699 - input: &Url, 703 + input: &CowStr<'_>, 700 704 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 701 705 resolve_from_service_impl(self, input) 702 706 } ··· 733 737 #[cfg(not(target_arch = "wasm32"))] 734 738 fn get_authorization_server_metadata( 735 739 &self, 736 - issuer: &Url, 740 + issuer: &CowStr<'_>, 737 741 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 738 742 where 739 743 Self: Sync, ··· 744 748 #[cfg(target_arch = "wasm32")] 745 749 fn get_authorization_server_metadata( 746 750 &self, 747 - issuer: &Url, 751 + issuer: &CowStr<'_>, 748 752 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 749 753 get_authorization_server_metadata_impl(self, issuer) 750 754 } ··· 752 756 #[cfg(not(target_arch = "wasm32"))] 753 757 fn get_resource_server_metadata( 754 758 &self, 755 - pds: &Url, 759 + pds: &CowStr<'_>, 756 760 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> + Send 757 761 where 758 762 Self: Sync, ··· 763 767 #[cfg(target_arch = "wasm32")] 764 768 fn get_resource_server_metadata( 765 769 &self, 766 - pds: &Url, 770 + pds: &CowStr<'_>, 767 771 ) -> impl Future<Output = Result<OAuthAuthorizationServerMetadata<'static>>> { 768 772 get_resource_server_metadata_impl(self, pds) 769 773 } ··· 771 775 772 776 pub async fn resolve_authorization_server<T: HttpClient + ?Sized>( 773 777 client: &T, 774 - server: &Url, 778 + server: &CowStr<'_>, 775 779 ) -> Result<OAuthAuthorizationServerMetadata<'static>> { 776 - let url = server 777 - .join("/.well-known/oauth-authorization-server") 778 - .map_err(|e| ResolverError::transport(e))?; 780 + let url = format!( 781 + "{}/.well-known/oauth-authorization-server", 782 + server.trim_end_matches("/") 783 + ); 779 784 780 785 let req = Request::builder() 781 - .uri(url.to_string()) 786 + .uri(url) 782 787 .body(Vec::new()) 783 788 .map_err(|e| ResolverError::transport(e))?; 784 789 let res = client ··· 804 809 805 810 pub async fn resolve_protected_resource_info<T: HttpClient + ?Sized>( 806 811 client: &T, 807 - server: &Url, 812 + server: &CowStr<'_>, 808 813 ) -> Result<OAuthProtectedResourceMetadata<'static>> { 809 - let url = server 810 - .join("/.well-known/oauth-protected-resource") 811 - .map_err(|e| ResolverError::transport(e))?; 814 + let url = format!( 815 + "{}/.well-known/oauth-protected-resource", 816 + server.trim_end_matches("/") 817 + ); 812 818 813 819 let req = Request::builder() 814 820 .uri(url.to_string()) ··· 873 879 .body(Vec::new()) 874 880 .unwrap(), 875 881 ); 876 - let issuer = url::Url::parse("https://issuer").unwrap(); 882 + let issuer = CowStr::new_static("https://issuer"); 877 883 let err = super::resolve_authorization_server(&client, &issuer) 878 884 .await 879 885 .unwrap_err(); ··· 892 898 .body(b"{not json}".to_vec()) 893 899 .unwrap(), 894 900 ); 895 - let issuer = url::Url::parse("https://issuer").unwrap(); 901 + let issuer = CowStr::new_static("https://issuer"); 896 902 let err = super::resolve_authorization_server(&client, &issuer) 897 903 .await 898 904 .unwrap_err();
+4 -4
crates/jacquard-oauth/src/session.rs
··· 43 43 pub session_id: CowStr<'s>, 44 44 45 45 // Base URL of the "resource server" (eg, PDS). Should include scheme, hostname, port; no path or auth info. 46 - pub host_url: Url, 46 + pub host_url: CowStr<'s>, 47 47 48 48 // Base URL of the "auth server" (eg, PDS or entryway). Should include scheme, hostname, port; no path or auth info. 49 - pub authserver_url: Url, 49 + pub authserver_url: CowStr<'s>, 50 50 51 51 // Full token endpoint 52 52 pub authserver_token_endpoint: CowStr<'s>, ··· 70 70 71 71 fn into_static(self) -> Self::Output { 72 72 ClientSessionData { 73 - authserver_url: self.authserver_url, 73 + authserver_url: self.authserver_url.into_static(), 74 74 authserver_token_endpoint: self.authserver_token_endpoint.into_static(), 75 75 authserver_revocation_endpoint: self 76 76 .authserver_revocation_endpoint ··· 80 80 token_set: self.token_set.into_static(), 81 81 account_did: self.account_did.into_static(), 82 82 session_id: self.session_id.into_static(), 83 - host_url: self.host_url, 83 + host_url: self.host_url.into_static(), 84 84 } 85 85 } 86 86 }
+14 -14
crates/jacquard-oauth/src/types/client_metadata.rs
··· 6 6 7 7 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 8 8 pub struct OAuthClientMetadata<'c> { 9 - pub client_id: Url, 9 + pub client_id: CowStr<'c>, 10 10 #[serde(skip_serializing_if = "Option::is_none")] 11 - pub client_uri: Option<Url>, 12 - pub redirect_uris: Vec<Url>, 11 + pub client_uri: Option<CowStr<'c>>, 12 + pub redirect_uris: Vec<CowStr<'c>>, 13 13 #[serde(skip_serializing_if = "Option::is_none")] 14 14 #[serde(borrow)] 15 15 pub scope: Option<CowStr<'c>>, ··· 22 22 pub dpop_bound_access_tokens: Option<bool>, 23 23 // https://datatracker.ietf.org/doc/html/rfc7591#section-2 24 24 #[serde(skip_serializing_if = "Option::is_none")] 25 - pub jwks_uri: Option<Url>, 25 + pub jwks_uri: Option<CowStr<'c>>, 26 26 #[serde(skip_serializing_if = "Option::is_none")] 27 27 pub jwks: Option<JwkSet>, 28 28 // https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata ··· 31 31 #[serde(skip_serializing_if = "Option::is_none")] 32 32 pub client_name: Option<SmolStr>, 33 33 #[serde(skip_serializing_if = "Option::is_none")] 34 - pub logo_uri: Option<Url>, 34 + pub logo_uri: Option<CowStr<'c>>, 35 35 #[serde(skip_serializing_if = "Option::is_none")] 36 - pub tos_uri: Option<Url>, 36 + pub tos_uri: Option<CowStr<'c>>, 37 37 #[serde(skip_serializing_if = "Option::is_none")] 38 - pub privacy_policy_uri: Option<Url>, 38 + pub privacy_policy_uri: Option<CowStr<'c>>, 39 39 } 40 40 41 41 impl OAuthClientMetadata<'_> {} ··· 45 45 46 46 fn into_static(self) -> Self::Output { 47 47 OAuthClientMetadata { 48 - client_id: self.client_id, 49 - client_uri: self.client_uri, 50 - redirect_uris: self.redirect_uris, 48 + client_id: self.client_id.into_static(), 49 + client_uri: self.client_uri.into_static(), 50 + redirect_uris: self.redirect_uris.into_static(), 51 51 scope: self.scope.map(|scope| scope.into_static()), 52 52 grant_types: self.grant_types.map(|types| types.into_static()), 53 53 token_endpoint_auth_method: self 54 54 .token_endpoint_auth_method 55 55 .map(|method| method.into_static()), 56 56 dpop_bound_access_tokens: self.dpop_bound_access_tokens, 57 - jwks_uri: self.jwks_uri, 57 + jwks_uri: self.jwks_uri.into_static(), 58 58 jwks: self.jwks, 59 59 token_endpoint_auth_signing_alg: self 60 60 .token_endpoint_auth_signing_alg 61 61 .map(|alg| alg.into_static()), 62 62 client_name: self.client_name, 63 - logo_uri: self.logo_uri, 64 - tos_uri: self.tos_uri, 65 - privacy_policy_uri: self.privacy_policy_uri, 63 + logo_uri: self.logo_uri.into_static(), 64 + tos_uri: self.tos_uri.into_static(), 65 + privacy_policy_uri: self.privacy_policy_uri.into_static(), 66 66 } 67 67 } 68 68 }
+2 -2
crates/jacquard-oauth/src/types/metadata.rs
··· 56 56 pub struct OAuthProtectedResourceMetadata<'s> { 57 57 #[serde(borrow)] 58 58 pub resource: CowStr<'s>, 59 - pub authorization_servers: Option<Vec<Url>>, 59 + pub authorization_servers: Option<Vec<CowStr<'s>>>, 60 60 pub jwks_uri: Option<CowStr<'s>>, 61 61 pub scopes_supported: Vec<CowStr<'s>>, 62 62 pub bearer_methods_supported: Option<Vec<CowStr<'s>>>, ··· 71 71 fn into_static(self) -> Self::Output { 72 72 OAuthProtectedResourceMetadata { 73 73 resource: self.resource.into_static(), 74 - authorization_servers: self.authorization_servers, 74 + authorization_servers: self.authorization_servers.into_static(), 75 75 jwks_uri: self.jwks_uri.map(|v| v.into_static()), 76 76 scopes_supported: self.scopes_supported.into_static(), 77 77 bearer_methods_supported: self.bearer_methods_supported.map(|v| v.into_static()),
+18 -14
crates/jacquard/src/client.rs
··· 40 40 }, 41 41 server::{create_session::CreateSessionOutput, refresh_session::RefreshSessionOutput}, 42 42 }; 43 + use jacquard_common::cowstr::ToCowStr; 43 44 use jacquard_common::error::XrpcResult; 44 45 pub use jacquard_common::error::{ClientError, XrpcResult as ClientResult}; 45 46 use jacquard_common::http_client::HttpClient; ··· 96 97 fn session_info(&self) 97 98 -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>>; 98 99 /// Current base endpoint. 99 - fn endpoint(&self) -> impl Future<Output = url::Url>; 100 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>>; 100 101 /// Override per-session call options. 101 102 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()>; 102 103 /// Refresh the session and return a fresh AuthorizationToken. ··· 154 155 } 155 156 pub struct UnauthenticatedSession<T> { 156 157 resolver: Arc<T>, 157 - endpoint: Arc<RwLock<Option<Url>>>, 158 + endpoint: Arc<RwLock<Option<CowStr<'static>>>>, 158 159 options: Arc<RwLock<CallOptions<'static>>>, 159 160 } 160 161 ··· 213 214 T: Sync + Send, 214 215 { 215 216 #[doc = " Get the base URI for the client."] 216 - fn base_uri(&self) -> impl Future<Output = Url> + Send { 217 + fn base_uri(&self) -> impl Future<Output = CowStr<'static>> + Send { 217 218 async move { 218 - self.endpoint.read().await.clone().unwrap_or( 219 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 220 - ) 219 + self.endpoint 220 + .read() 221 + .await 222 + .clone() 223 + .unwrap_or(CowStr::new_static("https://public.bsky.app")) 221 224 } 222 225 } 223 226 ··· 249 252 { 250 253 async move { 251 254 let base_uri = self.base_uri().await; 255 + let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url"); 252 256 self.resolver 253 257 .xrpc(base_uri.clone()) 254 258 .with_options(opts.clone()) ··· 295 299 fn set_base_uri(&self, url: Url) -> impl Future<Output = ()> + Send { 296 300 async move { 297 301 let mut guard = self.endpoint.write().await; 298 - *guard = Some(url); 302 + *guard = Some(url.to_cowstr().into_static()); 299 303 } 300 304 } 301 305 ··· 326 330 async { None } // no session 327 331 } 328 332 329 - fn endpoint(&self) -> impl Future<Output = Url> { 333 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>> { 330 334 async { self.base_uri().await } 331 335 } 332 336 ··· 537 541 } 538 542 539 543 /// Get current endpoint. 540 - pub async fn endpoint(&self) -> url::Url { 544 + pub async fn endpoint(&self) -> CowStr<'static> { 541 545 self.inner.endpoint().await 542 546 } 543 547 ··· 1199 1203 .map(|(did, sid)| (did, Some(sid))) 1200 1204 } 1201 1205 } 1202 - fn endpoint(&self) -> impl Future<Output = url::Url> { 1206 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>> { 1203 1207 async move { CredentialSession::<S, T, W>::endpoint(self).await } 1204 1208 } 1205 1209 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { ··· 1231 1235 Some((did.into_static(), Some(sid.into_static()))) 1232 1236 } 1233 1237 } 1234 - fn endpoint(&self) -> impl Future<Output = url::Url> { 1238 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>> { 1235 1239 async { self.endpoint().await } 1236 1240 } 1237 1241 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { ··· 1260 1264 ) -> impl Future<Output = Option<(Did<'static>, Option<CowStr<'static>>)>> { 1261 1265 async { None } 1262 1266 } 1263 - fn endpoint(&self) -> impl Future<Output = url::Url> { 1267 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>> { 1264 1268 async { self.base_uri().await } 1265 1269 } 1266 1270 fn set_options<'a>(&'a self, opts: CallOptions<'a>) -> impl Future<Output = ()> { ··· 1368 1372 } 1369 1373 1370 1374 impl<A: AgentSession> XrpcClient for Agent<A> { 1371 - async fn base_uri(&self) -> url::Url { 1375 + async fn base_uri(&self) -> CowStr<'static> { 1372 1376 self.inner.base_uri().await 1373 1377 } 1374 1378 fn opts(&self) -> impl Future<Output = CallOptions<'_>> { ··· 1513 1517 async { self.info().await } 1514 1518 } 1515 1519 1516 - fn endpoint(&self) -> impl Future<Output = url::Url> { 1520 + fn endpoint(&self) -> impl Future<Output = CowStr<'static>> { 1517 1521 async { self.endpoint().await } 1518 1522 } 1519 1523
+50 -20
crates/jacquard/src/client/credential_session.rs
··· 5 5 }; 6 6 use jacquard_common::{ 7 7 AuthorizationToken, CowStr, IntoStatic, 8 + cowstr::ToCowStr, 8 9 error::{AuthError, ClientError, XrpcResult}, 9 10 http_client::HttpClient, 10 11 session::SessionStore, ··· 13 14 CallOptions, Response, XrpcClient, XrpcError, XrpcExt, XrpcRequest, XrpcResp, XrpcResponse, 14 15 }, 15 16 }; 17 + use smol_str::ToSmolStr; 16 18 use tokio::sync::RwLock; 17 19 use url::Url; 18 20 ··· 48 50 /// Active session key, if any. 49 51 pub key: RwLock<Option<SessionKey>>, 50 52 /// Current base endpoint (PDS); defaults to public appview when unset. 51 - pub endpoint: RwLock<Option<Url>>, 53 + pub endpoint: RwLock<Option<CowStr<'static>>>, 52 54 } 53 55 54 56 impl<S, T> CredentialSession<S, T, ()> ··· 112 114 } 113 115 114 116 /// Current base endpoint. Defaults to the public appview when unset. 115 - pub async fn endpoint(&self) -> Url { 116 - self.endpoint.read().await.clone().unwrap_or( 117 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 118 - ) 117 + pub async fn endpoint(&self) -> CowStr<'static> { 118 + self.endpoint 119 + .read() 120 + .await 121 + .clone() 122 + .unwrap_or(CowStr::new_static("https://public.bsky.app")) 119 123 } 120 124 121 125 /// Override the current base endpoint. 122 126 pub async fn set_endpoint(&self, endpoint: Url) { 123 - *self.endpoint.write().await = Some(endpoint); 127 + *self.endpoint.write().await = Some(endpoint.to_cowstr().into_static()); 124 128 } 125 129 126 130 /// Current access token (Bearer), if logged in. ··· 153 157 .ok_or_else(|| ClientError::auth(AuthError::NotAuthenticated))?; 154 158 let session = self.store.get(&key).await; 155 159 let endpoint = self.endpoint().await; 160 + let endpoint = Url::parse(endpoint.as_str()).map_err(|_| { 161 + ClientError::auth(AuthError::NotAuthenticated) 162 + .with_help("ensure endpoint is valid") 163 + .with_url("com.atproto.server.refreshSession") 164 + })?; 156 165 let mut opts = self.options.read().await.clone(); 157 166 opts.auth = session.map(|s| AuthorizationToken::Bearer(s.refresh_jwt)); 158 167 let response = self ··· 277 286 } 278 287 // Activate 279 288 *self.key.write().await = Some(key); 280 - *self.endpoint.write().await = Some(pds); 289 + *self.endpoint.write().await = Some(pds.to_cowstr().into_static()); 281 290 282 291 Ok(session) 283 292 } ··· 318 327 319 328 // Activate 320 329 *self.key.write().await = Some(key.clone()); 321 - *self.endpoint.write().await = Some(pds); 330 + *self.endpoint.write().await = Some(pds.to_cowstr().into_static()); 322 331 // ensure store has the session (no-op if it existed) 323 332 self.store 324 333 .set((sess.did.clone(), session_id.into_static()), sess) ··· 326 335 if let Some(file_store) = 327 336 (&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>() 328 337 { 329 - let _ = file_store.set_atp_pds(&key, &self.endpoint().await); 338 + let _ = file_store.set_atp_pds( 339 + &key, 340 + &Url::parse(&self.endpoint().await).map_err(|e| { 341 + ClientError::invalid_request("invalid PDS endpoint") 342 + .with_help(format!("Failed to parse PDS endpoint: {}", e)) 343 + })?, 344 + ); 330 345 } 331 346 Ok(()) 332 347 } ··· 360 375 })? 361 376 }); 362 377 *self.key.write().await = Some(key.clone()); 363 - *self.endpoint.write().await = Some(pds); 378 + *self.endpoint.write().await = Some(pds.to_cowstr().into_static()); 364 379 if let Some(file_store) = 365 380 (&*self.store as &dyn Any).downcast_ref::<crate::client::token::FileAuthStore>() 366 381 { 367 - let _ = file_store.set_atp_pds(&key, &self.endpoint().await); 382 + let _ = file_store.set_atp_pds( 383 + &key, 384 + &Url::parse(&self.endpoint().await).map_err(|e| { 385 + ClientError::invalid_request("invalid PDS endpoint") 386 + .with_help(format!("Failed to parse PDS endpoint: {}", e)) 387 + })?, 388 + ); 368 389 } 369 390 Ok(()) 370 391 } ··· 402 423 T: HttpClient + XrpcExt + Send + Sync + 'static, 403 424 W: Send + Sync, 404 425 { 405 - async fn base_uri(&self) -> Url { 406 - self.endpoint.read().await.clone().unwrap_or( 407 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 408 - ) 426 + async fn base_uri(&self) -> CowStr<'static> { 427 + self.endpoint 428 + .read() 429 + .await 430 + .clone() 431 + .unwrap_or(CowStr::new_static("https://public.bsky.app")) 409 432 } 410 433 411 434 async fn opts(&self) -> CallOptions<'_> { ··· 419 442 420 443 async fn set_base_uri(&self, url: Url) { 421 444 let mut guard = self.endpoint.write().await; 422 - *guard = Some(url); 445 + *guard = Some(url.to_cowstr().into_static()); 423 446 } 424 447 425 448 async fn send<R>(&self, request: R) -> XrpcResult<XrpcResponse<R>> ··· 441 464 <R as XrpcRequest>::Response: Send + Sync, 442 465 { 443 466 let base_uri = self.base_uri().await; 467 + let base_uri = 468 + Url::parse(&base_uri).map_err(|e| ClientError::invalid_request(e.to_smolstr()))?; 444 469 let auth = self.access_token().await; 445 470 opts.auth = auth; 446 471 let resp = self ··· 546 571 use jacquard_common::{StreamError, xrpc::build_http_request}; 547 572 548 573 let base_uri = <Self as XrpcClient>::base_uri(self).await; 574 + let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?; 549 575 let mut opts = self.options.read().await.clone(); 550 576 opts.auth = self.access_token().await; 551 577 ··· 599 625 use n0_future::TryStreamExt; 600 626 601 627 let base_uri = self.base_uri().await; 628 + let base_uri = Url::parse(&base_uri).map_err(|e| StreamError::encode(e))?; 602 629 let mut opts = self.options.read().await.clone(); 603 630 opts.auth = self.access_token().await; 604 631 ··· 782 809 T: Send + Sync + 'static, 783 810 W: WebSocketClient + Send + Sync, 784 811 { 785 - async fn base_uri(&self) -> Url { 786 - self.endpoint.read().await.clone().unwrap_or( 787 - Url::parse("https://public.bsky.app").expect("public appview should be valid url"), 788 - ) 812 + async fn base_uri(&self) -> CowStr<'static> { 813 + self.endpoint 814 + .read() 815 + .await 816 + .clone() 817 + .unwrap_or(CowStr::new_static("https://public.bsky.app")) 789 818 } 790 819 791 820 async fn subscription_opts(&self) -> jacquard_common::xrpc::SubscriptionOptions<'_> { ··· 822 851 { 823 852 use jacquard_common::xrpc::SubscriptionExt; 824 853 let base = self.base_uri().await; 854 + let base = Url::parse(&base).expect("base uri should be valid url"); 825 855 self.subscription(base) 826 856 .with_options(opts) 827 857 .subscribe(params)
+6 -6
crates/jacquard/src/client/token.rs
··· 48 48 session_id: String, 49 49 50 50 /// Base URL of the resource server (PDS) 51 - host_url: Url, 51 + host_url: String, 52 52 53 53 /// Base URL of the authorization server (PDS or entryway) 54 - authserver_url: Url, 54 + authserver_url: String, 55 55 56 56 /// Full token endpoint URL 57 57 authserver_token_endpoint: String, ··· 95 95 OAuthSession { 96 96 account_did: data.account_did.to_string(), 97 97 session_id: data.session_id.to_string(), 98 - host_url: data.host_url, 99 - authserver_url: data.authserver_url, 98 + host_url: data.host_url.to_string(), 99 + authserver_url: data.authserver_url.to_string(), 100 100 authserver_token_endpoint: data.authserver_token_endpoint.to_string(), 101 101 authserver_revocation_endpoint: data 102 102 .authserver_revocation_endpoint ··· 122 122 ClientSessionData { 123 123 account_did: session.account_did.into(), 124 124 session_id: session.session_id.to_cowstr(), 125 - host_url: session.host_url, 126 - authserver_url: session.authserver_url, 125 + host_url: session.host_url.to_cowstr(), 126 + authserver_url: session.authserver_url.to_cowstr(), 127 127 authserver_token_endpoint: session.authserver_token_endpoint.to_cowstr(), 128 128 authserver_revocation_endpoint: session 129 129 .authserver_revocation_endpoint
+9 -9
crates/jacquard/tests/oauth_auto_refresh.rs
··· 3 3 4 4 use bytes::Bytes; 5 5 use http::{HeaderValue, Method, Response as HttpResponse, StatusCode}; 6 - use jacquard::IntoStatic; 7 6 use jacquard::client::Agent; 8 7 use jacquard::types::did::Did; 9 8 use jacquard::xrpc::XrpcClient; 9 + use jacquard::{CowStr, IntoStatic}; 10 10 use jacquard_common::http_client::HttpClient; 11 11 use jacquard_oauth::atproto::AtprotoClientMetadata; 12 12 use jacquard_oauth::client::OAuthSession; ··· 84 84 impl OAuthResolver for MockClient { 85 85 async fn get_authorization_server_metadata( 86 86 &self, 87 - issuer: &url::Url, 87 + issuer: &CowStr<'_>, 88 88 ) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError> 89 89 { 90 90 // Return minimal metadata with supported auth method "none" and DPoP support ··· 103 103 104 104 async fn get_resource_server_metadata( 105 105 &self, 106 - _pds: &url::Url, 106 + _pds: &CowStr<'_>, 107 107 ) -> Result<OAuthAuthorizationServerMetadata<'static>, jacquard_oauth::resolver::ResolverError> 108 108 { 109 109 // Return metadata pointing to the same issuer as above ··· 217 217 let session_data = ClientSessionData { 218 218 account_did: Did::new_static("did:plc:alice").unwrap(), 219 219 session_id: jacquard::CowStr::from("state"), 220 - host_url: url::Url::parse("https://pds").unwrap(), 221 - authserver_url: url::Url::parse("https://issuer").unwrap(), 220 + host_url: CowStr::new_static("https://pds"), 221 + authserver_url: CowStr::new_static("https://issuer"), 222 222 authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 223 223 authserver_revocation_endpoint: None, 224 224 scopes: vec![Scope::Atproto], ··· 246 246 let data_store = ClientSessionData { 247 247 account_did: Did::new_static("did:plc:alice").unwrap(), 248 248 session_id: jacquard::CowStr::from("state"), 249 - host_url: url::Url::parse("https://pds").unwrap(), 250 - authserver_url: url::Url::parse("https://issuer").unwrap(), 249 + host_url: CowStr::new_static("https://pds"), 250 + authserver_url: CowStr::new_static("https://issuer"), 251 251 authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 252 252 authserver_revocation_endpoint: None, 253 253 scopes: vec![Scope::Atproto], ··· 348 348 let session_data = ClientSessionData { 349 349 account_did: Did::new_static("did:plc:alice").unwrap(), 350 350 session_id: jacquard::CowStr::from("state"), 351 - host_url: url::Url::parse("https://pds").unwrap(), 352 - authserver_url: url::Url::parse("https://issuer").unwrap(), 351 + host_url: CowStr::new_static("https://pds"), 352 + authserver_url: CowStr::new_static("https://issuer"), 353 353 authserver_token_endpoint: jacquard::CowStr::from("https://issuer/token"), 354 354 authserver_revocation_endpoint: None, 355 355 scopes: vec![Scope::Atproto],
+4 -4
crates/jacquard/tests/oauth_flow.rs
··· 3 3 4 4 use bytes::Bytes; 5 5 use http::{Response as HttpResponse, StatusCode}; 6 - use jacquard::IntoStatic; 7 6 use jacquard::client::Agent; 8 7 use jacquard::xrpc::XrpcClient; 8 + use jacquard::{CowStr, IntoStatic}; 9 9 use jacquard_common::http_client::HttpClient; 10 10 use jacquard_oauth::atproto::AtprotoClientMetadata; 11 11 use jacquard_oauth::authstore::ClientAuthStore; ··· 115 115 } 116 116 async fn get_authorization_server_metadata( 117 117 &self, 118 - issuer: &url::Url, 118 + issuer: &CowStr<'_>, 119 119 ) -> Result< 120 120 jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>, 121 121 jacquard_oauth::resolver::ResolverError, ··· 134 134 135 135 async fn get_resource_server_metadata( 136 136 &self, 137 - _pds: &url::Url, 137 + _pds: &CowStr<'_>, 138 138 ) -> Result< 139 139 jacquard_oauth::types::OAuthAuthorizationServerMetadata<'static>, 140 140 jacquard_oauth::resolver::ResolverError, ··· 243 243 // Construct authorization URL as OAuthClient::start_auth would do 244 244 #[derive(serde::Serialize)] 245 245 struct Parameters<'s> { 246 - client_id: url::Url, 246 + client_id: CowStr<'s>, 247 247 request_uri: jacquard::CowStr<'s>, 248 248 } 249 249 let auth_url = format!(