# Jacquard: AT Protocol Library for Rust > Simple and powerful AT Protocol (Bluesky) client library emphasizing spec compliance, zero-copy deserialization, and minimal boilerplate. ## Core Philosophy **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. **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. **Batteries included, but replaceable.** High-level `Agent` for convenience, or use stateless `XrpcCall` builder for full control. Mix and match as needed. --- ## Critical Patterns to Internalize ### String Type Constructors ALL validated string types (Did, Handle, AtUri, Nsid, etc.) follow this pattern: ```rust // ✅ PREFERRED: Zero-allocation for borrowed strings let did = Did::new("did:plc:abc123")?; // Borrows from input // ✅ BEST: Zero-allocation for static strings let nsid = Nsid::new_static("com.atproto.repo.getRecord")?; // ✅ When you need ownership let owned = Did::new_owned("did:plc:abc123")?; // ❌ AVOID: FromStr always allocates let did: Did = "did:plc:abc123".parse()?; // Always allocates! // ❌ NEVER: Roundtripping through String let s = did.as_str().to_string(); let did2 = Did::new(&s)?; // Pointless allocation ``` **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. ### Response Parsing: Borrow vs Own XRPC responses wrap a `Bytes` buffer. You choose when to parse and whether to own the data: ```rust let response = agent.send(request).await?; // Option 1: Borrow from response buffer (zero-copy) let output: GetPostOutput<'_> = response.parse()?; // ⚠️ `output` borrows from `response`, both must stay in scope // Option 2: Convert to owned (allocates, but can outlive response) let output: GetPostOutput<'static> = response.into_output()?; drop(response); // OK, output is now fully owned // ❌ WRONG: Can't drop response while holding borrowed parse let output = response.parse()?; drop(response); // ERROR: output borrows from response ``` **Rule**: Use `.parse()` when processing immediately in the same scope. Use `.into_output()` when returning from functions or storing long-term. ### Lifetime Pattern: GATs Not HRTBs Jacquard uses **Generic Associated Types** on `XrpcResp` to avoid Higher-Rank Trait Bounds: ```rust // ✅ Jacquard's approach (GAT) trait XrpcResp { type Output<'de>: Deserialize<'de> + IntoStatic; type Err<'de>: Error + Deserialize<'de> + IntoStatic; } // ❌ Alternative that forces DeserializeOwned semantics trait BadXrpcResp<'de> { type Output: Deserialize<'de>; } // Would require: where R: for<'any> BadXrpcResp<'any> // This forces owned deserialization! ``` **Why this matters**: Async methods return `Response` that owns the buffer. Caller controls lifetime by choosing `.parse()` (borrow) or `.into_output()` (owned). No HRTB needed, zero-copy works in async contexts. **When implementing custom types**: Use method-level lifetime generics (`<'de>`), not trait-level lifetimes. ### CRITICAL: Never Use `for<'de> Deserialize<'de>` Bounds **The bound `T: for<'de> Deserialize<'de>` is EQUIVALENT to `T: DeserializeOwned`** and will break all Jacquard types. ```rust // ❌ CATASTROPHIC - No Jacquard type can satisfy this fn bad(data: &[u8]) -> Result where T: for<'de> Deserialize<'de> // Forces owned deserialization! { serde_json::from_slice(data) // Can't borrow from data } // ✅ CORRECT - Use method-level lifetime fn good<'de, T>(data: &'de [u8]) -> Result where T: Deserialize<'de> // Can borrow from data { serde_json::from_slice(data) } ``` **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. **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. --- ## Crate-by-Crate Guide ### jacquard (Main Crate) **Primary entry point**: `Agent` ```rust use jacquard::client::{Agent, CredentialSession, MemorySessionStore}; use jacquard::identity::PublicResolver; // App password auth let (session, _info) = CredentialSession::authenticated( "alice.bsky.social".into(), "app-password".into(), None, // session_id ).await?; let agent = Agent::from(session); // Make typed XRPC calls use jacquard::api::app_bsky::feed::get_timeline::GetTimeline; let response = agent.send(&GetTimeline::new().limit(50).build()).await?; let timeline = response.into_output()?; ``` **Auto-refresh**: Both `CredentialSession` and `OAuthSession` automatically refresh tokens on 401/expired errors. One retry per request. **Typed record operations** (via `AgentSessionExt` trait): ```rust use jacquard::api::app_bsky::feed::post::Post; // Create let post = Post::builder() .text("Hello ATProto!") .created_at(Datetime::now()) .build(); agent.create_record(post, None).await?; // Get (type-safe!) let uri = Post::uri("at://did:plc:abc/app.bsky.feed.post/123")?; let response = agent.get_record::(&uri).await?; let post_output = response.parse()?; // Update with fetch-modify-put pattern agent.update_record::(&uri, |profile| { profile.display_name = Some("New Name".into()); }).await?; ``` **Key traits**: - `AgentSession`: Common interface for both auth types - `XrpcClient`: Stateful XRPC (has base URI, auth tokens) - `HttpClient`: Low-level HTTP abstraction **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. ### jacquard-common (Foundation) **Core types**: `Did`, `Handle`, `AtUri`, `Nsid`, `Tid`, `Cid`, `CowStr`, `Data`, `RawData` **String type traits** (ALL validated types implement these): - `new(&str)` - Validates, borrows (zero alloc) - `new_static(&'static str)` - Validates, zero alloc - `new_owned(impl Into)` - Validates, takes ownership - `raw(&str)` - Panics on invalid (use when you KNOW it's valid) - `unchecked(&str)` - Unsafe, no validation - `as_str(&self) -> &str` - Get string reference - `Display`, `FromStr`, `Serialize`, `Deserialize`, `IntoStatic` **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). **XRPC layer**: ```rust // Stateless XRPC (with any HttpClient) use jacquard::common::xrpc::XrpcExt; let http = reqwest::Client::new(); let response = http .xrpc(Url::parse("https://bsky.social")?) .auth(AuthorizationToken::Bearer(token)) .proxy(did) .send(&request) .await?; // Stateful XRPC (implement XrpcClient trait) let response = agent.send(request).await?; ``` **Data vs RawData**: - `Data<'a>`: Validated, type-inferred atproto values (strings parsed to Did/Handle/etc.) - `RawData<'a>`: Minimal validation, suitable for pass-through/relay use cases ```rust // Convert typed → untyped → typed let post = Post::builder().text("test").build(); let data: Data = to_data(&post)?; let post2: Post = from_data(&data)?; // NEVER use serde_json::Value // ❌ let value: serde_json::Value = ...; // ✅ let data: Data = ...; ``` **Streaming** (feature: `streaming`): - `ByteStream` / `ByteSink`: Platform-agnostic (works on WASM via `n0-future`) - `HttpClientExt::send_http_streaming()`: Stream response - `HttpClientExt::send_http_bidirectional()`: Stream both request and response **WebSocket** (feature: `websocket`): - `WebSocketClient` trait, `WebSocketConnection` - Native + WASM: tokio-tungstenite-wasm **Collection trait** (for record types): ```rust pub trait Collection { const NSID: &'static str; type Record: XrpcResp; // Marker type for get_record() } // Enables typed record retrieval: let response: Response = agent.get_record(did, rkey).await?; ``` **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)]`. ### jacquard-api (Generated Bindings) **764 lexicon schemas** across 52+ namespaces. **Feature organization**: - `minimal`: Core atproto only - `bluesky`: Bluesky app + chat + ozone - `other`: Curated third-party lexicons - `lexicon_community`: Community extensions - `ufos`: Experimental/niche **Generated patterns**: - All types have `'a` lifetime for zero-copy deserialization - All implement `IntoStatic`, `Serialize`, `Deserialize`, `Clone`, `PartialEq`, `Eq` - Builders (`bon::Builder`) on types with 2+ fields (some required) - Open unions have `Unknown(Data<'a>)` variant via `#[open_union]` macro - Objects have `extra_data: BTreeMap>` via `#[lexicon]` macro **Union collision detection**: When multiple namespaces define similar types, foreign refs get prefixed: ```rust // If both app.bsky.embed.images and sh.custom.embed.images exist: pub enum SomeUnion<'a> { BskyImages(Box>), CustomImages(Box>), } ``` **For each collection (record type)**, generated code includes: 1. Main record struct (e.g., `Post<'a>`) 2. `GetRecordOutput` wrapper (`PostGetRecordOutput<'a>` with uri, cid, value) 3. Marker struct (`PostRecord`) implementing `XrpcResp` 4. `Collection` trait impl 5. Helper: `Post::uri()` for constructing typed URIs **For each XRPC endpoint**: 1. Request struct with builder 2. Output struct 3. Error enum (open union, includes `Unknown` variant) 4. Response marker implementing `XrpcResp` 5. Request marker implementing `XrpcRequest` 6. Endpoint marker implementing `XrpcEndpoint` (server-side) **Common mistakes**: - Not handling `Unknown` variant in union matches (non-exhaustive!) - 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. - Calling `.into_static()` in tight loops (it clones all borrowed data) ### jacquard-derive (Macros) **`#[lexicon]`**: Adds `extra_data` field to capture unknown fields during deserialization. ```rust #[lexicon] #[derive(Serialize, Deserialize)] struct MyType<'a> { known_field: CowStr<'a>, // Macro adds: pub extra_data: BTreeMap> } ``` With `bon::Builder`: Automatically adds `#[builder(default)]` to `extra_data`. **`#[open_union]`**: Adds `Unknown(Data<'a>)` variant to enums. ```rust #[open_union] #[serde(tag = "$type")] enum MyUnion<'a> { KnownVariant(Foo<'a>), // Macro adds: #[serde(untagged)] Unknown(Data<'a>) } ``` **`#[derive(IntoStatic)]`**: Generates owned conversion. ```rust #[derive(IntoStatic)] struct Post<'a> { text: CowStr<'a>, likes: u32, } // Generates: impl IntoStatic for Post<'_> { type Output = Post<'static>; fn into_static(self) -> Post<'static> { Post { text: self.text.into_static(), // Converts to owned likes: self.likes.into_static(), // Passthrough (Copy) } } } ``` **`#[derive(XrpcRequest)]`**: Generates XRPC boilerplate for custom endpoints. ```rust #[derive(Serialize, Deserialize, XrpcRequest)] #[xrpc( nsid = "com.example.getThing", method = Query, output = GetThingOutput, error = GetThingError, // Optional, defaults to GenericError server // Optional, generates XrpcEndpoint marker )] struct GetThing<'a> { #[serde(borrow)] pub id: CowStr<'a>, } ``` **Critical**: All custom types with lifetimes MUST derive `IntoStatic` or manually implement it. Otherwise, you can't use them with `.into_output()`. ### jacquard-oauth (OAuth/DPoP) **OAuth flow**: ```rust use jacquard::oauth::client::OAuthClient; use jacquard::client::FileAuthStore; let oauth = OAuthClient::with_default_config( FileAuthStore::new("./auth.json") ); // Loopback flow (feature: loopback) let session = oauth.login_with_local_server( "alice.bsky.social", Default::default(), LoopbackConfig::default(), ).await?; let agent = Agent::from(session); ``` **DPoP proofs**: Automatically generated for every request. Include: - `jti`: Unique token ID (random) - `htm`: HTTP method - `htu`: Target URI - `iat`: Issued at timestamp - `nonce`: Server-provided (cached and retried on `use_dpop_nonce` error) - `ath`: SHA-256 hash of access token (when present) **Nonce handling**: Automatic retry on `use_dpop_nonce` errors (400 for auth server, 401 for PDS). Max one retry per request. **Nonce storage**: - `dpop_authserver_nonce`: For token endpoint - `dpop_host_nonce`: For PDS XRPC requests **Token refresh**: Automatic on `invalid_token` errors. Uses `SessionRegistry` with per-DID+session_id locks to prevent concurrent refresh races. **private_key_jwt**: For non-loopback clients. Automatically used if server supports it. **Issuer verification**: ALWAYS verify issuer has authority over the DID before trusting tokens. This is a **critical security check**. ```rust // In callback flow, after exchanging code: let token_response = exchange_code(...).await?; // ⚠️ MUST verify before using token let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?; ``` **Common mistakes**: - Skipping issuer verification (security vulnerability!) - Not updating session after refresh (next request uses expired token) - Reusing DPoP proofs across requests (each request needs a fresh proof with new jti/iat) - Mixing auth server and host nonces (they're tracked separately) - Forgetting `ath` claim when including access token in PDS requests ### jacquard-identity (Identity Resolution) **Resolution chains** (configurable fallback order): **Handle → DID**: 1. DNS TXT `_atproto.` (feature: `dns`, skipped on WASM) 2. HTTPS `https:///.well-known/atproto-did` 3. PDS XRPC `com.atproto.identity.resolveHandle` 4. Public API fallback `https://public.api.bsky.app` (if enabled) 5. Slingshot mini-doc (if configured) **DID → Document**: 1. `did:web`: HTTPS `.well-known/did.json` 2. `did:plc`: PLC directory or Slingshot 3. PDS XRPC `com.atproto.identity.resolveDid` ```rust use jacquard::identity::{JacquardResolver, PublicResolver}; let resolver = PublicResolver::default(); // DNS + public fallbacks enabled // Handle → DID let did: Did<'static> = resolver.resolve_handle(&handle).await?; // DID → Document let response = resolver.resolve_did_doc(&did).await?; let doc = response.parse_validated()?; // Validates doc.id matches requested DID // Combined: Get PDS endpoint let pds_url = resolver.pds_for_did(&did).await?; ``` **DidDocResponse pattern** (same as XRPC responses): ```rust let response = resolver.resolve_did_doc(&did).await?; // Borrow from buffer let doc: DidDocument<'_> = response.parse()?; // Validate doc ID let doc = response.parse_validated()?; // Error if doc.id != requested DID // Convert to owned let doc: DidDocument<'static> = response.into_owned()?; ``` **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. **OAuthResolver trait**: Auto-implemented for `JacquardResolver`. Adds OAuth metadata resolution: ```rust // High-level: accepts handle, DID, or HTTPS URL let (server_metadata, doc_opt) = resolver.resolve_oauth("alice.bsky.social").await?; // From identity (handle or DID) let (server_metadata, doc) = resolver.resolve_from_identity("alice.bsky.social").await?; // From service URL (PDS or entryway) let server_metadata = resolver.resolve_from_service(&pds_url).await?; // Verify issuer authority over DID let pds = resolver.verify_issuer(&server_metadata, &sub_did).await?; ``` **Common mistakes**: - Not validating doc ID (use `.parse_validated()`, not just `.parse()`) - Assuming DNS resolution works on WASM (it doesn't) - Comparing issuer URLs with string equality (use `issuer_equivalent()` for trailing slash tolerance) - Trusting DID documents without checking `alsoKnownAs` for handle aliases - Not caching resolver (it's `Clone`, cheap to share) ### jacquard-axum (Server-Side) **ExtractXrpc**: Type-safe XRPC request extraction. ```rust use jacquard_axum::{ExtractXrpc, IntoRouter}; use jacquard::api::com_atproto::identity::resolve_handle::ResolveHandleRequest; async fn handle_resolve( ExtractXrpc(req): ExtractXrpc ) -> Json> { let did = resolve_handle_logic(&req.handle).await; Json(ResolveHandleOutput { did, extra_data: Default::default() }) } // Automatic routing let app = Router::new() .merge(ResolveHandleRequest::into_router(handle_resolve)); ``` **Query vs Procedure**: - Query (GET): Extracts from query string via `serde_html_form` - Procedure (POST): Calls `Request::decode_body()` (default: JSON, override for CBOR) **Zero-copy → owned conversion**: Extractor borrows during deserialization, then converts to `'static` via `IntoStatic`. This is why all request types must implement `IntoStatic`. **Custom encodings**: ```rust impl XrpcRequest for MyRequest<'_> { fn decode_body<'de>(body: &'de [u8]) -> Result> { let req = serde_ipld_dagcbor::from_slice(body)?; Ok(Box::new(req)) } } ``` **Service auth** (feature: `service-auth`): ```rust use jacquard_axum::service_auth::{ServiceAuthConfig, ExtractServiceAuth}; let config = ServiceAuthConfig::new( Did::new_static("did:web:feedgen.example.com")?, resolver, ); async fn handler( ExtractServiceAuth(auth): ExtractServiceAuth, ) -> String { format!("Authenticated as {}", auth.did()) } let app = Router::new() .route("/xrpc/app.bsky.feed.getFeedSkeleton", get(handler)) .with_state(config); ``` **Method binding (`lxm` claim)**: Default enabled. Binds JWTs to specific XRPC methods to prevent token reuse across endpoints. **JTI replay protection**: NOT built-in. You must implement: ```rust if let Some(jti) = auth.jti() { if state.seen_jtis.contains(jti) { return Err(StatusCode::UNAUTHORIZED); } state.seen_jtis.insert(jti.to_string(), auth.exp); } ``` **Common mistakes**: - Forgetting `IntoStatic` derive on custom request types - Using wrong trait (use `XrpcEndpoint` marker, not `XrpcRequest`) - Not implementing JTI tracking (allows replay attacks) - Disabling method binding without understanding security implications ### jacquard-repo (Repository Primitives) **MST (Merkle Search Tree)**: Immutable, persistent data structure. ```rust use jacquard_repo::mst::Mst; use jacquard_repo::storage::MemoryBlockStore; let storage = MemoryBlockStore::new(); let mst = Mst::new(storage.clone()); // ⚠️ IMMUTABLE: Always reassign let mst = mst.add("app.bsky.feed.post/abc", record_cid).await?; let mst = mst.add("app.bsky.feed.post/xyz", record_cid2).await?; // Persist and get root CID let root_cid = mst.persist().await?; ``` **Key validation**: `[a-zA-Z0-9._:~-]+` with exactly one `/` separator. Max 256 bytes. Format: `collection/rkey`. **Diff operations**: ```rust let diff = old_mst.diff(&new_mst).await?; diff.validate_limits()?; // Enforce 200 op limit (protocol) // Convert to different formats let verified_ops = diff.to_verified_ops(); // For batch() let repo_ops = diff.to_repo_ops(); // For firehose ``` **Commits**: ```rust use jacquard_repo::commit::Commit; // Create and sign let commit = Commit::new_unsigned(did, data_cid, rev, prev_cid) .sign(&signing_key)?; // Verify commit.verify(&public_key)?; ``` **Supported signature algorithms**: Ed25519, ECDSA P-256, ECDSA secp256k1. **Firehose validation**: ```rust // Sync v1.0 (requires prev MST state) let new_root = commit.validate_v1_0(prev_mst_root, prev_storage, pubkey).await?; // Sync v1.1 (inductive, requires prev_data field + op prev CIDs) let new_root = commit.validate_v1_1(pubkey).await?; ``` **v1.1 inductive validation**: Inverts operations on claimed result, verifies inverted result matches `prev_data`. Requires: - `prev_data` field in commit - All operations have `prev` CIDs for updates/deletes - All required MST blocks in CAR **CAR I/O**: ```rust // Read let parsed = parse_car_bytes(&bytes)?; let root_cid = parsed.root; let blocks = parsed.blocks; // BTreeMap // Write let bytes = write_car_bytes(root_cid, blocks)?; ``` **BlockStore trait**: Pluggable storage backend. ```rust #[trait_variant::make(Send)] // Conditionally Send on non-WASM pub trait BlockStore: Clone { async fn get(&self, cid: &CID) -> Result>; async fn put(&self, data: &[u8]) -> Result; async fn apply_commit(&self, commit: CommitData) -> Result<()>; // Atomic } ``` **Implementations**: - `MemoryBlockStore`: In-memory (testing) - `FileBlockStore`: File-based (persistent) - `LayeredBlockStore`: Read-through cache (e.g., temp over persistent for firehose) **Repository API** (high-level): ```rust use jacquard_repo::repo::Repository; let repo = Repository::create(storage, did, signing_key, None).await?; // Single operations (don't auto-commit) repo.create_record(collection, rkey, cid).await?; let old_cid = repo.update_record(collection, rkey, new_cid).await?; let deleted_cid = repo.delete_record(collection, rkey).await?; // Batch commit let ops = vec![ RecordWriteOp::Create { collection, rkey, record }, RecordWriteOp::Update { collection, rkey, record, prev }, ]; let (repo_ops, commit_data) = repo.create_commit(&ops, &did, prev, &key).await?; repo.apply_commit(commit_data).await?; ``` **Common mistakes**: - Forgetting immutability (not reassigning MST operations) - Not calling `validate_limits()` before creating commits (protocol violation) - Using v1.1 validation without `prev_data` field (will fail) - Missing `prev` CIDs on update/delete operations for v1.1 - Not implementing `Clone` cheaply on custom `BlockStore` (use `Arc` internally) - Ignoring CID mismatch errors (indicates data corruption) ### jacquard-lexicon (Code Generation) **ONLY use `just` commands** for code generation: ```bash just lex-gen # Fetch + generate just lex-fetch # Fetch only just codegen # Generate from existing lexicons ``` **Union collision detection**: When multiple namespaces have similar type names in a union, foreign refs get prefixed with second NSID segment: ``` app.bsky.embed.images → BskyImages sh.custom.embed.images → CustomImages ``` **Builder heuristics**: - Has builder: 1+ required fields, not all bare `CowStr` - Has `Default`: 0 required fields OR all required are bare `CowStr` **Empty objects**: Generate as empty structs with `#[lexicon]` attribute (adds `extra_data`), not as `Data<'a>`. **Local refs**: `#fragment` normalized to `{current_nsid}#fragment` during generation. **Feature generation**: Tracks cross-namespace dependencies: ```toml net_anisota = ["app_bsky"] # Uses Bluesky embeds ``` **Token types**: Unit structs with `Display` impl: ```rust pub struct ClickthroughAuthor; impl Display for ClickthroughAuthor { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "clickthroughAuthor") } } ``` **Common mistakes**: Running codegen commands manually without `just` (wrong flags, paths). --- ## Anti-Patterns to Avoid ### ❌ Roundtripping through String ```rust // ❌ BAD let did_str = did.as_str().to_string(); let did2 = Did::new(&did_str)?; // ✅ GOOD let did2 = did.clone(); // or let did2 = did.into_static(); // If you need 'static ``` ### ❌ Using serde_json::Value ```rust // ❌ NEVER let value: serde_json::Value = serde_json::from_slice(bytes)?; let post: Post = serde_json::from_value(value)?; // ✅ ALWAYS let data: Data = serde_json::from_slice(bytes)?; let post: Post = from_data(&data)?; ``` ### ❌ Using FromStr for validated types ```rust // ❌ SLOW (always allocates) let did: Did = "did:plc:abc".parse()?; // ✅ FAST (zero allocation) let did = Did::new("did:plc:abc")?; ``` ### ❌ Not deriving IntoStatic on custom types ```rust // ❌ WILL NOT COMPILE with XRPC responses struct MyOutput<'a> { field: CowStr<'a>, } // ✅ REQUIRED #[derive(jacquard_derive::IntoStatic)] struct MyOutput<'a> { field: CowStr<'a>, } ``` ### ❌ Dropping response while holding borrowed parse ```rust // ❌ WILL NOT COMPILE let output = { let response = agent.send(request).await?; response.parse()? // Borrows from response! }; // ✅ Keep response alive OR convert to owned let response = agent.send(request).await?; let output = response.parse()?; // OR let output = agent.send(request).await?.into_output()?; ``` ### ❌ Calling .into_static() in tight loops ```rust // ❌ WASTEFUL (clones all borrowed data every iteration) for post in timeline.feed { let owned = post.into_static(); process(owned); } // ✅ EFFICIENT (borrow when possible) for post in &timeline.feed { process(post); } // Only convert to static if storing long-term: let stored: Vec> = timeline.feed.into_iter() .map(|p| p.into_static()) .collect(); ``` ### ❌ Non-exhaustive union matches ```rust // ❌ WILL NOT COMPILE (missing Unknown variant) match embed { PostEmbed::Images(img) => { /* ... */ } PostEmbed::Video(vid) => { /* ... */ } } // ✅ HANDLE ALL VARIANTS match embed { PostEmbed::Images(img) => { /* ... */ } PostEmbed::Video(vid) => { /* ... */ } _ => { /* Unknown or other variants */ } } ``` ### ❌ Forgetting MST immutability ```rust // ❌ WRONG (loses result) mst.add(key, cid).await?; // ✅ CORRECT (reassign) let mst = mst.add(key, cid).await?; ``` ### ❌ Skipping issuer verification in OAuth ```rust // ❌ SECURITY VULNERABILITY let token_response = exchange_code(...).await?; // Immediately trusting token_response.sub without verification! // ✅ ALWAYS VERIFY let token_response = exchange_code(...).await?; let pds = resolver.verify_issuer(&server_metadata, &token_response.sub).await?; // Now safe to use token_response ``` ### ❌ Using `for<'de> Deserialize<'de>` bounds (CATASTROPHIC) **This is the SINGLE MOST COMMON mistake LLMs make with Jacquard.** The HRTB `for<'de>` forces owned deserialization and breaks ALL Jacquard types. ```rust // ❌ CATASTROPHIC - Breaks all Jacquard types fn deserialize_anything(data: &[u8]) -> Result where T: for<'de> Deserialize<'de> // Equivalent to DeserializeOwned! { serde_json::from_slice(data) } // Attempting to use: let post: Post = deserialize_anything(&bytes)?; // ERROR: Post<'_> doesn't satisfy bound // ❌ Also breaks in generic contexts struct MyContainer where T: for<'de> Deserialize<'de> // No Jacquard type can be stored here! { value: T, } // ❌ Even if you think you need it for async async fn fetch(url: &str) -> Result where T: for<'de> Deserialize<'de> // Still wrong! Use Response pattern instead { let bytes = http_get(url).await?; serde_json::from_slice(&bytes) // Can't borrow, bytes about to be dropped } ``` ```rust // ✅ CORRECT - Method-level lifetime fn deserialize_anything<'de, T>(data: &'de [u8]) -> Result where T: Deserialize<'de> { serde_json::from_slice(data) } // ✅ CORRECT - With IntoStatic for async fn deserialize_owned<'de, T>(data: &'de [u8]) -> Result where T: Deserialize<'de> + IntoStatic, T::Output: 'static, { let borrowed: T = serde_json::from_slice(data)?; Ok(borrowed.into_static()) } // ✅ CORRECT - Use Jacquard's Response pattern for async async fn fetch(url: &str) -> Result> { let bytes = http_get(url).await?; Ok(Response::new(bytes)) // Caller chooses .parse() or .into_output() } ``` **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. **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. --- ## WASM Compatibility Core crates support `wasm32-unknown-unknown` target: - jacquard-common - jacquard-api - jacquard-identity (no DNS resolution) - jacquard-oauth **Pattern**: `#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]` **What's different on WASM**: - No `Send` bounds on traits - DNS resolution skipped in handle→DID chain - Tokio-specific features disabled **Test WASM compilation**: ```bash just check-wasm # or cargo build --target wasm32-unknown-unknown -p jacquard-common --no-default-features ``` --- ## Quick Reference ### String Type Constructors | Method | Allocates? | Use When | |--------|-----------|----------| | `new(&str)` | No | Borrowed string | | `new_static(&'static str)` | No | Static string literal | | `new_owned(String)` | Reuses | Already have owned String | | `FromStr::parse()` | Yes | Don't care about performance | ### Response Parsing | Method | Lifetime | Allocates? | |--------|----------|-----------| | `.parse()` | `<'_>` | No (zero-copy) | | `.into_output()` | `'static` | Yes (converts to owned) | ### XRPC Traits | Trait | Side | Purpose | |-------|------|---------| | `XrpcRequest` | Client + Server | Request with NSID, method, encode/decode | | `XrpcResp` | Client + Server | Response marker with GAT Output/Err | | `XrpcEndpoint` | Server | Routing marker with PATH, METHOD | | `XrpcClient` | Client | Stateful XRPC with base_uri, send() | | `XrpcExt` | Client | Stateless XRPC builder on any HttpClient | ### Session Types | Type | Auth Method | Auto-Refresh | Storage | |------|------------|--------------|---------| | `CredentialSession` | Bearer (app password) | Via refreshSession | SessionStore | | `OAuthSession` | DPoP (OAuth) | Via token endpoint | ClientAuthStore | ### BlockStore Implementations | Type | Persistent? | Use Case | |------|------------|----------| | `MemoryBlockStore` | No | Testing | | `FileBlockStore` | Yes | Production | | `LayeredBlockStore` | Depends | Read-through cache | --- ## Common Operations ### Making an XRPC call ```rust use jacquard::api::app_bsky::feed::get_author_feed::GetAuthorFeed; let request = GetAuthorFeed::new() .actor("alice.bsky.social".into()) .limit(50) .build(); let response = agent.send(request).await?; let output = response.into_output()?; for post in output.feed { println!("{}: {}", post.post.author.handle, post.post.uri); } ``` ### Creating a record ```rust use jacquard::api::app_bsky::feed::post::Post; use jacquard::common::types::string::Datetime; let post = Post::builder() .text("Hello ATProto from Jacquard!") .created_at(Datetime::now()) .build(); agent.create_record(post, None).await?; ``` ### Resolving identity ```rust use jacquard::identity::PublicResolver; let resolver = PublicResolver::default(); // Handle → DID let did = resolver.resolve_handle(&handle).await?; // DID → PDS endpoint let pds = resolver.pds_for_did(&did).await?; // Combined let (did, pds) = resolver.pds_for_handle(&handle).await?; ``` ### OAuth login ```rust use jacquard::oauth::client::OAuthClient; use jacquard::client::FileAuthStore; let oauth = OAuthClient::with_default_config( FileAuthStore::new("./auth.json") ); let session = oauth.login_with_local_server( "alice.bsky.social", Default::default(), Default::default(), ).await?; let agent = Agent::from(session); ``` ### Server-side XRPC handler ```rust use jacquard_axum::{ExtractXrpc, IntoRouter}; use axum::{Router, Json}; async fn handler( ExtractXrpc(req): ExtractXrpc ) -> Json> { // Process request Json(output) } let app = Router::new() .merge(MyRequest::into_router(handler)); ``` ### MST operations ```rust use jacquard_repo::mst::Mst; use jacquard_repo::storage::MemoryBlockStore; let storage = MemoryBlockStore::new(); let mst = Mst::new(storage.clone()); let mst = mst.add("app.bsky.feed.post/abc123", record_cid).await?; let mst = mst.add("app.bsky.feed.post/xyz789", record_cid2).await?; let root_cid = mst.persist().await?; ``` --- ## Documentation Links - [docs.rs/jacquard](https://docs.rs/jacquard/latest/jacquard/) - [docs.rs/jacquard-common](https://docs.rs/jacquard-common/latest/jacquard_common/) - [docs.rs/jacquard-api](https://docs.rs/jacquard-api/latest/jacquard_api/) - [docs.rs/jacquard-oauth](https://docs.rs/jacquard-oauth/latest/jacquard_oauth/) - [docs.rs/jacquard-identity](https://docs.rs/jacquard-identity/latest/jacquard_identity/) - [docs.rs/jacquard-repo](https://docs.rs/jacquard-repo/latest/jacquard_repo/) - [docs.rs/jacquard-axum](https://docs.rs/jacquard-axum/latest/jacquard_axum/) - [Repository](https://tangled.org/@nonbinary.computer/jacquard) --- ## Philosophy Summary Jacquard is designed for **correctness**, **performance**, and **ergonomics** in that order. It favors: 1. **Validation at construction time** - Invalid inputs fail fast, not deep in your code 2. **Zero-copy by default** - Borrow from buffers, convert to owned only when needed 3. **Explicit lifetime control** - You choose when to allocate via `.into_static()` or `.into_output()` 4. **Type safety without boilerplate** - Generated bindings just work, with strong typing 5. **Batteries included, but replaceable** - High-level `Agent` for convenience, low-level primitives for control **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.