Something went wrong. Try again.
A better Rust ATProto crate forked from nonbinary.computer/jacquard
Something went wrong. Try again.
jacquard llms.txt
34 kB · 1114 lines
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115# 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 stringslet did = Did::new("did:plc:abc123")?; // Borrows from input
// ✅ BEST: Zero-allocation for static stringslet nsid = Nsid::new_static("com.atproto.repo.getRecord")?;
// ✅ When you need ownershiplet owned = Did::new_owned("did:plc:abc123")?;
// ❌ AVOID: FromStr always allocateslet did: Did = "did:plc:abc123".parse()?; // Always allocates!
// ❌ NEVER: Roundtripping through Stringlet 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:
```rustlet 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 parselet 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 semanticstrait 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<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.
**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 thisfn bad<T>(data: &[u8]) -> Result<T>where T: for<'de> Deserialize<'de> // Forces owned deserialization!{ serde_json::from_slice(data) // Can't borrow from data}
// ✅ CORRECT - Use method-level lifetimefn good<'de, T>(data: &'de [u8]) -> Result<T>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<A: AgentSession>`
```rustuse jacquard::client::{Agent, CredentialSession, MemorySessionStore};use jacquard::identity::PublicResolver;
// App password authlet (session, _info) = CredentialSession::authenticated( "alice.bsky.social".into(), "app-password".into(), None, // session_id).await?;let agent = Agent::from(session);
// Make typed XRPC callsuse 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):
```rustuse jacquard::api::app_bsky::feed::post::Post;
// Createlet 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::<Post>(&uri).await?;let post_output = response.parse()?;
// Update with fetch-modify-put patternagent.update_record::<Profile>(&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<String>)` - 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<str>).
**XRPC layer**:
```rust// Stateless XRPC (with any HttpClient)use jacquard::common::xrpc::XrpcExt;let http = reqwest::Client::new();let response = http .xrpc(Uri::parse("https://bsky.social").map_err(|(e, _)| e)?.to_owned()) .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 → typedlet 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):
```rustpub trait Collection { const NSID: &'static str; type Record: XrpcResp; // Marker type for get_record()}
// Enables typed record retrieval:let response: Response<Post::Record> = 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<SmolStr, Data<'a>>` 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<app_bsky::embed::images::View<'a>>), CustomImages(Box<sh_custom::embed::images::View<'a>>),}```
**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 impl5. Helper: `Post::uri()` for constructing typed URIs
**For each XRPC endpoint**:1. Request struct with builder2. Output struct3. 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<SmolStr, Data<'a>>}```
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**:
```rustuse 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 tokenlet 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.<handle>` (feature: `dns`, skipped on WASM)2. HTTPS `https://<handle>/.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 Slingshot3. PDS XRPC `com.atproto.identity.resolveDid`
```rustuse jacquard::identity::{JacquardResolver, PublicResolver};
let resolver = PublicResolver::default(); // DNS + public fallbacks enabled
// Handle → DIDlet did: Did<'static> = resolver.resolve_handle(&handle).await?;
// DID → Documentlet response = resolver.resolve_did_doc(&did).await?;let doc = response.parse_validated()?; // Validates doc.id matches requested DID
// Combined: Get PDS endpointlet pds_url = resolver.pds_for_did(&did).await?;```
**DidDocResponse pattern** (same as XRPC responses):
```rustlet response = resolver.resolve_did_doc(&did).await?;
// Borrow from bufferlet doc: DidDocument<'_> = response.parse()?;
// Validate doc IDlet doc = response.parse_validated()?; // Error if doc.id != requested DID
// Convert to ownedlet 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 URLlet (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 DIDlet 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 URIs with string equality (normalise trailing slashes before comparison)- 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.
```rustuse jacquard_axum::{ExtractXrpc, IntoRouter};use jacquard::api::com_atproto::identity::resolve_handle::ResolveHandleRequest;
async fn handle_resolve( ExtractXrpc(req): ExtractXrpc<ResolveHandleRequest>) -> Json<ResolveHandleOutput<'static>> { let did = resolve_handle_logic(&req.handle).await; Json(ResolveHandleOutput { did, extra_data: Default::default() })}
// Automatic routinglet 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**:
```rustimpl XrpcRequest for MyRequest<'_> { fn decode_body<'de>(body: &'de [u8]) -> Result<Box<Self>> { let req = serde_ipld_dagcbor::from_slice(body)?; Ok(Box::new(req)) }}```
**Service auth** (feature: `service-auth`):
```rustuse 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:
```rustif 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.
```rustuse jacquard_repo::mst::Mst;use jacquard_repo::storage::MemoryBlockStore;
let storage = MemoryBlockStore::new();let mst = Mst::new(storage.clone());
// ⚠️ IMMUTABLE: Always reassignlet 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 CIDlet root_cid = mst.persist().await?;```
**Key validation**: `[a-zA-Z0-9._:~-]+` with exactly one `/` separator. Max 256 bytes. Format: `collection/rkey`.
**Diff operations**:
```rustlet diff = old_mst.diff(&new_mst).await?;diff.validate_limits()?; // Enforce 200 op limit (protocol)
// Convert to different formatslet verified_ops = diff.to_verified_ops(); // For batch()let repo_ops = diff.to_repo_ops(); // For firehose```
**Commits**:
```rustuse jacquard_repo::commit::Commit;
// Create and signlet commit = Commit::new_unsigned(did, data_cid, rev, prev_cid) .sign(&signing_key)?;
// Verifycommit.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// Readlet parsed = parse_car_bytes(&bytes)?;let root_cid = parsed.root;let blocks = parsed.blocks; // BTreeMap<CID, Bytes>
// Writelet bytes = write_car_bytes(root_cid, blocks)?;```
**BlockStore trait**: Pluggable storage backend.
```rust#[trait_variant::make(Send)] // Conditionally Send on non-WASMpub trait BlockStore: Clone { async fn get(&self, cid: &CID) -> Result<Option<Bytes>>; async fn put(&self, data: &[u8]) -> Result<CID>; 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):
```rustuse 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 commitlet 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:
```bashjust lex-gen # Fetch + generatejust lex-fetch # Fetch onlyjust 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 → BskyImagessh.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:
```tomlnet_anisota = ["app_bsky"] # Uses Bluesky embeds```
**Token types**: Unit structs with `Display` impl:
```rustpub 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// ❌ BADlet did_str = did.as_str().to_string();let did2 = Did::new(&did_str)?;
// ✅ GOODlet did2 = did.clone();// orlet did2 = did.into_static(); // If you need 'static```
### ❌ Using serde_json::Value
```rust// ❌ NEVERlet value: serde_json::Value = serde_json::from_slice(bytes)?;let post: Post = serde_json::from_value(value)?;
// ✅ ALWAYSlet 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 responsesstruct 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 COMPILElet output = { let response = agent.send(request).await?; response.parse()? // Borrows from response!};
// ✅ Keep response alive OR convert to ownedlet response = agent.send(request).await?;let output = response.parse()?;// ORlet 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<Post<'static>> = 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 VARIANTSmatch 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 VULNERABILITYlet token_response = exchange_code(...).await?;// Immediately trusting token_response.sub without verification!
// ✅ ALWAYS VERIFYlet 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 typesfn deserialize_anything<T>(data: &[u8]) -> Result<T>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 contextsstruct MyContainer<T>where T: for<'de> Deserialize<'de> // No Jacquard type can be stored here!{ value: T,}
// ❌ Even if you think you need it for asyncasync fn fetch<T>(url: &str) -> Result<T>where T: for<'de> Deserialize<'de> // Still wrong! Use Response<R> 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 lifetimefn deserialize_anything<'de, T>(data: &'de [u8]) -> Result<T>where T: Deserialize<'de>{ serde_json::from_slice(data)}
// ✅ CORRECT - With IntoStatic for asyncfn deserialize_owned<'de, T>(data: &'de [u8]) -> Result<T::Output>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 asyncasync fn fetch<R: XrpcRequest>(url: &str) -> Result<Response<R::Response>> { 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**:
```bashjust check-wasm# orcargo 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
```rustuse 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
```rustuse 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
```rustuse jacquard::identity::PublicResolver;
let resolver = PublicResolver::default();
// Handle → DIDlet did = resolver.resolve_handle(&handle).await?;
// DID → PDS endpointlet pds = resolver.pds_for_did(&did).await?;
// Combinedlet (did, pds) = resolver.pds_for_handle(&handle).await?;```
### OAuth login
```rustuse 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
```rustuse jacquard_axum::{ExtractXrpc, IntoRouter};use axum::{Router, Json};
async fn handler( ExtractXrpc(req): ExtractXrpc<MyRequest>) -> Json<MyOutput<'static>> { // Process request Json(output)}
let app = Router::new() .merge(MyRequest::into_router(handler));```
### MST operations
```rustuse 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 code2. **Zero-copy by default** - Borrow from buffers, convert to owned only when needed3. **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 typing5. **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.