A set of utilities for working with the AT Protocol in Elixir.

Compare changes

Choose any two refs to compare.

Changed files
+5818 -319
config
examples
lib
atex
atproto
com
atproto
admin
identity
label
lexicon
moderation
repo
server
sync
mix
+2 -2
.formatter.exs
···
# Used by "mix format"
[
-
inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"],
-
import_deps: [:typedstruct, :peri],
+
inputs: ["{mix,.formatter,.credo}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"],
+
import_deps: [:typedstruct, :peri, :plug],
export: [
locals_without_parens: [deflexicon: 1]
]
+5 -16
.gitignore
···
-
# The directory Mix will write compiled artifacts to.
/_build/
-
-
# If you run "mix test --cover", coverage assets end up here.
/cover/
-
-
# The directory Mix downloads your dependencies sources to.
/deps/
-
-
# Where third-party dependencies like ExDoc output generated docs.
/doc/
-
-
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
-
-
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
-
-
# Ignore package tarball (built via "mix hex.build").
atex-*.tar
-
-
# Temporary files, for example, from tests.
/tmp/
.envrc
···
.vscode/
.elixir_ls
lexicons
-
lib/atproto
+
secrets
+
.DS_Store
+
CLAUDE.md
+
tmp
+
temp
+37 -1
CHANGELOG.md
···
<!-- ## [Unreleased] -->
+
## [0.6.0] - 2025-11-25
+
+
### Breaking Changes
+
+
- `deflexicon` now converts all def names to be in snake_case instead of the
+
casing as written the lexicon.
+
+
### Added
+
+
- `deflexicon` now emits structs for records, objects, queries, and procedures.
+
- `Atex.XRPC.get/3` and `Atex.XRPC.post/3` now support having a lexicon struct
+
as the second argument instead of the method's name, making it easier to have
+
properly checked XRPC calls.
+
- Add pre-transpiled modules for the core `com.atproto` lexicons.
+
+
## [0.5.0] - 2025-10-11
+
+
### Breaking Changes
+
+
- Remove `Atex.HTTP` and associated modules as the abstraction caused a bit too
+
much complexities for how early atex is. It may come back in the future as
+
something more fleshed out once we're more stable.
+
- Rename `Atex.XRPC.Client` to `Atex.XRPC.LoginClient`
+
+
### Added
+
+
- `Atex.OAuth` module with utilites for handling some OAuth functionality.
+
- `Atex.OAuth.Plug` module (if Plug is loaded) which provides a basic but
+
complete OAuth flow, including storing the tokens in `Plug.Session`.
+
- `Atex.XRPC.Client` behaviour for implementing custom client variants.
+
- `Atex.XRPC` now supports using different client implementations.
+
- `Atex.XRPC.OAuthClient` to make XRPC calls on the behalf of a user who has
+
authenticated with ATProto OAuth.
+
## [0.4.0] - 2025-08-27
### Added
···
Initial release.
-
[unreleased]: https://github.com/cometsh/atex/compare/v0.4.0...HEAD
+
[unreleased]: https://github.com/cometsh/atex/compare/v0.6.0...HEAD
+
[0.6.0]: https://github.com/cometsh/atex/releases/tag/v0.6.0
+
[0.5.0]: https://github.com/cometsh/atex/releases/tag/v0.5.0
[0.4.0]: https://github.com/cometsh/atex/releases/tag/v0.4.0
[0.3.0]: https://github.com/cometsh/atex/releases/tag/v0.3.0
[0.2.0]: https://github.com/cometsh/atex/releases/tag/v0.2.0
+5 -3
README.md
···
- [x] DID & handle resolution service with a cache
- [x] Macro for converting a Lexicon definition into a runtime-validation schema
- [x] Codegen to convert a directory of lexicons
-
- [ ] Extended XRPC client with support for validated inputs/outputs
-
- [ ] Oauth stuff
+
- [x] Oauth stuff
+
- [x] Extended XRPC client with support for validated inputs/outputs
+
- [ ] Proper MST & CAR handling things
+
- [ ] Pre-transpiled libraries for popular lexicons
## Installation
···
```elixir
def deps do
[
-
{:atex, "~> 0.3"}
+
{:atex, "~> 0.6"}
]
end
```
+11
bump_builtin_lexicons.sh
···
+
#!/usr/bin/env bash
+
+
set -euo pipefail
+
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+
cd "$SCRIPT_DIR"
+
+
mkdir -p ./tmp
+
git clone --depth 1 --single-branch https://github.com/bluesky-social/atproto.git ./tmp/atproto
+
mix atex.lexicons ./tmp/atproto/lexicons/com/atproto/**/*.json
+
rm -rf ./tmp
+10
config/runtime.exs
···
+
import Config
+
+
config :atex, Atex.OAuth,
+
# base_url: "https://comet.sh/aaaa",
+
base_url: "http://127.0.0.1:4000/oauth",
+
is_localhost: true,
+
scopes: ~w(transition:generic),
+
private_key:
+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyIpxhuDm0i3mPkrk6UdX4Sd9Jsv6YtAmSTza+A2nArShRANCAAQLF1GLueOBZOVnKWfrcnoDOO9NSRqH2utmfGMz+Rce18MDB7Z6CwFWjEq2UFYNBI4MI5cMI0+m+UYAmj4OZm+m",
+
key_id: "awooga"
+60
examples/oauth.ex
···
+
defmodule ExampleOAuthPlug do
+
use Plug.Router
+
alias Atex.OAuth
+
alias Atex.XRPC
+
+
plug :put_secret_key_base
+
+
plug Plug.Session,
+
store: :cookie,
+
key: "atex-oauth",
+
signing_salt: "signing-salt"
+
+
plug :match
+
plug :dispatch
+
+
forward "/oauth", to: Atex.OAuth.Plug
+
+
get "/whoami" do
+
conn = fetch_session(conn)
+
+
with {:ok, client} <- XRPC.OAuthClient.from_conn(conn),
+
{:ok, response, client} <-
+
XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
+
input: %Com.Atproto.Repo.CreateRecord.Input{
+
repo: client.did,
+
collection: "app.bsky.feed.post",
+
rkey: Atex.TID.now() |> to_string(),
+
record: %App.Bsky.Feed.Post{
+
text: "Hello world from atex!",
+
createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
+
}
+
}
+
}) do
+
IO.inspect(response, label: "output")
+
+
conn
+
|> XRPC.OAuthClient.update_plug(client)
+
|> send_resp(200, response.uri)
+
else
+
:error ->
+
send_resp(conn, 401, "Unauthorized")
+
+
err ->
+
IO.inspect(err, label: "xrpc failed")
+
send_resp(conn, 500, "xrpc failed")
+
end
+
end
+
+
match _ do
+
send_resp(conn, 404, "oops")
+
end
+
+
def put_secret_key_base(conn, _) do
+
put_in(
+
conn.secret_key_base,
+
# Don't use this in production
+
"5ef1078e1617463a3eb3feb9b152e76587a75a6809e0485a125b6bb7ae468f086680771f700d77ff61dfdc8d8ee8a5c7848024a41cf5ad4b6eb3115f74ce6e46"
+
)
+
end
+
end
+86
lib/atex/config/oauth.ex
···
+
defmodule Atex.Config.OAuth do
+
@moduledoc """
+
Configuration management for `Atex.OAuth`.
+
+
Contains all the logic for fetching configuration needed for the OAuth
+
module, as well as deriving useful values from them.
+
+
## Configuration
+
+
The following structure is expected in your application config:
+
+
config :atex, Atex.OAuth,
+
base_url: "https://example.com/oauth", # Your application's base URL, including the path `Atex.OAuth` is mounted on.
+
private_key: "base64-encoded-private-key", # ES256 private key
+
key_id: "your-key-id", # Key identifier for JWTs
+
scopes: ["transition:generic", "transition:email"], # Optional additional scopes
+
extra_redirect_uris: ["https://alternative.com/callback"], # Optional additional redirect URIs
+
is_localhost: false # Set to true for local development
+
"""
+
+
@doc """
+
Returns the configured public base URL for OAuth routes.
+
"""
+
@spec base_url() :: String.t()
+
def base_url, do: Application.fetch_env!(:atex, Atex.OAuth)[:base_url]
+
+
@doc """
+
Returns the configured private key as a `JOSE.JWK`.
+
"""
+
@spec get_key() :: JOSE.JWK.t()
+
def get_key() do
+
private_key =
+
Application.fetch_env!(:atex, Atex.OAuth)[:private_key]
+
|> Base.decode64!()
+
|> JOSE.JWK.from_der()
+
+
key_id = Application.fetch_env!(:atex, Atex.OAuth)[:key_id]
+
+
%{private_key | fields: %{"kid" => key_id}}
+
end
+
+
@doc """
+
Returns the client ID based on configuration.
+
+
If `is_localhost` is set, it'll be a string handling the "http://localhost"
+
special case, with the redirect URI and scopes configured, otherwise it is a
+
string pointing to the location of the `client-metadata.json` route.
+
"""
+
@spec client_id() :: String.t()
+
def client_id() do
+
is_localhost = Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :is_localhost, false)
+
+
if is_localhost do
+
query =
+
%{redirect_uri: redirect_uri(), scope: scopes()}
+
|> URI.encode_query()
+
+
"http://localhost?#{query}"
+
else
+
"#{base_url()}/client-metadata.json"
+
end
+
end
+
+
@doc """
+
Returns the configured redirect URI.
+
"""
+
@spec redirect_uri() :: String.t()
+
def redirect_uri(), do: "#{base_url()}/callback"
+
+
@doc """
+
Returns the configured scopes joined as a space-separated string.
+
"""
+
@spec scopes() :: String.t()
+
def scopes() do
+
config_scopes = Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :scopes, [])
+
Enum.join(["atproto" | config_scopes], " ")
+
end
+
+
@doc """
+
Returns the configured extra redirect URIs.
+
"""
+
@spec extra_redirect_uris() :: [String.t()]
+
def extra_redirect_uris() do
+
Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :extra_redirect_uris, [])
+
end
+
end
-38
lib/atex/http/adapter/req.ex
···
-
defmodule Atex.HTTP.Adapter.Req do
-
@moduledoc """
-
`Req` adapter for atex.
-
"""
-
-
@behaviour Atex.HTTP.Adapter
-
-
@impl true
-
def get(url, opts) do
-
Req.get(url, opts) |> adapt()
-
end
-
-
@impl true
-
def post(url, opts) do
-
Req.post(url, opts) |> adapt()
-
end
-
-
@spec adapt({:ok, Req.Response.t()} | {:error, any()}) :: Atex.HTTP.Adapter.result()
-
defp adapt({:ok, %Req.Response{status: status} = res}) when status < 400 do
-
{:ok, to_response(res)}
-
end
-
-
defp adapt({:ok, %Req.Response{} = res}) do
-
{:error, to_response(res)}
-
end
-
-
defp adapt({:error, exception}) do
-
{:error, exception}
-
end
-
-
defp to_response(%Req.Response{} = res) do
-
%Atex.HTTP.Response{
-
body: res.body,
-
status: res.status,
-
__raw__: res
-
}
-
end
-
end
-13
lib/atex/http/adapter.ex
···
-
defmodule Atex.HTTP.Adapter do
-
@moduledoc """
-
Behaviour for defining a HTTP client adapter to be used within atex.
-
"""
-
alias Atex.HTTP.Response
-
-
@type success() :: {:ok, Response.t()}
-
@type error() :: {:error, Response.t() | term()}
-
@type result() :: success() | error()
-
-
@callback get(url :: String.t(), opts :: keyword()) :: result()
-
@callback post(url :: String.t(), opts :: keyword()) :: result()
-
end
-13
lib/atex/http/response.ex
···
-
defmodule Atex.HTTP.Response do
-
@moduledoc """
-
A generic response struct to be returned by an `Atex.HTTP.Adapter`.
-
"""
-
-
use TypedStruct
-
-
typedstruct enforce: true do
-
field :status, integer()
-
field :body, any()
-
field :__raw__, any()
-
end
-
end
-6
lib/atex/http.ex
···
-
defmodule Atex.HTTP do
-
@adapter Application.compile_env(:atex, :adapter, Atex.HTTP.Adapter.Req)
-
-
defdelegate get(url, opts), to: @adapter
-
defdelegate post(url, opts), to: @adapter
-
end
+2 -2
lib/atex/identity_resolver/did.ex
···
@spec resolve_plc(String.t()) :: resolution_result()
defp resolve_plc("did:plc:" <> _id = did) do
with {:ok, resp} when resp.status in 200..299 <-
-
Atex.HTTP.get("https://plc.directory/#{did}", []),
+
Req.get("https://plc.directory/#{did}"),
{:ok, body} <- decode_body(resp.body),
{:ok, document} <- DIDDocument.from_json(body),
:ok <- DIDDocument.validate_for_atproto(document, did) do
···
@spec resolve_web(String.t()) :: resolution_result()
defp resolve_web("did:web:" <> domain = did) do
with {:ok, resp} when resp.status in 200..299 <-
-
Atex.HTTP.get("https://#{domain}/.well-known/did.json", []),
+
Req.get("https://#{domain}/.well-known/did.json"),
{:ok, body} <- decode_body(resp.body),
{:ok, document} <- DIDDocument.from_json(body),
:ok <- DIDDocument.validate_for_atproto(document, did) do
+13
lib/atex/identity_resolver/did_document.ex
···
end)
end
+
@spec get_pds_endpoint(t()) :: String.t() | nil
+
def get_pds_endpoint(%__MODULE__{} = doc) do
+
doc.service
+
|> Enum.find(fn
+
%{id: "#atproto_pds", type: "AtprotoPersonalDataServer"} -> true
+
_ -> false
+
end)
+
|> case do
+
nil -> nil
+
pds -> pds.service_endpoint
+
end
+
end
+
defp valid_pds_endpoint?(endpoint) do
case URI.new(endpoint) do
{:ok, uri} ->
+1 -1
lib/atex/identity_resolver/handle.ex
···
@spec resolve_via_http(String.t()) :: {:ok, String.t()} | :error
defp resolve_via_http(handle) do
-
case Atex.HTTP.get("https://#{handle}/.well-known/atproto-did", []) do
+
case Req.get("https://#{handle}/.well-known/atproto-did") do
{:ok, %{body: "did:" <> _ = did}} -> {:ok, did}
_ -> :error
end
+2 -2
lib/atex/lexicon/validators/array.ex
···
@option_keys [:min_length, :max_length]
# Needs type input
-
@spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result()
-
def validate(inner_type, value, options) when is_list(value) do
+
@spec validate(term(), Peri.schema_def(), list(option())) :: Peri.validation_result()
+
def validate(value, inner_type, options) when is_list(value) do
# TODO: validate inner_type with Peri to make sure it's correct?
options
+13 -44
lib/atex/lexicon/validators/string.ex
···
defmodule Atex.Lexicon.Validators.String do
alias Atex.Lexicon.Validators
-
@type format() ::
-
:at_identifier
-
| :at_uri
-
| :cid
-
| :datetime
-
| :did
-
| :handle
-
| :nsid
-
| :tid
-
| :record_key
-
| :uri
-
| :language
-
@type option() ::
-
{:format, format()}
+
{:format, String.t()}
| {:min_length, non_neg_integer()}
| {:max_length, non_neg_integer()}
| {:min_graphemes, non_neg_integer()}
···
@record_key_re ~r"^[a-zA-Z0-9.-_:~]$"
-
# TODO: probably should go into a different module, one with general lexicon -> validator gen conversions
-
@spec format_to_atom(String.t()) :: format()
-
def format_to_atom(format) do
-
case format do
-
"at-identifier" -> :at_identifier
-
"at-uri" -> :at_uri
-
"cid" -> :cid
-
"datetime" -> :datetime
-
"did" -> :did
-
"handle" -> :handle
-
"nsid" -> :nsid
-
"tid" -> :tid
-
"record-key" -> :record_key
-
"uri" -> :uri
-
"language" -> :language
-
_ -> raise "Unknown lexicon string format `#{format}`"
-
end
-
end
-
@spec validate(term(), list(option())) :: Peri.validation_result()
def validate(value, options) when is_binary(value) do
options
···
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
-
defp validate_option(value, {:format, :at_identifier}),
+
defp validate_option(value, {:format, "at-identifier"}),
do:
Validators.boolean_validate(
Atex.DID.match?(value) or Atex.Handle.match?(value),
"should be a valid DID or handle"
)
-
defp validate_option(value, {:format, :at_uri}),
+
defp validate_option(value, {:format, "at-uri"}),
do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI")
-
defp validate_option(value, {:format, :cid}) do
+
defp validate_option(value, {:format, "cid"}) do
# TODO: is there a regex provided by the lexicon docs/somewhere?
try do
Multiformats.CID.decode(value)
+
:ok
rescue
_ -> {:error, "should be a valid CID", []}
end
end
-
defp validate_option(value, {:format, :datetime}) do
+
defp validate_option(value, {:format, "datetime"}) do
# NaiveDateTime is used over DateTime because the result isn't actually
# being used, so we don't need to include a calendar library just for this.
case NaiveDateTime.from_iso8601(value) do
···
end
end
-
defp validate_option(value, {:format, :did}),
+
defp validate_option(value, {:format, "did"}),
do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID")
-
defp validate_option(value, {:format, :handle}),
+
defp validate_option(value, {:format, "handle"}),
do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle")
-
defp validate_option(value, {:format, :nsid}),
+
defp validate_option(value, {:format, "nsid"}),
do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID")
-
defp validate_option(value, {:format, :tid}),
+
defp validate_option(value, {:format, "tid"}),
do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID")
-
defp validate_option(value, {:format, :record_key}),
+
defp validate_option(value, {:format, "record-key"}),
do:
Validators.boolean_validate(
Regex.match?(@record_key_re, value),
"should be a valid record key"
)
-
defp validate_option(value, {:format, :uri}) do
+
defp validate_option(value, {:format, "uri"}) do
case URI.new(value) do
{:ok, _} -> :ok
{:error, _} -> {:error, "should be a valid URI", []}
end
end
-
defp validate_option(value, {:format, :language}) do
+
defp validate_option(value, {:format, "language"}) do
case Cldr.LanguageTag.parse(value) do
{:ok, _} -> :ok
{:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
+5
lib/atex/lexicon/validators.ex
···
}
end
+
@spec lazy_ref(module(), atom()) :: Peri.schema()
+
def lazy_ref(module, schema_name) do
+
{:custom, {module, schema_name, []}}
+
end
+
@spec boolean_validate(boolean(), String.t(), keyword() | map()) ::
Peri.validation_result()
def boolean_validate(success?, error_message, context \\ []) do
+307 -59
lib/atex/lexicon.ex
···
defmodule Atex.Lexicon do
-
@moduledoc """
-
Provide `deflexicon` macro for defining a module with types and schemas from an entire lexicon definition.
-
-
Should it also define structs, with functions to convert from input case to snake case?
-
"""
-
alias Atex.Lexicon.Validators
defmacro __using__(_opts) do
···
end
end
+
@doc """
+
Defines a lexicon module from a JSON lexicon definition.
+
+
The `deflexicon` macro processes the provided lexicon map (typically loaded
+
from a JSON file) and generates:
+
+
- **Typespecs** for each definition, exposing a `t/0` type for the main
+
definition and named types for any additional definitions.
+
- **`Peri` schemas** via `defschema/2` for runtime validation of data.
+
- **Structs** for object and record definitions, with `@enforce_keys` ensuring
+
required fields are present.
+
- For **queries** and **procedures**, it creates structs for `params`,
+
`input`, and `output` when those sections exist in the lexicon. It also
+
generates a topโ€‘level struct that aggregates `params` and `input` (when
+
applicable); this struct is used by the XRPC client to locate the
+
appropriate output struct.
+
+
If a procedure doesn't have a schema for a JSON body specified as it's input,
+
the top-level struct will instead have a `raw_input` field, allowing for
+
miscellaneous bodies such as a binary blob.
+
+
The generated structs also implement the `JSON.Encoder` and `Jason.Encoder`
+
protocols (the latter currently present for compatibility), as well as a
+
`from_json` function which is used to validate an input map - e.g. from a JSON
+
HTTP response - and turn it into a struct.
+
+
## Example
+
+
deflexicon(%{
+
"lexicon" => 1,
+
"id" => "com.ovyerus.testing",
+
"defs" => %{
+
"main" => %{
+
"type" => "record",
+
"key" => "tid",
+
"record" => %{
+
"type" => "object",
+
"required" => ["foobar"],
+
"properties" => %{ "foobar" => %{ "type" => "string" } }
+
}
+
}
+
}
+
})
+
+
The macro expands to following code (truncated for brevity):
+
+
@type main() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
+
@type t() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
+
+
defschema(:main, %{
+
foobar: {:required, {:custom, {Atex.Lexicon.Validators.String, :validate, [[]]}}},
+
"$type": {{:literal, "com.ovyerus.testing"}, {:default, "com.ovyerus.testing"}}
+
})
+
+
@enforce_keys [:foobar]
+
defstruct foobar: nil, "$type": "com.ovyerus.testing"
+
+
def from_json(json) do
+
case apply(Com.Ovyerus.Testing, :main, [json]) do
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
+
err -> err
+
end
+
end
+
+
The generated module can be used directly with `Atex.XRPC` functions, allowing
+
typeโ€‘safe construction of requests and automatic decoding of responses.
+
"""
defmacro deflexicon(lexicon) do
# Better way to get the real map, without having to eval? (custom function to compose one from quoted?)
lexicon =
···
defs =
lexicon.defs
|> Enum.flat_map(fn {def_name, def} -> def_to_schema(lexicon.id, def_name, def) end)
-
|> Enum.map(fn {schema_key, quoted_schema, quoted_type} ->
+
|> Enum.map(fn
+
{schema_key, quoted_schema, quoted_type} -> {schema_key, quoted_schema, quoted_type, nil}
+
x -> x
+
end)
+
|> Enum.map(fn {schema_key, quoted_schema, quoted_type, quoted_struct} ->
identity_type =
-
if schema_key === :main do
+
if schema_key == :main do
quote do
@type t() :: unquote(quoted_type)
end
end
+
struct_def =
+
if schema_key == :main do
+
quoted_struct
+
else
+
nested_module_name =
+
schema_key
+
|> Recase.to_pascal()
+
|> atomise()
+
+
quote do
+
defmodule unquote({:__aliases__, [alias: false], [nested_module_name]}) do
+
unquote(quoted_struct)
+
end
+
end
+
end
+
quote do
-
@type unquote(schema_key)() :: unquote(quoted_type)
+
@type unquote(Recase.to_snake(schema_key))() :: unquote(quoted_type)
unquote(identity_type)
-
defschema unquote(schema_key), unquote(quoted_schema)
+
defschema unquote(Recase.to_snake(schema_key)), unquote(quoted_schema)
+
+
unquote(struct_def)
end
end)
quote do
-
def id, do: unquote(Atex.NSID.to_atom(lexicon.id))
+
def id, do: unquote(lexicon.id)
unquote_splicing(defs)
end
end
+
+
# - [ ] `t()` type should be the struct in it. (add to non-main structs too?)
@spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) ::
-
list({key :: atom(), quoted_schema :: term(), quoted_type :: term()})
+
list(
+
{
+
key :: atom(),
+
quoted_schema :: term(),
+
quoted_type :: term()
+
}
+
| {
+
key :: atom(),
+
quoted_schema :: term(),
+
quoted_type :: term(),
+
quoted_struct :: term()
+
}
+
)
defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do
# TODO: record rkey format validator
+
type_name = Atex.NSID.canonical_name(nsid, to_string(def_name))
+
+
record =
+
put_in(record, [:properties, :"$type"], %{
+
type: "string",
+
const: type_name,
+
default: type_name
+
})
+
def_to_schema(nsid, def_name, record)
end
-
# TODO: need to spit out an extra 'branded' type with `$type` field, for use in union refs.
+
# TODO: add struct to types
defp def_to_schema(
nsid,
def_name,
···
required = Map.get(def, :required, [])
nullable = Map.get(def, :nullable, [])
-
properties
-
|> Enum.map(fn {key, field} ->
-
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
-
is_nullable = key in nullable
-
is_required = key in required
+
{quoted_schemas, quoted_types} =
+
properties
+
|> Enum.map(fn {key, field} ->
+
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
+
string_key = to_string(key)
+
is_nullable = string_key in nullable
+
is_required = string_key in required
+
+
quoted_schema =
+
quoted_schema
+
|> then(
+
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
+
)
+
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
+
|> then(&{key, &1})
-
quoted_schema =
-
quoted_schema
-
|> then(
-
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
-
)
-
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
-
|> then(&{key, &1})
+
key_type = if is_required, do: :required, else: :optional
-
key_type = if is_required, do: :required, else: :optional
+
quoted_type =
+
quoted_type
+
|> then(
+
&if is_nullable do
+
{:|, [], [&1, nil]}
+
else
+
&1
+
end
+
)
+
|> then(&{{key_type, [], [key]}, &1})
-
quoted_type =
-
quoted_type
-
|> then(
-
&if is_nullable do
-
{:|, [], [&1, nil]}
-
else
-
&1
+
{quoted_schema, quoted_type}
+
end)
+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
+
{[quoted_schema | schemas], [quoted_type | types]}
+
end)
+
+
struct_keys =
+
properties
+
|> Enum.filter(fn {key, _} -> key !== :"$type" end)
+
|> Enum.map(fn
+
{key, %{default: default}} -> {key, default}
+
{key, _field} -> {key, nil}
+
end)
+
|> then(&(&1 ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}]))
+
+
enforced_keys =
+
properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required && &1 != :"$type"))
+
+
optional_if_nil_keys =
+
properties
+
|> Map.keys()
+
|> Enum.filter(fn key ->
+
key = to_string(key)
+
# TODO: what if it is nullable but not required?
+
key not in required && key not in nullable && key != "$type"
+
end)
+
+
schema_module = Atex.NSID.to_atom(nsid)
+
+
quoted_struct =
+
quote do
+
@enforce_keys unquote(enforced_keys)
+
defstruct unquote(struct_keys)
+
+
def from_json(json) do
+
case apply(unquote(schema_module), unquote(atomise(def_name)), [json]) do
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
+
err -> err
end
-
)
-
|> then(&{{key_type, [], [key]}, &1})
+
end
-
{quoted_schema, quoted_type}
-
end)
-
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
-
{[quoted_schema | schemas], [quoted_type | types]}
-
end)
-
|> then(fn {quoted_schemas, quoted_types} ->
-
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}}]
-
end)
+
defimpl JSON.Encoder do
+
@optional_if_nil_keys unquote(optional_if_nil_keys)
+
+
def encode(value, encoder) do
+
value
+
|> Map.from_struct()
+
|> Enum.reject(fn {k, v} -> k in @optional_if_nil_keys && v == nil end)
+
|> Enum.into(%{})
+
|> Jason.Encoder.encode(encoder)
+
end
+
end
+
+
defimpl Jason.Encoder do
+
@optional_if_nil_keys unquote(optional_if_nil_keys)
+
+
def encode(value, options) do
+
value
+
|> Map.from_struct()
+
|> Enum.reject(fn {k, v} -> k in @optional_if_nil_keys && v == nil end)
+
|> Enum.into(%{})
+
|> Jason.Encode.map(options)
+
end
+
end
+
end
+
+
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}, quoted_struct}]
end
# TODO: validating errors?
···
schema
end
-
[params, output]
+
# Root struct containing `params`
+
main =
+
if params do
+
{
+
:main,
+
nil,
+
quote do
+
%__MODULE__{params: params()}
+
end,
+
quote do
+
@enforce_keys [:params]
+
defstruct params: nil
+
end
+
}
+
else
+
{
+
:main,
+
nil,
+
quote do
+
%__MODULE__{}
+
end,
+
quote do
+
defstruct []
+
end
+
}
+
end
+
+
[main, params, output]
|> Enum.reject(&is_nil/1)
end
···
schema
end
-
[params, output, input]
+
# Root struct containing `input`, `raw_input`, and `params`
+
main =
+
{
+
:main,
+
nil,
+
cond do
+
params && input ->
+
quote do
+
%__MODULE__{input: input(), params: params()}
+
end
+
+
input ->
+
quote do
+
%__MODULE__{input: input()}
+
end
+
+
params ->
+
quote do
+
%__MODULE__{raw_input: any(), params: params()}
+
end
+
+
true ->
+
quote do
+
%__MODULE__{raw_input: any()}
+
end
+
end,
+
cond do
+
params && input ->
+
quote do
+
defstruct input: nil, params: nil
+
end
+
+
input ->
+
quote do
+
defstruct input: nil
+
end
+
+
params ->
+
quote do
+
defstruct raw_input: nil, params: nil
+
end
+
+
true ->
+
quote do
+
defstruct raw_input: nil
+
end
+
end
+
}
+
+
[main, params, output, input]
|> Enum.reject(&is_nil/1)
end
···
:minGraphemes
])
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
-
|> then(&{:custom, {Validators.String, :validate, [&1]}})
+
|> Validators.string()
|> maybe_default(field)
end
|> then(
···
field
|> Map.take([:maximum, :minimum])
|> Keyword.new()
-
|> then(&{:custom, {Validators.Integer, [&1]}})
+
|> Validators.integer()
|> maybe_default(field)
end
|> then(
···
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
|> then(&Validators.array(inner_schema, &1))
|> then(&Macro.escape/1)
+
# TODO: we should be able to unquote this now...
# Can't unquote the inner_schema beforehand as that would risk evaluating `get_schema`s which don't exist yet.
# There's probably a better way to do this lol.
|> then(fn {:custom, {:{}, c, [Validators.Array, :validate, [quoted_inner_schema | args]]}} ->
···
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
|> Atex.NSID.to_atom_with_fragment()
-
{quote do
-
unquote(nsid).get_schema(unquote(fragment))
-
end,
-
quote do
-
unquote(nsid).unquote(fragment)()
-
end}
+
fragment = Recase.to_snake(fragment)
+
+
{
+
Macro.escape(Validators.lazy_ref(nsid, fragment)),
+
quote do
+
unquote(nsid).unquote(fragment)()
+
end
+
}
end
defp field_to_schema(%{type: "union", refs: refs}, nsid) do
···
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
|> Atex.NSID.to_atom_with_fragment()
-
{quote do
-
unquote(nsid).get_schema(unquote(fragment))
-
end,
-
quote do
-
unquote(nsid).unquote(fragment)()
-
end}
+
fragment = Recase.to_snake(fragment)
+
+
{
+
Macro.escape(Validators.lazy_ref(nsid, fragment)),
+
quote do
+
unquote(nsid).unquote(fragment)()
+
end
+
}
end)
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
{[quoted_schema | schemas], [quoted_type | types]}
+9
lib/atex/nsid.ex
···
possible_fragment
end
end
+
+
@spec canonical_name(String.t(), String.t()) :: String.t()
+
def canonical_name(nsid, fragment) do
+
if fragment == "main" do
+
nsid
+
else
+
"#{nsid}##{fragment}"
+
end
+
end
end
+165
lib/atex/oauth/plug.ex
···
+
defmodule Atex.OAuth.Plug do
+
@moduledoc """
+
Plug router for handling AT Protocol's OAuth flow.
+
+
This module provides three endpoints:
+
+
- `GET /login?handle=<handle>` - Initiates the OAuth authorization flow for
+
a given handle
+
- `GET /callback` - Handles the OAuth callback after user authorization
+
- `GET /client-metadata.json` - Serves the OAuth client metadata
+
+
## Usage
+
+
This module requires `Plug.Session` to be in your pipeline, as well as
+
`secret_key_base` to have been set on your connections. Ideally it should be
+
routed to via `Plug.Router.forward/2`, under a route like "/oauth".
+
+
## Example
+
+
Example implementation showing how to set up the OAuth plug with proper
+
session handling:
+
+
defmodule ExampleOAuthPlug do
+
use Plug.Router
+
+
plug :put_secret_key_base
+
+
plug Plug.Session,
+
store: :cookie,
+
key: "atex-oauth",
+
signing_salt: "signing-salt"
+
+
plug :match
+
plug :dispatch
+
+
forward "/oauth", to: Atex.OAuth.Plug
+
+
def put_secret_key_base(conn, _) do
+
put_in(
+
conn.secret_key_base,
+
"very long key base with at least 64 bytes"
+
)
+
end
+
end
+
+
## Session Storage
+
+
After successful authentication, the plug stores these in the session:
+
+
- `:tokens` - The access token response containing access_token,
+
refresh_token, did, and expires_at
+
- `:dpop_nonce` -
+
- `:dpop_key` - The DPoP JWK for generating DPoP proofs
+
"""
+
require Logger
+
use Plug.Router
+
require Plug.Router
+
alias Atex.OAuth
+
alias Atex.{IdentityResolver, IdentityResolver.DIDDocument}
+
+
@oauth_cookie_opts [path: "/", http_only: true, secure: true, same_site: "lax", max_age: 600]
+
+
plug :match
+
plug :dispatch
+
+
get "/login" do
+
conn = fetch_query_params(conn)
+
handle = conn.query_params["handle"]
+
+
if !handle do
+
send_resp(conn, 400, "Need `handle` query parameter")
+
else
+
case IdentityResolver.resolve(handle) do
+
{:ok, identity} ->
+
pds = DIDDocument.get_pds_endpoint(identity.document)
+
{:ok, authz_server} = OAuth.get_authorization_server(pds)
+
{:ok, authz_metadata} = OAuth.get_authorization_server_metadata(authz_server)
+
state = OAuth.create_nonce()
+
code_verifier = OAuth.create_nonce()
+
+
case OAuth.create_authorization_url(
+
authz_metadata,
+
state,
+
code_verifier,
+
handle
+
) do
+
{:ok, authz_url} ->
+
conn
+
|> put_resp_cookie("state", state, @oauth_cookie_opts)
+
|> put_resp_cookie("code_verifier", code_verifier, @oauth_cookie_opts)
+
|> put_resp_cookie("issuer", authz_metadata.issuer, @oauth_cookie_opts)
+
|> put_resp_header("location", authz_url)
+
|> send_resp(307, "")
+
+
err ->
+
Logger.error("failed to reate authorization url, #{inspect(err)}")
+
send_resp(conn, 500, "Internal server error")
+
end
+
+
{:error, err} ->
+
Logger.error("Failed to resolve handle, #{inspect(err)}")
+
send_resp(conn, 400, "Invalid handle")
+
end
+
end
+
end
+
+
get "/client-metadata.json" do
+
conn
+
|> put_resp_content_type("application/json")
+
|> send_resp(200, JSON.encode_to_iodata!(OAuth.create_client_metadata()))
+
end
+
+
get "/callback" do
+
conn = conn |> fetch_query_params() |> fetch_session()
+
cookies = get_cookies(conn)
+
stored_state = cookies["state"]
+
stored_code_verifier = cookies["code_verifier"]
+
stored_issuer = cookies["issuer"]
+
+
code = conn.query_params["code"]
+
state = conn.query_params["state"]
+
+
if !stored_state || !stored_code_verifier || !stored_issuer || (!code || !state) ||
+
stored_state != state do
+
send_resp(conn, 400, "Invalid request")
+
else
+
with {:ok, authz_metadata} <- OAuth.get_authorization_server_metadata(stored_issuer),
+
dpop_key <- JOSE.JWK.generate_key({:ec, "P-256"}),
+
{:ok, tokens, nonce} <-
+
OAuth.validate_authorization_code(
+
authz_metadata,
+
dpop_key,
+
code,
+
stored_code_verifier
+
),
+
{:ok, identity} <- IdentityResolver.resolve(tokens.did),
+
# Make sure pds' issuer matches the stored one (just in case)
+
pds <- DIDDocument.get_pds_endpoint(identity.document),
+
{:ok, authz_server} <- OAuth.get_authorization_server(pds),
+
true <- authz_server == stored_issuer do
+
conn
+
|> delete_resp_cookie("state", @oauth_cookie_opts)
+
|> delete_resp_cookie("code_verifier", @oauth_cookie_opts)
+
|> delete_resp_cookie("issuer", @oauth_cookie_opts)
+
|> put_session(:atex_oauth, %{
+
access_token: tokens.access_token,
+
refresh_token: tokens.refresh_token,
+
did: tokens.did,
+
pds: pds,
+
expires_at: tokens.expires_at,
+
dpop_nonce: nonce,
+
dpop_key: dpop_key
+
})
+
|> send_resp(200, "success!! hello #{tokens.did}")
+
else
+
false ->
+
send_resp(conn, 400, "OAuth issuer does not match your PDS' authorization server")
+
+
err ->
+
Logger.error("failed to validate oauth callback: #{inspect(err)}")
+
send_resp(conn, 500, "Internal server error")
+
end
+
end
+
end
+
end
+531
lib/atex/oauth.ex
···
+
defmodule Atex.OAuth do
+
@moduledoc """
+
OAuth 2.0 implementation for AT Protocol authentication.
+
+
This module provides utilities for implementing OAuth flows compliant with the
+
AT Protocol specification. It includes support for:
+
+
- Pushed Authorization Requests (PAR)
+
- DPoP (Demonstration of Proof of Possession) tokens
+
- JWT client assertions
+
- PKCE (Proof Key for Code Exchange)
+
- Token refresh
+
- Handle to PDS resolution
+
+
## Configuration
+
+
See `Atex.Config.OAuth` module for configuration documentation.
+
+
## Usage Example
+
+
iex> pds = "https://bsky.social"
+
iex> login_hint = "example.com"
+
iex> {:ok, authz_server} = Atex.OAuth.get_authorization_server(pds)
+
iex> {:ok, authz_metadata} = Atex.OAuth.get_authorization_server_metadata(authz_server)
+
iex> state = Atex.OAuth.create_nonce()
+
iex> code_verifier = Atex.OAuth.create_nonce()
+
iex> {:ok, auth_url} = Atex.OAuth.create_authorization_url(
+
authz_metadata,
+
state,
+
code_verifier,
+
login_hint
+
)
+
"""
+
+
@type authorization_metadata() :: %{
+
issuer: String.t(),
+
par_endpoint: String.t(),
+
token_endpoint: String.t(),
+
authorization_endpoint: String.t()
+
}
+
+
@type tokens() :: %{
+
access_token: String.t(),
+
refresh_token: String.t(),
+
did: String.t(),
+
expires_at: NaiveDateTime.t()
+
}
+
+
alias Atex.Config.OAuth, as: Config
+
+
@doc """
+
Get a map cnotaining the client metadata information needed for an
+
authorization server to validate this client.
+
"""
+
@spec create_client_metadata() :: map()
+
def create_client_metadata() do
+
key = Config.get_key()
+
{_, jwk} = key |> JOSE.JWK.to_public_map()
+
jwk = Map.merge(jwk, %{use: "sig", kid: key.fields["kid"]})
+
+
%{
+
client_id: Config.client_id(),
+
redirect_uris: [Config.redirect_uri() | Config.extra_redirect_uris()],
+
application_type: "web",
+
grant_types: ["authorization_code", "refresh_token"],
+
scope: Config.scopes(),
+
response_type: ["code"],
+
token_endpoint_auth_method: "private_key_jwt",
+
token_endpoint_auth_signing_alg: "ES256",
+
dpop_bound_access_tokens: true,
+
jwks: %{keys: [jwk]}
+
}
+
end
+
+
@doc """
+
Retrieves the configured JWT private key for signing client assertions.
+
+
Loads the private key from configuration, decodes the base64-encoded DER data,
+
and creates a JOSE JWK structure with the key ID field set.
+
+
## Returns
+
+
A `JOSE.JWK` struct containing the private key and key identifier.
+
+
## Raises
+
+
* `Application.Env.Error` if the private_key or key_id configuration is missing
+
+
## Examples
+
+
key = OAuth.get_key()
+
key = OAuth.get_key()
+
"""
+
@spec get_key() :: JOSE.JWK.t()
+
def get_key(), do: Config.get_key()
+
+
@doc false
+
@spec random_b64(integer()) :: String.t()
+
def random_b64(length) do
+
:crypto.strong_rand_bytes(length)
+
|> Base.url_encode64(padding: false)
+
end
+
+
@doc false
+
@spec create_nonce() :: String.t()
+
def create_nonce(), do: random_b64(32)
+
+
@doc """
+
Create an OAuth authorization URL for a PDS.
+
+
Submits a PAR request to the authorization server and constructs the
+
authorization URL with the returned request URI. Supports PKCE, DPoP, and
+
client assertions as required by the AT Protocol.
+
+
## Parameters
+
+
- `authz_metadata` - Authorization server metadata containing endpoints, fetched from `get_authorization_server_metadata/1`
+
- `state` - Random token for session validation
+
- `code_verifier` - PKCE code verifier
+
- `login_hint` - User identifier (handle or DID) for pre-filled login
+
+
## Returns
+
+
- `{:ok, authorization_url}` - Successfully created authorization URL
+
- `{:ok, :invalid_par_response}` - Server respondend incorrectly to the request
+
- `{:error, reason}` - Error creating authorization URL
+
"""
+
@spec create_authorization_url(
+
authorization_metadata(),
+
String.t(),
+
String.t(),
+
String.t()
+
) :: {:ok, String.t()} | {:error, any()}
+
def create_authorization_url(
+
authz_metadata,
+
state,
+
code_verifier,
+
login_hint
+
) do
+
code_challenge = :crypto.hash(:sha256, code_verifier) |> Base.url_encode64(padding: false)
+
key = get_key()
+
+
client_assertion =
+
create_client_assertion(key, Config.client_id(), authz_metadata.issuer)
+
+
body =
+
%{
+
response_type: "code",
+
client_id: Config.client_id(),
+
redirect_uri: Config.redirect_uri(),
+
state: state,
+
code_challenge_method: "S256",
+
code_challenge: code_challenge,
+
scope: Config.scopes(),
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+
client_assertion: client_assertion,
+
login_hint: login_hint
+
}
+
+
case Req.post(authz_metadata.par_endpoint, form: body) do
+
{:ok, %{body: %{"request_uri" => request_uri}}} ->
+
query =
+
%{client_id: Config.client_id(), request_uri: request_uri}
+
|> URI.encode_query()
+
+
{:ok, "#{authz_metadata.authorization_endpoint}?#{query}"}
+
+
{:ok, _} ->
+
{:error, :invalid_par_response}
+
+
err ->
+
err
+
end
+
end
+
+
@doc """
+
Exchange an OAuth authorization code for a set of access and refresh tokens.
+
+
Validates the authorization code by submitting it to the token endpoint along with
+
the PKCE code verifier and client assertion. Returns access tokens for making authenticated
+
requests to the relevant user's PDS.
+
+
## Parameters
+
+
- `authz_metadata` - Authorization server metadata containing token endpoint
+
- `dpop_key` - JWK for DPoP token generation
+
- `code` - Authorization code from OAuth callback
+
- `code_verifier` - PKCE code verifier from authorization flow
+
+
## Returns
+
+
- `{:ok, tokens, nonce}` - Successfully obtained tokens with returned DPoP nonce
+
- `{:error, reason}` - Error exchanging code for tokens
+
"""
+
@spec validate_authorization_code(
+
authorization_metadata(),
+
JOSE.JWK.t(),
+
String.t(),
+
String.t()
+
) :: {:ok, tokens(), String.t()} | {:error, any()}
+
def validate_authorization_code(
+
authz_metadata,
+
dpop_key,
+
code,
+
code_verifier
+
) do
+
key = get_key()
+
+
client_assertion =
+
create_client_assertion(key, Config.client_id(), authz_metadata.issuer)
+
+
body =
+
%{
+
grant_type: "authorization_code",
+
client_id: Config.client_id(),
+
redirect_uri: Config.redirect_uri(),
+
code: code,
+
code_verifier: code_verifier,
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+
client_assertion: client_assertion
+
}
+
+
Req.new(method: :post, url: authz_metadata.token_endpoint, form: body)
+
|> send_oauth_dpop_request(dpop_key)
+
|> case do
+
{:ok,
+
%{
+
"access_token" => access_token,
+
"refresh_token" => refresh_token,
+
"expires_in" => expires_in,
+
"sub" => did
+
}, nonce} ->
+
expires_at = NaiveDateTime.utc_now() |> NaiveDateTime.add(expires_in, :second)
+
+
{:ok,
+
%{
+
access_token: access_token,
+
refresh_token: refresh_token,
+
did: did,
+
expires_at: expires_at
+
}, nonce}
+
+
err ->
+
err
+
end
+
end
+
+
def refresh_token(refresh_token, dpop_key, issuer, token_endpoint) do
+
key = get_key()
+
+
client_assertion =
+
create_client_assertion(key, Config.client_id(), issuer)
+
+
body = %{
+
grant_type: "refresh_token",
+
refresh_token: refresh_token,
+
client_id: Config.client_id(),
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
+
client_assertion: client_assertion
+
}
+
+
Req.new(method: :post, url: token_endpoint, form: body)
+
|> send_oauth_dpop_request(dpop_key)
+
|> case do
+
{:ok,
+
%{
+
"access_token" => access_token,
+
"refresh_token" => refresh_token,
+
"expires_in" => expires_in,
+
"sub" => did
+
}, nonce} ->
+
expires_at = NaiveDateTime.utc_now() |> NaiveDateTime.add(expires_in, :second)
+
+
{:ok,
+
%{
+
access_token: access_token,
+
refresh_token: refresh_token,
+
did: did,
+
expires_at: expires_at
+
}, nonce}
+
+
err ->
+
err
+
end
+
end
+
+
@doc """
+
Fetch the authorization server for a given Personal Data Server (PDS).
+
+
Makes a request to the PDS's `.well-known/oauth-protected-resource` endpoint
+
to discover the associated authorization server that should be used for the
+
OAuth flow.
+
+
## Parameters
+
+
- `pds_host` - Base URL of the PDS (e.g., "https://bsky.social")
+
+
## Returns
+
+
- `{:ok, authorization_server}` - Successfully discovered authorization
+
server URL
+
- `{:error, :invalid_metadata}` - Server returned invalid metadata
+
- `{:error, reason}` - Error discovering authorization server
+
"""
+
@spec get_authorization_server(String.t()) :: {:ok, String.t()} | {:error, any()}
+
def get_authorization_server(pds_host) do
+
"#{pds_host}/.well-known/oauth-protected-resource"
+
|> Req.get()
+
|> case do
+
# TODO: what to do when multiple authorization servers?
+
{:ok, %{body: %{"authorization_servers" => [authz_server | _]}}} -> {:ok, authz_server}
+
{:ok, _} -> {:error, :invalid_metadata}
+
err -> err
+
end
+
end
+
+
@doc """
+
Fetch the metadata for an OAuth authorization server.
+
+
Retrieves the metadata from the authorization server's
+
`.well-known/oauth-authorization-server` endpoint, providing endpoint URLs
+
required for the OAuth flow.
+
+
## Parameters
+
+
- `issuer` - Authorization server issuer URL
+
+
## Returns
+
+
- `{:ok, metadata}` - Successfully retrieved authorization server metadata
+
- `{:error, :invalid_metadata}` - Server returned invalid metadata
+
- `{:error, :invalid_issuer}` - Issuer mismatch in metadata
+
- `{:error, any()}` - Other error fetching metadata
+
"""
+
@spec get_authorization_server_metadata(String.t()) ::
+
{:ok, authorization_metadata()} | {:error, any()}
+
def get_authorization_server_metadata(issuer) do
+
"#{issuer}/.well-known/oauth-authorization-server"
+
|> Req.get()
+
|> case do
+
{:ok,
+
%{
+
body: %{
+
"issuer" => metadata_issuer,
+
"pushed_authorization_request_endpoint" => par_endpoint,
+
"token_endpoint" => token_endpoint,
+
"authorization_endpoint" => authorization_endpoint
+
}
+
}} ->
+
if issuer != metadata_issuer do
+
{:error, :invaild_issuer}
+
else
+
{:ok,
+
%{
+
issuer: metadata_issuer,
+
par_endpoint: par_endpoint,
+
token_endpoint: token_endpoint,
+
authorization_endpoint: authorization_endpoint
+
}}
+
end
+
+
{:ok, _} ->
+
{:error, :invalid_metadata}
+
+
err ->
+
err
+
end
+
end
+
+
@spec send_oauth_dpop_request(Req.Request.t(), JOSE.JWK.t(), String.t() | nil) ::
+
{:ok, map(), String.t()} | {:error, any(), String.t()}
+
def send_oauth_dpop_request(request, dpop_key, nonce \\ nil) do
+
dpop_token = create_dpop_token(dpop_key, request, nonce)
+
+
request
+
|> Req.Request.put_header("dpop", dpop_token)
+
|> Req.request()
+
|> case do
+
{:ok, resp} ->
+
dpop_nonce =
+
case resp.headers["dpop-nonce"] do
+
[new_nonce | _] -> new_nonce
+
_ -> nonce
+
end
+
+
cond do
+
resp.status == 200 ->
+
{:ok, resp.body, dpop_nonce}
+
+
resp.body["error"] === "use_dpop_nonce" ->
+
dpop_token = create_dpop_token(dpop_key, request, dpop_nonce)
+
+
request
+
|> Req.Request.put_header("dpop", dpop_token)
+
|> Req.request()
+
|> case do
+
{:ok, %{status: 200, body: body}} ->
+
{:ok, body, dpop_nonce}
+
+
{:ok, %{body: %{"error" => error, "error_description" => error_description}}} ->
+
{:error, {:oauth_error, error, error_description}, dpop_nonce}
+
+
{:ok, _} ->
+
{:error, :unexpected_response, dpop_nonce}
+
+
{:error, err} ->
+
{:error, err, dpop_nonce}
+
end
+
+
true ->
+
{:error, {:oauth_error, resp.body["error"], resp.body["error_description"]},
+
dpop_nonce}
+
end
+
+
{:error, err} ->
+
{:error, err, nonce}
+
end
+
end
+
+
@spec request_protected_dpop_resource(
+
Req.Request.t(),
+
String.t(),
+
String.t(),
+
JOSE.JWK.t(),
+
String.t() | nil
+
) :: {:ok, Req.Response.t(), String.t() | nil} | {:error, any()}
+
def request_protected_dpop_resource(request, issuer, access_token, dpop_key, nonce \\ nil) do
+
access_token_hash = :crypto.hash(:sha256, access_token) |> Base.url_encode64(padding: false)
+
# access_token_hash = Base.url_encode64(access_token, padding: false)
+
+
dpop_token =
+
create_dpop_token(dpop_key, request, nonce, %{iss: issuer, ath: access_token_hash})
+
+
request
+
|> Req.Request.put_header("dpop", dpop_token)
+
|> Req.request()
+
|> case do
+
{:ok, resp} ->
+
dpop_nonce =
+
case resp.headers["dpop-nonce"] do
+
[new_nonce | _] -> new_nonce
+
_ -> nonce
+
end
+
+
www_authenticate = Req.Response.get_header(resp, "www-authenticate")
+
+
www_dpop_problem =
+
www_authenticate != [] && String.starts_with?(Enum.at(www_authenticate, 0), "DPoP")
+
+
if resp.status != 401 || !www_dpop_problem do
+
{:ok, resp, dpop_nonce}
+
else
+
dpop_token =
+
create_dpop_token(dpop_key, request, dpop_nonce, %{
+
iss: issuer,
+
ath: access_token_hash
+
})
+
+
request
+
|> Req.Request.put_header("dpop", dpop_token)
+
|> Req.request()
+
|> case do
+
{:ok, resp} ->
+
dpop_nonce =
+
case resp.headers["dpop-nonce"] do
+
[new_nonce | _] -> new_nonce
+
_ -> dpop_nonce
+
end
+
+
{:ok, resp, dpop_nonce}
+
+
err ->
+
err
+
end
+
end
+
+
err ->
+
err
+
end
+
end
+
+
@spec create_client_assertion(JOSE.JWK.t(), String.t(), String.t()) :: String.t()
+
def create_client_assertion(jwk, client_id, issuer) do
+
iat = System.os_time(:second)
+
jti = random_b64(20)
+
jws = %{"alg" => "ES256", "kid" => jwk.fields["kid"]}
+
+
jwt = %{
+
iss: client_id,
+
sub: client_id,
+
aud: issuer,
+
jti: jti,
+
iat: iat,
+
exp: iat + 60
+
}
+
+
JOSE.JWT.sign(jwk, jws, jwt)
+
|> JOSE.JWS.compact()
+
|> elem(1)
+
end
+
+
@spec create_dpop_token(JOSE.JWK.t(), Req.Request.t(), any(), map()) :: String.t()
+
def create_dpop_token(jwk, request, nonce \\ nil, attrs \\ %{}) do
+
iat = System.os_time(:second)
+
jti = random_b64(20)
+
{_, public_jwk} = JOSE.JWK.to_public_map(jwk)
+
jws = %{"alg" => "ES256", "typ" => "dpop+jwt", "jwk" => public_jwk}
+
[request_url | _] = request.url |> to_string() |> String.split("?")
+
+
jwt =
+
Map.merge(attrs, %{
+
jti: jti,
+
htm: atom_to_upcase_string(request.method),
+
htu: request_url,
+
iat: iat
+
})
+
|> then(fn m ->
+
if nonce, do: Map.put(m, :nonce, nonce), else: m
+
end)
+
+
JOSE.JWT.sign(jwk, jws, jwt)
+
|> JOSE.JWS.compact()
+
|> elem(1)
+
end
+
+
@doc false
+
@spec atom_to_upcase_string(atom()) :: String.t()
+
def atom_to_upcase_string(atom) do
+
atom |> to_string() |> String.upcase()
+
end
+
end
+18 -74
lib/atex/xrpc/client.ex
···
defmodule Atex.XRPC.Client do
@moduledoc """
-
Struct to store client information for ATProto XRPC.
-
"""
+
Behaviour that defines the interface for XRPC clients.
-
alias Atex.{XRPC, HTTP}
-
use TypedStruct
+
This behaviour allows different types of clients (login-based, OAuth-based, etc.)
+
to implement authentication and request handling while maintaining a consistent interface.
-
typedstruct do
-
field :endpoint, String.t(), enforce: true
-
field :access_token, String.t() | nil
-
field :refresh_token, String.t() | nil
-
end
-
-
@doc """
-
Create a new `Atex.XRPC.Client` from an endpoint, and optionally an
-
access/refresh token.
-
-
Endpoint should be the base URL of a PDS, or an AppView in the case of
-
unauthenticated requests (like Bluesky's public API), e.g.
-
`https://bsky.social`.
+
Implementations must handle token refresh internally when requests fail due to
+
expired tokens, and return both the result and potentially updated client state.
"""
-
@spec new(String.t()) :: t()
-
@spec new(String.t(), String.t() | nil) :: t()
-
@spec new(String.t(), String.t() | nil, String.t() | nil) :: t()
-
def new(endpoint, access_token \\ nil, refresh_token \\ nil) do
-
%__MODULE__{endpoint: endpoint, access_token: access_token, refresh_token: refresh_token}
-
end
+
+
@type client :: struct()
+
@type request_opts :: keyword()
+
@type request_result :: {:ok, Req.Response.t(), client()} | {:error, any(), client()}
@doc """
-
Create a new `Atex.XRPC.Client` by logging in with an `identifier` and
-
`password` to fetch an initial pair of access & refresh tokens.
+
Perform an authenticated HTTP GET request on an XRPC resource.
-
Uses `com.atproto.server.createSession` under the hood, so `identifier` can be
-
either a handle or a DID.
-
-
## Examples
-
-
iex> Atex.XRPC.Client.login("https://bsky.social", "example.com", "password123")
-
{:ok, %Atex.XRPC.Client{...}}
+
Implementations should handle token refresh if the request fails due to
+
expired authentication, returning both the response and the (potentially updated) client.
"""
-
@spec login(String.t(), String.t(), String.t()) :: {:ok, t()} | HTTP.Adapter.error()
-
@spec login(String.t(), String.t(), String.t(), String.t() | nil) ::
-
{:ok, t()} | HTTP.Adapter.error()
-
def login(endpoint, identifier, password, auth_factor_token \\ nil) do
-
json =
-
%{identifier: identifier, password: password}
-
|> then(
-
&if auth_factor_token do
-
Map.merge(&1, %{authFactorToken: auth_factor_token})
-
else
-
&1
-
end
-
)
-
-
response = XRPC.unauthed_post(endpoint, "com.atproto.server.createSession", json: json)
-
-
case response do
-
{:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
-
{:ok, new(endpoint, access_token, refresh_token)}
-
-
err ->
-
err
-
end
-
end
+
@callback get(client(), String.t(), request_opts()) :: request_result()
@doc """
-
Request a new `refresh_token` for the given client.
+
Perform an authenticated HTTP POST request on an XRPC resource.
+
+
Implementations should handle token refresh if the request fails due to
+
expired authentication, returning both the response and the (potentially updated) client.
"""
-
@spec refresh(t()) :: {:ok, t()} | HTTP.Adapter.error()
-
def refresh(%__MODULE__{endpoint: endpoint, refresh_token: refresh_token} = client) do
-
response =
-
XRPC.unauthed_post(
-
endpoint,
-
"com.atproto.server.refreshSession",
-
XRPC.put_auth([], refresh_token)
-
)
-
-
case response do
-
{:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
-
%{client | access_token: access_token, refresh_token: refresh_token}
-
-
err ->
-
err
-
end
-
end
+
@callback post(client(), String.t(), request_opts()) :: request_result()
end
+148
lib/atex/xrpc/login_client.ex
···
+
defmodule Atex.XRPC.LoginClient do
+
alias Atex.XRPC
+
use TypedStruct
+
+
@behaviour Atex.XRPC.Client
+
+
typedstruct do
+
field :endpoint, String.t(), enforce: true
+
field :access_token, String.t() | nil
+
field :refresh_token, String.t() | nil
+
end
+
+
@doc """
+
Create a new `Atex.XRPC.LoginClient` from an endpoint, and optionally an
+
existing access/refresh token.
+
+
Endpoint should be the base URL of a PDS, or an AppView in the case of
+
unauthenticated requests (like Bluesky's public API), e.g.
+
`https://bsky.social`.
+
"""
+
@spec new(String.t(), String.t() | nil, String.t() | nil) :: t()
+
def new(endpoint, access_token \\ nil, refresh_token \\ nil) do
+
%__MODULE__{endpoint: endpoint, access_token: access_token, refresh_token: refresh_token}
+
end
+
+
@doc """
+
Create a new `Atex.XRPC.LoginClient` by logging in with an `identifier` and
+
`password` to fetch an initial pair of access & refresh tokens.
+
+
Also supports providing a MFA token in the situation that is required.
+
+
Uses `com.atproto.server.createSession` under the hood, so `identifier` can be
+
either a handle or a DID.
+
+
## Examples
+
+
iex> Atex.XRPC.LoginClient.login("https://bsky.social", "example.com", "password123")
+
{:ok, %Atex.XRPC.LoginClient{...}}
+
"""
+
@spec login(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, any()}
+
@spec login(String.t(), String.t(), String.t(), String.t() | nil) ::
+
{:ok, t()} | {:error, any()}
+
def login(endpoint, identifier, password, auth_factor_token \\ nil) do
+
json =
+
%{identifier: identifier, password: password}
+
|> then(
+
&if auth_factor_token do
+
Map.merge(&1, %{authFactorToken: auth_factor_token})
+
else
+
&1
+
end
+
)
+
+
response = XRPC.unauthed_post(endpoint, "com.atproto.server.createSession", json: json)
+
+
case response do
+
{:ok, %{body: %{"accessJwt" => access_token, "refreshJwt" => refresh_token}}} ->
+
{:ok, new(endpoint, access_token, refresh_token)}
+
+
err ->
+
err
+
end
+
end
+
+
@doc """
+
Request a new `refresh_token` for the given client.
+
"""
+
@spec refresh(t()) :: {:ok, t()} | {:error, any()}
+
def refresh(%__MODULE__{endpoint: endpoint, refresh_token: refresh_token} = client) do
+
request =
+
Req.new(method: :post, url: XRPC.url(endpoint, "com.atproto.server.refreshSession"))
+
|> put_auth(refresh_token)
+
+
case Req.request(request) do
+
{:ok, %{body: %{"accessJwt" => access_token, "refreshJwt" => refresh_token}}} ->
+
{:ok, %{client | access_token: access_token, refresh_token: refresh_token}}
+
+
{:ok, response} ->
+
{:error, response}
+
+
err ->
+
err
+
end
+
end
+
+
@impl true
+
def get(%__MODULE__{} = client, resource, opts \\ []) do
+
request(client, opts ++ [method: :get, url: XRPC.url(client.endpoint, resource)])
+
end
+
+
@impl true
+
def post(%__MODULE__{} = client, resource, opts \\ []) do
+
request(client, opts ++ [method: :post, url: XRPC.url(client.endpoint, resource)])
+
end
+
+
@spec request(t(), keyword()) :: {:ok, Req.Response.t(), t()} | {:error, any()}
+
defp request(client, opts) do
+
with {:ok, client} <- validate_client(client) do
+
request = opts |> Req.new() |> put_auth(client.access_token)
+
+
case Req.request(request) do
+
{:ok, %{status: 200} = response} ->
+
{:ok, response, client}
+
+
{:ok, response} ->
+
handle_failure(client, response, request)
+
+
err ->
+
err
+
end
+
end
+
end
+
+
@spec handle_failure(t(), Req.Response.t(), Req.Request.t()) ::
+
{:ok, Req.Response.t(), t()} | {:error, any()}
+
defp handle_failure(client, response, request) do
+
IO.inspect(response, label: "got failure")
+
+
if auth_error?(response.body) and client.refresh_token do
+
case refresh(client) do
+
{:ok, client} ->
+
case Req.request(put_auth(request, client.access_token)) do
+
{:ok, %{status: 200} = response} -> {:ok, response, client}
+
{:ok, response} -> {:error, response}
+
err -> err
+
end
+
+
err ->
+
err
+
end
+
else
+
{:error, response}
+
end
+
end
+
+
@spec validate_client(t()) :: {:ok, t()} | {:error, any()}
+
defp validate_client(%__MODULE__{access_token: nil}), do: {:error, :no_token}
+
defp validate_client(%__MODULE__{} = client), do: {:ok, client}
+
+
@spec auth_error?(body :: Req.Response.t()) :: boolean()
+
defp auth_error?(%{status: status}) when status in [401, 403], do: true
+
defp auth_error?(%{body: %{"error" => "InvalidToken"}}), do: true
+
defp auth_error?(_response), do: false
+
+
@spec put_auth(Req.Request.t(), String.t()) :: Req.Request.t()
+
defp put_auth(request, token),
+
do: Req.Request.put_header(request, "authorization", "Bearer #{token}")
+
end
+228
lib/atex/xrpc/oauth_client.ex
···
+
defmodule Atex.XRPC.OAuthClient do
+
alias Atex.OAuth
+
alias Atex.XRPC
+
use TypedStruct
+
+
@behaviour Atex.XRPC.Client
+
+
typedstruct enforce: true do
+
field :endpoint, String.t()
+
field :issuer, String.t()
+
field :access_token, String.t()
+
field :refresh_token, String.t()
+
field :did, String.t()
+
field :expires_at, NaiveDateTime.t()
+
field :dpop_nonce, String.t() | nil, enforce: false
+
field :dpop_key, JOSE.JWK.t()
+
end
+
+
@doc """
+
Create a new OAuthClient struct.
+
"""
+
@spec new(
+
String.t(),
+
String.t(),
+
String.t(),
+
String.t(),
+
NaiveDateTime.t(),
+
JOSE.JWK.t(),
+
String.t() | nil
+
) :: t()
+
def new(endpoint, did, access_token, refresh_token, expires_at, dpop_key, dpop_nonce) do
+
{:ok, issuer} = OAuth.get_authorization_server(endpoint)
+
+
%__MODULE__{
+
endpoint: endpoint,
+
issuer: issuer,
+
access_token: access_token,
+
refresh_token: refresh_token,
+
did: did,
+
expires_at: expires_at,
+
dpop_nonce: dpop_nonce,
+
dpop_key: dpop_key
+
}
+
end
+
+
@doc """
+
Create an OAuthClient struct from a `Plug.Conn`.
+
+
Requires the conn to have passed through `Plug.Session` and
+
`Plug.Conn.fetch_session/2` so that the session can be acquired and have the
+
`atex_oauth` key fetched from it.
+
+
Returns `:error` if the state is missing or is not the expected shape.
+
"""
+
@spec from_conn(Plug.Conn.t()) :: {:ok, t()} | :error
+
def from_conn(%Plug.Conn{} = conn) do
+
oauth_state = Plug.Conn.get_session(conn, :atex_oauth)
+
+
case oauth_state do
+
%{
+
access_token: access_token,
+
refresh_token: refresh_token,
+
did: did,
+
pds: pds,
+
expires_at: expires_at,
+
dpop_nonce: dpop_nonce,
+
dpop_key: dpop_key
+
} ->
+
{:ok, new(pds, did, access_token, refresh_token, expires_at, dpop_key, dpop_nonce)}
+
+
_ ->
+
:error
+
end
+
end
+
+
@doc """
+
Updates a `Plug.Conn` session with the latest values from the client.
+
+
Ideally should be called at the end of routes where XRPC calls occur, in case
+
the client has transparently refreshed, so that the user is always up to date.
+
"""
+
@spec update_plug(Plug.Conn.t(), t()) :: Plug.Conn.t()
+
def update_plug(%Plug.Conn{} = conn, %__MODULE__{} = client) do
+
Plug.Conn.put_session(conn, :atex_oauth, %{
+
access_token: client.access_token,
+
refresh_token: client.refresh_token,
+
did: client.did,
+
pds: client.endpoint,
+
expires_at: client.expires_at,
+
dpop_nonce: client.dpop_nonce,
+
dpop_key: client.dpop_key
+
})
+
end
+
+
@doc """
+
Ask the client's OAuth server for a new set of auth tokens.
+
+
You shouldn't need to call this manually for the most part, the client does
+
it's best to refresh automatically when it needs to.
+
"""
+
@spec refresh(t()) :: {:ok, t()} | {:error, any()}
+
def refresh(%__MODULE__{} = client) do
+
with {:ok, authz_server} <- OAuth.get_authorization_server(client.endpoint),
+
{:ok, %{token_endpoint: token_endpoint}} <-
+
OAuth.get_authorization_server_metadata(authz_server) do
+
case OAuth.refresh_token(
+
client.refresh_token,
+
client.dpop_key,
+
client.issuer,
+
token_endpoint
+
) do
+
{:ok, tokens, nonce} ->
+
{:ok,
+
%{
+
client
+
| access_token: tokens.access_token,
+
refresh_token: tokens.refresh_token,
+
dpop_nonce: nonce
+
}}
+
+
err ->
+
err
+
end
+
end
+
end
+
+
@doc """
+
See `Atex.XRPC.get/3`.
+
"""
+
@impl true
+
def get(%__MODULE__{} = client, resource, opts \\ []) do
+
request(client, opts ++ [method: :get, url: XRPC.url(client.endpoint, resource)])
+
end
+
+
@doc """
+
See `Atex.XRPC.post/3`.
+
"""
+
@impl true
+
def post(%__MODULE__{} = client, resource, opts \\ []) do
+
request(client, opts ++ [method: :post, url: XRPC.url(client.endpoint, resource)])
+
end
+
+
@spec request(t(), keyword()) :: {:ok, Req.Response.t(), t()} | {:error, any(), any()}
+
defp request(client, opts) do
+
# Preemptively refresh token if it's about to expire
+
with {:ok, client} <- maybe_refresh(client) do
+
request = opts |> Req.new() |> put_auth(client.access_token)
+
+
case OAuth.request_protected_dpop_resource(
+
request,
+
client.issuer,
+
client.access_token,
+
client.dpop_key,
+
client.dpop_nonce
+
) do
+
{:ok, %{status: 200} = response, nonce} ->
+
client = %{client | dpop_nonce: nonce}
+
{:ok, response, client}
+
+
{:ok, response, nonce} ->
+
client = %{client | dpop_nonce: nonce}
+
handle_failure(client, response, request)
+
+
err ->
+
err
+
end
+
end
+
end
+
+
@spec handle_failure(t(), Req.Response.t(), Req.Request.t()) ::
+
{:ok, Req.Response.t(), t()} | {:error, any(), t()}
+
defp handle_failure(client, response, request) do
+
IO.inspect(response, label: "got failure")
+
+
if auth_error?(response.body) and client.refresh_token do
+
case refresh(client) do
+
{:ok, client} ->
+
case OAuth.request_protected_dpop_resource(
+
request,
+
client.issuer,
+
client.access_token,
+
client.dpop_key,
+
client.dpop_nonce
+
) do
+
{:ok, %{status: 200} = response, nonce} ->
+
{:ok, response, %{client | dpop_nonce: nonce}}
+
+
{:ok, response, nonce} ->
+
{:error, response, %{client | dpop_nonce: nonce}}
+
+
{:error, err} ->
+
{:error, err, client}
+
end
+
+
err ->
+
err
+
end
+
else
+
{:error, response, client}
+
end
+
end
+
+
@spec maybe_refresh(t(), integer()) :: {:ok, t()} | {:error, any()}
+
defp maybe_refresh(%__MODULE__{expires_at: expires_at} = client, buffer_minutes \\ 5) do
+
if token_expiring_soon?(expires_at, buffer_minutes) do
+
refresh(client)
+
else
+
{:ok, client}
+
end
+
end
+
+
@spec token_expiring_soon?(NaiveDateTime.t(), integer()) :: boolean()
+
defp token_expiring_soon?(expires_at, buffer_minutes) do
+
now = NaiveDateTime.utc_now()
+
expiry_threshold = NaiveDateTime.add(now, buffer_minutes * 60, :second)
+
+
NaiveDateTime.compare(expires_at, expiry_threshold) in [:lt, :eq]
+
end
+
+
@spec auth_error?(body :: Req.Response.t()) :: boolean()
+
defp auth_error?(%{status: status}) when status in [401, 403], do: true
+
defp auth_error?(%{body: %{"error" => "InvalidToken"}}), do: true
+
defp auth_error?(_response), do: false
+
+
@spec put_auth(Req.Request.t(), String.t()) :: Req.Request.t()
+
defp put_auth(request, token),
+
do: Req.Request.put_header(request, "authorization", "DPoP #{token}")
+
end
+181 -38
lib/atex/xrpc.ex
···
defmodule Atex.XRPC do
-
alias Atex.{HTTP, XRPC}
+
@moduledoc """
+
XRPC client module for AT Protocol RPC calls.
-
# TODO: automatic user-agent, and env for changing it
+
This module provides both authenticated and unauthenticated access to AT Protocol
+
XRPC endpoints. The authenticated functions (`get/3`, `post/3`) work with any
+
client that implements the `Atex.XRPC.Client`.
-
# TODO: consistent struct shape/protocol for Lexicon schemas so that user can pass in
-
# an object (hopefully validated by its module) without needing to specify the
-
# name & opts separately, and possibly verify the output response against it?
+
## Example usage
-
# TODO: auto refresh, will need to return a client instance in each method.
+
# Login-based client
+
{:ok, client} = Atex.XRPC.LoginClient.login("https://bsky.social", "user.bsky.social", "password")
+
{:ok, response, client} = Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
+
+
# OAuth-based client
+
{:ok, oauth_client} = Atex.XRPC.OAuthClient.from_conn(conn)
+
{:ok, response, oauth_client} = Atex.XRPC.get(oauth_client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
+
+
## Unauthenticated requests
+
+
Unauthenticated functions (`unauthed_get/3`, `unauthed_post/3`) do not require a client
+
and work directly with endpoints:
+
+
{:ok, response} = Atex.XRPC.unauthed_get("https://bsky.social", "com.atproto.sync.getHead", params: [did: "did:plc:..."])
+
"""
+
+
alias Atex.XRPC.Client
@doc """
Perform a HTTP GET on a XRPC resource. Called a "query" in lexicons.
+
+
Accepts any client that implements `Atex.XRPC.Client` and returns
+
both the response and the (potentially updated) client.
+
+
Can be called either with the XRPC operation name as a string, or with a lexicon
+
struct (generated via `deflexicon`) for type safety and automatic parameter/response handling.
+
+
When using a lexicon struct, the response body will be automatically converted to the
+
corresponding type if an Output struct exists for the lexicon.
+
+
## Examples
+
+
# Using string XRPC name
+
{:ok, response, client} =
+
Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "ovyerus.com"])
+
+
# Using lexicon struct with typed construction
+
{:ok, response, client} =
+
Atex.XRPC.get(client, %App.Bsky.Actor.GetProfile{
+
params: %App.Bsky.Actor.GetProfile.Params{actor: "ovyerus.com"}
+
})
"""
-
@spec get(XRPC.Client.t(), String.t(), keyword()) :: HTTP.Adapter.result()
-
def get(%XRPC.Client{} = client, name, opts \\ []) do
-
opts = put_auth(opts, client.access_token)
-
HTTP.get(url(client, name), opts)
+
@spec get(Client.client(), String.t() | struct(), keyword()) ::
+
{:ok, Req.Response.t(), Client.client()}
+
| {:error, any(), Client.client()}
+
def get(client, name, opts \\ [])
+
+
def get(client, name, opts) when is_binary(name) do
+
client.__struct__.get(client, name, opts)
+
end
+
+
def get(client, %{__struct__: module} = query, opts) do
+
opts = if Map.get(query, :params), do: Keyword.put(opts, :params, query.params), else: opts
+
output_struct = Module.concat(module, Output)
+
output_exists = Code.ensure_loaded?(output_struct)
+
+
case client.__struct__.get(client, module.id(), opts) do
+
{:ok, %{status: 200} = response, client} ->
+
if output_exists do
+
case output_struct.from_json(response.body) do
+
{:ok, output} ->
+
{:ok, %{response | body: output}, client}
+
+
err ->
+
err
+
end
+
else
+
{:ok, response, client}
+
end
+
+
{:ok, _, _} = ok ->
+
ok
+
+
err ->
+
err
+
end
end
@doc """
-
Perform a HTTP POST on a XRPC resource. Called a "prodecure" in lexicons.
+
Perform a HTTP POST on a XRPC resource. Called a "procedure" in lexicons.
+
+
Accepts any client that implements `Atex.XRPC.Client` and returns both the
+
response and the (potentially updated) client.
+
+
Can be called either with the XRPC operation name as a string, or with a
+
lexicon struct (generated via `deflexicon`) for type safety and automatic
+
input/parameter mapping.
+
+
When using a lexicon struct, the response body will be automatically converted
+
to the corresponding type if an Output struct exists for the lexicon.
+
+
## Examples
+
+
# Using string XRPC name
+
{:ok, response, client} =
+
Atex.XRPC.post(
+
client,
+
"com.atproto.repo.createRecord",
+
json: %{
+
repo: "did:plc:...",
+
collection: "app.bsky.feed.post",
+
rkey: Atex.TID.now() |> to_string(),
+
record: %{
+
text: "Hello World",
+
createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
+
}
+
}
+
)
+
+
# Using lexicon struct with typed construction
+
{:ok, response, client} =
+
Atex.XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
+
input: %Com.Atproto.Repo.CreateRecord.Input{
+
repo: "did:plc:...",
+
collection: "app.bsky.feed.post",
+
rkey: Atex.TID.now() |> to_string(),
+
record: %App.Bsky.Feed.Post{
+
text: "Hello World!",
+
createdAt: NaiveDateTime.to_iso8601(NaiveDateTime.utc_now())
+
}
+
}
+
})
"""
-
@spec post(XRPC.Client.t(), String.t(), keyword()) :: HTTP.Adapter.result()
-
def post(%XRPC.Client{} = client, name, opts \\ []) do
-
# TODO: look through available HTTP clients and see if they have a
-
# consistent way of providing JSON bodies with auto content-type. If not,
-
# create one for adapters.
-
opts = put_auth(opts, client.access_token)
-
HTTP.post(url(client, name), opts)
+
@spec post(Client.client(), String.t() | struct(), keyword()) ::
+
{:ok, Req.Response.t(), Client.client()}
+
| {:error, any(), Client.client()}
+
def post(client, name, opts \\ [])
+
+
def post(client, name, opts) when is_binary(name) do
+
client.__struct__.post(client, name, opts)
+
end
+
+
def post(client, %{__struct__: module} = procedure, opts) do
+
opts =
+
opts
+
|> then(
+
&if Map.get(procedure, :params), do: Keyword.put(&1, :params, procedure.params), else: &1
+
)
+
|> then(
+
&cond do
+
Map.get(procedure, :input) -> Keyword.put(&1, :json, procedure.input)
+
Map.get(procedure, :raw_input) -> Keyword.put(&1, :body, procedure.raw_input)
+
true -> &1
+
end
+
)
+
+
output_struct = Module.concat(module, Output)
+
output_exists = Code.ensure_loaded?(output_struct)
+
+
case client.__struct__.post(client, module.id(), opts) do
+
{:ok, %{status: 200} = response, client} ->
+
if output_exists do
+
case output_struct.from_json(response.body) do
+
{:ok, output} ->
+
{:ok, %{response | body: output}, client}
+
+
err ->
+
err
+
end
+
else
+
{:ok, response, client}
+
end
+
+
{:ok, _, _} = ok ->
+
ok
+
+
err ->
+
err
+
end
end
@doc """
Like `get/3` but is unauthenticated by default.
"""
-
@spec unauthed_get(String.t(), String.t(), keyword()) :: HTTP.Adapter.result()
+
@spec unauthed_get(String.t(), String.t(), keyword()) ::
+
{:ok, Req.Response.t()} | {:error, any()}
def unauthed_get(endpoint, name, opts \\ []) do
-
HTTP.get(url(endpoint, name), opts)
+
Req.get(url(endpoint, name), opts)
end
@doc """
Like `post/3` but is unauthenticated by default.
"""
-
@spec unauthed_post(String.t(), String.t(), keyword()) :: HTTP.Adapter.result()
+
@spec unauthed_post(String.t(), String.t(), keyword()) ::
+
{:ok, Req.Response.t()} | {:error, any()}
def unauthed_post(endpoint, name, opts \\ []) do
-
HTTP.post(url(endpoint, name), opts)
+
Req.post(url(endpoint, name), opts)
end
-
# TODO: use URI module for joining instead?
-
@spec url(XRPC.Client.t() | String.t(), String.t()) :: String.t()
-
defp url(%XRPC.Client{endpoint: endpoint}, name), do: url(endpoint, name)
-
defp url(endpoint, name) when is_binary(endpoint), do: "#{endpoint}/xrpc/#{name}"
+
@doc """
+
Create an XRPC url based on an endpoint and a resource name.
+
+
## Example
-
@doc """
-
Put an `authorization` header into a keyword list of options to pass to a HTTP client.
+
iex> Atex.XRPC.url("https://bsky.app", "app.bsky.actor.getProfile")
+
"https://bsky.app/xrpc/app.bsky.actor.getProfile"
"""
-
@spec put_auth(keyword(), String.t()) :: keyword()
-
def put_auth(opts, token),
-
do: put_headers(opts, authorization: "Bearer #{token}")
-
-
@spec put_headers(keyword(), keyword()) :: keyword()
-
defp put_headers(opts, headers) do
-
opts
-
|> Keyword.put_new(:headers, [])
-
|> Keyword.update(:headers, [], &Keyword.merge(&1, headers))
-
end
+
@spec url(String.t(), String.t()) :: String.t()
+
def url(endpoint, resource) when is_binary(endpoint), do: "#{endpoint}/xrpc/#{resource}"
end
+73
lib/atproto/com/atproto/admin/defs.ex
···
+
defmodule Com.Atproto.Admin.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"accountView" => %{
+
"properties" => %{
+
"deactivatedAt" => %{"format" => "datetime", "type" => "string"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"email" => %{"type" => "string"},
+
"emailConfirmedAt" => %{"format" => "datetime", "type" => "string"},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"indexedAt" => %{"format" => "datetime", "type" => "string"},
+
"inviteNote" => %{"type" => "string"},
+
"invitedBy" => %{
+
"ref" => "com.atproto.server.defs#inviteCode",
+
"type" => "ref"
+
},
+
"invites" => %{
+
"items" => %{
+
"ref" => "com.atproto.server.defs#inviteCode",
+
"type" => "ref"
+
},
+
"type" => "array"
+
},
+
"invitesDisabled" => %{"type" => "boolean"},
+
"relatedRecords" => %{
+
"items" => %{"type" => "unknown"},
+
"type" => "array"
+
},
+
"threatSignatures" => %{
+
"items" => %{"ref" => "#threatSignature", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["did", "handle", "indexedAt"],
+
"type" => "object"
+
},
+
"repoBlobRef" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"recordUri" => %{"format" => "at-uri", "type" => "string"}
+
},
+
"required" => ["did", "cid"],
+
"type" => "object"
+
},
+
"repoRef" => %{
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
+
"required" => ["did"],
+
"type" => "object"
+
},
+
"statusAttr" => %{
+
"properties" => %{
+
"applied" => %{"type" => "boolean"},
+
"ref" => %{"type" => "string"}
+
},
+
"required" => ["applied"],
+
"type" => "object"
+
},
+
"threatSignature" => %{
+
"properties" => %{
+
"property" => %{"type" => "string"},
+
"value" => %{"type" => "string"}
+
},
+
"required" => ["property", "value"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.admin.defs",
+
"lexicon" => 1
+
})
+
end
+22
lib/atproto/com/atproto/admin/deleteAccount.ex
···
+
defmodule Com.Atproto.Admin.DeleteAccount do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Delete a user account as an administrator.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
+
"required" => ["did"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.deleteAccount",
+
"lexicon" => 1
+
})
+
end
+29
lib/atproto/com/atproto/admin/disableAccountInvites.ex
···
+
defmodule Com.Atproto.Admin.DisableAccountInvites do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Disable an account from receiving new invite codes, but does not invalidate existing codes.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"account" => %{"format" => "did", "type" => "string"},
+
"note" => %{
+
"description" => "Optional reason for disabled invites.",
+
"type" => "string"
+
}
+
},
+
"required" => ["account"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.disableAccountInvites",
+
"lexicon" => 1
+
})
+
end
+25
lib/atproto/com/atproto/admin/disableInviteCodes.ex
···
+
defmodule Com.Atproto.Admin.DisableInviteCodes do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Disable some set of codes and/or all codes associated with a set of users.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"accounts" => %{"items" => %{"type" => "string"}, "type" => "array"},
+
"codes" => %{"items" => %{"type" => "string"}, "type" => "array"}
+
},
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.disableInviteCodes",
+
"lexicon" => 1
+
})
+
end
+28
lib/atproto/com/atproto/admin/enableAccountInvites.ex
···
+
defmodule Com.Atproto.Admin.EnableAccountInvites do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Re-enable an account's ability to receive invite codes.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"account" => %{"format" => "did", "type" => "string"},
+
"note" => %{
+
"description" => "Optional reason for enabled invites.",
+
"type" => "string"
+
}
+
},
+
"required" => ["account"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.enableAccountInvites",
+
"lexicon" => 1
+
})
+
end
+26
lib/atproto/com/atproto/admin/getAccountInfo.ex
···
+
defmodule Com.Atproto.Admin.GetAccountInfo do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get details about an account.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"ref" => "com.atproto.admin.defs#accountView",
+
"type" => "ref"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.admin.getAccountInfo",
+
"lexicon" => 1
+
})
+
end
+40
lib/atproto/com/atproto/admin/getAccountInfos.ex
···
+
defmodule Com.Atproto.Admin.GetAccountInfos do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get details about some accounts.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"infos" => %{
+
"items" => %{
+
"ref" => "com.atproto.admin.defs#accountView",
+
"type" => "ref"
+
},
+
"type" => "array"
+
}
+
},
+
"required" => ["infos"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"dids" => %{
+
"items" => %{"format" => "did", "type" => "string"},
+
"type" => "array"
+
}
+
},
+
"required" => ["dids"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.admin.getAccountInfos",
+
"lexicon" => 1
+
})
+
end
+48
lib/atproto/com/atproto/admin/getInviteCodes.ex
···
+
defmodule Com.Atproto.Admin.GetInviteCodes do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get an admin view of invite codes.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"codes" => %{
+
"items" => %{
+
"ref" => "com.atproto.server.defs#inviteCode",
+
"type" => "ref"
+
},
+
"type" => "array"
+
},
+
"cursor" => %{"type" => "string"}
+
},
+
"required" => ["codes"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 100,
+
"maximum" => 500,
+
"minimum" => 1,
+
"type" => "integer"
+
},
+
"sort" => %{
+
"default" => "recent",
+
"knownValues" => ["recent", "usage"],
+
"type" => "string"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.admin.getInviteCodes",
+
"lexicon" => 1
+
})
+
end
+48
lib/atproto/com/atproto/admin/getSubjectStatus.ex
···
+
defmodule Com.Atproto.Admin.GetSubjectStatus do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get the service-specific admin status of a subject (account, record, or blob).",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"deactivated" => %{
+
"ref" => "com.atproto.admin.defs#statusAttr",
+
"type" => "ref"
+
},
+
"subject" => %{
+
"refs" => [
+
"com.atproto.admin.defs#repoRef",
+
"com.atproto.repo.strongRef",
+
"com.atproto.admin.defs#repoBlobRef"
+
],
+
"type" => "union"
+
},
+
"takedown" => %{
+
"ref" => "com.atproto.admin.defs#statusAttr",
+
"type" => "ref"
+
}
+
},
+
"required" => ["subject"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"blob" => %{"format" => "cid", "type" => "string"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.admin.getSubjectStatus",
+
"lexicon" => 1
+
})
+
end
+44
lib/atproto/com/atproto/admin/searchAccounts.ex
···
+
defmodule Com.Atproto.Admin.SearchAccounts do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get list of accounts that matches your search query.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"accounts" => %{
+
"items" => %{
+
"ref" => "com.atproto.admin.defs#accountView",
+
"type" => "ref"
+
},
+
"type" => "array"
+
},
+
"cursor" => %{"type" => "string"}
+
},
+
"required" => ["accounts"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"email" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 50,
+
"maximum" => 100,
+
"minimum" => 1,
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.admin.searchAccounts",
+
"lexicon" => 1
+
})
+
end
+40
lib/atproto/com/atproto/admin/sendEmail.ex
···
+
defmodule Com.Atproto.Admin.SendEmail do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Send email to a user's account email address.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"comment" => %{
+
"description" =>
+
"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers",
+
"type" => "string"
+
},
+
"content" => %{"type" => "string"},
+
"recipientDid" => %{"format" => "did", "type" => "string"},
+
"senderDid" => %{"format" => "did", "type" => "string"},
+
"subject" => %{"type" => "string"}
+
},
+
"required" => ["recipientDid", "content", "senderDid"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"sent" => %{"type" => "boolean"}},
+
"required" => ["sent"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.sendEmail",
+
"lexicon" => 1
+
})
+
end
+29
lib/atproto/com/atproto/admin/updateAccountEmail.ex
···
+
defmodule Com.Atproto.Admin.UpdateAccountEmail do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Administrative action to update an account's email.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"account" => %{
+
"description" => "The handle or DID of the repo.",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"email" => %{"type" => "string"}
+
},
+
"required" => ["account", "email"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.updateAccountEmail",
+
"lexicon" => 1
+
})
+
end
+25
lib/atproto/com/atproto/admin/updateAccountHandle.ex
···
+
defmodule Com.Atproto.Admin.UpdateAccountHandle do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Administrative action to update an account's handle.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"handle" => %{"format" => "handle", "type" => "string"}
+
},
+
"required" => ["did", "handle"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.updateAccountHandle",
+
"lexicon" => 1
+
})
+
end
+25
lib/atproto/com/atproto/admin/updateAccountPassword.ex
···
+
defmodule Com.Atproto.Admin.UpdateAccountPassword do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Update the password for a user account as an administrator.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"password" => %{"type" => "string"}
+
},
+
"required" => ["did", "password"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.updateAccountPassword",
+
"lexicon" => 1
+
})
+
end
+30
lib/atproto/com/atproto/admin/updateAccountSigningKey.ex
···
+
defmodule Com.Atproto.Admin.UpdateAccountSigningKey do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Administrative action to update an account's signing key in their Did document.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"signingKey" => %{
+
"description" => "Did-key formatted public key",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "signingKey"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.updateAccountSigningKey",
+
"lexicon" => 1
+
})
+
end
+61
lib/atproto/com/atproto/admin/updateSubjectStatus.ex
···
+
defmodule Com.Atproto.Admin.UpdateSubjectStatus do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Update the service-specific admin status of a subject (account, record, or blob).",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"deactivated" => %{
+
"ref" => "com.atproto.admin.defs#statusAttr",
+
"type" => "ref"
+
},
+
"subject" => %{
+
"refs" => [
+
"com.atproto.admin.defs#repoRef",
+
"com.atproto.repo.strongRef",
+
"com.atproto.admin.defs#repoBlobRef"
+
],
+
"type" => "union"
+
},
+
"takedown" => %{
+
"ref" => "com.atproto.admin.defs#statusAttr",
+
"type" => "ref"
+
}
+
},
+
"required" => ["subject"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"subject" => %{
+
"refs" => [
+
"com.atproto.admin.defs#repoRef",
+
"com.atproto.repo.strongRef",
+
"com.atproto.admin.defs#repoBlobRef"
+
],
+
"type" => "union"
+
},
+
"takedown" => %{
+
"ref" => "com.atproto.admin.defs#statusAttr",
+
"type" => "ref"
+
}
+
},
+
"required" => ["subject"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.admin.updateSubjectStatus",
+
"lexicon" => 1
+
})
+
end
+27
lib/atproto/com/atproto/identity/defs.ex
···
+
defmodule Com.Atproto.Identity.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"identityInfo" => %{
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"didDoc" => %{
+
"description" => "The complete DID document for the identity.",
+
"type" => "unknown"
+
},
+
"handle" => %{
+
"description" =>
+
"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.",
+
"format" => "handle",
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "handle", "didDoc"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.identity.defs",
+
"lexicon" => 1
+
})
+
end
+35
lib/atproto/com/atproto/identity/getRecommendedDidCredentials.ex
···
+
defmodule Com.Atproto.Identity.GetRecommendedDidCredentials do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Describe the credentials that should be included in the DID doc of an account that is migrating to this service.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"alsoKnownAs" => %{
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
},
+
"rotationKeys" => %{
+
"description" =>
+
"Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.",
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
},
+
"services" => %{"type" => "unknown"},
+
"verificationMethods" => %{"type" => "unknown"}
+
},
+
"type" => "object"
+
}
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.identity.getRecommendedDidCredentials",
+
"lexicon" => 1
+
})
+
end
+47
lib/atproto/com/atproto/identity/refreshIdentity.ex
···
+
defmodule Com.Atproto.Identity.RefreshIdentity do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server.",
+
"errors" => [
+
%{
+
"description" =>
+
"The resolution process confirmed that the handle does not resolve to any DID.",
+
"name" => "HandleNotFound"
+
},
+
%{
+
"description" => "The DID resolution process confirmed that there is no current DID.",
+
"name" => "DidNotFound"
+
},
+
%{
+
"description" => "The DID previously existed, but has been deactivated.",
+
"name" => "DidDeactivated"
+
}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"identifier" => %{"format" => "at-identifier", "type" => "string"}
+
},
+
"required" => ["identifier"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"ref" => "com.atproto.identity.defs#identityInfo",
+
"type" => "ref"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.identity.refreshIdentity",
+
"lexicon" => 1
+
})
+
end
+15
lib/atproto/com/atproto/identity/requestPlcOperationSignature.ex
···
+
defmodule Com.Atproto.Identity.RequestPlcOperationSignature do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Request an email with a code to in order to request a signed PLC operation. Requires Auth.",
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.identity.requestPlcOperationSignature",
+
"lexicon" => 1
+
})
+
end
+48
lib/atproto/com/atproto/identity/resolveDid.ex
···
+
defmodule Com.Atproto.Identity.ResolveDid do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Resolves DID to DID document. Does not bi-directionally verify handle.",
+
"errors" => [
+
%{
+
"description" => "The DID resolution process confirmed that there is no current DID.",
+
"name" => "DidNotFound"
+
},
+
%{
+
"description" => "The DID previously existed, but has been deactivated.",
+
"name" => "DidDeactivated"
+
}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"didDoc" => %{
+
"description" => "The complete DID document for the identity.",
+
"type" => "unknown"
+
}
+
},
+
"required" => ["didDoc"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "DID to resolve.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.identity.resolveDid",
+
"lexicon" => 1
+
})
+
end
+41
lib/atproto/com/atproto/identity/resolveHandle.ex
···
+
defmodule Com.Atproto.Identity.ResolveHandle do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
+
"errors" => [
+
%{
+
"description" =>
+
"The resolution process confirmed that the handle does not resolve to any DID.",
+
"name" => "HandleNotFound"
+
}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
+
"required" => ["did"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"handle" => %{
+
"description" => "The handle to resolve.",
+
"format" => "handle",
+
"type" => "string"
+
}
+
},
+
"required" => ["handle"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.identity.resolveHandle",
+
"lexicon" => 1
+
})
+
end
+48
lib/atproto/com/atproto/identity/resolveIdentity.ex
···
+
defmodule Com.Atproto.Identity.ResolveIdentity do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).",
+
"errors" => [
+
%{
+
"description" =>
+
"The resolution process confirmed that the handle does not resolve to any DID.",
+
"name" => "HandleNotFound"
+
},
+
%{
+
"description" => "The DID resolution process confirmed that there is no current DID.",
+
"name" => "DidNotFound"
+
},
+
%{
+
"description" => "The DID previously existed, but has been deactivated.",
+
"name" => "DidDeactivated"
+
}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"ref" => "com.atproto.identity.defs#identityInfo",
+
"type" => "ref"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"identifier" => %{
+
"description" => "Handle or DID to resolve.",
+
"format" => "at-identifier",
+
"type" => "string"
+
}
+
},
+
"required" => ["identifier"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.identity.resolveIdentity",
+
"lexicon" => 1
+
})
+
end
+51
lib/atproto/com/atproto/identity/signPlcOperation.ex
···
+
defmodule Com.Atproto.Identity.SignPlcOperation do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Signs a PLC operation to update some value(s) in the requesting DID's document.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"alsoKnownAs" => %{
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
},
+
"rotationKeys" => %{
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
},
+
"services" => %{"type" => "unknown"},
+
"token" => %{
+
"description" =>
+
"A token received through com.atproto.identity.requestPlcOperationSignature",
+
"type" => "string"
+
},
+
"verificationMethods" => %{"type" => "unknown"}
+
},
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"operation" => %{
+
"description" => "A signed DID PLC operation.",
+
"type" => "unknown"
+
}
+
},
+
"required" => ["operation"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.identity.signPlcOperation",
+
"lexicon" => 1
+
})
+
end
+23
lib/atproto/com/atproto/identity/submitPlcOperation.ex
···
+
defmodule Com.Atproto.Identity.SubmitPlcOperation do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"operation" => %{"type" => "unknown"}},
+
"required" => ["operation"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.identity.submitPlcOperation",
+
"lexicon" => 1
+
})
+
end
+29
lib/atproto/com/atproto/identity/updateHandle.ex
···
+
defmodule Com.Atproto.Identity.UpdateHandle do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"handle" => %{
+
"description" => "The new handle.",
+
"format" => "handle",
+
"type" => "string"
+
}
+
},
+
"required" => ["handle"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.identity.updateHandle",
+
"lexicon" => 1
+
})
+
end
+170
lib/atproto/com/atproto/label/defs.ex
···
+
defmodule Com.Atproto.Label.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"label" => %{
+
"description" => "Metadata tag on an atproto resource (eg, repo or record).",
+
"properties" => %{
+
"cid" => %{
+
"description" =>
+
"Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"cts" => %{
+
"description" => "Timestamp when this label was created.",
+
"format" => "datetime",
+
"type" => "string"
+
},
+
"exp" => %{
+
"description" => "Timestamp at which this label expires (no longer applies).",
+
"format" => "datetime",
+
"type" => "string"
+
},
+
"neg" => %{
+
"description" => "If true, this is a negation label, overwriting a previous label.",
+
"type" => "boolean"
+
},
+
"sig" => %{
+
"description" => "Signature of dag-cbor encoded label.",
+
"type" => "bytes"
+
},
+
"src" => %{
+
"description" => "DID of the actor who created this label.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"uri" => %{
+
"description" =>
+
"AT URI of the record, repository (account), or other resource that this label applies to.",
+
"format" => "uri",
+
"type" => "string"
+
},
+
"val" => %{
+
"description" => "The short string name of the value or type of this label.",
+
"maxLength" => 128,
+
"type" => "string"
+
},
+
"ver" => %{
+
"description" => "The AT Protocol version of the label object.",
+
"type" => "integer"
+
}
+
},
+
"required" => ["src", "uri", "val", "cts"],
+
"type" => "object"
+
},
+
"labelValue" => %{
+
"knownValues" => [
+
"!hide",
+
"!no-promote",
+
"!warn",
+
"!no-unauthenticated",
+
"dmca-violation",
+
"doxxing",
+
"porn",
+
"sexual",
+
"nudity",
+
"nsfl",
+
"gore"
+
],
+
"type" => "string"
+
},
+
"labelValueDefinition" => %{
+
"description" => "Declares a label value and its expected interpretations and behaviors.",
+
"properties" => %{
+
"adultOnly" => %{
+
"description" =>
+
"Does the user need to have adult content enabled in order to configure this label?",
+
"type" => "boolean"
+
},
+
"blurs" => %{
+
"description" =>
+
"What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.",
+
"knownValues" => ["content", "media", "none"],
+
"type" => "string"
+
},
+
"defaultSetting" => %{
+
"default" => "warn",
+
"description" => "The default setting for this label.",
+
"knownValues" => ["ignore", "warn", "hide"],
+
"type" => "string"
+
},
+
"identifier" => %{
+
"description" =>
+
"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
+
"maxGraphemes" => 100,
+
"maxLength" => 100,
+
"type" => "string"
+
},
+
"locales" => %{
+
"items" => %{"ref" => "#labelValueDefinitionStrings", "type" => "ref"},
+
"type" => "array"
+
},
+
"severity" => %{
+
"description" =>
+
"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
+
"knownValues" => ["inform", "alert", "none"],
+
"type" => "string"
+
}
+
},
+
"required" => ["identifier", "severity", "blurs", "locales"],
+
"type" => "object"
+
},
+
"labelValueDefinitionStrings" => %{
+
"description" =>
+
"Strings which describe the label in the UI, localized into a specific language.",
+
"properties" => %{
+
"description" => %{
+
"description" =>
+
"A longer description of what the label means and why it might be applied.",
+
"maxGraphemes" => 10000,
+
"maxLength" => 100_000,
+
"type" => "string"
+
},
+
"lang" => %{
+
"description" => "The code of the language these strings are written in.",
+
"format" => "language",
+
"type" => "string"
+
},
+
"name" => %{
+
"description" => "A short human-readable name for the label.",
+
"maxGraphemes" => 64,
+
"maxLength" => 640,
+
"type" => "string"
+
}
+
},
+
"required" => ["lang", "name", "description"],
+
"type" => "object"
+
},
+
"selfLabel" => %{
+
"description" =>
+
"Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
+
"properties" => %{
+
"val" => %{
+
"description" => "The short string name of the value or type of this label.",
+
"maxLength" => 128,
+
"type" => "string"
+
}
+
},
+
"required" => ["val"],
+
"type" => "object"
+
},
+
"selfLabels" => %{
+
"description" =>
+
"Metadata tags on an atproto record, published by the author within the record.",
+
"properties" => %{
+
"values" => %{
+
"items" => %{"ref" => "#selfLabel", "type" => "ref"},
+
"maxLength" => 10,
+
"type" => "array"
+
}
+
},
+
"required" => ["values"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.label.defs",
+
"lexicon" => 1
+
})
+
end
+56
lib/atproto/com/atproto/label/queryLabels.ex
···
+
defmodule Com.Atproto.Label.QueryLabels do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"labels" => %{
+
"items" => %{
+
"ref" => "com.atproto.label.defs#label",
+
"type" => "ref"
+
},
+
"type" => "array"
+
}
+
},
+
"required" => ["labels"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 50,
+
"maximum" => 250,
+
"minimum" => 1,
+
"type" => "integer"
+
},
+
"sources" => %{
+
"description" => "Optional list of label sources (DIDs) to filter on.",
+
"items" => %{"format" => "did", "type" => "string"},
+
"type" => "array"
+
},
+
"uriPatterns" => %{
+
"description" =>
+
"List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI.",
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
}
+
},
+
"required" => ["uriPatterns"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.label.queryLabels",
+
"lexicon" => 1
+
})
+
end
+47
lib/atproto/com/atproto/label/subscribeLabels.ex
···
+
defmodule Com.Atproto.Label.SubscribeLabels do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"info" => %{
+
"properties" => %{
+
"message" => %{"type" => "string"},
+
"name" => %{"knownValues" => ["OutdatedCursor"], "type" => "string"}
+
},
+
"required" => ["name"],
+
"type" => "object"
+
},
+
"labels" => %{
+
"properties" => %{
+
"labels" => %{
+
"items" => %{"ref" => "com.atproto.label.defs#label", "type" => "ref"},
+
"type" => "array"
+
},
+
"seq" => %{"type" => "integer"}
+
},
+
"required" => ["seq", "labels"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" =>
+
"Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.",
+
"errors" => [%{"name" => "FutureCursor"}],
+
"message" => %{
+
"schema" => %{"refs" => ["#labels", "#info"], "type" => "union"}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{
+
"description" => "The last known event seq number to backfill from.",
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "subscription"
+
}
+
},
+
"id" => "com.atproto.label.subscribeLabels",
+
"lexicon" => 1
+
})
+
end
+27
lib/atproto/com/atproto/lexicon/schema.ex
···
+
defmodule Com.Atproto.Lexicon.Schema do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc).",
+
"key" => "nsid",
+
"record" => %{
+
"properties" => %{
+
"lexicon" => %{
+
"description" =>
+
"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.",
+
"type" => "integer"
+
}
+
},
+
"required" => ["lexicon"],
+
"type" => "object"
+
},
+
"type" => "record"
+
}
+
},
+
"id" => "com.atproto.lexicon.schema",
+
"lexicon" => 1
+
})
+
end
+81
lib/atproto/com/atproto/moderation/createReport.ex
···
+
defmodule Com.Atproto.Moderation.CreateReport do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"modTool" => %{"ref" => "#modTool", "type" => "ref"},
+
"reason" => %{
+
"description" => "Additional context about the content and violation.",
+
"maxGraphemes" => 2000,
+
"maxLength" => 20000,
+
"type" => "string"
+
},
+
"reasonType" => %{
+
"description" => "Indicates the broad category of violation the report is for.",
+
"ref" => "com.atproto.moderation.defs#reasonType",
+
"type" => "ref"
+
},
+
"subject" => %{
+
"refs" => ["com.atproto.admin.defs#repoRef", "com.atproto.repo.strongRef"],
+
"type" => "union"
+
}
+
},
+
"required" => ["reasonType", "subject"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
+
"id" => %{"type" => "integer"},
+
"reason" => %{
+
"maxGraphemes" => 2000,
+
"maxLength" => 20000,
+
"type" => "string"
+
},
+
"reasonType" => %{
+
"ref" => "com.atproto.moderation.defs#reasonType",
+
"type" => "ref"
+
},
+
"reportedBy" => %{"format" => "did", "type" => "string"},
+
"subject" => %{
+
"refs" => ["com.atproto.admin.defs#repoRef", "com.atproto.repo.strongRef"],
+
"type" => "union"
+
}
+
},
+
"required" => ["id", "reasonType", "subject", "reportedBy", "createdAt"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
},
+
"modTool" => %{
+
"description" => "Moderation tool information for tracing the source of the action",
+
"properties" => %{
+
"meta" => %{
+
"description" => "Additional arbitrary metadata about the source",
+
"type" => "unknown"
+
},
+
"name" => %{
+
"description" =>
+
"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')",
+
"type" => "string"
+
}
+
},
+
"required" => ["name"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.moderation.createReport",
+
"lexicon" => 1
+
})
+
end
+101
lib/atproto/com/atproto/moderation/defs.ex
···
+
defmodule Com.Atproto.Moderation.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"reasonAppeal" => %{
+
"description" => "Appeal a previously taken moderation action",
+
"type" => "token"
+
},
+
"reasonMisleading" => %{
+
"description" =>
+
"Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.",
+
"type" => "token"
+
},
+
"reasonOther" => %{
+
"description" =>
+
"Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.",
+
"type" => "token"
+
},
+
"reasonRude" => %{
+
"description" =>
+
"Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.",
+
"type" => "token"
+
},
+
"reasonSexual" => %{
+
"description" =>
+
"Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.",
+
"type" => "token"
+
},
+
"reasonSpam" => %{
+
"description" =>
+
"Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.",
+
"type" => "token"
+
},
+
"reasonType" => %{
+
"knownValues" => [
+
"com.atproto.moderation.defs#reasonSpam",
+
"com.atproto.moderation.defs#reasonViolation",
+
"com.atproto.moderation.defs#reasonMisleading",
+
"com.atproto.moderation.defs#reasonSexual",
+
"com.atproto.moderation.defs#reasonRude",
+
"com.atproto.moderation.defs#reasonOther",
+
"com.atproto.moderation.defs#reasonAppeal",
+
"tools.ozone.report.defs#reasonAppeal",
+
"tools.ozone.report.defs#reasonOther",
+
"tools.ozone.report.defs#reasonViolenceAnimal",
+
"tools.ozone.report.defs#reasonViolenceThreats",
+
"tools.ozone.report.defs#reasonViolenceGraphicContent",
+
"tools.ozone.report.defs#reasonViolenceGlorification",
+
"tools.ozone.report.defs#reasonViolenceExtremistContent",
+
"tools.ozone.report.defs#reasonViolenceTrafficking",
+
"tools.ozone.report.defs#reasonViolenceOther",
+
"tools.ozone.report.defs#reasonSexualAbuseContent",
+
"tools.ozone.report.defs#reasonSexualNCII",
+
"tools.ozone.report.defs#reasonSexualDeepfake",
+
"tools.ozone.report.defs#reasonSexualAnimal",
+
"tools.ozone.report.defs#reasonSexualUnlabeled",
+
"tools.ozone.report.defs#reasonSexualOther",
+
"tools.ozone.report.defs#reasonChildSafetyCSAM",
+
"tools.ozone.report.defs#reasonChildSafetyGroom",
+
"tools.ozone.report.defs#reasonChildSafetyPrivacy",
+
"tools.ozone.report.defs#reasonChildSafetyHarassment",
+
"tools.ozone.report.defs#reasonChildSafetyOther",
+
"tools.ozone.report.defs#reasonHarassmentTroll",
+
"tools.ozone.report.defs#reasonHarassmentTargeted",
+
"tools.ozone.report.defs#reasonHarassmentHateSpeech",
+
"tools.ozone.report.defs#reasonHarassmentDoxxing",
+
"tools.ozone.report.defs#reasonHarassmentOther",
+
"tools.ozone.report.defs#reasonMisleadingBot",
+
"tools.ozone.report.defs#reasonMisleadingImpersonation",
+
"tools.ozone.report.defs#reasonMisleadingSpam",
+
"tools.ozone.report.defs#reasonMisleadingScam",
+
"tools.ozone.report.defs#reasonMisleadingElections",
+
"tools.ozone.report.defs#reasonMisleadingOther",
+
"tools.ozone.report.defs#reasonRuleSiteSecurity",
+
"tools.ozone.report.defs#reasonRuleProhibitedSales",
+
"tools.ozone.report.defs#reasonRuleBanEvasion",
+
"tools.ozone.report.defs#reasonRuleOther",
+
"tools.ozone.report.defs#reasonSelfHarmContent",
+
"tools.ozone.report.defs#reasonSelfHarmED",
+
"tools.ozone.report.defs#reasonSelfHarmStunts",
+
"tools.ozone.report.defs#reasonSelfHarmSubstances",
+
"tools.ozone.report.defs#reasonSelfHarmOther"
+
],
+
"type" => "string"
+
},
+
"reasonViolation" => %{
+
"description" =>
+
"Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.",
+
"type" => "token"
+
},
+
"subjectType" => %{
+
"description" => "Tag describing a type of subject that might be reported.",
+
"knownValues" => ["account", "record", "chat"],
+
"type" => "string"
+
}
+
},
+
"id" => "com.atproto.moderation.defs",
+
"lexicon" => 1
+
})
+
end
+140
lib/atproto/com/atproto/repo/applyWrites.ex
···
+
defmodule Com.Atproto.Repo.ApplyWrites do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"create" => %{
+
"description" => "Operation which creates a new record.",
+
"properties" => %{
+
"collection" => %{"format" => "nsid", "type" => "string"},
+
"rkey" => %{
+
"description" =>
+
"NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.",
+
"format" => "record-key",
+
"maxLength" => 512,
+
"type" => "string"
+
},
+
"value" => %{"type" => "unknown"}
+
},
+
"required" => ["collection", "value"],
+
"type" => "object"
+
},
+
"createResult" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"validationStatus" => %{
+
"knownValues" => ["valid", "unknown"],
+
"type" => "string"
+
}
+
},
+
"required" => ["uri", "cid"],
+
"type" => "object"
+
},
+
"delete" => %{
+
"description" => "Operation which deletes an existing record.",
+
"properties" => %{
+
"collection" => %{"format" => "nsid", "type" => "string"},
+
"rkey" => %{"format" => "record-key", "type" => "string"}
+
},
+
"required" => ["collection", "rkey"],
+
"type" => "object"
+
},
+
"deleteResult" => %{
+
"properties" => %{},
+
"required" => [],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" =>
+
"Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.",
+
"errors" => [
+
%{
+
"description" =>
+
"Indicates that the 'swapCommit' parameter did not match current commit.",
+
"name" => "InvalidSwap"
+
}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"repo" => %{
+
"description" => "The handle or DID of the repo (aka, current account).",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"swapCommit" => %{
+
"description" =>
+
"If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"validate" => %{
+
"description" =>
+
"Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons.",
+
"type" => "boolean"
+
},
+
"writes" => %{
+
"items" => %{
+
"closed" => true,
+
"refs" => ["#create", "#update", "#delete"],
+
"type" => "union"
+
},
+
"type" => "array"
+
}
+
},
+
"required" => ["repo", "writes"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"commit" => %{
+
"ref" => "com.atproto.repo.defs#commitMeta",
+
"type" => "ref"
+
},
+
"results" => %{
+
"items" => %{
+
"closed" => true,
+
"refs" => ["#createResult", "#updateResult", "#deleteResult"],
+
"type" => "union"
+
},
+
"type" => "array"
+
}
+
},
+
"required" => [],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
},
+
"update" => %{
+
"description" => "Operation which updates an existing record.",
+
"properties" => %{
+
"collection" => %{"format" => "nsid", "type" => "string"},
+
"rkey" => %{"format" => "record-key", "type" => "string"},
+
"value" => %{"type" => "unknown"}
+
},
+
"required" => ["collection", "rkey", "value"],
+
"type" => "object"
+
},
+
"updateResult" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"validationStatus" => %{
+
"knownValues" => ["valid", "unknown"],
+
"type" => "string"
+
}
+
},
+
"required" => ["uri", "cid"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.repo.applyWrites",
+
"lexicon" => 1
+
})
+
end
+79
lib/atproto/com/atproto/repo/createRecord.ex
···
+
defmodule Com.Atproto.Repo.CreateRecord do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Create a single new repository record. Requires auth, implemented by PDS.",
+
"errors" => [
+
%{
+
"description" => "Indicates that 'swapCommit' didn't match current repo commit.",
+
"name" => "InvalidSwap"
+
}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"collection" => %{
+
"description" => "The NSID of the record collection.",
+
"format" => "nsid",
+
"type" => "string"
+
},
+
"record" => %{
+
"description" => "The record itself. Must contain a $type field.",
+
"type" => "unknown"
+
},
+
"repo" => %{
+
"description" => "The handle or DID of the repo (aka, current account).",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"rkey" => %{
+
"description" => "The Record Key.",
+
"format" => "record-key",
+
"maxLength" => 512,
+
"type" => "string"
+
},
+
"swapCommit" => %{
+
"description" => "Compare and swap with the previous commit by CID.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"validate" => %{
+
"description" =>
+
"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["repo", "collection", "record"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"commit" => %{
+
"ref" => "com.atproto.repo.defs#commitMeta",
+
"type" => "ref"
+
},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"validationStatus" => %{
+
"knownValues" => ["valid", "unknown"],
+
"type" => "string"
+
}
+
},
+
"required" => ["uri", "cid"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.repo.createRecord",
+
"lexicon" => 1
+
})
+
end
+18
lib/atproto/com/atproto/repo/defs.ex
···
+
defmodule Com.Atproto.Repo.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"commitMeta" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"rev" => %{"format" => "tid", "type" => "string"}
+
},
+
"required" => ["cid", "rev"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.repo.defs",
+
"lexicon" => 1
+
})
+
end
+62
lib/atproto/com/atproto/repo/deleteRecord.ex
···
+
defmodule Com.Atproto.Repo.DeleteRecord do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.",
+
"errors" => [%{"name" => "InvalidSwap"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"collection" => %{
+
"description" => "The NSID of the record collection.",
+
"format" => "nsid",
+
"type" => "string"
+
},
+
"repo" => %{
+
"description" => "The handle or DID of the repo (aka, current account).",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"rkey" => %{
+
"description" => "The Record Key.",
+
"format" => "record-key",
+
"type" => "string"
+
},
+
"swapCommit" => %{
+
"description" => "Compare and swap with the previous commit by CID.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"swapRecord" => %{
+
"description" => "Compare and swap with the previous record by CID.",
+
"format" => "cid",
+
"type" => "string"
+
}
+
},
+
"required" => ["repo", "collection", "rkey"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"commit" => %{
+
"ref" => "com.atproto.repo.defs#commitMeta",
+
"type" => "ref"
+
}
+
},
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.repo.deleteRecord",
+
"lexicon" => 1
+
})
+
end
+52
lib/atproto/com/atproto/repo/describeRepo.ex
···
+
defmodule Com.Atproto.Repo.DescribeRepo do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get information about an account and repository, including the list of collections. Does not require auth.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"collections" => %{
+
"description" =>
+
"List of all the collections (NSIDs) for which this repo contains at least one record.",
+
"items" => %{"format" => "nsid", "type" => "string"},
+
"type" => "array"
+
},
+
"did" => %{"format" => "did", "type" => "string"},
+
"didDoc" => %{
+
"description" => "The complete DID document for this account.",
+
"type" => "unknown"
+
},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"handleIsCorrect" => %{
+
"description" =>
+
"Indicates if handle is currently valid (resolves bi-directionally)",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["handle", "did", "didDoc", "collections", "handleIsCorrect"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"repo" => %{
+
"description" => "The handle or DID of the repo.",
+
"format" => "at-identifier",
+
"type" => "string"
+
}
+
},
+
"required" => ["repo"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.repo.describeRepo",
+
"lexicon" => 1
+
})
+
end
+54
lib/atproto/com/atproto/repo/getRecord.ex
···
+
defmodule Com.Atproto.Repo.GetRecord do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get a single record from a repository. Does not require auth.",
+
"errors" => [%{"name" => "RecordNotFound"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"value" => %{"type" => "unknown"}
+
},
+
"required" => ["uri", "value"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cid" => %{
+
"description" =>
+
"The CID of the version of the record. If not specified, then return the most recent version.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"collection" => %{
+
"description" => "The NSID of the record collection.",
+
"format" => "nsid",
+
"type" => "string"
+
},
+
"repo" => %{
+
"description" => "The handle or DID of the repo.",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"rkey" => %{
+
"description" => "The Record Key.",
+
"format" => "record-key",
+
"type" => "string"
+
}
+
},
+
"required" => ["repo", "collection", "rkey"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.repo.getRecord",
+
"lexicon" => 1
+
})
+
end
+16
lib/atproto/com/atproto/repo/importRepo.ex
···
+
defmodule Com.Atproto.Repo.ImportRepo do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.",
+
"input" => %{"encoding" => "application/vnd.ipld.car"},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.repo.importRepo",
+
"lexicon" => 1
+
})
+
end
+49
lib/atproto/com/atproto/repo/listMissingBlobs.ex
···
+
defmodule Com.Atproto.Repo.ListMissingBlobs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"blobs" => %{
+
"items" => %{"ref" => "#recordBlob", "type" => "ref"},
+
"type" => "array"
+
},
+
"cursor" => %{"type" => "string"}
+
},
+
"required" => ["blobs"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 500,
+
"maximum" => 1000,
+
"minimum" => 1,
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
},
+
"recordBlob" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"recordUri" => %{"format" => "at-uri", "type" => "string"}
+
},
+
"required" => ["cid", "recordUri"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.repo.listMissingBlobs",
+
"lexicon" => 1
+
})
+
end
+66
lib/atproto/com/atproto/repo/listRecords.ex
···
+
defmodule Com.Atproto.Repo.ListRecords do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"List a range of records in a repository, matching a specific collection. Does not require auth.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"records" => %{
+
"items" => %{"ref" => "#record", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["records"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"collection" => %{
+
"description" => "The NSID of the record type.",
+
"format" => "nsid",
+
"type" => "string"
+
},
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 50,
+
"description" => "The number of records to return.",
+
"maximum" => 100,
+
"minimum" => 1,
+
"type" => "integer"
+
},
+
"repo" => %{
+
"description" => "The handle or DID of the repo.",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"reverse" => %{
+
"description" => "Flag to reverse the order of the returned records.",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["repo", "collection"],
+
"type" => "params"
+
},
+
"type" => "query"
+
},
+
"record" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"value" => %{"type" => "unknown"}
+
},
+
"required" => ["uri", "cid", "value"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.repo.listRecords",
+
"lexicon" => 1
+
})
+
end
+81
lib/atproto/com/atproto/repo/putRecord.ex
···
+
defmodule Com.Atproto.Repo.PutRecord do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.",
+
"errors" => [%{"name" => "InvalidSwap"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"nullable" => ["swapRecord"],
+
"properties" => %{
+
"collection" => %{
+
"description" => "The NSID of the record collection.",
+
"format" => "nsid",
+
"type" => "string"
+
},
+
"record" => %{
+
"description" => "The record to write.",
+
"type" => "unknown"
+
},
+
"repo" => %{
+
"description" => "The handle or DID of the repo (aka, current account).",
+
"format" => "at-identifier",
+
"type" => "string"
+
},
+
"rkey" => %{
+
"description" => "The Record Key.",
+
"format" => "record-key",
+
"maxLength" => 512,
+
"type" => "string"
+
},
+
"swapCommit" => %{
+
"description" => "Compare and swap with the previous commit by CID.",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"swapRecord" => %{
+
"description" =>
+
"Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"validate" => %{
+
"description" =>
+
"Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons.",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["repo", "collection", "rkey", "record"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"commit" => %{
+
"ref" => "com.atproto.repo.defs#commitMeta",
+
"type" => "ref"
+
},
+
"uri" => %{"format" => "at-uri", "type" => "string"},
+
"validationStatus" => %{
+
"knownValues" => ["valid", "unknown"],
+
"type" => "string"
+
}
+
},
+
"required" => ["uri", "cid"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.repo.putRecord",
+
"lexicon" => 1
+
})
+
end
+19
lib/atproto/com/atproto/repo/strongRef.ex
···
+
defmodule Com.Atproto.Repo.StrongRef do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"uri" => %{"format" => "at-uri", "type" => "string"}
+
},
+
"required" => ["uri", "cid"],
+
"type" => "object"
+
}
+
},
+
"description" => "A URI with a content-hash fingerprint.",
+
"id" => "com.atproto.repo.strongRef",
+
"lexicon" => 1
+
})
+
end
+24
lib/atproto/com/atproto/repo/uploadBlob.ex
···
+
defmodule Com.Atproto.Repo.UploadBlob do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS.",
+
"input" => %{"encoding" => "*/*"},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"blob" => %{"type" => "blob"}},
+
"required" => ["blob"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.repo.uploadBlob",
+
"lexicon" => 1
+
})
+
end
+15
lib/atproto/com/atproto/server/activateAccount.ex
···
+
defmodule Com.Atproto.Server.ActivateAccount do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.",
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.activateAccount",
+
"lexicon" => 1
+
})
+
end
+43
lib/atproto/com/atproto/server/checkAccountStatus.ex
···
+
defmodule Com.Atproto.Server.CheckAccountStatus do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"activated" => %{"type" => "boolean"},
+
"expectedBlobs" => %{"type" => "integer"},
+
"importedBlobs" => %{"type" => "integer"},
+
"indexedRecords" => %{"type" => "integer"},
+
"privateStateValues" => %{"type" => "integer"},
+
"repoBlocks" => %{"type" => "integer"},
+
"repoCommit" => %{"format" => "cid", "type" => "string"},
+
"repoRev" => %{"type" => "string"},
+
"validDid" => %{"type" => "boolean"}
+
},
+
"required" => [
+
"activated",
+
"validDid",
+
"repoCommit",
+
"repoRev",
+
"repoBlocks",
+
"indexedRecords",
+
"privateStateValues",
+
"expectedBlobs",
+
"importedBlobs"
+
],
+
"type" => "object"
+
}
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.checkAccountStatus",
+
"lexicon" => 1
+
})
+
end
+32
lib/atproto/com/atproto/server/confirmEmail.ex
···
+
defmodule Com.Atproto.Server.ConfirmEmail do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Confirm an email using a token from com.atproto.server.requestEmailConfirmation.",
+
"errors" => [
+
%{"name" => "AccountNotFound"},
+
%{"name" => "ExpiredToken"},
+
%{"name" => "InvalidToken"},
+
%{"name" => "InvalidEmail"}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"email" => %{"type" => "string"},
+
"token" => %{"type" => "string"}
+
},
+
"required" => ["email", "token"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.confirmEmail",
+
"lexicon" => 1
+
})
+
end
+83
lib/atproto/com/atproto/server/createAccount.ex
···
+
defmodule Com.Atproto.Server.CreateAccount do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Create an account. Implemented by PDS.",
+
"errors" => [
+
%{"name" => "InvalidHandle"},
+
%{"name" => "InvalidPassword"},
+
%{"name" => "InvalidInviteCode"},
+
%{"name" => "HandleNotAvailable"},
+
%{"name" => "UnsupportedDomain"},
+
%{"name" => "UnresolvableDid"},
+
%{"name" => "IncompatibleDidDoc"}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "Pre-existing atproto DID, being imported to a new account.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"email" => %{"type" => "string"},
+
"handle" => %{
+
"description" => "Requested handle for the account.",
+
"format" => "handle",
+
"type" => "string"
+
},
+
"inviteCode" => %{"type" => "string"},
+
"password" => %{
+
"description" =>
+
"Initial account password. May need to meet instance-specific password strength requirements.",
+
"type" => "string"
+
},
+
"plcOp" => %{
+
"description" =>
+
"A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented.",
+
"type" => "unknown"
+
},
+
"recoveryKey" => %{
+
"description" =>
+
"DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.",
+
"type" => "string"
+
},
+
"verificationCode" => %{"type" => "string"},
+
"verificationPhone" => %{"type" => "string"}
+
},
+
"required" => ["handle"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"description" => "Account login session returned on successful account creation.",
+
"properties" => %{
+
"accessJwt" => %{"type" => "string"},
+
"did" => %{
+
"description" => "The DID of the new account.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"didDoc" => %{
+
"description" => "Complete DID document.",
+
"type" => "unknown"
+
},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"refreshJwt" => %{"type" => "string"}
+
},
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.createAccount",
+
"lexicon" => 1
+
})
+
end
+47
lib/atproto/com/atproto/server/createAppPassword.ex
···
+
defmodule Com.Atproto.Server.CreateAppPassword do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"appPassword" => %{
+
"properties" => %{
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
+
"name" => %{"type" => "string"},
+
"password" => %{"type" => "string"},
+
"privileged" => %{"type" => "boolean"}
+
},
+
"required" => ["name", "password", "createdAt"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" => "Create an App Password.",
+
"errors" => [%{"name" => "AccountTakedown"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"name" => %{
+
"description" => "A short name for the App Password, to help distinguish them.",
+
"type" => "string"
+
},
+
"privileged" => %{
+
"description" =>
+
"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["name"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{"ref" => "#appPassword", "type" => "ref"}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.createAppPassword",
+
"lexicon" => 1
+
})
+
end
+33
lib/atproto/com/atproto/server/createInviteCode.ex
···
+
defmodule Com.Atproto.Server.CreateInviteCode do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Create an invite code.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"forAccount" => %{"format" => "did", "type" => "string"},
+
"useCount" => %{"type" => "integer"}
+
},
+
"required" => ["useCount"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"code" => %{"type" => "string"}},
+
"required" => ["code"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.createInviteCode",
+
"lexicon" => 1
+
})
+
end
+50
lib/atproto/com/atproto/server/createInviteCodes.ex
···
+
defmodule Com.Atproto.Server.CreateInviteCodes do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"accountCodes" => %{
+
"properties" => %{
+
"account" => %{"type" => "string"},
+
"codes" => %{"items" => %{"type" => "string"}, "type" => "array"}
+
},
+
"required" => ["account", "codes"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" => "Create invite codes.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"codeCount" => %{"default" => 1, "type" => "integer"},
+
"forAccounts" => %{
+
"items" => %{"format" => "did", "type" => "string"},
+
"type" => "array"
+
},
+
"useCount" => %{"type" => "integer"}
+
},
+
"required" => ["codeCount", "useCount"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"codes" => %{
+
"items" => %{"ref" => "#accountCodes", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["codes"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.createInviteCodes",
+
"lexicon" => 1
+
})
+
end
+63
lib/atproto/com/atproto/server/createSession.ex
···
+
defmodule Com.Atproto.Server.CreateSession do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Create an authentication session.",
+
"errors" => [
+
%{"name" => "AccountTakedown"},
+
%{"name" => "AuthFactorTokenRequired"}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"allowTakendown" => %{
+
"description" =>
+
"When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned",
+
"type" => "boolean"
+
},
+
"authFactorToken" => %{"type" => "string"},
+
"identifier" => %{
+
"description" =>
+
"Handle or other identifier supported by the server for the authenticating user.",
+
"type" => "string"
+
},
+
"password" => %{"type" => "string"}
+
},
+
"required" => ["identifier", "password"],
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"accessJwt" => %{"type" => "string"},
+
"active" => %{"type" => "boolean"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"didDoc" => %{"type" => "unknown"},
+
"email" => %{"type" => "string"},
+
"emailAuthFactor" => %{"type" => "boolean"},
+
"emailConfirmed" => %{"type" => "boolean"},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"refreshJwt" => %{"type" => "string"},
+
"status" => %{
+
"description" =>
+
"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+
"knownValues" => ["takendown", "suspended", "deactivated"],
+
"type" => "string"
+
}
+
},
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.createSession",
+
"lexicon" => 1
+
})
+
end
+29
lib/atproto/com/atproto/server/deactivateAccount.ex
···
+
defmodule Com.Atproto.Server.DeactivateAccount do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"deleteAfter" => %{
+
"description" =>
+
"A recommendation to server as to how long they should hold onto the deactivated account before deleting.",
+
"format" => "datetime",
+
"type" => "string"
+
}
+
},
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.deactivateAccount",
+
"lexicon" => 1
+
})
+
end
+42
lib/atproto/com/atproto/server/defs.ex
···
+
defmodule Com.Atproto.Server.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"inviteCode" => %{
+
"properties" => %{
+
"available" => %{"type" => "integer"},
+
"code" => %{"type" => "string"},
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
+
"createdBy" => %{"type" => "string"},
+
"disabled" => %{"type" => "boolean"},
+
"forAccount" => %{"type" => "string"},
+
"uses" => %{
+
"items" => %{"ref" => "#inviteCodeUse", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => [
+
"code",
+
"available",
+
"disabled",
+
"forAccount",
+
"createdBy",
+
"createdAt",
+
"uses"
+
],
+
"type" => "object"
+
},
+
"inviteCodeUse" => %{
+
"properties" => %{
+
"usedAt" => %{"format" => "datetime", "type" => "string"},
+
"usedBy" => %{"format" => "did", "type" => "string"}
+
},
+
"required" => ["usedBy", "usedAt"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.server.defs",
+
"lexicon" => 1
+
})
+
end
+28
lib/atproto/com/atproto/server/deleteAccount.ex
···
+
defmodule Com.Atproto.Server.DeleteAccount do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.",
+
"errors" => [%{"name" => "ExpiredToken"}, %{"name" => "InvalidToken"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"password" => %{"type" => "string"},
+
"token" => %{"type" => "string"}
+
},
+
"required" => ["did", "password", "token"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.deleteAccount",
+
"lexicon" => 1
+
})
+
end
+14
lib/atproto/com/atproto/server/deleteSession.ex
···
+
defmodule Com.Atproto.Server.DeleteSession do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Delete the current session. Requires auth.",
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.deleteSession",
+
"lexicon" => 1
+
})
+
end
+61
lib/atproto/com/atproto/server/describeServer.ex
···
+
defmodule Com.Atproto.Server.DescribeServer do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"contact" => %{
+
"properties" => %{"email" => %{"type" => "string"}},
+
"type" => "object"
+
},
+
"links" => %{
+
"properties" => %{
+
"privacyPolicy" => %{"format" => "uri", "type" => "string"},
+
"termsOfService" => %{"format" => "uri", "type" => "string"}
+
},
+
"type" => "object"
+
},
+
"main" => %{
+
"description" =>
+
"Describes the server's account creation requirements and capabilities. Implemented by PDS.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"availableUserDomains" => %{
+
"description" => "List of domain suffixes that can be used in account handles.",
+
"items" => %{"type" => "string"},
+
"type" => "array"
+
},
+
"contact" => %{
+
"description" => "Contact information",
+
"ref" => "#contact",
+
"type" => "ref"
+
},
+
"did" => %{"format" => "did", "type" => "string"},
+
"inviteCodeRequired" => %{
+
"description" =>
+
"If true, an invite code must be supplied to create an account on this instance.",
+
"type" => "boolean"
+
},
+
"links" => %{
+
"description" => "URLs of service policy documents.",
+
"ref" => "#links",
+
"type" => "ref"
+
},
+
"phoneVerificationRequired" => %{
+
"description" =>
+
"If true, a phone verification token must be supplied to create an account on this instance.",
+
"type" => "boolean"
+
}
+
},
+
"required" => ["did", "availableUserDomains"],
+
"type" => "object"
+
}
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.describeServer",
+
"lexicon" => 1
+
})
+
end
+43
lib/atproto/com/atproto/server/getAccountInviteCodes.ex
···
+
defmodule Com.Atproto.Server.GetAccountInviteCodes do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get all invite codes for the current account. Requires auth.",
+
"errors" => [%{"name" => "DuplicateCreate"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"codes" => %{
+
"items" => %{
+
"ref" => "com.atproto.server.defs#inviteCode",
+
"type" => "ref"
+
},
+
"type" => "array"
+
}
+
},
+
"required" => ["codes"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"createAvailable" => %{
+
"default" => true,
+
"description" =>
+
"Controls whether any new 'earned' but not 'created' invites should be created.",
+
"type" => "boolean"
+
},
+
"includeUsed" => %{"default" => true, "type" => "boolean"}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.getAccountInviteCodes",
+
"lexicon" => 1
+
})
+
end
+52
lib/atproto/com/atproto/server/getServiceAuth.ex
···
+
defmodule Com.Atproto.Server.GetServiceAuth do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get a signed token on behalf of the requesting DID for the requested service.",
+
"errors" => [
+
%{
+
"description" =>
+
"Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.",
+
"name" => "BadExpiration"
+
}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"token" => %{"type" => "string"}},
+
"required" => ["token"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"aud" => %{
+
"description" =>
+
"The DID of the service that the token will be used to authenticate with",
+
"format" => "did",
+
"type" => "string"
+
},
+
"exp" => %{
+
"description" =>
+
"The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope.",
+
"type" => "integer"
+
},
+
"lxm" => %{
+
"description" => "Lexicon (XRPC) method to bind the requested token to",
+
"format" => "nsid",
+
"type" => "string"
+
}
+
},
+
"required" => ["aud"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.getServiceAuth",
+
"lexicon" => 1
+
})
+
end
+36
lib/atproto/com/atproto/server/getSession.ex
···
+
defmodule Com.Atproto.Server.GetSession do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Get information about the current auth session. Requires auth.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"active" => %{"type" => "boolean"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"didDoc" => %{"type" => "unknown"},
+
"email" => %{"type" => "string"},
+
"emailAuthFactor" => %{"type" => "boolean"},
+
"emailConfirmed" => %{"type" => "boolean"},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"status" => %{
+
"description" =>
+
"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+
"knownValues" => ["takendown", "suspended", "deactivated"],
+
"type" => "string"
+
}
+
},
+
"required" => ["handle", "did"],
+
"type" => "object"
+
}
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.getSession",
+
"lexicon" => 1
+
})
+
end
+37
lib/atproto/com/atproto/server/listAppPasswords.ex
···
+
defmodule Com.Atproto.Server.ListAppPasswords do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"appPassword" => %{
+
"properties" => %{
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
+
"name" => %{"type" => "string"},
+
"privileged" => %{"type" => "boolean"}
+
},
+
"required" => ["name", "createdAt"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" => "List all App Passwords.",
+
"errors" => [%{"name" => "AccountTakedown"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"passwords" => %{
+
"items" => %{"ref" => "#appPassword", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["passwords"],
+
"type" => "object"
+
}
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.server.listAppPasswords",
+
"lexicon" => 1
+
})
+
end
+37
lib/atproto/com/atproto/server/refreshSession.ex
···
+
defmodule Com.Atproto.Server.RefreshSession do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').",
+
"errors" => [%{"name" => "AccountTakedown"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"accessJwt" => %{"type" => "string"},
+
"active" => %{"type" => "boolean"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"didDoc" => %{"type" => "unknown"},
+
"handle" => %{"format" => "handle", "type" => "string"},
+
"refreshJwt" => %{"type" => "string"},
+
"status" => %{
+
"description" =>
+
"Hosting status of the account. If not specified, then assume 'active'.",
+
"knownValues" => ["takendown", "suspended", "deactivated"],
+
"type" => "string"
+
}
+
},
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.refreshSession",
+
"lexicon" => 1
+
})
+
end
+14
lib/atproto/com/atproto/server/requestAccountDelete.ex
···
+
defmodule Com.Atproto.Server.RequestAccountDelete do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Initiate a user account deletion via email.",
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.requestAccountDelete",
+
"lexicon" => 1
+
})
+
end
+14
lib/atproto/com/atproto/server/requestEmailConfirmation.ex
···
+
defmodule Com.Atproto.Server.RequestEmailConfirmation do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Request an email with a code to confirm ownership of email.",
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.requestEmailConfirmation",
+
"lexicon" => 1
+
})
+
end
+22
lib/atproto/com/atproto/server/requestEmailUpdate.ex
···
+
defmodule Com.Atproto.Server.RequestEmailUpdate do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Request a token in order to update email.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"tokenRequired" => %{"type" => "boolean"}},
+
"required" => ["tokenRequired"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.requestEmailUpdate",
+
"lexicon" => 1
+
})
+
end
+22
lib/atproto/com/atproto/server/requestPasswordReset.ex
···
+
defmodule Com.Atproto.Server.RequestPasswordReset do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Initiate a user account password reset via email.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"email" => %{"type" => "string"}},
+
"required" => ["email"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.requestPasswordReset",
+
"lexicon" => 1
+
})
+
end
+42
lib/atproto/com/atproto/server/reserveSigningKey.ex
···
+
defmodule Com.Atproto.Server.ReserveSigningKey do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID to reserve a key for.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"type" => "object"
+
}
+
},
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"signingKey" => %{
+
"description" =>
+
"The public key for the reserved signing key, in did:key serialization.",
+
"type" => "string"
+
}
+
},
+
"required" => ["signingKey"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.reserveSigningKey",
+
"lexicon" => 1
+
})
+
end
+26
lib/atproto/com/atproto/server/resetPassword.ex
···
+
defmodule Com.Atproto.Server.ResetPassword do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Reset a user account password using a token.",
+
"errors" => [%{"name" => "ExpiredToken"}, %{"name" => "InvalidToken"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"password" => %{"type" => "string"},
+
"token" => %{"type" => "string"}
+
},
+
"required" => ["token", "password"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.resetPassword",
+
"lexicon" => 1
+
})
+
end
+22
lib/atproto/com/atproto/server/revokeAppPassword.ex
···
+
defmodule Com.Atproto.Server.RevokeAppPassword do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Revoke an App Password by name.",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"name" => %{"type" => "string"}},
+
"required" => ["name"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.revokeAppPassword",
+
"lexicon" => 1
+
})
+
end
+35
lib/atproto/com/atproto/server/updateEmail.ex
···
+
defmodule Com.Atproto.Server.UpdateEmail do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "Update an account's email.",
+
"errors" => [
+
%{"name" => "ExpiredToken"},
+
%{"name" => "InvalidToken"},
+
%{"name" => "TokenRequired"}
+
],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"email" => %{"type" => "string"},
+
"emailAuthFactor" => %{"type" => "boolean"},
+
"token" => %{
+
"description" =>
+
"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
+
"type" => "string"
+
}
+
},
+
"required" => ["email"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.server.updateEmail",
+
"lexicon" => 1
+
})
+
end
+14
lib/atproto/com/atproto/sync/defs.ex
···
+
defmodule Com.Atproto.Sync.Defs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"hostStatus" => %{
+
"knownValues" => ["active", "idle", "offline", "throttled", "banned"],
+
"type" => "string"
+
}
+
},
+
"id" => "com.atproto.sync.defs",
+
"lexicon" => 1
+
})
+
end
+39
lib/atproto/com/atproto/sync/getBlob.ex
···
+
defmodule Com.Atproto.Sync.GetBlob do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.",
+
"errors" => [
+
%{"name" => "BlobNotFound"},
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{"encoding" => "*/*"},
+
"parameters" => %{
+
"properties" => %{
+
"cid" => %{
+
"description" => "The CID of the blob to fetch",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"did" => %{
+
"description" => "The DID of the account.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "cid"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getBlob",
+
"lexicon" => 1
+
})
+
end
+38
lib/atproto/com/atproto/sync/getBlocks.ex
···
+
defmodule Com.Atproto.Sync.GetBlocks do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.",
+
"errors" => [
+
%{"name" => "BlockNotFound"},
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
+
"parameters" => %{
+
"properties" => %{
+
"cids" => %{
+
"items" => %{"format" => "cid", "type" => "string"},
+
"type" => "array"
+
},
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "cids"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getBlocks",
+
"lexicon" => 1
+
})
+
end
+26
lib/atproto/com/atproto/sync/getCheckout.ex
···
+
defmodule Com.Atproto.Sync.GetCheckout do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "DEPRECATED - please use com.atproto.sync.getRepo instead",
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getCheckout",
+
"lexicon" => 1
+
})
+
end
+34
lib/atproto/com/atproto/sync/getHead.ex
···
+
defmodule Com.Atproto.Sync.GetHead do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" => "DEPRECATED - please use com.atproto.sync.getLatestCommit instead",
+
"errors" => [%{"name" => "HeadNotFound"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{"root" => %{"format" => "cid", "type" => "string"}},
+
"required" => ["root"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getHead",
+
"lexicon" => 1
+
})
+
end
+50
lib/atproto/com/atproto/sync/getHostStatus.ex
···
+
defmodule Com.Atproto.Sync.GetHostStatus do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Returns information about a specified upstream host, as consumed by the server. Implemented by relays.",
+
"errors" => [%{"name" => "HostNotFound"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"accountCount" => %{
+
"description" =>
+
"Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.",
+
"type" => "integer"
+
},
+
"hostname" => %{"type" => "string"},
+
"seq" => %{
+
"description" =>
+
"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).",
+
"type" => "integer"
+
},
+
"status" => %{
+
"ref" => "com.atproto.sync.defs#hostStatus",
+
"type" => "ref"
+
}
+
},
+
"required" => ["hostname"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"hostname" => %{
+
"description" => "Hostname of the host (eg, PDS or relay) being queried.",
+
"type" => "string"
+
}
+
},
+
"required" => ["hostname"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getHostStatus",
+
"lexicon" => 1
+
})
+
end
+43
lib/atproto/com/atproto/sync/getLatestCommit.ex
···
+
defmodule Com.Atproto.Sync.GetLatestCommit do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get the current commit CID & revision of the specified repo. Does not require auth.",
+
"errors" => [
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cid" => %{"format" => "cid", "type" => "string"},
+
"rev" => %{"format" => "tid", "type" => "string"}
+
},
+
"required" => ["cid", "rev"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getLatestCommit",
+
"lexicon" => 1
+
})
+
end
+40
lib/atproto/com/atproto/sync/getRecord.ex
···
+
defmodule Com.Atproto.Sync.GetRecord do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.",
+
"errors" => [
+
%{"name" => "RecordNotFound"},
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
+
"parameters" => %{
+
"properties" => %{
+
"collection" => %{"format" => "nsid", "type" => "string"},
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"rkey" => %{
+
"description" => "Record Key",
+
"format" => "record-key",
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "collection", "rkey"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getRecord",
+
"lexicon" => 1
+
})
+
end
+38
lib/atproto/com/atproto/sync/getRepo.ex
···
+
defmodule Com.Atproto.Sync.GetRepo do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
+
"errors" => [
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"since" => %{
+
"description" => "The revision ('rev') of the repo to create a diff from.",
+
"format" => "tid",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getRepo",
+
"lexicon" => 1
+
})
+
end
+56
lib/atproto/com/atproto/sync/getRepoStatus.ex
···
+
defmodule Com.Atproto.Sync.GetRepoStatus do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.",
+
"errors" => [%{"name" => "RepoNotFound"}],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"active" => %{"type" => "boolean"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"rev" => %{
+
"description" => "Optional field, the current rev of the repo, if active=true",
+
"format" => "tid",
+
"type" => "string"
+
},
+
"status" => %{
+
"description" =>
+
"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+
"knownValues" => [
+
"takendown",
+
"suspended",
+
"deleted",
+
"deactivated",
+
"desynchronized",
+
"throttled"
+
],
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "active"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.getRepoStatus",
+
"lexicon" => 1
+
})
+
end
+58
lib/atproto/com/atproto/sync/listBlobs.ex
···
+
defmodule Com.Atproto.Sync.ListBlobs do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.",
+
"errors" => [
+
%{"name" => "RepoNotFound"},
+
%{"name" => "RepoTakendown"},
+
%{"name" => "RepoSuspended"},
+
%{"name" => "RepoDeactivated"}
+
],
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cids" => %{
+
"items" => %{"format" => "cid", "type" => "string"},
+
"type" => "array"
+
},
+
"cursor" => %{"type" => "string"}
+
},
+
"required" => ["cids"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"did" => %{
+
"description" => "The DID of the repo.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"limit" => %{
+
"default" => 500,
+
"maximum" => 1000,
+
"minimum" => 1,
+
"type" => "integer"
+
},
+
"since" => %{
+
"description" => "Optional revision of the repo to list blobs since.",
+
"format" => "tid",
+
"type" => "string"
+
}
+
},
+
"required" => ["did"],
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.listBlobs",
+
"lexicon" => 1
+
})
+
end
+63
lib/atproto/com/atproto/sync/listHosts.ex
···
+
defmodule Com.Atproto.Sync.ListHosts do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"host" => %{
+
"properties" => %{
+
"accountCount" => %{"type" => "integer"},
+
"hostname" => %{
+
"description" => "hostname of server; not a URL (no scheme)",
+
"type" => "string"
+
},
+
"seq" => %{
+
"description" =>
+
"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).",
+
"type" => "integer"
+
},
+
"status" => %{
+
"ref" => "com.atproto.sync.defs#hostStatus",
+
"type" => "ref"
+
}
+
},
+
"required" => ["hostname"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" =>
+
"Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"hosts" => %{
+
"description" =>
+
"Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.",
+
"items" => %{"ref" => "#host", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["hosts"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 200,
+
"maximum" => 1000,
+
"minimum" => 1,
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
}
+
},
+
"id" => "com.atproto.sync.listHosts",
+
"lexicon" => 1
+
})
+
end
+68
lib/atproto/com/atproto/sync/listRepos.ex
···
+
defmodule Com.Atproto.Sync.ListRepos do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"repos" => %{
+
"items" => %{"ref" => "#repo", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["repos"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 500,
+
"maximum" => 1000,
+
"minimum" => 1,
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "query"
+
},
+
"repo" => %{
+
"properties" => %{
+
"active" => %{"type" => "boolean"},
+
"did" => %{"format" => "did", "type" => "string"},
+
"head" => %{
+
"description" => "Current repo commit CID",
+
"format" => "cid",
+
"type" => "string"
+
},
+
"rev" => %{"format" => "tid", "type" => "string"},
+
"status" => %{
+
"description" =>
+
"If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.",
+
"knownValues" => [
+
"takendown",
+
"suspended",
+
"deleted",
+
"deactivated",
+
"desynchronized",
+
"throttled"
+
],
+
"type" => "string"
+
}
+
},
+
"required" => ["did", "head", "rev"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.sync.listRepos",
+
"lexicon" => 1
+
})
+
end
+50
lib/atproto/com/atproto/sync/listReposByCollection.ex
···
+
defmodule Com.Atproto.Sync.ListReposByCollection do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Enumerates all the DIDs which have records with the given collection NSID.",
+
"output" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"cursor" => %{"type" => "string"},
+
"repos" => %{
+
"items" => %{"ref" => "#repo", "type" => "ref"},
+
"type" => "array"
+
}
+
},
+
"required" => ["repos"],
+
"type" => "object"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"collection" => %{"format" => "nsid", "type" => "string"},
+
"cursor" => %{"type" => "string"},
+
"limit" => %{
+
"default" => 500,
+
"description" =>
+
"Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.",
+
"maximum" => 2000,
+
"minimum" => 1,
+
"type" => "integer"
+
}
+
},
+
"required" => ["collection"],
+
"type" => "params"
+
},
+
"type" => "query"
+
},
+
"repo" => %{
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
+
"required" => ["did"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.sync.listReposByCollection",
+
"lexicon" => 1
+
})
+
end
+29
lib/atproto/com/atproto/sync/notifyOfUpdate.ex
···
+
defmodule Com.Atproto.Sync.NotifyOfUpdate do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl",
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"hostname" => %{
+
"description" =>
+
"Hostname of the current service (usually a PDS) that is notifying of update.",
+
"type" => "string"
+
}
+
},
+
"required" => ["hostname"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.sync.notifyOfUpdate",
+
"lexicon" => 1
+
})
+
end
+30
lib/atproto/com/atproto/sync/requestCrawl.ex
···
+
defmodule Com.Atproto.Sync.RequestCrawl do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"main" => %{
+
"description" =>
+
"Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.",
+
"errors" => [%{"name" => "HostBanned"}],
+
"input" => %{
+
"encoding" => "application/json",
+
"schema" => %{
+
"properties" => %{
+
"hostname" => %{
+
"description" =>
+
"Hostname of the current service (eg, PDS) that is requesting to be crawled.",
+
"type" => "string"
+
}
+
},
+
"required" => ["hostname"],
+
"type" => "object"
+
}
+
},
+
"type" => "procedure"
+
}
+
},
+
"id" => "com.atproto.sync.requestCrawl",
+
"lexicon" => 1
+
})
+
end
+237
lib/atproto/com/atproto/sync/subscribeRepos.ex
···
+
defmodule Com.Atproto.Sync.SubscribeRepos do
+
use Atex.Lexicon
+
+
deflexicon(%{
+
"defs" => %{
+
"account" => %{
+
"description" =>
+
"Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active.",
+
"properties" => %{
+
"active" => %{
+
"description" =>
+
"Indicates that the account has a repository which can be fetched from the host that emitted this event.",
+
"type" => "boolean"
+
},
+
"did" => %{"format" => "did", "type" => "string"},
+
"seq" => %{"type" => "integer"},
+
"status" => %{
+
"description" =>
+
"If active=false, this optional field indicates a reason for why the account is not active.",
+
"knownValues" => [
+
"takendown",
+
"suspended",
+
"deleted",
+
"deactivated",
+
"desynchronized",
+
"throttled"
+
],
+
"type" => "string"
+
},
+
"time" => %{"format" => "datetime", "type" => "string"}
+
},
+
"required" => ["seq", "did", "time", "active"],
+
"type" => "object"
+
},
+
"commit" => %{
+
"description" =>
+
"Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature.",
+
"nullable" => ["since"],
+
"properties" => %{
+
"blobs" => %{
+
"items" => %{
+
"description" =>
+
"DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.",
+
"type" => "cid-link"
+
},
+
"type" => "array"
+
},
+
"blocks" => %{
+
"description" =>
+
"CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list.",
+
"maxLength" => 2_000_000,
+
"type" => "bytes"
+
},
+
"commit" => %{
+
"description" => "Repo commit object CID.",
+
"type" => "cid-link"
+
},
+
"ops" => %{
+
"items" => %{
+
"description" =>
+
"List of repo mutation operations in this commit (eg, records created, updated, or deleted).",
+
"ref" => "#repoOp",
+
"type" => "ref"
+
},
+
"maxLength" => 200,
+
"type" => "array"
+
},
+
"prevData" => %{
+
"description" =>
+
"The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose.",
+
"type" => "cid-link"
+
},
+
"rebase" => %{
+
"description" => "DEPRECATED -- unused",
+
"type" => "boolean"
+
},
+
"repo" => %{
+
"description" =>
+
"The repo this event comes from. Note that all other message types name this field 'did'.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"rev" => %{
+
"description" =>
+
"The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event.",
+
"format" => "tid",
+
"type" => "string"
+
},
+
"seq" => %{
+
"description" => "The stream sequence number of this message.",
+
"type" => "integer"
+
},
+
"since" => %{
+
"description" => "The rev of the last emitted commit from this repo (if any).",
+
"format" => "tid",
+
"type" => "string"
+
},
+
"time" => %{
+
"description" => "Timestamp of when this message was originally broadcast.",
+
"format" => "datetime",
+
"type" => "string"
+
},
+
"tooBig" => %{
+
"description" =>
+
"DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data.",
+
"type" => "boolean"
+
}
+
},
+
"required" => [
+
"seq",
+
"rebase",
+
"tooBig",
+
"repo",
+
"commit",
+
"rev",
+
"since",
+
"blocks",
+
"ops",
+
"blobs",
+
"time"
+
],
+
"type" => "object"
+
},
+
"identity" => %{
+
"description" =>
+
"Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache.",
+
"properties" => %{
+
"did" => %{"format" => "did", "type" => "string"},
+
"handle" => %{
+
"description" =>
+
"The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details.",
+
"format" => "handle",
+
"type" => "string"
+
},
+
"seq" => %{"type" => "integer"},
+
"time" => %{"format" => "datetime", "type" => "string"}
+
},
+
"required" => ["seq", "did", "time"],
+
"type" => "object"
+
},
+
"info" => %{
+
"properties" => %{
+
"message" => %{"type" => "string"},
+
"name" => %{"knownValues" => ["OutdatedCursor"], "type" => "string"}
+
},
+
"required" => ["name"],
+
"type" => "object"
+
},
+
"main" => %{
+
"description" =>
+
"Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay.",
+
"errors" => [
+
%{"name" => "FutureCursor"},
+
%{
+
"description" =>
+
"If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.",
+
"name" => "ConsumerTooSlow"
+
}
+
],
+
"message" => %{
+
"schema" => %{
+
"refs" => ["#commit", "#sync", "#identity", "#account", "#info"],
+
"type" => "union"
+
}
+
},
+
"parameters" => %{
+
"properties" => %{
+
"cursor" => %{
+
"description" => "The last known event seq number to backfill from.",
+
"type" => "integer"
+
}
+
},
+
"type" => "params"
+
},
+
"type" => "subscription"
+
},
+
"repoOp" => %{
+
"description" => "A repo operation, ie a mutation of a single record.",
+
"nullable" => ["cid"],
+
"properties" => %{
+
"action" => %{
+
"knownValues" => ["create", "update", "delete"],
+
"type" => "string"
+
},
+
"cid" => %{
+
"description" => "For creates and updates, the new record CID. For deletions, null.",
+
"type" => "cid-link"
+
},
+
"path" => %{"type" => "string"},
+
"prev" => %{
+
"description" =>
+
"For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.",
+
"type" => "cid-link"
+
}
+
},
+
"required" => ["action", "path", "cid"],
+
"type" => "object"
+
},
+
"sync" => %{
+
"description" =>
+
"Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository.",
+
"properties" => %{
+
"blocks" => %{
+
"description" =>
+
"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.",
+
"maxLength" => 10000,
+
"type" => "bytes"
+
},
+
"did" => %{
+
"description" =>
+
"The account this repo event corresponds to. Must match that in the commit object.",
+
"format" => "did",
+
"type" => "string"
+
},
+
"rev" => %{
+
"description" =>
+
"The rev of the commit. This value must match that in the commit object.",
+
"type" => "string"
+
},
+
"seq" => %{
+
"description" => "The stream sequence number of this message.",
+
"type" => "integer"
+
},
+
"time" => %{
+
"description" => "Timestamp of when this message was originally broadcast.",
+
"format" => "datetime",
+
"type" => "string"
+
}
+
},
+
"required" => ["seq", "did", "blocks", "rev", "time"],
+
"type" => "object"
+
}
+
},
+
"id" => "com.atproto.sync.subscribeRepos",
+
"lexicon" => 1
+
})
+
end
+1 -1
lib/mix/tasks/atex.lexicons.ex
···
@aliases [o: :output]
@template_path Path.expand("../../../priv/templates/lexicon.eex", __DIR__)
-
@impl Mix.Task
+
@impl true
def run(args) do
{options, globs} = OptionParser.parse!(args, switches: @switches, aliases: @aliases)
+16 -4
mix.exs
···
defmodule Atex.MixProject do
use Mix.Project
-
@version "0.4.0"
+
@version "0.6.0"
@github "https://github.com/cometsh/atex"
@tangled "https://tangled.sh/@comet.sh/atex"
···
{:typedstruct, "~> 0.5"},
{:ex_cldr, "~> 2.42"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
-
{:ex_doc, "~> 0.34", only: :dev, runtime: false, warn_if_outdated: true}
+
{:ex_doc, "~> 0.34", only: :dev, runtime: false, warn_if_outdated: true},
+
{:plug, "~> 1.18"},
+
{:jason, "~> 1.4"},
+
{:jose, "~> 1.11"},
+
{:bandit, "~> 1.0", only: [:dev, :test]}
]
end
···
[
extras: [
LICENSE: [title: "License"],
-
"README.md": [title: "Overview"]
+
"README.md": [title: "Overview"],
+
"CHANGELOG.md": [title: "Changelog"]
],
main: "readme",
source_url: @github,
source_ref: "v#{@version}",
-
formatters: ["html"]
+
formatters: ["html"],
+
groups_for_modules: [
+
"Data types": [Atex.AtURI, Atex.DID, Atex.Handle, Atex.NSID, Atex.TID],
+
XRPC: ~r/^Atex\.XRPC/,
+
OAuth: [Atex.Config.OAuth, Atex.OAuth, Atex.OAuth.Plug],
+
Lexicons: ~r/^Atex\.Lexicon/,
+
Identity: ~r/^Atex\.IdentityResolver/
+
]
]
end
end
+8 -2
mix.lock
···
%{
+
"bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"},
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
"cldr_utils": {:hex, :cldr_utils, "2.28.3", "d0ac5ed25913349dfaca8b7fe14722d588d8ccfa3e335b0510c7cc3f3c54d4e6", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "40083cd9a5d187f12d675cfeeb39285f0d43e7b7f2143765161b72205d57ffb5"},
"credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"},
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
-
"ex_cldr": {:hex, :ex_cldr, "2.42.0", "17ea930e88b8802b330e1c1e288cdbaba52cbfafcccf371ed34b299a47101ffb", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "07264a7225810ecae6bdd6715d8800c037a1248dc0063923cddc4ca3c4888df6"},
+
"ex_cldr": {:hex, :ex_cldr, "2.43.0", "8700031e30a03501cf65f7ba7c8287bb67339d03559f3108f3c54fe86d926b19", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "1524eb01275b89473ee5f53fcc6169bae16e4a5267ef109229f37694799e0b20"},
"ex_doc": {:hex, :ex_doc, "0.38.3", "ddafe36b8e9fe101c093620879f6604f6254861a95133022101c08e75e6c759a", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "ecaa785456a67f63b4e7d7f200e8832fa108279e7eb73fd9928e7e66215a01f9"},
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
"finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"},
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
+
"jose": {:hex, :jose, "1.11.10", "a903f5227417bd2a08c8a00a0cbcc458118be84480955e8d251297a425723f83", [:mix, :rebar3], [], "hexpm", "0d6cd36ff8ba174db29148fc112b5842186b68a90ce9fc2b3ec3afe76593e614"},
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
"makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
···
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
"peri": {:hex, :peri, "0.6.1", "6a90ca728a27aef8fef37ce307444255d20364b0c8f8d39e52499d8d825cb514", [:mix], [{:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.1", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "e20ffc659967baf9c4f28799fe7302b656d6662a8b3db7646fdafd017e192743"},
+
"plug": {:hex, :plug, "1.18.1", "5067f26f7745b7e31bc3368bc1a2b818b9779faa959b49c934c17730efc911cf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "57a57db70df2b422b564437d2d33cf8d33cd16339c1edb190cd11b1a3a546cc2"},
+
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
"recase": {:hex, :recase, "0.9.0", "437982693fdfbec125f11c8868eb3b4d32e9aa6995d3a68ac8686f3e2bf5d8d1", [:mix], [], "hexpm", "efa7549ebd128988d1723037a6f6a61948055aec107db6288f1c52830cb6501c"},
"req": {:hex, :req, "0.5.15", "662020efb6ea60b9f0e0fac9be88cd7558b53fe51155a2d9899de594f9906ba9", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a6513a35fad65467893ced9785457e91693352c70b58bbc045b47e5eb2ef0c53"},
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
-
"typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"},
+
"thousand_island": {:hex, :thousand_island, "1.4.1", "8df065e627407e281f7935da5ad0f3842d10eb721afa92e760b720d71e2e37aa", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "204a8640e5d2818589b87286ae66160978628d7edf6095181cbe0440765fb6c1"},
+
"typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"},
"varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"},
+
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
}