atproto utils for zig zat.dev
atproto sdk zig
at v0.1.0 188 lines 5.3 kB view raw view rendered
1# zat - zig atproto primitives 2 3low-level building blocks for atproto applications in zig. not a full sdk - just the pieces that everyone reimplements. 4 5## philosophy 6 7from studying the wishlists: the pain is real, but the suggested solutions often over-engineer. we want: 8 91. **primitives, not frameworks** - types and parsers, not http clients or feed scaffolds 102. **layered design** - each piece usable independently 113. **zig idioms** - explicit buffers, comptime validation, no hidden allocations 124. **minimal scope** - solve the repeated pain, not every possible need 13 14## scope 15 16### in scope (v0.1) 17 18**tid** - timestamp identifiers 19- parse tid string to timestamp (microseconds) 20- generate tid from timestamp 21- extract clock id 22- comptime validation of format 23 24**at-uri** - `at://did:plc:xyz/collection/rkey` 25- parse to components (did, collection, rkey) 26- construct from components 27- validation 28 29**did** - decentralized identifiers 30- parse did:plc and did:web 31- validate format 32- type-safe wrapper (not just `[]const u8`) 33 34### maybe v0.2 35 36**facets** - extract links/mentions/tags from post records 37- given a json value with `text` and `facets`, extract urls 38- byte-offset handling for utf-8 39 40**cid** - content identifiers 41- parse cid strings 42- validate format 43 44### out of scope (for now) 45 46- lexicon codegen (too big, could be its own project) 47- xrpc client (std.http.Client is fine) 48- session management (app-specific) 49- jetstream client (websocket.zig exists, just wire it) 50- feed generator framework (each feed is unique) 51- did resolution (requires http, out of primitive scope) 52 53## design 54 55### tid.zig 56 57```zig 58pub const Tid = struct { 59 raw: [13]u8, 60 61 /// parse a tid string. returns null if invalid. 62 pub fn parse(s: []const u8) ?Tid 63 64 /// timestamp in microseconds since unix epoch 65 pub fn timestamp(self: Tid) u64 66 67 /// clock identifier (lower 10 bits) 68 pub fn clockId(self: Tid) u10 69 70 /// generate tid for current time 71 pub fn now() Tid 72 73 /// generate tid for specific timestamp 74 pub fn fromTimestamp(ts: u64, clock_id: u10) Tid 75 76 /// format to string 77 pub fn format(self: Tid, buf: *[13]u8) void 78}; 79``` 80 81encoding: base32-sortable (chars `234567abcdefghijklmnopqrstuvwxyz`), 13 chars, first 11 encode 53-bit timestamp, last 2 encode 10-bit clock id. 82 83### at_uri.zig 84 85```zig 86pub const AtUri = struct { 87 /// the full uri string (borrowed, not owned) 88 raw: []const u8, 89 90 /// offsets into raw for each component 91 did_end: usize, 92 collection_end: usize, 93 94 pub fn parse(s: []const u8) ?AtUri 95 96 pub fn did(self: AtUri) []const u8 97 pub fn collection(self: AtUri) []const u8 98 pub fn rkey(self: AtUri) []const u8 99 100 /// construct a new uri. caller owns the buffer. 101 pub fn format( 102 buf: []u8, 103 did: []const u8, 104 collection: []const u8, 105 rkey: []const u8, 106 ) ?[]const u8 107}; 108``` 109 110### did.zig 111 112```zig 113pub const Did = union(enum) { 114 plc: [24]u8, // the identifier after "did:plc:" 115 web: []const u8, // the domain after "did:web:" 116 117 pub fn parse(s: []const u8) ?Did 118 119 /// format to string 120 pub fn format(self: Did, buf: []u8) ?[]const u8 121 122 /// check if this is a plc did 123 pub fn isPlc(self: Did) bool 124}; 125``` 126 127## structure 128 129``` 130zat/ 131├── build.zig 132├── build.zig.zon 133├── src/ 134│ ├── root.zig # public API (stable exports) 135│ ├── internal.zig # internal API (experimental) 136│ └── internal/ 137│ ├── tid.zig 138│ ├── at_uri.zig 139│ └── did.zig 140└── docs/ 141 └── plan.md 142``` 143 144## internal → public promotion 145 146new features start in `internal` where we can iterate freely. when an API stabilizes: 147 148```zig 149// in root.zig, uncomment to promote: 150pub const Tid = internal.Tid; 151``` 152 153users who need bleeding-edge access can always use: 154 155```zig 156const zat = @import("zat"); 157const tid = zat.internal.Tid.parse("..."); 158``` 159 160this pattern exists indefinitely - even after 1.0, new experimental features start in internal. 161 162## decisions 163 164### why not typed lexicons? 165 166codegen from lexicon json is a big project on its own. the core pain (json navigation) can be partially addressed by documenting patterns, and the sdk should work regardless of how people parse json. 167 168### why not an http client wrapper? 169 170zig 0.15's `std.http.Client` with `Io.Writer.Allocating` works well. wrapping it doesn't add much value. the real pain is around auth token refresh and rate limiting - those are better solved at the application level where retry logic is domain-specific. 171 172### why not websocket/jetstream? 173 174websocket.zig already exists and works well. the jetstream protocol is simple json messages. a thin wrapper doesn't justify a dependency. 175 176### borrowing vs owning 177 178for parse operations, we borrow slices into the input rather than allocating. callers who need owned data can dupe. this matches zig's explicit memory style. 179 180## next steps 181 1821. ~~implement tid.zig with tests~~ done 1832. ~~implement at_uri.zig with tests~~ done 1843. ~~implement did.zig with tests~~ done 1854. ~~wire up build.zig as a module~~ done 1865. try using it in find-bufo or music-atmosphere-feed to validate the api 1876. iterate on internal APIs based on real usage 1887. promote stable APIs to root.zig