DomainConnect.Discovery (DomainConnect v0.5.0)

Copy Markdown View Source

Domain Connect discovery: find the DNS provider's Domain Connect API for a domain, then fetch its settings.

Three steps, matching the canonical reference implementation:

  1. Compute the registrable domain (the zone) using the Public Suffix List, so rent.theirplace.co.uk resolves to the zone theirplace.co.uk (not co.uk) and host rent. This is correct for multi-label TLDs.
  2. Look up the TXT record at _domainconnect.<zone>. Its value is the host of the provider's Domain Connect API. The host is validated by DomainConnect.HostGuard (SSRF protection) before any request.
  3. GET https://<api-host>/v2/<zone>/settings (no redirects, short timeouts), validate the response, and decode it into a DomainConnect.Config.

The DNS resolver, address resolver (for the SSRF guard), and HTTP client are all injectable for testing — see the options on discover/2.

Registrable zone only

Discovery uses the PSL registrable domain, matching the reference library. A Domain Connect record published on a delegated sub-zone (not the registrable domain) is not discovered. This is a deliberate limitation.

Summary

Functions

Discovers the Domain Connect configuration for domain.

Splits a domain into its registrable zone and the sub-host using the Public Suffix List.

Types

resolver()

@type resolver() :: (String.t() -> [String.t()])

Functions

discover(domain, opts \\ [])

@spec discover(
  String.t(),
  keyword()
) :: {:ok, DomainConnect.Config.t()} | {:error, term()}

Discovers the Domain Connect configuration for domain.

Options

  • :resolver — a 1-arity fun taking a name (e.g. "_domainconnect.example.com") and returning a list of TXT record strings. Defaults to a :inet_res-backed resolver.
  • :address_resolver — passed to DomainConnect.HostGuard.check/2 to resolve the API host's IPs for the SSRF guard.
  • :req_options — keyword options merged into the Req request that fetches /settings (e.g. plug: for Req.Test).

Returns {:ok, %DomainConnect.Config{}} or {:error, reason} where reason is one of :invalid_domain, :not_supported (no usable _domainconnect record), :invalid_settings, {:unsafe_host, host_guard_reason}, {:settings_http_error, status}, or {:settings_transport_error, term}.

registrable(domain)

@spec registrable(String.t()) ::
  {:ok, String.t(), String.t() | nil} | {:error, :invalid_domain}

Splits a domain into its registrable zone and the sub-host using the Public Suffix List.

iex> DomainConnect.Discovery.registrable("rent.theirplace.co.uk")
{:ok, "theirplace.co.uk", "rent"}

iex> DomainConnect.Discovery.registrable("example.com")
{:ok, "example.com", nil}

Returns {:error, :invalid_domain} if the input can't be parsed.