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.

fix: update atex.lexicons to validate JSON schema, and work with new NSID struct

+32 -47
+13 -11
lib/atex/crypto.ex
··· 31 31 @p256_a @p256_p - 3 32 32 @p256_b 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B 33 33 @p256_n 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551 34 - @p256_oid {1, 2, 840, 10045, 3, 1, 7} 34 + @p256_oid {1, 2, 840, 10_045, 3, 1, 7} 35 35 36 36 # secp256k1 37 37 @k256_p 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F ··· 398 398 # If s > n/2, replace s with n - s and re-encode the DER sequence. 399 399 @spec normalize_low_s(binary(), pos_integer()) :: binary() 400 400 defp normalize_low_s(der_sig, curve_order) do 401 - with {:ok, r_bin, s_bin} <- parse_der_ecdsa(der_sig) do 402 - s = :binary.decode_unsigned(s_bin) 401 + case parse_der_ecdsa(der_sig) do 402 + {:ok, r_bin, s_bin} -> 403 + s = :binary.decode_unsigned(s_bin) 403 404 404 - if s <= div(curve_order, 2) do 405 + if s <= div(curve_order, 2) do 406 + der_sig 407 + else 408 + new_s = curve_order - s 409 + new_s_bin = :binary.encode_unsigned(new_s) 410 + encode_der_ecdsa(r_bin, new_s_bin) 411 + end 412 + 413 + _ -> 405 414 der_sig 406 - else 407 - new_s = curve_order - s 408 - new_s_bin = :binary.encode_unsigned(new_s) 409 - encode_der_ecdsa(r_bin, new_s_bin) 410 - end 411 - else 412 - _ -> der_sig 413 415 end 414 416 end 415 417
-6
lib/atex/identity_resolver/cache.ex
··· 1 1 defmodule Atex.IdentityResolver.Cache do 2 - # TODO: need the following: 3 - # did -> handle mapping 4 - # handle -> did mapping 5 - # did -> document mapping? 6 - # User should be able to call a single function to fetch all info for either did and handle, including the link between them. 7 - # Need some sort of TTL so that we can refresh as necessary 8 2 alias Atex.IdentityResolver.Identity 9 3 10 4 @cache Application.compile_env(:atex, :identity_cache, Atex.IdentityResolver.Cache.ETS)
-2
lib/atex/lexicon.ex
··· 171 171 def_to_schema(nsid, def_name, record) 172 172 end 173 173 174 - # TODO: add struct to types 175 174 defp def_to_schema( 176 175 nsid, 177 176 def_name, ··· 290 289 [{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}, quoted_struct}] 291 290 end 292 291 293 - # TODO: validating errors? 294 292 defp def_to_schema(nsid, _def_name, %{type: "query"} = def) do 295 293 params = 296 294 if def[:parameters] do
+1 -2
lib/atex/lexicon/validators.ex
··· 99 99 @spec strings_to_re(list(String.t())) :: Regex.t() 100 100 defp strings_to_re(strings) do 101 101 strings 102 - |> Enum.map(&String.replace(&1, "*", ".+")) 103 - |> Enum.join("|") 102 + |> Enum.map_join("|", &String.replace(&1, "*", ".+")) 104 103 |> then(&~r/^(#{&1})$/) 105 104 end 106 105 end
+4 -7
lib/atex/oauth.ex
··· 366 366 } 367 367 368 368 body = 369 - if !Config.is_localhost(), 370 - do: 369 + if Config.is_localhost(), 370 + do: body, 371 + else: 371 372 Map.merge(body, %{ 372 373 client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", 373 374 client_assertion: client_assertion 374 - }), 375 - else: body 375 + }) 376 376 377 377 Req.new(method: :post, url: authz_metadata.token_endpoint, form: body) 378 378 |> send_oauth_dpop_request(dpop_key) ··· 620 620 {:ok, body, dpop_nonce} 621 621 622 622 {:ok, %{body: %{"error" => error, "error_description" => error_description}}} -> 623 - IO.inspect(request) 624 623 {:error, {:oauth_error, error, error_description}, dpop_nonce} 625 624 626 625 {:ok, _} -> ··· 631 630 end 632 631 633 632 true -> 634 - IO.inspect(request) 635 - 636 633 {:error, {:oauth_error, resp.body["error"], resp.body["error_description"]}, 637 634 dpop_nonce} 638 635 end
+3 -4
lib/atex/oauth/permission.ex
··· 268 268 269 269 defp stringify_parameters(params) do 270 270 params 271 - |> Enum.map(fn {key, value} -> "#{key}=#{encode_param_value(value)}" end) 272 - |> Enum.join("&") 271 + |> Enum.map_join("&", fn {key, value} -> "#{key}=#{encode_param_value(value)}" end) 273 272 |> then(&"?#{&1}") 274 273 end 275 274 ··· 352 351 struct = %__MODULE__{ 353 352 resource: "account", 354 353 positional: Atom.to_string(attr), 355 - parameters: if(!is_nil(action), do: [{"action", Atom.to_string(action)}], else: []) 354 + parameters: if(action != nil, do: [{"action", Atom.to_string(action)}], else: []) 356 355 } 357 356 358 357 if as_string, do: to_string(struct), else: struct ··· 792 791 aud = Keyword.get(opts, :aud) 793 792 as_string = Keyword.get(opts, :as_string) 794 793 795 - parameters = if !is_nil(aud), do: [{"aud", aud}], else: [] 794 + parameters = if aud != nil, do: [{"aud", aud}], else: [] 796 795 797 796 struct = %__MODULE__{ 798 797 resource: "include",
+1 -3
lib/atex/oauth/plug.ex
··· 268 268 message: "OAuth issuer does not match PDS' authorization server", 269 269 reason: :issuer_mismatch 270 270 271 - err -> 272 - IO.inspect(err) 273 - 271 + _err -> 274 272 raise Atex.OAuth.Error, 275 273 message: "Failed to validate authorization code or token", 276 274 reason: :token_validation_failed
+1 -2
lib/atex/util.ex
··· 79 79 80 80 defp format_ipv6({a, b, c, d, e, f, g, h}) when is_ipv6(a, b, c, d, e, f, g, h) do 81 81 [a, b, c, d, e, f, g, h] 82 - |> Enum.map(&Integer.to_string(&1, 16)) 83 - |> Enum.join(":") 82 + |> Enum.map_join(":", &Integer.to_string(&1, 16)) 84 83 |> String.downcase() 85 84 end 86 85 end
+3 -1
lib/atex/xrpc.ex
··· 174 174 175 175 if Code.ensure_loaded?(module) and function_exported?(module, :content_type, 0) do 176 176 headers = Keyword.get(opts, :headers, []) 177 - has_content_type? = Enum.any?(headers, fn {k, _} -> String.downcase(k) == "content-type" end) 177 + 178 + has_content_type? = 179 + Enum.any?(headers, fn {k, _} -> String.downcase(k) == "content-type" end) 178 180 179 181 unless has_content_type? do 180 182 raise """
+4 -7
lib/mix/tasks/atex.lexicons.ex
··· 51 51 output = Keyword.get(options, :output, "lib/atproto") 52 52 paths = Enum.flat_map(globs, &Path.wildcard/1) 53 53 54 - if length(paths) == 0 do 54 + if paths == [] do 55 55 Mix.shell().error("No valid search paths have been provided, aborting.") 56 56 else 57 57 Mix.shell().info("Generating modules for lexicons into #{output}") ··· 63 63 end 64 64 end 65 65 66 - # TODO: validate schema? 67 66 defp generate(input, output) do 68 67 lexicon = 69 68 input 70 69 |> File.read!() 71 70 |> JSON.decode!() 72 - 73 - if not is_binary(lexicon["id"]) do 74 - raise ArgumentError, message: "Malformed lexicon: does not have an `id` field." 75 - end 71 + |> Recase.Enumerable.atomize_keys() 72 + |> Atex.Lexicon.Schema.lexicon!() 76 73 77 74 code = lexicon |> template() |> Code.format_string!() |> Enum.join("") 78 75 79 76 file_path = 80 - lexicon["id"] 77 + lexicon.id 81 78 |> String.split(".") 82 79 |> Enum.join("/") 83 80 |> then(&(&1 <> ".ex"))
+1 -1
priv/templates/lexicon.eex
··· 1 - defmodule <%= Atex.NSID.to_atom(lexicon["id"], false) %> do 1 + defmodule <%= lexicon.id |> Atex.NSID.new!() |> Atex.NSID.to_atom(false) %> do 2 2 @moduledoc false 3 3 use Atex.Lexicon 4 4
+1 -1
test/atex/repo/fixtures_test.exs
··· 103 103 104 104 test "list_record_keys returns rkeys for a collection", %{repo: repo} do 105 105 {:ok, keys} = Repo.list_record_keys(repo, "app.bsky.feed.post") 106 - assert length(keys) > 0 106 + assert keys != [] 107 107 assert Enum.all?(keys, &is_binary/1) 108 108 assert keys == Enum.sort(keys) 109 109 end