+34
-22
README.md
+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
+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
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
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
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
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
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
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
+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
+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.