1# zat
2
3zig primitives for AT Protocol.
4
5## install
6
7```bash
8zig fetch --save https://tangled.sh/zzstoatzz.io/zat/archive/main
9```
10
11then in `build.zig`:
12
13```zig
14const zat = b.dependency("zat", .{}).module("zat");
15exe.root_module.addImport("zat", zat);
16```
17
18## what's here
19
20<details>
21<summary><strong>string primitives</strong> - parsing and validation for atproto identifiers</summary>
22
23- **Tid** - timestamp identifiers (base32-sortable)
24- **Did** - decentralized identifiers
25- **Handle** - domain-based handles
26- **Nsid** - namespaced identifiers (lexicon types)
27- **Rkey** - record keys
28- **AtUri** - `at://` URIs
29
30```zig
31const zat = @import("zat");
32
33if (zat.AtUri.parse(uri_string)) |uri| {
34 const authority = uri.authority();
35 const collection = uri.collection();
36 const rkey = uri.rkey();
37}
38```
39
40</details>
41
42<details>
43<summary><strong>did resolution</strong> - resolve did:plc and did:web to documents</summary>
44
45```zig
46var resolver = zat.DidResolver.init(allocator);
47defer resolver.deinit();
48
49const did = zat.Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?;
50var doc = try resolver.resolve(did);
51defer doc.deinit();
52
53const handle = doc.handle(); // "bsky.app"
54const pds = doc.pdsEndpoint(); // "https://..."
55const key = doc.signingKey(); // verification method
56```
57
58</details>
59
60<details>
61<summary><strong>handle resolution</strong> - resolve handles to DIDs via HTTP well-known</summary>
62
63```zig
64var resolver = zat.HandleResolver.init(allocator);
65defer resolver.deinit();
66
67const handle = zat.Handle.parse("bsky.app").?;
68const did = try resolver.resolve(handle);
69defer allocator.free(did);
70// did = "did:plc:z72i7hdynmk6r22z27h6tvur"
71```
72
73</details>
74
75<details>
76<summary><strong>xrpc client</strong> - call AT Protocol endpoints</summary>
77
78```zig
79var client = zat.XrpcClient.init(allocator, "https://bsky.social");
80defer client.deinit();
81
82const nsid = zat.Nsid.parse("app.bsky.actor.getProfile").?;
83var response = try client.query(nsid, params);
84defer response.deinit();
85
86if (response.ok()) {
87 var json = try response.json();
88 defer json.deinit();
89 // use json.value
90}
91```
92
93</details>
94
95<details>
96<summary><strong>sync types</strong> - enums for firehose/event stream consumption</summary>
97
98```zig
99// use in struct definitions for automatic json parsing:
100const RepoOp = struct {
101 action: zat.CommitAction, // .create, .update, .delete
102 path: []const u8,
103 cid: ?[]const u8,
104};
105
106// then exhaustive switch:
107switch (op.action) {
108 .create, .update => processUpsert(op),
109 .delete => processDelete(op),
110}
111```
112
113- **CommitAction** - `.create`, `.update`, `.delete`
114- **EventKind** - `.commit`, `.sync`, `.identity`, `.account`, `.info`
115- **AccountStatus** - `.takendown`, `.suspended`, `.deleted`, `.deactivated`, `.desynchronized`, `.throttled`
116
117</details>
118
119<details>
120<summary><strong>json helpers</strong> - navigate nested json without verbose if-chains</summary>
121
122```zig
123// runtime paths for one-offs:
124const uri = zat.json.getString(value, "embed.external.uri");
125const count = zat.json.getInt(value, "meta.count");
126
127// comptime extraction for complex structures:
128const FeedPost = struct {
129 uri: []const u8,
130 cid: []const u8,
131 record: struct {
132 text: []const u8 = "",
133 },
134};
135const post = try zat.json.extractAt(FeedPost, allocator, value, .{"post"});
136```
137
138</details>
139
140<details>
141<summary><strong>jwt verification</strong> - verify service auth tokens</summary>
142
143```zig
144var jwt = try zat.Jwt.parse(allocator, token_string);
145defer jwt.deinit();
146
147// check claims
148if (jwt.isExpired()) return error.TokenExpired;
149if (!std.mem.eql(u8, jwt.payload.aud, expected_audience)) return error.InvalidAudience;
150
151// verify signature against issuer's public key (from DID document)
152try jwt.verify(public_key_multibase);
153```
154
155supports ES256 (P-256) and ES256K (secp256k1) signing algorithms.
156
157</details>
158
159<details>
160<summary><strong>multibase decoding</strong> - decode public keys from DID documents</summary>
161
162```zig
163const key_bytes = try zat.multibase.decode(allocator, "zQ3sh...");
164defer allocator.free(key_bytes);
165
166const parsed = try zat.multicodec.parsePublicKey(key_bytes);
167// parsed.key_type: .secp256k1 or .p256
168// parsed.raw: 33-byte compressed public key
169```
170
171</details>
172
173## specs
174
175validation follows [atproto.com/specs](https://atproto.com/specs/atp).
176
177## versioning
178
179pre-1.0 semver:
180- `0.x.0` - new features (backwards compatible)
181- `0.x.y` - bug fixes
182
183breaking changes bump the minor version and are documented in commit messages.
184
185## license
186
187MIT