atproto utils for zig
zat.dev
atproto
sdk
zig
1# [zat](https://zat.dev)
2
3AT Protocol building blocks for zig.
4
5<details>
6<summary><strong>this readme is an ATProto record</strong></summary>
7
8> [view in zat.dev's repository](https://at-me.zzstoatzz.io/view?handle=zat.dev)
9
10zat publishes these docs as [`site.standard.document`](https://standard.site) records, signed by its DID.
11
12</details>
13
14## install
15
16requires zig 0.16+.
17
18```bash
19zig fetch --save https://tangled.org/zat.dev/zat/archive/main
20```
21
22then in `build.zig`:
23
24```zig
25const zat = b.dependency("zat", .{}).module("zat");
26exe.root_module.addImport("zat", zat);
27```
28
29## what's here
30
31<details>
32<summary><strong>string primitives</strong> - parsing and validation for atproto identifiers</summary>
33
34- **Tid** - timestamp identifiers (base32-sortable)
35- **Did** - decentralized identifiers
36- **Handle** - domain-based handles
37- **Nsid** - namespaced identifiers (lexicon types)
38- **Rkey** - record keys
39- **AtUri** - `at://` URIs
40
41```zig
42const zat = @import("zat");
43
44if (zat.AtUri.parse(uri_string)) |uri| {
45 const authority = uri.authority();
46 const collection = uri.collection();
47 const rkey = uri.rkey();
48}
49```
50
51</details>
52
53<details>
54<summary><strong>identity resolution</strong> - resolve handles and DIDs to documents</summary>
55
56```zig
57// handle → DID
58var handle_resolver = zat.HandleResolver.init(io, allocator);
59defer handle_resolver.deinit();
60const did = try handle_resolver.resolve(zat.Handle.parse("bsky.app").?);
61defer allocator.free(did);
62
63// DID → document
64var did_resolver = zat.DidResolver.init(io, allocator);
65defer did_resolver.deinit();
66var doc = try did_resolver.resolve(zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?);
67defer doc.deinit();
68
69const pds = doc.pdsEndpoint(); // "https://..."
70const key = doc.signingKey(); // verification method
71```
72
73supports did:plc (via plc.directory) and did:web. handle resolution via HTTP well-known and DNS TXT.
74
75</details>
76
77<details>
78<summary><strong>CBOR codec</strong> - DAG-CBOR encoding and decoding</summary>
79
80```zig
81// decode
82const decoded = try zat.cbor.decode(allocator, bytes);
83defer decoded.deinit();
84
85// navigate values
86const text = decoded.value.getStr("text");
87const cid = decoded.value.getCid("data");
88
89// encode (deterministic key ordering)
90const encoded = try zat.cbor.encodeAlloc(allocator, value);
91defer allocator.free(encoded);
92```
93
94full DAG-CBOR support: maps, arrays, byte strings, text strings, integers, floats, booleans, null, CID tags (tag 42). deterministic encoding with sorted keys for signature verification.
95
96</details>
97
98<details>
99<summary><strong>CAR codec</strong> - Content Addressable aRchive parsing with CID verification</summary>
100
101```zig
102// parse with SHA-256 CID verification (default)
103const parsed = try zat.car.read(allocator, car_bytes);
104defer parsed.deinit();
105
106const root_cid = parsed.roots[0];
107for (parsed.blocks.items) |block| {
108 // block.cid_raw, block.data
109}
110
111// skip verification for trusted local data
112const fast = try zat.car.readWithOptions(allocator, car_bytes, .{
113 .verify_block_hashes = false,
114});
115```
116
117enforces size limits (configurable `max_size`, `max_blocks`) matching indigo's production defaults.
118
119</details>
120
121<details>
122<summary><strong>MST</strong> - Merkle Search Tree</summary>
123
124```zig
125var tree = zat.mst.Mst.init(allocator);
126defer tree.deinit();
127
128try tree.put(allocator, "app.bsky.feed.post/abc123", value_cid);
129const found = tree.get("app.bsky.feed.post/abc123");
130try tree.delete(allocator, "app.bsky.feed.post/abc123");
131
132// compute root CID (serialize → hash → CID)
133const root = try tree.rootCid(allocator);
134```
135
136the core data structure of an atproto repo. key layer derived from leading zero bits of SHA-256(key), nodes serialized with prefix compression.
137
138</details>
139
140<details>
141<summary><strong>crypto</strong> - signing, verification, key encoding</summary>
142
143```zig
144// JWT verification
145var token = try zat.Jwt.parse(allocator, token_string);
146defer token.deinit();
147try token.verify(public_key_multibase);
148
149// ECDSA signature verification (P-256 and secp256k1)
150try zat.jwt.verifySecp256k1(hash, signature, public_key);
151try zat.jwt.verifyP256(hash, signature, public_key);
152
153// multibase/multicodec key parsing
154const key_bytes = try zat.multibase.decode(allocator, "zQ3sh...");
155defer allocator.free(key_bytes);
156const parsed = try zat.multicodec.parsePublicKey(key_bytes);
157// parsed.key_type: .secp256k1 or .p256
158// parsed.raw: 33-byte compressed public key
159```
160
161ES256 (P-256) and ES256K (secp256k1) with low-S normalization. RFC 6979 deterministic signing. `did:key` construction and multibase encoding.
162
163</details>
164
165<details>
166<summary><strong>repo verification</strong> - full AT Protocol trust chain</summary>
167
168```zig
169const result = try zat.verifyRepo(io, allocator, "pfrazee.com");
170defer result.deinit();
171
172// result.did, result.signing_key, result.pds_endpoint
173// result.record_count, result.block_count
174// result.commit_verified (signature check passed)
175// result.root_cid_match (MST rebuild matches commit)
176```
177
178given a handle or DID, resolves identity, fetches the repo, parses every CAR block with SHA-256 verification, verifies the commit signature, walks the MST, and rebuilds the tree to verify the root CID.
179
180</details>
181
182<details>
183<summary><strong>firehose client</strong> - raw CBOR event stream from relay</summary>
184
185```zig
186var client = zat.FirehoseClient.init(io, allocator, .{});
187defer client.deinit();
188
189const Handler = struct {
190 pub fn onEvent(_: *@This(), event: zat.FirehoseClient.Event) void {
191 switch (event.header.type) {
192 .commit => {
193 // event.body.blocks, event.body.ops, ...
194 },
195 else => {},
196 }
197 }
198};
199var handler: Handler = .{};
200try client.subscribe(&handler);
201```
202
203connects to `com.atproto.sync.subscribeRepos` via WebSocket. decodes binary CBOR frames into typed events. round-robin host rotation with backoff.
204
205</details>
206
207<details>
208<summary><strong>jetstream client</strong> - typed JSON event stream</summary>
209
210```zig
211var client = zat.JetstreamClient.init(io, allocator, .{
212 .wanted_collections = &.{"app.bsky.feed.post"},
213});
214defer client.deinit();
215
216const Handler = struct {
217 pub fn onEvent(_: *@This(), event: zat.JetstreamClient.Event) void {
218 if (event.commit) |commit| {
219 const record = commit.record;
220 // process...
221 _ = record;
222 }
223 }
224};
225var handler: Handler = .{};
226try client.subscribe(&handler);
227```
228
229connects to jetstream (bluesky's JSON event stream). typed events, automatic reconnection with cursor tracking, round-robin across community relays.
230
231</details>
232
233<details>
234<summary><strong>xrpc client</strong> - call AT Protocol endpoints</summary>
235
236```zig
237var client = zat.XrpcClient.init(io, allocator, "https://bsky.social");
238defer client.deinit();
239
240const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?;
241var response = try client.query(nsid, params);
242defer response.deinit();
243
244if (response.ok()) {
245 var json = try response.json();
246 defer json.deinit();
247 // use json.value
248}
249```
250
251</details>
252
253<details>
254<summary><strong>json helpers</strong> - navigate nested json without verbose if-chains</summary>
255
256```zig
257// runtime paths for one-offs:
258const uri = zat.json.getString(value, "embed.external.uri");
259const count = zat.json.getInt(value, "meta.count");
260
261// comptime extraction for complex structures:
262const FeedPost = struct {
263 uri: []const u8,
264 cid: []const u8,
265 record: struct {
266 text: []const u8 = "",
267 },
268};
269const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"});
270```
271
272</details>
273
274## benchmarks
275
276zat is benchmarked against Go (indigo), Rust (rsky), and Python (atproto) in [atproto-bench](https://tangled.org/zzstoatzz.io/atproto-bench):
277
278- **decode**: 202k frames/sec (zig) vs 39k (rust) vs 15k (go) — with CID hash verification and full CBOR validation
279- **sig-verify**: 15k–19k verifies/sec across all three — ECDSA is table stakes
280- **trust chain**: full repo verification in ~300ms compute (zig) vs ~410ms (go) vs ~422ms (rust)
281
282## specs
283
284validation follows [atproto.com/specs](https://atproto.com/specs/atp). passes the [atproto interop test suite](https://github.com/bluesky-social/atproto-interop-tests) (syntax, crypto, MST vectors).
285
286## versioning
287
288pre-1.0 semver:
289- `0.x.0` - new features (backwards compatible)
290- `0.x.y` - bug fixes
291
292breaking changes bump the minor version and are documented in commit messages.
293
294## license
295
296MIT
297
298---
299
300[devlog](devlog/) · [changelog](CHANGELOG.md)