Packages

Aether ATProto is a set of common & shared logic to implement the AT Protocol

Current section

Files

Jump to
aether_atproto lib aether atproto did document.ex
Raw

lib/aether/atproto/did/document.ex

defmodule Aether.ATProto.DID.Document do
@moduledoc """
Pure data structures and logic for ATProto DID Documents.
This module contains no network I/O and can be used by both clients and servers.
"""
alias Aether.ATProto.DID.Document.Service
defstruct [:id, :alsoKnownAs, :verificationMethod, :service]
@type t :: %__MODULE__{
id: String.t(),
alsoKnownAs: [String.t()] | nil,
verificationMethod: [map()] | nil,
service: [Service.t()] | nil
}
@doc """
Get the PDS (Personal Data Server) endpoint from a DID Document.
"""
@spec get_pds_endpoint(t()) :: {:ok, String.t()} | {:error, :not_found}
def get_pds_endpoint(%__MODULE__{service: services}) when is_list(services) do
case Enum.find(services, fn service ->
service.type == "AtprotoPersonalDataServer"
end) do
%Service{serviceEndpoint: endpoint} -> {:ok, endpoint}
nil -> {:error, :not_found}
end
end
def get_pds_endpoint(_), do: {:error, :not_found}
@doc """
Get a service endpoint by type from a DID Document.
"""
@spec get_service(t(), String.t()) :: {:ok, Service.t()} | {:error, :not_found}
def get_service(%__MODULE__{service: services}, type) when is_list(services) do
case Enum.find(services, fn service -> service.type == type end) do
nil -> {:error, :not_found}
service -> {:ok, service}
end
end
def get_service(_, _), do: {:error, :not_found}
@doc """
Get the handle from a DID Document's alsoKnownAs field.
"""
@spec get_handle(t()) :: String.t() | nil
def get_handle(%__MODULE__{alsoKnownAs: also_known_as}) when is_list(also_known_as) do
Enum.find_value(also_known_as, fn aka ->
case String.split(aka, "at://", parts: 2) do
["", handle] -> handle
_ -> nil
end
end)
end
def get_handle(_), do: nil
@doc """
Get the signing key (atproto verification method) from a DID Document.
"""
@spec get_signing_key(t()) :: {:ok, map()} | {:error, :not_found}
def get_signing_key(%__MODULE__{verificationMethod: methods}) when is_list(methods) do
case Enum.find(methods, fn method ->
String.ends_with?(method["id"] || "", "#atproto")
end) do
nil -> {:error, :not_found}
key -> {:ok, key}
end
end
def get_signing_key(_), do: {:error, :not_found}
@doc """
Parse a raw document map into a structured Document.
"""
@spec parse_document(map()) :: {:ok, t()}
def parse_document(doc_map) when is_map(doc_map) do
services =
doc_map
|> Map.get("service", [])
|> Enum.map(&parse_service/1)
|> Enum.reject(&is_nil/1)
{:ok,
%__MODULE__{
id: doc_map["id"],
alsoKnownAs: doc_map["alsoKnownAs"],
verificationMethod: doc_map["verificationMethod"],
service: services
}}
end
@doc """
Parse a service map into a structured Service.
"""
@spec parse_service(map()) :: Service.t() | nil
def parse_service(%{"id" => id, "type" => type, "serviceEndpoint" => endpoint}) do
%Service{
id: id,
type: type,
serviceEndpoint: endpoint
}
end
def parse_service(_), do: nil
@doc """
Create a new DID document for a PDS-hosted user.
"""
@spec create(String.t(), keyword()) :: t()
def create(did, opts \\ []) do
handle = Keyword.get(opts, :handle)
pds_endpoint = Keyword.get(opts, :pds_endpoint)
signing_key = Keyword.get(opts, :signing_key)
also_known_as = Keyword.get(opts, :also_known_as) || build_also_known_as(handle)
%__MODULE__{
id: did,
alsoKnownAs: also_known_as,
verificationMethod: build_verification_method(did, signing_key),
service: build_services(pds_endpoint)
}
end
@doc """
Create a DID document for did:web.
"""
@spec create_web(String.t(), keyword()) :: t()
def create_web(domain, opts \\ []) do
did = "did:web:#{domain}"
signing_key = Keyword.get(opts, :signing_key)
service_endpoint = Keyword.get(opts, :service_endpoint)
%__MODULE__{
id: did,
alsoKnownAs: Keyword.get(opts, :also_known_as, []),
verificationMethod: build_verification_method(did, signing_key),
service: build_services(service_endpoint)
}
end
@doc """
Add a service endpoint to a DID document.
"""
@spec add_service(t(), keyword()) :: t()
def add_service(%__MODULE__{service: services} = doc, opts) do
service = %Service{
id: Keyword.fetch!(opts, :id),
type: Keyword.fetch!(opts, :type),
serviceEndpoint: Keyword.fetch!(opts, :endpoint)
}
%{doc | service: (services || []) ++ [service]}
end
@doc """
Update the verification method (signing key) in a DID document.
"""
@spec update_signing_key(t(), String.t()) :: t()
def update_signing_key(%__MODULE__{id: did} = doc, signing_key) do
%{doc | verificationMethod: build_verification_method(did, signing_key)}
end
# Private helper functions
defp build_also_known_as(nil), do: []
defp build_also_known_as(handle), do: ["at://#{handle}"]
defp build_verification_method(_did, nil), do: []
defp build_verification_method(did, signing_key) do
[
%{
"id" => "#{did}#atproto",
"type" => "Multikey",
"controller" => did,
"publicKeyMultibase" => signing_key
}
]
end
defp build_services(nil), do: []
defp build_services(pds_endpoint) do
[
%Service{
id: "#atproto_pds",
type: "AtprotoPersonalDataServer",
serviceEndpoint: pds_endpoint
}
]
end
end