···4848 validation and validation if passed the name of a module using `deflexicon`.
4949- `deflexicon` now emits `content_type/0` functions (on `Input` submodules for typed JSON bodies,
5050 otherwise on the root module) for procedures.
5151+- `Atex.XRPC.ServiceAuthClient` module for making requests to other atproto services using a service auth token.
51525253### Fixed
5354
+79
lib/atex/xrpc/service_auth_client.ex
···11+defmodule Atex.XRPC.ServiceAuthClient do
22+ @moduledoc """
33+ An XRPC client that uses a inter-service auth JWT to interact with another
44+ service on a user's behalf. See `Atex.ServiceAuth` and
55+ [`com.atproto.server.getServiceAuth`](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/getServiceAuth.json)
66+ for more information.
77+88+ ## Usage
99+1010+ client = Atex.XRPC.ServiceAuthClient.new("<jwt>")
1111+1212+ {:ok, response, _} = Atex.XRPC.get(client, "com.example.authenticatedXRPC")
1313+ """
1414+1515+ alias Atex.{DID.Document, IdentityResolver, XRPC}
1616+1717+ use TypedStruct
1818+ @behaviour Atex.XRPC.Client
1919+2020+ typedstruct do
2121+ field :token, String.t(), enforce: true
2222+ end
2323+2424+ @doc """
2525+ Create a new `Atex.XRPC.ServiceAuthClient` from a service auth JWT.
2626+2727+ The JWT is stored as-is; no validation is performed at construction time.
2828+ Endpoint resolution and token use happen on the first (and only valid) call
2929+ to `get/3` or `post/3`.
3030+3131+ ## Examples
3232+3333+ iex> Atex.XRPC.ServiceAuthClient.new("eyJ...")
3434+ %Atex.XRPC.ServiceAuthClient{token: "eyJ..."}
3535+ """
3636+ @spec new(String.t()) :: t()
3737+ def new(token) when is_binary(token), do: %__MODULE__{token: token}
3838+3939+ @impl true
4040+ def get(%__MODULE__{} = client, resource, opts \\ []) do
4141+ with {:ok, endpoint} <- resolve_endpoint(client) do
4242+ request(client, opts ++ [method: :get, url: XRPC.url(endpoint, resource)])
4343+ end
4444+ end
4545+4646+ @impl true
4747+ def post(%__MODULE__{} = client, resource, opts \\ []) do
4848+ with {:ok, endpoint} <- resolve_endpoint(client) do
4949+ request(client, opts ++ [method: :post, url: XRPC.url(endpoint, resource)])
5050+ end
5151+ end
5252+5353+ @spec request(t(), keyword()) :: {:ok, Req.Response.t(), t()} | {:error, any(), t()}
5454+ defp request(client, opts) do
5555+ req = opts |> Req.new() |> put_auth(client.token)
5656+5757+ case Req.request(req) do
5858+ {:ok, response} -> {:ok, response, client}
5959+ {:error, reason} -> {:error, reason, client}
6060+ end
6161+ end
6262+6363+ @spec resolve_endpoint(t()) :: {:ok, String.t()} | {:error, any()}
6464+ defp resolve_endpoint(%__MODULE__{token: token}) do
6565+ %{fields: %{"aud" => aud}} = JOSE.JWT.peek(token)
6666+6767+ with {:ok, identity} <- IdentityResolver.resolve(aud),
6868+ endpoint when not is_nil(endpoint) <- Document.get_pds_endpoint(identity.document) do
6969+ {:ok, endpoint}
7070+ else
7171+ nil -> {:error, :no_pds_endpoint}
7272+ err -> err
7373+ end
7474+ end
7575+7676+ @spec put_auth(Req.Request.t(), String.t()) :: Req.Request.t()
7777+ defp put_auth(request, token),
7878+ do: Req.Request.put_header(request, "authorization", "Bearer #{token}")
7979+end