atproto utils for zig zat.dev
atproto sdk zig

docs: README rewrite, embed chart in devlog 005

README was missing everything since 0.1.3 — added CBOR, CAR, MST,
firehose, jetstream, signing, repo verification sections. embedded
verify-compute.svg in devlog 005. updated changelog and roadmap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+288 -79
+7
CHANGELOG.md
··· 1 1 # changelog 2 2 3 + ## 0.2.4 4 + 5 + - **feat**: configurable CAR size limits — `max_size` and `max_blocks` options in `readWithOptions` for large repo verification 6 + - **feat**: export `jwt` module (not just `Jwt` type) for direct access to `verifySecp256k1`/`verifyP256` 7 + - **docs**: devlog 005 — three-way trust chain verification (zig vs Go vs Rust) 8 + - **docs**: README rewrite — added CBOR, CAR, MST, firehose, jetstream, signing, repo verification 9 + 3 10 ## 0.2.3 4 11 5 12 - **docs**: devlog 004 — the sig-verify saga (k256 5×52-bit field, Fermat scalar inversion, three-way bench with rsky)
+164 -72
README.md
··· 5 5 <details> 6 6 <summary><strong>this readme is an ATProto record</strong></summary> 7 7 8 - → [view in zat.dev's repository](https://at-me.zzstoatzz.io/view?handle=zat.dev) 8 + > [view in zat.dev's repository](https://at-me.zzstoatzz.io/view?handle=zat.dev) 9 9 10 10 zat publishes these docs as [`site.standard.document`](https://standard.site) records, signed by its DID. 11 11 ··· 49 49 </details> 50 50 51 51 <details> 52 - <summary><strong>did resolution</strong> - resolve did:plc and did:web to documents</summary> 52 + <summary><strong>identity resolution</strong> - resolve handles and DIDs to documents</summary> 53 53 54 54 ```zig 55 - var resolver = zat.DidResolver.init(allocator); 56 - defer resolver.deinit(); 55 + // handle → DID 56 + var handle_resolver = zat.HandleResolver.init(allocator); 57 + defer handle_resolver.deinit(); 58 + const did = try handle_resolver.resolve(zat.Handle.parse("bsky.app").?); 59 + defer allocator.free(did); 57 60 58 - const did = zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; 59 - var doc = try resolver.resolve(did); 61 + // DID → document 62 + var did_resolver = zat.DidResolver.init(allocator); 63 + defer did_resolver.deinit(); 64 + var doc = try did_resolver.resolve(zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?); 60 65 defer doc.deinit(); 61 66 62 - const handle = doc.handle(); // "bsky.app" 63 - const pds = doc.pdsEndpoint(); // "https://..." 64 - const key = doc.signingKey(); // verification method 67 + const pds = doc.pdsEndpoint(); // "https://..." 68 + const key = doc.signingKey(); // verification method 65 69 ``` 66 70 71 + supports did:plc (via plc.directory) and did:web. handle resolution via HTTP well-known and DNS TXT. 72 + 67 73 </details> 68 74 69 75 <details> 70 - <summary><strong>handle resolution</strong> - resolve handles to DIDs via HTTP well-known</summary> 76 + <summary><strong>CBOR codec</strong> - DAG-CBOR encoding and decoding</summary> 71 77 72 78 ```zig 73 - var resolver = zat.HandleResolver.init(allocator); 74 - defer resolver.deinit(); 79 + // decode 80 + const decoded = try zat.cbor.decode(allocator, bytes); 81 + defer decoded.deinit(); 82 + 83 + // navigate values 84 + const text = decoded.value.getStr("text"); 85 + const cid = decoded.value.getCid("data"); 75 86 76 - const handle = zat.Handle.parse("bsky.app").?; 77 - const did = try resolver.resolve(handle); 78 - defer allocator.free(did); 79 - // did = "did:plc:z72i7hdynmk6r22z27h6tvur" 87 + // encode (deterministic key ordering) 88 + const encoded = try zat.cbor.encodeAlloc(allocator, value); 89 + defer allocator.free(encoded); 80 90 ``` 81 91 92 + full 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 + 82 94 </details> 83 95 84 96 <details> 85 - <summary><strong>xrpc client</strong> - call AT Protocol endpoints</summary> 97 + <summary><strong>CAR codec</strong> - Content Addressable aRchive parsing with CID verification</summary> 86 98 87 99 ```zig 88 - var client = zat.XrpcClient.init(allocator, "https://bsky.social"); 89 - defer client.deinit(); 90 - 91 - const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?; 92 - var response = try client.query(nsid, params); 93 - defer response.deinit(); 100 + // parse with SHA-256 CID verification (default) 101 + const parsed = try zat.car.read(allocator, car_bytes); 102 + defer parsed.deinit(); 94 103 95 - if (response.ok()) { 96 - var json = try response.json(); 97 - defer json.deinit(); 98 - // use json.value 104 + const root_cid = parsed.roots[0]; 105 + for (parsed.blocks.items) |block| { 106 + // block.cid_raw, block.data 99 107 } 108 + 109 + // skip verification for trusted local data 110 + const fast = try zat.car.readWithOptions(allocator, car_bytes, .{ 111 + .verify_block_hashes = false, 112 + }); 100 113 ``` 101 114 115 + enforces size limits (configurable `max_size`, `max_blocks`) matching indigo's production defaults. 116 + 102 117 </details> 103 118 104 119 <details> 105 - <summary><strong>sync types</strong> - enums for firehose/event stream consumption</summary> 120 + <summary><strong>MST</strong> - Merkle Search Tree</summary> 106 121 107 122 ```zig 108 - // use in struct definitions for automatic json parsing: 109 - const RepoOp = struct { 110 - action: zat.CommitAction, // .create, .update, .delete 111 - path: []const u8, 112 - cid: ?[]const u8, 113 - }; 123 + var tree = zat.mst.Mst.init(allocator); 124 + defer tree.deinit(); 125 + 126 + try tree.put(allocator, "app.bsky.feed.post/abc123", value_cid); 127 + const found = tree.get("app.bsky.feed.post/abc123"); 128 + try tree.delete(allocator, "app.bsky.feed.post/abc123"); 114 129 115 - // then exhaustive switch: 116 - switch (op.action) { 117 - .create, .update => processUpsert(op), 118 - .delete => processDelete(op), 119 - } 130 + // compute root CID (serialize → hash → CID) 131 + const root = try tree.rootCid(allocator); 120 132 ``` 121 133 122 - - **CommitAction** - `.create`, `.update`, `.delete` 123 - - **EventKind** - `.commit`, `.sync`, `.identity`, `.account`, `.info` 124 - - **AccountStatus** - `.takendown`, `.suspended`, `.deleted`, `.deactivated`, `.desynchronized`, `.throttled` 134 + the core data structure of an atproto repo. key layer derived from leading zero bits of SHA-256(key), nodes serialized with prefix compression. 125 135 126 136 </details> 127 137 128 138 <details> 129 - <summary><strong>json helpers</strong> - navigate nested json without verbose if-chains</summary> 139 + <summary><strong>crypto</strong> - signing, verification, key encoding</summary> 130 140 131 141 ```zig 132 - // runtime paths for one-offs: 133 - const uri = zat.json.getString(value, "embed.external.uri"); 134 - const count = zat.json.getInt(value, "meta.count"); 142 + // JWT verification 143 + var token = try zat.Jwt.parse(allocator, token_string); 144 + defer token.deinit(); 145 + try token.verify(public_key_multibase); 146 + 147 + // ECDSA signature verification (P-256 and secp256k1) 148 + try zat.jwt.verifySecp256k1(hash, signature, public_key); 149 + try zat.jwt.verifyP256(hash, signature, public_key); 135 150 136 - // comptime extraction for complex structures: 137 - const FeedPost = struct { 138 - uri: []const u8, 139 - cid: []const u8, 140 - record: struct { 141 - text: []const u8 = "", 142 - }, 143 - }; 144 - const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"}); 151 + // multibase/multicodec key parsing 152 + const key_bytes = try zat.multibase.decode(allocator, "zQ3sh..."); 153 + defer allocator.free(key_bytes); 154 + const parsed = try zat.multicodec.parsePublicKey(key_bytes); 155 + // parsed.key_type: .secp256k1 or .p256 156 + // parsed.raw: 33-byte compressed public key 145 157 ``` 146 158 159 + ES256 (P-256) and ES256K (secp256k1) with low-S normalization. RFC 6979 deterministic signing. `did:key` construction and multibase encoding. 160 + 147 161 </details> 148 162 149 163 <details> 150 - <summary><strong>jwt verification</strong> - verify service auth tokens</summary> 164 + <summary><strong>repo verification</strong> - full AT Protocol trust chain</summary> 151 165 152 166 ```zig 153 - var jwt = try zat.Jwt.parse(allocator, token_string); 154 - defer jwt.deinit(); 167 + const result = try zat.verifyRepo(allocator, "pfrazee.com"); 168 + defer result.deinit(); 155 169 156 - // check claims 157 - if (jwt.isExpired()) return error.TokenExpired; 158 - if (!std.mem.eql(u8, jwt.payload.aud, expected_audience)) return error.InvalidAudience; 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 + 176 + given 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 184 + var client = zat.FirehoseClient.init(allocator, .{}); 185 + defer client.deinit(); 159 186 160 - // verify signature against issuer's public key (from DID document) 161 - try jwt.verify(public_key_multibase); 187 + try client.connect(); 188 + while (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 + } 162 197 ``` 163 198 164 - supports ES256 (P-256) and ES256K (secp256k1) signing algorithms. 199 + connects to `com.atproto.sync.subscribeRepos` via WebSocket. decodes binary CBOR frames into typed events. round-robin host rotation with backoff. 165 200 166 201 </details> 167 202 168 203 <details> 169 - <summary><strong>multibase decoding</strong> - decode public keys from DID documents</summary> 204 + <summary><strong>jetstream client</strong> - typed JSON event stream</summary> 170 205 171 206 ```zig 172 - const key_bytes = try zat.multibase.decode(allocator, "zQ3sh..."); 173 - defer allocator.free(key_bytes); 207 + var client = zat.JetstreamClient.init(allocator, .{ 208 + .wanted_collections = &.{"app.bsky.feed.post"}, 209 + }); 210 + defer client.deinit(); 174 211 175 - const parsed = try zat.multicodec.parsePublicKey(key_bytes); 176 - // parsed.key_type: .secp256k1 or .p256 177 - // parsed.raw: 33-byte compressed public key 212 + try client.connect(); 213 + while (try client.next()) |event| { 214 + if (event.commit) |commit| { 215 + const record = commit.record; 216 + // process... 217 + } 218 + } 219 + ``` 220 + 221 + connects 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 229 + var client = zat.XrpcClient.init(allocator, "https://bsky.social"); 230 + defer client.deinit(); 231 + 232 + const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?; 233 + var response = try client.query(nsid, params); 234 + defer response.deinit(); 235 + 236 + if (response.ok()) { 237 + var json = try response.json(); 238 + defer json.deinit(); 239 + // use json.value 240 + } 178 241 ``` 179 242 180 243 </details> 181 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: 250 + const uri = zat.json.getString(value, "embed.external.uri"); 251 + const count = zat.json.getInt(value, "meta.count"); 252 + 253 + // comptime extraction for complex structures: 254 + const FeedPost = struct { 255 + uri: []const u8, 256 + cid: []const u8, 257 + record: struct { 258 + text: []const u8 = "", 259 + }, 260 + }; 261 + const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"}); 262 + ``` 263 + 264 + </details> 265 + 266 + ## benchmarks 267 + 268 + zat 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 + 182 274 ## specs 183 275 184 - validation follows [atproto.com/specs](https://atproto.com/specs/atp). 276 + validation 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). 185 277 186 278 ## versioning 187 279 ··· 197 289 198 290 --- 199 291 200 - [roadmap](docs/roadmap.md) · [changelog](CHANGELOG.md) 292 + [devlog](devlog/) · [changelog](CHANGELOG.md)
+2
devlog/005-three-way-verify.md
··· 38 38 39 39 _pfrazee.com — 192,144 records, 243,470 blocks, 70.6 MB CAR, macOS arm64 (M3 Max)_ 40 40 41 + <img src="img/verify-compute.svg" alt="trust chain compute breakdown" width="790"> 42 + 41 43 | SDK | CAR parse | sig verify | MST walk | MST rebuild | compute total | 42 44 |-----|----------:|----------:|---------:|------------:|-------------:| 43 45 | zig (zat) | 81.6ms | 0.6ms | 45.5ms | 172.6ms | **300.4ms** |
+47
devlog/img/verify-compute.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 790 267" font-family="'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace"> 2 + <rect width="790" height="267" fill="#1a1a2e" rx="8"/> 3 + <text x="395.0" y="28" text-anchor="middle" fill="#e0e0e0" font-size="15" font-weight="600">AT Protocol trust chain — compute</text> 4 + <text x="395.0" y="44" text-anchor="middle" fill="#666" font-size="11">192,144 records</text> 5 + <line x1="160.0" y1="53" x2="160.0" y2="211" stroke="#262640" stroke-width="1"/> 6 + <line x1="264.0" y1="53" x2="264.0" y2="211" stroke="#262640" stroke-width="1"/> 7 + <line x1="368.0" y1="53" x2="368.0" y2="211" stroke="#262640" stroke-width="1"/> 8 + <line x1="472.0" y1="53" x2="472.0" y2="211" stroke="#262640" stroke-width="1"/> 9 + <line x1="576.0" y1="53" x2="576.0" y2="211" stroke="#262640" stroke-width="1"/> 10 + <line x1="680.0" y1="53" x2="680.0" y2="211" stroke="#262640" stroke-width="1"/> 11 + <text x="146" y="79.0" text-anchor="end" fill="#c0c0c0" font-size="13">zig (zat)</text> 12 + <rect x="160.0" y="55" width="100.5" height="38" fill="#e8944a" rx="3"/> 13 + <text x="210.3" y="78.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">CAR parse</text> 14 + <rect x="260.5" y="55" width="1.0" height="38" fill="#ed7d31" rx="3"/> 15 + <rect x="261.5" y="55" width="56.1" height="38" fill="#c55a11" rx="3"/> 16 + <text x="289.6" y="78.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">MST walk</text> 17 + <rect x="317.6" y="55" width="212.6" height="38" fill="#a04000" rx="3"/> 18 + <text x="423.9" y="78.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">MST rebuild</text> 19 + <text x="540.2" y="79.0" fill="#a0a0a0" font-size="12" font-weight="500">300ms</text> 20 + <text x="146" y="137.0" text-anchor="end" fill="#c0c0c0" font-size="13">go (indigo)</text> 21 + <rect x="160.0" y="113" width="497.5" height="38" fill="#e8944a" rx="3"/> 22 + <text x="408.7" y="136.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">CAR parse</text> 23 + <rect x="657.5" y="113" width="1.0" height="38" fill="#ed7d31" rx="3"/> 24 + <rect x="658.5" y="113" width="7.1" height="38" fill="#c55a11" rx="3"/> 25 + <text x="675.6" y="137.0" fill="#a0a0a0" font-size="12" font-weight="500">410ms</text> 26 + <text x="146" y="195.0" text-anchor="end" fill="#c0c0c0" font-size="13">rust (RustCrypto)</text> 27 + <rect x="160.0" y="171" width="370.8" height="38" fill="#e8944a" rx="3"/> 28 + <text x="345.4" y="194.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">CAR parse</text> 29 + <rect x="530.8" y="171" width="1.0" height="38" fill="#ed7d31" rx="3"/> 30 + <rect x="531.8" y="171" width="148.9" height="38" fill="#c55a11" rx="3"/> 31 + <text x="606.3" y="194.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">MST walk</text> 32 + <text x="690.8" y="195.0" fill="#a0a0a0" font-size="12" font-weight="500">422ms</text> 33 + <text x="160.0" y="227" text-anchor="middle" fill="#606060" font-size="10">0</text> 34 + <text x="264.0" y="227" text-anchor="middle" fill="#606060" font-size="10">84ms</text> 35 + <text x="368.0" y="227" text-anchor="middle" fill="#606060" font-size="10">169ms</text> 36 + <text x="472.0" y="227" text-anchor="middle" fill="#606060" font-size="10">253ms</text> 37 + <text x="576.0" y="227" text-anchor="middle" fill="#606060" font-size="10">338ms</text> 38 + <text x="680.0" y="227" text-anchor="middle" fill="#606060" font-size="10">422ms</text> 39 + <rect x="160" y="241" width="10" height="10" fill="#e8944a" rx="2"/> 40 + <text x="174" y="249" fill="#808080" font-size="10">CAR parse</text> 41 + <rect x="242" y="241" width="10" height="10" fill="#ed7d31" rx="2"/> 42 + <text x="256" y="249" fill="#808080" font-size="10">sig verify</text> 43 + <rect x="330" y="241" width="10" height="10" fill="#c55a11" rx="2"/> 44 + <text x="344" y="249" fill="#808080" font-size="10">MST walk</text> 45 + <rect x="405" y="241" width="10" height="10" fill="#a04000" rx="2"/> 46 + <text x="419" y="249" fill="#808080" font-size="10">MST rebuild</text> 47 + </svg>
+59
devlog/img/verify-total.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 790 267" font-family="'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace"> 2 + <rect width="790" height="267" fill="#1a1a2e" rx="8"/> 3 + <text x="395.0" y="28" text-anchor="middle" fill="#e0e0e0" font-size="15" font-weight="600">AT Protocol trust chain — total (network + compute)</text> 4 + <text x="395.0" y="44" text-anchor="middle" fill="#666" font-size="11">192,144 records</text> 5 + <line x1="160.0" y1="53" x2="160.0" y2="211" stroke="#262640" stroke-width="1"/> 6 + <line x1="264.0" y1="53" x2="264.0" y2="211" stroke="#262640" stroke-width="1"/> 7 + <line x1="368.0" y1="53" x2="368.0" y2="211" stroke="#262640" stroke-width="1"/> 8 + <line x1="472.0" y1="53" x2="472.0" y2="211" stroke="#262640" stroke-width="1"/> 9 + <line x1="576.0" y1="53" x2="576.0" y2="211" stroke="#262640" stroke-width="1"/> 10 + <line x1="680.0" y1="53" x2="680.0" y2="211" stroke="#262640" stroke-width="1"/> 11 + <text x="146" y="79.0" text-anchor="end" fill="#c0c0c0" font-size="13">zig (zat)</text> 12 + <rect x="160.0" y="55" width="18.0" height="38" fill="#6ea8d9" rx="3"/> 13 + <rect x="178.0" y="55" width="8.4" height="38" fill="#5b9bd5" rx="3"/> 14 + <rect x="186.4" y="55" width="214.2" height="38" fill="#4472c4" rx="3"/> 15 + <text x="293.5" y="78.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">fetch</text> 16 + <rect x="400.6" y="55" width="2.0" height="38" fill="#e8944a" rx="3"/> 17 + <rect x="402.6" y="55" width="1.0" height="38" fill="#ed7d31" rx="3"/> 18 + <rect x="403.6" y="55" width="1.1" height="38" fill="#c55a11" rx="3"/> 19 + <rect x="404.7" y="55" width="4.2" height="38" fill="#a04000" rx="3"/> 20 + <text x="419.0" y="79.0" fill="#a0a0a0" font-size="12" font-weight="500">10.1s</text> 21 + <text x="146" y="137.0" text-anchor="end" fill="#c0c0c0" font-size="13">go (indigo)</text> 22 + <rect x="160.0" y="113" width="1.3" height="38" fill="#6ea8d9" rx="3"/> 23 + <rect x="161.3" y="113" width="3.6" height="38" fill="#5b9bd5" rx="3"/> 24 + <rect x="164.9" y="113" width="505.0" height="38" fill="#4472c4" rx="3"/> 25 + <text x="417.4" y="136.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">fetch</text> 26 + <rect x="670.0" y="113" width="9.9" height="38" fill="#e8944a" rx="3"/> 27 + <rect x="679.8" y="113" width="1.0" height="38" fill="#ed7d31" rx="3"/> 28 + <rect x="680.8" y="113" width="1.0" height="38" fill="#c55a11" rx="3"/> 29 + <text x="691.8" y="137.0" fill="#a0a0a0" font-size="12" font-weight="500">21.2s</text> 30 + <text x="146" y="195.0" text-anchor="end" fill="#c0c0c0" font-size="13">rust (RustCrypto)</text> 31 + <rect x="160.0" y="171" width="16.3" height="38" fill="#6ea8d9" rx="3"/> 32 + <rect x="176.3" y="171" width="5.0" height="38" fill="#5b9bd5" rx="3"/> 33 + <rect x="181.2" y="171" width="191.0" height="38" fill="#4472c4" rx="3"/> 34 + <text x="276.7" y="194.0" text-anchor="middle" fill="white" font-size="10" font-weight="500">fetch</text> 35 + <rect x="372.2" y="171" width="7.4" height="38" fill="#e8944a" rx="3"/> 36 + <rect x="379.6" y="171" width="1.0" height="38" fill="#ed7d31" rx="3"/> 37 + <rect x="380.6" y="171" width="3.0" height="38" fill="#c55a11" rx="3"/> 38 + <text x="393.6" y="195.0" fill="#a0a0a0" font-size="12" font-weight="500">9.1s</text> 39 + <text x="160.0" y="227" text-anchor="middle" fill="#606060" font-size="10">0</text> 40 + <text x="264.0" y="227" text-anchor="middle" fill="#606060" font-size="10">4.2s</text> 41 + <text x="368.0" y="227" text-anchor="middle" fill="#606060" font-size="10">8.5s</text> 42 + <text x="472.0" y="227" text-anchor="middle" fill="#606060" font-size="10">12.7s</text> 43 + <text x="576.0" y="227" text-anchor="middle" fill="#606060" font-size="10">17.0s</text> 44 + <text x="680.0" y="227" text-anchor="middle" fill="#606060" font-size="10">21.2s</text> 45 + <rect x="160" y="241" width="10" height="10" fill="#6ea8d9" rx="2"/> 46 + <text x="174" y="249" fill="#808080" font-size="10">handle</text> 47 + <rect x="223" y="241" width="10" height="10" fill="#5b9bd5" rx="2"/> 48 + <text x="237" y="249" fill="#808080" font-size="10">DID doc</text> 49 + <rect x="293" y="241" width="10" height="10" fill="#4472c4" rx="2"/> 50 + <text x="307" y="249" fill="#808080" font-size="10">fetch</text> 51 + <rect x="350" y="241" width="10" height="10" fill="#e8944a" rx="2"/> 52 + <text x="364" y="249" fill="#808080" font-size="10">CAR parse</text> 53 + <rect x="431" y="241" width="10" height="10" fill="#ed7d31" rx="2"/> 54 + <text x="445" y="249" fill="#808080" font-size="10">sig verify</text> 55 + <rect x="519" y="241" width="10" height="10" fill="#c55a11" rx="2"/> 56 + <text x="533" y="249" fill="#808080" font-size="10">MST walk</text> 57 + <rect x="595" y="241" width="10" height="10" fill="#a04000" rx="2"/> 58 + <text x="609" y="249" fill="#808080" font-size="10">MST rebuild</text> 59 + </svg>
+9 -7
docs/roadmap.md
··· 7 7 **initial scope** - string primitives with parsing and validation. the philosophy: primitives not frameworks, layered design, zig idioms, minimal scope. 8 8 9 9 **what grew from usage:** 10 - - DID resolution was originally "out of scope" - real projects needed it, so `DidResolver` and `DidDocument` got added 11 - - XRPC client and JSON helpers - same story 10 + - DID/handle resolution — real projects needed it, so `DidResolver`, `DidDocument`, `HandleResolver` got added 11 + - XRPC client and JSON helpers — same story 12 12 - JWT verification for service auth 13 - - handle resolution via HTTP well-known 14 - - handle resolution via DNS-over-HTTP (community contribution) 15 - - sync types for firehose consumption (`CommitAction`, `EventKind`, `AccountStatus`) 13 + - jetstream client — typed JSON event stream with reconnection (0.1.3) 14 + - firehose client — raw CBOR event stream, DAG-CBOR codec, CAR codec, CID creation (0.1.4) 15 + - MST, ECDSA signing, `did:key` construction, multibase encoding (0.1.9) 16 + - full repo verification — end-to-end trust chain from handle to MST root CID match (0.2.0) 17 + - CID hash verification in CAR parser (0.2.1), size limits (0.2.2) 16 18 17 19 this pattern - start minimal, expand based on real pain - continues. 18 20 19 21 ## now 20 22 21 - use zat in real projects. let usage drive what's next. 23 + the library covers the full AT Protocol verification pipeline: identity resolution, repo parsing, signature verification, and MST validation. benchmarked against Go (indigo) and Rust (rsky) in [atproto-bench](https://tangled.sh/@zzstoatzz.io/atproto-bench). 22 24 23 - the primitives are reasonably complete. what's missing will show up when people build things. until then, no speculative features. 25 + what's missing will show up when people build things. until then, no speculative features. 24 26 25 27 ## maybe later 26 28