atproto utils for zig zat.dev
atproto sdk zig
at main 292 lines 8.2 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 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)