An Elixir toolkit for the AT Protocol. hexdocs.pm/atex
elixir bluesky atproto decentralization
25
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: service auth xrpc client

+82 -1
+1
CHANGELOG.md
··· 48 48 validation and validation if passed the name of a module using `deflexicon`. 49 49 - `deflexicon` now emits `content_type/0` functions (on `Input` submodules for typed JSON bodies, 50 50 otherwise on the root module) for procedures. 51 + - `Atex.XRPC.ServiceAuthClient` module for making requests to other atproto services using a service auth token. 51 52 52 53 ### Fixed 53 54
+79
lib/atex/xrpc/service_auth_client.ex
··· 1 + defmodule Atex.XRPC.ServiceAuthClient do 2 + @moduledoc """ 3 + An XRPC client that uses a inter-service auth JWT to interact with another 4 + service on a user's behalf. See `Atex.ServiceAuth` and 5 + [`com.atproto.server.getServiceAuth`](https://github.com/bluesky-social/atproto/blob/main/lexicons/com/atproto/server/getServiceAuth.json) 6 + for more information. 7 + 8 + ## Usage 9 + 10 + client = Atex.XRPC.ServiceAuthClient.new("<jwt>") 11 + 12 + {:ok, response, _} = Atex.XRPC.get(client, "com.example.authenticatedXRPC") 13 + """ 14 + 15 + alias Atex.{DID.Document, IdentityResolver, XRPC} 16 + 17 + use TypedStruct 18 + @behaviour Atex.XRPC.Client 19 + 20 + typedstruct do 21 + field :token, String.t(), enforce: true 22 + end 23 + 24 + @doc """ 25 + Create a new `Atex.XRPC.ServiceAuthClient` from a service auth JWT. 26 + 27 + The JWT is stored as-is; no validation is performed at construction time. 28 + Endpoint resolution and token use happen on the first (and only valid) call 29 + to `get/3` or `post/3`. 30 + 31 + ## Examples 32 + 33 + iex> Atex.XRPC.ServiceAuthClient.new("eyJ...") 34 + %Atex.XRPC.ServiceAuthClient{token: "eyJ..."} 35 + """ 36 + @spec new(String.t()) :: t() 37 + def new(token) when is_binary(token), do: %__MODULE__{token: token} 38 + 39 + @impl true 40 + def get(%__MODULE__{} = client, resource, opts \\ []) do 41 + with {:ok, endpoint} <- resolve_endpoint(client) do 42 + request(client, opts ++ [method: :get, url: XRPC.url(endpoint, resource)]) 43 + end 44 + end 45 + 46 + @impl true 47 + def post(%__MODULE__{} = client, resource, opts \\ []) do 48 + with {:ok, endpoint} <- resolve_endpoint(client) do 49 + request(client, opts ++ [method: :post, url: XRPC.url(endpoint, resource)]) 50 + end 51 + end 52 + 53 + @spec request(t(), keyword()) :: {:ok, Req.Response.t(), t()} | {:error, any(), t()} 54 + defp request(client, opts) do 55 + req = opts |> Req.new() |> put_auth(client.token) 56 + 57 + case Req.request(req) do 58 + {:ok, response} -> {:ok, response, client} 59 + {:error, reason} -> {:error, reason, client} 60 + end 61 + end 62 + 63 + @spec resolve_endpoint(t()) :: {:ok, String.t()} | {:error, any()} 64 + defp resolve_endpoint(%__MODULE__{token: token}) do 65 + %{fields: %{"aud" => aud}} = JOSE.JWT.peek(token) 66 + 67 + with {:ok, identity} <- IdentityResolver.resolve(aud), 68 + endpoint when not is_nil(endpoint) <- Document.get_pds_endpoint(identity.document) do 69 + {:ok, endpoint} 70 + else 71 + nil -> {:error, :no_pds_endpoint} 72 + err -> err 73 + end 74 + end 75 + 76 + @spec put_auth(Req.Request.t(), String.t()) :: Req.Request.t() 77 + defp put_auth(request, token), 78 + do: Req.Request.put_header(request, "authorization", "Bearer #{token}") 79 + end
+2 -1
mix.exs
··· 69 69 source_ref: "v#{@version}", 70 70 formatters: ["html"], 71 71 groups_for_modules: [ 72 - "Data types": [Atex.AtURI, Atex.DID, Atex.Handle, Atex.NSID, Atex.TID], 72 + "Data types": [Atex.AtURI, ~r/^Atex\.DID/, Atex.Handle, Atex.NSID, Atex.TID], 73 73 XRPC: ~r/^Atex\.XRPC/, 74 74 PLC: [Atex.PLC], 75 75 OAuth: [Atex.Config.OAuth, ~r/^Atex\.OAuth/], 76 76 Identity: [Atex.Config.IdentityResolver, ~r/^Atex\.IdentityResolver/], 77 77 Lexicons: ~r/^Atex\.Lexicon/, 78 + "Service Auth": ~r/^Atex\.ServiceAuth/, 78 79 "Implementation details": [Atex.Base32Sortable, Atex.Peri] 79 80 ] 80 81 ]