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)