atproto utils for zig zat.dev
atproto sdk zig
at main 8.6 kB view raw
1//! DID Document - resolved identity information 2//! 3//! a did document contains: 4//! - handle (from alsoKnownAs) 5//! - signing key (from verificationMethod) 6//! - pds endpoint (from service) 7//! 8//! see: https://atproto.com/specs/did 9 10const std = @import("std"); 11const Did = @import("did.zig").Did; 12 13pub const DidDocument = struct { 14 allocator: std.mem.Allocator, 15 16 /// the did this document describes 17 id: []const u8, 18 19 /// handles (from alsoKnownAs, stripped of at:// prefix) 20 handles: [][]const u8, 21 22 /// verification methods (signing keys) 23 verification_methods: []VerificationMethod, 24 25 /// services (pds endpoints) 26 services: []Service, 27 28 pub const VerificationMethod = struct { 29 id: []const u8, 30 type: []const u8, 31 controller: []const u8, 32 public_key_multibase: []const u8, 33 }; 34 35 pub const Service = struct { 36 id: []const u8, 37 type: []const u8, 38 service_endpoint: []const u8, 39 }; 40 41 /// get the primary handle (first valid one) 42 pub fn handle(self: DidDocument) ?[]const u8 { 43 if (self.handles.len == 0) return null; 44 return self.handles[0]; 45 } 46 47 /// get the atproto signing key 48 pub fn signingKey(self: DidDocument) ?VerificationMethod { 49 for (self.verification_methods) |vm| { 50 if (std.mem.endsWith(u8, vm.id, "#atproto")) { 51 return vm; 52 } 53 } 54 return null; 55 } 56 57 /// get the pds endpoint 58 pub fn pdsEndpoint(self: DidDocument) ?[]const u8 { 59 for (self.services) |svc| { 60 if (std.mem.endsWith(u8, svc.id, "#atproto_pds")) { 61 return svc.service_endpoint; 62 } 63 } 64 return null; 65 } 66 67 /// parse a did document from json 68 pub fn parse(allocator: std.mem.Allocator, json_str: []const u8) !DidDocument { 69 const parsed = try std.json.parseFromSlice(std.json.Value, allocator, json_str, .{}); 70 defer parsed.deinit(); 71 72 return try parseValue(allocator, parsed.value); 73 } 74 75 /// parse from an already-parsed json value 76 pub fn parseValue(allocator: std.mem.Allocator, root: std.json.Value) !DidDocument { 77 if (root != .object) return error.InvalidDidDocument; 78 const obj = root.object; 79 80 // id is required 81 const id = if (obj.get("id")) |v| switch (v) { 82 .string => |s| try allocator.dupe(u8, s), 83 else => return error.InvalidDidDocument, 84 } else return error.InvalidDidDocument; 85 errdefer allocator.free(id); 86 87 // parse alsoKnownAs -> handles 88 var handles: std.ArrayList([]const u8) = .empty; 89 errdefer { 90 for (handles.items) |h| allocator.free(h); 91 handles.deinit(allocator); 92 } 93 94 if (obj.get("alsoKnownAs")) |aka| { 95 if (aka == .array) { 96 for (aka.array.items) |item| { 97 if (item == .string) { 98 const s = item.string; 99 // strip at:// prefix if present 100 const h = if (std.mem.startsWith(u8, s, "at://")) 101 s[5..] 102 else 103 s; 104 try handles.append(allocator, try allocator.dupe(u8, h)); 105 } 106 } 107 } 108 } 109 110 // parse verificationMethod 111 var vms: std.ArrayList(VerificationMethod) = .empty; 112 errdefer { 113 for (vms.items) |vm| { 114 allocator.free(vm.id); 115 allocator.free(vm.type); 116 allocator.free(vm.controller); 117 allocator.free(vm.public_key_multibase); 118 } 119 vms.deinit(allocator); 120 } 121 122 if (obj.get("verificationMethod")) |vm_arr| { 123 if (vm_arr == .array) { 124 for (vm_arr.array.items) |item| { 125 if (item == .object) { 126 const vm_obj = item.object; 127 const vm = VerificationMethod{ 128 .id = try allocator.dupe(u8, getStr(vm_obj, "id") orelse continue), 129 .type = try allocator.dupe(u8, getStr(vm_obj, "type") orelse ""), 130 .controller = try allocator.dupe(u8, getStr(vm_obj, "controller") orelse ""), 131 .public_key_multibase = try allocator.dupe(u8, getStr(vm_obj, "publicKeyMultibase") orelse ""), 132 }; 133 try vms.append(allocator, vm); 134 } 135 } 136 } 137 } 138 139 // parse service 140 var svcs: std.ArrayList(Service) = .empty; 141 errdefer { 142 for (svcs.items) |svc| { 143 allocator.free(svc.id); 144 allocator.free(svc.type); 145 allocator.free(svc.service_endpoint); 146 } 147 svcs.deinit(allocator); 148 } 149 150 if (obj.get("service")) |svc_arr| { 151 if (svc_arr == .array) { 152 for (svc_arr.array.items) |item| { 153 if (item == .object) { 154 const svc_obj = item.object; 155 const svc = Service{ 156 .id = try allocator.dupe(u8, getStr(svc_obj, "id") orelse continue), 157 .type = try allocator.dupe(u8, getStr(svc_obj, "type") orelse ""), 158 .service_endpoint = try allocator.dupe(u8, getStr(svc_obj, "serviceEndpoint") orelse ""), 159 }; 160 try svcs.append(allocator, svc); 161 } 162 } 163 } 164 } 165 166 return .{ 167 .allocator = allocator, 168 .id = id, 169 .handles = try handles.toOwnedSlice(allocator), 170 .verification_methods = try vms.toOwnedSlice(allocator), 171 .services = try svcs.toOwnedSlice(allocator), 172 }; 173 } 174 175 pub fn deinit(self: *DidDocument) void { 176 for (self.handles) |h| self.allocator.free(h); 177 self.allocator.free(self.handles); 178 179 for (self.verification_methods) |vm| { 180 self.allocator.free(vm.id); 181 self.allocator.free(vm.type); 182 self.allocator.free(vm.controller); 183 self.allocator.free(vm.public_key_multibase); 184 } 185 self.allocator.free(self.verification_methods); 186 187 for (self.services) |svc| { 188 self.allocator.free(svc.id); 189 self.allocator.free(svc.type); 190 self.allocator.free(svc.service_endpoint); 191 } 192 self.allocator.free(self.services); 193 194 self.allocator.free(self.id); 195 } 196 197 fn getStr(obj: std.json.ObjectMap, key: []const u8) ?[]const u8 { 198 if (obj.get(key)) |v| { 199 if (v == .string) return v.string; 200 } 201 return null; 202 } 203}; 204 205// === tests === 206 207test "parse did document" { 208 const json = 209 \\{ 210 \\ "id": "did:plc:z72i7hdynmk6r22z27h6tvur", 211 \\ "alsoKnownAs": ["at://jay.bsky.social"], 212 \\ "verificationMethod": [ 213 \\ { 214 \\ "id": "did:plc:z72i7hdynmk6r22z27h6tvur#atproto", 215 \\ "type": "Multikey", 216 \\ "controller": "did:plc:z72i7hdynmk6r22z27h6tvur", 217 \\ "publicKeyMultibase": "zQ3shXjHeiBuRCKmM36cuYnm7YEMzhGnCmCyW92sRJ9pribSF" 218 \\ } 219 \\ ], 220 \\ "service": [ 221 \\ { 222 \\ "id": "#atproto_pds", 223 \\ "type": "AtprotoPersonalDataServer", 224 \\ "serviceEndpoint": "https://shimeji.us-east.host.bsky.network" 225 \\ } 226 \\ ] 227 \\} 228 ; 229 230 var doc = try DidDocument.parse(std.testing.allocator, json); 231 defer doc.deinit(); 232 233 try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id); 234 try std.testing.expectEqualStrings("jay.bsky.social", doc.handle().?); 235 try std.testing.expectEqualStrings("https://shimeji.us-east.host.bsky.network", doc.pdsEndpoint().?); 236 237 const key = doc.signingKey().?; 238 try std.testing.expect(std.mem.endsWith(u8, key.id, "#atproto")); 239} 240 241test "parse did document with no handle" { 242 const json = 243 \\{ 244 \\ "id": "did:plc:test123", 245 \\ "alsoKnownAs": [], 246 \\ "verificationMethod": [], 247 \\ "service": [] 248 \\} 249 ; 250 251 var doc = try DidDocument.parse(std.testing.allocator, json); 252 defer doc.deinit(); 253 254 try std.testing.expect(doc.handle() == null); 255 try std.testing.expect(doc.pdsEndpoint() == null); 256 try std.testing.expect(doc.signingKey() == null); 257}