atproto utils for zig zat.dev
atproto sdk zig

docs: changelog and devlog for v0.2.0

devlog 003: trust chain verification — covers interop tests, MST,
code reorg, and the new repo verifier pipeline.

also registers devlog 002 and 003 in publish-docs script.

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

+108 -1
+5
CHANGELOG.md
··· 1 1 # changelog 2 2 3 + ## 0.2.0 4 + 5 + - **feat**: end-to-end repo verification — `verifyRepo(allocator, identifier)` exercises the full AT Protocol trust chain: handle → DID → DID document → signing key → fetch repo CAR → verify commit signature → walk MST → rebuild tree → CID match 6 + - **refactor**: organize `src/internal/` into domain subdirectories following bluesky-social/indigo: `syntax/`, `crypto/`, `identity/`, `repo/`, `xrpc/`, `streaming/`, `testing/` 7 + 3 8 ## 0.1.9 4 9 5 10 - **feat**: merkle search tree (MST) — `mst.Mst` with `put`, `get`, `delete`, `rootCid`
+1 -1
build.zig.zon
··· 1 1 .{ 2 2 .name = .zat, 3 - .version = "0.1.9", 3 + .version = "0.2.0", 4 4 .fingerprint = 0x8da9db57ee82fbe4, 5 5 .minimum_zig_version = "0.15.0", 6 6 .dependencies = .{
+100
devlog/003-trust-chain.md
··· 1 + # verifying the trust chain 2 + 3 + since the last devlog (firehose benchmarks), zat picked up a bunch of correctness work — interop test suites, signature fixes, a full MST implementation — and now ties it all together: given a handle, verify everything about a repo from scratch. 4 + 5 + ## what happened since last time 6 + 7 + ### correctness first (0.1.8) 8 + 9 + we joined the [atproto interop test suite](https://github.com/bluesky-social/atproto-interop-tests). this is bluesky's official cross-implementation test vectors — the same fixtures that the TypeScript SDK, Go SDK, and others validate against. zat now passes all of them: 10 + 11 + - **syntax**: 6 types (TID, DID, Handle, NSID, RecordKey, AT-URI), valid + invalid vectors 12 + - **crypto**: 6 signature verification vectors (P-256 and secp256k1) 13 + - **MST**: 9 key height vectors, 13 common prefix vectors, 6 commit proof fixtures 14 + 15 + this also surfaced two bugs: 16 + - NSID parser wasn't rejecting TLDs starting with a digit (`1.0.0.127.record` should fail) 17 + - AT-URI parser wasn't validating its components (authority, collection, rkey) — it was just splitting on `/` 18 + 19 + and a spec compliance issue: ECDSA signature verification wasn't rejecting high-S values. atproto requires low-S normalization (BIP-62 style), and we were accepting both. fixed with explicit half-order checks in `verifyP256` and `verifySecp256k1`. 20 + 21 + ### MST and crypto signing (0.1.9) 22 + 23 + the merkle search tree is the core data structure of an atproto repo. each key's tree layer is derived from the leading zero bits of SHA-256(key), and nodes are serialized with prefix compression. `mst.Mst` supports `put`, `get`, `delete`, and `rootCid` (serialize → hash → CID). 24 + 25 + alongside that: ECDSA signing (`signSecp256k1`, `signP256` with RFC 6979 deterministic nonces), `did:key` construction, and multibase encoding. these round out the crypto layer — zat can now both sign and verify. 26 + 27 + ### code organization (0.2.0) 28 + 29 + 22 files in a flat `src/internal/` was getting unwieldy. we reorganized into domain subdirectories following bluesky's own boundaries (from [bluesky-social/indigo](https://github.com/bluesky-social/indigo)): 30 + 31 + ``` 32 + internal/ 33 + syntax/ — tid, did, handle, nsid, rkey, at_uri 34 + crypto/ — jwt, multibase, multicodec 35 + identity/ — did_document, did_resolver, handle_resolver 36 + repo/ — cbor, car, mst, repo_verifier 37 + xrpc/ — transport, xrpc, json 38 + streaming/ — firehose, jetstream, sync 39 + testing/ — interop_tests 40 + ``` 41 + 42 + the groupings aren't arbitrary. in both indigo (Go) and the TypeScript SDK, `syntax` is pure parsing with zero deps, `identity` handles network resolution, `crypto` is P-256 + K-256, and `repo` contains the MST, CAR, and CBOR together (CBOR isn't a standalone package — it lives with the types that need it). 43 + 44 + ## the repo verifier 45 + 46 + `verifyRepo(allocator, "pfrazee.com")` exercises the entire trust chain in one call: 47 + 48 + ``` 49 + handle → DID → DID document → signing key 50 + 51 + repo CAR → commit → signature ← verified against key 52 + 53 + MST root CID → walk nodes → rebuild tree → CID match 54 + ``` 55 + 56 + the pipeline: 57 + 58 + 1. **resolve handle** — HTTP well-known or DNS TXT → DID string 59 + 2. **resolve DID** — did:plc via plc.directory, did:web via .well-known/did.json → DID document 60 + 3. **extract signing key** — find the `#atproto` verification method, multibase decode, multicodec parse → key type + raw bytes 61 + 4. **extract PDS endpoint** — find the `#atproto_pds` service 62 + 5. **fetch repo** — HTTP GET `{pds}/xrpc/com.atproto.sync.getRepo?did={did}` → raw CAR bytes 63 + 6. **parse CAR** — extract roots and blocks 64 + 7. **find + decode commit** — the root block is the signed commit (DAG-CBOR map with `did`, `version`, `rev`, `data`, `sig`) 65 + 8. **verify signature** — strip `sig` from the commit map, re-encode to DAG-CBOR (deterministic key ordering), verify with the signing key 66 + 9. **walk MST** — starting from the commit's `data` CID, recursively decode MST nodes with prefix decompression, collect all (key, value_cid) pairs 67 + 10. **rebuild MST** — insert every record into a fresh `mst.Mst`, compute root CID, compare against the commit's `data` CID 68 + 69 + if any step fails, you know exactly where the trust chain breaks. 70 + 71 + ### what this exercises 72 + 73 + every major module in zat participates: 74 + 75 + | step | modules used | 76 + |------|-------------| 77 + | handle resolution | `HandleResolver`, `Handle` | 78 + | DID resolution | `DidResolver`, `Did`, `DidDocument` | 79 + | key extraction | `multibase`, `multicodec` | 80 + | HTTP fetch | `HttpTransport` | 81 + | repo parsing | `car`, `cbor` | 82 + | signature verification | `jwt.verifyP256` / `jwt.verifySecp256k1` | 83 + | MST walk + rebuild | `mst.Mst`, `cbor.Value` | 84 + 85 + it's the first feature that crosses all the domain boundaries — identity, crypto, repo, and network all working together. 86 + 87 + ### the integration tests 88 + 89 + two accounts, two PDS backends: 90 + 91 + - **zzstoatzz.io** — self-hosted PDS (`pds.zzstoatzz.io`), ~12k records. verifies the self-hosting path works. 92 + - **pfrazee.com** — bluesky CTO, hosted on `bsky.network`, ~192k records. verifies against the canonical infrastructure. 93 + 94 + both use the graceful-catch pattern: if the network isn't available (CI, offline), the test prints a message and passes. when the network is there, it runs the full chain and asserts on the DID and record count. 95 + 96 + ## what's next 97 + 98 + this is the first "full pipeline" feature — it validates that the primitives compose correctly end to end. from here, the natural next steps are incremental: repo diffing (compare two commits), record-level verification (check a specific record's inclusion proof), or sync protocol support. 99 + 100 + but following the pattern: we ship when something real needs it, not before.
+2
scripts/publish-docs.zig
··· 15 15 /// devlog entries 16 16 const devlog = [_]DocEntry{ 17 17 .{ .path = "/devlog/001", .file = "devlog/001-self-publishing-docs.md" }, 18 + .{ .path = "/devlog/002", .file = "devlog/002-firehose-and-benchmarks.md" }, 19 + .{ .path = "/devlog/003", .file = "devlog/003-trust-chain.md" }, 18 20 }; 19 21 20 22 pub fn main() !void {