A better Rust ATProto crate

moved identity resolution stuff into its own crate

Orual 102d6666 36ef115b

Changed files
+134 -81
crates
+38 -3
Cargo.lock
··· 1539 1539 "bon", 1540 1540 "bytes", 1541 1541 "clap", 1542 - "hickory-resolver", 1543 1542 "http", 1544 1543 "jacquard-api", 1545 1544 "jacquard-common", 1546 1545 "jacquard-derive", 1546 + "jacquard-identity", 1547 1547 "jacquard-oauth", 1548 1548 "jose-jwk", 1549 1549 "miette", ··· 1588 1588 "cid", 1589 1589 "ed25519-dalek", 1590 1590 "enum_dispatch", 1591 - "hickory-resolver", 1592 1591 "http", 1593 1592 "ipld-core", 1594 1593 "k256", ··· 1610 1609 "smol_str", 1611 1610 "thiserror 2.0.17", 1612 1611 "tokio", 1612 + "trait-variant", 1613 1613 "url", 1614 1614 ] 1615 1615 ··· 1631 1631 ] 1632 1632 1633 1633 [[package]] 1634 + name = "jacquard-identity" 1635 + version = "0.2.0" 1636 + dependencies = [ 1637 + "async-trait", 1638 + "bon", 1639 + "bytes", 1640 + "hickory-resolver", 1641 + "http", 1642 + "jacquard-api", 1643 + "jacquard-common", 1644 + "miette", 1645 + "percent-encoding", 1646 + "reqwest", 1647 + "serde", 1648 + "serde_html_form", 1649 + "serde_json", 1650 + "thiserror 2.0.17", 1651 + "tokio", 1652 + "url", 1653 + "urlencoding", 1654 + ] 1655 + 1656 + [[package]] 1634 1657 name = "jacquard-lexicon" 1635 1658 version = "0.2.0" 1636 1659 dependencies = [ ··· 1657 1680 dependencies = [ 1658 1681 "async-trait", 1659 1682 "base64 0.22.1", 1660 - "bon", 1661 1683 "chrono", 1662 1684 "dashmap", 1663 1685 "elliptic-curve", 1664 1686 "http", 1665 1687 "jacquard-common", 1688 + "jacquard-identity", 1666 1689 "jose-jwa", 1667 1690 "jose-jwk", 1668 1691 "miette", ··· 1678 1701 "smol_str", 1679 1702 "thiserror 2.0.17", 1680 1703 "tokio", 1704 + "trait-variant", 1681 1705 "url", 1682 1706 "uuid", 1683 1707 ] ··· 3344 3368 checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" 3345 3369 dependencies = [ 3346 3370 "once_cell", 3371 + ] 3372 + 3373 + [[package]] 3374 + name = "trait-variant" 3375 + version = "0.1.2" 3376 + source = "registry+https://github.com/rust-lang/crates.io-index" 3377 + checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" 3378 + dependencies = [ 3379 + "proc-macro2", 3380 + "quote", 3381 + "syn 2.0.106", 3347 3382 ] 3348 3383 3349 3384 [[package]]
+6
Cargo.toml
··· 35 35 miette = "7.6" 36 36 thiserror = "2.0" 37 37 38 + # trait stuff 39 + trait-variant = "0.1.2" 40 + 41 + 42 + bon = "3.8.0" 43 + 38 44 # Data types 39 45 bytes = "1.10" 40 46 smol_str = { version = "0.3", features = ["serde"] }
+1 -2
crates/jacquard-common/Cargo.toml
··· 39 39 async-trait = "0.1" 40 40 tokio = { version = "1", features = ["sync"] } 41 41 reqwest = { workspace = true, optional = true, features = ["charset", "http2", "json", "system-proxy", "gzip", "rustls-tls"] } 42 - hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true } 43 42 serde_ipld_dagcbor.workspace = true 43 + trait-variant.workspace = true 44 44 45 45 [features] 46 46 default = [] 47 - dns = ["dep:hickory-resolver"] 48 47 crypto = [] 49 48 crypto-ed25519 = ["crypto", "dep:ed25519-dalek"] 50 49 crypto-k256 = ["crypto", "dep:k256"]
+2 -2
crates/jacquard-common/src/cowstr.rs
··· 1 - use serde::{Deserialize, Serialize, de::DeserializeOwned}; 2 - use smol_str::{SmolStr, ToSmolStr}; 1 + use serde::{Deserialize, Serialize}; 2 + use smol_str::SmolStr; 3 3 use std::{ 4 4 borrow::Cow, 5 5 fmt,
+12 -12
crates/jacquard-common/src/ident_resolver.rs crates/jacquard-identity/src/resolver.rs
··· 12 12 use std::collections::BTreeMap; 13 13 use std::str::FromStr; 14 14 15 - use crate::error::TransportError; 16 - use crate::types::did_doc::Service; 17 - use crate::types::ident::AtIdentifier; 18 - use crate::types::string::AtprotoStr; 19 - use crate::types::uri::Uri; 20 - use crate::types::value::Data; 21 - use crate::{CowStr, IntoStatic}; 22 15 use bon::Builder; 23 16 use bytes::Bytes; 24 17 use http::StatusCode; 18 + use jacquard_common::error::TransportError; 19 + use jacquard_common::types::did::Did; 20 + use jacquard_common::types::did_doc::{DidDocument, Service}; 21 + use jacquard_common::types::ident::AtIdentifier; 22 + use jacquard_common::types::string::{AtprotoStr, Handle}; 23 + use jacquard_common::types::uri::Uri; 24 + use jacquard_common::types::value::{AtDataError, Data}; 25 + use jacquard_common::{CowStr, IntoStatic}; 25 26 use miette::Diagnostic; 26 27 use thiserror::Error; 27 28 use url::Url; 28 29 29 - use crate::types::did_doc::DidDocument; 30 - use crate::types::string::{Did, Handle}; 31 - use crate::types::value::AtDataError; 32 30 /// Errors that can occur during identity resolution. 33 31 /// 34 32 /// Note: when validating a fetched DID document against a requested DID, a ··· 114 112 /// mismatch). Use `into_owned()` to parse into an owned document. 115 113 #[derive(Clone)] 116 114 pub struct DidDocResponse { 115 + #[allow(missing_docs)] 117 116 pub buffer: Bytes, 117 + #[allow(missing_docs)] 118 118 pub status: StatusCode, 119 119 /// Optional DID we intended to resolve; used for validation helpers 120 120 pub requested: Option<Did<'static>>, ··· 205 205 #[serde(borrow)] 206 206 pub handle: Handle<'a>, 207 207 #[serde(borrow)] 208 - pub pds: crate::CowStr<'a>, 208 + pub pds: CowStr<'a>, 209 209 #[serde(borrow, rename = "signingKey", alias = "signing_key")] 210 - pub signing_key: crate::CowStr<'a>, 210 + pub signing_key: CowStr<'a>, 211 211 } 212 212 213 213 /// Handle → DID fallback step.
-1
crates/jacquard-common/src/lib.rs
··· 16 16 pub mod error; 17 17 /// HTTP client abstraction used by jacquard crates. 18 18 pub mod http_client; 19 - pub mod ident_resolver; 20 19 pub mod macros; 21 20 /// Generic session storage traits and utilities. 22 21 pub mod session;
+35
crates/jacquard-identity/Cargo.toml
··· 1 + [package] 2 + name = "jacquard-identity" 3 + edition.workspace = true 4 + version.workspace = true 5 + authors.workspace = true 6 + repository.workspace = true 7 + keywords.workspace = true 8 + categories.workspace = true 9 + readme.workspace = true 10 + exclude.workspace = true 11 + homepage.workspace = true 12 + license.workspace = true 13 + description.workspace = true 14 + 15 + [features] 16 + dns = ["dep:hickory-resolver"] 17 + 18 + [dependencies] 19 + async-trait = "0.1.89" 20 + bon.workspace = true 21 + bytes.workspace = true 22 + jacquard-common = { version = "0.2", path = "../jacquard-common" } 23 + percent-encoding = "2.3.2" 24 + reqwest.workspace = true 25 + url.workspace = true 26 + tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } 27 + hickory-resolver = { optional = true, version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"]} 28 + serde.workspace = true 29 + serde_json.workspace = true 30 + thiserror.workspace = true 31 + miette.workspace = true 32 + http.workspace = true 33 + jacquard-api = { version = "0.2.0", path = "../jacquard-api" } 34 + serde_html_form.workspace = true 35 + urlencoding = "2.1.3"
+3 -1
crates/jacquard-oauth/Cargo.toml
··· 29 29 async-trait = "0.1.89" 30 30 dashmap = "6.1.0" 31 31 tokio = { version = "1.47.1", features = ["sync"] } 32 - bon = "3.8.0" 32 + 33 33 reqwest.workspace = true 34 + trait-variant.workspace = true 35 + jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" }
+1 -1
crates/jacquard-oauth/src/atproto.rs
··· 5 5 use jacquard_common::CowStr; 6 6 use serde::{Deserialize, Serialize}; 7 7 use thiserror::Error; 8 - use url::{Host, Url}; 8 + use url::Url; 9 9 10 10 #[derive(Error, Debug)] 11 11 pub enum Error {
-13
crates/jacquard-oauth/src/dpop.rs
··· 63 63 { 64 64 DpopCall::client(self, data_source) 65 65 } 66 - 67 - async fn wrap_with_dpop<'r, D>( 68 - &'r self, 69 - is_to_auth_server: bool, 70 - data_source: &'r mut D, 71 - request: Request<Vec<u8>>, 72 - ) -> Result<Response<Vec<u8>>> 73 - where 74 - Self: Sized, 75 - D: DpopDataSource, 76 - { 77 - wrap_request_with_dpop(self, data_source, is_to_auth_server, request).await 78 - } 79 66 } 80 67 81 68 pub struct DpopCall<'r, C: HttpClient, D: DpopDataSource> {
+13 -17
crates/jacquard-oauth/src/request.rs
··· 1 - use chrono::{DateTime, FixedOffset, TimeDelta, Utc}; 1 + use chrono::{TimeDelta, Utc}; 2 2 use http::{Method, Request, StatusCode}; 3 3 use jacquard_common::{ 4 4 CowStr, IntoStatic, 5 5 cowstr::ToCowStr, 6 6 http_client::HttpClient, 7 - ident_resolver::{IdentityError, IdentityResolver}, 8 7 session::SessionStoreError, 9 8 types::{ 10 9 did::Did, 11 10 string::{AtStrError, Datetime}, 12 11 }, 13 12 }; 14 - use jose_jwk::Key; 15 - use serde::{Serialize, de::DeserializeOwned}; 13 + use jacquard_identity::resolver::IdentityError; 14 + use serde::Serialize; 16 15 use serde_json::Value; 17 16 use smol_str::ToSmolStr; 18 - use std::sync::Arc; 19 17 use thiserror::Error; 20 - use url::Url; 21 18 22 19 use crate::{ 23 20 FALLBACK_ALG, 24 - atproto::{AtprotoClientMetadata, atproto_client_metadata}, 25 - dpop::{DpopClient, DpopExt}, 21 + atproto::atproto_client_metadata, 22 + dpop::DpopExt, 26 23 jose::jwt::{RegisteredClaims, RegisteredClaimsAud}, 27 24 keyset::Keyset, 28 25 resolver::OAuthResolver, ··· 424 421 } 425 422 } 426 423 424 + #[inline] 427 425 fn endpoint_for_req<'a, 'r>( 428 426 server_metadata: &'r OAuthAuthorizationServerMetadata<'a>, 429 427 request: &'r OAuthRequest, ··· 438 436 } 439 437 } 440 438 441 - fn build_oauth_req_body<'a, S>( 442 - client_assertions: ClientAssertions<'a>, 443 - parameters: S, 444 - ) -> Result<String> 439 + #[inline] 440 + fn build_oauth_req_body<'a, S>(client_assertions: ClientAuth<'a>, parameters: S) -> Result<String> 445 441 where 446 442 S: Serialize, 447 443 { ··· 454 450 } 455 451 456 452 #[derive(Debug, Clone, Default)] 457 - pub struct ClientAssertions<'a> { 453 + pub struct ClientAuth<'a> { 458 454 client_id: CowStr<'a>, 459 455 assertion_type: Option<CowStr<'a>>, // either none or `CLIENT_ASSERTION_TYPE_JWT_BEARER` 460 456 assertion: Option<CowStr<'a>>, 461 457 } 462 458 463 - impl<'s> ClientAssertions<'s> { 459 + impl<'s> ClientAuth<'s> { 464 460 pub fn new_id(client_id: CowStr<'s>) -> Self { 465 461 Self { 466 462 client_id, ··· 474 470 keyset: Option<&Keyset>, 475 471 server_metadata: &OAuthAuthorizationServerMetadata<'a>, 476 472 client_metadata: &OAuthClientMetadata<'a>, 477 - ) -> Result<ClientAssertions<'a>> { 473 + ) -> Result<ClientAuth<'a>> { 478 474 let method_supported = server_metadata 479 475 .token_endpoint_auth_methods_supported 480 476 .as_ref(); ··· 494 490 .unwrap_or(vec![FALLBACK_ALG.into()]); 495 491 algs.sort_by(compare_algos); 496 492 let iat = Utc::now().timestamp(); 497 - return Ok(ClientAssertions { 493 + return Ok(ClientAuth { 498 494 client_id: client_id.clone(), 499 495 assertion_type: Some(CowStr::new_static(CLIENT_ASSERTION_TYPE_JWT_BEARER)), 500 496 assertion: Some( ··· 526 522 .as_ref() 527 523 .is_some_and(|v| v.contains(&CowStr::new_static("none"))) => 528 524 { 529 - return Ok(ClientAssertions::new_id(client_id)); 525 + return Ok(ClientAuth::new_id(client_id)); 530 526 } 531 527 _ => {} 532 528 }
+1 -5
crates/jacquard-oauth/src/resolver.rs
··· 1 1 use crate::types::{OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata}; 2 2 use http::{Request, StatusCode}; 3 3 use jacquard_common::IntoStatic; 4 - use jacquard_common::ident_resolver::{IdentityError, IdentityResolver}; 5 4 use jacquard_common::types::did_doc::DidDocument; 6 5 use jacquard_common::types::ident::AtIdentifier; 7 6 use jacquard_common::{http_client::HttpClient, types::did::Did}; 8 - use sha2::digest::const_oid::Arc; 7 + use jacquard_identity::resolver::{IdentityError, IdentityResolver}; 9 8 use url::Url; 10 9 11 10 #[derive(thiserror::Error, Debug, miette::Diagnostic)] ··· 160 159 Ok(as_metadata) 161 160 } 162 161 } 163 - 164 - #[async_trait::async_trait] 165 - impl<T: OAuthResolver + Sync + Send> OAuthResolver for std::sync::Arc<T> {} 166 162 167 163 pub async fn resolve_authorization_server<T: HttpClient + ?Sized>( 168 164 client: &T,
+2 -1
crates/jacquard-oauth/src/session.rs
··· 308 308 return Ok(session); 309 309 } 310 310 } 311 - let metadata = OAuthMetadata::new(&self.client, &self.client_data, &session).await?; 311 + let metadata = 312 + OAuthMetadata::new(self.client.as_ref(), &self.client_data, &session).await?; 312 313 session = refresh(self.client.as_ref(), session, &metadata).await?; 313 314 self.store.upsert_session(session.clone()).await?; 314 315
+1 -2
crates/jacquard-oauth/src/utils.rs
··· 1 1 use base64::Engine; 2 2 use base64::engine::general_purpose::URL_SAFE_NO_PAD; 3 3 use elliptic_curve::SecretKey; 4 - use jacquard_common::{CowStr, IntoStatic, cowstr::ToCowStr}; 4 + use jacquard_common::CowStr; 5 5 use jose_jwk::{Key, crypto}; 6 6 use rand::{CryptoRng, RngCore, rngs::ThreadRng}; 7 7 use sha2::{Digest, Sha256}; 8 - use smol_str::ToSmolStr; 9 8 use std::cmp::Ordering; 10 9 11 10 use crate::{FALLBACK_ALG, types::OAuthAuthorizationServerMetadata};
+2 -2
crates/jacquard/Cargo.toml
··· 16 16 derive = ["dep:jacquard-derive"] 17 17 api = ["jacquard-api/com_atproto"] 18 18 api_all = ["api", "jacquard-api/app_bsky", "jacquard-api/chat_bsky", "jacquard-api/tools_ozone"] 19 - dns = ["dep:hickory-resolver", "jacquard-common/dns"] 19 + dns = ["jacquard-identity/dns"] 20 20 fancy = ["miette/fancy"] 21 21 loopback = ["dep:rouille"] 22 22 ··· 47 47 serde_json.workspace = true 48 48 thiserror.workspace = true 49 49 tokio = { version = "1", features = ["macros", "rt-multi-thread", "fs"] } 50 - hickory-resolver = { version = "0.24", default-features = false, features = ["system-config", "tokio-runtime"], optional = true } 51 50 url.workspace = true 52 51 smol_str.workspace = true 53 52 percent-encoding = "2" ··· 56 55 p256 = { version = "0.13", features = ["ecdsa"] } 57 56 rand_core = "0.6" 58 57 rouille = { version = "3.6.2", optional = true } 58 + jacquard-identity = { version = "0.2.0", path = "../jacquard-identity" }
+3 -5
crates/jacquard/src/client.rs
··· 21 21 pub use token::FileTokenStore; 22 22 use url::Url; 23 23 24 - use p256::SecretKey; 25 - 26 24 // Note: Stateless and stateful XRPC clients are implemented in xrpc_call.rs and at_client.rs 27 25 28 26 pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession"; ··· 133 131 #[derive(Debug, Clone)] 134 132 pub enum AuthSession { 135 133 AppPassword(AtpSession), 136 - OAuth(jacquard_oauth::session::OauthSession<'static>), 134 + OAuth(jacquard_oauth::session::ClientSessionData<'static>), 137 135 } 138 136 139 137 impl AuthSession { ··· 187 185 } 188 186 } 189 187 190 - impl From<jacquard_oauth::session::OauthSession<'static>> for AuthSession { 191 - fn from(session: jacquard_oauth::session::OauthSession<'static>) -> Self { 188 + impl From<jacquard_oauth::session::ClientSessionData<'static>> for AuthSession { 189 + fn from(session: jacquard_oauth::session::ClientSessionData<'static>) -> Self { 192 190 AuthSession::OAuth(session) 193 191 } 194 192 }
+1 -1
crates/jacquard/src/client/at_client.rs
··· 13 13 14 14 use jacquard_common::types::xrpc::{XrpcRequest, build_http_request}; 15 15 16 - use crate::client::{AtpSession, AuthSession, FileTokenStore, NSID_REFRESH_SESSION}; 16 + use crate::client::{AtpSession, AuthSession, NSID_REFRESH_SESSION}; 17 17 18 18 /// Per-call overrides when sending via `AtClient`. 19 19 #[derive(Debug, Default, Clone)]
+11 -10
crates/jacquard/src/identity.rs crates/jacquard-identity/src/lib.rs
··· 12 12 //! and optionally validate the document `id` against the requested DID. 13 13 14 14 // use crate::CowStr; // not currently needed directly here 15 + pub mod resolver; 15 16 17 + use crate::resolver::{ 18 + DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource, 19 + ResolverOptions, 20 + }; 16 21 use bytes::Bytes; 17 - use jacquard_common::IntoStatic; 22 + use jacquard_api::com_atproto::identity::resolve_did; 23 + use jacquard_api::com_atproto::identity::resolve_handle::ResolveHandle; 18 24 use jacquard_common::error::TransportError; 19 25 use jacquard_common::http_client::HttpClient; 20 - use jacquard_common::ident_resolver::{ 21 - DidDocResponse, DidStep, HandleStep, IdentityError, IdentityResolver, MiniDoc, PlcSource, 22 - ResolverOptions, 23 - }; 26 + use jacquard_common::types::did::Did; 27 + use jacquard_common::types::did_doc::DidDocument; 28 + use jacquard_common::types::ident::AtIdentifier; 24 29 use jacquard_common::types::xrpc::XrpcExt; 30 + use jacquard_common::{IntoStatic, types::string::Handle}; 25 31 use percent_encoding::percent_decode_str; 26 32 use reqwest::StatusCode; 27 33 use url::{ParseError, Url}; 28 - 29 - use crate::api::com_atproto::identity::{resolve_did, resolve_handle::ResolveHandle}; 30 - use crate::types::did_doc::DidDocument; 31 - use crate::types::ident::AtIdentifier; 32 - use crate::types::string::{Did, Handle}; 33 34 34 35 #[cfg(feature = "dns")] 35 36 use hickory_resolver::{TokioAsyncResolver, config::ResolverConfig};
+1 -2
crates/jacquard/src/lib.rs
··· 174 174 /// if enabled, reexport the attribute macros 175 175 pub use jacquard_derive::*; 176 176 177 - /// Identity resolution helpers (DIDs, handles, PDS endpoints) 178 - pub mod identity; 177 + pub use jacquard_identity as identity;
+1 -1
crates/jacquard/src/main.rs
··· 3 3 use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; 4 4 use jacquard::api::com_atproto::server::create_session::CreateSession; 5 5 use jacquard::client::{AtpSession, AuthSession, BasicClient}; 6 - use jacquard::ident_resolver::IdentityResolver; 6 + use jacquard::identity::resolver::IdentityResolver; 7 7 use jacquard::identity::slingshot_resolver_default; 8 8 use jacquard::types::string::Handle; 9 9 use miette::IntoDiagnostic;