DomainConnect (DomainConnect v0.5.0)

Copy Markdown View Source

Elixir client for the Domain Connect protocol.

Domain Connect lets a Service Provider (you) set up the DNS a custom domain needs to point at your app without the domain owner having to understand CNAME/TXT records. The owner clicks "connect", consents at their own DNS provider, and the records are applied. ~20 providers implement it, including GoDaddy, IONOS, Cloudflare, Squarespace Domains, WordPress.com, and Plesk.

This library covers the Service Provider side of the synchronous flow:

# 1. Discover whether (and how) the domain's DNS provider supports it.
{:ok, config} = DomainConnect.discover("rent.theirplace.com")

# 2. Build the URL to send the owner to. They click "apply" at their
#    provider, the records land, your custom domain goes live.
{:ok, url} =
  DomainConnect.apply_url(config,
    provider_id: "exampleservice.com",
    service_id: "rentals",
    params: %{"target" => "portal.unitops.app"}
  )

provider_id/service_id identify your Domain Connect template (the record set you register with each DNS provider), not the DNS provider. See apply_url/2.

The asynchronous OAuth flow (programmatic record writes) is supported via async_consent_url/2, async_token/2, async_apply/3, and async_refresh/2 (see DomainConnect.Async). Signed templates are supported on both flows by passing :private_key + :key_id (see apply_url/2 and DomainConnect.Signing).

Summary

Functions

Builds the synchronous apply URL to redirect the domain owner to.

Asynchronous flow: apply a template using a token. See DomainConnect.Async.apply/3.

Asynchronous (OAuth) flow: build the consent URL to redirect the owner to. See DomainConnect.Async.consent_url/2.

Asynchronous flow: refresh an access token. See DomainConnect.Async.refresh/2.

Asynchronous flow: exchange an authorization code for a token. See DomainConnect.Async.get_token/2.

Discovers the Domain Connect configuration for domain. See DomainConnect.Discovery.discover/2 for options.

Convenience: true if domain's provider supports the synchronous apply flow.

Splits a domain into its registrable zone and sub-host via the Public Suffix List, without any DNS lookup.

Functions

apply_url(config, opts)

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

Builds the synchronous apply URL to redirect the domain owner to.

On arrival the DNS provider shows a consent screen for your template and, on approval, writes the records. Resolves to:

<url_sync_ux>/v2/domainTemplates/providers/<provider_id>/services/<service_id>/apply?...

Required options

  • :provider_id — your template's providerId (e.g. "exampleservice.com").
  • :service_id — your template's serviceId (e.g. "hosting").

Optional options

  • :params — a map of template variable values (string keys), merged into the query (e.g. %{"target" => "portal.unitops.app", "ttl" => "3600"}). Must not contain the reserved keys domain, host, redirect_uri, state, or groupId.
  • :redirect_uri — where the provider returns the owner after applying.
  • :state — opaque value echoed back to :redirect_uri.
  • :group_ids — record group ids for partial templates; a list of strings or a comma-separated string.

Returns {:error, :sync_not_supported} if the provider advertised no sync UX, {:error, {:missing, key}} if a required id is absent, or {:error, {:reserved_params, keys}} if :params collides with a reserved key.

Signed templates

For templates that require a signed request, pass :private_key (an unencrypted RSA private-key PEM) and :key_id (the TXT record name where the matching public key is published). The query is signed (RSA-SHA256) and sig /key are appended. Without both, an unsigned URL is built.

async_apply(config, token, opts)

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

Asynchronous flow: apply a template using a token. See DomainConnect.Async.apply/3.

async_refresh(config, opts)

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

Asynchronous flow: refresh an access token. See DomainConnect.Async.refresh/2.

async_token(config, opts)

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

Asynchronous flow: exchange an authorization code for a token. See DomainConnect.Async.get_token/2.

discover(domain, opts \\ [])

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

Discovers the Domain Connect configuration for domain. See DomainConnect.Discovery.discover/2 for options.

supported?(domain, opts \\ [])

@spec supported?(
  String.t(),
  keyword()
) :: boolean()

Convenience: true if domain's provider supports the synchronous apply flow.

Swallows discovery errors into false. Accepts the same options as discover/2.

zone_and_host(domain)

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

Splits a domain into its registrable zone and sub-host via the Public Suffix List, without any DNS lookup.

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

Useful for showing the owner exactly what you'll configure ("we'll set up rent on theirplace.co.uk") before discovery.