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