this repo has no description

readme updates, new codegen binaries, llms.txt

Orual b8978f16 b65973b5

+34 -22
README.md
··· 8 8 9 9 It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://tangled.org/@nonbinary.computer/jacquard/blob/main/crates/jacquard-api/src/app_bsky/feed/post.rs) can borrow data (via the [`CowStr<'_>`](https://docs.rs/jacquard/latest/jacquard/cowstr/enum.CowStr.html) type and a host of other types built on top of it) directly from the response buffer instead of allocating owned copies. Owned versions are themselves mostly inlined or reference-counted pointers and are therefore still quite efficient. The `IntoStatic` trait (which is derivable) makes it easy to get an owned version and avoid worrying about lifetimes. 10 10 11 + ## Features 12 + 13 + - Validated, spec-compliant, easy to work with, and performant baseline types 14 + - Designed such that you can just work with generated API bindings easily 15 + - Straightforward OAuth 16 + - Server-side convenience features 17 + - Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 18 + - An order of magnitude less boilerplate than some existing crates 19 + - Batteries-included, but easily replaceable batteries. 20 + - Easy to extend with custom lexicons using code generation or handwritten api types 21 + - Stateless options (or options where you handle the state) for rolling your own 22 + - All the building blocks of the convenient abstractions are available 23 + - Use as much or as little from the crates as you need 24 + 11 25 ## 0.8.0 Release Highlights: 12 26 13 27 **`jacquard-repo` crate** ··· 17 31 - CAR file write order compatible with streaming mode from the [sync iteration proposal](https://github.com/bluesky-social/proposals/blob/main/0006-sync-iteration/README.md#streaming-car-processing) 18 32 - Big rewrite of all the errors in the crate, improvements to context and overall structure 19 33 - Made handle parsing a bit more permissive for a common case ('handle.invalid' when someone has a messed up handle), added a method to confirm syntactic validity (the correct way to confirm validity is resolve_handle() from the IdentityResolver trait, then fetching and comparing to the DID document). 20 - 21 - > [!WARNING] 22 - > A lot of the streaming code is still pretty experimental. The examples work, though.\ 23 - The modules are also less well-documented, and don't have code examples. There are also a lot of utility functions for conveniently working with the streams and transforming them which are lacking. Use [`n0-future`](https://docs.rs/n0-future/latest/n0_future/index.html) to work with them, that is what Jacquard uses internally as much as possible.\ 24 - >I would also note the same for the repository crate until I've had more third parties test it. 25 - 26 - ### Changelog 27 - 28 - [CHANGELOG.md](./CHANGELOG.md) 29 - 30 - ## Goals and Features 31 - 32 - - Validated, spec-compliant, easy to work with, and performant baseline types 33 - - Batteries-included, but easily replaceable batteries. 34 - - Easy to extend with custom lexicons using code generation or handwritten api types 35 - - Straightforward OAuth 36 - - Stateless options (or options where you handle the state) for rolling your own 37 - - All the building blocks of the convenient abstractions are available 38 - - Server-side convenience features 39 - - Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 40 - - An order of magnitude less boilerplate than some existing crates 41 - - Use as much or as little from the crates as you need 42 34 43 35 ## Example 44 36 ··· 100 92 ``` 101 93 102 94 If you have `just` installed, you can run the [examples](https://tangled.org/@nonbinary.computer/jacquard/tree/main/examples) using `just example {example-name} {ARGS}` or `just examples` to see what's available. 95 + 96 + > [!WARNING] 97 + > A lot of the streaming code is still pretty experimental. The examples work, though.\ 98 + The modules are also less well-documented, and don't have code examples. There are also a lot of utility functions for conveniently working with the streams and transforming them which are lacking. Use [`n0-future`](https://docs.rs/n0-future/latest/n0_future/index.html) to work with them, that is what Jacquard uses internally as much as possible.\ 99 + >I would also note the same for the repository crate until I've had more third parties test it. 100 + 101 + ### Changelog 102 + 103 + [CHANGELOG.md](./CHANGELOG.md) 104 + 105 + <!--### Testimonials 106 + 107 + - ["the most straightforward interface to atproto I've encountered so far."](https://bsky.app/profile/offline.mountainherder.xyz/post/3m3xwewzs3k2v) - @offline.mountainherder.xyz 108 + 109 + - "It has saved me a lot of time already! Well worth a few beers and or microcontrollers" - [@baileytownsend.dev](https://bsky.app/profile/baileytownsend.dev)--> 110 + 111 + ### Projects using Jacquard 112 + 113 + - [skywatch-phash-rs](https://tangled.org/@skywatch.blue/skywatch-phash-rs) 114 + - [PDS MOOver](https://pdsmoover.com/) - [tangled repository](https://tangled.org/@baileytownsend.dev/pds-moover) 103 115 104 116 ## Component crates 105 117
+6 -6
binaries/SHA256SUMS
··· 1 - 1cca585492c14f4fbf2b913bb00023523a95aa0736696b5328e72c090fc7d356 jacquard-codegen_aarch64-unknown-linux-gnu.tar.xz 2 - a55847e84237c5775ab17f2079ce263efc3670d2ad15fb5b660dde40051ef8d6 jacquard-codegen_x86_64-unknown-linux-gnu.tar.xz 3 - e232a64747a107286311de80e4d868ad51c1515f9bc45a13cd71f4494a0e0fd4 lex-fetch_aarch64-unknown-linux-gnu.tar.xz 4 - f52f5e5cc78ef95747e26ba3583f36ad2d05ba040382593b9d4d85544b1d2789 lex-fetch_x86_64-unknown-linux-gnu.tar.xz 5 - cc9c53669e2d5532589d83bca168182eab049675ac5f0e59612add18e9e587b2 jacquard-codegen_x86_64-pc-windows-gnu.zip 6 - de3b47c2f0c76543d96cebf4bac6d5468a06cac79a5997473fdbfc92a559c2c3 lex-fetch_x86_64-pc-windows-gnu.zip 1 + 53587765038d68a813f9d270ea478b03f30e573222760486969106bec14c9283 jacquard-codegen_aarch64-unknown-linux-gnu.tar.xz 2 + 74692d2ab91869681a32a9d25fb6d48b9461ff490d5fc289536f739f8f277cd9 jacquard-codegen_x86_64-unknown-linux-gnu.tar.xz 3 + 948c675059875880468cda4c35ea2bad1bf9e8a0310d47e32e62f60b925a6cdd lex-fetch_aarch64-unknown-linux-gnu.tar.xz 4 + 451e5361280f059c269827c0272431e604d3473507897239753171e249837323 lex-fetch_x86_64-unknown-linux-gnu.tar.xz 5 + 5d5adab46222e0f1fd5f541213991c14b7b1ece4b22cd62b5ab5e1e0d6c65eca jacquard-codegen_x86_64-pc-windows-gnu.zip 6 + 3123702603ffea860151fcec913422cf4fa102d4961e85319d50a07a98705bcf lex-fetch_x86_64-pc-windows-gnu.zip
binaries/jacquard-codegen_aarch64-unknown-linux-gnu.tar.xz

This is a binary file and will not be displayed.

binaries/jacquard-codegen_x86_64-pc-windows-gnu.zip

This is a binary file and will not be displayed.

binaries/jacquard-codegen_x86_64-unknown-linux-gnu.tar.xz

This is a binary file and will not be displayed.

binaries/lex-fetch_aarch64-unknown-linux-gnu.tar.xz

This is a binary file and will not be displayed.

binaries/lex-fetch_x86_64-pc-windows-gnu.zip

This is a binary file and will not be displayed.

binaries/lex-fetch_x86_64-unknown-linux-gnu.tar.xz

This is a binary file and will not be displayed.

+7 -7
crates/jacquard/src/lib.rs
··· 9 9 //! It is also designed around zero-copy/borrowed deserialization: types like [`Post<'_>`](https://docs.rs/jacquard-api/latest/jacquard_api/app_bsky/feed/post/struct.Post.html) can borrow data (via the [`CowStr<'_>`](https://docs.rs/jacquard/latest/jacquard/cowstr/enum.CowStr.html) type and a host of other types built on top of it) directly from the response buffer instead of allocating owned copies. Owned versions are themselves mostly inlined or reference-counted pointers and are therefore still quite efficient. The `IntoStatic` trait (which is derivable) makes it easy to get an owned version and avoid worrying about lifetimes. 10 10 //! 11 11 //! 12 - //! ## Goals and Features 12 + //! ## Features 13 13 //! 14 14 //! - Validated, spec-compliant, easy to work with, and performant baseline types 15 + //! - Designed such that you can just work with generated API bindings easily 16 + //! - Straightforward OAuth 17 + //! - Server-side convenience features 18 + //! - Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 19 + //! - An order of magnitude less boilerplate than some existing crates 15 20 //! - Batteries-included, but easily replaceable batteries. 16 21 //! - Easy to extend with custom lexicons using code generation or handwritten api types 17 - //! - Straightforward OAuth 18 22 //! - Stateless options (or options where you handle the state) for rolling your own 19 23 //! - All the building blocks of the convenient abstractions are available 20 - //! - Server-side convenience features 21 - //! - Lexicon Data value type for working with unknown atproto data (dag-cbor or json) 22 - //! - An order of magnitude less boilerplate than some existing crates 23 - //! - Use as much or as little from the crates as you need 24 - //! 24 + //! - Use as much or as little from the crates as you need 25 25 //! 26 26 //! 27 27 //!
+1114
llms.txt
··· 1 + # Jacquard: AT Protocol Library for Rust 2 + 3 + > Simple and powerful AT Protocol (Bluesky) client library emphasizing spec compliance, zero-copy deserialization, and minimal boilerplate. 4 + 5 + ## Core Philosophy 6 + 7 + **Zero-copy by default, owned when needed.** All API types support borrowed deserialization via lifetimes, with `IntoStatic` trait for conversion to `'static` when needed. This avoids the performance penalty of `DeserializeOwned` while giving you control over ownership. 8 + 9 + **Validated types everywhere.** DIDs, handles, AT-URIs, NSIDs, TIDs, CIDs—all have strongly-typed, validated wrappers. Invalid inputs fail at construction time, not deep in your application logic. 10 + 11 + **Batteries included, but replaceable.** High-level `Agent` for convenience, or use stateless `XrpcCall` builder for full control. Mix and match as needed. 12 + 13 + --- 14 + 15 + ## Critical Patterns to Internalize 16 + 17 + ### String Type Constructors 18 + 19 + ALL validated string types (Did, Handle, AtUri, Nsid, etc.) follow this pattern: 20 + 21 + ```rust 22 + // ✅ PREFERRED: Zero-allocation for borrowed strings 23 + let did = Did::new("did:plc:abc123")?; // Borrows from input 24 + 25 + // ✅ BEST: Zero-allocation for static strings 26 + let nsid = Nsid::new_static("com.atproto.repo.getRecord")?; 27 + 28 + // ✅ When you need ownership 29 + let owned = Did::new_owned("did:plc:abc123")?; 30 + 31 + // ❌ AVOID: FromStr always allocates 32 + let did: Did = "did:plc:abc123".parse()?; // Always allocates! 33 + 34 + // ❌ NEVER: Roundtripping through String 35 + let s = did.as_str().to_string(); 36 + let did2 = Did::new(&s)?; // Pointless allocation 37 + ``` 38 + 39 + **Rule**: Use `new()` for borrowed, `new_static()` for `'static` strings, `new_owned()` when you have ownership. Avoid `FromStr::parse()` unless you don't care about allocations. 40 + 41 + ### Response Parsing: Borrow vs Own 42 + 43 + XRPC responses wrap a `Bytes` buffer. You choose when to parse and whether to own the data: 44 + 45 + ```rust 46 + let response = agent.send(request).await?; 47 + 48 + // Option 1: Borrow from response buffer (zero-copy) 49 + let output: GetPostOutput<'_> = response.parse()?; 50 + // ⚠️ `output` borrows from `response`, both must stay in scope 51 + 52 + // Option 2: Convert to owned (allocates, but can outlive response) 53 + let output: GetPostOutput<'static> = response.into_output()?; 54 + drop(response); // OK, output is now fully owned 55 + 56 + // ❌ WRONG: Can't drop response while holding borrowed parse 57 + let output = response.parse()?; 58 + drop(response); // ERROR: output borrows from response 59 + ``` 60 + 61 + **Rule**: Use `.parse()` when processing immediately in the same scope. Use `.into_output()` when returning from functions or storing long-term. 62 + 63 + ### Lifetime Pattern: GATs Not HRTBs 64 + 65 + Jacquard uses **Generic Associated Types** on `XrpcResp` to avoid Higher-Rank Trait Bounds: 66 + 67 + ```rust 68 + // ✅ Jacquard's approach (GAT) 69 + trait XrpcResp { 70 + type Output<'de>: Deserialize<'de> + IntoStatic; 71 + type Err<'de>: Error + Deserialize<'de> + IntoStatic; 72 + } 73 + 74 + // ❌ Alternative that forces DeserializeOwned semantics 75 + trait BadXrpcResp<'de> { 76 + type Output: Deserialize<'de>; 77 + } 78 + // Would require: where R: for<'any> BadXrpcResp<'any> 79 + // This forces owned deserialization! 80 + ``` 81 + 82 + **Why this matters**: Async methods return `Response<R>` that owns the buffer. Caller controls lifetime by choosing `.parse()` (borrow) or `.into_output()` (owned). No HRTB needed, zero-copy works in async contexts. 83 + 84 + **When implementing custom types**: Use method-level lifetime generics (`<'de>`), not trait-level lifetimes. 85 + 86 + ### CRITICAL: Never Use `for<'de> Deserialize<'de>` Bounds 87 + 88 + **The bound `T: for<'de> Deserialize<'de>` is EQUIVALENT to `T: DeserializeOwned`** and will break all Jacquard types. 89 + 90 + ```rust 91 + // ❌ CATASTROPHIC - No Jacquard type can satisfy this 92 + fn bad<T>(data: &[u8]) -> Result<T> 93 + where 94 + T: for<'de> Deserialize<'de> // Forces owned deserialization! 95 + { 96 + serde_json::from_slice(data) // Can't borrow from data 97 + } 98 + 99 + // ✅ CORRECT - Use method-level lifetime 100 + fn good<'de, T>(data: &'de [u8]) -> Result<T> 101 + where 102 + T: Deserialize<'de> // Can borrow from data 103 + { 104 + serde_json::from_slice(data) 105 + } 106 + ``` 107 + 108 + **Why this breaks**: `CowStr<'a>` and all Jacquard types need to borrow from the input buffer. The HRTB `for<'de>` means "must work with ANY lifetime", which forces the deserializer to allocate owned copies instead of borrowing. 109 + 110 + **What to do instead**: Always use method-level lifetime parameters (`<'de>`) and pass the lifetime through to the `Deserialize` bound. Jacquard's entire design (GATs, Response wrapper, IntoStatic) exists to make this pattern work in async contexts. 111 + 112 + --- 113 + 114 + ## Crate-by-Crate Guide 115 + 116 + ### jacquard (Main Crate) 117 + 118 + **Primary entry point**: `Agent<A: AgentSession>` 119 + 120 + ```rust 121 + use jacquard::client::{Agent, CredentialSession, MemorySessionStore}; 122 + use jacquard::identity::PublicResolver; 123 + 124 + // App password auth 125 + let (session, _info) = CredentialSession::authenticated( 126 + "alice.bsky.social".into(), 127 + "app-password".into(), 128 + None, // session_id 129 + ).await?; 130 + let agent = Agent::from(session); 131 + 132 + // Make typed XRPC calls 133 + use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; 134 + let response = agent.send(&GetTimeline::new().limit(50).build()).await?; 135 + let timeline = response.into_output()?; 136 + ``` 137 + 138 + **Auto-refresh**: Both `CredentialSession` and `OAuthSession` automatically refresh tokens on 401/expired errors. One retry per request. 139 + 140 + **Typed record operations** (via `AgentSessionExt` trait): 141 + 142 + ```rust 143 + use jacquard::api::app_bsky::feed::post::Post; 144 + 145 + // Create 146 + let post = Post::builder() 147 + .text("Hello ATProto!") 148 + .created_at(Datetime::now()) 149 + .build(); 150 + agent.create_record(post, None).await?; 151 + 152 + // Get (type-safe!) 153 + let uri = Post::uri("at://did:plc:abc/app.bsky.feed.post/123")?; 154 + let response = agent.get_record::<Post>(&uri).await?; 155 + let post_output = response.parse()?; 156 + 157 + // Update with fetch-modify-put pattern 158 + agent.update_record::<Profile>(&uri, |profile| { 159 + profile.display_name = Some("New Name".into()); 160 + }).await?; 161 + ``` 162 + 163 + **Key traits**: 164 + - `AgentSession`: Common interface for both auth types 165 + - `XrpcClient`: Stateful XRPC (has base URI, auth tokens) 166 + - `HttpClient`: Low-level HTTP abstraction 167 + 168 + **Common mistake**: Not converting to owned when returning from functions. If your function returns `Post<'_>`, caller can't use it after function returns. Return `Post<'static>` and call `.into_static()` on the value. 169 + 170 + ### jacquard-common (Foundation) 171 + 172 + **Core types**: `Did`, `Handle`, `AtUri`, `Nsid`, `Tid`, `Cid`, `CowStr`, `Data`, `RawData` 173 + 174 + **String type traits** (ALL validated types implement these): 175 + - `new(&str)` - Validates, borrows (zero alloc) 176 + - `new_static(&'static str)` - Validates, zero alloc 177 + - `new_owned(impl Into<String>)` - Validates, takes ownership 178 + - `raw(&str)` - Panics on invalid (use when you KNOW it's valid) 179 + - `unchecked(&str)` - Unsafe, no validation 180 + - `as_str(&self) -> &str` - Get string reference 181 + - `Display`, `FromStr`, `Serialize`, `Deserialize`, `IntoStatic` 182 + 183 + **CowStr internals**: Uses `SmolStr` for owned variant (inline storage ≤23 bytes), so most AT Protocol strings (handles, DIDs) don't heap allocate when owned, or are O(1) copy (due to the allocated variant using Arc<str>). 184 + 185 + **XRPC layer**: 186 + 187 + ```rust 188 + // Stateless XRPC (with any HttpClient) 189 + use jacquard::common::xrpc::XrpcExt; 190 + let http = reqwest::Client::new(); 191 + let response = http 192 + .xrpc(Url::parse("https://bsky.social")?) 193 + .auth(AuthorizationToken::Bearer(token)) 194 + .proxy(did) 195 + .send(&request) 196 + .await?; 197 + 198 + // Stateful XRPC (implement XrpcClient trait) 199 + let response = agent.send(request).await?; 200 + ``` 201 + 202 + **Data vs RawData**: 203 + - `Data<'a>`: Validated, type-inferred atproto values (strings parsed to Did/Handle/etc.) 204 + - `RawData<'a>`: Minimal validation, suitable for pass-through/relay use cases 205 + 206 + ```rust 207 + // Convert typed → untyped → typed 208 + let post = Post::builder().text("test").build(); 209 + let data: Data = to_data(&post)?; 210 + let post2: Post = from_data(&data)?; 211 + 212 + // NEVER use serde_json::Value 213 + // ❌ let value: serde_json::Value = ...; 214 + // ✅ let data: Data = ...; 215 + ``` 216 + 217 + **Streaming** (feature: `streaming`): 218 + - `ByteStream` / `ByteSink`: Platform-agnostic (works on WASM via `n0-future`) 219 + - `HttpClientExt::send_http_streaming()`: Stream response 220 + - `HttpClientExt::send_http_bidirectional()`: Stream both request and response 221 + 222 + **WebSocket** (feature: `websocket`): 223 + - `WebSocketClient` trait, `WebSocketConnection` 224 + - Native + WASM: tokio-tungstenite-wasm 225 + 226 + **Collection trait** (for record types): 227 + 228 + ```rust 229 + pub trait Collection { 230 + const NSID: &'static str; 231 + type Record: XrpcResp; // Marker type for get_record() 232 + } 233 + 234 + // Enables typed record retrieval: 235 + let response: Response<Post::Record> = agent.get_record(did, rkey).await?; 236 + ``` 237 + 238 + **Critical error**: Using types that don't implement `IntoStatic` in response positions. All generated API types do, but custom types need `#[derive(jacquard_derive::IntoStatic)]`. 239 + 240 + ### jacquard-api (Generated Bindings) 241 + 242 + **764 lexicon schemas** across 52+ namespaces. 243 + 244 + **Feature organization**: 245 + - `minimal`: Core atproto only 246 + - `bluesky`: Bluesky app + chat + ozone 247 + - `other`: Curated third-party lexicons 248 + - `lexicon_community`: Community extensions 249 + - `ufos`: Experimental/niche 250 + 251 + **Generated patterns**: 252 + - All types have `'a` lifetime for zero-copy deserialization 253 + - All implement `IntoStatic`, `Serialize`, `Deserialize`, `Clone`, `PartialEq`, `Eq` 254 + - Builders (`bon::Builder`) on types with 2+ fields (some required) 255 + - Open unions have `Unknown(Data<'a>)` variant via `#[open_union]` macro 256 + - Objects have `extra_data: BTreeMap<SmolStr, Data<'a>>` via `#[lexicon]` macro 257 + 258 + **Union collision detection**: When multiple namespaces define similar types, foreign refs get prefixed: 259 + ```rust 260 + // If both app.bsky.embed.images and sh.custom.embed.images exist: 261 + pub enum SomeUnion<'a> { 262 + BskyImages(Box<app_bsky::embed::images::View<'a>>), 263 + CustomImages(Box<sh_custom::embed::images::View<'a>>), 264 + } 265 + ``` 266 + 267 + **For each collection (record type)**, generated code includes: 268 + 1. Main record struct (e.g., `Post<'a>`) 269 + 2. `GetRecordOutput` wrapper (`PostGetRecordOutput<'a>` with uri, cid, value) 270 + 3. Marker struct (`PostRecord`) implementing `XrpcResp` 271 + 4. `Collection` trait impl 272 + 5. Helper: `Post::uri()` for constructing typed URIs 273 + 274 + **For each XRPC endpoint**: 275 + 1. Request struct with builder 276 + 2. Output struct 277 + 3. Error enum (open union, includes `Unknown` variant) 278 + 4. Response marker implementing `XrpcResp` 279 + 5. Request marker implementing `XrpcRequest` 280 + 6. Endpoint marker implementing `XrpcEndpoint` (server-side) 281 + 282 + **Common mistakes**: 283 + - Not handling `Unknown` variant in union matches (non-exhaustive!) 284 + - Forgetting that when not using the builder pattern, or Default construction, you must supply `extra_data: BTreeMap::new()` in the constructor, in addition to explicitly named fields. 285 + - Calling `.into_static()` in tight loops (it clones all borrowed data) 286 + 287 + ### jacquard-derive (Macros) 288 + 289 + **`#[lexicon]`**: Adds `extra_data` field to capture unknown fields during deserialization. 290 + 291 + ```rust 292 + #[lexicon] 293 + #[derive(Serialize, Deserialize)] 294 + struct MyType<'a> { 295 + known_field: CowStr<'a>, 296 + // Macro adds: pub extra_data: BTreeMap<SmolStr, Data<'a>> 297 + } 298 + ``` 299 + 300 + With `bon::Builder`: Automatically adds `#[builder(default)]` to `extra_data`. 301 + 302 + **`#[open_union]`**: Adds `Unknown(Data<'a>)` variant to enums. 303 + 304 + ```rust 305 + #[open_union] 306 + #[serde(tag = "$type")] 307 + enum MyUnion<'a> { 308 + KnownVariant(Foo<'a>), 309 + // Macro adds: #[serde(untagged)] Unknown(Data<'a>) 310 + } 311 + ``` 312 + 313 + **`#[derive(IntoStatic)]`**: Generates owned conversion. 314 + 315 + ```rust 316 + #[derive(IntoStatic)] 317 + struct Post<'a> { 318 + text: CowStr<'a>, 319 + likes: u32, 320 + } 321 + 322 + // Generates: 323 + impl IntoStatic for Post<'_> { 324 + type Output = Post<'static>; 325 + fn into_static(self) -> Post<'static> { 326 + Post { 327 + text: self.text.into_static(), // Converts to owned 328 + likes: self.likes.into_static(), // Passthrough (Copy) 329 + } 330 + } 331 + } 332 + ``` 333 + 334 + **`#[derive(XrpcRequest)]`**: Generates XRPC boilerplate for custom endpoints. 335 + 336 + ```rust 337 + #[derive(Serialize, Deserialize, XrpcRequest)] 338 + #[xrpc( 339 + nsid = "com.example.getThing", 340 + method = Query, 341 + output = GetThingOutput, 342 + error = GetThingError, // Optional, defaults to GenericError 343 + server // Optional, generates XrpcEndpoint marker 344 + )] 345 + struct GetThing<'a> { 346 + #[serde(borrow)] 347 + pub id: CowStr<'a>, 348 + } 349 + ``` 350 + 351 + **Critical**: All custom types with lifetimes MUST derive `IntoStatic` or manually implement it. Otherwise, you can't use them with `.into_output()`. 352 + 353 + ### jacquard-oauth (OAuth/DPoP) 354 + 355 + **OAuth flow**: 356 + 357 + ```rust 358 + use jacquard::oauth::client::OAuthClient; 359 + use jacquard::client::FileAuthStore; 360 + 361 + let oauth = OAuthClient::with_default_config( 362 + FileAuthStore::new("./auth.json") 363 + ); 364 + 365 + // Loopback flow (feature: loopback) 366 + let session = oauth.login_with_local_server( 367 + "alice.bsky.social", 368 + Default::default(), 369 + LoopbackConfig::default(), 370 + ).await?; 371 + 372 + let agent = Agent::from(session); 373 + ``` 374 + 375 + **DPoP proofs**: Automatically generated for every request. Include: 376 + - `jti`: Unique token ID (random) 377 + - `htm`: HTTP method 378 + - `htu`: Target URI 379 + - `iat`: Issued at timestamp 380 + - `nonce`: Server-provided (cached and retried on `use_dpop_nonce` error) 381 + - `ath`: SHA-256 hash of access token (when present) 382 + 383 + **Nonce handling**: Automatic retry on `use_dpop_nonce` errors (400 for auth server, 401 for PDS). Max one retry per request. 384 + 385 + **Nonce storage**: 386 + - `dpop_authserver_nonce`: For token endpoint 387 + - `dpop_host_nonce`: For PDS XRPC requests 388 + 389 + **Token refresh**: Automatic on `invalid_token` errors. Uses `SessionRegistry` with per-DID+session_id locks to prevent concurrent refresh races. 390 + 391 + **private_key_jwt**: For non-loopback clients. Automatically used if server supports it. 392 + 393 + **Issuer verification**: ALWAYS verify issuer has authority over the DID before trusting tokens. This is a **critical security check**. 394 + 395 + ```rust 396 + // In callback flow, after exchanging code: 397 + let token_response = exchange_code(...).await?; 398 + // ⚠️ MUST verify before using token 399 + let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?; 400 + ``` 401 + 402 + **Common mistakes**: 403 + - Skipping issuer verification (security vulnerability!) 404 + - Not updating session after refresh (next request uses expired token) 405 + - Reusing DPoP proofs across requests (each request needs a fresh proof with new jti/iat) 406 + - Mixing auth server and host nonces (they're tracked separately) 407 + - Forgetting `ath` claim when including access token in PDS requests 408 + 409 + ### jacquard-identity (Identity Resolution) 410 + 411 + **Resolution chains** (configurable fallback order): 412 + 413 + **Handle → DID**: 414 + 1. DNS TXT `_atproto.<handle>` (feature: `dns`, skipped on WASM) 415 + 2. HTTPS `https://<handle>/.well-known/atproto-did` 416 + 3. PDS XRPC `com.atproto.identity.resolveHandle` 417 + 4. Public API fallback `https://public.api.bsky.app` (if enabled) 418 + 5. Slingshot mini-doc (if configured) 419 + 420 + **DID → Document**: 421 + 1. `did:web`: HTTPS `.well-known/did.json` 422 + 2. `did:plc`: PLC directory or Slingshot 423 + 3. PDS XRPC `com.atproto.identity.resolveDid` 424 + 425 + ```rust 426 + use jacquard::identity::{JacquardResolver, PublicResolver}; 427 + 428 + let resolver = PublicResolver::default(); // DNS + public fallbacks enabled 429 + 430 + // Handle → DID 431 + let did: Did<'static> = resolver.resolve_handle(&handle).await?; 432 + 433 + // DID → Document 434 + let response = resolver.resolve_did_doc(&did).await?; 435 + let doc = response.parse_validated()?; // Validates doc.id matches requested DID 436 + 437 + // Combined: Get PDS endpoint 438 + let pds_url = resolver.pds_for_did(&did).await?; 439 + ``` 440 + 441 + **DidDocResponse pattern** (same as XRPC responses): 442 + 443 + ```rust 444 + let response = resolver.resolve_did_doc(&did).await?; 445 + 446 + // Borrow from buffer 447 + let doc: DidDocument<'_> = response.parse()?; 448 + 449 + // Validate doc ID 450 + let doc = response.parse_validated()?; // Error if doc.id != requested DID 451 + 452 + // Convert to owned 453 + let doc: DidDocument<'static> = response.into_owned()?; 454 + ``` 455 + 456 + **Mini-doc fallback**: If full DID document parsing fails, automatically tries parsing as `MiniDoc` (Slingshot's minimal format) and synthesizes a minimal `DidDocument`. This is transparent to caller. 457 + 458 + **OAuthResolver trait**: Auto-implemented for `JacquardResolver`. Adds OAuth metadata resolution: 459 + 460 + ```rust 461 + // High-level: accepts handle, DID, or HTTPS URL 462 + let (server_metadata, doc_opt) = resolver.resolve_oauth("alice.bsky.social").await?; 463 + 464 + // From identity (handle or DID) 465 + let (server_metadata, doc) = resolver.resolve_from_identity("alice.bsky.social").await?; 466 + 467 + // From service URL (PDS or entryway) 468 + let server_metadata = resolver.resolve_from_service(&pds_url).await?; 469 + 470 + // Verify issuer authority over DID 471 + let pds = resolver.verify_issuer(&server_metadata, &sub_did).await?; 472 + ``` 473 + 474 + **Common mistakes**: 475 + - Not validating doc ID (use `.parse_validated()`, not just `.parse()`) 476 + - Assuming DNS resolution works on WASM (it doesn't) 477 + - Comparing issuer URLs with string equality (use `issuer_equivalent()` for trailing slash tolerance) 478 + - Trusting DID documents without checking `alsoKnownAs` for handle aliases 479 + - Not caching resolver (it's `Clone`, cheap to share) 480 + 481 + ### jacquard-axum (Server-Side) 482 + 483 + **ExtractXrpc**: Type-safe XRPC request extraction. 484 + 485 + ```rust 486 + use jacquard_axum::{ExtractXrpc, IntoRouter}; 487 + use jacquard::api::com_atproto::identity::resolve_handle::ResolveHandleRequest; 488 + 489 + async fn handle_resolve( 490 + ExtractXrpc(req): ExtractXrpc<ResolveHandleRequest> 491 + ) -> Json<ResolveHandleOutput<'static>> { 492 + let did = resolve_handle_logic(&req.handle).await; 493 + Json(ResolveHandleOutput { did, extra_data: Default::default() }) 494 + } 495 + 496 + // Automatic routing 497 + let app = Router::new() 498 + .merge(ResolveHandleRequest::into_router(handle_resolve)); 499 + ``` 500 + 501 + **Query vs Procedure**: 502 + - Query (GET): Extracts from query string via `serde_html_form` 503 + - Procedure (POST): Calls `Request::decode_body()` (default: JSON, override for CBOR) 504 + 505 + **Zero-copy → owned conversion**: Extractor borrows during deserialization, then converts to `'static` via `IntoStatic`. This is why all request types must implement `IntoStatic`. 506 + 507 + **Custom encodings**: 508 + 509 + ```rust 510 + impl XrpcRequest for MyRequest<'_> { 511 + fn decode_body<'de>(body: &'de [u8]) -> Result<Box<Self>> { 512 + let req = serde_ipld_dagcbor::from_slice(body)?; 513 + Ok(Box::new(req)) 514 + } 515 + } 516 + ``` 517 + 518 + **Service auth** (feature: `service-auth`): 519 + 520 + ```rust 521 + use jacquard_axum::service_auth::{ServiceAuthConfig, ExtractServiceAuth}; 522 + 523 + let config = ServiceAuthConfig::new( 524 + Did::new_static("did:web:feedgen.example.com")?, 525 + resolver, 526 + ); 527 + 528 + async fn handler( 529 + ExtractServiceAuth(auth): ExtractServiceAuth, 530 + ) -> String { 531 + format!("Authenticated as {}", auth.did()) 532 + } 533 + 534 + let app = Router::new() 535 + .route("/xrpc/app.bsky.feed.getFeedSkeleton", get(handler)) 536 + .with_state(config); 537 + ``` 538 + 539 + **Method binding (`lxm` claim)**: Default enabled. Binds JWTs to specific XRPC methods to prevent token reuse across endpoints. 540 + 541 + **JTI replay protection**: NOT built-in. You must implement: 542 + 543 + ```rust 544 + if let Some(jti) = auth.jti() { 545 + if state.seen_jtis.contains(jti) { 546 + return Err(StatusCode::UNAUTHORIZED); 547 + } 548 + state.seen_jtis.insert(jti.to_string(), auth.exp); 549 + } 550 + ``` 551 + 552 + **Common mistakes**: 553 + - Forgetting `IntoStatic` derive on custom request types 554 + - Using wrong trait (use `XrpcEndpoint` marker, not `XrpcRequest`) 555 + - Not implementing JTI tracking (allows replay attacks) 556 + - Disabling method binding without understanding security implications 557 + 558 + ### jacquard-repo (Repository Primitives) 559 + 560 + **MST (Merkle Search Tree)**: Immutable, persistent data structure. 561 + 562 + ```rust 563 + use jacquard_repo::mst::Mst; 564 + use jacquard_repo::storage::MemoryBlockStore; 565 + 566 + let storage = MemoryBlockStore::new(); 567 + let mst = Mst::new(storage.clone()); 568 + 569 + // ⚠️ IMMUTABLE: Always reassign 570 + let mst = mst.add("app.bsky.feed.post/abc", record_cid).await?; 571 + let mst = mst.add("app.bsky.feed.post/xyz", record_cid2).await?; 572 + 573 + // Persist and get root CID 574 + let root_cid = mst.persist().await?; 575 + ``` 576 + 577 + **Key validation**: `[a-zA-Z0-9._:~-]+` with exactly one `/` separator. Max 256 bytes. Format: `collection/rkey`. 578 + 579 + **Diff operations**: 580 + 581 + ```rust 582 + let diff = old_mst.diff(&new_mst).await?; 583 + diff.validate_limits()?; // Enforce 200 op limit (protocol) 584 + 585 + // Convert to different formats 586 + let verified_ops = diff.to_verified_ops(); // For batch() 587 + let repo_ops = diff.to_repo_ops(); // For firehose 588 + ``` 589 + 590 + **Commits**: 591 + 592 + ```rust 593 + use jacquard_repo::commit::Commit; 594 + 595 + // Create and sign 596 + let commit = Commit::new_unsigned(did, data_cid, rev, prev_cid) 597 + .sign(&signing_key)?; 598 + 599 + // Verify 600 + commit.verify(&public_key)?; 601 + ``` 602 + 603 + **Supported signature algorithms**: Ed25519, ECDSA P-256, ECDSA secp256k1. 604 + 605 + **Firehose validation**: 606 + 607 + ```rust 608 + // Sync v1.0 (requires prev MST state) 609 + let new_root = commit.validate_v1_0(prev_mst_root, prev_storage, pubkey).await?; 610 + 611 + // Sync v1.1 (inductive, requires prev_data field + op prev CIDs) 612 + let new_root = commit.validate_v1_1(pubkey).await?; 613 + ``` 614 + 615 + **v1.1 inductive validation**: Inverts operations on claimed result, verifies inverted result matches `prev_data`. Requires: 616 + - `prev_data` field in commit 617 + - All operations have `prev` CIDs for updates/deletes 618 + - All required MST blocks in CAR 619 + 620 + **CAR I/O**: 621 + 622 + ```rust 623 + // Read 624 + let parsed = parse_car_bytes(&bytes)?; 625 + let root_cid = parsed.root; 626 + let blocks = parsed.blocks; // BTreeMap<CID, Bytes> 627 + 628 + // Write 629 + let bytes = write_car_bytes(root_cid, blocks)?; 630 + ``` 631 + 632 + **BlockStore trait**: Pluggable storage backend. 633 + 634 + ```rust 635 + #[trait_variant::make(Send)] // Conditionally Send on non-WASM 636 + pub trait BlockStore: Clone { 637 + async fn get(&self, cid: &CID) -> Result<Option<Bytes>>; 638 + async fn put(&self, data: &[u8]) -> Result<CID>; 639 + async fn apply_commit(&self, commit: CommitData) -> Result<()>; // Atomic 640 + } 641 + ``` 642 + 643 + **Implementations**: 644 + - `MemoryBlockStore`: In-memory (testing) 645 + - `FileBlockStore`: File-based (persistent) 646 + - `LayeredBlockStore`: Read-through cache (e.g., temp over persistent for firehose) 647 + 648 + **Repository API** (high-level): 649 + 650 + ```rust 651 + use jacquard_repo::repo::Repository; 652 + 653 + let repo = Repository::create(storage, did, signing_key, None).await?; 654 + 655 + // Single operations (don't auto-commit) 656 + repo.create_record(collection, rkey, cid).await?; 657 + let old_cid = repo.update_record(collection, rkey, new_cid).await?; 658 + let deleted_cid = repo.delete_record(collection, rkey).await?; 659 + 660 + // Batch commit 661 + let ops = vec![ 662 + RecordWriteOp::Create { collection, rkey, record }, 663 + RecordWriteOp::Update { collection, rkey, record, prev }, 664 + ]; 665 + let (repo_ops, commit_data) = repo.create_commit(&ops, &did, prev, &key).await?; 666 + repo.apply_commit(commit_data).await?; 667 + ``` 668 + 669 + **Common mistakes**: 670 + - Forgetting immutability (not reassigning MST operations) 671 + - Not calling `validate_limits()` before creating commits (protocol violation) 672 + - Using v1.1 validation without `prev_data` field (will fail) 673 + - Missing `prev` CIDs on update/delete operations for v1.1 674 + - Not implementing `Clone` cheaply on custom `BlockStore` (use `Arc` internally) 675 + - Ignoring CID mismatch errors (indicates data corruption) 676 + 677 + ### jacquard-lexicon (Code Generation) 678 + 679 + **ONLY use `just` commands** for code generation: 680 + 681 + ```bash 682 + just lex-gen # Fetch + generate 683 + just lex-fetch # Fetch only 684 + just codegen # Generate from existing lexicons 685 + ``` 686 + 687 + **Union collision detection**: When multiple namespaces have similar type names in a union, foreign refs get prefixed with second NSID segment: 688 + 689 + ``` 690 + app.bsky.embed.images → BskyImages 691 + sh.custom.embed.images → CustomImages 692 + ``` 693 + 694 + **Builder heuristics**: 695 + - Has builder: 1+ required fields, not all bare `CowStr` 696 + - Has `Default`: 0 required fields OR all required are bare `CowStr` 697 + 698 + **Empty objects**: Generate as empty structs with `#[lexicon]` attribute (adds `extra_data`), not as `Data<'a>`. 699 + 700 + **Local refs**: `#fragment` normalized to `{current_nsid}#fragment` during generation. 701 + 702 + **Feature generation**: Tracks cross-namespace dependencies: 703 + 704 + ```toml 705 + net_anisota = ["app_bsky"] # Uses Bluesky embeds 706 + ``` 707 + 708 + **Token types**: Unit structs with `Display` impl: 709 + 710 + ```rust 711 + pub struct ClickthroughAuthor; 712 + impl Display for ClickthroughAuthor { 713 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 714 + write!(f, "clickthroughAuthor") 715 + } 716 + } 717 + ``` 718 + 719 + **Common mistakes**: Running codegen commands manually without `just` (wrong flags, paths). 720 + 721 + --- 722 + 723 + ## Anti-Patterns to Avoid 724 + 725 + ### ❌ Roundtripping through String 726 + 727 + ```rust 728 + // ❌ BAD 729 + let did_str = did.as_str().to_string(); 730 + let did2 = Did::new(&did_str)?; 731 + 732 + // ✅ GOOD 733 + let did2 = did.clone(); 734 + // or 735 + let did2 = did.into_static(); // If you need 'static 736 + ``` 737 + 738 + ### ❌ Using serde_json::Value 739 + 740 + ```rust 741 + // ❌ NEVER 742 + let value: serde_json::Value = serde_json::from_slice(bytes)?; 743 + let post: Post = serde_json::from_value(value)?; 744 + 745 + // ✅ ALWAYS 746 + let data: Data = serde_json::from_slice(bytes)?; 747 + let post: Post = from_data(&data)?; 748 + ``` 749 + 750 + ### ❌ Using FromStr for validated types 751 + 752 + ```rust 753 + // ❌ SLOW (always allocates) 754 + let did: Did = "did:plc:abc".parse()?; 755 + 756 + // ✅ FAST (zero allocation) 757 + let did = Did::new("did:plc:abc")?; 758 + ``` 759 + 760 + ### ❌ Not deriving IntoStatic on custom types 761 + 762 + ```rust 763 + // ❌ WILL NOT COMPILE with XRPC responses 764 + struct MyOutput<'a> { 765 + field: CowStr<'a>, 766 + } 767 + 768 + // ✅ REQUIRED 769 + #[derive(jacquard_derive::IntoStatic)] 770 + struct MyOutput<'a> { 771 + field: CowStr<'a>, 772 + } 773 + ``` 774 + 775 + ### ❌ Dropping response while holding borrowed parse 776 + 777 + ```rust 778 + // ❌ WILL NOT COMPILE 779 + let output = { 780 + let response = agent.send(request).await?; 781 + response.parse()? // Borrows from response! 782 + }; 783 + 784 + // ✅ Keep response alive OR convert to owned 785 + let response = agent.send(request).await?; 786 + let output = response.parse()?; 787 + // OR 788 + let output = agent.send(request).await?.into_output()?; 789 + ``` 790 + 791 + ### ❌ Calling .into_static() in tight loops 792 + 793 + ```rust 794 + // ❌ WASTEFUL (clones all borrowed data every iteration) 795 + for post in timeline.feed { 796 + let owned = post.into_static(); 797 + process(owned); 798 + } 799 + 800 + // ✅ EFFICIENT (borrow when possible) 801 + for post in &timeline.feed { 802 + process(post); 803 + } 804 + 805 + // Only convert to static if storing long-term: 806 + let stored: Vec<Post<'static>> = timeline.feed.into_iter() 807 + .map(|p| p.into_static()) 808 + .collect(); 809 + ``` 810 + 811 + ### ❌ Non-exhaustive union matches 812 + 813 + ```rust 814 + // ❌ WILL NOT COMPILE (missing Unknown variant) 815 + match embed { 816 + PostEmbed::Images(img) => { /* ... */ } 817 + PostEmbed::Video(vid) => { /* ... */ } 818 + } 819 + 820 + // ✅ HANDLE ALL VARIANTS 821 + match embed { 822 + PostEmbed::Images(img) => { /* ... */ } 823 + PostEmbed::Video(vid) => { /* ... */ } 824 + _ => { /* Unknown or other variants */ } 825 + } 826 + ``` 827 + 828 + ### ❌ Forgetting MST immutability 829 + 830 + ```rust 831 + // ❌ WRONG (loses result) 832 + mst.add(key, cid).await?; 833 + 834 + // ✅ CORRECT (reassign) 835 + let mst = mst.add(key, cid).await?; 836 + ``` 837 + 838 + ### ❌ Skipping issuer verification in OAuth 839 + 840 + ```rust 841 + // ❌ SECURITY VULNERABILITY 842 + let token_response = exchange_code(...).await?; 843 + // Immediately trusting token_response.sub without verification! 844 + 845 + // ✅ ALWAYS VERIFY 846 + let token_response = exchange_code(...).await?; 847 + let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?; 848 + // Now safe to use token_response 849 + ``` 850 + 851 + ### ❌ Using `for<'de> Deserialize<'de>` bounds (CATASTROPHIC) 852 + 853 + **This is the SINGLE MOST COMMON mistake LLMs make with Jacquard.** The HRTB `for<'de>` forces owned deserialization and breaks ALL Jacquard types. 854 + 855 + ```rust 856 + // ❌ CATASTROPHIC - Breaks all Jacquard types 857 + fn deserialize_anything<T>(data: &[u8]) -> Result<T> 858 + where 859 + T: for<'de> Deserialize<'de> // Equivalent to DeserializeOwned! 860 + { 861 + serde_json::from_slice(data) 862 + } 863 + 864 + // Attempting to use: 865 + let post: Post = deserialize_anything(&bytes)?; // ERROR: Post<'_> doesn't satisfy bound 866 + 867 + // ❌ Also breaks in generic contexts 868 + struct MyContainer<T> 869 + where 870 + T: for<'de> Deserialize<'de> // No Jacquard type can be stored here! 871 + { 872 + value: T, 873 + } 874 + 875 + // ❌ Even if you think you need it for async 876 + async fn fetch<T>(url: &str) -> Result<T> 877 + where 878 + T: for<'de> Deserialize<'de> // Still wrong! Use Response<R> pattern instead 879 + { 880 + let bytes = http_get(url).await?; 881 + serde_json::from_slice(&bytes) // Can't borrow, bytes about to be dropped 882 + } 883 + ``` 884 + 885 + ```rust 886 + // ✅ CORRECT - Method-level lifetime 887 + fn deserialize_anything<'de, T>(data: &'de [u8]) -> Result<T> 888 + where 889 + T: Deserialize<'de> 890 + { 891 + serde_json::from_slice(data) 892 + } 893 + 894 + // ✅ CORRECT - With IntoStatic for async 895 + fn deserialize_owned<'de, T>(data: &'de [u8]) -> Result<T::Output> 896 + where 897 + T: Deserialize<'de> + IntoStatic, 898 + T::Output: 'static, 899 + { 900 + let borrowed: T = serde_json::from_slice(data)?; 901 + Ok(borrowed.into_static()) 902 + } 903 + 904 + // ✅ CORRECT - Use Jacquard's Response pattern for async 905 + async fn fetch<R: XrpcRequest>(url: &str) -> Result<Response<R::Response>> { 906 + let bytes = http_get(url).await?; 907 + Ok(Response::new(bytes)) // Caller chooses .parse() or .into_output() 908 + } 909 + ``` 910 + 911 + **Why `for<'de>` breaks**: It means "T must deserialize from ANY lifetime", which is impossible if T contains borrowed data. The deserializer can't satisfy "any lifetime" including lifetimes shorter than the input buffer, so it forces owned allocation. 912 + 913 + **Rule**: If you see `for<'de> Deserialize<'de>` anywhere in your code with Jacquard types, it's wrong. Use method-level `<'de>` parameters and propagate lifetimes explicitly. 914 + 915 + --- 916 + 917 + ## WASM Compatibility 918 + 919 + Core crates support `wasm32-unknown-unknown` target: 920 + - jacquard-common 921 + - jacquard-api 922 + - jacquard-identity (no DNS resolution) 923 + - jacquard-oauth 924 + 925 + **Pattern**: `#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]` 926 + 927 + **What's different on WASM**: 928 + - No `Send` bounds on traits 929 + - DNS resolution skipped in handle→DID chain 930 + - Tokio-specific features disabled 931 + 932 + **Test WASM compilation**: 933 + 934 + ```bash 935 + just check-wasm 936 + # or 937 + cargo build --target wasm32-unknown-unknown -p jacquard-common --no-default-features 938 + ``` 939 + 940 + --- 941 + 942 + ## Quick Reference 943 + 944 + ### String Type Constructors 945 + 946 + | Method | Allocates? | Use When | 947 + |--------|-----------|----------| 948 + | `new(&str)` | No | Borrowed string | 949 + | `new_static(&'static str)` | No | Static string literal | 950 + | `new_owned(String)` | Reuses | Already have owned String | 951 + | `FromStr::parse()` | Yes | Don't care about performance | 952 + 953 + ### Response Parsing 954 + 955 + | Method | Lifetime | Allocates? | 956 + |--------|----------|-----------| 957 + | `.parse()` | `<'_>` | No (zero-copy) | 958 + | `.into_output()` | `'static` | Yes (converts to owned) | 959 + 960 + ### XRPC Traits 961 + 962 + | Trait | Side | Purpose | 963 + |-------|------|---------| 964 + | `XrpcRequest` | Client + Server | Request with NSID, method, encode/decode | 965 + | `XrpcResp` | Client + Server | Response marker with GAT Output/Err | 966 + | `XrpcEndpoint` | Server | Routing marker with PATH, METHOD | 967 + | `XrpcClient` | Client | Stateful XRPC with base_uri, send() | 968 + | `XrpcExt` | Client | Stateless XRPC builder on any HttpClient | 969 + 970 + ### Session Types 971 + 972 + | Type | Auth Method | Auto-Refresh | Storage | 973 + |------|------------|--------------|---------| 974 + | `CredentialSession` | Bearer (app password) | Via refreshSession | SessionStore | 975 + | `OAuthSession` | DPoP (OAuth) | Via token endpoint | ClientAuthStore | 976 + 977 + ### BlockStore Implementations 978 + 979 + | Type | Persistent? | Use Case | 980 + |------|------------|----------| 981 + | `MemoryBlockStore` | No | Testing | 982 + | `FileBlockStore` | Yes | Production | 983 + | `LayeredBlockStore` | Depends | Read-through cache | 984 + 985 + --- 986 + 987 + ## Common Operations 988 + 989 + ### Making an XRPC call 990 + 991 + ```rust 992 + use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed; 993 + 994 + let request = GetAuthorFeed::new() 995 + .actor("alice.bsky.social".into()) 996 + .limit(50) 997 + .build(); 998 + 999 + let response = agent.send(request).await?; 1000 + let output = response.into_output()?; 1001 + 1002 + for post in output.feed { 1003 + println!("{}: {}", post.post.author.handle, post.post.uri); 1004 + } 1005 + ``` 1006 + 1007 + ### Creating a record 1008 + 1009 + ```rust 1010 + use jacquard::api::app_bsky::feed::post::Post; 1011 + use jacquard::common::types::string::Datetime; 1012 + 1013 + let post = Post::builder() 1014 + .text("Hello ATProto from Jacquard!") 1015 + .created_at(Datetime::now()) 1016 + .build(); 1017 + 1018 + agent.create_record(post, None).await?; 1019 + ``` 1020 + 1021 + ### Resolving identity 1022 + 1023 + ```rust 1024 + use jacquard::identity::PublicResolver; 1025 + 1026 + let resolver = PublicResolver::default(); 1027 + 1028 + // Handle → DID 1029 + let did = resolver.resolve_handle(&handle).await?; 1030 + 1031 + // DID → PDS endpoint 1032 + let pds = resolver.pds_for_did(&did).await?; 1033 + 1034 + // Combined 1035 + let (did, pds) = resolver.pds_for_handle(&handle).await?; 1036 + ``` 1037 + 1038 + ### OAuth login 1039 + 1040 + ```rust 1041 + use jacquard::oauth::client::OAuthClient; 1042 + use jacquard::client::FileAuthStore; 1043 + 1044 + let oauth = OAuthClient::with_default_config( 1045 + FileAuthStore::new("./auth.json") 1046 + ); 1047 + 1048 + let session = oauth.login_with_local_server( 1049 + "alice.bsky.social", 1050 + Default::default(), 1051 + Default::default(), 1052 + ).await?; 1053 + 1054 + let agent = Agent::from(session); 1055 + ``` 1056 + 1057 + ### Server-side XRPC handler 1058 + 1059 + ```rust 1060 + use jacquard_axum::{ExtractXrpc, IntoRouter}; 1061 + use axum::{Router, Json}; 1062 + 1063 + async fn handler( 1064 + ExtractXrpc(req): ExtractXrpc<MyRequest> 1065 + ) -> Json<MyOutput<'static>> { 1066 + // Process request 1067 + Json(output) 1068 + } 1069 + 1070 + let app = Router::new() 1071 + .merge(MyRequest::into_router(handler)); 1072 + ``` 1073 + 1074 + ### MST operations 1075 + 1076 + ```rust 1077 + use jacquard_repo::mst::Mst; 1078 + use jacquard_repo::storage::MemoryBlockStore; 1079 + 1080 + let storage = MemoryBlockStore::new(); 1081 + let mst = Mst::new(storage.clone()); 1082 + 1083 + let mst = mst.add("app.bsky.feed.post/abc123", record_cid).await?; 1084 + let mst = mst.add("app.bsky.feed.post/xyz789", record_cid2).await?; 1085 + 1086 + let root_cid = mst.persist().await?; 1087 + ``` 1088 + 1089 + --- 1090 + 1091 + ## Documentation Links 1092 + 1093 + - [docs.rs/jacquard](https://docs.rs/jacquard/latest/jacquard/) 1094 + - [docs.rs/jacquard-common](https://docs.rs/jacquard-common/latest/jacquard_common/) 1095 + - [docs.rs/jacquard-api](https://docs.rs/jacquard-api/latest/jacquard_api/) 1096 + - [docs.rs/jacquard-oauth](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/) 1097 + - [docs.rs/jacquard-identity](https://docs.rs/jacquard-identity/latest/jacquard_identity/) 1098 + - [docs.rs/jacquard-repo](https://docs.rs/jacquard-repo/latest/jacquard_repo/) 1099 + - [docs.rs/jacquard-axum](https://docs.rs/jacquard-axum/latest/jacquard_axum/) 1100 + - [Repository](https://tangled.org/@nonbinary.computer/jacquard) 1101 + 1102 + --- 1103 + 1104 + ## Philosophy Summary 1105 + 1106 + Jacquard is designed for **correctness**, **performance**, and **ergonomics** in that order. It favors: 1107 + 1108 + 1. **Validation at construction time** - Invalid inputs fail fast, not deep in your code 1109 + 2. **Zero-copy by default** - Borrow from buffers, convert to owned only when needed 1110 + 3. **Explicit lifetime control** - You choose when to allocate via `.into_static()` or `.into_output()` 1111 + 4. **Type safety without boilerplate** - Generated bindings just work, with strong typing 1112 + 5. **Batteries included, but replaceable** - High-level `Agent` for convenience, low-level primitives for control 1113 + 1114 + **When in doubt**: Read the documentation, use the validated types, respect the lifetimes, and trust the zero-copy patterns. Jacquard is designed to guide you toward correct, performant code.