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}