atproto utils for zig zat.dev
atproto sdk zig
26
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 300 lines 8.5 kB view raw view rendered
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)