From 0dee7a196b2b5801cb2e5deb05b48643ee0598b7 Mon Sep 17 00:00:00 2001 From: Anna M Date: Thu, 11 Dec 2025 18:00:40 -0800 Subject: [PATCH 1/6] Add mark/parallel edits --- Sources/ATResolve/ATResolver.swift | 66 +++++++++++++++++++---- Sources/ATResolve/ResponseProviding.swift | 2 +- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index c3bfad6..f5c6ec5 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -40,20 +40,64 @@ public struct ATResolver { } public func didForDomain(_ name: String) async throws -> String? { - // I don't understand exactly why, but this triggers a timeout. When I do it with `dig` it returns right away... - if name.hasSuffix(".bsky.social") { + return await withTaskGroup(of: Optional.self) { group in + let provider = provider + group.addTask { + await Self.checkWellKnown(handle: name, provider: provider) + } + + group.addTask { + await Self.checkDNS(handle: name) + } + + let first = await group.next() + if let first { + return first + } + + return await group.next() ?? nil + } + } + + static func checkWellKnown(handle: String, provider: Provider) async -> String? { + do { + let dataResult = try await provider.data( + for: .init( + host: handle, + path: "/.well-known/atproto-did", + method: .get, + headers: ["Accept": "text/plain;charset=UTF-8"], + queryItems: [] + ) + ) + let result = String(data: dataResult, encoding: .utf8) + + if let result { + //workaround if we get erroneous 200 code but body return is e.g. + //"404 error" + guard result.hasPrefix("did:") else { + return nil + } + } + return result + } catch { return nil } - - let resolver = try AsyncDNSResolver() - - let txtRecords = try await resolver.queryTXT(name: "_atproto." + name) - - let didRecord = txtRecords.first { record in - record.txt.hasPrefix("did=") + } + + static func checkDNS(handle: String) async -> String? { + do { + let resolver = try AsyncDNSResolver() + let txtRecords = try await resolver.queryTXT( + name: "_atproto." + handle + ) + let didRecord = txtRecords.first { record in + record.txt.hasPrefix("did=") + } + return didRecord?.txt.components(separatedBy: "=").last + } catch { + return nil } - - return didRecord?.txt.components(separatedBy: "=").last } public func didForHandle(_ handle: String) async throws -> String? { diff --git a/Sources/ATResolve/ResponseProviding.swift b/Sources/ATResolve/ResponseProviding.swift index 5688e6e..955d924 100644 --- a/Sources/ATResolve/ResponseProviding.swift +++ b/Sources/ATResolve/ResponseProviding.swift @@ -28,7 +28,7 @@ public struct Request: Sendable { public let queryItems: [(String, String?)] } -public protocol ResponseProviding { +public protocol ResponseProviding: Sendable { func data(for: Request) async throws -> Data } From 9c360c71cf331ae9fbcc9fec31df708a62e2c68e Mon Sep 17 00:00:00 2001 From: Anna M Date: Thu, 11 Dec 2025 18:40:00 -0800 Subject: [PATCH 2/6] Remove bluesky-specific handling --- Sources/ATResolve/ATResolver.swift | 15 +-------------- Tests/ATResolveTests/ATResolveTests.swift | 23 ++++++++++++----------- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index f5c6ec5..c536938 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -101,20 +101,7 @@ public struct ATResolver { } public func didForHandle(_ handle: String) async throws -> String? { - if let did = try await didForDomain(handle) { - return did - } - - return try await blueskyGetProfile(handle).did - } - - public func blueskyGetProfile(_ actor: String) async throws -> BlueskyProfile { - try await provider.decodeJSON( - host: "public.api.bsky.app", - path: "/xrpc/app.bsky.actor.getProfile", - headers: ["Accept": "application/json"], - queryItems: [("actor", actor)] - ) + try await didForDomain(handle) } public func plcDirectoryQuery( diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index a5f39b1..85f45a6 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -17,8 +17,8 @@ struct ATResolveTests { #expect(data?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } - @Test - func didForDomain() async throws { + // Custom handle: Managed via DNS TXT record + @Test func didForCustomHandle() async throws { let resolver = ATResolver(provider: URLSession.shared) let did = try await resolver.didForDomain("massicotte.org") @@ -26,21 +26,22 @@ struct ATResolveTests { #expect(did == "did:plc:klsh7edzj3jmxucibyjqstb3") } - @Test - func blueskyGetProfile() async throws { + // Bluesky: Managed via /.well-known/atproto-did + @Test func didForBskySocialHandle() async throws { let resolver = ATResolver(provider: URLSession.shared) - let profile = try await resolver.blueskyGetProfile("massicotte.org") - - #expect(profile.did == "did:plc:klsh7edzj3jmxucibyjqstb3") + let did = try await resolver.didForDomain("cjrdev.bsky.social") + + #expect(did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") } - @Test func bskySocialHandle() async throws { + // Blacksky: Managed via /.well-known/atproto-did + @Test func didForMyatprotoSocialHandle() async throws { let resolver = ATResolver(provider: URLSession.shared) - let profile = try await resolver.resolveHandle("cjrdev.bsky.social") - - #expect(profile != nil) + let did = try await resolver.didForDomain("cosmo-dev.myatproto.social") + + #expect(did == "did:plc:ccuttodko4ijw24ga6yln3l6") } @Test func decodeWithCustomProvider() async throws { From 9252b854da6262742be944c616c6dd1c4101d6ec Mon Sep 17 00:00:00 2001 From: Anna M Date: Thu, 11 Dec 2025 21:56:44 -0800 Subject: [PATCH 3/6] Putting back bluesky-specific handling This reverts commit 9c360c71cf331ae9fbcc9fec31df708a62e2c68e. --- Sources/ATResolve/ATResolver.swift | 15 ++++++++++++++- Tests/ATResolveTests/ATResolveTests.swift | 23 +++++++++++------------ 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index c536938..f5c6ec5 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -101,7 +101,20 @@ public struct ATResolver { } public func didForHandle(_ handle: String) async throws -> String? { - try await didForDomain(handle) + if let did = try await didForDomain(handle) { + return did + } + + return try await blueskyGetProfile(handle).did + } + + public func blueskyGetProfile(_ actor: String) async throws -> BlueskyProfile { + try await provider.decodeJSON( + host: "public.api.bsky.app", + path: "/xrpc/app.bsky.actor.getProfile", + headers: ["Accept": "application/json"], + queryItems: [("actor", actor)] + ) } public func plcDirectoryQuery( diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index 85f45a6..a5f39b1 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -17,8 +17,8 @@ struct ATResolveTests { #expect(data?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } - // Custom handle: Managed via DNS TXT record - @Test func didForCustomHandle() async throws { + @Test + func didForDomain() async throws { let resolver = ATResolver(provider: URLSession.shared) let did = try await resolver.didForDomain("massicotte.org") @@ -26,22 +26,21 @@ struct ATResolveTests { #expect(did == "did:plc:klsh7edzj3jmxucibyjqstb3") } - // Bluesky: Managed via /.well-known/atproto-did - @Test func didForBskySocialHandle() async throws { + @Test + func blueskyGetProfile() async throws { let resolver = ATResolver(provider: URLSession.shared) - let did = try await resolver.didForDomain("cjrdev.bsky.social") - - #expect(did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") + let profile = try await resolver.blueskyGetProfile("massicotte.org") + + #expect(profile.did == "did:plc:klsh7edzj3jmxucibyjqstb3") } - // Blacksky: Managed via /.well-known/atproto-did - @Test func didForMyatprotoSocialHandle() async throws { + @Test func bskySocialHandle() async throws { let resolver = ATResolver(provider: URLSession.shared) - let did = try await resolver.didForDomain("cosmo-dev.myatproto.social") - - #expect(did == "did:plc:ccuttodko4ijw24ga6yln3l6") + let profile = try await resolver.resolveHandle("cjrdev.bsky.social") + + #expect(profile != nil) } @Test func decodeWithCustomProvider() async throws { From 28b30107ef7066cfff6dd49efdb07ec1990cc811 Mon Sep 17 00:00:00 2001 From: Anna M Date: Mon, 22 Dec 2025 14:27:48 -0800 Subject: [PATCH 4/6] DNS and HTTPS timeouts --- Sources/ATResolve/ATResolver.swift | 7 +++++- Sources/ATResolve/Networking.swift | 1 + Tests/ATResolveTests/ATResolveTests.swift | 28 +++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index f5c6ec5..3d72df0 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -87,7 +87,12 @@ public struct ATResolver { static func checkDNS(handle: String) async -> String? { do { - let resolver = try AsyncDNSResolver() + var dnsOptions = CAresDNSResolver.Options.default + dnsOptions.attempts = 1 + dnsOptions.timeoutMillis = 3000 + // Cloudflare and Google DNS servers + dnsOptions.servers = ["1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"] + let resolver = try AsyncDNSResolver(options: dnsOptions) let txtRecords = try await resolver.queryTXT( name: "_atproto." + handle ) diff --git a/Sources/ATResolve/Networking.swift b/Sources/ATResolve/Networking.swift index 3b9f823..b59733c 100644 --- a/Sources/ATResolve/Networking.swift +++ b/Sources/ATResolve/Networking.swift @@ -18,6 +18,7 @@ extension URLSession: ResponseProviding { throw URLError(.badURL) } var urlRequest = URLRequest(url: url) + urlRequest.timeoutInterval = 3 urlRequest.httpMethod = request.method.rawValue for (key, value) in request.headers { urlRequest.addValue(value, forHTTPHeaderField: key) diff --git a/Tests/ATResolveTests/ATResolveTests.swift b/Tests/ATResolveTests/ATResolveTests.swift index a5f39b1..7f33b71 100644 --- a/Tests/ATResolveTests/ATResolveTests.swift +++ b/Tests/ATResolveTests/ATResolveTests.swift @@ -42,6 +42,27 @@ struct ATResolveTests { #expect(profile != nil) } + + @Test + func timedTestWellKnownTimeout() async throws { + // The /.well-known endpoint times out for @thisismissem.social + // It should time out after 3 seconds, so this test should be ~3 seconds + try await timedTest { + let resolver = ATResolver(provider: URLSession.shared) + let profile = try await resolver.resolveHandle("thisismissem.social") + #expect(profile?.did == "did:plc:5w4eqcxzw5jv5qfnmzxcakfy") + } + } + + @Test + func timedTestDNSTimeout() async throws { + // DNS should time out for any .bsky.social handle + try await timedTest { + let resolver = ATResolver(provider: URLSession.shared) + let profile = try await resolver.resolveHandle("cjrdev.bsky.social") + #expect(profile?.did == "did:plc:wlef3srsa3hlyzj2hy6yncrh") + } + } @Test func decodeWithCustomProvider() async throws { struct CustomProvider: ResponseProviding { @@ -60,4 +81,11 @@ struct ATResolveTests { #expect(response.pds?.serviceEndpoint == "https://milkcap.us-west.host.bsky.network") } + + private func timedTest(_ test: () async throws -> ()) async throws { + let start = CFAbsoluteTimeGetCurrent() + try await test() + let diff = CFAbsoluteTimeGetCurrent() - start + print("This test took \(diff) seconds") + } } From 2cbedc88eaa74dabb33f6577b2fd153b73545182 Mon Sep 17 00:00:00 2001 From: Anna M Date: Mon, 22 Dec 2025 14:40:02 -0800 Subject: [PATCH 5/6] Removing attempts/timeoutMillis --- Sources/ATResolve/ATResolver.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index 3d72df0..4dc850f 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -87,10 +87,8 @@ public struct ATResolver { static func checkDNS(handle: String) async -> String? { do { + // Only check Cloudflare and Google DNS servers var dnsOptions = CAresDNSResolver.Options.default - dnsOptions.attempts = 1 - dnsOptions.timeoutMillis = 3000 - // Cloudflare and Google DNS servers dnsOptions.servers = ["1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"] let resolver = try AsyncDNSResolver(options: dnsOptions) let txtRecords = try await resolver.queryTXT( From 6a01c468f8ad28dd20d5efffd64868391a98b081 Mon Sep 17 00:00:00 2001 From: Anna M Date: Mon, 22 Dec 2025 14:53:33 -0800 Subject: [PATCH 6/6] Remove conditional conformance --- Sources/ATResolve/ATResolver.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/ATResolve/ATResolver.swift b/Sources/ATResolve/ATResolver.swift index 4dc850f..eb1d1e5 100644 --- a/Sources/ATResolve/ATResolver.swift +++ b/Sources/ATResolve/ATResolver.swift @@ -142,8 +142,6 @@ public struct ATResolver { } } -extension ATResolver: Sendable where Provider: Sendable {} - #if canImport(Foundation) import Foundation