atproto utils for zig zat.dev
atproto sdk zig
Something went wrong. Try again.
Zig
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192//! DID Resolver - fetches and parses DID documents//!//! resolves did:plc via plc.directory and did:web via .well-known/did.json//!//! see: https://atproto.com/specs/did
const std = @import("std");const Did = @import("../syntax/did.zig").Did;const DidDocument = @import("did_document.zig").DidDocument;const HttpTransport = @import("../xrpc/transport.zig").HttpTransport;const network_safety = @import("network_safety.zig");
const max_did_document_size = 1 * 1024 * 1024;
pub const DidResolver = struct { allocator: std.mem.Allocator, transport: HttpTransport,
/// plc directory url (default: https://plc.directory) plc_url: []const u8 = "https://plc.directory", /// DoH endpoint for did:web host safety preflight. doh_endpoint: []const u8 = "https://cloudflare-dns.com/dns-query",
pub fn init(io: std.Io, allocator: std.mem.Allocator) DidResolver { return initWithOptions(io, allocator, .{}); }
pub const Options = struct { keep_alive: bool = true, };
pub fn initWithOptions(io: std.Io, allocator: std.mem.Allocator, options: Options) DidResolver { var transport = HttpTransport.init(io, allocator); transport.keep_alive = options.keep_alive; return .{ .allocator = allocator, .transport = transport, }; }
pub fn deinit(self: *DidResolver) void { self.transport.deinit(); }
/// resolve a did to its document pub fn resolve(self: *DidResolver, did: Did) !DidDocument { return switch (did.method()) { .plc => try self.resolvePlc(did), .web => try self.resolveWeb(did), .other => error.UnsupportedDidMethod, }; }
/// resolve did:plc via plc.directory fn resolvePlc(self: *DidResolver, did: Did) !DidDocument { // build url: {plc_url}/{did} const url = try std.fmt.allocPrint(self.allocator, "{s}/{s}", .{ self.plc_url, did.raw }); defer self.allocator.free(url);
return try self.fetchDidDocument(url, null); }
/// resolve did:web via .well-known fn resolveWeb(self: *DidResolver, did: Did) !DidDocument { // did:web:example.com -> https://example.com/.well-known/did.json // did:web:example.com:path:to -> https://example.com/path/to/did.json const domain_and_path = did.raw["did:web:".len..];
// decode percent-encoded colons in path var url_buf: std.ArrayList(u8) = .empty; defer url_buf.deinit(self.allocator);
try url_buf.appendSlice(self.allocator, "https://");
var first_segment = true; var it = std.mem.splitScalar(u8, domain_and_path, ':'); while (it.next()) |segment| { if (first_segment) { // first segment is the domain try url_buf.appendSlice(self.allocator, segment); first_segment = false; } else { // subsequent segments are path components try url_buf.append(self.allocator, '/'); try url_buf.appendSlice(self.allocator, segment); } }
// add .well-known/did.json or /did.json if (std.mem.indexOf(u8, domain_and_path, ":") == null) { // no path, use .well-known try url_buf.appendSlice(self.allocator, "/.well-known/did.json"); } else { // has path, append did.json try url_buf.appendSlice(self.allocator, "/did.json"); }
var checked_url = try network_safety.resolveIdentityUrl( self.allocator, &self.transport, self.doh_endpoint, url_buf.items, ); defer checked_url.deinit(self.allocator); return try self.fetchDidDocument(url_buf.items, checked_url.resolvedConnection()); }
/// fetch and parse a did document from url fn fetchDidDocument( self: *DidResolver, url: []const u8, resolved_connection: ?HttpTransport.ResolvedConnection, ) !DidDocument { const result = try self.transport.fetch(.{ .url = url, .max_response_size = max_did_document_size, .redirect_behavior = .not_allowed, .resolved_connection = resolved_connection, }); defer self.allocator.free(result.body);
if (result.status != .ok) { return error.DidResolutionFailed; }
return try DidDocument.parse(self.allocator, result.body); }};
// === tests ===
test "resolve did:plc - integration" { // use arena for http client internals that may leak var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit();
var resolver = DidResolver.init(std.Options.debug_io, arena.allocator()); defer resolver.deinit();
const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; var doc = resolver.resolve(did) catch { return; // network error, expected in CI }; defer doc.deinit();
try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id); try std.testing.expect(doc.handle() != null);}
test "resolve did:plc - leak check (no arena)" { // repro for memory leak report: use testing.allocator directly // (no arena) to see if std.http.Client leaks on deinit var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); defer resolver.deinit();
const did = Did.parse("did:plc:z72i7hdynmk6r22z27h6tvur").?; var doc = resolver.resolve(did) catch { return; // network error, expected in CI }; defer doc.deinit();
try std.testing.expectEqualStrings("did:plc:z72i7hdynmk6r22z27h6tvur", doc.id);}
test "did:web loopback host is rejected before fetch" { var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); defer resolver.deinit();
const did = Did.parse("did:web:127.0.0.1") orelse return error.SkipZigTest; try std.testing.expectError(error.UnsafeIdentityHost, resolver.resolve(did));}
test "did:web url construction" { // test url building without network var resolver = DidResolver.init(std.Options.debug_io, std.testing.allocator); defer resolver.deinit();
// simple domain { const did = Did.parse("did:web:example.com").?; _ = did; // would resolve to https://example.com/.well-known/did.json }
// domain with path { const did = Did.parse("did:web:example.com:user:alice").?; _ = did; // would resolve to https://example.com/user/alice/did.json }}