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