diff --git a/Cargo.lock b/Cargo.lock index dc6626d9f..fef9597bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1539,11 +1539,11 @@ dependencies = [ "bon", "bytes", "clap", - "hickory-resolver", "http", "jacquard-api", "jacquard-common", "jacquard-derive", + "jacquard-identity", "jacquard-oauth", "jose-jwk", "miette", @@ -1588,7 +1588,6 @@ dependencies = [ "cid", "ed25519-dalek", "enum_dispatch", - "hickory-resolver", "http", "ipld-core", "k256", @@ -1610,6 +1609,7 @@ dependencies = [ "smol_str", "thiserror 2.0.17", "tokio", + "trait-variant", "url", ] @@ -1630,6 +1630,29 @@ dependencies = [ "syn 2.0.106", ] +[[package]] +name = "jacquard-identity" +version = "0.2.0" +dependencies = [ + "async-trait", + "bon", + "bytes", + "hickory-resolver", + "http", + "jacquard-api", + "jacquard-common", + "miette", + "percent-encoding", + "reqwest", + "serde", + "serde_html_form", + "serde_json", + "thiserror 2.0.17", + "tokio", + "url", + "urlencoding", +] + [[package]] name = "jacquard-lexicon" version = "0.2.0" @@ -1657,12 +1680,12 @@ version = "0.1.0" dependencies = [ "async-trait", "base64 0.22.1", - "bon", "chrono", "dashmap", "elliptic-curve", "http", "jacquard-common", + "jacquard-identity", "jose-jwa", "jose-jwk", "miette", @@ -1678,6 +1701,7 @@ dependencies = [ "smol_str", "thiserror 2.0.17", "tokio", + "trait-variant", "url", "uuid", ] @@ -3346,6 +3370,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 97040ab41..10d1f68ad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,12 @@ serde_repr = "0.1" miette = "7.6" thiserror = "2.0" +# trait stuff +trait-variant = "0.1.2" + + +bon = "3.8.0" + # Data types bytes = "1.10" smol_str = { version = "0.3", features = ["serde"] } diff --git a/crates/jacquard-common/Cargo.toml b/crates/jacquard-common/Cargo.toml index b68f6de2b..84b32b36b 100644 --- a/crates/jacquard-common/Cargo.toml +++ b/crates/jacquard-common/Cargo.toml @@ -39,12 +39,11 @@ http.workspace = true async-trait = "0.1" tokio = { version = "1", features = ["sync"] } reqwest = { workspace = true, optional = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } -hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true } serde_ipld_dagcbor.workspace = true +trait-variant.workspace = true [features] default = [] -dns = ["dep:hickory-resolver"] crypto = [] crypto-ed25519 = ["crypto", "dep:ed25519-dalek"] crypto-k256 = ["crypto", "dep:k256"] diff --git a/crates/jacquard-common/src/cowstr.rs b/crates/jacquard-common/src/cowstr.rs index 32583c1f2..0da115526 100644 --- a/crates/jacquard-common/src/cowstr.rs +++ b/crates/jacquard-common/src/cowstr.rs @@ -1,5 +1,5 @@ -use serde::{Deserialize, Serialize, de::DeserializeOwned}; -use smol_str::{SmolStr, ToSmolStr}; +use serde::{Deserialize, Serialize}; +use smol_str::SmolStr; use std::{ borrow::Cow, fmt, diff --git a/crates/jacquard-common/src/lib.rs b/crates/jacquard-common/src/lib.rs index 745b51952..e9514d3bc 100644 --- a/crates/jacquard-common/src/lib.rs +++ b/crates/jacquard-common/src/lib.rs @@ -16,7 +16,6 @@ pub mod into_static; pub mod error; /// HTTP client abstraction used by jacquard crates. pub mod http_client; -pub mod ident_resolver; pub mod macros; /// Generic session storage traits and utilities. pub mod session; diff --git a/crates/jacquard-identity/Cargo.toml b/crates/jacquard-identity/Cargo.toml new file mode 100644 index 000000000..1d49939df --- /dev/null +++ b/crates/jacquard-identity/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "jacquard-identity" +edition.workspace = true +version.workspace = true +authors.workspace = true +repository.workspace = true +keywords.workspace = true +categories.workspace = true +readme.workspace = true +exclude.workspace = true +homepage.workspace = true +license.workspace = true +description.workspace = true + +[features] +dns = ["dep:hickory-resolver"] + +[dependencies] +async-trait = "0.1.89" +bon.workspace = true +bytes.workspace = true +jacquard-common = { version = "0.2", path = "../jacquard-common" } +percent-encoding = "2.3.2" +reqwest.workspace = true +url.workspace = true +tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } +hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]} +serde.workspace = true +serde_json.workspace = true +thiserror.workspace = true +miette.workspace = true +http.workspace = true +jacquard-api = { version = "0.2.0", path = "../jacquard-api" } +serde_html_form.workspace = true +urlencoding = "2.1.3" diff --git a/crates/jacquard/src/identity.rs b/crates/jacquard-identity/src/lib.rs similarity index 98% rename from crates/jacquard/src/identity.rs rename to crates/jacquard-identity/src/lib.rs index ad65b1d5a..e278349c7 100644 --- a/crates/jacquard/src/identity.rs +++ b/crates/jacquard-identity/src/lib.rs @@ -12,25 +12,26 @@ //! and optionally validate the document `id` against the requested DID. // use crate::CowStr; // not currently needed directly here +pub mod resolver; -use bytes::Bytes; -use jacquard_common::IntoStatic; -use jacquard_common::error::TransportError; -use jacquard_common::http_client::HttpClient; -use jacquard_common::ident_resolver::{ +use crate::resolver::{ DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource, ResolverOptions, }; +use bytes::Bytes; +use jacquard_api::com_atproto::identity::resolve_did; +use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle; +use jacquard_common::error::TransportError; +use jacquard_common::http_client::HttpClient; +use jacquard_common::types::did::Did; +use jacquard_common::types::did_doc::DidDocument; +use jacquard_common::types::ident::AtIdentifier; use jacquard_common::types::xrpc::XrpcExt; +use jacquard_common::{IntoStatic, types::string::Handle}; use percent_encoding::percent_decode_str; use reqwest::StatusCode; use url::{ParseError, Url}; -use crate::api::com_atproto::identity::{resolve_did, resolve_handle::ResolveHandle}; -use crate::types::did_doc::DidDocument; -use crate::types::ident::AtIdentifier; -use crate::types::string::{Did, Handle}; - #[cfg(feature = "dns")] use hickory_resolver::{TokioAsyncResolver, config::ResolverConfig}; diff --git a/crates/jacquard-common/src/ident_resolver.rs b/crates/jacquard-identity/src/resolver.rs similarity index 96% rename from crates/jacquard-common/src/ident_resolver.rs rename to crates/jacquard-identity/src/resolver.rs index 451453dc3..61521988f 100644 --- a/crates/jacquard-common/src/ident_resolver.rs +++ b/crates/jacquard-identity/src/resolver.rs @@ -12,23 +12,21 @@ use std::collections::BTreeMap; use std::str::FromStr; -use crate::error::TransportError; -use crate::types::did_doc::Service; -use crate::types::ident::AtIdentifier; -use crate::types::string::AtprotoStr; -use crate::types::uri::Uri; -use crate::types::value::Data; -use crate::{CowStr, IntoStatic}; use bon::Builder; use bytes::Bytes; use http::StatusCode; +use jacquard_common::error::TransportError; +use jacquard_common::types::did::Did; +use jacquard_common::types::did_doc::{DidDocument, Service}; +use jacquard_common::types::ident::AtIdentifier; +use jacquard_common::types::string::{AtprotoStr, Handle}; +use jacquard_common::types::uri::Uri; +use jacquard_common::types::value::{AtDataError, Data}; +use jacquard_common::{CowStr, IntoStatic}; use miette::Diagnostic; use thiserror::Error; use url::Url; -use crate::types::did_doc::DidDocument; -use crate::types::string::{Did, Handle}; -use crate::types::value::AtDataError; /// Errors that can occur during identity resolution. /// /// Note: when validating a fetched DID document against a requested DID, a @@ -114,7 +112,9 @@ impl PlcSource { /// mismatch). Use `into_owned()` to parse into an owned document. #[derive(Clone)] pub struct DidDocResponse { + #[allow(missing_docs)] pub buffer: Bytes, + #[allow(missing_docs)] pub status: StatusCode, /// Optional DID we intended to resolve; used for validation helpers pub requested: Option>, @@ -205,9 +205,9 @@ pub struct MiniDoc<'a> { #[serde(borrow)] pub handle: Handle<'a>, #[serde(borrow)] - pub pds: crate::CowStr<'a>, + pub pds: CowStr<'a>, #[serde(borrow, rename = "signingKey", alias = "signing_key")] - pub signing_key: crate::CowStr<'a>, + pub signing_key: CowStr<'a>, } /// Handle → DID fallback step. diff --git a/crates/jacquard-oauth/Cargo.toml b/crates/jacquard-oauth/Cargo.toml index bb0ff282b..551e4f80b 100644 --- a/crates/jacquard-oauth/Cargo.toml +++ b/crates/jacquard-oauth/Cargo.toml @@ -29,5 +29,7 @@ rand = { version = "0.8.5", features = ["small_rng"] } async-trait = "0.1.89" dashmap = "6.1.0" tokio = { version = "1.47.1", features = ["sync"] } -bon = "3.8.0" + reqwest.workspace = true +trait-variant.workspace = true +jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" } diff --git a/crates/jacquard-oauth/src/atproto.rs b/crates/jacquard-oauth/src/atproto.rs index 2a9bcfae4..f579e57d1 100644 --- a/crates/jacquard-oauth/src/atproto.rs +++ b/crates/jacquard-oauth/src/atproto.rs @@ -5,7 +5,7 @@ use crate::{keyset::Keyset, scopes::Scope}; use jacquard_common::CowStr; use serde::{Deserialize, Serialize}; use thiserror::Error; -use url::{Host, Url}; +use url::Url; #[derive(Error, Debug)] pub enum Error { diff --git a/crates/jacquard-oauth/src/dpop.rs b/crates/jacquard-oauth/src/dpop.rs index 9a68072ae..9b164b9ee 100644 --- a/crates/jacquard-oauth/src/dpop.rs +++ b/crates/jacquard-oauth/src/dpop.rs @@ -63,19 +63,6 @@ pub trait DpopExt: HttpClient { { DpopCall::client(self, data_source) } - - async fn wrap_with_dpop<'r, D>( - &'r self, - is_to_auth_server: bool, - data_source: &'r mut D, - request: Request>, - ) -> Result>> - where - Self: Sized, - D: DpopDataSource, - { - wrap_request_with_dpop(self, data_source, is_to_auth_server, request).await - } } pub struct DpopCall<'r, C: HttpClient, D: DpopDataSource> { diff --git a/crates/jacquard-oauth/src/request.rs b/crates/jacquard-oauth/src/request.rs index dbf3f0be1..eb4239426 100644 --- a/crates/jacquard-oauth/src/request.rs +++ b/crates/jacquard-oauth/src/request.rs @@ -1,28 +1,25 @@ -use chrono::{DateTime, FixedOffset, TimeDelta, Utc}; +use chrono::{TimeDelta, Utc}; use http::{Method, Request, StatusCode}; use jacquard_common::{ CowStr, IntoStatic, cowstr::ToCowStr, http_client::HttpClient, - ident_resolver::{IdentityError, IdentityResolver}, session::SessionStoreError, types::{ did::Did, string::{AtStrError, Datetime}, }, }; -use jose_jwk::Key; -use serde::{Serialize, de::DeserializeOwned}; +use jacquard_identity::resolver::IdentityError; +use serde::Serialize; use serde_json::Value; use smol_str::ToSmolStr; -use std::sync::Arc; use thiserror::Error; -use url::Url; use crate::{ FALLBACK_ALG, - atproto::{AtprotoClientMetadata, atproto_client_metadata}, - dpop::{DpopClient, DpopExt}, + atproto::atproto_client_metadata, + dpop::DpopExt, jose::jwt::{RegisteredClaims, RegisteredClaimsAud}, keyset::Keyset, resolver::OAuthResolver, @@ -424,6 +421,7 @@ where } } +#[inline] fn endpoint_for_req<'a, 'r>( server_metadata: &'r OAuthAuthorizationServerMetadata<'a>, request: &'r OAuthRequest, @@ -438,10 +436,8 @@ fn endpoint_for_req<'a, 'r>( } } -fn build_oauth_req_body<'a, S>( - client_assertions: ClientAssertions<'a>, - parameters: S, -) -> Result +#[inline] +fn build_oauth_req_body<'a, S>(client_assertions: ClientAuth<'a>, parameters: S) -> Result where S: Serialize, { @@ -454,13 +450,13 @@ where } #[derive(Debug, Clone, Default)] -pub struct ClientAssertions<'a> { +pub struct ClientAuth<'a> { client_id: CowStr<'a>, assertion_type: Option>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER` assertion: Option>, } -impl<'s> ClientAssertions<'s> { +impl<'s> ClientAuth<'s> { pub fn new_id(client_id: CowStr<'s>) -> Self { Self { client_id, @@ -474,7 +470,7 @@ fn build_auth<'a>( keyset: Option<&Keyset>, server_metadata: &OAuthAuthorizationServerMetadata<'a>, client_metadata: &OAuthClientMetadata<'a>, -) -> Result> { +) -> Result> { let method_supported = server_metadata .token_endpoint_auth_methods_supported .as_ref(); @@ -494,7 +490,7 @@ fn build_auth<'a>( .unwrap_or(vec![FALLBACK_ALG.into()]); algs.sort_by(compare_algos); let iat = Utc::now().timestamp(); - return Ok(ClientAssertions { + return Ok(ClientAuth { client_id: client_id.clone(), assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)), assertion: Some( @@ -526,7 +522,7 @@ fn build_auth<'a>( .as_ref() .is_some_and(|v| v.contains(&CowStr::new_static("none"))) => { - return Ok(ClientAssertions::new_id(client_id)); + return Ok(ClientAuth::new_id(client_id)); } _ => {} } diff --git a/crates/jacquard-oauth/src/resolver.rs b/crates/jacquard-oauth/src/resolver.rs index e581f189d..8588dcbb6 100644 --- a/crates/jacquard-oauth/src/resolver.rs +++ b/crates/jacquard-oauth/src/resolver.rs @@ -1,11 +1,10 @@ use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata}; use http::{Request, StatusCode}; use jacquard_common::IntoStatic; -use jacquard_common::ident_resolver::{IdentityError, IdentityResolver}; use jacquard_common::types::did_doc::DidDocument; use jacquard_common::types::ident::AtIdentifier; use jacquard_common::{http_client::HttpClient, types::did::Did}; -use sha2::digest::const_oid::Arc; +use jacquard_identity::resolver::{IdentityError, IdentityResolver}; use url::Url; #[derive(thiserror::Error, Debug, miette::Diagnostic)] @@ -161,9 +160,6 @@ pub trait OAuthResolver: IdentityResolver + HttpClient { } } -#[async_trait::async_trait] -impl OAuthResolver for std::sync::Arc {} - pub async fn resolve_authorization_server( client: &T, server: &Url, diff --git a/crates/jacquard-oauth/src/session.rs b/crates/jacquard-oauth/src/session.rs index 57ebb4a4f..26fed59f2 100644 --- a/crates/jacquard-oauth/src/session.rs +++ b/crates/jacquard-oauth/src/session.rs @@ -308,7 +308,8 @@ where return Ok(session); } } - let metadata = OAuthMetadata::new(&self.client, &self.client_data, &session).await?; + let metadata = + OAuthMetadata::new(self.client.as_ref(), &self.client_data, &session).await?; session = refresh(self.client.as_ref(), session, &metadata).await?; self.store.upsert_session(session.clone()).await?; diff --git a/crates/jacquard-oauth/src/utils.rs b/crates/jacquard-oauth/src/utils.rs index fe3ef2388..7c3584aa1 100644 --- a/crates/jacquard-oauth/src/utils.rs +++ b/crates/jacquard-oauth/src/utils.rs @@ -1,11 +1,10 @@ use base64::Engine; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use elliptic_curve::SecretKey; -use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr}; +use jacquard_common::CowStr; use jose_jwk::{Key, crypto}; use rand::{CryptoRng, RngCore, rngs::ThreadRng}; use sha2::{Digest, Sha256}; -use smol_str::ToSmolStr; use std::cmp::Ordering; use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata}; diff --git a/crates/jacquard/Cargo.toml b/crates/jacquard/Cargo.toml index be9358e64..5b45e0fe1 100644 --- a/crates/jacquard/Cargo.toml +++ b/crates/jacquard/Cargo.toml @@ -16,7 +16,7 @@ default = ["api_all", "dns", "fancy", "loopback"] derive = ["dep:jacquard-derive"] api = ["jacquard-api/com_atproto"] api_all = ["api", "jacquard-api/app_bsky", "jacquard-api/chat_bsky", "jacquard-api/tools_ozone"] -dns = ["dep:hickory-resolver", "jacquard-common/dns"] +dns = ["jacquard-identity/dns"] fancy = ["miette/fancy"] loopback = ["dep:rouille"] @@ -47,7 +47,6 @@ serde_ipld_dagcbor.workspace = true serde_json.workspace = true thiserror.workspace = true tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } -hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true } url.workspace = true smol_str.workspace = true percent-encoding = "2" @@ -56,3 +55,4 @@ jose-jwk = { version = "0.1", features = ["p256"] } p256 = { version = "0.13", features = ["ecdsa"] } rand_core = "0.6" rouille = { version = "3.6.2", optional = true } +jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" } diff --git a/crates/jacquard/src/client.rs b/crates/jacquard/src/client.rs index ce1d6af74..36c511f7b 100644 --- a/crates/jacquard/src/client.rs +++ b/crates/jacquard/src/client.rs @@ -21,8 +21,6 @@ use jacquard_common::{ pub use token::FileTokenStore; use url::Url; -use p256::SecretKey; - // Note: Stateless and stateful XRPC clients are implemented in xrpc_call.rs and at_client.rs pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession"; @@ -133,7 +131,7 @@ impl From), + OAuth(jacquard_oauth::session::ClientSessionData<'static>), } impl AuthSession { @@ -187,8 +185,8 @@ impl From for AuthSession { } } -impl From> for AuthSession { - fn from(session: jacquard_oauth::session::OauthSession<'static>) -> Self { +impl From> for AuthSession { + fn from(session: jacquard_oauth::session::ClientSessionData<'static>) -> Self { AuthSession::OAuth(session) } } diff --git a/crates/jacquard/src/client/at_client.rs b/crates/jacquard/src/client/at_client.rs index 929dc26a4..a36e33a69 100644 --- a/crates/jacquard/src/client/at_client.rs +++ b/crates/jacquard/src/client/at_client.rs @@ -13,7 +13,7 @@ use url::Url; use jacquard_common::types::xrpc::{XrpcRequest, build_http_request}; -use crate::client::{AtpSession, AuthSession, FileTokenStore, NSID_REFRESH_SESSION}; +use crate::client::{AtpSession, AuthSession, NSID_REFRESH_SESSION}; /// Per-call overrides when sending via `AtClient`. #[derive(Debug, Default, Clone)] diff --git a/crates/jacquard/src/lib.rs b/crates/jacquard/src/lib.rs index 592e4c459..3d2d26a3f 100644 --- a/crates/jacquard/src/lib.rs +++ b/crates/jacquard/src/lib.rs @@ -174,5 +174,4 @@ pub use jacquard_common::*; /// if enabled, reexport the attribute macros pub use jacquard_derive::*; -/// Identity resolution helpers (DIDs, handles, PDS endpoints) -pub mod identity; +pub use jacquard_identity as identity; diff --git a/crates/jacquard/src/main.rs b/crates/jacquard/src/main.rs index 44c0dc518..5dfd64618 100644 --- a/crates/jacquard/src/main.rs +++ b/crates/jacquard/src/main.rs @@ -3,7 +3,7 @@ use jacquard::CowStr; use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; use jacquard::api::com_atproto::server::create_session::CreateSession; use jacquard::client::{AtpSession, AuthSession, BasicClient}; -use jacquard::ident_resolver::IdentityResolver; +use jacquard::identity::resolver::IdentityResolver; use jacquard::identity::slingshot_resolver_default; use jacquard::types::string::Handle; use miette::IntoDiagnostic;