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: user-agent config

+169 -2
+1
.gitignore
··· 19 19 tmp 20 20 temp 21 21 .dexter* 22 + docs/superpowers
+3
CHANGELOG.md
··· 34 34 - Optional `:telemetry` instrumentation via `Atex.Telemetry`. Add `{:telemetry, "~> 1.0"}` to 35 35 your deps to receive events from XRPC requests, identity resolution, OAuth flows, and service 36 36 auth validation. See `Atex.Telemetry` for the full event catalogue. 37 + - New `:user_agent` config key under `config :atex`. When set, all outgoing XRPC 38 + requests include a `User-Agent` header of `"<user_agent> (atex/<version>)"`. 39 + Defaults to `"atex/<version>"`. See `Atex.Config.user_agent/0`. 37 40 38 41 ### Fixed 39 42
+1 -1
README.md
··· 4 4 5 5 ## Feature map 6 6 7 - - [x] ATProto strings 7 + - [x] atproto strings 8 8 - [x] `at://` links 9 9 - [x] TIDs 10 10 - [x] NSIDs
+32 -1
lib/atex/config.ex
··· 8 8 9 9 config :atex, 10 10 plc_directory_url: "https://plc.directory", 11 - service_did: "did:web:my-service.example" 11 + service_did: "did:web:my-service.example", 12 + user_agent: "my-app/1.0.0" 12 13 13 14 - `:plc_directory_url` - Base URL for the did:plc directory server. 14 15 Defaults to `"https://plc.directory"`. 15 16 - `:service_did` - The DID of this service, used as the expected `aud` claim 16 17 when validating incoming inter-service auth JWTs via `Atex.XRPC.Router`. 17 18 Required when using `Atex.XRPC.Router` with auth enabled. 19 + - `:user_agent` - Custom User-Agent prefix for outgoing XRPC requests. When 20 + set, the `User-Agent` header becomes `"<user_agent> (atex/<version>)"`. 21 + Defaults to `"atex/<version>"`. 18 22 """ 19 23 20 24 @doc """ ··· 34 38 """ 35 39 @spec service_did :: String.t() | nil 36 40 def service_did, do: Application.get_env(:atex, :service_did) 41 + 42 + @doc """ 43 + Returns the `User-Agent` header value for outgoing XRPC requests. 44 + 45 + Reads `:user_agent` from the `:atex` application environment. When set, the 46 + atex library version is appended in parentheses. When unset, returns only 47 + the atex version. 48 + 49 + ## Examples 50 + 51 + # Default (no :user_agent configured): 52 + # => "atex/<version>" 53 + 54 + # With `config :atex, user_agent: "my-app/1.0.0"`: 55 + # => "my-app/1.0.0 (atex/<version>)" 56 + 57 + """ 58 + @spec user_agent :: String.t() 59 + def user_agent do 60 + version = to_string(Application.spec(:atex, :vsn)) 61 + atex_ua = "atex/#{version}" 62 + 63 + case Application.get_env(:atex, :user_agent) do 64 + nil -> atex_ua 65 + custom -> "#{custom} (#{atex_ua})" 66 + end 67 + end 37 68 end
+23
lib/atex/xrpc.ex
··· 164 164 def unauthed_get(endpoint, name, opts \\ []) do 165 165 (opts ++ [method: :get, url: url(endpoint, name)]) 166 166 |> Req.new() 167 + |> attach_user_agent() 167 168 |> Atex.Telemetry.attach_req_plugin(client_type: :unauthed) 168 169 |> Req.request() 169 170 end ··· 176 177 def unauthed_post(endpoint, name, opts \\ []) do 177 178 (opts ++ [method: :post, url: url(endpoint, name)]) 178 179 |> Req.new() 180 + |> attach_user_agent() 179 181 |> Atex.Telemetry.attach_req_plugin(client_type: :unauthed) 180 182 |> Req.request() 181 183 end ··· 190 192 """ 191 193 @spec url(String.t(), String.t()) :: String.t() 192 194 def url(endpoint, resource) when is_binary(endpoint), do: "#{endpoint}/xrpc/#{resource}" 195 + 196 + # TODO: if cross-cutting request concerns (user-agent, auth, telemetry) accumulate 197 + # further, consider a shared build_request/2 that all clients use as their starting point. 198 + @doc """ 199 + Attach the `User-Agent` header to a `Req.Request`. 200 + 201 + Sets the `user-agent` header based on the `:user_agent` config key (see 202 + `Atex.Config.user_agent/0`). All built-in XRPC clients call this when 203 + building requests. Custom `Atex.XRPC.Client` implementations should call 204 + this too. 205 + 206 + ## Example 207 + 208 + Req.new(method: :get, url: url) 209 + |> Atex.XRPC.attach_user_agent() 210 + |> Atex.Telemetry.attach_req_plugin(client_type: :login) 211 + |> Req.request() 212 + """ 213 + @spec attach_user_agent(Req.Request.t()) :: Req.Request.t() 214 + def attach_user_agent(req), 215 + do: Req.Request.put_header(req, "user-agent", Atex.Config.user_agent()) 193 216 194 217 @spec put_params(keyword(), struct()) :: keyword() 195 218 defp put_params(keyword, %{params: params}),
+2
lib/atex/xrpc/login_client.ex
··· 86 86 fn -> 87 87 request = 88 88 Req.new(method: :post, url: XRPC.url(endpoint, "com.atproto.server.refreshSession")) 89 + |> Atex.XRPC.attach_user_agent() 89 90 |> put_auth(refresh_token) 90 91 91 92 result = ··· 121 122 request = 122 123 opts 123 124 |> Req.new() 125 + |> Atex.XRPC.attach_user_agent() 124 126 |> put_auth(client.access_token) 125 127 |> Atex.Telemetry.attach_req_plugin(client_type: :login) 126 128
+1
lib/atex/xrpc/oauth_client.ex
··· 237 237 opts 238 238 |> Keyword.put(:url, url) 239 239 |> Req.new() 240 + |> Atex.XRPC.attach_user_agent() 240 241 |> Req.Request.put_header("authorization", "DPoP #{session.access_token}") 241 242 |> Atex.Telemetry.attach_req_plugin(client_type: :oauth) 242 243
+1
lib/atex/xrpc/service_auth_client.ex
··· 54 54 req = 55 55 opts 56 56 |> Req.new() 57 + |> Atex.XRPC.attach_user_agent() 57 58 |> put_auth(client.token) 58 59 |> Atex.Telemetry.attach_req_plugin(client_type: :service_auth) 59 60
+2
lib/atex/xrpc/unauthed_client.ex
··· 27 27 def get(%__MODULE__{endpoint: endpoint} = client, resource, opts \\ []) do 28 28 (opts ++ [method: :get, url: Atex.XRPC.url(endpoint, resource)]) 29 29 |> Req.new() 30 + |> Atex.XRPC.attach_user_agent() 30 31 |> Atex.Telemetry.attach_req_plugin(client_type: :unauthed) 31 32 |> Req.request() 32 33 |> case do ··· 39 40 def post(%__MODULE__{endpoint: endpoint} = client, resource, opts \\ []) do 40 41 (opts ++ [method: :post, url: Atex.XRPC.url(endpoint, resource)]) 41 42 |> Req.new() 43 + |> Atex.XRPC.attach_user_agent() 42 44 |> Atex.Telemetry.attach_req_plugin(client_type: :unauthed) 43 45 |> Req.request() 44 46 |> case do
+103
test/atex/xrpc/user_agent_test.exs
··· 1 + defmodule Atex.XRPC.UserAgentTest do 2 + use ExUnit.Case, async: false 3 + 4 + # Captures the user-agent header sent by a request and sends it to the test process. 5 + defmodule CaptureUAPlug do 6 + @moduledoc false 7 + import Plug.Conn 8 + def init(opts), do: opts 9 + def call(conn, _opts) do 10 + send(self(), {:user_agent, get_req_header(conn, "user-agent")}) 11 + send_resp(conn, 200, Jason.encode!(%{})) 12 + end 13 + end 14 + 15 + defp version, do: to_string(Application.spec(:atex, :vsn)) 16 + 17 + describe "Atex.Config.user_agent/0" do 18 + test "returns atex/<version> when :user_agent not configured" do 19 + assert Atex.Config.user_agent() == "atex/#{version()}" 20 + end 21 + 22 + test "returns custom ua with atex suffix when :user_agent is configured" do 23 + Application.put_env(:atex, :user_agent, "my-app/1.0.0") 24 + on_exit(fn -> Application.delete_env(:atex, :user_agent) end) 25 + 26 + assert Atex.Config.user_agent() == "my-app/1.0.0 (atex/#{version()})" 27 + end 28 + end 29 + 30 + describe "Atex.XRPC.attach_user_agent/1" do 31 + test "sets user-agent header to default" do 32 + expected_ua = "atex/#{version()}" 33 + 34 + req = 35 + Req.new( 36 + method: :get, 37 + url: "http://example.com/xrpc/com.example.test", 38 + plug: CaptureUAPlug 39 + ) 40 + |> Atex.XRPC.attach_user_agent() 41 + 42 + {:ok, _} = Req.request(req) 43 + 44 + assert_receive {:user_agent, [^expected_ua]} 45 + end 46 + 47 + test "sets user-agent header to configured value" do 48 + expected_ua = "my-app/1.0.0 (atex/#{version()})" 49 + Application.put_env(:atex, :user_agent, "my-app/1.0.0") 50 + on_exit(fn -> Application.delete_env(:atex, :user_agent) end) 51 + 52 + req = 53 + Req.new( 54 + method: :get, 55 + url: "http://example.com/xrpc/com.example.test", 56 + plug: CaptureUAPlug 57 + ) 58 + |> Atex.XRPC.attach_user_agent() 59 + 60 + {:ok, _} = Req.request(req) 61 + 62 + assert_receive {:user_agent, [^expected_ua]} 63 + end 64 + end 65 + 66 + describe "UnauthedClient" do 67 + test "sends default user-agent" do 68 + expected_ua = "atex/#{version()}" 69 + client = Atex.XRPC.UnauthedClient.new("http://example.com") 70 + Atex.XRPC.get(client, "com.example.test", plug: CaptureUAPlug) 71 + assert_receive {:user_agent, [^expected_ua]} 72 + end 73 + 74 + test "sends configured user-agent" do 75 + expected_ua = "my-app/1.0.0 (atex/#{version()})" 76 + Application.put_env(:atex, :user_agent, "my-app/1.0.0") 77 + on_exit(fn -> Application.delete_env(:atex, :user_agent) end) 78 + 79 + client = Atex.XRPC.UnauthedClient.new("http://example.com") 80 + Atex.XRPC.get(client, "com.example.test", plug: CaptureUAPlug) 81 + assert_receive {:user_agent, [^expected_ua]} 82 + end 83 + end 84 + 85 + describe "LoginClient" do 86 + test "sends default user-agent" do 87 + expected_ua = "atex/#{version()}" 88 + client = Atex.XRPC.LoginClient.new("http://example.com", "fake-access-token", nil) 89 + Atex.XRPC.get(client, "com.example.test", plug: CaptureUAPlug) 90 + assert_receive {:user_agent, [^expected_ua]} 91 + end 92 + 93 + test "sends configured user-agent" do 94 + expected_ua = "my-app/1.0.0 (atex/#{version()})" 95 + Application.put_env(:atex, :user_agent, "my-app/1.0.0") 96 + on_exit(fn -> Application.delete_env(:atex, :user_agent) end) 97 + 98 + client = Atex.XRPC.LoginClient.new("http://example.com", "fake-access-token", nil) 99 + Atex.XRPC.get(client, "com.example.test", plug: CaptureUAPlug) 100 + assert_receive {:user_agent, [^expected_ua]} 101 + end 102 + end 103 + end