···1818- `Atex.HTTP.Response` struct to be returned by `Atex.HTTP.Adapter`.
1919- `Atex.IdentityResolver` module for resolving and validating an identity,
2020 either by DID or a handle.
2121+ - Also has a pluggable cache (with a default ETS implementation) for keeping
2222+ some data locally.
21232224## [0.2.0] - 2025-06-09
2325
+10
lib/atex/application.ex
···11+defmodule Atex.Application do
22+ @moduledoc false
33+44+ use Application
55+66+ def start(_type, _args) do
77+ children = [Atex.IdentityResolver.Cache]
88+ Supervisor.start_link(children, strategy: :one_for_one)
99+ end
1010+end
+17-9
lib/atex/identity_resolver.ex
···11defmodule Atex.IdentityResolver do
22- alias Atex.IdentityResolver.{DID, DIDDocument, Handle}
22+ alias Atex.IdentityResolver.{Cache, DID, DIDDocument, Handle, Identity}
3344 @handle_strategy Application.compile_env(:atex, :handle_resolver_strategy, :dns_first)
5566 # TODO: simplify errors
7788- @spec resolve(identity :: String.t()) ::
99- {:ok, document :: DIDDocument.t(), did :: String.t(), handle :: String.t()}
1010- | {:ok, DIDDocument.t()}
88+ def resolve(identifier) do
99+ # If cache fetch succeeds, then the ok tuple will be retuned by the default `with` behaviour
1010+ with {:error, :not_found} <- Cache.get(identifier),
1111+ {:ok, identity} <- do_resolve(identifier),
1212+ identity <- Cache.insert(identity) do
1313+ {:ok, identity}
1414+ end
1515+ end
1616+1717+ @spec do_resolve(identity :: String.t()) ::
1818+ {:ok, Identity.t()}
1119 | {:error, :handle_mismatch}
1220 | {:error, any()}
1313- def resolve("did:" <> _ = did) do
2121+ defp do_resolve("did:" <> _ = did) do
1422 with {:ok, document} <- DID.resolve(did),
1523 :ok <- DIDDocument.validate_for_atproto(document, did) do
1624 with handle when not is_nil(handle) <- DIDDocument.get_atproto_handle(document),
1725 {:ok, handle_did} <- Handle.resolve(handle, @handle_strategy),
1826 true <- handle_did == did do
1919- {:ok, document, did, handle}
2727+ {:ok, Identity.new(did, handle, document)}
2028 else
2129 # Not having a handle, while a little un-ergonomic, is totally valid.
2222- nil -> {:ok, document}
3030+ nil -> {:ok, Identity.new(did, nil, document)}
2331 false -> {:error, :handle_mismatch}
2432 e -> e
2533 end
2634 end
2735 end
28362929- def resolve(handle) do
3737+ defp do_resolve(handle) do
3038 with {:ok, did} <- Handle.resolve(handle, @handle_strategy),
3139 {:ok, document} <- DID.resolve(did),
3240 did_handle when not is_nil(handle) <- DIDDocument.get_atproto_handle(document),
3341 true <- did_handle == handle do
3434- {:ok, document, did, handle}
4242+ {:ok, Identity.new(did, handle, document)}
3543 else
3644 nil -> {:error, :handle_mismatch}
3745 false -> {:error, :handle_mismatch}
+42
lib/atex/identity_resolver/cache.ex
···11+defmodule Atex.IdentityResolver.Cache do
22+ # TODO: need the following:
33+ # did -> handle mapping
44+ # handle -> did mapping
55+ # did -> document mapping?
66+ # User should be able to call a single function to fetch all info for either did and handle, including the link between them.
77+ # Need some sort of TTL so that we can refresh as necessary
88+ alias Atex.IdentityResolver.Identity
99+1010+ @cache Application.compile_env(:atex, :identity_cache, Atex.IdentityResolver.Cache.ETS)
1111+1212+ @doc """
1313+ Add a new identity to the cache. Can also be used to update an identity that may already exist.
1414+1515+ Returns the input `t:Atex.IdentityResolver.Identity.t/0`.
1616+ """
1717+ @callback insert(identity :: Identity.t()) :: Identity.t()
1818+1919+ @doc """
2020+ Retrieve an identity from the cache by DID *or* handle.
2121+ """
2222+ @callback get(String.t()) :: {:ok, Identity.t()} | {:error, atom()}
2323+2424+ @doc """
2525+ Delete an identity in the cache.
2626+ """
2727+ @callback delete(String.t()) :: :noop | Identity.t()
2828+2929+ @doc """
3030+ Get the child specification for starting the cache in a supervision tree.
3131+ """
3232+ @callback child_spec(any()) :: Supervisor.child_spec()
3333+3434+ defdelegate get(identifier), to: @cache
3535+3636+ @doc false
3737+ defdelegate insert(payload), to: @cache
3838+ @doc false
3939+ defdelegate delete(snowflake), to: @cache
4040+ @doc false
4141+ defdelegate child_spec(opts), to: @cache
4242+end
+57
lib/atex/identity_resolver/cache/ets.ex
···11+defmodule Atex.IdentityResolver.Cache.ETS do
22+ alias Atex.IdentityResolver.Identity
33+ @behaviour Atex.IdentityResolver.Cache
44+ use Supervisor
55+66+ @table :atex_identities
77+88+ def start_link(opts) do
99+ Supervisor.start_link(__MODULE__, opts)
1010+ end
1111+1212+ @impl Supervisor
1313+ def init(_opts) do
1414+ :ets.new(@table, [:set, :public, :named_table])
1515+ Supervisor.init([], strategy: :one_for_one)
1616+ end
1717+1818+ @impl Atex.IdentityResolver.Cache
1919+ @spec insert(Identity.t()) :: Identity.t()
2020+ def insert(identity) do
2121+ # TODO: benchmark lookups vs match performance, is it better to use a "composite" key or two inserts?
2222+ :ets.insert(@table, {{identity.did, identity.handle}, identity})
2323+ identity
2424+ end
2525+2626+ @impl Atex.IdentityResolver.Cache
2727+ @spec get(String.t()) :: {:ok, Identity.t()} | {:error, atom()}
2828+ def get(identifier) do
2929+ lookup(identifier)
3030+ end
3131+3232+ @impl Atex.IdentityResolver.Cache
3333+ @spec delete(String.t()) :: :noop | Identity.t()
3434+ def delete(identifier) do
3535+ case lookup(identifier) do
3636+ {:ok, identity} ->
3737+ :ets.delete(@table, {identity.did, identity.handle})
3838+ identity
3939+4040+ _ ->
4141+ :noop
4242+ end
4343+ end
4444+4545+ defp lookup(identifier) do
4646+ case :ets.match(@table, {{identifier, :_}, :"$1"}) do
4747+ [] ->
4848+ case :ets.match(@table, {{:_, identifier}, :"$1"}) do
4949+ [] -> {:error, :not_found}
5050+ [[identity]] -> {:ok, identity}
5151+ end
5252+5353+ [[identity]] ->
5454+ {:ok, identity}
5555+ end
5656+ end
5757+end
+25
lib/atex/identity_resolver/identity.ex
···11+defmodule Atex.IdentityResolver.Identity do
22+ use TypedStruct
33+44+ @typedoc """
55+ The controlling DID for an identity.
66+ """
77+ @type did() :: String.t()
88+ @typedoc """
99+ The human-readable handle for an identity. Can be missing.
1010+ """
1111+ @type handle() :: String.t() | nil
1212+ @typedoc """
1313+ The resolved DID document for an identity.
1414+ """
1515+ @type document() :: Atex.IdentityResolver.DIDDocument.t()
1616+1717+ typedstruct do
1818+ field :did, did(), enforce: true
1919+ field :handle, handle()
2020+ field :document, document(), enforce: true
2121+ end
2222+2323+ @spec new(did(), handle(), document()) :: t()
2424+ def new(did, handle, document), do: %__MODULE__{did: did, handle: handle, document: document}
2525+end