A better Rust ATProto crate
103
fork

Configure Feed

Select the types of activity you want to include in your feed.

moved identity resolution stuff into its own crate

Orual 102d6666 36ef115b

+134 -81
+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;