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>did resolution</strong> - resolve did:plc and did:web to documents</summary> 53 54```zig 55var resolver = zat.DidResolver.init(allocator); 56defer resolver.deinit(); 57 58const did = zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; 59var doc = try resolver.resolve(did); 60defer doc.deinit(); 61 62const handle = doc.handle(); // "bsky.app" 63const pds = doc.pdsEndpoint(); // "https://..." 64const key = doc.signingKey(); // verification method 65``` 66 67</details> 68 69<details> 70<summary><strong>handle resolution</strong> - resolve handles to DIDs via HTTP well-known</summary> 71 72```zig 73var resolver = zat.HandleResolver.init(allocator); 74defer resolver.deinit(); 75 76const handle = zat.Handle.parse("bsky.app").?; 77const did = try resolver.resolve(handle); 78defer allocator.free(did); 79// did = "did:plc:z72i7hdynmk6r22z27h6tvur" 80``` 81 82</details> 83 84<details> 85<summary><strong>xrpc client</strong> - call AT Protocol endpoints</summary> 86 87```zig 88var client = zat.XrpcClient.init(allocator, "https://bsky.social"); 89defer client.deinit(); 90 91const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?; 92var response = try client.query(nsid, params); 93defer response.deinit(); 94 95if (response.ok()) { 96 var json = try response.json(); 97 defer json.deinit(); 98 // use json.value 99} 100``` 101 102</details> 103 104<details> 105<summary><strong>sync types</strong> - enums for firehose/event stream consumption</summary> 106 107```zig 108// use in struct definitions for automatic json parsing: 109const RepoOp = struct { 110 action: zat.CommitAction, // .create, .update, .delete 111 path: []const u8, 112 cid: ?[]const u8, 113}; 114 115// then exhaustive switch: 116switch (op.action) { 117 .create, .update => processUpsert(op), 118 .delete => processDelete(op), 119} 120``` 121 122- **CommitAction** - `.create`, `.update`, `.delete` 123- **EventKind** - `.commit`, `.sync`, `.identity`, `.account`, `.info` 124- **AccountStatus** - `.takendown`, `.suspended`, `.deleted`, `.deactivated`, `.desynchronized`, `.throttled` 125 126</details> 127 128<details> 129<summary><strong>json helpers</strong> - navigate nested json without verbose if-chains</summary> 130 131```zig 132// runtime paths for one-offs: 133const uri = zat.json.getString(value, "embed.external.uri"); 134const count = zat.json.getInt(value, "meta.count"); 135 136// comptime extraction for complex structures: 137const FeedPost = struct { 138 uri: []const u8, 139 cid: []const u8, 140 record: struct { 141 text: []const u8 = "", 142 }, 143}; 144const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"}); 145``` 146 147</details> 148 149<details> 150<summary><strong>jwt verification</strong> - verify service auth tokens</summary> 151 152```zig 153var jwt = try zat.Jwt.parse(allocator, token_string); 154defer jwt.deinit(); 155 156// check claims 157if (jwt.isExpired()) return error.TokenExpired; 158if (!std.mem.eql(u8, jwt.payload.aud, expected_audience)) return error.InvalidAudience; 159 160// verify signature against issuer's public key (from DID document) 161try jwt.verify(public_key_multibase); 162``` 163 164supports ES256 (P-256) and ES256K (secp256k1) signing algorithms. 165 166</details> 167 168<details> 169<summary><strong>multibase decoding</strong> - decode public keys from DID documents</summary> 170 171```zig 172const key_bytes = try zat.multibase.decode(allocator, "zQ3sh..."); 173defer allocator.free(key_bytes); 174 175const parsed = try zat.multicodec.parsePublicKey(key_bytes); 176// parsed.key_type: .secp256k1 or .p256 177// parsed.raw: 33-byte compressed public key 178``` 179 180</details> 181 182## specs 183 184validation follows [atproto.com/specs](https://atproto.com/specs/atp). 185 186## versioning 187 188pre-1.0 semver: 189- `0.x.0` - new features (backwards compatible) 190- `0.x.y` - bug fixes 191 192breaking changes bump the minor version and are documented in commit messages. 193 194## license 195 196MIT 197 198--- 199 200[roadmap](docs/roadmap.md) · [changelog](CHANGELOG.md)