Skip to content

Handle->DID resolution via /.well-known/atproto-did endpoint - #3

Closed
anna-germ wants to merge 7 commits into
ATProtoKit:mainfrom
germ-network:wellknown-atproto
Closed

Handle->DID resolution via /.well-known/atproto-did endpoint#3
anna-germ wants to merge 7 commits into
ATProtoKit:mainfrom
germ-network:wellknown-atproto

Conversation

@anna-germ

@anna-germ anna-germ commented Nov 18, 2025

Copy link
Copy Markdown

Hi Matt! This may not be the exact way you'd want to implement this, but I wanted to propose an ATResolve update that accounts for the second way people can register handles, via .well-known path instead of DNS TXT record, as per:

Organizations like newsrooms or companies may want to verify multiple affiliated individuals' accounts via subdomains. For example, a newsroom could set its journalists' handles to be @name.newsroom.com, or have multiple accounts for different verticals like @sports.newsroom.com. But managing many individual subdomains via TXT records can be cumbersome. In that case, you can use a non-DNS option by resolving multiple handles via HTTP under a .well-known route.
Instead of using DNS TXT records, you can return each account's DID from the route https://${handle}/.well-known/atproto-did. The expected payload is a DID (such as did:plc:abcdef...) with content-type text/plain. The handle resolution function explicitly expects a HTTP 200 OK status. (source)

I've found that the DNS check takes a while (~1 min) if the appropriate TXT file isn't there (it falls back appropriately to the getProfile function, it just takes a while to reach there), and in my experience the well-known check can be slow as well if that endpoint doesn't exist, so I'm suggesting racing the DNS check against the ./well-known/atproto-did and returning whenever we find a value.

@mattmassicotte

Copy link
Copy Markdown
Contributor

Ohh this is a super good idea! Thanks so much for suggesting this.

Yeah, the DNS check is kinda awful. Unsure why exactly... I also don't know if this is the best long-term thing, however it does not change public API in any ways, and it seems reasonable to me. So let's give it a shot!

@mattmassicotte

Copy link
Copy Markdown
Contributor

Ahh, ok so the issue here, unfortunately, is that this now has a dependency on Foundation but it is not imported. This actually is a real problem on Linux. But that's something I will address eventually if it becomes important to do so.

@germ-mark

Copy link
Copy Markdown
Contributor

#4 should address the URLSession dependency that is failing the Linux tests (it also fixes the visionOS test issue)

We could then adopt that API here to make it work on Linux.

@anna-germ
anna-germ marked this pull request as draft December 9, 2025 21:12
@anna-germ

Copy link
Copy Markdown
Author

Converting to draft as I (and potentially Mark) work on a few to-do’s:

  • Fix the failing Linux tests
  • Make DNS and http requests in parallel (evidently both of them could take a while to fail, so seems better to race them)
  • Maybe adjust the Bluesky handling to be more generic

Improve handle resolution by racing DNS vs. /.well-known
@anna-germ
anna-germ marked this pull request as ready for review December 12, 2025 06:03
@anna-germ

Copy link
Copy Markdown
Author

Hellooooo! In the wake of Mark's PR, and also some work Mark did on running the DNS and well-known checks in parallel, I've replaced this PR with a new version.

One note on top of a code-specific flag I'll send:

  • The .bsky.social DNS check was failing because those accounts use the well-known endpoint. So you can take out Bluesky-specific handling. I did that but then put it back because Mark pointed out it's a public API. Oops. Anyways, you can consider removing the Bluesky-specific handling!

}

public protocol ResponseProviding {
public protocol ResponseProviding: Sendable {

@anna-germ anna-germ Dec 12, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark pointed out that you have a conditional conformance of ATResolve being Sendable if ResponseProviding is Sendable -- I'm making new problems for you by now requiring ResponseProviding to always be sendable. This is our best attempt at making the task groups work, but would defer to your expertise!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very generally speaking, Sendable protocols making the lives of the users of the protocol (that's us here) easier, while making the conformer's lives (that's the libraries clients) harder. In this case, however, I think it is pretty reasonable. So I think it's fine, we just need to remove the conditional conformance.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anna-germ
anna-germ marked this pull request as draft December 12, 2025 21:22
@anna-germ

Copy link
Copy Markdown
Author

The problem with this newer PR (with the task group) is that it now takes as long as the longest task. I'd add a guard !Task.isCancelled else return but the well-known and DNS checks are pretty atomic (unless I dig deeper into DNSSDDNSResolver) so I'm not quite sure where would be the best place to cancel the task.

@mattmassicotte do you have any good suggestions? Some discussion happening here: https://forums.swift.org/t/accept-the-first-task-to-complete/54386

@mattmassicotte

Copy link
Copy Markdown
Contributor

Cancellation could end up being tricky to support here, because it ultimately relies on the subsystems doing that internally. That thread kind of gets into the details.

Comment thread Sources/ATResolve/ATResolver.swift Outdated
let didRecord = txtRecords.first { record in
record.txt.hasPrefix("did=")
}
return didRecord?.txt.components(separatedBy: "=").last

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could technically have issues, as some did methods can include query strings, so you probably want to just split on = and then drop the first component, then join the rest.

@ThisIsMissEm ThisIsMissEm left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect the cancellation issue is due to the checkDNS being an attached task, where as in this implementation, they're explicitly detaching the DNS resolution task: https://github.com/ATProtoKit/ATIdentityTools/blob/main/Sources/ATIdentityTools/Handle/HandleResolver.swift#L80 (Note: Apache 2.0 not BSD license)


static func checkWellKnown(handle: String, provider: Provider) async -> String? {
do {
let dataResult = try await provider.data(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if the Provider here would allow it, but you could set a maximum response body size to like 1kb, and stop reading the response after that, since anything larger and you know it's not a valid response (it's likely HTML)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it should be possible to make an appropriate provider function that does this, if the underlying request system allows it?

Comment on lines +90 to +92
// Only check Cloudflare and Google DNS servers
var dnsOptions = CAresDNSResolver.Options.default
dnsOptions.servers = ["1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"]

@anna-germ anna-germ Dec 22, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolves the DNS timeout issue, but we can also adjust dnsOptions.timeoutMillis and dnsOptions.attempts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could make this a variable on the resolver, and allow overriding it, but using these defaults — since they're major DNS server providers. We may want to provide IPv6 options here too.

Another option is to not use AsyncDNSResolver and instead just use DNS over HTTPS: https://github.com/bluesky-social/atproto/blob/9dac8b0c600520ecb0066ac104787b27668dea47/packages/internal/handle-resolver/src/atproto-doh-handle-resolver.ts#L37

which would be somewhat more secure than using standard DNS (which is cleartext). That would also then allow this to be fully cancellable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds like a really cool option. Perhaps that could be another thing in the chain of checks this system does?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, with a bit of reorganisation we could hsve different handle resolvers, DNS, HTTP and DNS over HTTPS, and then folks could choose the right methods for them?

i suspect DoH would be superior here even though response times might be slightly higher than DNS

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great to me! What I was trying to go for was a "all discrete options public API" so you can get the behaviors you need, if you have advanced requirements. And then also something pre-configured so there's an easy (but potentially suboptimal) thing for people that aren't interested in learning how it all works.

@mattmassicotte

Copy link
Copy Markdown
Contributor

The use of Task.detached in that linked code is very unlikely doing what the author intended. That's a commonly-misunderstood API.

@mattmassicotte

Copy link
Copy Markdown
Contributor

Do you still have any interest in pursuing this one?

@germ-mark

Copy link
Copy Markdown
Contributor

We aren't pursuing this, you can feel free to close.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants