A better Rust ATProto crate

bugfix for upload_blob()

Orual 1553db73 81c91caa

Changed files
+103 -38
crates
jacquard
jacquard-common
jacquard-oauth
+18
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + 4 + ## [0.5.4] - 2025-10-16 5 + 6 + ### Added 7 + 8 + **Initial streaming client support** (`jacquard-common`) 9 + - First primitives for streamed requests and responses 10 + 11 + **`send_with_options()` method on XrpcClient** (`jacquard-common`, `jacquard-oauth`, `jacquard`) 12 + - allows setting custom options per request in stateful client 13 + - updated oauth and credential session clients to use it 14 + - implementations should generally override provided auth with own internal auth 15 + 16 + ### Fixed 17 + 18 + **`AgentSessionExt::upload_blob()` failed to authenticate** (`jacquard`) 19 + - new `XrpcClient::send_with_options()` method now allows properly overriding the content-type header while still handling auth internally 20 + 3 21 ## [0.5.3] - 2025-10-15 4 22 5 23 ### Added
+14 -14
Cargo.lock
··· 1850 1850 1851 1851 [[package]] 1852 1852 name = "jacquard" 1853 - version = "0.5.3" 1853 + version = "0.5.4" 1854 1854 dependencies = [ 1855 1855 "bon", 1856 1856 "bytes", ··· 1858 1858 "getrandom 0.2.16", 1859 1859 "http", 1860 1860 "jacquard-api 0.5.3", 1861 - "jacquard-common 0.5.3", 1862 - "jacquard-derive 0.5.3", 1861 + "jacquard-common 0.5.4", 1862 + "jacquard-derive 0.5.4", 1863 1863 "jacquard-identity 0.5.3", 1864 1864 "jacquard-oauth", 1865 1865 "jose-jwk", ··· 1900 1900 dependencies = [ 1901 1901 "bon", 1902 1902 "bytes", 1903 - "jacquard-common 0.5.3", 1904 - "jacquard-derive 0.5.3", 1903 + "jacquard-common 0.5.4", 1904 + "jacquard-derive 0.5.4", 1905 1905 "miette", 1906 1906 "serde", 1907 1907 "thiserror 2.0.17", ··· 1918 1918 "bytes", 1919 1919 "chrono", 1920 1920 "jacquard", 1921 - "jacquard-common 0.5.3", 1922 - "jacquard-derive 0.5.3", 1921 + "jacquard-common 0.5.4", 1922 + "jacquard-derive 0.5.4", 1923 1923 "jacquard-identity 0.5.3", 1924 1924 "k256", 1925 1925 "miette", ··· 1973 1973 1974 1974 [[package]] 1975 1975 name = "jacquard-common" 1976 - version = "0.5.3" 1976 + version = "0.5.4" 1977 1977 dependencies = [ 1978 1978 "base64 0.22.1", 1979 1979 "bon", ··· 2029 2029 2030 2030 [[package]] 2031 2031 name = "jacquard-derive" 2032 - version = "0.5.3" 2032 + version = "0.5.4" 2033 2033 dependencies = [ 2034 - "jacquard-common 0.5.3", 2034 + "jacquard-common 0.5.4", 2035 2035 "proc-macro2", 2036 2036 "quote", 2037 2037 "serde", ··· 2071 2071 "hickory-resolver", 2072 2072 "http", 2073 2073 "jacquard-api 0.5.3", 2074 - "jacquard-common 0.5.3", 2074 + "jacquard-common 0.5.4", 2075 2075 "miette", 2076 2076 "percent-encoding", 2077 2077 "reqwest", ··· 2088 2088 2089 2089 [[package]] 2090 2090 name = "jacquard-lexicon" 2091 - version = "0.5.3" 2091 + version = "0.5.4" 2092 2092 dependencies = [ 2093 2093 "async-trait", 2094 2094 "clap", ··· 2116 2116 2117 2117 [[package]] 2118 2118 name = "jacquard-oauth" 2119 - version = "0.5.3" 2119 + version = "0.5.4" 2120 2120 dependencies = [ 2121 2121 "base64 0.22.1", 2122 2122 "bytes", ··· 2124 2124 "dashmap", 2125 2125 "elliptic-curve", 2126 2126 "http", 2127 - "jacquard-common 0.5.3", 2127 + "jacquard-common 0.5.4", 2128 2128 "jacquard-identity 0.5.3", 2129 2129 "jose-jwa", 2130 2130 "jose-jwk",
+1 -1
Cargo.toml
··· 5 5 6 6 [workspace.package] 7 7 edition = "2024" 8 - version = "0.5.3" 8 + version = "0.5.4" 9 9 authors = ["Orual <orual@nonbinary.computer>"] 10 10 #repository = "https://github.com/rsform/jacquard" 11 11 repository = "https://tangled.org/@nonbinary.computer/jacquard"
+3 -2
README.md
··· 104 104 105 105 Highlights: 106 106 107 + - initial streaming support 107 108 - experimental WASM support 108 109 - better value type deserialization helpers 109 110 - service auth implementation ··· 124 125 ``` 125 126 126 127 127 - ### Streaming Support 128 + ### Initial Streaming Support 128 129 129 - Jacquard supports efficient streaming for large payloads: 130 + Jacquard is building out support for efficient streaming for large payloads: 130 131 131 132 - **Blob uploads/downloads**: Stream media without loading into memory 132 133 - **CAR file streaming**: Efficient repo sync operations
+1 -1
crates/jacquard-common/Cargo.toml
··· 2 2 name = "jacquard-common" 3 3 description = "Core AT Protocol types and utilities for Jacquard" 4 4 edition.workspace = true 5 - version = "0.5.3" 5 + version = "0.5.4" 6 6 authors.workspace = true 7 7 repository.workspace = true 8 8 keywords.workspace = true
+23
crates/jacquard-common/src/xrpc.rs
··· 263 263 where 264 264 R: XrpcRequest + Send + Sync, 265 265 <R as XrpcRequest>::Response: Send + Sync; 266 + 267 + /// Send an XRPC request and parse the response 268 + #[cfg(not(target_arch = "wasm32"))] 269 + fn send_with_opts<R>( 270 + &self, 271 + request: R, 272 + opts: CallOptions<'_>, 273 + ) -> impl Future<Output = XrpcResult<Response<<R as XrpcRequest>::Response>>> 274 + where 275 + R: XrpcRequest + Send + Sync, 276 + <R as XrpcRequest>::Response: Send + Sync, 277 + Self: Sync; 278 + 279 + /// Send an XRPC request and parse the response 280 + #[cfg(target_arch = "wasm32")] 281 + fn send_with_opts<R>( 282 + &self, 283 + request: R, 284 + opts: CallOptions<'_>, 285 + ) -> impl Future<Output = XrpcResult<Response<<R as XrpcRequest>::Response>>> 286 + where 287 + R: XrpcRequest + Send + Sync, 288 + <R as XrpcRequest>::Response: Send + Sync; 266 289 } 267 290 268 291 /// Stateless XRPC call builder.
+1 -1
crates/jacquard-oauth/Cargo.toml
··· 1 1 [package] 2 2 name = "jacquard-oauth" 3 - version = "0.5.3" 3 + version = "0.5.4" 4 4 edition.workspace = true 5 5 description = "AT Protocol OAuth 2.1 core types and helpers for Jacquard" 6 6 authors.workspace = true
+15 -2
crates/jacquard-oauth/src/client.rs
··· 436 436 437 437 async fn send<R>(&self, request: R) -> XrpcResult<Response<<R as XrpcRequest>::Response>> 438 438 where 439 - R: XrpcRequest, 439 + R: XrpcRequest + Send + Sync, 440 + <R as XrpcRequest>::Response: Send + Sync, 441 + { 442 + let opts = self.options.read().await.clone(); 443 + self.send_with_opts(request, opts).await 444 + } 445 + 446 + async fn send_with_opts<R>( 447 + &self, 448 + request: R, 449 + mut opts: CallOptions<'_>, 450 + ) -> XrpcResult<Response<<R as XrpcRequest>::Response>> 451 + where 452 + R: XrpcRequest + Send + Sync, 453 + <R as XrpcRequest>::Response: Send + Sync, 440 454 { 441 455 let base_uri = self.base_uri(); 442 - let mut opts = self.options.read().await.clone(); 443 456 opts.auth = Some(self.access_token().await); 444 457 let guard = self.data.read().await; 445 458 let mut dpop = guard.dpop_data.clone();
+14 -16
crates/jacquard/src/client.rs
··· 321 321 repo::{ 322 322 create_record::CreateRecordOutput, delete_record::DeleteRecordOutput, 323 323 get_record::GetRecordResponse, put_record::PutRecordOutput, 324 - upload_blob::UploadBlobResponse, 325 324 }, 326 325 server::{create_session::CreateSessionOutput, refresh_session::RefreshSessionOutput}, 327 326 }; ··· 738 737 let request = UploadBlob::new().body(bytes).build(); 739 738 740 739 // Override Content-Type header with actual mime type instead of */* 741 - let base = self.base_uri(); 742 740 let mut opts = self.opts().await; 741 + 743 742 opts.extra_headers.push(( 744 743 CONTENT_TYPE, 745 744 http::HeaderValue::from_str(mime_type.as_str()).map_err(|e| { ··· 749 748 } 750 749 })?, 751 750 )); 752 - 753 - let response: Response<UploadBlobResponse> = { 754 - let http_request = 755 - xrpc::build_http_request(&base, &request, &opts).map_err(|e| { 756 - AgentError::Client(ClientError::Transport(TransportError::from(e))) 757 - })?; 758 - 759 - let http_response = self.send_http(http_request).await.map_err(|e| { 760 - AgentError::Client(ClientError::Transport(TransportError::Other(Box::new(e)))) 761 - })?; 762 - 763 - xrpc::process_response(http_response) 764 - }?; 765 - 751 + let response = self.send_with_opts(request, opts).await?; 766 752 let output = response.into_output().map_err(|e| match e { 767 753 XrpcError::Auth(auth) => AgentError::Auth(auth), 768 754 XrpcError::Generic(g) => AgentError::Generic(g), ··· 912 898 <R as XrpcRequest>::Response: Send + Sync, 913 899 { 914 900 async move { self.inner.send(request).await } 901 + } 902 + 903 + async fn send_with_opts<R>( 904 + &self, 905 + request: R, 906 + opts: CallOptions<'_>, 907 + ) -> XrpcResult<Response<<R as XrpcRequest>::Response>> 908 + where 909 + R: XrpcRequest + Send + Sync, 910 + <R as XrpcRequest>::Response: Send + Sync, 911 + { 912 + self.inner.send_with_opts(request, opts).await 915 913 } 916 914 } 917 915
+13 -1
crates/jacquard/src/client/credential_session.rs
··· 431 431 R: XrpcRequest + Send + Sync, 432 432 <R as XrpcRequest>::Response: Send + Sync, 433 433 { 434 + let opts = self.options.read().await.clone(); 435 + self.send_with_opts(request, opts).await 436 + } 437 + 438 + async fn send_with_opts<R>( 439 + &self, 440 + request: R, 441 + mut opts: CallOptions<'_>, 442 + ) -> XrpcResult<Response<<R as XrpcRequest>::Response>> 443 + where 444 + R: XrpcRequest + Send + Sync, 445 + <R as XrpcRequest>::Response: Send + Sync, 446 + { 434 447 let base_uri = self.base_uri(); 435 448 let auth = self.access_token().await; 436 - let mut opts = self.options.read().await.clone(); 437 449 opts.auth = auth; 438 450 let resp = self 439 451 .client