+90
src/internal/handle_resolver.zig
+90
src/internal/handle_resolver.zig
···
1
+
//! Handle Resolver - resolve handles to DIDs
2
+
//!
3
+
//! resolves AT Protocol handles via HTTP:
4
+
//! https://{handle}/.well-known/atproto-did
5
+
//!
6
+
//! note: DNS TXT resolution (_atproto.{handle}) not yet implemented
7
+
//! as zig std doesn't provide TXT record lookup.
8
+
//!
9
+
//! see: https://atproto.com/specs/handle
10
+
11
+
const std = @import("std");
12
+
const Handle = @import("handle.zig").Handle;
13
+
const Did = @import("did.zig").Did;
14
+
15
+
pub const HandleResolver = struct {
16
+
allocator: std.mem.Allocator,
17
+
http_client: std.http.Client,
18
+
19
+
pub fn init(allocator: std.mem.Allocator) HandleResolver {
20
+
return .{
21
+
.allocator = allocator,
22
+
.http_client = .{ .allocator = allocator },
23
+
};
24
+
}
25
+
26
+
pub fn deinit(self: *HandleResolver) void {
27
+
self.http_client.deinit();
28
+
}
29
+
30
+
/// resolve a handle to a DID via HTTP well-known
31
+
pub fn resolve(self: *HandleResolver, handle: Handle) ![]const u8 {
32
+
return try self.resolveHttp(handle);
33
+
}
34
+
35
+
/// resolve via HTTP at https://{handle}/.well-known/atproto-did
36
+
fn resolveHttp(self: *HandleResolver, handle: Handle) ![]const u8 {
37
+
const url = try std.fmt.allocPrint(
38
+
self.allocator,
39
+
"https://{s}/.well-known/atproto-did",
40
+
.{handle.str()},
41
+
);
42
+
defer self.allocator.free(url);
43
+
44
+
var aw: std.Io.Writer.Allocating = .init(self.allocator);
45
+
defer aw.deinit();
46
+
47
+
const result = self.http_client.fetch(.{
48
+
.location = .{ .url = url },
49
+
.response_writer = &aw.writer,
50
+
}) catch return error.HttpResolutionFailed;
51
+
52
+
if (result.status != .ok) {
53
+
return error.HttpResolutionFailed;
54
+
}
55
+
56
+
// response body should be the DID as plain text
57
+
const did_str = std.mem.trim(u8, aw.toArrayList().items, &std.ascii.whitespace);
58
+
59
+
// validate it's a proper DID
60
+
if (Did.parse(did_str) == null) {
61
+
return error.InvalidDidInResponse;
62
+
}
63
+
64
+
return try self.allocator.dupe(u8, did_str);
65
+
}
66
+
};
67
+
68
+
// === integration tests ===
69
+
// these actually hit the network - run with: zig test src/internal/handle_resolver.zig
70
+
71
+
test "resolve handle - integration" {
72
+
// use arena for http client internals that may leak
73
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
74
+
defer arena.deinit();
75
+
76
+
var resolver = HandleResolver.init(arena.allocator());
77
+
defer resolver.deinit();
78
+
79
+
// resolve a known handle that has .well-known/atproto-did
80
+
const handle = Handle.parse("jay.bsky.social") orelse return error.InvalidHandle;
81
+
const did = resolver.resolve(handle) catch |err| {
82
+
// network errors are ok in CI without network access
83
+
std.debug.print("network error (expected in some CI): {}\n", .{err});
84
+
return;
85
+
};
86
+
87
+
// should be a valid did:plc
88
+
try std.testing.expect(Did.parse(did) != null);
89
+
try std.testing.expect(std.mem.startsWith(u8, did, "did:plc:"));
90
+
}
+1
src/root.zig
+1
src/root.zig
···
14
14
// did resolution
15
15
pub const DidDocument = @import("internal/did_document.zig").DidDocument;
16
16
pub const DidResolver = @import("internal/did_resolver.zig").DidResolver;
17
+
pub const HandleResolver = @import("internal/handle_resolver.zig").HandleResolver;
17
18
18
19
// xrpc
19
20
pub const XrpcClient = @import("internal/xrpc.zig").XrpcClient;