diff --git a/Cargo.lock b/Cargo.lock index bdb391a0..3d4bb66a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2506,7 +2506,6 @@ dependencies = [ "serde_html_form", "serde_json", "sha2", - "signature", "smol_str", "thiserror 2.0.17", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 89262a97..9972c9eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,6 @@ cid = { version = "0.11.1", features = ["serde", "std"] } ipld-core = { version = "0.4.2", features = ["serde"] } multihash = "0.19" dashmap = "6.1" -moka = "0.12" mini-moka = "0.10" # Proc macros diff --git a/crates/jacquard-common/Cargo.toml b/crates/jacquard-common/Cargo.toml index 13bf85c7..1e413b80 100644 --- a/crates/jacquard-common/Cargo.toml +++ b/crates/jacquard-common/Cargo.toml @@ -93,4 +93,4 @@ tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread"] } futures-lite = "2.6" [package.metadata.docs.rs] -features = [ "crypto-k256", "crypto-k256", "crypto-p256", "websocket", "zstd", "service-auth", "reqwest-client", "crypto"] +features = [ "crypto-k256", "crypto-ed22519", "crypto-p256", "websocket", "zstd", "service-auth", "reqwest-client", "crypto"] diff --git a/crates/jacquard-oauth/Cargo.toml b/crates/jacquard-oauth/Cargo.toml index 55ca58e8..4de46c3e 100644 --- a/crates/jacquard-oauth/Cargo.toml +++ b/crates/jacquard-oauth/Cargo.toml @@ -33,7 +33,6 @@ thiserror = { workspace = true } serde_html_form = { workspace = true } miette = { workspace = true } p256 = { workspace = true, features = ["ecdsa"] } -signature = "2" jose-jwa = "0.1" jose-jwk = { workspace = true, features = ["p256"] } chrono.workspace = true diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index 81f16eab..98a23efc 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -152,18 +152,22 @@ impl<'m> AtprotoClientMetadata<'m> { #[derive(serde::Serialize)] struct Parameters<'a> { #[serde(skip_serializing_if = "Option::is_none")] - redirect_uri: Option>, + redirect_uri: Option>>, #[serde(skip_serializing_if = "Option::is_none")] scope: Option>, } let query = serde_html_form::to_string(Parameters { - redirect_uri: redirect_uris.clone(), + redirect_uri: redirect_uris.as_ref().map(|u| { + u.iter() + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) + .collect() + }), scope: scopes .as_ref() .map(|s| Scope::serialize_multiple(s.as_slice())), }) .ok(); - let mut client_id = String::from("http://localhost"); + let mut client_id = String::from("http://localhost/"); if let Some(query) = query && !query.is_empty() { @@ -173,8 +177,8 @@ impl<'m> AtprotoClientMetadata<'m> { client_id: Url::parse(&client_id).unwrap(), client_uri: None, redirect_uris: redirect_uris.unwrap_or(vec![ - Url::from_str("http://127.0.0.1/").unwrap(), - Url::from_str("http://[::1]/").unwrap(), + Url::from_str("http://127.0.0.1").unwrap(), + Url::from_str("http://[::1]").unwrap(), ]), grant_types: vec![GrantType::AuthorizationCode, GrantType::RefreshToken], scopes: scopes.unwrap_or(vec![Scope::Atproto]), @@ -216,15 +220,20 @@ pub fn atproto_client_metadata<'m>( } else { (AuthMethod::None, None, None) }; - + let client_id = metadata.client_id.as_str().trim_end_matches("/"); + let client_uri = metadata + .client_uri + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()); + let redirect_uris = metadata + .redirect_uris + .iter() + .map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()) + .collect(); + let jwks_uri = jwks_uri.map(|u| u.as_str().trim_end_matches("/").to_cowstr().into_static()); Ok(OAuthClientMetadata { - client_id: metadata.client_id.to_cowstr().into_static(), - client_uri: metadata.client_uri.map(|u| u.to_cowstr().into_static()), - redirect_uris: metadata - .redirect_uris - .iter() - .map(|u| u.to_cowstr().into_static()) - .collect(), + client_id: client_id.to_cowstr().into_static(), + client_uri, + redirect_uris, token_endpoint_auth_method: Some(auth_method.into()), grant_types: if keyset.is_some() { Some(metadata.grant_types.into_iter().map(|v| v.into()).collect()) @@ -233,7 +242,7 @@ pub fn atproto_client_metadata<'m>( }, scope: Some(Scope::serialize_multiple(metadata.scopes.as_slice())), dpop_bound_access_tokens: Some(true), - jwks_uri: jwks_uri.map(|u| u.to_cowstr().into_static()), + jwks_uri, jwks, token_endpoint_auth_signing_alg: if keyset.is_some() { Some(CowStr::new_static("ES256")) @@ -275,8 +284,8 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 client_id: CowStr::new_static("http://localhost"), client_uri: None, redirect_uris: vec![ - CowStr::new_static("http://127.0.0.1/"), - CowStr::new_static("http://[::1]/"), + CowStr::new_static("http://127.0.0.1"), + CowStr::new_static("http://[::1]"), ], scope: Some(CowStr::new_static("atproto")), grant_types: None, @@ -313,7 +322,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 .expect("failed to convert metadata"), OAuthClientMetadata { client_id: CowStr::new_static( - "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" + "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" ), client_uri: None, redirect_uris: vec![ @@ -354,7 +363,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 out, OAuthClientMetadata { client_id: CowStr::new_static( - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1" ), client_uri: None, redirect_uris: vec![CowStr::new_static("http://127.0.0.1")], @@ -385,7 +394,7 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 out, OAuthClientMetadata { client_id: CowStr::new_static( - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000%2F" + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1%3A8000" ), client_uri: None, redirect_uris: vec![CowStr::new_static("http://127.0.0.1:8000")], @@ -416,10 +425,10 @@ gbGGr0pN+oSing7cZ0169JaRHTNh+0LNQXrFobInX6cj95FzEdRyT4T3 out, OAuthClientMetadata { client_id: CowStr::new_static( - "http://localhost?redirect_uri=http%3A%2F%2F127.0.0.1%2F" + "http://localhost/?redirect_uri=http%3A%2F%2F127.0.0.1" ), client_uri: None, - redirect_uris: vec![CowStr::new_static("http://127.0.0.1/")], + redirect_uris: vec![CowStr::new_static("http://127.0.0.1")], scope: Some(CowStr::new_static("atproto")), grant_types: None, token_endpoint_auth_method: Some(AuthMethod::None.into()), diff --git a/crates/jacquard-oauth/src/client.rs b/crates/jacquard-oauth/src/client.rs index bd8db0ef..f2c3bb3a 100644 --- a/crates/jacquard-oauth/src/client.rs +++ b/crates/jacquard-oauth/src/client.rs @@ -593,7 +593,7 @@ where async fn set_base_uri(&self, url: Url) { let mut guard = self.data.write().await; - guard.host_url = url.to_cowstr().into_static(); + guard.host_url = url.as_str().trim_end_matches("/").to_cowstr().into_static(); } async fn send(&self, request: R) -> XrpcResult> diff --git a/crates/jacquard-oauth/src/resolver.rs b/crates/jacquard-oauth/src/resolver.rs index 7f3c9666..d17c0578 100644 --- a/crates/jacquard-oauth/src/resolver.rs +++ b/crates/jacquard-oauth/src/resolver.rs @@ -5,6 +5,7 @@ use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetad use http::{Request, StatusCode}; use jacquard_common::CowStr; use jacquard_common::IntoStatic; +use jacquard_common::cowstr::ToCowStr; use jacquard_common::types::did_doc::DidDocument; use jacquard_common::types::ident::AtIdentifier; use jacquard_common::{http_client::HttpClient, types::did::Did}; @@ -423,7 +424,7 @@ async fn resolve_oauth_impl( // resolve to a DID) Ok(if input.starts_with("https://") { let url = Url::parse(input).map_err(|_| ResolverError::not_found())?; - (resolver.resolve_from_service(&url).await?, None) + (resolver.resolve_from_service(&url.to_cowstr()).await?, None) } else { let (metadata, identity) = resolver.resolve_from_identity(input).await?; (metadata, Some(identity)) @@ -491,7 +492,9 @@ async fn resolve_from_identity_impl( .map_err(|e| ResolverError::at_identifier(smol_str::format_smolstr!("{:?}", e)))?; let identity = resolver.resolve_ident_owned(&actor).await?; if let Some(pds) = &identity.pds_endpoint() { - let metadata = resolver.get_resource_server_metadata(pds).await?; + let metadata = resolver + .get_resource_server_metadata(&pds.to_cowstr()) + .await?; Ok((metadata, identity)) } else { Err(ResolverError::did_document("Did doc lacking pds")) @@ -514,7 +517,7 @@ async fn get_authorization_server_metadata_impl( issuer: &CowStr<'_>, ) -> Result> { let mut md = resolve_authorization_server(client, issuer).await?; - md.issuer = issuer.into_static(); + md.issuer = issuer.clone().into_static(); Ok(md) } diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index f919ca07..eeeb50e6 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -144,7 +144,7 @@ trait-variant.workspace = true bytes.workspace = true http.workspace = true miette = { workspace = true } -reqwest = { workspace = true, features = ["charset", "json", "gzip"] } +reqwest = { workspace = true, features = ["json", "gzip"] } serde.workspace = true serde_html_form.workspace = true serde_json.workspace = true diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index c7074e46..edfd8662 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -254,7 +254,7 @@ where let base_uri = self.base_uri().await; let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url"); self.resolver - .xrpc(base_uri.clone()) + .xrpc(base_uri) .with_options(opts.clone()) .send(&request) .await @@ -287,8 +287,9 @@ where { async move { let base_uri = self.base_uri().await; + let base_uri = Url::parse(&base_uri).expect("base_uri should be valid url"); self.resolver - .xrpc(base_uri.clone()) + .xrpc(base_uri) .with_options(opts.clone()) .send(&request) .await diff --git a/crates/jacquard/src/client/credential_session.rs b/crates/jacquard/src/client/credential_session.rs index f963da16..f51b2f59 100644 --- a/crates/jacquard/src/client/credential_session.rs +++ b/crates/jacquard/src/client/credential_session.rs @@ -286,7 +286,8 @@ where } // Activate *self.key.write().await = Some(key); - *self.endpoint.write().await = Some(pds.to_cowstr().into_static()); + *self.endpoint.write().await = + Some(pds.as_str().trim_end_matches("/").to_cowstr().into_static()); Ok(session) } @@ -442,7 +443,7 @@ where async fn set_base_uri(&self, url: Url) { let mut guard = self.endpoint.write().await; - *guard = Some(url.to_cowstr().into_static()); + *guard = Some(url.as_str().trim_end_matches("/").to_cowstr().into_static()); } async fn send(&self, request: R) -> XrpcResult> diff --git a/crates/jacquard/tests/credential_session.rs b/crates/jacquard/tests/credential_session.rs index b18e8ff0..443ccdbc 100644 --- a/crates/jacquard/tests/credential_session.rs +++ b/crates/jacquard/tests/credential_session.rs @@ -169,10 +169,7 @@ async fn credential_login_and_auto_refresh() { let session = CredentialSession::new(store.clone(), client.clone()); // Before login, default endpoint should be public appview - assert_eq!( - session.endpoint().await.as_str(), - "https://public.bsky.app/" - ); + assert_eq!(session.endpoint().await.as_str(), "https://public.bsky.app"); // Login using handle; resolves to PDS and persists session session @@ -187,7 +184,7 @@ async fn credential_login_and_auto_refresh() { .expect("login ok"); // Endpoint switches to PDS - assert_eq!(session.endpoint().await.as_str(), "https://pds/"); + assert_eq!(session.endpoint().await.as_str(), "https://pds"); // Send a request that will first 401 (ExpiredToken), then refresh, then succeed let resp = session diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f21ace0b..ab00bdd8 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "stable" +channel = "nightly" profile = "default" targets = [ "wasm32-unknown-unknown" ]