+7
-2
.formatter.exs
+7
-2
.formatter.exs
···
1
1
# Used by "mix format"
2
2
[
3
-
inputs: ["{mix,.formatter,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"],
4
-
import_deps: [:typedstruct, :peri],
3
+
inputs:
4
+
Enum.flat_map(
5
+
["{mix,.formatter,.credo}.exs", "{config,examples,lib,test}/**/*.{ex,exs}"],
6
+
&Path.wildcard(&1, match_dot: true)
7
+
) -- Path.wildcard("lib/atproto/**/*.ex"),
8
+
import_deps: [:typedstruct, :peri, :plug],
9
+
# excludes: ["lib/atproto/**/*.ex"],
5
10
export: [
6
11
locals_without_parens: [deflexicon: 1]
7
12
]
+6
-16
.gitignore
+6
-16
.gitignore
···
1
-
# The directory Mix will write compiled artifacts to.
2
1
/_build/
3
-
4
-
# If you run "mix test --cover", coverage assets end up here.
5
2
/cover/
6
-
7
-
# The directory Mix downloads your dependencies sources to.
8
3
/deps/
9
-
10
-
# Where third-party dependencies like ExDoc output generated docs.
11
4
/doc/
12
-
13
-
# If the VM crashes, it generates a dump, let's ignore it too.
14
5
erl_crash.dump
15
-
16
-
# Also ignore archive artifacts (built via "mix archive.build").
17
6
*.ez
18
-
19
-
# Ignore package tarball (built via "mix hex.build").
20
7
atex-*.tar
21
-
22
-
# Temporary files, for example, from tests.
23
8
/tmp/
9
+
/priv/dets/
24
10
25
11
.envrc
26
12
.direnv
27
13
.vscode/
28
14
.elixir_ls
29
15
lexicons
30
-
lib/atproto
16
+
secrets
17
+
.DS_Store
18
+
CLAUDE.md
19
+
tmp
20
+
temp
+40
AGENTS.md
+40
AGENTS.md
···
1
+
# Agent Guidelines for atex
2
+
3
+
## Commands
4
+
5
+
- **Test**: `mix test` (all), `mix test test/path/to/file_test.exs` (single
6
+
file), `mix test test/path/to/file_test.exs:42` (single test at line)
7
+
- **Format**: `mix format` (auto-formats all code)
8
+
- **Lint**: `mix credo` (static analysis, TODO checks disabled)
9
+
- **Compile**: `mix compile`
10
+
- **Docs**: `mix docs`
11
+
12
+
## Code Style
13
+
14
+
- **Imports**: Use `alias` for modules (e.g.,
15
+
`alias Atex.Config.OAuth, as: Config`), import macros sparingly
16
+
- **Formatting**: Elixir 1.18+, auto-formatted via `.formatter.exs` with
17
+
`import_deps: [:typedstruct, :peri, :plug]`
18
+
- **Naming**: snake_case for functions/variables, PascalCase for modules,
19
+
descriptive names (e.g., `authorization_metadata`, not `auth_meta`)
20
+
- **Types**: Use `@type` and `@spec` for all public functions; leverage
21
+
TypedStruct for structs
22
+
- **Moduledocs**: All public modules need `@moduledoc`, public functions need
23
+
`@doc` with examples
24
+
- When writing lists in documentation, use `-` as the list character.
25
+
- **Error Handling**: Return `{:ok, result}` or `{:error, reason}` tuples; use
26
+
pattern matching in case statements
27
+
- **Pattern Matching**: Prefer pattern matching over conditionals; use guards
28
+
when appropriate
29
+
- **Macros**: Use `deflexicon` macro for lexicon definitions; use `defschema`
30
+
(from Peri) for validation schemas
31
+
- **Tests**: Async by default (`use ExUnit.Case, async: true`), use doctests
32
+
where applicable
33
+
- **Dependencies**: Core deps include Peri (validation), Req (HTTP), JOSE
34
+
(JWT/OAuth), TypedStruct (structs)
35
+
36
+
## Important Notes
37
+
38
+
- **DO NOT modify** `lib/atproto/**/` - autogenerated from official AT Protocol
39
+
lexicons
40
+
- **Update CHANGELOG.md** when adding features, changes, or fixes
+82
-1
CHANGELOG.md
+82
-1
CHANGELOG.md
···
8
8
9
9
<!-- ## [Unreleased] -->
10
10
11
+
## [0.7.0] - 2026-01-07
12
+
13
+
### Breaking Changes
14
+
15
+
- `Atex.OAuth.Plug` now raises `Atex.OAuth.Error` exceptions instead of handling
16
+
error situations internally. Applications should implement `Plug.ErrorHandler`
17
+
to catch and gracefully handle them.
18
+
- `Atex.OAuth.Plug` now saves only the user's DID in the session instead of the
19
+
entire OAuth session object. Applications must use `Atex.OAuth.SessionStore`
20
+
to manage OAuth sessions.
21
+
- `Atex.XRPC.OAuthClient` has been overhauled to use `Atex.OAuth.SessionStore`
22
+
for retrieving and managing OAuth sessions, making it easier to use with not
23
+
needing to manually keep a Plug session in sync.
24
+
25
+
### Added
26
+
27
+
- `Atex.OAuth.SessionStore` behaviour and `Atex.OAuth.Session` struct for
28
+
managing OAuth sessions with pluggable storage backends.
29
+
- `Atex.OAuth.SessionStore.ETS` - in-memory session store implementation.
30
+
- `Atex.OAuth.SessionStore.DETS` - persistent disk-based session store
31
+
implementation.
32
+
- `Atex.OAuth.Plug` now requires a `:callback` option that is a MFA tuple
33
+
(Module, Function, Args), denoting a callback function to be invoked by after
34
+
a successful OAuth login. See [the OAuth example](./examples/oauth.ex) for a
35
+
simple usage of this.
36
+
- `Atex.OAuth.Permission` module for creating
37
+
[AT Protocol permission](https://atproto.com/specs/permission) strings for
38
+
OAuth.
39
+
- `Atex.OAuth.Error` exception module for OAuth flow errors. Contains both a
40
+
human-readable `message` string and a machine-readable `reason` atom for error
41
+
handling.
42
+
- `Atex.OAuth.Cache` module provides TTL caching for OAuth authorization server
43
+
metadata with a 1-hour default TTL to reduce load on third-party PDSs.
44
+
- `Atex.OAuth.get_authorization_server/2` and
45
+
`Atex.OAuth.get_authorization_server_metadata/2` now support an optional
46
+
`fresh` parameter to bypass the cache when needed.
47
+
48
+
### Changed
49
+
50
+
- `mix atex.lexicons` now adds `@moduledoc false` to generated modules to stop
51
+
them from automatically cluttering documentation.
52
+
- `Atex.IdentityResolver.Cache.ETS` now uses ConCache instead of ETS directly,
53
+
with a 1-hour TTL for cached identity information.
54
+
55
+
## [0.6.0] - 2025-11-25
56
+
57
+
### Breaking Changes
58
+
59
+
- `deflexicon` now converts all def names to be in snake_case instead of the
60
+
casing as written the lexicon.
61
+
62
+
### Added
63
+
64
+
- `deflexicon` now emits structs for records, objects, queries, and procedures.
65
+
- `Atex.XRPC.get/3` and `Atex.XRPC.post/3` now support having a lexicon struct
66
+
as the second argument instead of the method's name, making it easier to have
67
+
properly checked XRPC calls.
68
+
- Add pre-transpiled modules for the core `com.atproto` lexicons.
69
+
70
+
## [0.5.0] - 2025-10-11
71
+
72
+
### Breaking Changes
73
+
74
+
- Remove `Atex.HTTP` and associated modules as the abstraction caused a bit too
75
+
much complexities for how early atex is. It may come back in the future as
76
+
something more fleshed out once we're more stable.
77
+
- Rename `Atex.XRPC.Client` to `Atex.XRPC.LoginClient`
78
+
79
+
### Added
80
+
81
+
- `Atex.OAuth` module with utilites for handling some OAuth functionality.
82
+
- `Atex.OAuth.Plug` module (if Plug is loaded) which provides a basic but
83
+
complete OAuth flow, including storing the tokens in `Plug.Session`.
84
+
- `Atex.XRPC.Client` behaviour for implementing custom client variants.
85
+
- `Atex.XRPC` now supports using different client implementations.
86
+
- `Atex.XRPC.OAuthClient` to make XRPC calls on the behalf of a user who has
87
+
authenticated with ATProto OAuth.
88
+
11
89
## [0.4.0] - 2025-08-27
12
90
13
91
### Added
···
46
124
47
125
Initial release.
48
126
49
-
[unreleased]: https://github.com/cometsh/atex/compare/v0.4.0...HEAD
127
+
[unreleased]: https://github.com/cometsh/atex/compare/v0.7.0...HEAD
128
+
[0.7.0]: https://github.com/cometsh/atex/releases/tag/v0.7.0
129
+
[0.6.0]: https://github.com/cometsh/atex/releases/tag/v0.6.0
130
+
[0.5.0]: https://github.com/cometsh/atex/releases/tag/v0.5.0
50
131
[0.4.0]: https://github.com/cometsh/atex/releases/tag/v0.4.0
51
132
[0.3.0]: https://github.com/cometsh/atex/releases/tag/v0.3.0
52
133
[0.2.0]: https://github.com/cometsh/atex/releases/tag/v0.2.0
+5
-3
README.md
+5
-3
README.md
···
11
11
- [x] DID & handle resolution service with a cache
12
12
- [x] Macro for converting a Lexicon definition into a runtime-validation schema
13
13
- [x] Codegen to convert a directory of lexicons
14
-
- [ ] Extended XRPC client with support for validated inputs/outputs
15
-
- [ ] Oauth stuff
14
+
- [x] Oauth stuff
15
+
- [x] Extended XRPC client with support for validated inputs/outputs
16
+
- [ ] Proper MST & CAR handling things
17
+
- [ ] Pre-transpiled libraries for popular lexicons
16
18
17
19
## Installation
18
20
···
21
23
```elixir
22
24
def deps do
23
25
[
24
-
{:atex, "~> 0.3"}
26
+
{:atex, "~> 0.7"}
25
27
]
26
28
end
27
29
```
+11
bump_builtin_lexicons.sh
+11
bump_builtin_lexicons.sh
···
1
+
#!/usr/bin/env bash
2
+
3
+
set -euo pipefail
4
+
5
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+
cd "$SCRIPT_DIR"
7
+
8
+
mkdir -p ./tmp
9
+
git clone --depth 1 --single-branch https://github.com/bluesky-social/atproto.git ./tmp/atproto
10
+
mix atex.lexicons ./tmp/atproto/lexicons/com/atproto/**/*.json
11
+
rm -rf ./tmp
+10
config/runtime.exs
+10
config/runtime.exs
···
1
+
import Config
2
+
3
+
config :atex, Atex.OAuth,
4
+
# base_url: "https://comet.sh/aaaa",
5
+
base_url: "http://127.0.0.1:4000/oauth",
6
+
is_localhost: true,
7
+
scopes: ~w(transition:generic),
8
+
private_key:
9
+
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgyIpxhuDm0i3mPkrk6UdX4Sd9Jsv6YtAmSTza+A2nArShRANCAAQLF1GLueOBZOVnKWfrcnoDOO9NSRqH2utmfGMz+Rce18MDB7Z6CwFWjEq2UFYNBI4MI5cMI0+m+UYAmj4OZm+m",
10
+
key_id: "awooga"
+113
examples/oauth.ex
+113
examples/oauth.ex
···
1
+
defmodule ExampleOAuthPlug do
2
+
require Logger
3
+
use Plug.Router
4
+
use Plug.ErrorHandler
5
+
alias Atex.OAuth
6
+
alias Atex.XRPC
7
+
8
+
plug :put_secret_key_base
9
+
10
+
plug Plug.Session,
11
+
store: :cookie,
12
+
key: "atex-oauth",
13
+
signing_salt: "signing-salt"
14
+
15
+
plug :match
16
+
plug :dispatch
17
+
18
+
forward "/oauth", to: Atex.OAuth.Plug, init_opts: [callback: {__MODULE__, :oauth_callback, []}]
19
+
20
+
def oauth_callback(conn) do
21
+
IO.inspect(conn, label: "callback from oauth!")
22
+
23
+
conn
24
+
|> put_resp_header("Location", "/whoami")
25
+
|> resp(307, "")
26
+
|> send_resp()
27
+
end
28
+
29
+
get "/whoami" do
30
+
conn = fetch_session(conn)
31
+
32
+
case XRPC.OAuthClient.from_conn(conn) do
33
+
{:ok, client} ->
34
+
send_resp(conn, 200, "hello #{client.did}")
35
+
36
+
:error ->
37
+
send_resp(conn, 401, "Unauthorized")
38
+
end
39
+
end
40
+
41
+
get "/create-post" do
42
+
conn = fetch_session(conn)
43
+
44
+
with {:ok, client} <- XRPC.OAuthClient.from_conn(conn),
45
+
{:ok, response, client} <-
46
+
XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
47
+
input: %Com.Atproto.Repo.CreateRecord.Input{
48
+
repo: client.did,
49
+
collection: "app.bsky.feed.post",
50
+
rkey: Atex.TID.now() |> to_string(),
51
+
record: %{
52
+
"$type": "app.bsky.feed.post",
53
+
text: "Hello world from atex!",
54
+
createdAt: DateTime.to_iso8601(DateTime.utc_now())
55
+
}
56
+
}
57
+
}) do
58
+
IO.inspect(response, label: "output")
59
+
60
+
send_resp(conn, 200, response.body.uri)
61
+
else
62
+
:error ->
63
+
send_resp(conn, 401, "Unauthorized")
64
+
65
+
{:error, :reauth} ->
66
+
send_resp(conn, 401, "session expired but still in your cookie")
67
+
68
+
err ->
69
+
IO.inspect(err, label: "xrpc failed")
70
+
send_resp(conn, 500, "xrpc failed")
71
+
end
72
+
end
73
+
74
+
match _ do
75
+
send_resp(conn, 404, "oops")
76
+
end
77
+
78
+
def put_secret_key_base(conn, _) do
79
+
put_in(
80
+
conn.secret_key_base,
81
+
# Don't use this in production
82
+
"5ef1078e1617463a3eb3feb9b152e76587a75a6809e0485a125b6bb7ae468f086680771f700d77ff61dfdc8d8ee8a5c7848024a41cf5ad4b6eb3115f74ce6e46"
83
+
)
84
+
end
85
+
86
+
# Error handler for OAuth exceptions
87
+
@impl Plug.ErrorHandler
88
+
def handle_errors(conn, %{kind: :error, reason: %Atex.OAuth.Error{} = error, stack: _stack}) do
89
+
status =
90
+
case error.reason do
91
+
reason
92
+
when reason in [
93
+
:missing_handle,
94
+
:invalid_handle,
95
+
:invalid_callback_request,
96
+
:issuer_mismatch
97
+
] ->
98
+
400
99
+
100
+
_ ->
101
+
500
102
+
end
103
+
104
+
conn
105
+
|> put_resp_content_type("text/plain")
106
+
|> send_resp(status, error.message)
107
+
end
108
+
109
+
# Fallback for other errors
110
+
def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
111
+
send_resp(conn, conn.status, "Something went wrong")
112
+
end
113
+
end
+3
-3
flake.lock
+3
-3
flake.lock
···
2
2
"nodes": {
3
3
"nixpkgs": {
4
4
"locked": {
5
-
"lastModified": 1755615617,
6
-
"narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
5
+
"lastModified": 1767379071,
6
+
"narHash": "sha256-EgE0pxsrW9jp9YFMkHL9JMXxcqi/OoumPJYwf+Okucw=",
7
7
"owner": "nixos",
8
8
"repo": "nixpkgs",
9
-
"rev": "20075955deac2583bb12f07151c2df830ef346b4",
9
+
"rev": "fb7944c166a3b630f177938e478f0378e64ce108",
10
10
"type": "github"
11
11
},
12
12
"original": {
+7
-1
lib/atex/application.ex
+7
-1
lib/atex/application.ex
···
4
4
use Application
5
5
6
6
def start(_type, _args) do
7
-
children = [Atex.IdentityResolver.Cache]
7
+
children = [
8
+
Atex.IdentityResolver.Cache,
9
+
Atex.OAuth.Cache,
10
+
Atex.OAuth.SessionStore,
11
+
{Mutex, name: Atex.SessionMutex}
12
+
]
13
+
8
14
Supervisor.start_link(children, strategy: :one_for_one)
9
15
end
10
16
end
+86
lib/atex/config/oauth.ex
+86
lib/atex/config/oauth.ex
···
1
+
defmodule Atex.Config.OAuth do
2
+
@moduledoc """
3
+
Configuration management for `Atex.OAuth`.
4
+
5
+
Contains all the logic for fetching configuration needed for the OAuth
6
+
module, as well as deriving useful values from them.
7
+
8
+
## Configuration
9
+
10
+
The following structure is expected in your application config:
11
+
12
+
config :atex, Atex.OAuth,
13
+
base_url: "https://example.com/oauth", # Your application's base URL, including the path `Atex.OAuth` is mounted on.
14
+
private_key: "base64-encoded-private-key", # ES256 private key
15
+
key_id: "your-key-id", # Key identifier for JWTs
16
+
scopes: ["transition:generic", "transition:email"], # Optional additional scopes
17
+
extra_redirect_uris: ["https://alternative.com/callback"], # Optional additional redirect URIs
18
+
is_localhost: false # Set to true for local development
19
+
"""
20
+
21
+
@doc """
22
+
Returns the configured public base URL for OAuth routes.
23
+
"""
24
+
@spec base_url() :: String.t()
25
+
def base_url, do: Application.fetch_env!(:atex, Atex.OAuth)[:base_url]
26
+
27
+
@doc """
28
+
Returns the configured private key as a `JOSE.JWK`.
29
+
"""
30
+
@spec get_key() :: JOSE.JWK.t()
31
+
def get_key() do
32
+
private_key =
33
+
Application.fetch_env!(:atex, Atex.OAuth)[:private_key]
34
+
|> Base.decode64!()
35
+
|> JOSE.JWK.from_der()
36
+
37
+
key_id = Application.fetch_env!(:atex, Atex.OAuth)[:key_id]
38
+
39
+
%{private_key | fields: %{"kid" => key_id}}
40
+
end
41
+
42
+
@doc """
43
+
Returns the client ID based on configuration.
44
+
45
+
If `is_localhost` is set, it'll be a string handling the "http://localhost"
46
+
special case, with the redirect URI and scopes configured, otherwise it is a
47
+
string pointing to the location of the `client-metadata.json` route.
48
+
"""
49
+
@spec client_id() :: String.t()
50
+
def client_id() do
51
+
is_localhost = Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :is_localhost, false)
52
+
53
+
if is_localhost do
54
+
query =
55
+
%{redirect_uri: redirect_uri(), scope: scopes()}
56
+
|> URI.encode_query()
57
+
58
+
"http://localhost?#{query}"
59
+
else
60
+
"#{base_url()}/client-metadata.json"
61
+
end
62
+
end
63
+
64
+
@doc """
65
+
Returns the configured redirect URI.
66
+
"""
67
+
@spec redirect_uri() :: String.t()
68
+
def redirect_uri(), do: "#{base_url()}/callback"
69
+
70
+
@doc """
71
+
Returns the configured scopes joined as a space-separated string.
72
+
"""
73
+
@spec scopes() :: String.t()
74
+
def scopes() do
75
+
config_scopes = Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :scopes, [])
76
+
Enum.join(["atproto" | config_scopes], " ")
77
+
end
78
+
79
+
@doc """
80
+
Returns the configured extra redirect URIs.
81
+
"""
82
+
@spec extra_redirect_uris() :: [String.t()]
83
+
def extra_redirect_uris() do
84
+
Keyword.get(Application.get_env(:atex, Atex.OAuth, []), :extra_redirect_uris, [])
85
+
end
86
+
end
-38
lib/atex/http/adapter/req.ex
-38
lib/atex/http/adapter/req.ex
···
1
-
defmodule Atex.HTTP.Adapter.Req do
2
-
@moduledoc """
3
-
`Req` adapter for atex.
4
-
"""
5
-
6
-
@behaviour Atex.HTTP.Adapter
7
-
8
-
@impl true
9
-
def get(url, opts) do
10
-
Req.get(url, opts) |> adapt()
11
-
end
12
-
13
-
@impl true
14
-
def post(url, opts) do
15
-
Req.post(url, opts) |> adapt()
16
-
end
17
-
18
-
@spec adapt({:ok, Req.Response.t()} | {:error, any()}) :: Atex.HTTP.Adapter.result()
19
-
defp adapt({:ok, %Req.Response{status: status} = res}) when status < 400 do
20
-
{:ok, to_response(res)}
21
-
end
22
-
23
-
defp adapt({:ok, %Req.Response{} = res}) do
24
-
{:error, to_response(res)}
25
-
end
26
-
27
-
defp adapt({:error, exception}) do
28
-
{:error, exception}
29
-
end
30
-
31
-
defp to_response(%Req.Response{} = res) do
32
-
%Atex.HTTP.Response{
33
-
body: res.body,
34
-
status: res.status,
35
-
__raw__: res
36
-
}
37
-
end
38
-
end
-13
lib/atex/http/adapter.ex
-13
lib/atex/http/adapter.ex
···
1
-
defmodule Atex.HTTP.Adapter do
2
-
@moduledoc """
3
-
Behaviour for defining a HTTP client adapter to be used within atex.
4
-
"""
5
-
alias Atex.HTTP.Response
6
-
7
-
@type success() :: {:ok, Response.t()}
8
-
@type error() :: {:error, Response.t() | term()}
9
-
@type result() :: success() | error()
10
-
11
-
@callback get(url :: String.t(), opts :: keyword()) :: result()
12
-
@callback post(url :: String.t(), opts :: keyword()) :: result()
13
-
end
-13
lib/atex/http/response.ex
-13
lib/atex/http/response.ex
-6
lib/atex/http.ex
-6
lib/atex/http.ex
+35
-22
lib/atex/identity_resolver/cache/ets.ex
+35
-22
lib/atex/identity_resolver/cache/ets.ex
···
1
1
defmodule Atex.IdentityResolver.Cache.ETS do
2
+
@moduledoc """
3
+
ConCache-based implementation for Identity Resolver caching.
4
+
5
+
Stores identity information (DID and handle mappings) with a 1-hour TTL.
6
+
Uses two separate cache entries per identity to allow lookups by either DID or handle.
7
+
"""
8
+
2
9
alias Atex.IdentityResolver.Identity
3
10
@behaviour Atex.IdentityResolver.Cache
4
11
use Supervisor
5
12
6
-
@table :atex_identities
13
+
@cache :atex_identities_cache
14
+
@ttl_ms :timer.hours(1)
7
15
8
16
def start_link(opts) do
9
-
Supervisor.start_link(__MODULE__, opts)
17
+
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
10
18
end
11
19
12
20
@impl Supervisor
13
21
def init(_opts) do
14
-
:ets.new(@table, [:set, :public, :named_table])
15
-
Supervisor.init([], strategy: :one_for_one)
22
+
children = [
23
+
{ConCache,
24
+
[
25
+
name: @cache,
26
+
ttl_check_interval: :timer.minutes(5),
27
+
global_ttl: @ttl_ms
28
+
]}
29
+
]
30
+
31
+
Supervisor.init(children, strategy: :one_for_one)
16
32
end
17
33
18
34
@impl Atex.IdentityResolver.Cache
19
35
@spec insert(Identity.t()) :: Identity.t()
20
36
def insert(identity) do
21
-
# TODO: benchmark lookups vs match performance, is it better to use a "composite" key or two inserts?
22
-
:ets.insert(@table, {{identity.did, identity.handle}, identity})
37
+
ConCache.put(@cache, {:did, identity.did}, identity)
38
+
ConCache.put(@cache, {:handle, identity.handle}, identity)
23
39
identity
24
40
end
25
41
26
42
@impl Atex.IdentityResolver.Cache
27
43
@spec get(String.t()) :: {:ok, Identity.t()} | {:error, atom()}
28
44
def get(identifier) do
29
-
lookup(identifier)
45
+
case ConCache.get(@cache, {:did, identifier}) do
46
+
nil ->
47
+
case ConCache.get(@cache, {:handle, identifier}) do
48
+
nil -> {:error, :not_found}
49
+
identity -> {:ok, identity}
50
+
end
51
+
52
+
identity ->
53
+
{:ok, identity}
54
+
end
30
55
end
31
56
32
57
@impl Atex.IdentityResolver.Cache
33
58
@spec delete(String.t()) :: :noop | Identity.t()
34
59
def delete(identifier) do
35
-
case lookup(identifier) do
60
+
case get(identifier) do
36
61
{:ok, identity} ->
37
-
:ets.delete(@table, {identity.did, identity.handle})
62
+
ConCache.delete(@cache, {:did, identity.did})
63
+
ConCache.delete(@cache, {:handle, identity.handle})
38
64
identity
39
65
40
66
_ ->
41
67
:noop
42
-
end
43
-
end
44
-
45
-
defp lookup(identifier) do
46
-
case :ets.match(@table, {{identifier, :_}, :"$1"}) do
47
-
[] ->
48
-
case :ets.match(@table, {{:_, identifier}, :"$1"}) do
49
-
[] -> {:error, :not_found}
50
-
[[identity]] -> {:ok, identity}
51
-
end
52
-
53
-
[[identity]] ->
54
-
{:ok, identity}
55
68
end
56
69
end
57
70
end
+2
-2
lib/atex/identity_resolver/did.ex
+2
-2
lib/atex/identity_resolver/did.ex
···
14
14
@spec resolve_plc(String.t()) :: resolution_result()
15
15
defp resolve_plc("did:plc:" <> _id = did) do
16
16
with {:ok, resp} when resp.status in 200..299 <-
17
-
Atex.HTTP.get("https://plc.directory/#{did}", []),
17
+
Req.get("https://plc.directory/#{did}"),
18
18
{:ok, body} <- decode_body(resp.body),
19
19
{:ok, document} <- DIDDocument.from_json(body),
20
20
:ok <- DIDDocument.validate_for_atproto(document, did) do
···
29
29
@spec resolve_web(String.t()) :: resolution_result()
30
30
defp resolve_web("did:web:" <> domain = did) do
31
31
with {:ok, resp} when resp.status in 200..299 <-
32
-
Atex.HTTP.get("https://#{domain}/.well-known/did.json", []),
32
+
Req.get("https://#{domain}/.well-known/did.json"),
33
33
{:ok, body} <- decode_body(resp.body),
34
34
{:ok, document} <- DIDDocument.from_json(body),
35
35
:ok <- DIDDocument.validate_for_atproto(document, did) do
+13
lib/atex/identity_resolver/did_document.ex
+13
lib/atex/identity_resolver/did_document.ex
···
125
125
end)
126
126
end
127
127
128
+
@spec get_pds_endpoint(t()) :: String.t() | nil
129
+
def get_pds_endpoint(%__MODULE__{} = doc) do
130
+
doc.service
131
+
|> Enum.find(fn
132
+
%{id: "#atproto_pds", type: "AtprotoPersonalDataServer"} -> true
133
+
_ -> false
134
+
end)
135
+
|> case do
136
+
nil -> nil
137
+
pds -> pds.service_endpoint
138
+
end
139
+
end
140
+
128
141
defp valid_pds_endpoint?(endpoint) do
129
142
case URI.new(endpoint) do
130
143
{:ok, uri} ->
+1
-1
lib/atex/identity_resolver/handle.ex
+1
-1
lib/atex/identity_resolver/handle.ex
···
55
55
56
56
@spec resolve_via_http(String.t()) :: {:ok, String.t()} | :error
57
57
defp resolve_via_http(handle) do
58
-
case Atex.HTTP.get("https://#{handle}/.well-known/atproto-did", []) do
58
+
case Req.get("https://#{handle}/.well-known/atproto-did") do
59
59
{:ok, %{body: "did:" <> _ = did}} -> {:ok, did}
60
60
_ -> :error
61
61
end
+2
-2
lib/atex/lexicon/validators/array.ex
+2
-2
lib/atex/lexicon/validators/array.ex
···
4
4
@option_keys [:min_length, :max_length]
5
5
6
6
# Needs type input
7
-
@spec validate(Peri.schema_def(), term(), list(option())) :: Peri.validation_result()
8
-
def validate(inner_type, value, options) when is_list(value) do
7
+
@spec validate(term(), Peri.schema_def(), list(option())) :: Peri.validation_result()
8
+
def validate(value, inner_type, options) when is_list(value) do
9
9
# TODO: validate inner_type with Peri to make sure it's correct?
10
10
11
11
options
+13
-44
lib/atex/lexicon/validators/string.ex
+13
-44
lib/atex/lexicon/validators/string.ex
···
1
1
defmodule Atex.Lexicon.Validators.String do
2
2
alias Atex.Lexicon.Validators
3
3
4
-
@type format() ::
5
-
:at_identifier
6
-
| :at_uri
7
-
| :cid
8
-
| :datetime
9
-
| :did
10
-
| :handle
11
-
| :nsid
12
-
| :tid
13
-
| :record_key
14
-
| :uri
15
-
| :language
16
-
17
4
@type option() ::
18
-
{:format, format()}
5
+
{:format, String.t()}
19
6
| {:min_length, non_neg_integer()}
20
7
| {:max_length, non_neg_integer()}
21
8
| {:min_graphemes, non_neg_integer()}
···
31
18
32
19
@record_key_re ~r"^[a-zA-Z0-9.-_:~]$"
33
20
34
-
# TODO: probably should go into a different module, one with general lexicon -> validator gen conversions
35
-
@spec format_to_atom(String.t()) :: format()
36
-
def format_to_atom(format) do
37
-
case format do
38
-
"at-identifier" -> :at_identifier
39
-
"at-uri" -> :at_uri
40
-
"cid" -> :cid
41
-
"datetime" -> :datetime
42
-
"did" -> :did
43
-
"handle" -> :handle
44
-
"nsid" -> :nsid
45
-
"tid" -> :tid
46
-
"record-key" -> :record_key
47
-
"uri" -> :uri
48
-
"language" -> :language
49
-
_ -> raise "Unknown lexicon string format `#{format}`"
50
-
end
51
-
end
52
-
53
21
@spec validate(term(), list(option())) :: Peri.validation_result()
54
22
def validate(value, options) when is_binary(value) do
55
23
options
···
74
42
75
43
defp validate_option(_value, {option, nil}) when option in @option_keys, do: :ok
76
44
77
-
defp validate_option(value, {:format, :at_identifier}),
45
+
defp validate_option(value, {:format, "at-identifier"}),
78
46
do:
79
47
Validators.boolean_validate(
80
48
Atex.DID.match?(value) or Atex.Handle.match?(value),
81
49
"should be a valid DID or handle"
82
50
)
83
51
84
-
defp validate_option(value, {:format, :at_uri}),
52
+
defp validate_option(value, {:format, "at-uri"}),
85
53
do: Validators.boolean_validate(Atex.AtURI.match?(value), "should be a valid at:// URI")
86
54
87
-
defp validate_option(value, {:format, :cid}) do
55
+
defp validate_option(value, {:format, "cid"}) do
88
56
# TODO: is there a regex provided by the lexicon docs/somewhere?
89
57
try do
90
58
Multiformats.CID.decode(value)
59
+
:ok
91
60
rescue
92
61
_ -> {:error, "should be a valid CID", []}
93
62
end
94
63
end
95
64
96
-
defp validate_option(value, {:format, :datetime}) do
65
+
defp validate_option(value, {:format, "datetime"}) do
97
66
# NaiveDateTime is used over DateTime because the result isn't actually
98
67
# being used, so we don't need to include a calendar library just for this.
99
68
case NaiveDateTime.from_iso8601(value) do
···
102
71
end
103
72
end
104
73
105
-
defp validate_option(value, {:format, :did}),
74
+
defp validate_option(value, {:format, "did"}),
106
75
do: Validators.boolean_validate(Atex.DID.match?(value), "should be a valid DID")
107
76
108
-
defp validate_option(value, {:format, :handle}),
77
+
defp validate_option(value, {:format, "handle"}),
109
78
do: Validators.boolean_validate(Atex.Handle.match?(value), "should be a valid handle")
110
79
111
-
defp validate_option(value, {:format, :nsid}),
80
+
defp validate_option(value, {:format, "nsid"}),
112
81
do: Validators.boolean_validate(Atex.NSID.match?(value), "should be a valid NSID")
113
82
114
-
defp validate_option(value, {:format, :tid}),
83
+
defp validate_option(value, {:format, "tid"}),
115
84
do: Validators.boolean_validate(Atex.TID.match?(value), "should be a valid TID")
116
85
117
-
defp validate_option(value, {:format, :record_key}),
86
+
defp validate_option(value, {:format, "record-key"}),
118
87
do:
119
88
Validators.boolean_validate(
120
89
Regex.match?(@record_key_re, value),
121
90
"should be a valid record key"
122
91
)
123
92
124
-
defp validate_option(value, {:format, :uri}) do
93
+
defp validate_option(value, {:format, "uri"}) do
125
94
case URI.new(value) do
126
95
{:ok, _} -> :ok
127
96
{:error, _} -> {:error, "should be a valid URI", []}
128
97
end
129
98
end
130
99
131
-
defp validate_option(value, {:format, :language}) do
100
+
defp validate_option(value, {:format, "language"}) do
132
101
case Cldr.LanguageTag.parse(value) do
133
102
{:ok, _} -> :ok
134
103
{:error, _} -> {:error, "should be a valid BCP 47 language tag", []}
+5
lib/atex/lexicon/validators.ex
+5
lib/atex/lexicon/validators.ex
···
81
81
}
82
82
end
83
83
84
+
@spec lazy_ref(module(), atom()) :: Peri.schema()
85
+
def lazy_ref(module, schema_name) do
86
+
{:custom, {module, schema_name, []}}
87
+
end
88
+
84
89
@spec boolean_validate(boolean(), String.t(), keyword() | map()) ::
85
90
Peri.validation_result()
86
91
def boolean_validate(success?, error_message, context \\ []) do
+307
-59
lib/atex/lexicon.ex
+307
-59
lib/atex/lexicon.ex
···
1
1
defmodule Atex.Lexicon do
2
-
@moduledoc """
3
-
Provide `deflexicon` macro for defining a module with types and schemas from an entire lexicon definition.
4
-
5
-
Should it also define structs, with functions to convert from input case to snake case?
6
-
"""
7
-
8
2
alias Atex.Lexicon.Validators
9
3
10
4
defmacro __using__(_opts) do
···
15
9
end
16
10
end
17
11
12
+
@doc """
13
+
Defines a lexicon module from a JSON lexicon definition.
14
+
15
+
The `deflexicon` macro processes the provided lexicon map (typically loaded
16
+
from a JSON file) and generates:
17
+
18
+
- **Typespecs** for each definition, exposing a `t/0` type for the main
19
+
definition and named types for any additional definitions.
20
+
- **`Peri` schemas** via `defschema/2` for runtime validation of data.
21
+
- **Structs** for object and record definitions, with `@enforce_keys` ensuring
22
+
required fields are present.
23
+
- For **queries** and **procedures**, it creates structs for `params`,
24
+
`input`, and `output` when those sections exist in the lexicon. It also
25
+
generates a topโlevel struct that aggregates `params` and `input` (when
26
+
applicable); this struct is used by the XRPC client to locate the
27
+
appropriate output struct.
28
+
29
+
If a procedure doesn't have a schema for a JSON body specified as it's input,
30
+
the top-level struct will instead have a `raw_input` field, allowing for
31
+
miscellaneous bodies such as a binary blob.
32
+
33
+
The generated structs also implement the `JSON.Encoder` and `Jason.Encoder`
34
+
protocols (the latter currently present for compatibility), as well as a
35
+
`from_json` function which is used to validate an input map - e.g. from a JSON
36
+
HTTP response - and turn it into a struct.
37
+
38
+
## Example
39
+
40
+
deflexicon(%{
41
+
"lexicon" => 1,
42
+
"id" => "com.ovyerus.testing",
43
+
"defs" => %{
44
+
"main" => %{
45
+
"type" => "record",
46
+
"key" => "tid",
47
+
"record" => %{
48
+
"type" => "object",
49
+
"required" => ["foobar"],
50
+
"properties" => %{ "foobar" => %{ "type" => "string" } }
51
+
}
52
+
}
53
+
}
54
+
})
55
+
56
+
The macro expands to following code (truncated for brevity):
57
+
58
+
@type main() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
59
+
@type t() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
60
+
61
+
defschema(:main, %{
62
+
foobar: {:required, {:custom, {Atex.Lexicon.Validators.String, :validate, [[]]}}},
63
+
"$type": {{:literal, "com.ovyerus.testing"}, {:default, "com.ovyerus.testing"}}
64
+
})
65
+
66
+
@enforce_keys [:foobar]
67
+
defstruct foobar: nil, "$type": "com.ovyerus.testing"
68
+
69
+
def from_json(json) do
70
+
case apply(Com.Ovyerus.Testing, :main, [json]) do
71
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
72
+
err -> err
73
+
end
74
+
end
75
+
76
+
The generated module can be used directly with `Atex.XRPC` functions, allowing
77
+
typeโsafe construction of requests and automatic decoding of responses.
78
+
"""
18
79
defmacro deflexicon(lexicon) do
19
80
# Better way to get the real map, without having to eval? (custom function to compose one from quoted?)
20
81
lexicon =
···
27
88
defs =
28
89
lexicon.defs
29
90
|> Enum.flat_map(fn {def_name, def} -> def_to_schema(lexicon.id, def_name, def) end)
30
-
|> Enum.map(fn {schema_key, quoted_schema, quoted_type} ->
91
+
|> Enum.map(fn
92
+
{schema_key, quoted_schema, quoted_type} -> {schema_key, quoted_schema, quoted_type, nil}
93
+
x -> x
94
+
end)
95
+
|> Enum.map(fn {schema_key, quoted_schema, quoted_type, quoted_struct} ->
31
96
identity_type =
32
-
if schema_key === :main do
97
+
if schema_key == :main do
33
98
quote do
34
99
@type t() :: unquote(quoted_type)
35
100
end
36
101
end
37
102
103
+
struct_def =
104
+
if schema_key == :main do
105
+
quoted_struct
106
+
else
107
+
nested_module_name =
108
+
schema_key
109
+
|> Recase.to_pascal()
110
+
|> atomise()
111
+
112
+
quote do
113
+
defmodule unquote({:__aliases__, [alias: false], [nested_module_name]}) do
114
+
unquote(quoted_struct)
115
+
end
116
+
end
117
+
end
118
+
38
119
quote do
39
-
@type unquote(schema_key)() :: unquote(quoted_type)
120
+
@type unquote(Recase.to_snake(schema_key))() :: unquote(quoted_type)
40
121
unquote(identity_type)
41
122
42
-
defschema unquote(schema_key), unquote(quoted_schema)
123
+
defschema unquote(Recase.to_snake(schema_key)), unquote(quoted_schema)
124
+
125
+
unquote(struct_def)
43
126
end
44
127
end)
45
128
46
129
quote do
47
-
def id, do: unquote(Atex.NSID.to_atom(lexicon.id))
130
+
def id, do: unquote(lexicon.id)
48
131
49
132
unquote_splicing(defs)
50
133
end
51
134
end
135
+
136
+
# - [ ] `t()` type should be the struct in it. (add to non-main structs too?)
52
137
53
138
@spec def_to_schema(nsid :: String.t(), def_name :: String.t(), lexicon_def :: map()) ::
54
-
list({key :: atom(), quoted_schema :: term(), quoted_type :: term()})
139
+
list(
140
+
{
141
+
key :: atom(),
142
+
quoted_schema :: term(),
143
+
quoted_type :: term()
144
+
}
145
+
| {
146
+
key :: atom(),
147
+
quoted_schema :: term(),
148
+
quoted_type :: term(),
149
+
quoted_struct :: term()
150
+
}
151
+
)
55
152
56
153
defp def_to_schema(nsid, def_name, %{type: "record", record: record}) do
57
154
# TODO: record rkey format validator
155
+
type_name = Atex.NSID.canonical_name(nsid, to_string(def_name))
156
+
157
+
record =
158
+
put_in(record, [:properties, :"$type"], %{
159
+
type: "string",
160
+
const: type_name,
161
+
default: type_name
162
+
})
163
+
58
164
def_to_schema(nsid, def_name, record)
59
165
end
60
166
61
-
# TODO: need to spit out an extra 'branded' type with `$type` field, for use in union refs.
167
+
# TODO: add struct to types
62
168
defp def_to_schema(
63
169
nsid,
64
170
def_name,
···
70
176
required = Map.get(def, :required, [])
71
177
nullable = Map.get(def, :nullable, [])
72
178
73
-
properties
74
-
|> Enum.map(fn {key, field} ->
75
-
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
76
-
is_nullable = key in nullable
77
-
is_required = key in required
179
+
{quoted_schemas, quoted_types} =
180
+
properties
181
+
|> Enum.map(fn {key, field} ->
182
+
{quoted_schema, quoted_type} = field_to_schema(field, nsid)
183
+
string_key = to_string(key)
184
+
is_nullable = string_key in nullable
185
+
is_required = string_key in required
186
+
187
+
quoted_schema =
188
+
quoted_schema
189
+
|> then(
190
+
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
191
+
)
192
+
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
193
+
|> then(&{key, &1})
78
194
79
-
quoted_schema =
80
-
quoted_schema
81
-
|> then(
82
-
&if is_nullable, do: quote(do: {:either, {{:literal, nil}, unquote(&1)}}), else: &1
83
-
)
84
-
|> then(&if is_required, do: quote(do: {:required, unquote(&1)}), else: &1)
85
-
|> then(&{key, &1})
195
+
key_type = if is_required, do: :required, else: :optional
86
196
87
-
key_type = if is_required, do: :required, else: :optional
197
+
quoted_type =
198
+
quoted_type
199
+
|> then(
200
+
&if is_nullable do
201
+
{:|, [], [&1, nil]}
202
+
else
203
+
&1
204
+
end
205
+
)
206
+
|> then(&{{key_type, [], [key]}, &1})
88
207
89
-
quoted_type =
90
-
quoted_type
91
-
|> then(
92
-
&if is_nullable do
93
-
{:|, [], [&1, nil]}
94
-
else
95
-
&1
208
+
{quoted_schema, quoted_type}
209
+
end)
210
+
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
211
+
{[quoted_schema | schemas], [quoted_type | types]}
212
+
end)
213
+
214
+
struct_keys =
215
+
properties
216
+
|> Enum.filter(fn {key, _} -> key !== :"$type" end)
217
+
|> Enum.map(fn
218
+
{key, %{default: default}} -> {key, default}
219
+
{key, _field} -> {key, nil}
220
+
end)
221
+
|> then(&(&1 ++ [{:"$type", if(def_name == :main, do: nsid, else: "#{nsid}##{def_name}")}]))
222
+
223
+
enforced_keys =
224
+
properties |> Map.keys() |> Enum.filter(&(to_string(&1) in required && &1 != :"$type"))
225
+
226
+
optional_if_nil_keys =
227
+
properties
228
+
|> Map.keys()
229
+
|> Enum.filter(fn key ->
230
+
key = to_string(key)
231
+
# TODO: what if it is nullable but not required?
232
+
key not in required && key not in nullable && key != "$type"
233
+
end)
234
+
235
+
schema_module = Atex.NSID.to_atom(nsid)
236
+
237
+
quoted_struct =
238
+
quote do
239
+
@enforce_keys unquote(enforced_keys)
240
+
defstruct unquote(struct_keys)
241
+
242
+
def from_json(json) do
243
+
case apply(unquote(schema_module), unquote(atomise(def_name)), [json]) do
244
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
245
+
err -> err
96
246
end
97
-
)
98
-
|> then(&{{key_type, [], [key]}, &1})
247
+
end
99
248
100
-
{quoted_schema, quoted_type}
101
-
end)
102
-
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
103
-
{[quoted_schema | schemas], [quoted_type | types]}
104
-
end)
105
-
|> then(fn {quoted_schemas, quoted_types} ->
106
-
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}}]
107
-
end)
249
+
defimpl JSON.Encoder do
250
+
@optional_if_nil_keys unquote(optional_if_nil_keys)
251
+
252
+
def encode(value, encoder) do
253
+
value
254
+
|> Map.from_struct()
255
+
|> Enum.reject(fn {k, v} -> k in @optional_if_nil_keys && v == nil end)
256
+
|> Enum.into(%{})
257
+
|> Jason.Encoder.encode(encoder)
258
+
end
259
+
end
260
+
261
+
defimpl Jason.Encoder do
262
+
@optional_if_nil_keys unquote(optional_if_nil_keys)
263
+
264
+
def encode(value, options) do
265
+
value
266
+
|> Map.from_struct()
267
+
|> Enum.reject(fn {k, v} -> k in @optional_if_nil_keys && v == nil end)
268
+
|> Enum.into(%{})
269
+
|> Jason.Encode.map(options)
270
+
end
271
+
end
272
+
end
273
+
274
+
[{atomise(def_name), {:%{}, [], quoted_schemas}, {:%{}, [], quoted_types}, quoted_struct}]
108
275
end
109
276
110
277
# TODO: validating errors?
···
127
294
schema
128
295
end
129
296
130
-
[params, output]
297
+
# Root struct containing `params`
298
+
main =
299
+
if params do
300
+
{
301
+
:main,
302
+
nil,
303
+
quote do
304
+
%__MODULE__{params: params()}
305
+
end,
306
+
quote do
307
+
@enforce_keys [:params]
308
+
defstruct params: nil
309
+
end
310
+
}
311
+
else
312
+
{
313
+
:main,
314
+
nil,
315
+
quote do
316
+
%__MODULE__{}
317
+
end,
318
+
quote do
319
+
defstruct []
320
+
end
321
+
}
322
+
end
323
+
324
+
[main, params, output]
131
325
|> Enum.reject(&is_nil/1)
132
326
end
133
327
···
157
351
schema
158
352
end
159
353
160
-
[params, output, input]
354
+
# Root struct containing `input`, `raw_input`, and `params`
355
+
main =
356
+
{
357
+
:main,
358
+
nil,
359
+
cond do
360
+
params && input ->
361
+
quote do
362
+
%__MODULE__{input: input(), params: params()}
363
+
end
364
+
365
+
input ->
366
+
quote do
367
+
%__MODULE__{input: input()}
368
+
end
369
+
370
+
params ->
371
+
quote do
372
+
%__MODULE__{raw_input: any(), params: params()}
373
+
end
374
+
375
+
true ->
376
+
quote do
377
+
%__MODULE__{raw_input: any()}
378
+
end
379
+
end,
380
+
cond do
381
+
params && input ->
382
+
quote do
383
+
defstruct input: nil, params: nil
384
+
end
385
+
386
+
input ->
387
+
quote do
388
+
defstruct input: nil
389
+
end
390
+
391
+
params ->
392
+
quote do
393
+
defstruct raw_input: nil, params: nil
394
+
end
395
+
396
+
true ->
397
+
quote do
398
+
defstruct raw_input: nil
399
+
end
400
+
end
401
+
}
402
+
403
+
[main, params, output, input]
161
404
|> Enum.reject(&is_nil/1)
162
405
end
163
406
···
231
474
:minGraphemes
232
475
])
233
476
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
234
-
|> then(&{:custom, {Validators.String, :validate, [&1]}})
477
+
|> Validators.string()
235
478
|> maybe_default(field)
236
479
end
237
480
|> then(
···
262
505
field
263
506
|> Map.take([:maximum, :minimum])
264
507
|> Keyword.new()
265
-
|> then(&{:custom, {Validators.Integer, [&1]}})
508
+
|> Validators.integer()
266
509
|> maybe_default(field)
267
510
end
268
511
|> then(
···
284
527
|> Enum.map(fn {k, v} -> {Recase.to_snake(k), v} end)
285
528
|> then(&Validators.array(inner_schema, &1))
286
529
|> then(&Macro.escape/1)
530
+
# TODO: we should be able to unquote this now...
287
531
# Can't unquote the inner_schema beforehand as that would risk evaluating `get_schema`s which don't exist yet.
288
532
# There's probably a better way to do this lol.
289
533
|> then(fn {:custom, {:{}, c, [Validators.Array, :validate, [quoted_inner_schema | args]]}} ->
···
341
585
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
342
586
|> Atex.NSID.to_atom_with_fragment()
343
587
344
-
{quote do
345
-
unquote(nsid).get_schema(unquote(fragment))
346
-
end,
347
-
quote do
348
-
unquote(nsid).unquote(fragment)()
349
-
end}
588
+
fragment = Recase.to_snake(fragment)
589
+
590
+
{
591
+
Macro.escape(Validators.lazy_ref(nsid, fragment)),
592
+
quote do
593
+
unquote(nsid).unquote(fragment)()
594
+
end
595
+
}
350
596
end
351
597
352
598
defp field_to_schema(%{type: "union", refs: refs}, nsid) do
···
362
608
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
363
609
|> Atex.NSID.to_atom_with_fragment()
364
610
365
-
{quote do
366
-
unquote(nsid).get_schema(unquote(fragment))
367
-
end,
368
-
quote do
369
-
unquote(nsid).unquote(fragment)()
370
-
end}
611
+
fragment = Recase.to_snake(fragment)
612
+
613
+
{
614
+
Macro.escape(Validators.lazy_ref(nsid, fragment)),
615
+
quote do
616
+
unquote(nsid).unquote(fragment)()
617
+
end
618
+
}
371
619
end)
372
620
|> Enum.reduce({[], []}, fn {quoted_schema, quoted_type}, {schemas, types} ->
373
621
{[quoted_schema | schemas], [quoted_type | types]}
+9
lib/atex/nsid.ex
+9
lib/atex/nsid.ex
+127
lib/atex/oauth/cache.ex
+127
lib/atex/oauth/cache.ex
···
1
+
defmodule Atex.OAuth.Cache do
2
+
@moduledoc """
3
+
TTL cache for OAuth authorization server information.
4
+
5
+
This module manages two separate ConCache instances:
6
+
- Authorization server cache (stores PDS -> authz server mappings)
7
+
- Authorization metadata cache (stores authz server -> metadata mappings)
8
+
9
+
Both caches use a 1-hour TTL to reduce load on third-party PDSs.
10
+
"""
11
+
12
+
use Supervisor
13
+
14
+
@authz_server_cache :oauth_authz_server_cache
15
+
@authz_metadata_cache :oauth_authz_metadata_cache
16
+
@ttl_ms :timer.hours(1)
17
+
18
+
@doc """
19
+
Starts the OAuth cache supervisor.
20
+
"""
21
+
def start_link(opts) do
22
+
Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
23
+
end
24
+
25
+
@impl Supervisor
26
+
def init(_opts) do
27
+
children = [
28
+
Supervisor.child_spec(
29
+
{ConCache,
30
+
[
31
+
name: @authz_server_cache,
32
+
ttl_check_interval: :timer.minutes(5),
33
+
global_ttl: @ttl_ms
34
+
]},
35
+
id: :authz_server_cache
36
+
),
37
+
Supervisor.child_spec(
38
+
{ConCache,
39
+
[
40
+
name: @authz_metadata_cache,
41
+
ttl_check_interval: :timer.seconds(30),
42
+
global_ttl: @ttl_ms
43
+
]},
44
+
id: :authz_metadata_cache
45
+
)
46
+
]
47
+
48
+
Supervisor.init(children, strategy: :one_for_one)
49
+
end
50
+
51
+
@doc """
52
+
Get authorization server from cache.
53
+
54
+
## Parameters
55
+
56
+
- `pds_host` - Base URL of the PDS (e.g., "https://bsky.social")
57
+
58
+
## Returns
59
+
60
+
- `{:ok, authorization_server}` - Successfully retrieved from cache
61
+
- `{:error, :not_found}` - Not present in cache
62
+
"""
63
+
@spec get_authorization_server(String.t()) :: {:ok, String.t()} | {:error, :not_found}
64
+
def get_authorization_server(pds_host) do
65
+
case ConCache.get(@authz_server_cache, pds_host) do
66
+
nil -> {:error, :not_found}
67
+
value -> {:ok, value}
68
+
end
69
+
end
70
+
71
+
@doc """
72
+
Store authorization server in cache.
73
+
74
+
## Parameters
75
+
76
+
- `pds_host` - Base URL of the PDS
77
+
- `authorization_server` - Authorization server URL to cache
78
+
79
+
## Returns
80
+
81
+
- `:ok`
82
+
"""
83
+
@spec set_authorization_server(String.t(), String.t()) :: :ok
84
+
def set_authorization_server(pds_host, authorization_server) do
85
+
ConCache.put(@authz_server_cache, pds_host, authorization_server)
86
+
:ok
87
+
end
88
+
89
+
@doc """
90
+
Get authorization server metadata from cache.
91
+
92
+
## Parameters
93
+
94
+
- `issuer` - Authorization server issuer URL
95
+
96
+
## Returns
97
+
98
+
- `{:ok, metadata}` - Successfully retrieved from cache
99
+
- `{:error, :not_found}` - Not present in cache
100
+
"""
101
+
@spec get_authorization_server_metadata(String.t()) ::
102
+
{:ok, Atex.OAuth.authorization_metadata()} | {:error, :not_found}
103
+
def get_authorization_server_metadata(issuer) do
104
+
case ConCache.get(@authz_metadata_cache, issuer) do
105
+
nil -> {:error, :not_found}
106
+
value -> {:ok, value}
107
+
end
108
+
end
109
+
110
+
@doc """
111
+
Store authorization server metadata in cache.
112
+
113
+
## Parameters
114
+
115
+
- `issuer` - Authorization server issuer URL
116
+
- `metadata` - Authorization server metadata to cache
117
+
118
+
## Returns
119
+
120
+
- `:ok`
121
+
"""
122
+
@spec set_authorization_server_metadata(String.t(), Atex.OAuth.authorization_metadata()) :: :ok
123
+
def set_authorization_server_metadata(issuer, metadata) do
124
+
ConCache.put(@authz_metadata_cache, issuer, metadata)
125
+
:ok
126
+
end
127
+
end
+25
lib/atex/oauth/error.ex
+25
lib/atex/oauth/error.ex
···
1
+
defmodule Atex.OAuth.Error do
2
+
@moduledoc """
3
+
Exception raised by `Atex.OAuth.Plug` when errors occurred. When using the
4
+
Plug, you should set up a `Plug.ErrorHandler` to gracefully catch these and
5
+
give messages to the end user.
6
+
7
+
This extesion has two fields: a human-readable `message` string, and an atom
8
+
`reason` for each specific error.
9
+
10
+
## Reasons
11
+
12
+
- `:missing_handle` - The handle query parameter was not provided
13
+
- `:invalid_handle` - The provided handle could not be resolved
14
+
- `:authorization_url_failed` - Failed to create the authorization URL
15
+
- `:invalid_callback_request` - Missing or invalid state/code in callback
16
+
- `:authorization_server_metadata_failed` - Could not fetch authorization
17
+
server metadata
18
+
- `:token_validation_failed` - Failed to validate the authorization code or
19
+
token
20
+
- `:issuer_mismatch` - OAuth issuer does not match PDS authorization server
21
+
- `:session_store_failed` - OAuth succeeded but failed to store the session
22
+
"""
23
+
24
+
defexception [:message, :reason]
25
+
end
+809
lib/atex/oauth/permission.ex
+809
lib/atex/oauth/permission.ex
···
1
+
defmodule Atex.OAuth.Permission do
2
+
use TypedStruct
3
+
import Kernel, except: [to_string: 1]
4
+
5
+
@type t_tuple() :: {
6
+
resource :: String.t(),
7
+
positional :: String.t() | nil,
8
+
parameters :: list({String.t(), String.t()})
9
+
}
10
+
11
+
@typep as_string() :: {:as_string, boolean()}
12
+
@type account_attr() :: :email | :repo
13
+
@type account_action() :: :read | :manage
14
+
@type account_opt() ::
15
+
{:attr, account_attr()} | {:action, account_action()} | as_string()
16
+
17
+
@type repo_opt() ::
18
+
{:create, boolean()} | {:update, boolean()} | {:delete, boolean()} | as_string()
19
+
20
+
@type rpc_opt() :: {:aud, String.t()} | {:inherit_aud, boolean()} | as_string()
21
+
22
+
@type include_opt() :: {:aud, String.t()} | as_string()
23
+
24
+
typedstruct enforce: true do
25
+
field :resource, String.t()
26
+
field :positional, String.t() | nil
27
+
# like a Keyword list but with a string instead of an atom
28
+
field :parameters, list({String.t(), String.t()}), enforce: false, default: []
29
+
end
30
+
31
+
@doc """
32
+
Creates a new permission struct from a permission scope string.
33
+
34
+
Parses an AT Protocol OAuth permission scope string and returns a structured
35
+
representation. Permission strings follow the format
36
+
`resource:positional?key=value&key2=value2`
37
+
38
+
The positional parameter is resource-specific and may be omitted in some cases
39
+
(e.g., collection for `repo`, lxm for `rpc`, attr for `account`/`identity`,
40
+
accept for `blob`).
41
+
42
+
See the [AT Protocol
43
+
documentation](https://atproto.com/specs/permission#scope-string-syntax) for
44
+
the full syntax and rules for permission scope strings.
45
+
46
+
## Parameters
47
+
- `string` - A permission scope string (e.g., "repo:app.example.profile")
48
+
49
+
Returns `{:ok, permission}` if a valid scope string was given, otherwise it
50
+
will return `{:error, reason}`.
51
+
52
+
## Examples
53
+
54
+
# Simple with just a positional
55
+
iex> Atex.OAuth.Permission.new("repo:app.example.profile")
56
+
{:ok, %Atex.OAuth.Permission{
57
+
resource: "repo",
58
+
positional: "app.example.profile",
59
+
parameters: []
60
+
}}
61
+
62
+
# With parameters
63
+
iex> Atex.OAuth.Permission.new("repo?collection=app.example.profile&collection=app.example.post")
64
+
{:ok, %Atex.OAuth.Permission{
65
+
resource: "repo",
66
+
positional: nil,
67
+
parameters: [
68
+
{"collection", "app.example.profile"},
69
+
{"collection", "app.example.post"}
70
+
]
71
+
}}
72
+
73
+
# Positional with parameters
74
+
iex> Atex.OAuth.Permission.new("rpc:app.example.moderation.createReport?aud=*")
75
+
{:ok, %Atex.OAuth.Permission{
76
+
resource: "rpc",
77
+
positional: "app.example.moderation.createReport",
78
+
parameters: [{"aud", "*"}]
79
+
}}
80
+
81
+
iex> Atex.OAuth.Permission.new("blob:*/*")
82
+
{:ok, %Atex.OAuth.Permission{
83
+
resource: "blob",
84
+
positional: "*/*",
85
+
parameters: []
86
+
}}
87
+
88
+
# Invalid: resource without positional or parameters
89
+
iex> Atex.OAuth.Permission.new("resource")
90
+
{:error, :missing_positional_or_parameters}
91
+
92
+
"""
93
+
@spec new(String.t()) :: {:ok, t()} | {:error, reason :: atom()}
94
+
def new(string) do
95
+
case parse(string) do
96
+
{:ok, {resource, positional, parameters}} ->
97
+
{:ok, %__MODULE__{resource: resource, positional: positional, parameters: parameters}}
98
+
99
+
err ->
100
+
err
101
+
end
102
+
end
103
+
104
+
@doc """
105
+
Parses an AT Protocol permission scope string into its components.
106
+
107
+
Returns a tuple containing the resource name, optional positional parameter,
108
+
and a list of key-value parameter pairs. This is a lower-level function
109
+
compared to `new/1`, returning the raw components instead of a struct.
110
+
111
+
## Parameters
112
+
- `string` - A permission scope string following the format
113
+
`resource:positional?key=value&key2=value2`
114
+
115
+
Returns `{:ok, {resource, positional, parameters}}` if a valid scope string
116
+
was given, otherwise it will return `{:error, reason}`.
117
+
118
+
## Examples
119
+
120
+
# Simple with just a positional
121
+
iex> Atex.OAuth.Permission.parse("repo:app.example.profile")
122
+
{:ok, {"repo", "app.example.profile", []}}
123
+
124
+
# With parameters
125
+
iex> Atex.OAuth.Permission.parse("repo?collection=app.example.profile&collection=app.example.post")
126
+
{:ok, {
127
+
"repo",
128
+
nil,
129
+
[
130
+
{"collection", "app.example.profile"},
131
+
{"collection", "app.example.post"}
132
+
]
133
+
}}
134
+
135
+
# Positional with parameters
136
+
iex> Atex.OAuth.Permission.parse("rpc:app.example.moderation.createReport?aud=*")
137
+
{:ok, {"rpc", "app.example.moderation.createReport", [{"aud", "*"}]}}
138
+
139
+
iex> Atex.OAuth.Permission.parse("blob:*/*")
140
+
{:ok, {"blob", "*/*", []}}
141
+
142
+
# Invalid: resource without positional or parameters
143
+
iex> Atex.OAuth.Permission.parse("resource")
144
+
{:error, :missing_positional_or_parameters}
145
+
146
+
"""
147
+
@spec parse(String.t()) ::
148
+
{:ok, t_tuple()}
149
+
| {:error, reason :: atom()}
150
+
def parse(string) do
151
+
case String.split(string, "?", parts: 2) do
152
+
[resource_part] ->
153
+
parse_resource_and_positional(resource_part)
154
+
155
+
# Empty parameter string is treated as absent
156
+
[resource_part, ""] ->
157
+
parse_resource_and_positional(resource_part)
158
+
159
+
[resource_part, params_part] ->
160
+
params_part
161
+
|> parse_parameters()
162
+
|> then(&parse_resource_and_positional(resource_part, &1))
163
+
end
164
+
end
165
+
166
+
@spec parse_resource_and_positional(String.t(), list({String.t(), String.t()})) ::
167
+
{:ok, t_tuple()} | {:error, reason :: atom()}
168
+
defp parse_resource_and_positional(resource_part, parameters \\ []) do
169
+
case String.split(resource_part, ":", parts: 2) do
170
+
[resource_name, positional] ->
171
+
{:ok, {resource_name, positional, parameters}}
172
+
173
+
[resource_name] ->
174
+
if parameters == [] do
175
+
{:error, :missing_positional_or_parameters}
176
+
else
177
+
{:ok, {resource_name, nil, parameters}}
178
+
end
179
+
end
180
+
end
181
+
182
+
@spec parse_parameters(String.t()) :: list({String.t(), String.t()})
183
+
defp parse_parameters(params_string) do
184
+
params_string
185
+
|> String.split("&")
186
+
|> Enum.map(fn param ->
187
+
case String.split(param, "=", parts: 2) do
188
+
[key, value] -> {key, URI.decode(value)}
189
+
[key] -> {key, ""}
190
+
end
191
+
end)
192
+
end
193
+
194
+
@doc """
195
+
Converts a permission struct back into its scope string representation.
196
+
197
+
This is the inverse operation of `new/1`, converting a structured permission
198
+
back into the AT Protocol OAuth scope string format. The resulting string
199
+
can be used directly as an OAuth scope parameter.
200
+
201
+
Values in `parameters` are automatically URL-encoded as needed (e.g., `#` becomes `%23`).
202
+
203
+
## Parameters
204
+
- `struct` - An `%Atex.OAuth.Permission{}` struct
205
+
206
+
Returns a permission scope string.
207
+
208
+
## Examples
209
+
210
+
# Simple with just a positional
211
+
iex> perm = %Atex.OAuth.Permission{
212
+
...> resource: "repo",
213
+
...> positional: "app.example.profile",
214
+
...> parameters: []
215
+
...> }
216
+
iex> Atex.OAuth.Permission.to_string(perm)
217
+
"repo:app.example.profile"
218
+
219
+
# With parameters
220
+
iex> perm = %Atex.OAuth.Permission{
221
+
...> resource: "repo",
222
+
...> positional: nil,
223
+
...> parameters: [
224
+
...> {"collection", "app.example.profile"},
225
+
...> {"collection", "app.example.post"}
226
+
...> ]
227
+
...> }
228
+
iex> Atex.OAuth.Permission.to_string(perm)
229
+
"repo?collection=app.example.profile&collection=app.example.post"
230
+
231
+
# Positional with parameters
232
+
iex> perm = %Atex.OAuth.Permission{
233
+
...> resource: "rpc",
234
+
...> positional: "app.example.moderation.createReport",
235
+
...> parameters: [{"aud", "*"}]
236
+
...> }
237
+
iex> Atex.OAuth.Permission.to_string(perm)
238
+
"rpc:app.example.moderation.createReport?aud=*"
239
+
240
+
iex> perm = %Atex.OAuth.Permission{
241
+
...> resource: "blob",
242
+
...> positional: "*/*",
243
+
...> parameters: []
244
+
...> }
245
+
iex> Atex.OAuth.Permission.to_string(perm)
246
+
"blob:*/*"
247
+
248
+
# Works via String.Chars protocol
249
+
iex> perm = %Atex.OAuth.Permission{
250
+
...> resource: "account",
251
+
...> positional: "email",
252
+
...> parameters: []
253
+
...> }
254
+
iex> to_string(perm)
255
+
"account:email"
256
+
257
+
"""
258
+
@spec to_string(t()) :: String.t()
259
+
def to_string(%__MODULE__{} = struct) do
260
+
positional_part = if struct.positional, do: ":#{struct.positional}", else: ""
261
+
parameters_part = stringify_parameters(struct.parameters)
262
+
263
+
struct.resource <> positional_part <> parameters_part
264
+
end
265
+
266
+
@spec stringify_parameters(list({String.t(), String.t()})) :: String.t()
267
+
defp stringify_parameters([]), do: ""
268
+
269
+
defp stringify_parameters(params) do
270
+
params
271
+
|> Enum.map(fn {key, value} -> "#{key}=#{encode_param_value(value)}" end)
272
+
|> Enum.join("&")
273
+
|> then(&"?#{&1}")
274
+
end
275
+
276
+
# Encode parameter values for OAuth scope strings
277
+
# Preserves unreserved characters (A-Z, a-z, 0-9, -, ., _, ~) and common scope characters (*, :, /)
278
+
# Encodes reserved characters like # as %23
279
+
@spec encode_param_value(String.t()) :: String.t()
280
+
defp encode_param_value(value) do
281
+
URI.encode(value, fn char ->
282
+
URI.char_unreserved?(char) or char in [?*, ?:, ?/]
283
+
end)
284
+
end
285
+
286
+
@doc """
287
+
Creates an account permission for controlling PDS account hosting details.
288
+
289
+
Controls access to private account information such as email address and
290
+
repository import capabilities. These permissions cannot be included in
291
+
permission sets and must be requested directly by client apps.
292
+
293
+
See the [AT Protocol documentation](https://atproto.com/specs/permission#account)
294
+
for more information.
295
+
296
+
## Options
297
+
- `:attr` (required) - A component of account configuration. Must be `:email`
298
+
or `:repo`.
299
+
- `:action` (optional) - Degree of control. Can be `:read` or `:manage`.
300
+
Defaults to `:read`.
301
+
- `:as_string` (optional) - If `true` (default), returns a scope string,
302
+
otherwise returns a Permission struct.
303
+
304
+
If `:as_string` is true a scope string is returned, otherwise the underlying
305
+
Permission struct is returned.
306
+
307
+
## Examples
308
+
309
+
# Read account email (default action, as string)
310
+
iex> Atex.OAuth.Permission.account(attr: :email)
311
+
"account:email"
312
+
313
+
# Read account email (as struct)
314
+
iex> Atex.OAuth.Permission.account(attr: :email, as_string: false)
315
+
%Atex.OAuth.Permission{
316
+
resource: "account",
317
+
positional: "email",
318
+
parameters: []
319
+
}
320
+
321
+
# Read account email (explicit action)
322
+
iex> Atex.OAuth.Permission.account(attr: :email, action: :read)
323
+
"account:email?action=read"
324
+
325
+
# Manage account email
326
+
iex> Atex.OAuth.Permission.account(attr: :email, action: :manage)
327
+
"account:email?action=manage"
328
+
329
+
# Import repo
330
+
iex> Atex.OAuth.Permission.account(attr: :repo, action: :manage)
331
+
"account:repo?action=manage"
332
+
333
+
"""
334
+
@spec account(list(account_opt())) :: t() | String.t()
335
+
def account(opts \\ []) do
336
+
opts = Keyword.validate!(opts, attr: nil, action: nil, as_string: true)
337
+
attr = Keyword.get(opts, :attr)
338
+
action = Keyword.get(opts, :action)
339
+
as_string = Keyword.get(opts, :as_string)
340
+
341
+
cond do
342
+
is_nil(attr) ->
343
+
raise ArgumentError, "option `:attr` must be provided."
344
+
345
+
attr not in [:email, :repo] ->
346
+
raise ArgumentError, "option `:attr` must be `:email` or `:repo`."
347
+
348
+
action not in [nil, :read, :manage] ->
349
+
raise ArgumentError, "option `:action` must be `:read`, `:manage`, or `nil`."
350
+
351
+
true ->
352
+
struct = %__MODULE__{
353
+
resource: "account",
354
+
positional: Atom.to_string(attr),
355
+
parameters: if(!is_nil(action), do: [{"action", Atom.to_string(action)}], else: [])
356
+
}
357
+
358
+
if as_string, do: to_string(struct), else: struct
359
+
end
360
+
end
361
+
362
+
@doc """
363
+
Creates a blob permission for uploading media files to PDS.
364
+
365
+
Controls the ability to upload blobs (media files) to the PDS. Permissions can
366
+
be restricted by MIME type patterns.
367
+
368
+
See the [AT Protocol documentation](https://atproto.com/specs/permission#blob)
369
+
for more information.
370
+
371
+
<!-- TODO: When permission sets are supported, add the note from the docs about this not being allowed in permisison sets. -->
372
+
373
+
## Parameters
374
+
- `accept` - A single MIME type string or list of MIME type strings/patterns.
375
+
Supports glob patterns like `"*/*"` or `"video/*"`.
376
+
- `opts` - Keyword list of options.
377
+
378
+
## Options
379
+
- `:as_string` (optional) - If `true` (default), returns a scope string, otherwise
380
+
returns a Permission struct.
381
+
382
+
If `:as_string` is true a scope string is returned, otherwise the underlying
383
+
Permission struct is returned.
384
+
385
+
## Examples
386
+
387
+
# Upload any type of blob
388
+
iex> Atex.OAuth.Permission.blob("*/*")
389
+
"blob:*/*"
390
+
391
+
# Only images
392
+
iex> Atex.OAuth.Permission.blob("image/*", as_string: false)
393
+
%Atex.OAuth.Permission{
394
+
resource: "blob",
395
+
positional: "image/*",
396
+
parameters: []
397
+
}
398
+
399
+
# Multiple mimetypes
400
+
iex> Atex.OAuth.Permission.blob(["video/*", "text/html"])
401
+
"blob?accept=video/*&accept=text/html"
402
+
403
+
# Multiple more specific mimetypes
404
+
iex> Atex.OAuth.Permission.blob(["image/png", "image/jpeg"], as_string: false)
405
+
%Atex.OAuth.Permission{
406
+
resource: "blob",
407
+
positional: nil,
408
+
parameters: [{"accept", "image/png"}, {"accept", "image/jpeg"}]
409
+
}
410
+
411
+
"""
412
+
# TODO: should probably validate that these at least look like mimetypes (~r"^.+/.+$")
413
+
@spec blob(String.t() | list(String.t()), list(as_string())) :: t() | String.t()
414
+
def blob(accept, opts \\ [])
415
+
416
+
def blob(accept, opts) when is_binary(accept) do
417
+
opts = Keyword.validate!(opts, as_string: true)
418
+
as_string = Keyword.get(opts, :as_string)
419
+
struct = %__MODULE__{resource: "blob", positional: accept}
420
+
if as_string, do: to_string(struct), else: struct
421
+
end
422
+
423
+
def blob(accept, opts) when is_list(accept) do
424
+
opts = Keyword.validate!(opts, as_string: true)
425
+
as_string = Keyword.get(opts, :as_string)
426
+
427
+
struct = %__MODULE__{
428
+
resource: "blob",
429
+
positional: nil,
430
+
parameters: Enum.map(accept, &{"accept", &1})
431
+
}
432
+
433
+
if as_string, do: to_string(struct), else: struct
434
+
end
435
+
436
+
@doc """
437
+
Creates an identity permission for controlling network identity.
438
+
439
+
Controls access to the account's DID document and handle. Note that the PDS
440
+
might not be able to facilitate identity changes if it does not have control
441
+
over the DID document (e.g., when using `did:web`).
442
+
443
+
<!-- TODO: same thing about not allowed in permission sets. -->
444
+
445
+
See the [AT Protocol
446
+
documentation](https://atproto.com/specs/permission#identity) for more
447
+
information.
448
+
449
+
## Parameters
450
+
- `attr` - An aspect or component of identity. Must be `:handle` or `:*`
451
+
(wildcard).
452
+
- `opts` - Keyword list of options.
453
+
454
+
## Options
455
+
- `:as_string` (optional) - If `true` (default), returns a scope string,
456
+
otherwise returns a Permission struct.
457
+
458
+
If `:as_string` is true a scope string is returned, otherwise the underlying
459
+
Permission struct is returned.
460
+
461
+
## Examples
462
+
463
+
# Update account handle (as string)
464
+
iex> Atex.OAuth.Permission.identity(:handle)
465
+
"identity:handle"
466
+
467
+
# Full identity control (as struct)
468
+
iex> Atex.OAuth.Permission.identity(:*, as_string: false)
469
+
%Atex.OAuth.Permission{
470
+
resource: "identity",
471
+
positional: "*",
472
+
parameters: []
473
+
}
474
+
475
+
"""
476
+
@spec identity(:handle | :*, list(as_string())) :: t() | String.t()
477
+
def identity(attr, opts \\ []) when attr in [:handle, :*] do
478
+
opts = Keyword.validate!(opts, as_string: true)
479
+
as_string = Keyword.get(opts, :as_string)
480
+
481
+
struct = %__MODULE__{
482
+
resource: "identity",
483
+
positional: Atom.to_string(attr)
484
+
}
485
+
486
+
if as_string, do: to_string(struct), else: struct
487
+
end
488
+
489
+
@doc """
490
+
Creates a repo permission for write access to records in the account's public
491
+
repository.
492
+
493
+
Controls write access to specific record types (collections) with optional
494
+
restrictions on the types of operations allowed (create, update, delete).
495
+
496
+
When no options are provided, all operations are permitted. When any action
497
+
option is explicitly set, only the actions set to `true` are enabled. This
498
+
allows for precise control over permissions.
499
+
500
+
See the [AT Protocol documentation](https://atproto.com/specs/permission#repo)
501
+
for more information.
502
+
503
+
## Parameters
504
+
- `collection_or_collections` - A single collection NSID string or list of
505
+
collection NSIDs. Use `"*"` for wildcard access to all record types (not
506
+
allowed in permission sets).
507
+
- `options` - Keyword list to restrict operations. If omitted, all operations
508
+
are allowed. If any action is specified, only explicitly enabled actions are
509
+
permitted.
510
+
511
+
## Options
512
+
- `:create` - Allow creating new records.
513
+
- `:update` - Allow updating existing records.
514
+
- `:delete` - Allow deleting records.
515
+
- `:as_string` (optional) - If `true` (default), returns a scope string,
516
+
otherwise returns a Permission struct.
517
+
518
+
If `:as_string` is true a scope string is returned, otherwise the underlying
519
+
Permission struct is returned.
520
+
521
+
## Examples
522
+
523
+
# Full permission on a single record type (all actions enabled, actions omitted)
524
+
iex> Atex.OAuth.Permission.repo("app.example.profile")
525
+
"repo:app.example.profile"
526
+
527
+
# Create only permission (other actions implicitly disabled)
528
+
iex> Atex.OAuth.Permission.repo("app.example.post", create: true, as_string: false)
529
+
%Atex.OAuth.Permission{
530
+
resource: "repo",
531
+
positional: "app.example.post",
532
+
parameters: [{"action", "create"}]
533
+
}
534
+
535
+
# Delete only permission
536
+
iex> Atex.OAuth.Permission.repo("app.example.like", delete: true)
537
+
"repo:app.example.like?action=delete"
538
+
539
+
# Create and update only, delete implicitly disabled
540
+
iex> Atex.OAuth.Permission.repo("app.example.repost", create: true, update: true)
541
+
"repo:app.example.repost?action=update&action=create"
542
+
543
+
# Multiple collections with full permissions (no options provided, actions omitted)
544
+
iex> Atex.OAuth.Permission.repo(["app.example.profile", "app.example.post"])
545
+
"repo?collection=app.example.profile&collection=app.example.post"
546
+
547
+
# Multiple collections with only update permission (as struct)
548
+
iex> Atex.OAuth.Permission.repo(["app.example.like", "app.example.repost"], update: true, as_string: false)
549
+
%Atex.OAuth.Permission{
550
+
resource: "repo",
551
+
positional: nil,
552
+
parameters: [
553
+
{"collection", "app.example.like"},
554
+
{"collection", "app.example.repost"},
555
+
{"action", "update"}
556
+
]
557
+
}
558
+
559
+
# Wildcard permission (all record types, all actions enabled, actions omitted)
560
+
iex> Atex.OAuth.Permission.repo("*")
561
+
"repo:*"
562
+
"""
563
+
@spec repo(String.t() | list(String.t()), list(repo_opt())) :: t() | String.t()
564
+
def repo(collection_or_collections, actions \\ [create: true, update: true, delete: true])
565
+
566
+
def repo(_collection, []),
567
+
do:
568
+
raise(
569
+
ArgumentError,
570
+
":actions must not be an empty list. If you want to have all actions enabled, either set them explicitly or remove the empty list argument."
571
+
)
572
+
573
+
def repo(collection, actions) when is_binary(collection), do: repo([collection], actions)
574
+
575
+
def repo(collections, actions) when is_list(collections) do
576
+
actions =
577
+
Keyword.validate!(actions, [:create, :update, :delete, as_string: true])
578
+
579
+
# Check if any action keys were explicitly provided
580
+
has_explicit_actions =
581
+
Keyword.has_key?(actions, :create) ||
582
+
Keyword.has_key?(actions, :update) ||
583
+
Keyword.has_key?(actions, :delete)
584
+
585
+
# If no action keys provided, default all to true; otherwise use explicit values
586
+
create = if has_explicit_actions, do: Keyword.get(actions, :create, false), else: true
587
+
update = if has_explicit_actions, do: Keyword.get(actions, :update, false), else: true
588
+
delete = if has_explicit_actions, do: Keyword.get(actions, :delete, false), else: true
589
+
all_actions_true = create && update && delete
590
+
591
+
as_string = Keyword.get(actions, :as_string)
592
+
singular_collection = length(collections) == 1
593
+
collection_parameters = Enum.map(collections, &{"collection", &1})
594
+
595
+
parameters =
596
+
[]
597
+
|> add_repo_param(:create, create, all_actions_true)
598
+
|> add_repo_param(:update, update, all_actions_true)
599
+
|> add_repo_param(:delete, delete, all_actions_true)
600
+
|> add_repo_param(:collections, collection_parameters)
601
+
602
+
struct = %__MODULE__{
603
+
resource: "repo",
604
+
positional: if(singular_collection, do: hd(collections)),
605
+
parameters: parameters
606
+
}
607
+
608
+
if as_string, do: to_string(struct), else: struct
609
+
end
610
+
611
+
# When all actions are true, omit them
612
+
defp add_repo_param(list, _type, _value, true), do: list
613
+
# Otherwise add them in
614
+
defp add_repo_param(list, :create, true, false), do: [{"action", "create"} | list]
615
+
defp add_repo_param(list, :update, true, false), do: [{"action", "update"} | list]
616
+
defp add_repo_param(list, :delete, true, false), do: [{"action", "delete"} | list]
617
+
618
+
# Catch-all for 4-arity version (must be before 3-arity)
619
+
defp add_repo_param(list, _type, _value, _all_true), do: list
620
+
621
+
defp add_repo_param(list, :collections, [_ | [_ | _]] = collections),
622
+
do: Enum.concat(collections, list)
623
+
624
+
defp add_repo_param(list, _type, _value), do: list
625
+
626
+
@doc """
627
+
Creates an RPC permission for authenticated API requests to remote services.
628
+
629
+
The permission is parameterised by the remote endpoint (`lxm`, short for
630
+
"Lexicon Method") and the identity of the remote service (the audience,
631
+
`aud`). Permissions must be restricted by at least one of these parameters.
632
+
633
+
See the [AT Protocol documentation](https://atproto.com/specs/permission#rpc)
634
+
for more information.
635
+
636
+
## Parameters
637
+
- `lxm` - A single NSID string or list of NSID strings representing API
638
+
endpoints. Use `"*"` for wildcard access to all endpoints.
639
+
- `opts` - Keyword list of options.
640
+
641
+
## Options
642
+
- `:aud` (semi-required) - Audience of API requests as a DID service
643
+
reference (e.g., `"did:web:api.example.com#srvtype"`). Supports wildcard
644
+
(`"*"`).
645
+
- `:inherit_aud` (optional) - If `true`, the `aud` value will be inherited
646
+
from permission set invocation context. Only used inside permission sets.
647
+
- `:as_string` (optional) - If `true` (default), returns a scope string,
648
+
otherwise returns a Permission struct.
649
+
650
+
> #### Note {: .info}
651
+
>
652
+
> `aud` and `lxm` cannot both be wildcard. The permission must be restricted
653
+
> by at least one of them.
654
+
655
+
If `:as_string` is true a scope string is returned, otherwise the underlying
656
+
Permission struct is returned.
657
+
658
+
## Examples
659
+
660
+
# Single endpoint with wildcard audience (as string)
661
+
iex> Atex.OAuth.Permission.rpc("app.example.moderation.createReport", aud: "*")
662
+
"rpc:app.example.moderation.createReport?aud=*"
663
+
664
+
# Multiple endpoints with specific service (as struct)
665
+
iex> Atex.OAuth.Permission.rpc(
666
+
...> ["app.example.getFeed", "app.example.getProfile"],
667
+
...> aud: "did:web:api.example.com#svc_appview",
668
+
...> as_string: false
669
+
...> )
670
+
%Atex.OAuth.Permission{
671
+
resource: "rpc",
672
+
positional: nil,
673
+
parameters: [
674
+
{"aud", "did:web:api.example.com#svc_appview"},
675
+
{"lxm", "app.example.getFeed"},
676
+
{"lxm", "app.example.getProfile"}
677
+
]
678
+
}
679
+
680
+
# Wildcard method with specific service
681
+
iex> Atex.OAuth.Permission.rpc("*", aud: "did:web:api.example.com#svc_appview")
682
+
"rpc:*?aud=did:web:api.example.com%23svc_appview"
683
+
684
+
# Single endpoint with inherited audience (for permission sets)
685
+
iex> Atex.OAuth.Permission.rpc("app.example.getPreferences", inherit_aud: true)
686
+
"rpc:app.example.getPreferences?inheritAud=true"
687
+
688
+
"""
689
+
@spec rpc(String.t() | list(String.t()), list(rpc_opt())) :: t() | String.t()
690
+
def rpc(lxm_or_lxms, opts \\ [])
691
+
def rpc(lxm, opts) when is_binary(lxm), do: rpc([lxm], opts)
692
+
693
+
def rpc(lxms, opts) when is_list(lxms) do
694
+
opts = Keyword.validate!(opts, aud: nil, inherit_aud: false, as_string: true)
695
+
aud = Keyword.get(opts, :aud)
696
+
inherit_aud = Keyword.get(opts, :inherit_aud)
697
+
as_string = Keyword.get(opts, :as_string)
698
+
699
+
# Validation: must have at least one of aud or inherit_aud
700
+
cond do
701
+
is_nil(aud) && !inherit_aud ->
702
+
raise ArgumentError,
703
+
"RPC permissions must specify either `:aud` or `:inheritAud` option."
704
+
705
+
!is_nil(aud) && inherit_aud ->
706
+
raise ArgumentError,
707
+
"RPC permissions cannot specify both `:aud` and `:inheritAud` options."
708
+
709
+
# Both lxm and aud cannot be wildcard
710
+
length(lxms) == 1 && hd(lxms) == "*" && aud == "*" ->
711
+
raise ArgumentError, "RPC permissions cannot have both wildcard `lxm` and wildcard `aud`."
712
+
713
+
true ->
714
+
singular_lxm = length(lxms) == 1
715
+
lxm_parameters = Enum.map(lxms, &{"lxm", &1})
716
+
717
+
parameters =
718
+
cond do
719
+
inherit_aud && singular_lxm ->
720
+
[{"inheritAud", "true"}]
721
+
722
+
inherit_aud ->
723
+
[{"inheritAud", "true"} | lxm_parameters]
724
+
725
+
singular_lxm ->
726
+
[{"aud", aud}]
727
+
728
+
true ->
729
+
[{"aud", aud} | lxm_parameters]
730
+
end
731
+
732
+
struct = %__MODULE__{
733
+
resource: "rpc",
734
+
positional: if(singular_lxm, do: hd(lxms)),
735
+
parameters: parameters
736
+
}
737
+
738
+
if as_string, do: to_string(struct), else: struct
739
+
end
740
+
end
741
+
742
+
@doc """
743
+
Creates an include permission for referencing a permission set.
744
+
745
+
Permission sets are Lexicon schemas that bundle together multiple permissions
746
+
under a single NSID. This allows developers to request a group of related
747
+
permissions with a single scope string, improving user experience by reducing
748
+
the number of individual permissions that need to be reviewed.
749
+
750
+
The `nsid` parameter is required and must be a valid NSID that resolves to a
751
+
permission set Lexicon schema. An optional `aud` parameter can be used to specify
752
+
the audience for any RPC permissions within the set that have `inheritAud: true`.
753
+
754
+
See the [AT Protocol documentation](https://atproto.com/specs/permission#permission-sets)
755
+
for more information.
756
+
757
+
## Parameters
758
+
- `nsid` - The NSID of the permission set (e.g., "com.example.authBasicFeatures")
759
+
- `opts` - Keyword list of options.
760
+
761
+
## Options
762
+
- `:aud` (optional) - Audience of API requests as a DID service reference
763
+
(e.g., "did:web:api.example.com#srvtype"). Supports wildcard (`"*"`).
764
+
- `:as_string` (optional) - If `true` (default), returns a scope string,
765
+
otherwise returns a Permission struct.
766
+
767
+
If `:as_string` is true a scope string is returned, otherwise the underlying
768
+
Permission struct is returned.
769
+
770
+
## Examples
771
+
772
+
# Include a permission set (as string)
773
+
iex> Atex.OAuth.Permission.include("com.example.authBasicFeatures")
774
+
"include:com.example.authBasicFeatures"
775
+
776
+
# Include a permission set with audience (as struct)
777
+
iex> Atex.OAuth.Permission.include("com.example.authFull", aud: "did:web:api.example.com#svc_chat", as_string: false)
778
+
%Atex.OAuth.Permission{
779
+
resource: "include",
780
+
positional: "com.example.authFull",
781
+
parameters: [{"aud", "did:web:api.example.com#svc_chat"}]
782
+
}
783
+
784
+
# Include a permission set with wildcard audience
785
+
iex> Atex.OAuth.Permission.include("app.example.authFull", aud: "*")
786
+
"include:app.example.authFull?aud=*"
787
+
788
+
"""
789
+
@spec include(String.t(), list(include_opt())) :: t() | String.t()
790
+
def include(nsid, opts \\ []) do
791
+
opts = Keyword.validate!(opts, aud: nil, as_string: true)
792
+
aud = Keyword.get(opts, :aud)
793
+
as_string = Keyword.get(opts, :as_string)
794
+
795
+
parameters = if !is_nil(aud), do: [{"aud", aud}], else: []
796
+
797
+
struct = %__MODULE__{
798
+
resource: "include",
799
+
positional: nsid,
800
+
parameters: parameters
801
+
}
802
+
803
+
if as_string, do: to_string(struct), else: struct
804
+
end
805
+
end
806
+
807
+
defimpl String.Chars, for: Atex.OAuth.Permission do
808
+
def to_string(permission), do: Atex.OAuth.Permission.to_string(permission)
809
+
end
+244
lib/atex/oauth/plug.ex
+244
lib/atex/oauth/plug.ex
···
1
+
defmodule Atex.OAuth.Plug do
2
+
@moduledoc """
3
+
Plug router for handling AT Protocol's OAuth flow.
4
+
5
+
This module provides three endpoints:
6
+
7
+
- `GET /login?handle=<handle>` - Initiates the OAuth authorization flow for a
8
+
given handle
9
+
- `GET /callback` - Handles the OAuth callback after user authorization
10
+
- `GET /client-metadata.json` - Serves the OAuth client metadata
11
+
12
+
## Usage
13
+
14
+
This module requires `Plug.Session` to be in your pipeline, as well as
15
+
`secret_key_base` to have been set on your connections. Ideally it should be
16
+
routed to via `Plug.Router.forward/2`, under a route like "/oauth".
17
+
18
+
The plug requires a `:callback` option that must be an MFA tuple (Module,
19
+
Function, Args). This callback is invoked after successful OAuth
20
+
authentication, receiving the connection with the authenticated session data.
21
+
22
+
## Error Handling
23
+
24
+
`Atex.OAuth.Error` exceptions are raised when errors occur during the OAuth
25
+
flow (e.g. an invalid handle is provided, or validation failed). You should
26
+
implement a `Plug.ErrorHandler` to catch and handle these exceptions
27
+
gracefully.
28
+
29
+
## Example
30
+
31
+
Example implementation showing how to set up the OAuth plug with proper
32
+
session handling, error handling, and a callback function.
33
+
34
+
defmodule ExampleOAuthPlug do
35
+
use Plug.Router
36
+
use Plug.ErrorHandler
37
+
38
+
plug :put_secret_key_base
39
+
40
+
plug Plug.Session,
41
+
store: :cookie,
42
+
key: "atex-oauth",
43
+
signing_salt: "signing-salt"
44
+
45
+
plug :match
46
+
plug :dispatch
47
+
48
+
forward "/oauth", to: Atex.OAuth.Plug, init_opts: [callback: {__MODULE__, :oauth_callback, []}]
49
+
50
+
def oauth_callback(conn) do
51
+
# Handle successful OAuth authentication
52
+
conn
53
+
|> put_resp_header("Location", "/dashboard")
54
+
|> resp(307, "")
55
+
|> send_resp()
56
+
end
57
+
58
+
def put_secret_key_base(conn, _) do
59
+
put_in(
60
+
conn.secret_key_base,
61
+
"very long key base with at least 64 bytes"
62
+
)
63
+
end
64
+
65
+
# Error handler for OAuth exceptions
66
+
@impl Plug.ErrorHandler
67
+
def handle_errors(conn, %{kind: :error, reason: %Atex.OAuth.Error{} = error, stack: _stack}) do
68
+
status = case error.reason do
69
+
reason when reason in [:missing_handle, :invalid_handle, :invalid_callback_request, :issuer_mismatch] -> 400
70
+
_ -> 500
71
+
end
72
+
73
+
conn
74
+
|> put_resp_content_type("text/plain")
75
+
|> send_resp(status, error.message)
76
+
end
77
+
78
+
# Fallback for other errors
79
+
def handle_errors(conn, %{kind: _kind, reason: _reason, stack: _stack}) do
80
+
send_resp(conn, conn.status, "Something went wrong")
81
+
end
82
+
end
83
+
84
+
## Session Storage
85
+
86
+
After successful authentication, the plug stores these in the session:
87
+
88
+
- `:tokens` - The access token response containing access_token,
89
+
refresh_token, did, and expires_at
90
+
- `:dpop_nonce` -
91
+
- `:dpop_key` - The DPoP JWK for generating DPoP proofs
92
+
"""
93
+
require Logger
94
+
use Plug.Router
95
+
require Plug.Router
96
+
alias Atex.OAuth
97
+
alias Atex.{IdentityResolver, IdentityResolver.DIDDocument}
98
+
99
+
@oauth_cookie_opts [path: "/", http_only: true, secure: true, same_site: "lax", max_age: 600]
100
+
@session_name :atex_session
101
+
102
+
def init(opts) do
103
+
callback = Keyword.get(opts, :callback, nil)
104
+
105
+
if !match?({_module, _function, _args}, callback) do
106
+
raise "expected callback to be a MFA tuple"
107
+
end
108
+
109
+
opts
110
+
end
111
+
112
+
def call(conn, opts) do
113
+
conn
114
+
|> put_private(:atex_oauth_opts, opts)
115
+
|> super(opts)
116
+
end
117
+
118
+
plug :match
119
+
plug :dispatch
120
+
121
+
get "/login" do
122
+
conn = fetch_query_params(conn)
123
+
handle = conn.query_params["handle"]
124
+
125
+
if !handle do
126
+
raise Atex.OAuth.Error,
127
+
message: "Handle query parameter is required",
128
+
reason: :missing_handle
129
+
end
130
+
131
+
case IdentityResolver.resolve(handle) do
132
+
{:ok, identity} ->
133
+
pds = DIDDocument.get_pds_endpoint(identity.document)
134
+
{:ok, authz_server} = OAuth.get_authorization_server(pds)
135
+
{:ok, authz_metadata} = OAuth.get_authorization_server_metadata(authz_server)
136
+
state = OAuth.create_nonce()
137
+
code_verifier = OAuth.create_nonce()
138
+
139
+
case OAuth.create_authorization_url(
140
+
authz_metadata,
141
+
state,
142
+
code_verifier,
143
+
handle
144
+
) do
145
+
{:ok, authz_url} ->
146
+
conn
147
+
|> put_resp_cookie("state", state, @oauth_cookie_opts)
148
+
|> put_resp_cookie("code_verifier", code_verifier, @oauth_cookie_opts)
149
+
|> put_resp_cookie("issuer", authz_metadata.issuer, @oauth_cookie_opts)
150
+
|> put_resp_header("location", authz_url)
151
+
|> send_resp(307, "")
152
+
153
+
{:error, _err} ->
154
+
raise Atex.OAuth.Error,
155
+
message: "Failed to create authorization URL",
156
+
reason: :authorization_url_failed
157
+
end
158
+
159
+
_err ->
160
+
raise Atex.OAuth.Error, message: "Invalid or unresolvable handle", reason: :invalid_handle
161
+
end
162
+
end
163
+
164
+
get "/client-metadata.json" do
165
+
conn
166
+
|> put_resp_content_type("application/json")
167
+
|> send_resp(200, JSON.encode_to_iodata!(OAuth.create_client_metadata()))
168
+
end
169
+
170
+
get "/callback" do
171
+
conn = conn |> fetch_query_params() |> fetch_session()
172
+
callback = Keyword.get(conn.private.atex_oauth_opts, :callback)
173
+
cookies = get_cookies(conn)
174
+
stored_state = cookies["state"]
175
+
stored_code_verifier = cookies["code_verifier"]
176
+
stored_issuer = cookies["issuer"]
177
+
178
+
code = conn.query_params["code"]
179
+
state = conn.query_params["state"]
180
+
181
+
if !stored_state || !stored_code_verifier || !stored_issuer || (!code || !state) ||
182
+
stored_state != state do
183
+
raise Atex.OAuth.Error,
184
+
message: "Invalid callback request: missing or mismatched state/code parameters",
185
+
reason: :invalid_callback_request
186
+
end
187
+
188
+
with {:ok, authz_metadata} <- OAuth.get_authorization_server_metadata(stored_issuer),
189
+
dpop_key <- JOSE.JWK.generate_key({:ec, "P-256"}),
190
+
{:ok, tokens, nonce} <-
191
+
OAuth.validate_authorization_code(
192
+
authz_metadata,
193
+
dpop_key,
194
+
code,
195
+
stored_code_verifier
196
+
),
197
+
{:ok, identity} <- IdentityResolver.resolve(tokens.did),
198
+
# Make sure pds' issuer matches the stored one (just in case)
199
+
pds <- DIDDocument.get_pds_endpoint(identity.document),
200
+
{:ok, authz_server} <- OAuth.get_authorization_server(pds),
201
+
true <- authz_server == stored_issuer do
202
+
session = %OAuth.Session{
203
+
iss: authz_server,
204
+
aud: pds,
205
+
sub: tokens.did,
206
+
access_token: tokens.access_token,
207
+
refresh_token: tokens.refresh_token,
208
+
expires_at: tokens.expires_at,
209
+
dpop_key: dpop_key,
210
+
dpop_nonce: nonce
211
+
}
212
+
213
+
case OAuth.SessionStore.insert(session) do
214
+
:ok ->
215
+
conn =
216
+
conn
217
+
|> delete_resp_cookie("state", @oauth_cookie_opts)
218
+
|> delete_resp_cookie("code_verifier", @oauth_cookie_opts)
219
+
|> delete_resp_cookie("issuer", @oauth_cookie_opts)
220
+
|> put_session(@session_name, tokens.did)
221
+
222
+
{mod, func, args} = callback
223
+
apply(mod, func, [conn | args])
224
+
225
+
{:error, reason} ->
226
+
raise Atex.OAuth.Error,
227
+
message: "Failed to store OAuth session, reason: #{reason}",
228
+
reason: :session_store_failed
229
+
end
230
+
else
231
+
false ->
232
+
raise Atex.OAuth.Error,
233
+
message: "OAuth issuer does not match PDS' authorization server",
234
+
reason: :issuer_mismatch
235
+
236
+
_err ->
237
+
raise Atex.OAuth.Error,
238
+
message: "Failed to validate authorization code or token",
239
+
reason: :token_validation_failed
240
+
end
241
+
end
242
+
243
+
# TODO: logout route
244
+
end
+50
lib/atex/oauth/session.ex
+50
lib/atex/oauth/session.ex
···
1
+
defmodule Atex.OAuth.Session do
2
+
@moduledoc """
3
+
Struct representing an active OAuth session for an AT Protocol user.
4
+
5
+
Contains all the necessary credentials and metadata to make authenticated
6
+
requests to a user's PDS using OAuth with DPoP.
7
+
8
+
## Fields
9
+
10
+
- `:iss` - Authorization server issuer URL
11
+
- `:aud` - PDS endpoint URL (audience)
12
+
- `:sub` - User's DID (subject), used as the session key
13
+
- `:access_token` - OAuth access token for authenticating requests
14
+
- `:refresh_token` - OAuth refresh token for obtaining new access tokens
15
+
- `:expires_at` - When the current access token expires (NaiveDateTime in UTC)
16
+
- `:dpop_key` - DPoP signing key (Demonstrating Proof-of-Possession)
17
+
- `:dpop_nonce` - Server-provided nonce for DPoP proofs (optional, updated per-request)
18
+
19
+
## Usage
20
+
21
+
Sessions are typically created during the OAuth flow and stored in a `SessionStore`.
22
+
They should not be created manually in most cases.
23
+
24
+
session = %Atex.OAuth.Session{
25
+
iss: "https://bsky.social",
26
+
aud: "https://puffball.us-east.host.bsky.network",
27
+
sub: "did:plc:abc123",
28
+
access_token: "...",
29
+
refresh_token: "...",
30
+
expires_at: ~N[2026-01-04 12:00:00],
31
+
dpop_key: dpop_key,
32
+
dpop_nonce: "server-nonce"
33
+
}
34
+
"""
35
+
use TypedStruct
36
+
37
+
typedstruct enforce: true do
38
+
# Authz server issuer
39
+
field :iss, String.t()
40
+
# PDS endpoint
41
+
field :aud, String.t()
42
+
# User's DID
43
+
field :sub, String.t()
44
+
field :access_token, String.t()
45
+
field :refresh_token, String.t()
46
+
field :expires_at, NaiveDateTime.t()
47
+
field :dpop_key, JOSE.JWK.t()
48
+
field :dpop_nonce, String.t() | nil, enforce: false
49
+
end
50
+
end
+121
lib/atex/oauth/session_store/dets.ex
+121
lib/atex/oauth/session_store/dets.ex
···
1
+
defmodule Atex.OAuth.SessionStore.DETS do
2
+
@moduledoc """
3
+
DETS implementation for `Atex.OAuth.SessionStore`.
4
+
5
+
This is recommended for single-node production deployments, as sessions will
6
+
persist on disk between application restarts. For more complex, multi-node
7
+
deployments, consider making a custom implementation using Redis or some other
8
+
distributed store.
9
+
10
+
## Configuration
11
+
12
+
By default the DETS file is stored at `priv/dets/atex_oauth_sessions.dets`
13
+
relative to where your application is running. You can configure the file path
14
+
in your `config.exs`:
15
+
16
+
config :atex, Atex.OAuth.SessionStore.DETS,
17
+
file_path: "/var/lib/myapp/sessions.dets"
18
+
19
+
Parent directories will be created as necessary if possible.
20
+
"""
21
+
22
+
alias Atex.OAuth.Session
23
+
require Logger
24
+
use Supervisor
25
+
26
+
@behaviour Atex.OAuth.SessionStore
27
+
@table :atex_oauth_sessions
28
+
@default_file "priv/dets/atex_oauth_sessions.dets"
29
+
30
+
def start_link(opts) do
31
+
Supervisor.start_link(__MODULE__, opts)
32
+
end
33
+
34
+
@impl Supervisor
35
+
def init(_opts) do
36
+
dets_file =
37
+
case Application.get_env(:atex, __MODULE__, [])[:file_path] do
38
+
nil ->
39
+
@default_file
40
+
41
+
path ->
42
+
path
43
+
end
44
+
45
+
# Ensure parent directory exists
46
+
dets_file
47
+
|> Path.dirname()
48
+
|> File.mkdir_p!()
49
+
50
+
case :dets.open_file(@table, file: String.to_charlist(dets_file), type: :set) do
51
+
{:ok, @table} ->
52
+
Logger.info("DETS session store opened: #{dets_file}")
53
+
Supervisor.init([], strategy: :one_for_one)
54
+
55
+
{:error, reason} ->
56
+
Logger.error("Failed to open DETS file: #{inspect(reason)}")
57
+
raise "Failed to initialize DETS session store: #{inspect(reason)}"
58
+
end
59
+
end
60
+
61
+
@doc """
62
+
Insert a session into the DETS table.
63
+
64
+
Returns `:ok` on success, `{:error, reason}` if an unexpected error occurs.
65
+
"""
66
+
@impl Atex.OAuth.SessionStore
67
+
@spec insert(String.t(), Session.t()) :: :ok | {:error, atom()}
68
+
def insert(key, session) do
69
+
case :dets.insert(@table, {key, session}) do
70
+
:ok ->
71
+
:ok
72
+
73
+
{:error, reason} ->
74
+
Logger.error("DETS insert failed: #{inspect(reason)}")
75
+
{:error, reason}
76
+
end
77
+
end
78
+
79
+
@doc """
80
+
Update a session in the DETS table.
81
+
82
+
In DETS, this is the same as insert - it replaces the existing entry.
83
+
"""
84
+
@impl Atex.OAuth.SessionStore
85
+
@spec update(String.t(), Session.t()) :: :ok | {:error, atom()}
86
+
def update(key, session) do
87
+
insert(key, session)
88
+
end
89
+
90
+
@doc """
91
+
Retrieve a session from the DETS table.
92
+
93
+
Returns `{:ok, session}` if found, `{:error, :not_found}` otherwise.
94
+
"""
95
+
@impl Atex.OAuth.SessionStore
96
+
@spec get(String.t()) :: {:ok, Session.t()} | {:error, atom()}
97
+
def get(key) do
98
+
case :dets.lookup(@table, key) do
99
+
[{_key, session}] -> {:ok, session}
100
+
[] -> {:error, :not_found}
101
+
end
102
+
end
103
+
104
+
@doc """
105
+
Delete a session from the DETS table.
106
+
107
+
Returns `:ok` if deleted, `:noop` if the session didn't exist.
108
+
"""
109
+
@impl Atex.OAuth.SessionStore
110
+
@spec delete(String.t()) :: :ok | :error | :noop
111
+
def delete(key) do
112
+
case get(key) do
113
+
{:ok, _session} ->
114
+
:dets.delete(@table, key)
115
+
:ok
116
+
117
+
{:error, :not_found} ->
118
+
:noop
119
+
end
120
+
end
121
+
end
+88
lib/atex/oauth/session_store/ets.ex
+88
lib/atex/oauth/session_store/ets.ex
···
1
+
defmodule Atex.OAuth.SessionStore.ETS do
2
+
@moduledoc """
3
+
In-memory, ETS implementation for `Atex.OAuth.SessionStore`.
4
+
5
+
This is moreso intended for testing or some occasion where you want the
6
+
session store to be volatile for some reason. It's recommended you use
7
+
`Atex.OAuth.SessionStore.DETS` for single-node production deployments.
8
+
"""
9
+
10
+
alias Atex.OAuth.Session
11
+
require Logger
12
+
use Supervisor
13
+
14
+
@behaviour Atex.OAuth.SessionStore
15
+
@table :atex_oauth_sessions
16
+
17
+
def start_link(opts) do
18
+
Supervisor.start_link(__MODULE__, opts)
19
+
end
20
+
21
+
@impl Supervisor
22
+
def init(_opts) do
23
+
:ets.new(@table, [:set, :public, :named_table])
24
+
Supervisor.init([], strategy: :one_for_one)
25
+
end
26
+
27
+
@doc """
28
+
Insert a session into the ETS table.
29
+
30
+
Returns `:ok` on success, `{:error, :ets}` if an unexpected error occurs.
31
+
"""
32
+
@impl Atex.OAuth.SessionStore
33
+
@spec insert(String.t(), Session.t()) :: :ok | {:error, atom()}
34
+
def insert(key, session) do
35
+
try do
36
+
:ets.insert(@table, {key, session})
37
+
:ok
38
+
rescue
39
+
# Freak accidents can occur
40
+
e ->
41
+
Logger.error(Exception.format(:error, e, __STACKTRACE__))
42
+
{:error, :ets}
43
+
end
44
+
end
45
+
46
+
@doc """
47
+
Update a session in the ETS table.
48
+
49
+
In ETS, this is the same as insert - it replaces the existing entry.
50
+
"""
51
+
@impl Atex.OAuth.SessionStore
52
+
@spec update(String.t(), Session.t()) :: :ok | {:error, atom()}
53
+
def update(key, session) do
54
+
insert(key, session)
55
+
end
56
+
57
+
@doc """
58
+
Retrieve a session from the ETS table.
59
+
60
+
Returns `{:ok, session}` if found, `{:error, :not_found}` otherwise.
61
+
"""
62
+
@impl Atex.OAuth.SessionStore
63
+
@spec get(String.t()) :: {:ok, Session.t()} | {:error, atom()}
64
+
def get(key) do
65
+
case :ets.lookup(@table, key) do
66
+
[{_key, session}] -> {:ok, session}
67
+
[] -> {:error, :not_found}
68
+
end
69
+
end
70
+
71
+
@doc """
72
+
Delete a session from the ETS table.
73
+
74
+
Returns `:ok` if deleted, `:noop` if the session didn't exist.
75
+
"""
76
+
@impl Atex.OAuth.SessionStore
77
+
@spec delete(String.t()) :: :ok | :error | :noop
78
+
def delete(key) do
79
+
case get(key) do
80
+
{:ok, _session} ->
81
+
:ets.delete(@table, key)
82
+
:ok
83
+
84
+
{:error, :not_found} ->
85
+
:noop
86
+
end
87
+
end
88
+
end
+119
lib/atex/oauth/session_store.ex
+119
lib/atex/oauth/session_store.ex
···
1
+
defmodule Atex.OAuth.SessionStore do
2
+
@moduledoc """
3
+
Storage interface for OAuth sessions.
4
+
5
+
Provides a behaviour for implementing session storage backends, and functions
6
+
to operate the backend using `Atex.OAuth.Session`
7
+
8
+
## Configuration
9
+
10
+
The default implementation for the store is `Atex.OAuth.SessionStore.DETS`;
11
+
this can be changed to a custom implementation in your config.exs:
12
+
13
+
config :atex, :session_store, Atex.OAuth.SessionStore.ETS
14
+
15
+
DETS is the default implementation as it provides simple, on-disk storage for
16
+
sessions so they don't get discarded on an application restart, but a regular
17
+
ETS implementation is also provided out-of-the-box for testing or other
18
+
circumstances.
19
+
20
+
For multi-node deployments, you can write your own implementation using a
21
+
custom backend, such as Redis, by implementing the behaviour callbacks.
22
+
23
+
## Usage
24
+
25
+
Sessions are keyed by the user's DID (`sub` field).
26
+
27
+
session = %Atex.OAuth.Session{
28
+
iss: "https://bsky.social",
29
+
aud: "https://puffball.us-east.host.bsky.network",
30
+
sub: "did:plc:abc123",
31
+
access_token: "...",
32
+
refresh_token: "...",
33
+
expires_at: ~N[2026-01-04 12:00:00],
34
+
dpop_key: dpop_key,
35
+
dpop_nonce: "server-nonce"
36
+
}
37
+
38
+
# Insert a new session
39
+
:ok = Atex.OAuth.SessionStore.insert(session)
40
+
41
+
# Retrieve a session
42
+
{:ok, session} = Atex.OAuth.SessionStore.get("did:plc:abc123")
43
+
44
+
# Update an existing session (e.g., after token refresh)
45
+
updated_session = %{session | access_token: new_token}
46
+
:ok = Atex.OAuth.SessionStore.update(updated_session)
47
+
48
+
# Delete a session
49
+
Atex.OAuth.SessionStore.delete(session)
50
+
"""
51
+
52
+
@store Application.compile_env(:atex, :session_store, Atex.OAuth.SessionStore.DETS)
53
+
54
+
@doc """
55
+
Retrieve a session by DID.
56
+
57
+
Returns `{:ok, session}` if found, `{:error, :not_found}` otherwise.
58
+
"""
59
+
@callback get(key :: String.t()) :: {:ok, Atex.OAuth.Session.t()} | {:error, atom()}
60
+
61
+
@doc """
62
+
Insert a new session.
63
+
64
+
The key is the user's DID (`session.sub`). Returns `:ok` on success.
65
+
"""
66
+
@callback insert(key :: String.t(), session :: Atex.OAuth.Session.t()) ::
67
+
:ok | {:error, atom()}
68
+
69
+
@doc """
70
+
Update an existing session.
71
+
72
+
Replaces the existing session data for the given key. Returns `:ok` on success.
73
+
"""
74
+
@callback update(key :: String.t(), session :: Atex.OAuth.Session.t()) ::
75
+
:ok | {:error, atom()}
76
+
77
+
@doc """
78
+
Delete a session.
79
+
80
+
Returns `:ok` if deleted, `:noop` if the session didn't exist, :error if it failed.
81
+
"""
82
+
@callback delete(key :: String.t()) :: :ok | :error | :noop
83
+
84
+
@callback child_spec(any()) :: Supervisor.child_spec()
85
+
86
+
defdelegate child_spec(opts), to: @store
87
+
88
+
@doc """
89
+
Retrieve a session by DID.
90
+
"""
91
+
@spec get(String.t()) :: {:ok, Atex.OAuth.Session.t()} | {:error, atom()}
92
+
def get(key) do
93
+
@store.get(key)
94
+
end
95
+
96
+
@doc """
97
+
Insert a new session.
98
+
"""
99
+
@spec insert(Atex.OAuth.Session.t()) :: :ok | {:error, atom()}
100
+
def insert(session) do
101
+
@store.insert(session.sub, session)
102
+
end
103
+
104
+
@doc """
105
+
Update an existing session.
106
+
"""
107
+
@spec update(Atex.OAuth.Session.t()) :: :ok | {:error, atom()}
108
+
def update(session) do
109
+
@store.update(session.sub, session)
110
+
end
111
+
112
+
@doc """
113
+
Delete a session.
114
+
"""
115
+
@callback delete(Atex.OAuth.Session.t()) :: :ok | :error | :noop
116
+
def delete(session) do
117
+
@store.delete(session.sub)
118
+
end
119
+
end
+582
lib/atex/oauth.ex
+582
lib/atex/oauth.ex
···
1
+
defmodule Atex.OAuth do
2
+
@moduledoc """
3
+
OAuth 2.0 implementation for AT Protocol authentication.
4
+
5
+
This module provides utilities for implementing OAuth flows compliant with the
6
+
AT Protocol specification. It includes support for:
7
+
8
+
- Pushed Authorization Requests (PAR)
9
+
- DPoP (Demonstration of Proof of Possession) tokens
10
+
- JWT client assertions
11
+
- PKCE (Proof Key for Code Exchange)
12
+
- Token refresh
13
+
- Handle to PDS resolution
14
+
15
+
## Configuration
16
+
17
+
See `Atex.Config.OAuth` module for configuration documentation.
18
+
19
+
## Usage Example
20
+
21
+
iex> pds = "https://bsky.social"
22
+
iex> login_hint = "example.com"
23
+
iex> {:ok, authz_server} = Atex.OAuth.get_authorization_server(pds)
24
+
iex> {:ok, authz_metadata} = Atex.OAuth.get_authorization_server_metadata(authz_server)
25
+
iex> state = Atex.OAuth.create_nonce()
26
+
iex> code_verifier = Atex.OAuth.create_nonce()
27
+
iex> {:ok, auth_url} = Atex.OAuth.create_authorization_url(
28
+
authz_metadata,
29
+
state,
30
+
code_verifier,
31
+
login_hint
32
+
)
33
+
"""
34
+
35
+
@type authorization_metadata() :: %{
36
+
issuer: String.t(),
37
+
par_endpoint: String.t(),
38
+
token_endpoint: String.t(),
39
+
authorization_endpoint: String.t()
40
+
}
41
+
42
+
@type tokens() :: %{
43
+
access_token: String.t(),
44
+
refresh_token: String.t(),
45
+
did: String.t(),
46
+
expires_at: NaiveDateTime.t()
47
+
}
48
+
49
+
alias Atex.Config.OAuth, as: Config
50
+
51
+
@doc """
52
+
Get a map cnotaining the client metadata information needed for an
53
+
authorization server to validate this client.
54
+
"""
55
+
@spec create_client_metadata() :: map()
56
+
def create_client_metadata() do
57
+
key = Config.get_key()
58
+
{_, jwk} = key |> JOSE.JWK.to_public_map()
59
+
jwk = Map.merge(jwk, %{use: "sig", kid: key.fields["kid"]})
60
+
61
+
%{
62
+
client_id: Config.client_id(),
63
+
redirect_uris: [Config.redirect_uri() | Config.extra_redirect_uris()],
64
+
application_type: "web",
65
+
grant_types: ["authorization_code", "refresh_token"],
66
+
scope: Config.scopes(),
67
+
response_type: ["code"],
68
+
token_endpoint_auth_method: "private_key_jwt",
69
+
token_endpoint_auth_signing_alg: "ES256",
70
+
dpop_bound_access_tokens: true,
71
+
jwks: %{keys: [jwk]}
72
+
}
73
+
end
74
+
75
+
@doc """
76
+
Retrieves the configured JWT private key for signing client assertions.
77
+
78
+
Loads the private key from configuration, decodes the base64-encoded DER data,
79
+
and creates a JOSE JWK structure with the key ID field set.
80
+
81
+
## Returns
82
+
83
+
A `JOSE.JWK` struct containing the private key and key identifier.
84
+
85
+
## Raises
86
+
87
+
* `Application.Env.Error` if the private_key or key_id configuration is missing
88
+
89
+
## Examples
90
+
91
+
key = OAuth.get_key()
92
+
key = OAuth.get_key()
93
+
"""
94
+
@spec get_key() :: JOSE.JWK.t()
95
+
def get_key(), do: Config.get_key()
96
+
97
+
@doc false
98
+
@spec random_b64(integer()) :: String.t()
99
+
def random_b64(length) do
100
+
:crypto.strong_rand_bytes(length)
101
+
|> Base.url_encode64(padding: false)
102
+
end
103
+
104
+
@doc false
105
+
@spec create_nonce() :: String.t()
106
+
def create_nonce(), do: random_b64(32)
107
+
108
+
@doc """
109
+
Create an OAuth authorization URL for a PDS.
110
+
111
+
Submits a PAR request to the authorization server and constructs the
112
+
authorization URL with the returned request URI. Supports PKCE, DPoP, and
113
+
client assertions as required by the AT Protocol.
114
+
115
+
## Parameters
116
+
117
+
- `authz_metadata` - Authorization server metadata containing endpoints, fetched from `get_authorization_server_metadata/1`
118
+
- `state` - Random token for session validation
119
+
- `code_verifier` - PKCE code verifier
120
+
- `login_hint` - User identifier (handle or DID) for pre-filled login
121
+
122
+
## Returns
123
+
124
+
- `{:ok, authorization_url}` - Successfully created authorization URL
125
+
- `{:ok, :invalid_par_response}` - Server respondend incorrectly to the request
126
+
- `{:error, reason}` - Error creating authorization URL
127
+
"""
128
+
@spec create_authorization_url(
129
+
authorization_metadata(),
130
+
String.t(),
131
+
String.t(),
132
+
String.t()
133
+
) :: {:ok, String.t()} | {:error, any()}
134
+
def create_authorization_url(
135
+
authz_metadata,
136
+
state,
137
+
code_verifier,
138
+
login_hint
139
+
) do
140
+
code_challenge = :crypto.hash(:sha256, code_verifier) |> Base.url_encode64(padding: false)
141
+
key = get_key()
142
+
143
+
client_assertion =
144
+
create_client_assertion(key, Config.client_id(), authz_metadata.issuer)
145
+
146
+
body =
147
+
%{
148
+
response_type: "code",
149
+
client_id: Config.client_id(),
150
+
redirect_uri: Config.redirect_uri(),
151
+
state: state,
152
+
code_challenge_method: "S256",
153
+
code_challenge: code_challenge,
154
+
scope: Config.scopes(),
155
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
156
+
client_assertion: client_assertion,
157
+
login_hint: login_hint
158
+
}
159
+
160
+
case Req.post(authz_metadata.par_endpoint, form: body) do
161
+
{:ok, %{body: %{"request_uri" => request_uri}}} ->
162
+
query =
163
+
%{client_id: Config.client_id(), request_uri: request_uri}
164
+
|> URI.encode_query()
165
+
166
+
{:ok, "#{authz_metadata.authorization_endpoint}?#{query}"}
167
+
168
+
{:ok, _} ->
169
+
{:error, :invalid_par_response}
170
+
171
+
err ->
172
+
err
173
+
end
174
+
end
175
+
176
+
@doc """
177
+
Exchange an OAuth authorization code for a set of access and refresh tokens.
178
+
179
+
Validates the authorization code by submitting it to the token endpoint along with
180
+
the PKCE code verifier and client assertion. Returns access tokens for making authenticated
181
+
requests to the relevant user's PDS.
182
+
183
+
## Parameters
184
+
185
+
- `authz_metadata` - Authorization server metadata containing token endpoint
186
+
- `dpop_key` - JWK for DPoP token generation
187
+
- `code` - Authorization code from OAuth callback
188
+
- `code_verifier` - PKCE code verifier from authorization flow
189
+
190
+
## Returns
191
+
192
+
- `{:ok, tokens, nonce}` - Successfully obtained tokens with returned DPoP nonce
193
+
- `{:error, reason}` - Error exchanging code for tokens
194
+
"""
195
+
@spec validate_authorization_code(
196
+
authorization_metadata(),
197
+
JOSE.JWK.t(),
198
+
String.t(),
199
+
String.t()
200
+
) :: {:ok, tokens(), String.t()} | {:error, any()}
201
+
def validate_authorization_code(
202
+
authz_metadata,
203
+
dpop_key,
204
+
code,
205
+
code_verifier
206
+
) do
207
+
key = get_key()
208
+
209
+
client_assertion =
210
+
create_client_assertion(key, Config.client_id(), authz_metadata.issuer)
211
+
212
+
body =
213
+
%{
214
+
grant_type: "authorization_code",
215
+
client_id: Config.client_id(),
216
+
redirect_uri: Config.redirect_uri(),
217
+
code: code,
218
+
code_verifier: code_verifier,
219
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
220
+
client_assertion: client_assertion
221
+
}
222
+
223
+
Req.new(method: :post, url: authz_metadata.token_endpoint, form: body)
224
+
|> send_oauth_dpop_request(dpop_key)
225
+
|> case do
226
+
{:ok,
227
+
%{
228
+
"access_token" => access_token,
229
+
"refresh_token" => refresh_token,
230
+
"expires_in" => expires_in,
231
+
"sub" => did
232
+
}, nonce} ->
233
+
expires_at = NaiveDateTime.utc_now() |> NaiveDateTime.add(expires_in, :second)
234
+
235
+
{:ok,
236
+
%{
237
+
access_token: access_token,
238
+
refresh_token: refresh_token,
239
+
did: did,
240
+
expires_at: expires_at
241
+
}, nonce}
242
+
243
+
err ->
244
+
err
245
+
end
246
+
end
247
+
248
+
def refresh_token(refresh_token, dpop_key, issuer, token_endpoint) do
249
+
key = get_key()
250
+
251
+
client_assertion =
252
+
create_client_assertion(key, Config.client_id(), issuer)
253
+
254
+
body = %{
255
+
grant_type: "refresh_token",
256
+
refresh_token: refresh_token,
257
+
client_id: Config.client_id(),
258
+
client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
259
+
client_assertion: client_assertion
260
+
}
261
+
262
+
Req.new(method: :post, url: token_endpoint, form: body)
263
+
|> send_oauth_dpop_request(dpop_key)
264
+
|> case do
265
+
{:ok,
266
+
%{
267
+
"access_token" => access_token,
268
+
"refresh_token" => refresh_token,
269
+
"expires_in" => expires_in,
270
+
"sub" => did
271
+
}, nonce} ->
272
+
expires_at = NaiveDateTime.utc_now() |> NaiveDateTime.add(expires_in, :second)
273
+
274
+
{:ok,
275
+
%{
276
+
access_token: access_token,
277
+
refresh_token: refresh_token,
278
+
did: did,
279
+
expires_at: expires_at
280
+
}, nonce}
281
+
282
+
err ->
283
+
err
284
+
end
285
+
end
286
+
287
+
@doc """
288
+
Fetch the authorization server for a given Personal Data Server (PDS).
289
+
290
+
Makes a request to the PDS's `.well-known/oauth-protected-resource` endpoint
291
+
to discover the associated authorization server that should be used for the
292
+
OAuth flow. Results are cached for 1 hour to reduce load on third-party PDSs.
293
+
294
+
## Parameters
295
+
296
+
- `pds_host` - Base URL of the PDS (e.g., "https://bsky.social")
297
+
- `fresh` - If `true`, bypasses the cache and fetches fresh data (default: `false`)
298
+
299
+
## Returns
300
+
301
+
- `{:ok, authorization_server}` - Successfully discovered authorization
302
+
server URL
303
+
- `{:error, :invalid_metadata}` - Server returned invalid metadata
304
+
- `{:error, reason}` - Error discovering authorization server
305
+
"""
306
+
@spec get_authorization_server(String.t(), boolean()) :: {:ok, String.t()} | {:error, any()}
307
+
def get_authorization_server(pds_host, fresh \\ false) do
308
+
if fresh do
309
+
fetch_authorization_server(pds_host)
310
+
else
311
+
case Atex.OAuth.Cache.get_authorization_server(pds_host) do
312
+
{:ok, authz_server} ->
313
+
{:ok, authz_server}
314
+
315
+
{:error, :not_found} ->
316
+
fetch_authorization_server(pds_host)
317
+
end
318
+
end
319
+
end
320
+
321
+
defp fetch_authorization_server(pds_host) do
322
+
result =
323
+
"#{pds_host}/.well-known/oauth-protected-resource"
324
+
|> Req.get()
325
+
|> case do
326
+
# TODO: what to do when multiple authorization servers?
327
+
{:ok, %{body: %{"authorization_servers" => [authz_server | _]}}} -> {:ok, authz_server}
328
+
{:ok, _} -> {:error, :invalid_metadata}
329
+
err -> err
330
+
end
331
+
332
+
case result do
333
+
{:ok, authz_server} ->
334
+
Atex.OAuth.Cache.set_authorization_server(pds_host, authz_server)
335
+
{:ok, authz_server}
336
+
337
+
error ->
338
+
error
339
+
end
340
+
end
341
+
342
+
@doc """
343
+
Fetch the metadata for an OAuth authorization server.
344
+
345
+
Retrieves the metadata from the authorization server's
346
+
`.well-known/oauth-authorization-server` endpoint, providing endpoint URLs
347
+
required for the OAuth flow. Results are cached for 1 hour to reduce load on
348
+
third-party PDSs.
349
+
350
+
## Parameters
351
+
352
+
- `issuer` - Authorization server issuer URL
353
+
- `fresh` - If `true`, bypasses the cache and fetches fresh data (default: `false`)
354
+
355
+
## Returns
356
+
357
+
- `{:ok, metadata}` - Successfully retrieved authorization server metadata
358
+
- `{:error, :invalid_metadata}` - Server returned invalid metadata
359
+
- `{:error, :invalid_issuer}` - Issuer mismatch in metadata
360
+
- `{:error, any()}` - Other error fetching metadata
361
+
"""
362
+
@spec get_authorization_server_metadata(String.t(), boolean()) ::
363
+
{:ok, authorization_metadata()} | {:error, any()}
364
+
def get_authorization_server_metadata(issuer, fresh \\ false) do
365
+
if fresh do
366
+
fetch_authorization_server_metadata(issuer)
367
+
else
368
+
case Atex.OAuth.Cache.get_authorization_server_metadata(issuer) do
369
+
{:ok, metadata} ->
370
+
{:ok, metadata}
371
+
372
+
{:error, :not_found} ->
373
+
fetch_authorization_server_metadata(issuer)
374
+
end
375
+
end
376
+
end
377
+
378
+
defp fetch_authorization_server_metadata(issuer) do
379
+
result =
380
+
"#{issuer}/.well-known/oauth-authorization-server"
381
+
|> Req.get()
382
+
|> case do
383
+
{:ok,
384
+
%{
385
+
body: %{
386
+
"issuer" => metadata_issuer,
387
+
"pushed_authorization_request_endpoint" => par_endpoint,
388
+
"token_endpoint" => token_endpoint,
389
+
"authorization_endpoint" => authorization_endpoint
390
+
}
391
+
}} ->
392
+
if issuer != metadata_issuer do
393
+
{:error, :invaild_issuer}
394
+
else
395
+
{:ok,
396
+
%{
397
+
issuer: metadata_issuer,
398
+
par_endpoint: par_endpoint,
399
+
token_endpoint: token_endpoint,
400
+
authorization_endpoint: authorization_endpoint
401
+
}}
402
+
end
403
+
404
+
{:ok, _} ->
405
+
{:error, :invalid_metadata}
406
+
407
+
err ->
408
+
err
409
+
end
410
+
411
+
case result do
412
+
{:ok, metadata} ->
413
+
Atex.OAuth.Cache.set_authorization_server_metadata(issuer, metadata)
414
+
{:ok, metadata}
415
+
416
+
error ->
417
+
error
418
+
end
419
+
end
420
+
421
+
@spec send_oauth_dpop_request(Req.Request.t(), JOSE.JWK.t(), String.t() | nil) ::
422
+
{:ok, map(), String.t()} | {:error, any(), String.t()}
423
+
def send_oauth_dpop_request(request, dpop_key, nonce \\ nil) do
424
+
dpop_token = create_dpop_token(dpop_key, request, nonce)
425
+
426
+
request
427
+
|> Req.Request.put_header("dpop", dpop_token)
428
+
|> Req.request()
429
+
|> case do
430
+
{:ok, resp} ->
431
+
dpop_nonce =
432
+
case resp.headers["dpop-nonce"] do
433
+
[new_nonce | _] -> new_nonce
434
+
_ -> nonce
435
+
end
436
+
437
+
cond do
438
+
resp.status == 200 ->
439
+
{:ok, resp.body, dpop_nonce}
440
+
441
+
resp.body["error"] === "use_dpop_nonce" ->
442
+
dpop_token = create_dpop_token(dpop_key, request, dpop_nonce)
443
+
444
+
request
445
+
|> Req.Request.put_header("dpop", dpop_token)
446
+
|> Req.request()
447
+
|> case do
448
+
{:ok, %{status: 200, body: body}} ->
449
+
{:ok, body, dpop_nonce}
450
+
451
+
{:ok, %{body: %{"error" => error, "error_description" => error_description}}} ->
452
+
{:error, {:oauth_error, error, error_description}, dpop_nonce}
453
+
454
+
{:ok, _} ->
455
+
{:error, :unexpected_response, dpop_nonce}
456
+
457
+
{:error, err} ->
458
+
{:error, err, dpop_nonce}
459
+
end
460
+
461
+
true ->
462
+
{:error, {:oauth_error, resp.body["error"], resp.body["error_description"]},
463
+
dpop_nonce}
464
+
end
465
+
466
+
{:error, err} ->
467
+
{:error, err, nonce}
468
+
end
469
+
end
470
+
471
+
@spec request_protected_dpop_resource(
472
+
Req.Request.t(),
473
+
String.t(),
474
+
String.t(),
475
+
JOSE.JWK.t(),
476
+
String.t() | nil
477
+
) :: {:ok, Req.Response.t(), String.t() | nil} | {:error, any()}
478
+
def request_protected_dpop_resource(request, issuer, access_token, dpop_key, nonce \\ nil) do
479
+
access_token_hash = :crypto.hash(:sha256, access_token) |> Base.url_encode64(padding: false)
480
+
# access_token_hash = Base.url_encode64(access_token, padding: false)
481
+
482
+
dpop_token =
483
+
create_dpop_token(dpop_key, request, nonce, %{iss: issuer, ath: access_token_hash})
484
+
485
+
request
486
+
|> Req.Request.put_header("dpop", dpop_token)
487
+
|> Req.request()
488
+
|> case do
489
+
{:ok, resp} ->
490
+
dpop_nonce =
491
+
case resp.headers["dpop-nonce"] do
492
+
[new_nonce | _] -> new_nonce
493
+
_ -> nonce
494
+
end
495
+
496
+
www_authenticate = Req.Response.get_header(resp, "www-authenticate")
497
+
498
+
www_dpop_problem =
499
+
www_authenticate != [] && String.starts_with?(Enum.at(www_authenticate, 0), "DPoP")
500
+
501
+
if resp.status != 401 || !www_dpop_problem do
502
+
{:ok, resp, dpop_nonce}
503
+
else
504
+
dpop_token =
505
+
create_dpop_token(dpop_key, request, dpop_nonce, %{
506
+
iss: issuer,
507
+
ath: access_token_hash
508
+
})
509
+
510
+
request
511
+
|> Req.Request.put_header("dpop", dpop_token)
512
+
|> Req.request()
513
+
|> case do
514
+
{:ok, resp} ->
515
+
dpop_nonce =
516
+
case resp.headers["dpop-nonce"] do
517
+
[new_nonce | _] -> new_nonce
518
+
_ -> dpop_nonce
519
+
end
520
+
521
+
{:ok, resp, dpop_nonce}
522
+
523
+
err ->
524
+
err
525
+
end
526
+
end
527
+
528
+
err ->
529
+
err
530
+
end
531
+
end
532
+
533
+
@spec create_client_assertion(JOSE.JWK.t(), String.t(), String.t()) :: String.t()
534
+
def create_client_assertion(jwk, client_id, issuer) do
535
+
iat = System.os_time(:second)
536
+
jti = random_b64(20)
537
+
jws = %{"alg" => "ES256", "kid" => jwk.fields["kid"]}
538
+
539
+
jwt = %{
540
+
iss: client_id,
541
+
sub: client_id,
542
+
aud: issuer,
543
+
jti: jti,
544
+
iat: iat,
545
+
exp: iat + 60
546
+
}
547
+
548
+
JOSE.JWT.sign(jwk, jws, jwt)
549
+
|> JOSE.JWS.compact()
550
+
|> elem(1)
551
+
end
552
+
553
+
@spec create_dpop_token(JOSE.JWK.t(), Req.Request.t(), any(), map()) :: String.t()
554
+
def create_dpop_token(jwk, request, nonce \\ nil, attrs \\ %{}) do
555
+
iat = System.os_time(:second)
556
+
jti = random_b64(20)
557
+
{_, public_jwk} = JOSE.JWK.to_public_map(jwk)
558
+
jws = %{"alg" => "ES256", "typ" => "dpop+jwt", "jwk" => public_jwk}
559
+
[request_url | _] = request.url |> to_string() |> String.split("?")
560
+
561
+
jwt =
562
+
Map.merge(attrs, %{
563
+
jti: jti,
564
+
htm: atom_to_upcase_string(request.method),
565
+
htu: request_url,
566
+
iat: iat
567
+
})
568
+
|> then(fn m ->
569
+
if nonce, do: Map.put(m, :nonce, nonce), else: m
570
+
end)
571
+
572
+
JOSE.JWT.sign(jwk, jws, jwt)
573
+
|> JOSE.JWS.compact()
574
+
|> elem(1)
575
+
end
576
+
577
+
@doc false
578
+
@spec atom_to_upcase_string(atom()) :: String.t()
579
+
def atom_to_upcase_string(atom) do
580
+
atom |> to_string() |> String.upcase()
581
+
end
582
+
end
+18
-74
lib/atex/xrpc/client.ex
+18
-74
lib/atex/xrpc/client.ex
···
1
1
defmodule Atex.XRPC.Client do
2
2
@moduledoc """
3
-
Struct to store client information for ATProto XRPC.
4
-
"""
3
+
Behaviour that defines the interface for XRPC clients.
5
4
6
-
alias Atex.{XRPC, HTTP}
7
-
use TypedStruct
5
+
This behaviour allows different types of clients (login-based, OAuth-based, etc.)
6
+
to implement authentication and request handling while maintaining a consistent interface.
8
7
9
-
typedstruct do
10
-
field :endpoint, String.t(), enforce: true
11
-
field :access_token, String.t() | nil
12
-
field :refresh_token, String.t() | nil
13
-
end
14
-
15
-
@doc """
16
-
Create a new `Atex.XRPC.Client` from an endpoint, and optionally an
17
-
access/refresh token.
18
-
19
-
Endpoint should be the base URL of a PDS, or an AppView in the case of
20
-
unauthenticated requests (like Bluesky's public API), e.g.
21
-
`https://bsky.social`.
8
+
Implementations must handle token refresh internally when requests fail due to
9
+
expired tokens, and return both the result and potentially updated client state.
22
10
"""
23
-
@spec new(String.t()) :: t()
24
-
@spec new(String.t(), String.t() | nil) :: t()
25
-
@spec new(String.t(), String.t() | nil, String.t() | nil) :: t()
26
-
def new(endpoint, access_token \\ nil, refresh_token \\ nil) do
27
-
%__MODULE__{endpoint: endpoint, access_token: access_token, refresh_token: refresh_token}
28
-
end
11
+
12
+
@type client :: struct()
13
+
@type request_opts :: keyword()
14
+
@type request_result :: {:ok, Req.Response.t(), client()} | {:error, any(), client()}
29
15
30
16
@doc """
31
-
Create a new `Atex.XRPC.Client` by logging in with an `identifier` and
32
-
`password` to fetch an initial pair of access & refresh tokens.
17
+
Perform an authenticated HTTP GET request on an XRPC resource.
33
18
34
-
Uses `com.atproto.server.createSession` under the hood, so `identifier` can be
35
-
either a handle or a DID.
36
-
37
-
## Examples
38
-
39
-
iex> Atex.XRPC.Client.login("https://bsky.social", "example.com", "password123")
40
-
{:ok, %Atex.XRPC.Client{...}}
19
+
Implementations should handle token refresh if the request fails due to
20
+
expired authentication, returning both the response and the (potentially updated) client.
41
21
"""
42
-
@spec login(String.t(), String.t(), String.t()) :: {:ok, t()} | HTTP.Adapter.error()
43
-
@spec login(String.t(), String.t(), String.t(), String.t() | nil) ::
44
-
{:ok, t()} | HTTP.Adapter.error()
45
-
def login(endpoint, identifier, password, auth_factor_token \\ nil) do
46
-
json =
47
-
%{identifier: identifier, password: password}
48
-
|> then(
49
-
&if auth_factor_token do
50
-
Map.merge(&1, %{authFactorToken: auth_factor_token})
51
-
else
52
-
&1
53
-
end
54
-
)
55
-
56
-
response = XRPC.unauthed_post(endpoint, "com.atproto.server.createSession", json: json)
57
-
58
-
case response do
59
-
{:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
60
-
{:ok, new(endpoint, access_token, refresh_token)}
61
-
62
-
err ->
63
-
err
64
-
end
65
-
end
22
+
@callback get(client(), String.t(), request_opts()) :: request_result()
66
23
67
24
@doc """
68
-
Request a new `refresh_token` for the given client.
25
+
Perform an authenticated HTTP POST request on an XRPC resource.
26
+
27
+
Implementations should handle token refresh if the request fails due to
28
+
expired authentication, returning both the response and the (potentially updated) client.
69
29
"""
70
-
@spec refresh(t()) :: {:ok, t()} | HTTP.Adapter.error()
71
-
def refresh(%__MODULE__{endpoint: endpoint, refresh_token: refresh_token} = client) do
72
-
response =
73
-
XRPC.unauthed_post(
74
-
endpoint,
75
-
"com.atproto.server.refreshSession",
76
-
XRPC.put_auth([], refresh_token)
77
-
)
78
-
79
-
case response do
80
-
{:ok, %{"accessJwt" => access_token, "refreshJwt" => refresh_token}} ->
81
-
%{client | access_token: access_token, refresh_token: refresh_token}
82
-
83
-
err ->
84
-
err
85
-
end
86
-
end
30
+
@callback post(client(), String.t(), request_opts()) :: request_result()
87
31
end
+148
lib/atex/xrpc/login_client.ex
+148
lib/atex/xrpc/login_client.ex
···
1
+
defmodule Atex.XRPC.LoginClient do
2
+
alias Atex.XRPC
3
+
use TypedStruct
4
+
5
+
@behaviour Atex.XRPC.Client
6
+
7
+
typedstruct do
8
+
field :endpoint, String.t(), enforce: true
9
+
field :access_token, String.t() | nil
10
+
field :refresh_token, String.t() | nil
11
+
end
12
+
13
+
@doc """
14
+
Create a new `Atex.XRPC.LoginClient` from an endpoint, and optionally an
15
+
existing access/refresh token.
16
+
17
+
Endpoint should be the base URL of a PDS, or an AppView in the case of
18
+
unauthenticated requests (like Bluesky's public API), e.g.
19
+
`https://bsky.social`.
20
+
"""
21
+
@spec new(String.t(), String.t() | nil, String.t() | nil) :: t()
22
+
def new(endpoint, access_token \\ nil, refresh_token \\ nil) do
23
+
%__MODULE__{endpoint: endpoint, access_token: access_token, refresh_token: refresh_token}
24
+
end
25
+
26
+
@doc """
27
+
Create a new `Atex.XRPC.LoginClient` by logging in with an `identifier` and
28
+
`password` to fetch an initial pair of access & refresh tokens.
29
+
30
+
Also supports providing a MFA token in the situation that is required.
31
+
32
+
Uses `com.atproto.server.createSession` under the hood, so `identifier` can be
33
+
either a handle or a DID.
34
+
35
+
## Examples
36
+
37
+
iex> Atex.XRPC.LoginClient.login("https://bsky.social", "example.com", "password123")
38
+
{:ok, %Atex.XRPC.LoginClient{...}}
39
+
"""
40
+
@spec login(String.t(), String.t(), String.t()) :: {:ok, t()} | {:error, any()}
41
+
@spec login(String.t(), String.t(), String.t(), String.t() | nil) ::
42
+
{:ok, t()} | {:error, any()}
43
+
def login(endpoint, identifier, password, auth_factor_token \\ nil) do
44
+
json =
45
+
%{identifier: identifier, password: password}
46
+
|> then(
47
+
&if auth_factor_token do
48
+
Map.merge(&1, %{authFactorToken: auth_factor_token})
49
+
else
50
+
&1
51
+
end
52
+
)
53
+
54
+
response = XRPC.unauthed_post(endpoint, "com.atproto.server.createSession", json: json)
55
+
56
+
case response do
57
+
{:ok, %{body: %{"accessJwt" => access_token, "refreshJwt" => refresh_token}}} ->
58
+
{:ok, new(endpoint, access_token, refresh_token)}
59
+
60
+
err ->
61
+
err
62
+
end
63
+
end
64
+
65
+
@doc """
66
+
Request a new `refresh_token` for the given client.
67
+
"""
68
+
@spec refresh(t()) :: {:ok, t()} | {:error, any()}
69
+
def refresh(%__MODULE__{endpoint: endpoint, refresh_token: refresh_token} = client) do
70
+
request =
71
+
Req.new(method: :post, url: XRPC.url(endpoint, "com.atproto.server.refreshSession"))
72
+
|> put_auth(refresh_token)
73
+
74
+
case Req.request(request) do
75
+
{:ok, %{body: %{"accessJwt" => access_token, "refreshJwt" => refresh_token}}} ->
76
+
{:ok, %{client | access_token: access_token, refresh_token: refresh_token}}
77
+
78
+
{:ok, response} ->
79
+
{:error, response}
80
+
81
+
err ->
82
+
err
83
+
end
84
+
end
85
+
86
+
@impl true
87
+
def get(%__MODULE__{} = client, resource, opts \\ []) do
88
+
request(client, opts ++ [method: :get, url: XRPC.url(client.endpoint, resource)])
89
+
end
90
+
91
+
@impl true
92
+
def post(%__MODULE__{} = client, resource, opts \\ []) do
93
+
request(client, opts ++ [method: :post, url: XRPC.url(client.endpoint, resource)])
94
+
end
95
+
96
+
@spec request(t(), keyword()) :: {:ok, Req.Response.t(), t()} | {:error, any()}
97
+
defp request(client, opts) do
98
+
with {:ok, client} <- validate_client(client) do
99
+
request = opts |> Req.new() |> put_auth(client.access_token)
100
+
101
+
case Req.request(request) do
102
+
{:ok, %{status: 200} = response} ->
103
+
{:ok, response, client}
104
+
105
+
{:ok, response} ->
106
+
handle_failure(client, response, request)
107
+
108
+
err ->
109
+
err
110
+
end
111
+
end
112
+
end
113
+
114
+
@spec handle_failure(t(), Req.Response.t(), Req.Request.t()) ::
115
+
{:ok, Req.Response.t(), t()} | {:error, any()}
116
+
defp handle_failure(client, response, request) do
117
+
IO.inspect(response, label: "got failure")
118
+
119
+
if auth_error?(response.body) and client.refresh_token do
120
+
case refresh(client) do
121
+
{:ok, client} ->
122
+
case Req.request(put_auth(request, client.access_token)) do
123
+
{:ok, %{status: 200} = response} -> {:ok, response, client}
124
+
{:ok, response} -> {:error, response}
125
+
err -> err
126
+
end
127
+
128
+
err ->
129
+
err
130
+
end
131
+
else
132
+
{:error, response}
133
+
end
134
+
end
135
+
136
+
@spec validate_client(t()) :: {:ok, t()} | {:error, any()}
137
+
defp validate_client(%__MODULE__{access_token: nil}), do: {:error, :no_token}
138
+
defp validate_client(%__MODULE__{} = client), do: {:ok, client}
139
+
140
+
@spec auth_error?(body :: Req.Response.t()) :: boolean()
141
+
defp auth_error?(%{status: status}) when status in [401, 403], do: true
142
+
defp auth_error?(%{body: %{"error" => "InvalidToken"}}), do: true
143
+
defp auth_error?(_response), do: false
144
+
145
+
@spec put_auth(Req.Request.t(), String.t()) :: Req.Request.t()
146
+
defp put_auth(request, token),
147
+
do: Req.Request.put_header(request, "authorization", "Bearer #{token}")
148
+
end
+292
lib/atex/xrpc/oauth_client.ex
+292
lib/atex/xrpc/oauth_client.ex
···
1
+
defmodule Atex.XRPC.OAuthClient do
2
+
@moduledoc """
3
+
OAuth client for making authenticated XRPC requests to AT Protocol servers.
4
+
5
+
The client contains a user's DID and talks to `Atex.OAuth.SessionStore` to
6
+
retrieve sessions internally to make requests. As a result, it will only work
7
+
for users that have gone through an OAuth flow; see `Atex.OAuth.Plug` for an
8
+
existing method of doing that.
9
+
10
+
The entire OAuth session lifecycle is handled transparently, with the access
11
+
token being refreshed automatically as required.
12
+
13
+
## Usage
14
+
15
+
# Create from an existing OAuth session
16
+
{:ok, client} = Atex.XRPC.OAuthClient.new("did:plc:abc123")
17
+
18
+
# Or extract from a Plug.Conn after OAuth flow
19
+
{:ok, client} = Atex.XRPC.OAuthClient.from_conn(conn)
20
+
21
+
# Make XRPC requests
22
+
{:ok, response, client} = Atex.XRPC.get(client, "com.atproto.repo.listRecords")
23
+
"""
24
+
25
+
alias Atex.OAuth
26
+
use TypedStruct
27
+
28
+
@behaviour Atex.XRPC.Client
29
+
30
+
typedstruct enforce: true do
31
+
field :did, String.t()
32
+
end
33
+
34
+
@doc """
35
+
Create a new OAuthClient from a DID.
36
+
37
+
Validates that an OAuth session exists for the given DID in the session store
38
+
before returning the client struct.
39
+
40
+
## Examples
41
+
42
+
iex> Atex.XRPC.OAuthClient.new("did:plc:abc123")
43
+
{:ok, %Atex.XRPC.OAuthClient{did: "did:plc:abc123"}}
44
+
45
+
iex> Atex.XRPC.OAuthClient.new("did:plc:nosession")
46
+
{:error, :not_found}
47
+
48
+
"""
49
+
@spec new(String.t()) :: {:ok, t()} | {:error, atom()}
50
+
def new(did) do
51
+
# Make sure session exists before returning a struct
52
+
case Atex.OAuth.SessionStore.get(did) do
53
+
{:ok, _session} ->
54
+
{:ok, %__MODULE__{did: did}}
55
+
56
+
err ->
57
+
err
58
+
end
59
+
end
60
+
61
+
@doc """
62
+
Create an OAuthClient from a `Plug.Conn`.
63
+
64
+
Extracts the DID from the session (stored under `:atex_session` key) and validates
65
+
that the OAuth session is still valid. If the token is expired or expiring soon,
66
+
it attempts to refresh it.
67
+
68
+
Requires the conn to have passed through `Plug.Session` and `Plug.Conn.fetch_session/2`.
69
+
70
+
## Returns
71
+
72
+
- `{:ok, client}` - Successfully created client
73
+
- `{:error, :reauth}` - Session exists but refresh failed, user needs to re-authenticate
74
+
- `:error` - No session found in conn
75
+
76
+
## Examples
77
+
78
+
# After OAuth flow completes
79
+
conn = Plug.Conn.put_session(conn, :atex_session, "did:plc:abc123")
80
+
{:ok, client} = Atex.XRPC.OAuthClient.from_conn(conn)
81
+
82
+
"""
83
+
@spec from_conn(Plug.Conn.t()) :: {:ok, t()} | :error | {:error, atom()}
84
+
def from_conn(%Plug.Conn{} = conn) do
85
+
oauth_did = Plug.Conn.get_session(conn, :atex_session)
86
+
87
+
case oauth_did do
88
+
did when is_binary(did) ->
89
+
client = %__MODULE__{did: did}
90
+
91
+
with_session_lock(client, fn ->
92
+
case maybe_refresh(client) do
93
+
{:ok, _session} -> {:ok, client}
94
+
_ -> {:error, :reauth}
95
+
end
96
+
end)
97
+
98
+
_ ->
99
+
:error
100
+
end
101
+
end
102
+
103
+
@doc """
104
+
Ask the client's OAuth server for a new set of auth tokens.
105
+
106
+
Fetches the session, refreshes the tokens, creates a new session with the
107
+
updated tokens, stores it, and returns the new session.
108
+
109
+
You shouldn't need to call this manually for the most part, the client does
110
+
its best to refresh automatically when it needs to.
111
+
112
+
This function acquires a lock on the session to prevent concurrent refresh attempts.
113
+
"""
114
+
@spec refresh(client :: t()) :: {:ok, OAuth.Session.t()} | {:error, any()}
115
+
def refresh(%__MODULE__{} = client) do
116
+
with_session_lock(client, fn ->
117
+
do_refresh(client)
118
+
end)
119
+
end
120
+
121
+
@spec do_refresh(t()) :: {:ok, OAuth.Session.t()} | {:error, any()}
122
+
defp do_refresh(%__MODULE__{did: did}) do
123
+
with {:ok, session} <- OAuth.SessionStore.get(did),
124
+
{:ok, authz_server} <- OAuth.get_authorization_server(session.aud),
125
+
{:ok, %{token_endpoint: token_endpoint}} <-
126
+
OAuth.get_authorization_server_metadata(authz_server) do
127
+
case OAuth.refresh_token(
128
+
session.refresh_token,
129
+
session.dpop_key,
130
+
session.iss,
131
+
token_endpoint
132
+
) do
133
+
{:ok, tokens, nonce} ->
134
+
new_session = %OAuth.Session{
135
+
iss: session.iss,
136
+
aud: session.aud,
137
+
sub: tokens.did,
138
+
access_token: tokens.access_token,
139
+
refresh_token: tokens.refresh_token,
140
+
expires_at: tokens.expires_at,
141
+
dpop_key: session.dpop_key,
142
+
dpop_nonce: nonce
143
+
}
144
+
145
+
case OAuth.SessionStore.update(new_session) do
146
+
:ok -> {:ok, new_session}
147
+
err -> err
148
+
end
149
+
150
+
err ->
151
+
err
152
+
end
153
+
end
154
+
end
155
+
156
+
@spec maybe_refresh(t(), integer()) :: {:ok, OAuth.Session.t()} | {:error, any()}
157
+
defp maybe_refresh(%__MODULE__{did: did} = client, buffer_minutes \\ 5) do
158
+
with {:ok, session} <- OAuth.SessionStore.get(did) do
159
+
if token_expiring_soon?(session.expires_at, buffer_minutes) do
160
+
do_refresh(client)
161
+
else
162
+
{:ok, session}
163
+
end
164
+
end
165
+
end
166
+
167
+
@spec token_expiring_soon?(NaiveDateTime.t(), integer()) :: boolean()
168
+
defp token_expiring_soon?(expires_at, buffer_minutes) do
169
+
now = NaiveDateTime.utc_now()
170
+
expiry_threshold = NaiveDateTime.add(now, buffer_minutes * 60, :second)
171
+
172
+
NaiveDateTime.compare(expires_at, expiry_threshold) in [:lt, :eq]
173
+
end
174
+
175
+
@doc """
176
+
Make a GET request to an XRPC endpoint.
177
+
178
+
See `Atex.XRPC.get/3` for details.
179
+
"""
180
+
@impl true
181
+
def get(%__MODULE__{} = client, resource, opts \\ []) do
182
+
# TODO: Keyword.valiate to make sure :method isn't passed?
183
+
request(client, resource, opts ++ [method: :get])
184
+
end
185
+
186
+
@doc """
187
+
Make a POST request to an XRPC endpoint.
188
+
189
+
See `Atex.XRPC.post/3` for details.
190
+
"""
191
+
@impl true
192
+
def post(%__MODULE__{} = client, resource, opts \\ []) do
193
+
# Ditto
194
+
request(client, resource, opts ++ [method: :post])
195
+
end
196
+
197
+
defp request(%__MODULE__{} = client, resource, opts) do
198
+
with_session_lock(client, fn ->
199
+
case maybe_refresh(client) do
200
+
{:ok, session} ->
201
+
url = Atex.XRPC.url(session.aud, resource)
202
+
203
+
request =
204
+
opts
205
+
|> Keyword.put(:url, url)
206
+
|> Req.new()
207
+
|> Req.Request.put_header("authorization", "DPoP #{session.access_token}")
208
+
209
+
case OAuth.request_protected_dpop_resource(
210
+
request,
211
+
session.iss,
212
+
session.access_token,
213
+
session.dpop_key,
214
+
session.dpop_nonce
215
+
) do
216
+
{:ok, %{status: 200} = response, nonce} ->
217
+
update_session_nonce(session, nonce)
218
+
{:ok, response, client}
219
+
220
+
{:ok, response, nonce} ->
221
+
update_session_nonce(session, nonce)
222
+
handle_failure(client, request, response)
223
+
224
+
err ->
225
+
err
226
+
end
227
+
228
+
err ->
229
+
err
230
+
end
231
+
end)
232
+
end
233
+
234
+
# Execute a function with an exclusive lock on the session identified by the
235
+
# client's DID. This ensures that concurrent requests for the same user don't
236
+
# race during token refresh.
237
+
@spec with_session_lock(t(), (-> result)) :: result when result: any()
238
+
defp with_session_lock(%__MODULE__{did: did}, fun) do
239
+
Mutex.with_lock(Atex.SessionMutex, did, fun)
240
+
end
241
+
242
+
defp handle_failure(client, request, response) do
243
+
if auth_error?(response) do
244
+
case do_refresh(client) do
245
+
{:ok, session} ->
246
+
case OAuth.request_protected_dpop_resource(
247
+
request,
248
+
session.iss,
249
+
session.access_token,
250
+
session.dpop_key,
251
+
session.dpop_nonce
252
+
) do
253
+
{:ok, %{status: 200} = response, nonce} ->
254
+
update_session_nonce(session, nonce)
255
+
{:ok, response, client}
256
+
257
+
{:ok, response, _nonce} ->
258
+
if auth_error?(response) do
259
+
# We tried to refresh the token once but it's still failing
260
+
# Clear session and prompt dev to reauth or something
261
+
OAuth.SessionStore.delete(session)
262
+
{:error, response, :expired}
263
+
else
264
+
{:error, response, client}
265
+
end
266
+
267
+
err ->
268
+
err
269
+
end
270
+
271
+
err ->
272
+
err
273
+
end
274
+
else
275
+
{:error, response, client}
276
+
end
277
+
end
278
+
279
+
@spec auth_error?(Req.Response.t()) :: boolean()
280
+
defp auth_error?(%{status: 401, headers: %{"www-authenticate" => [www_auth]}}),
281
+
do:
282
+
(String.starts_with?(www_auth, "Bearer") or String.starts_with?(www_auth, "DPoP")) and
283
+
String.contains?(www_auth, "error=\"invalid_token\"")
284
+
285
+
defp auth_error?(_resp), do: false
286
+
287
+
defp update_session_nonce(session, nonce) do
288
+
session = %{session | dpop_nonce: nonce}
289
+
:ok = OAuth.SessionStore.update(session)
290
+
session
291
+
end
292
+
end
+181
-38
lib/atex/xrpc.ex
+181
-38
lib/atex/xrpc.ex
···
1
1
defmodule Atex.XRPC do
2
-
alias Atex.{HTTP, XRPC}
2
+
@moduledoc """
3
+
XRPC client module for AT Protocol RPC calls.
3
4
4
-
# TODO: automatic user-agent, and env for changing it
5
+
This module provides both authenticated and unauthenticated access to AT Protocol
6
+
XRPC endpoints. The authenticated functions (`get/3`, `post/3`) work with any
7
+
client that implements the `Atex.XRPC.Client`.
5
8
6
-
# TODO: consistent struct shape/protocol for Lexicon schemas so that user can pass in
7
-
# an object (hopefully validated by its module) without needing to specify the
8
-
# name & opts separately, and possibly verify the output response against it?
9
+
## Example usage
9
10
10
-
# TODO: auto refresh, will need to return a client instance in each method.
11
+
# Login-based client
12
+
{:ok, client} = Atex.XRPC.LoginClient.login("https://bsky.social", "user.bsky.social", "password")
13
+
{:ok, response, client} = Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
14
+
15
+
# OAuth-based client
16
+
{:ok, oauth_client} = Atex.XRPC.OAuthClient.from_conn(conn)
17
+
{:ok, response, oauth_client} = Atex.XRPC.get(oauth_client, "app.bsky.actor.getProfile", params: [actor: "user.bsky.social"])
18
+
19
+
## Unauthenticated requests
20
+
21
+
Unauthenticated functions (`unauthed_get/3`, `unauthed_post/3`) do not require a client
22
+
and work directly with endpoints:
23
+
24
+
{:ok, response} = Atex.XRPC.unauthed_get("https://bsky.social", "com.atproto.sync.getHead", params: [did: "did:plc:..."])
25
+
"""
26
+
27
+
alias Atex.XRPC.Client
11
28
12
29
@doc """
13
30
Perform a HTTP GET on a XRPC resource. Called a "query" in lexicons.
31
+
32
+
Accepts any client that implements `Atex.XRPC.Client` and returns
33
+
both the response and the (potentially updated) client.
34
+
35
+
Can be called either with the XRPC operation name as a string, or with a lexicon
36
+
struct (generated via `deflexicon`) for type safety and automatic parameter/response handling.
37
+
38
+
When using a lexicon struct, the response body will be automatically converted to the
39
+
corresponding type if an Output struct exists for the lexicon.
40
+
41
+
## Examples
42
+
43
+
# Using string XRPC name
44
+
{:ok, response, client} =
45
+
Atex.XRPC.get(client, "app.bsky.actor.getProfile", params: [actor: "ovyerus.com"])
46
+
47
+
# Using lexicon struct with typed construction
48
+
{:ok, response, client} =
49
+
Atex.XRPC.get(client, %App.Bsky.Actor.GetProfile{
50
+
params: %App.Bsky.Actor.GetProfile.Params{actor: "ovyerus.com"}
51
+
})
14
52
"""
15
-
@spec get(XRPC.Client.t(), String.t(), keyword()) :: HTTP.Adapter.result()
16
-
def get(%XRPC.Client{} = client, name, opts \\ []) do
17
-
opts = put_auth(opts, client.access_token)
18
-
HTTP.get(url(client, name), opts)
53
+
@spec get(Client.client(), String.t() | struct(), keyword()) ::
54
+
{:ok, Req.Response.t(), Client.client()}
55
+
| {:error, any(), Client.client()}
56
+
def get(client, name, opts \\ [])
57
+
58
+
def get(client, name, opts) when is_binary(name) do
59
+
client.__struct__.get(client, name, opts)
60
+
end
61
+
62
+
def get(client, %{__struct__: module} = query, opts) do
63
+
opts = if Map.get(query, :params), do: Keyword.put(opts, :params, query.params), else: opts
64
+
output_struct = Module.concat(module, Output)
65
+
output_exists = Code.ensure_loaded?(output_struct)
66
+
67
+
case client.__struct__.get(client, module.id(), opts) do
68
+
{:ok, %{status: 200} = response, client} ->
69
+
if output_exists do
70
+
case output_struct.from_json(response.body) do
71
+
{:ok, output} ->
72
+
{:ok, %{response | body: output}, client}
73
+
74
+
err ->
75
+
err
76
+
end
77
+
else
78
+
{:ok, response, client}
79
+
end
80
+
81
+
{:ok, _, _} = ok ->
82
+
ok
83
+
84
+
err ->
85
+
err
86
+
end
19
87
end
20
88
21
89
@doc """
22
-
Perform a HTTP POST on a XRPC resource. Called a "prodecure" in lexicons.
90
+
Perform a HTTP POST on a XRPC resource. Called a "procedure" in lexicons.
91
+
92
+
Accepts any client that implements `Atex.XRPC.Client` and returns both the
93
+
response and the (potentially updated) client.
94
+
95
+
Can be called either with the XRPC operation name as a string, or with a
96
+
lexicon struct (generated via `deflexicon`) for type safety and automatic
97
+
input/parameter mapping.
98
+
99
+
When using a lexicon struct, the response body will be automatically converted
100
+
to the corresponding type if an Output struct exists for the lexicon.
101
+
102
+
## Examples
103
+
104
+
# Using string XRPC name
105
+
{:ok, response, client} =
106
+
Atex.XRPC.post(
107
+
client,
108
+
"com.atproto.repo.createRecord",
109
+
json: %{
110
+
repo: "did:plc:...",
111
+
collection: "app.bsky.feed.post",
112
+
rkey: Atex.TID.now() |> to_string(),
113
+
record: %{
114
+
text: "Hello World",
115
+
createdAt: DateTime.to_iso8601(DateTime.utc_now())
116
+
}
117
+
}
118
+
)
119
+
120
+
# Using lexicon struct with typed construction
121
+
{:ok, response, client} =
122
+
Atex.XRPC.post(client, %Com.Atproto.Repo.CreateRecord{
123
+
input: %Com.Atproto.Repo.CreateRecord.Input{
124
+
repo: "did:plc:...",
125
+
collection: "app.bsky.feed.post",
126
+
rkey: Atex.TID.now() |> to_string(),
127
+
record: %App.Bsky.Feed.Post{
128
+
text: "Hello World!",
129
+
createdAt: DateTime.to_iso8601(DateTime.utc_now())
130
+
}
131
+
}
132
+
})
23
133
"""
24
-
@spec post(XRPC.Client.t(), String.t(), keyword()) :: HTTP.Adapter.result()
25
-
def post(%XRPC.Client{} = client, name, opts \\ []) do
26
-
# TODO: look through available HTTP clients and see if they have a
27
-
# consistent way of providing JSON bodies with auto content-type. If not,
28
-
# create one for adapters.
29
-
opts = put_auth(opts, client.access_token)
30
-
HTTP.post(url(client, name), opts)
134
+
@spec post(Client.client(), String.t() | struct(), keyword()) ::
135
+
{:ok, Req.Response.t(), Client.client()}
136
+
| {:error, any(), Client.client()}
137
+
def post(client, name, opts \\ [])
138
+
139
+
def post(client, name, opts) when is_binary(name) do
140
+
client.__struct__.post(client, name, opts)
141
+
end
142
+
143
+
def post(client, %{__struct__: module} = procedure, opts) do
144
+
opts =
145
+
opts
146
+
|> then(
147
+
&if Map.get(procedure, :params), do: Keyword.put(&1, :params, procedure.params), else: &1
148
+
)
149
+
|> then(
150
+
&cond do
151
+
Map.get(procedure, :input) -> Keyword.put(&1, :json, procedure.input)
152
+
Map.get(procedure, :raw_input) -> Keyword.put(&1, :body, procedure.raw_input)
153
+
true -> &1
154
+
end
155
+
)
156
+
157
+
output_struct = Module.concat(module, Output)
158
+
output_exists = Code.ensure_loaded?(output_struct)
159
+
160
+
case client.__struct__.post(client, module.id(), opts) do
161
+
{:ok, %{status: 200} = response, client} ->
162
+
if output_exists do
163
+
case output_struct.from_json(response.body) do
164
+
{:ok, output} ->
165
+
{:ok, %{response | body: output}, client}
166
+
167
+
err ->
168
+
err
169
+
end
170
+
else
171
+
{:ok, response, client}
172
+
end
173
+
174
+
{:ok, _, _} = ok ->
175
+
ok
176
+
177
+
err ->
178
+
err
179
+
end
31
180
end
32
181
33
182
@doc """
34
183
Like `get/3` but is unauthenticated by default.
35
184
"""
36
-
@spec unauthed_get(String.t(), String.t(), keyword()) :: HTTP.Adapter.result()
185
+
@spec unauthed_get(String.t(), String.t(), keyword()) ::
186
+
{:ok, Req.Response.t()} | {:error, any()}
37
187
def unauthed_get(endpoint, name, opts \\ []) do
38
-
HTTP.get(url(endpoint, name), opts)
188
+
Req.get(url(endpoint, name), opts)
39
189
end
40
190
41
191
@doc """
42
192
Like `post/3` but is unauthenticated by default.
43
193
"""
44
-
@spec unauthed_post(String.t(), String.t(), keyword()) :: HTTP.Adapter.result()
194
+
@spec unauthed_post(String.t(), String.t(), keyword()) ::
195
+
{:ok, Req.Response.t()} | {:error, any()}
45
196
def unauthed_post(endpoint, name, opts \\ []) do
46
-
HTTP.post(url(endpoint, name), opts)
197
+
Req.post(url(endpoint, name), opts)
47
198
end
48
199
49
-
# TODO: use URI module for joining instead?
50
-
@spec url(XRPC.Client.t() | String.t(), String.t()) :: String.t()
51
-
defp url(%XRPC.Client{endpoint: endpoint}, name), do: url(endpoint, name)
52
-
defp url(endpoint, name) when is_binary(endpoint), do: "#{endpoint}/xrpc/#{name}"
200
+
@doc """
201
+
Create an XRPC url based on an endpoint and a resource name.
202
+
203
+
## Example
53
204
54
-
@doc """
55
-
Put an `authorization` header into a keyword list of options to pass to a HTTP client.
205
+
iex> Atex.XRPC.url("https://bsky.app", "app.bsky.actor.getProfile")
206
+
"https://bsky.app/xrpc/app.bsky.actor.getProfile"
56
207
"""
57
-
@spec put_auth(keyword(), String.t()) :: keyword()
58
-
def put_auth(opts, token),
59
-
do: put_headers(opts, authorization: "Bearer #{token}")
60
-
61
-
@spec put_headers(keyword(), keyword()) :: keyword()
62
-
defp put_headers(opts, headers) do
63
-
opts
64
-
|> Keyword.put_new(:headers, [])
65
-
|> Keyword.update(:headers, [], &Keyword.merge(&1, headers))
66
-
end
208
+
@spec url(String.t(), String.t()) :: String.t()
209
+
def url(endpoint, resource) when is_binary(endpoint), do: "#{endpoint}/xrpc/#{resource}"
67
210
end
+74
lib/atproto/com/atproto/admin/defs.ex
+74
lib/atproto/com/atproto/admin/defs.ex
···
1
+
defmodule Com.Atproto.Admin.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"accountView" => %{
8
+
"properties" => %{
9
+
"deactivatedAt" => %{"format" => "datetime", "type" => "string"},
10
+
"did" => %{"format" => "did", "type" => "string"},
11
+
"email" => %{"type" => "string"},
12
+
"emailConfirmedAt" => %{"format" => "datetime", "type" => "string"},
13
+
"handle" => %{"format" => "handle", "type" => "string"},
14
+
"indexedAt" => %{"format" => "datetime", "type" => "string"},
15
+
"inviteNote" => %{"type" => "string"},
16
+
"invitedBy" => %{
17
+
"ref" => "com.atproto.server.defs#inviteCode",
18
+
"type" => "ref"
19
+
},
20
+
"invites" => %{
21
+
"items" => %{
22
+
"ref" => "com.atproto.server.defs#inviteCode",
23
+
"type" => "ref"
24
+
},
25
+
"type" => "array"
26
+
},
27
+
"invitesDisabled" => %{"type" => "boolean"},
28
+
"relatedRecords" => %{
29
+
"items" => %{"type" => "unknown"},
30
+
"type" => "array"
31
+
},
32
+
"threatSignatures" => %{
33
+
"items" => %{"ref" => "#threatSignature", "type" => "ref"},
34
+
"type" => "array"
35
+
}
36
+
},
37
+
"required" => ["did", "handle", "indexedAt"],
38
+
"type" => "object"
39
+
},
40
+
"repoBlobRef" => %{
41
+
"properties" => %{
42
+
"cid" => %{"format" => "cid", "type" => "string"},
43
+
"did" => %{"format" => "did", "type" => "string"},
44
+
"recordUri" => %{"format" => "at-uri", "type" => "string"}
45
+
},
46
+
"required" => ["did", "cid"],
47
+
"type" => "object"
48
+
},
49
+
"repoRef" => %{
50
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
51
+
"required" => ["did"],
52
+
"type" => "object"
53
+
},
54
+
"statusAttr" => %{
55
+
"properties" => %{
56
+
"applied" => %{"type" => "boolean"},
57
+
"ref" => %{"type" => "string"}
58
+
},
59
+
"required" => ["applied"],
60
+
"type" => "object"
61
+
},
62
+
"threatSignature" => %{
63
+
"properties" => %{
64
+
"property" => %{"type" => "string"},
65
+
"value" => %{"type" => "string"}
66
+
},
67
+
"required" => ["property", "value"],
68
+
"type" => "object"
69
+
}
70
+
},
71
+
"id" => "com.atproto.admin.defs",
72
+
"lexicon" => 1
73
+
})
74
+
end
+23
lib/atproto/com/atproto/admin/deleteAccount.ex
+23
lib/atproto/com/atproto/admin/deleteAccount.ex
···
1
+
defmodule Com.Atproto.Admin.DeleteAccount do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Delete a user account as an administrator.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
13
+
"required" => ["did"],
14
+
"type" => "object"
15
+
}
16
+
},
17
+
"type" => "procedure"
18
+
}
19
+
},
20
+
"id" => "com.atproto.admin.deleteAccount",
21
+
"lexicon" => 1
22
+
})
23
+
end
+30
lib/atproto/com/atproto/admin/disableAccountInvites.ex
+30
lib/atproto/com/atproto/admin/disableAccountInvites.ex
···
1
+
defmodule Com.Atproto.Admin.DisableAccountInvites do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Disable an account from receiving new invite codes, but does not invalidate existing codes.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"account" => %{"format" => "did", "type" => "string"},
15
+
"note" => %{
16
+
"description" => "Optional reason for disabled invites.",
17
+
"type" => "string"
18
+
}
19
+
},
20
+
"required" => ["account"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"type" => "procedure"
25
+
}
26
+
},
27
+
"id" => "com.atproto.admin.disableAccountInvites",
28
+
"lexicon" => 1
29
+
})
30
+
end
+26
lib/atproto/com/atproto/admin/disableInviteCodes.ex
+26
lib/atproto/com/atproto/admin/disableInviteCodes.ex
···
1
+
defmodule Com.Atproto.Admin.DisableInviteCodes do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Disable some set of codes and/or all codes associated with a set of users.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"accounts" => %{"items" => %{"type" => "string"}, "type" => "array"},
15
+
"codes" => %{"items" => %{"type" => "string"}, "type" => "array"}
16
+
},
17
+
"type" => "object"
18
+
}
19
+
},
20
+
"type" => "procedure"
21
+
}
22
+
},
23
+
"id" => "com.atproto.admin.disableInviteCodes",
24
+
"lexicon" => 1
25
+
})
26
+
end
+29
lib/atproto/com/atproto/admin/enableAccountInvites.ex
+29
lib/atproto/com/atproto/admin/enableAccountInvites.ex
···
1
+
defmodule Com.Atproto.Admin.EnableAccountInvites do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Re-enable an account's ability to receive invite codes.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"account" => %{"format" => "did", "type" => "string"},
14
+
"note" => %{
15
+
"description" => "Optional reason for enabled invites.",
16
+
"type" => "string"
17
+
}
18
+
},
19
+
"required" => ["account"],
20
+
"type" => "object"
21
+
}
22
+
},
23
+
"type" => "procedure"
24
+
}
25
+
},
26
+
"id" => "com.atproto.admin.enableAccountInvites",
27
+
"lexicon" => 1
28
+
})
29
+
end
+27
lib/atproto/com/atproto/admin/getAccountInfo.ex
+27
lib/atproto/com/atproto/admin/getAccountInfo.ex
···
1
+
defmodule Com.Atproto.Admin.GetAccountInfo do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get details about an account.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"ref" => "com.atproto.admin.defs#accountView",
13
+
"type" => "ref"
14
+
}
15
+
},
16
+
"parameters" => %{
17
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
18
+
"required" => ["did"],
19
+
"type" => "params"
20
+
},
21
+
"type" => "query"
22
+
}
23
+
},
24
+
"id" => "com.atproto.admin.getAccountInfo",
25
+
"lexicon" => 1
26
+
})
27
+
end
+41
lib/atproto/com/atproto/admin/getAccountInfos.ex
+41
lib/atproto/com/atproto/admin/getAccountInfos.ex
···
1
+
defmodule Com.Atproto.Admin.GetAccountInfos do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get details about some accounts.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"infos" => %{
14
+
"items" => %{
15
+
"ref" => "com.atproto.admin.defs#accountView",
16
+
"type" => "ref"
17
+
},
18
+
"type" => "array"
19
+
}
20
+
},
21
+
"required" => ["infos"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"parameters" => %{
26
+
"properties" => %{
27
+
"dids" => %{
28
+
"items" => %{"format" => "did", "type" => "string"},
29
+
"type" => "array"
30
+
}
31
+
},
32
+
"required" => ["dids"],
33
+
"type" => "params"
34
+
},
35
+
"type" => "query"
36
+
}
37
+
},
38
+
"id" => "com.atproto.admin.getAccountInfos",
39
+
"lexicon" => 1
40
+
})
41
+
end
+49
lib/atproto/com/atproto/admin/getInviteCodes.ex
+49
lib/atproto/com/atproto/admin/getInviteCodes.ex
···
1
+
defmodule Com.Atproto.Admin.GetInviteCodes do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get an admin view of invite codes.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"codes" => %{
14
+
"items" => %{
15
+
"ref" => "com.atproto.server.defs#inviteCode",
16
+
"type" => "ref"
17
+
},
18
+
"type" => "array"
19
+
},
20
+
"cursor" => %{"type" => "string"}
21
+
},
22
+
"required" => ["codes"],
23
+
"type" => "object"
24
+
}
25
+
},
26
+
"parameters" => %{
27
+
"properties" => %{
28
+
"cursor" => %{"type" => "string"},
29
+
"limit" => %{
30
+
"default" => 100,
31
+
"maximum" => 500,
32
+
"minimum" => 1,
33
+
"type" => "integer"
34
+
},
35
+
"sort" => %{
36
+
"default" => "recent",
37
+
"knownValues" => ["recent", "usage"],
38
+
"type" => "string"
39
+
}
40
+
},
41
+
"type" => "params"
42
+
},
43
+
"type" => "query"
44
+
}
45
+
},
46
+
"id" => "com.atproto.admin.getInviteCodes",
47
+
"lexicon" => 1
48
+
})
49
+
end
+49
lib/atproto/com/atproto/admin/getSubjectStatus.ex
+49
lib/atproto/com/atproto/admin/getSubjectStatus.ex
···
1
+
defmodule Com.Atproto.Admin.GetSubjectStatus do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get the service-specific admin status of a subject (account, record, or blob).",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"deactivated" => %{
15
+
"ref" => "com.atproto.admin.defs#statusAttr",
16
+
"type" => "ref"
17
+
},
18
+
"subject" => %{
19
+
"refs" => [
20
+
"com.atproto.admin.defs#repoRef",
21
+
"com.atproto.repo.strongRef",
22
+
"com.atproto.admin.defs#repoBlobRef"
23
+
],
24
+
"type" => "union"
25
+
},
26
+
"takedown" => %{
27
+
"ref" => "com.atproto.admin.defs#statusAttr",
28
+
"type" => "ref"
29
+
}
30
+
},
31
+
"required" => ["subject"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"parameters" => %{
36
+
"properties" => %{
37
+
"blob" => %{"format" => "cid", "type" => "string"},
38
+
"did" => %{"format" => "did", "type" => "string"},
39
+
"uri" => %{"format" => "at-uri", "type" => "string"}
40
+
},
41
+
"type" => "params"
42
+
},
43
+
"type" => "query"
44
+
}
45
+
},
46
+
"id" => "com.atproto.admin.getSubjectStatus",
47
+
"lexicon" => 1
48
+
})
49
+
end
+45
lib/atproto/com/atproto/admin/searchAccounts.ex
+45
lib/atproto/com/atproto/admin/searchAccounts.ex
···
1
+
defmodule Com.Atproto.Admin.SearchAccounts do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get list of accounts that matches your search query.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"accounts" => %{
14
+
"items" => %{
15
+
"ref" => "com.atproto.admin.defs#accountView",
16
+
"type" => "ref"
17
+
},
18
+
"type" => "array"
19
+
},
20
+
"cursor" => %{"type" => "string"}
21
+
},
22
+
"required" => ["accounts"],
23
+
"type" => "object"
24
+
}
25
+
},
26
+
"parameters" => %{
27
+
"properties" => %{
28
+
"cursor" => %{"type" => "string"},
29
+
"email" => %{"type" => "string"},
30
+
"limit" => %{
31
+
"default" => 50,
32
+
"maximum" => 100,
33
+
"minimum" => 1,
34
+
"type" => "integer"
35
+
}
36
+
},
37
+
"type" => "params"
38
+
},
39
+
"type" => "query"
40
+
}
41
+
},
42
+
"id" => "com.atproto.admin.searchAccounts",
43
+
"lexicon" => 1
44
+
})
45
+
end
+41
lib/atproto/com/atproto/admin/sendEmail.ex
+41
lib/atproto/com/atproto/admin/sendEmail.ex
···
1
+
defmodule Com.Atproto.Admin.SendEmail do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Send email to a user's account email address.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"comment" => %{
14
+
"description" =>
15
+
"Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers",
16
+
"type" => "string"
17
+
},
18
+
"content" => %{"type" => "string"},
19
+
"recipientDid" => %{"format" => "did", "type" => "string"},
20
+
"senderDid" => %{"format" => "did", "type" => "string"},
21
+
"subject" => %{"type" => "string"}
22
+
},
23
+
"required" => ["recipientDid", "content", "senderDid"],
24
+
"type" => "object"
25
+
}
26
+
},
27
+
"output" => %{
28
+
"encoding" => "application/json",
29
+
"schema" => %{
30
+
"properties" => %{"sent" => %{"type" => "boolean"}},
31
+
"required" => ["sent"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"type" => "procedure"
36
+
}
37
+
},
38
+
"id" => "com.atproto.admin.sendEmail",
39
+
"lexicon" => 1
40
+
})
41
+
end
+30
lib/atproto/com/atproto/admin/updateAccountEmail.ex
+30
lib/atproto/com/atproto/admin/updateAccountEmail.ex
···
1
+
defmodule Com.Atproto.Admin.UpdateAccountEmail do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Administrative action to update an account's email.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"account" => %{
14
+
"description" => "The handle or DID of the repo.",
15
+
"format" => "at-identifier",
16
+
"type" => "string"
17
+
},
18
+
"email" => %{"type" => "string"}
19
+
},
20
+
"required" => ["account", "email"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"type" => "procedure"
25
+
}
26
+
},
27
+
"id" => "com.atproto.admin.updateAccountEmail",
28
+
"lexicon" => 1
29
+
})
30
+
end
+26
lib/atproto/com/atproto/admin/updateAccountHandle.ex
+26
lib/atproto/com/atproto/admin/updateAccountHandle.ex
···
1
+
defmodule Com.Atproto.Admin.UpdateAccountHandle do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Administrative action to update an account's handle.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"did" => %{"format" => "did", "type" => "string"},
14
+
"handle" => %{"format" => "handle", "type" => "string"}
15
+
},
16
+
"required" => ["did", "handle"],
17
+
"type" => "object"
18
+
}
19
+
},
20
+
"type" => "procedure"
21
+
}
22
+
},
23
+
"id" => "com.atproto.admin.updateAccountHandle",
24
+
"lexicon" => 1
25
+
})
26
+
end
+26
lib/atproto/com/atproto/admin/updateAccountPassword.ex
+26
lib/atproto/com/atproto/admin/updateAccountPassword.ex
···
1
+
defmodule Com.Atproto.Admin.UpdateAccountPassword do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Update the password for a user account as an administrator.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"did" => %{"format" => "did", "type" => "string"},
14
+
"password" => %{"type" => "string"}
15
+
},
16
+
"required" => ["did", "password"],
17
+
"type" => "object"
18
+
}
19
+
},
20
+
"type" => "procedure"
21
+
}
22
+
},
23
+
"id" => "com.atproto.admin.updateAccountPassword",
24
+
"lexicon" => 1
25
+
})
26
+
end
+31
lib/atproto/com/atproto/admin/updateAccountSigningKey.ex
+31
lib/atproto/com/atproto/admin/updateAccountSigningKey.ex
···
1
+
defmodule Com.Atproto.Admin.UpdateAccountSigningKey do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Administrative action to update an account's signing key in their Did document.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"did" => %{"format" => "did", "type" => "string"},
15
+
"signingKey" => %{
16
+
"description" => "Did-key formatted public key",
17
+
"format" => "did",
18
+
"type" => "string"
19
+
}
20
+
},
21
+
"required" => ["did", "signingKey"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"type" => "procedure"
26
+
}
27
+
},
28
+
"id" => "com.atproto.admin.updateAccountSigningKey",
29
+
"lexicon" => 1
30
+
})
31
+
end
+62
lib/atproto/com/atproto/admin/updateSubjectStatus.ex
+62
lib/atproto/com/atproto/admin/updateSubjectStatus.ex
···
1
+
defmodule Com.Atproto.Admin.UpdateSubjectStatus do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Update the service-specific admin status of a subject (account, record, or blob).",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"deactivated" => %{
15
+
"ref" => "com.atproto.admin.defs#statusAttr",
16
+
"type" => "ref"
17
+
},
18
+
"subject" => %{
19
+
"refs" => [
20
+
"com.atproto.admin.defs#repoRef",
21
+
"com.atproto.repo.strongRef",
22
+
"com.atproto.admin.defs#repoBlobRef"
23
+
],
24
+
"type" => "union"
25
+
},
26
+
"takedown" => %{
27
+
"ref" => "com.atproto.admin.defs#statusAttr",
28
+
"type" => "ref"
29
+
}
30
+
},
31
+
"required" => ["subject"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"output" => %{
36
+
"encoding" => "application/json",
37
+
"schema" => %{
38
+
"properties" => %{
39
+
"subject" => %{
40
+
"refs" => [
41
+
"com.atproto.admin.defs#repoRef",
42
+
"com.atproto.repo.strongRef",
43
+
"com.atproto.admin.defs#repoBlobRef"
44
+
],
45
+
"type" => "union"
46
+
},
47
+
"takedown" => %{
48
+
"ref" => "com.atproto.admin.defs#statusAttr",
49
+
"type" => "ref"
50
+
}
51
+
},
52
+
"required" => ["subject"],
53
+
"type" => "object"
54
+
}
55
+
},
56
+
"type" => "procedure"
57
+
}
58
+
},
59
+
"id" => "com.atproto.admin.updateSubjectStatus",
60
+
"lexicon" => 1
61
+
})
62
+
end
+28
lib/atproto/com/atproto/identity/defs.ex
+28
lib/atproto/com/atproto/identity/defs.ex
···
1
+
defmodule Com.Atproto.Identity.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"identityInfo" => %{
8
+
"properties" => %{
9
+
"did" => %{"format" => "did", "type" => "string"},
10
+
"didDoc" => %{
11
+
"description" => "The complete DID document for the identity.",
12
+
"type" => "unknown"
13
+
},
14
+
"handle" => %{
15
+
"description" =>
16
+
"The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document.",
17
+
"format" => "handle",
18
+
"type" => "string"
19
+
}
20
+
},
21
+
"required" => ["did", "handle", "didDoc"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"id" => "com.atproto.identity.defs",
26
+
"lexicon" => 1
27
+
})
28
+
end
+36
lib/atproto/com/atproto/identity/getRecommendedDidCredentials.ex
+36
lib/atproto/com/atproto/identity/getRecommendedDidCredentials.ex
···
1
+
defmodule Com.Atproto.Identity.GetRecommendedDidCredentials do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Describe the credentials that should be included in the DID doc of an account that is migrating to this service.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"alsoKnownAs" => %{
15
+
"items" => %{"type" => "string"},
16
+
"type" => "array"
17
+
},
18
+
"rotationKeys" => %{
19
+
"description" =>
20
+
"Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs.",
21
+
"items" => %{"type" => "string"},
22
+
"type" => "array"
23
+
},
24
+
"services" => %{"type" => "unknown"},
25
+
"verificationMethods" => %{"type" => "unknown"}
26
+
},
27
+
"type" => "object"
28
+
}
29
+
},
30
+
"type" => "query"
31
+
}
32
+
},
33
+
"id" => "com.atproto.identity.getRecommendedDidCredentials",
34
+
"lexicon" => 1
35
+
})
36
+
end
+48
lib/atproto/com/atproto/identity/refreshIdentity.ex
+48
lib/atproto/com/atproto/identity/refreshIdentity.ex
···
1
+
defmodule Com.Atproto.Identity.RefreshIdentity do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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.",
10
+
"errors" => [
11
+
%{
12
+
"description" =>
13
+
"The resolution process confirmed that the handle does not resolve to any DID.",
14
+
"name" => "HandleNotFound"
15
+
},
16
+
%{
17
+
"description" => "The DID resolution process confirmed that there is no current DID.",
18
+
"name" => "DidNotFound"
19
+
},
20
+
%{
21
+
"description" => "The DID previously existed, but has been deactivated.",
22
+
"name" => "DidDeactivated"
23
+
}
24
+
],
25
+
"input" => %{
26
+
"encoding" => "application/json",
27
+
"schema" => %{
28
+
"properties" => %{
29
+
"identifier" => %{"format" => "at-identifier", "type" => "string"}
30
+
},
31
+
"required" => ["identifier"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"output" => %{
36
+
"encoding" => "application/json",
37
+
"schema" => %{
38
+
"ref" => "com.atproto.identity.defs#identityInfo",
39
+
"type" => "ref"
40
+
}
41
+
},
42
+
"type" => "procedure"
43
+
}
44
+
},
45
+
"id" => "com.atproto.identity.refreshIdentity",
46
+
"lexicon" => 1
47
+
})
48
+
end
+16
lib/atproto/com/atproto/identity/requestPlcOperationSignature.ex
+16
lib/atproto/com/atproto/identity/requestPlcOperationSignature.ex
···
1
+
defmodule Com.Atproto.Identity.RequestPlcOperationSignature do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Request an email with a code to in order to request a signed PLC operation. Requires Auth.",
10
+
"type" => "procedure"
11
+
}
12
+
},
13
+
"id" => "com.atproto.identity.requestPlcOperationSignature",
14
+
"lexicon" => 1
15
+
})
16
+
end
+49
lib/atproto/com/atproto/identity/resolveDid.ex
+49
lib/atproto/com/atproto/identity/resolveDid.ex
···
1
+
defmodule Com.Atproto.Identity.ResolveDid do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Resolves DID to DID document. Does not bi-directionally verify handle.",
9
+
"errors" => [
10
+
%{
11
+
"description" => "The DID resolution process confirmed that there is no current DID.",
12
+
"name" => "DidNotFound"
13
+
},
14
+
%{
15
+
"description" => "The DID previously existed, but has been deactivated.",
16
+
"name" => "DidDeactivated"
17
+
}
18
+
],
19
+
"output" => %{
20
+
"encoding" => "application/json",
21
+
"schema" => %{
22
+
"properties" => %{
23
+
"didDoc" => %{
24
+
"description" => "The complete DID document for the identity.",
25
+
"type" => "unknown"
26
+
}
27
+
},
28
+
"required" => ["didDoc"],
29
+
"type" => "object"
30
+
}
31
+
},
32
+
"parameters" => %{
33
+
"properties" => %{
34
+
"did" => %{
35
+
"description" => "DID to resolve.",
36
+
"format" => "did",
37
+
"type" => "string"
38
+
}
39
+
},
40
+
"required" => ["did"],
41
+
"type" => "params"
42
+
},
43
+
"type" => "query"
44
+
}
45
+
},
46
+
"id" => "com.atproto.identity.resolveDid",
47
+
"lexicon" => 1
48
+
})
49
+
end
+42
lib/atproto/com/atproto/identity/resolveHandle.ex
+42
lib/atproto/com/atproto/identity/resolveHandle.ex
···
1
+
defmodule Com.Atproto.Identity.ResolveHandle do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document.",
10
+
"errors" => [
11
+
%{
12
+
"description" =>
13
+
"The resolution process confirmed that the handle does not resolve to any DID.",
14
+
"name" => "HandleNotFound"
15
+
}
16
+
],
17
+
"output" => %{
18
+
"encoding" => "application/json",
19
+
"schema" => %{
20
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
21
+
"required" => ["did"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"parameters" => %{
26
+
"properties" => %{
27
+
"handle" => %{
28
+
"description" => "The handle to resolve.",
29
+
"format" => "handle",
30
+
"type" => "string"
31
+
}
32
+
},
33
+
"required" => ["handle"],
34
+
"type" => "params"
35
+
},
36
+
"type" => "query"
37
+
}
38
+
},
39
+
"id" => "com.atproto.identity.resolveHandle",
40
+
"lexicon" => 1
41
+
})
42
+
end
+49
lib/atproto/com/atproto/identity/resolveIdentity.ex
+49
lib/atproto/com/atproto/identity/resolveIdentity.ex
···
1
+
defmodule Com.Atproto.Identity.ResolveIdentity do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Resolves an identity (DID or Handle) to a full identity (DID document and verified handle).",
10
+
"errors" => [
11
+
%{
12
+
"description" =>
13
+
"The resolution process confirmed that the handle does not resolve to any DID.",
14
+
"name" => "HandleNotFound"
15
+
},
16
+
%{
17
+
"description" => "The DID resolution process confirmed that there is no current DID.",
18
+
"name" => "DidNotFound"
19
+
},
20
+
%{
21
+
"description" => "The DID previously existed, but has been deactivated.",
22
+
"name" => "DidDeactivated"
23
+
}
24
+
],
25
+
"output" => %{
26
+
"encoding" => "application/json",
27
+
"schema" => %{
28
+
"ref" => "com.atproto.identity.defs#identityInfo",
29
+
"type" => "ref"
30
+
}
31
+
},
32
+
"parameters" => %{
33
+
"properties" => %{
34
+
"identifier" => %{
35
+
"description" => "Handle or DID to resolve.",
36
+
"format" => "at-identifier",
37
+
"type" => "string"
38
+
}
39
+
},
40
+
"required" => ["identifier"],
41
+
"type" => "params"
42
+
},
43
+
"type" => "query"
44
+
}
45
+
},
46
+
"id" => "com.atproto.identity.resolveIdentity",
47
+
"lexicon" => 1
48
+
})
49
+
end
+52
lib/atproto/com/atproto/identity/signPlcOperation.ex
+52
lib/atproto/com/atproto/identity/signPlcOperation.ex
···
1
+
defmodule Com.Atproto.Identity.SignPlcOperation do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Signs a PLC operation to update some value(s) in the requesting DID's document.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"alsoKnownAs" => %{
15
+
"items" => %{"type" => "string"},
16
+
"type" => "array"
17
+
},
18
+
"rotationKeys" => %{
19
+
"items" => %{"type" => "string"},
20
+
"type" => "array"
21
+
},
22
+
"services" => %{"type" => "unknown"},
23
+
"token" => %{
24
+
"description" =>
25
+
"A token received through com.atproto.identity.requestPlcOperationSignature",
26
+
"type" => "string"
27
+
},
28
+
"verificationMethods" => %{"type" => "unknown"}
29
+
},
30
+
"type" => "object"
31
+
}
32
+
},
33
+
"output" => %{
34
+
"encoding" => "application/json",
35
+
"schema" => %{
36
+
"properties" => %{
37
+
"operation" => %{
38
+
"description" => "A signed DID PLC operation.",
39
+
"type" => "unknown"
40
+
}
41
+
},
42
+
"required" => ["operation"],
43
+
"type" => "object"
44
+
}
45
+
},
46
+
"type" => "procedure"
47
+
}
48
+
},
49
+
"id" => "com.atproto.identity.signPlcOperation",
50
+
"lexicon" => 1
51
+
})
52
+
end
+24
lib/atproto/com/atproto/identity/submitPlcOperation.ex
+24
lib/atproto/com/atproto/identity/submitPlcOperation.ex
···
1
+
defmodule Com.Atproto.Identity.SubmitPlcOperation do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{"operation" => %{"type" => "unknown"}},
14
+
"required" => ["operation"],
15
+
"type" => "object"
16
+
}
17
+
},
18
+
"type" => "procedure"
19
+
}
20
+
},
21
+
"id" => "com.atproto.identity.submitPlcOperation",
22
+
"lexicon" => 1
23
+
})
24
+
end
+30
lib/atproto/com/atproto/identity/updateHandle.ex
+30
lib/atproto/com/atproto/identity/updateHandle.ex
···
1
+
defmodule Com.Atproto.Identity.UpdateHandle do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"handle" => %{
15
+
"description" => "The new handle.",
16
+
"format" => "handle",
17
+
"type" => "string"
18
+
}
19
+
},
20
+
"required" => ["handle"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"type" => "procedure"
25
+
}
26
+
},
27
+
"id" => "com.atproto.identity.updateHandle",
28
+
"lexicon" => 1
29
+
})
30
+
end
+171
lib/atproto/com/atproto/label/defs.ex
+171
lib/atproto/com/atproto/label/defs.ex
···
1
+
defmodule Com.Atproto.Label.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"label" => %{
8
+
"description" => "Metadata tag on an atproto resource (eg, repo or record).",
9
+
"properties" => %{
10
+
"cid" => %{
11
+
"description" =>
12
+
"Optionally, CID specifying the specific version of 'uri' resource this label applies to.",
13
+
"format" => "cid",
14
+
"type" => "string"
15
+
},
16
+
"cts" => %{
17
+
"description" => "Timestamp when this label was created.",
18
+
"format" => "datetime",
19
+
"type" => "string"
20
+
},
21
+
"exp" => %{
22
+
"description" => "Timestamp at which this label expires (no longer applies).",
23
+
"format" => "datetime",
24
+
"type" => "string"
25
+
},
26
+
"neg" => %{
27
+
"description" => "If true, this is a negation label, overwriting a previous label.",
28
+
"type" => "boolean"
29
+
},
30
+
"sig" => %{
31
+
"description" => "Signature of dag-cbor encoded label.",
32
+
"type" => "bytes"
33
+
},
34
+
"src" => %{
35
+
"description" => "DID of the actor who created this label.",
36
+
"format" => "did",
37
+
"type" => "string"
38
+
},
39
+
"uri" => %{
40
+
"description" =>
41
+
"AT URI of the record, repository (account), or other resource that this label applies to.",
42
+
"format" => "uri",
43
+
"type" => "string"
44
+
},
45
+
"val" => %{
46
+
"description" => "The short string name of the value or type of this label.",
47
+
"maxLength" => 128,
48
+
"type" => "string"
49
+
},
50
+
"ver" => %{
51
+
"description" => "The AT Protocol version of the label object.",
52
+
"type" => "integer"
53
+
}
54
+
},
55
+
"required" => ["src", "uri", "val", "cts"],
56
+
"type" => "object"
57
+
},
58
+
"labelValue" => %{
59
+
"knownValues" => [
60
+
"!hide",
61
+
"!no-promote",
62
+
"!warn",
63
+
"!no-unauthenticated",
64
+
"dmca-violation",
65
+
"doxxing",
66
+
"porn",
67
+
"sexual",
68
+
"nudity",
69
+
"nsfl",
70
+
"gore"
71
+
],
72
+
"type" => "string"
73
+
},
74
+
"labelValueDefinition" => %{
75
+
"description" => "Declares a label value and its expected interpretations and behaviors.",
76
+
"properties" => %{
77
+
"adultOnly" => %{
78
+
"description" =>
79
+
"Does the user need to have adult content enabled in order to configure this label?",
80
+
"type" => "boolean"
81
+
},
82
+
"blurs" => %{
83
+
"description" =>
84
+
"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.",
85
+
"knownValues" => ["content", "media", "none"],
86
+
"type" => "string"
87
+
},
88
+
"defaultSetting" => %{
89
+
"default" => "warn",
90
+
"description" => "The default setting for this label.",
91
+
"knownValues" => ["ignore", "warn", "hide"],
92
+
"type" => "string"
93
+
},
94
+
"identifier" => %{
95
+
"description" =>
96
+
"The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).",
97
+
"maxGraphemes" => 100,
98
+
"maxLength" => 100,
99
+
"type" => "string"
100
+
},
101
+
"locales" => %{
102
+
"items" => %{"ref" => "#labelValueDefinitionStrings", "type" => "ref"},
103
+
"type" => "array"
104
+
},
105
+
"severity" => %{
106
+
"description" =>
107
+
"How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.",
108
+
"knownValues" => ["inform", "alert", "none"],
109
+
"type" => "string"
110
+
}
111
+
},
112
+
"required" => ["identifier", "severity", "blurs", "locales"],
113
+
"type" => "object"
114
+
},
115
+
"labelValueDefinitionStrings" => %{
116
+
"description" =>
117
+
"Strings which describe the label in the UI, localized into a specific language.",
118
+
"properties" => %{
119
+
"description" => %{
120
+
"description" =>
121
+
"A longer description of what the label means and why it might be applied.",
122
+
"maxGraphemes" => 10000,
123
+
"maxLength" => 100_000,
124
+
"type" => "string"
125
+
},
126
+
"lang" => %{
127
+
"description" => "The code of the language these strings are written in.",
128
+
"format" => "language",
129
+
"type" => "string"
130
+
},
131
+
"name" => %{
132
+
"description" => "A short human-readable name for the label.",
133
+
"maxGraphemes" => 64,
134
+
"maxLength" => 640,
135
+
"type" => "string"
136
+
}
137
+
},
138
+
"required" => ["lang", "name", "description"],
139
+
"type" => "object"
140
+
},
141
+
"selfLabel" => %{
142
+
"description" =>
143
+
"Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel.",
144
+
"properties" => %{
145
+
"val" => %{
146
+
"description" => "The short string name of the value or type of this label.",
147
+
"maxLength" => 128,
148
+
"type" => "string"
149
+
}
150
+
},
151
+
"required" => ["val"],
152
+
"type" => "object"
153
+
},
154
+
"selfLabels" => %{
155
+
"description" =>
156
+
"Metadata tags on an atproto record, published by the author within the record.",
157
+
"properties" => %{
158
+
"values" => %{
159
+
"items" => %{"ref" => "#selfLabel", "type" => "ref"},
160
+
"maxLength" => 10,
161
+
"type" => "array"
162
+
}
163
+
},
164
+
"required" => ["values"],
165
+
"type" => "object"
166
+
}
167
+
},
168
+
"id" => "com.atproto.label.defs",
169
+
"lexicon" => 1
170
+
})
171
+
end
+57
lib/atproto/com/atproto/label/queryLabels.ex
+57
lib/atproto/com/atproto/label/queryLabels.ex
···
1
+
defmodule Com.Atproto.Label.QueryLabels do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"cursor" => %{"type" => "string"},
15
+
"labels" => %{
16
+
"items" => %{
17
+
"ref" => "com.atproto.label.defs#label",
18
+
"type" => "ref"
19
+
},
20
+
"type" => "array"
21
+
}
22
+
},
23
+
"required" => ["labels"],
24
+
"type" => "object"
25
+
}
26
+
},
27
+
"parameters" => %{
28
+
"properties" => %{
29
+
"cursor" => %{"type" => "string"},
30
+
"limit" => %{
31
+
"default" => 50,
32
+
"maximum" => 250,
33
+
"minimum" => 1,
34
+
"type" => "integer"
35
+
},
36
+
"sources" => %{
37
+
"description" => "Optional list of label sources (DIDs) to filter on.",
38
+
"items" => %{"format" => "did", "type" => "string"},
39
+
"type" => "array"
40
+
},
41
+
"uriPatterns" => %{
42
+
"description" =>
43
+
"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.",
44
+
"items" => %{"type" => "string"},
45
+
"type" => "array"
46
+
}
47
+
},
48
+
"required" => ["uriPatterns"],
49
+
"type" => "params"
50
+
},
51
+
"type" => "query"
52
+
}
53
+
},
54
+
"id" => "com.atproto.label.queryLabels",
55
+
"lexicon" => 1
56
+
})
57
+
end
+48
lib/atproto/com/atproto/label/subscribeLabels.ex
+48
lib/atproto/com/atproto/label/subscribeLabels.ex
···
1
+
defmodule Com.Atproto.Label.SubscribeLabels do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"info" => %{
8
+
"properties" => %{
9
+
"message" => %{"type" => "string"},
10
+
"name" => %{"knownValues" => ["OutdatedCursor"], "type" => "string"}
11
+
},
12
+
"required" => ["name"],
13
+
"type" => "object"
14
+
},
15
+
"labels" => %{
16
+
"properties" => %{
17
+
"labels" => %{
18
+
"items" => %{"ref" => "com.atproto.label.defs#label", "type" => "ref"},
19
+
"type" => "array"
20
+
},
21
+
"seq" => %{"type" => "integer"}
22
+
},
23
+
"required" => ["seq", "labels"],
24
+
"type" => "object"
25
+
},
26
+
"main" => %{
27
+
"description" =>
28
+
"Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream.",
29
+
"errors" => [%{"name" => "FutureCursor"}],
30
+
"message" => %{
31
+
"schema" => %{"refs" => ["#labels", "#info"], "type" => "union"}
32
+
},
33
+
"parameters" => %{
34
+
"properties" => %{
35
+
"cursor" => %{
36
+
"description" => "The last known event seq number to backfill from.",
37
+
"type" => "integer"
38
+
}
39
+
},
40
+
"type" => "params"
41
+
},
42
+
"type" => "subscription"
43
+
}
44
+
},
45
+
"id" => "com.atproto.label.subscribeLabels",
46
+
"lexicon" => 1
47
+
})
48
+
end
+28
lib/atproto/com/atproto/lexicon/schema.ex
+28
lib/atproto/com/atproto/lexicon/schema.ex
···
1
+
defmodule Com.Atproto.Lexicon.Schema do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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).",
10
+
"key" => "nsid",
11
+
"record" => %{
12
+
"properties" => %{
13
+
"lexicon" => %{
14
+
"description" =>
15
+
"Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system.",
16
+
"type" => "integer"
17
+
}
18
+
},
19
+
"required" => ["lexicon"],
20
+
"type" => "object"
21
+
},
22
+
"type" => "record"
23
+
}
24
+
},
25
+
"id" => "com.atproto.lexicon.schema",
26
+
"lexicon" => 1
27
+
})
28
+
end
+82
lib/atproto/com/atproto/moderation/createReport.ex
+82
lib/atproto/com/atproto/moderation/createReport.ex
···
1
+
defmodule Com.Atproto.Moderation.CreateReport do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"modTool" => %{"ref" => "#modTool", "type" => "ref"},
15
+
"reason" => %{
16
+
"description" => "Additional context about the content and violation.",
17
+
"maxGraphemes" => 2000,
18
+
"maxLength" => 20000,
19
+
"type" => "string"
20
+
},
21
+
"reasonType" => %{
22
+
"description" => "Indicates the broad category of violation the report is for.",
23
+
"ref" => "com.atproto.moderation.defs#reasonType",
24
+
"type" => "ref"
25
+
},
26
+
"subject" => %{
27
+
"refs" => ["com.atproto.admin.defs#repoRef", "com.atproto.repo.strongRef"],
28
+
"type" => "union"
29
+
}
30
+
},
31
+
"required" => ["reasonType", "subject"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"output" => %{
36
+
"encoding" => "application/json",
37
+
"schema" => %{
38
+
"properties" => %{
39
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
40
+
"id" => %{"type" => "integer"},
41
+
"reason" => %{
42
+
"maxGraphemes" => 2000,
43
+
"maxLength" => 20000,
44
+
"type" => "string"
45
+
},
46
+
"reasonType" => %{
47
+
"ref" => "com.atproto.moderation.defs#reasonType",
48
+
"type" => "ref"
49
+
},
50
+
"reportedBy" => %{"format" => "did", "type" => "string"},
51
+
"subject" => %{
52
+
"refs" => ["com.atproto.admin.defs#repoRef", "com.atproto.repo.strongRef"],
53
+
"type" => "union"
54
+
}
55
+
},
56
+
"required" => ["id", "reasonType", "subject", "reportedBy", "createdAt"],
57
+
"type" => "object"
58
+
}
59
+
},
60
+
"type" => "procedure"
61
+
},
62
+
"modTool" => %{
63
+
"description" => "Moderation tool information for tracing the source of the action",
64
+
"properties" => %{
65
+
"meta" => %{
66
+
"description" => "Additional arbitrary metadata about the source",
67
+
"type" => "unknown"
68
+
},
69
+
"name" => %{
70
+
"description" =>
71
+
"Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')",
72
+
"type" => "string"
73
+
}
74
+
},
75
+
"required" => ["name"],
76
+
"type" => "object"
77
+
}
78
+
},
79
+
"id" => "com.atproto.moderation.createReport",
80
+
"lexicon" => 1
81
+
})
82
+
end
+102
lib/atproto/com/atproto/moderation/defs.ex
+102
lib/atproto/com/atproto/moderation/defs.ex
···
1
+
defmodule Com.Atproto.Moderation.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"reasonAppeal" => %{
8
+
"description" => "Appeal a previously taken moderation action",
9
+
"type" => "token"
10
+
},
11
+
"reasonMisleading" => %{
12
+
"description" =>
13
+
"Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`.",
14
+
"type" => "token"
15
+
},
16
+
"reasonOther" => %{
17
+
"description" =>
18
+
"Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`.",
19
+
"type" => "token"
20
+
},
21
+
"reasonRude" => %{
22
+
"description" =>
23
+
"Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`.",
24
+
"type" => "token"
25
+
},
26
+
"reasonSexual" => %{
27
+
"description" =>
28
+
"Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`.",
29
+
"type" => "token"
30
+
},
31
+
"reasonSpam" => %{
32
+
"description" =>
33
+
"Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`.",
34
+
"type" => "token"
35
+
},
36
+
"reasonType" => %{
37
+
"knownValues" => [
38
+
"com.atproto.moderation.defs#reasonSpam",
39
+
"com.atproto.moderation.defs#reasonViolation",
40
+
"com.atproto.moderation.defs#reasonMisleading",
41
+
"com.atproto.moderation.defs#reasonSexual",
42
+
"com.atproto.moderation.defs#reasonRude",
43
+
"com.atproto.moderation.defs#reasonOther",
44
+
"com.atproto.moderation.defs#reasonAppeal",
45
+
"tools.ozone.report.defs#reasonAppeal",
46
+
"tools.ozone.report.defs#reasonOther",
47
+
"tools.ozone.report.defs#reasonViolenceAnimal",
48
+
"tools.ozone.report.defs#reasonViolenceThreats",
49
+
"tools.ozone.report.defs#reasonViolenceGraphicContent",
50
+
"tools.ozone.report.defs#reasonViolenceGlorification",
51
+
"tools.ozone.report.defs#reasonViolenceExtremistContent",
52
+
"tools.ozone.report.defs#reasonViolenceTrafficking",
53
+
"tools.ozone.report.defs#reasonViolenceOther",
54
+
"tools.ozone.report.defs#reasonSexualAbuseContent",
55
+
"tools.ozone.report.defs#reasonSexualNCII",
56
+
"tools.ozone.report.defs#reasonSexualDeepfake",
57
+
"tools.ozone.report.defs#reasonSexualAnimal",
58
+
"tools.ozone.report.defs#reasonSexualUnlabeled",
59
+
"tools.ozone.report.defs#reasonSexualOther",
60
+
"tools.ozone.report.defs#reasonChildSafetyCSAM",
61
+
"tools.ozone.report.defs#reasonChildSafetyGroom",
62
+
"tools.ozone.report.defs#reasonChildSafetyPrivacy",
63
+
"tools.ozone.report.defs#reasonChildSafetyHarassment",
64
+
"tools.ozone.report.defs#reasonChildSafetyOther",
65
+
"tools.ozone.report.defs#reasonHarassmentTroll",
66
+
"tools.ozone.report.defs#reasonHarassmentTargeted",
67
+
"tools.ozone.report.defs#reasonHarassmentHateSpeech",
68
+
"tools.ozone.report.defs#reasonHarassmentDoxxing",
69
+
"tools.ozone.report.defs#reasonHarassmentOther",
70
+
"tools.ozone.report.defs#reasonMisleadingBot",
71
+
"tools.ozone.report.defs#reasonMisleadingImpersonation",
72
+
"tools.ozone.report.defs#reasonMisleadingSpam",
73
+
"tools.ozone.report.defs#reasonMisleadingScam",
74
+
"tools.ozone.report.defs#reasonMisleadingElections",
75
+
"tools.ozone.report.defs#reasonMisleadingOther",
76
+
"tools.ozone.report.defs#reasonRuleSiteSecurity",
77
+
"tools.ozone.report.defs#reasonRuleProhibitedSales",
78
+
"tools.ozone.report.defs#reasonRuleBanEvasion",
79
+
"tools.ozone.report.defs#reasonRuleOther",
80
+
"tools.ozone.report.defs#reasonSelfHarmContent",
81
+
"tools.ozone.report.defs#reasonSelfHarmED",
82
+
"tools.ozone.report.defs#reasonSelfHarmStunts",
83
+
"tools.ozone.report.defs#reasonSelfHarmSubstances",
84
+
"tools.ozone.report.defs#reasonSelfHarmOther"
85
+
],
86
+
"type" => "string"
87
+
},
88
+
"reasonViolation" => %{
89
+
"description" =>
90
+
"Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`.",
91
+
"type" => "token"
92
+
},
93
+
"subjectType" => %{
94
+
"description" => "Tag describing a type of subject that might be reported.",
95
+
"knownValues" => ["account", "record", "chat"],
96
+
"type" => "string"
97
+
}
98
+
},
99
+
"id" => "com.atproto.moderation.defs",
100
+
"lexicon" => 1
101
+
})
102
+
end
+141
lib/atproto/com/atproto/repo/applyWrites.ex
+141
lib/atproto/com/atproto/repo/applyWrites.ex
···
1
+
defmodule Com.Atproto.Repo.ApplyWrites do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"create" => %{
8
+
"description" => "Operation which creates a new record.",
9
+
"properties" => %{
10
+
"collection" => %{"format" => "nsid", "type" => "string"},
11
+
"rkey" => %{
12
+
"description" =>
13
+
"NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility.",
14
+
"format" => "record-key",
15
+
"maxLength" => 512,
16
+
"type" => "string"
17
+
},
18
+
"value" => %{"type" => "unknown"}
19
+
},
20
+
"required" => ["collection", "value"],
21
+
"type" => "object"
22
+
},
23
+
"createResult" => %{
24
+
"properties" => %{
25
+
"cid" => %{"format" => "cid", "type" => "string"},
26
+
"uri" => %{"format" => "at-uri", "type" => "string"},
27
+
"validationStatus" => %{
28
+
"knownValues" => ["valid", "unknown"],
29
+
"type" => "string"
30
+
}
31
+
},
32
+
"required" => ["uri", "cid"],
33
+
"type" => "object"
34
+
},
35
+
"delete" => %{
36
+
"description" => "Operation which deletes an existing record.",
37
+
"properties" => %{
38
+
"collection" => %{"format" => "nsid", "type" => "string"},
39
+
"rkey" => %{"format" => "record-key", "type" => "string"}
40
+
},
41
+
"required" => ["collection", "rkey"],
42
+
"type" => "object"
43
+
},
44
+
"deleteResult" => %{
45
+
"properties" => %{},
46
+
"required" => [],
47
+
"type" => "object"
48
+
},
49
+
"main" => %{
50
+
"description" =>
51
+
"Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS.",
52
+
"errors" => [
53
+
%{
54
+
"description" =>
55
+
"Indicates that the 'swapCommit' parameter did not match current commit.",
56
+
"name" => "InvalidSwap"
57
+
}
58
+
],
59
+
"input" => %{
60
+
"encoding" => "application/json",
61
+
"schema" => %{
62
+
"properties" => %{
63
+
"repo" => %{
64
+
"description" => "The handle or DID of the repo (aka, current account).",
65
+
"format" => "at-identifier",
66
+
"type" => "string"
67
+
},
68
+
"swapCommit" => %{
69
+
"description" =>
70
+
"If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations.",
71
+
"format" => "cid",
72
+
"type" => "string"
73
+
},
74
+
"validate" => %{
75
+
"description" =>
76
+
"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.",
77
+
"type" => "boolean"
78
+
},
79
+
"writes" => %{
80
+
"items" => %{
81
+
"closed" => true,
82
+
"refs" => ["#create", "#update", "#delete"],
83
+
"type" => "union"
84
+
},
85
+
"type" => "array"
86
+
}
87
+
},
88
+
"required" => ["repo", "writes"],
89
+
"type" => "object"
90
+
}
91
+
},
92
+
"output" => %{
93
+
"encoding" => "application/json",
94
+
"schema" => %{
95
+
"properties" => %{
96
+
"commit" => %{
97
+
"ref" => "com.atproto.repo.defs#commitMeta",
98
+
"type" => "ref"
99
+
},
100
+
"results" => %{
101
+
"items" => %{
102
+
"closed" => true,
103
+
"refs" => ["#createResult", "#updateResult", "#deleteResult"],
104
+
"type" => "union"
105
+
},
106
+
"type" => "array"
107
+
}
108
+
},
109
+
"required" => [],
110
+
"type" => "object"
111
+
}
112
+
},
113
+
"type" => "procedure"
114
+
},
115
+
"update" => %{
116
+
"description" => "Operation which updates an existing record.",
117
+
"properties" => %{
118
+
"collection" => %{"format" => "nsid", "type" => "string"},
119
+
"rkey" => %{"format" => "record-key", "type" => "string"},
120
+
"value" => %{"type" => "unknown"}
121
+
},
122
+
"required" => ["collection", "rkey", "value"],
123
+
"type" => "object"
124
+
},
125
+
"updateResult" => %{
126
+
"properties" => %{
127
+
"cid" => %{"format" => "cid", "type" => "string"},
128
+
"uri" => %{"format" => "at-uri", "type" => "string"},
129
+
"validationStatus" => %{
130
+
"knownValues" => ["valid", "unknown"],
131
+
"type" => "string"
132
+
}
133
+
},
134
+
"required" => ["uri", "cid"],
135
+
"type" => "object"
136
+
}
137
+
},
138
+
"id" => "com.atproto.repo.applyWrites",
139
+
"lexicon" => 1
140
+
})
141
+
end
+80
lib/atproto/com/atproto/repo/createRecord.ex
+80
lib/atproto/com/atproto/repo/createRecord.ex
···
1
+
defmodule Com.Atproto.Repo.CreateRecord do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Create a single new repository record. Requires auth, implemented by PDS.",
10
+
"errors" => [
11
+
%{
12
+
"description" => "Indicates that 'swapCommit' didn't match current repo commit.",
13
+
"name" => "InvalidSwap"
14
+
}
15
+
],
16
+
"input" => %{
17
+
"encoding" => "application/json",
18
+
"schema" => %{
19
+
"properties" => %{
20
+
"collection" => %{
21
+
"description" => "The NSID of the record collection.",
22
+
"format" => "nsid",
23
+
"type" => "string"
24
+
},
25
+
"record" => %{
26
+
"description" => "The record itself. Must contain a $type field.",
27
+
"type" => "unknown"
28
+
},
29
+
"repo" => %{
30
+
"description" => "The handle or DID of the repo (aka, current account).",
31
+
"format" => "at-identifier",
32
+
"type" => "string"
33
+
},
34
+
"rkey" => %{
35
+
"description" => "The Record Key.",
36
+
"format" => "record-key",
37
+
"maxLength" => 512,
38
+
"type" => "string"
39
+
},
40
+
"swapCommit" => %{
41
+
"description" => "Compare and swap with the previous commit by CID.",
42
+
"format" => "cid",
43
+
"type" => "string"
44
+
},
45
+
"validate" => %{
46
+
"description" =>
47
+
"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.",
48
+
"type" => "boolean"
49
+
}
50
+
},
51
+
"required" => ["repo", "collection", "record"],
52
+
"type" => "object"
53
+
}
54
+
},
55
+
"output" => %{
56
+
"encoding" => "application/json",
57
+
"schema" => %{
58
+
"properties" => %{
59
+
"cid" => %{"format" => "cid", "type" => "string"},
60
+
"commit" => %{
61
+
"ref" => "com.atproto.repo.defs#commitMeta",
62
+
"type" => "ref"
63
+
},
64
+
"uri" => %{"format" => "at-uri", "type" => "string"},
65
+
"validationStatus" => %{
66
+
"knownValues" => ["valid", "unknown"],
67
+
"type" => "string"
68
+
}
69
+
},
70
+
"required" => ["uri", "cid"],
71
+
"type" => "object"
72
+
}
73
+
},
74
+
"type" => "procedure"
75
+
}
76
+
},
77
+
"id" => "com.atproto.repo.createRecord",
78
+
"lexicon" => 1
79
+
})
80
+
end
+19
lib/atproto/com/atproto/repo/defs.ex
+19
lib/atproto/com/atproto/repo/defs.ex
···
1
+
defmodule Com.Atproto.Repo.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"commitMeta" => %{
8
+
"properties" => %{
9
+
"cid" => %{"format" => "cid", "type" => "string"},
10
+
"rev" => %{"format" => "tid", "type" => "string"}
11
+
},
12
+
"required" => ["cid", "rev"],
13
+
"type" => "object"
14
+
}
15
+
},
16
+
"id" => "com.atproto.repo.defs",
17
+
"lexicon" => 1
18
+
})
19
+
end
+63
lib/atproto/com/atproto/repo/deleteRecord.ex
+63
lib/atproto/com/atproto/repo/deleteRecord.ex
···
1
+
defmodule Com.Atproto.Repo.DeleteRecord do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS.",
10
+
"errors" => [%{"name" => "InvalidSwap"}],
11
+
"input" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"collection" => %{
16
+
"description" => "The NSID of the record collection.",
17
+
"format" => "nsid",
18
+
"type" => "string"
19
+
},
20
+
"repo" => %{
21
+
"description" => "The handle or DID of the repo (aka, current account).",
22
+
"format" => "at-identifier",
23
+
"type" => "string"
24
+
},
25
+
"rkey" => %{
26
+
"description" => "The Record Key.",
27
+
"format" => "record-key",
28
+
"type" => "string"
29
+
},
30
+
"swapCommit" => %{
31
+
"description" => "Compare and swap with the previous commit by CID.",
32
+
"format" => "cid",
33
+
"type" => "string"
34
+
},
35
+
"swapRecord" => %{
36
+
"description" => "Compare and swap with the previous record by CID.",
37
+
"format" => "cid",
38
+
"type" => "string"
39
+
}
40
+
},
41
+
"required" => ["repo", "collection", "rkey"],
42
+
"type" => "object"
43
+
}
44
+
},
45
+
"output" => %{
46
+
"encoding" => "application/json",
47
+
"schema" => %{
48
+
"properties" => %{
49
+
"commit" => %{
50
+
"ref" => "com.atproto.repo.defs#commitMeta",
51
+
"type" => "ref"
52
+
}
53
+
},
54
+
"type" => "object"
55
+
}
56
+
},
57
+
"type" => "procedure"
58
+
}
59
+
},
60
+
"id" => "com.atproto.repo.deleteRecord",
61
+
"lexicon" => 1
62
+
})
63
+
end
+53
lib/atproto/com/atproto/repo/describeRepo.ex
+53
lib/atproto/com/atproto/repo/describeRepo.ex
···
1
+
defmodule Com.Atproto.Repo.DescribeRepo do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get information about an account and repository, including the list of collections. Does not require auth.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"collections" => %{
15
+
"description" =>
16
+
"List of all the collections (NSIDs) for which this repo contains at least one record.",
17
+
"items" => %{"format" => "nsid", "type" => "string"},
18
+
"type" => "array"
19
+
},
20
+
"did" => %{"format" => "did", "type" => "string"},
21
+
"didDoc" => %{
22
+
"description" => "The complete DID document for this account.",
23
+
"type" => "unknown"
24
+
},
25
+
"handle" => %{"format" => "handle", "type" => "string"},
26
+
"handleIsCorrect" => %{
27
+
"description" =>
28
+
"Indicates if handle is currently valid (resolves bi-directionally)",
29
+
"type" => "boolean"
30
+
}
31
+
},
32
+
"required" => ["handle", "did", "didDoc", "collections", "handleIsCorrect"],
33
+
"type" => "object"
34
+
}
35
+
},
36
+
"parameters" => %{
37
+
"properties" => %{
38
+
"repo" => %{
39
+
"description" => "The handle or DID of the repo.",
40
+
"format" => "at-identifier",
41
+
"type" => "string"
42
+
}
43
+
},
44
+
"required" => ["repo"],
45
+
"type" => "params"
46
+
},
47
+
"type" => "query"
48
+
}
49
+
},
50
+
"id" => "com.atproto.repo.describeRepo",
51
+
"lexicon" => 1
52
+
})
53
+
end
+55
lib/atproto/com/atproto/repo/getRecord.ex
+55
lib/atproto/com/atproto/repo/getRecord.ex
···
1
+
defmodule Com.Atproto.Repo.GetRecord do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get a single record from a repository. Does not require auth.",
9
+
"errors" => [%{"name" => "RecordNotFound"}],
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"cid" => %{"format" => "cid", "type" => "string"},
15
+
"uri" => %{"format" => "at-uri", "type" => "string"},
16
+
"value" => %{"type" => "unknown"}
17
+
},
18
+
"required" => ["uri", "value"],
19
+
"type" => "object"
20
+
}
21
+
},
22
+
"parameters" => %{
23
+
"properties" => %{
24
+
"cid" => %{
25
+
"description" =>
26
+
"The CID of the version of the record. If not specified, then return the most recent version.",
27
+
"format" => "cid",
28
+
"type" => "string"
29
+
},
30
+
"collection" => %{
31
+
"description" => "The NSID of the record collection.",
32
+
"format" => "nsid",
33
+
"type" => "string"
34
+
},
35
+
"repo" => %{
36
+
"description" => "The handle or DID of the repo.",
37
+
"format" => "at-identifier",
38
+
"type" => "string"
39
+
},
40
+
"rkey" => %{
41
+
"description" => "The Record Key.",
42
+
"format" => "record-key",
43
+
"type" => "string"
44
+
}
45
+
},
46
+
"required" => ["repo", "collection", "rkey"],
47
+
"type" => "params"
48
+
},
49
+
"type" => "query"
50
+
}
51
+
},
52
+
"id" => "com.atproto.repo.getRecord",
53
+
"lexicon" => 1
54
+
})
55
+
end
+17
lib/atproto/com/atproto/repo/importRepo.ex
+17
lib/atproto/com/atproto/repo/importRepo.ex
···
1
+
defmodule Com.Atproto.Repo.ImportRepo do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set.",
10
+
"input" => %{"encoding" => "application/vnd.ipld.car"},
11
+
"type" => "procedure"
12
+
}
13
+
},
14
+
"id" => "com.atproto.repo.importRepo",
15
+
"lexicon" => 1
16
+
})
17
+
end
+50
lib/atproto/com/atproto/repo/listMissingBlobs.ex
+50
lib/atproto/com/atproto/repo/listMissingBlobs.ex
···
1
+
defmodule Com.Atproto.Repo.ListMissingBlobs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"blobs" => %{
15
+
"items" => %{"ref" => "#recordBlob", "type" => "ref"},
16
+
"type" => "array"
17
+
},
18
+
"cursor" => %{"type" => "string"}
19
+
},
20
+
"required" => ["blobs"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"parameters" => %{
25
+
"properties" => %{
26
+
"cursor" => %{"type" => "string"},
27
+
"limit" => %{
28
+
"default" => 500,
29
+
"maximum" => 1000,
30
+
"minimum" => 1,
31
+
"type" => "integer"
32
+
}
33
+
},
34
+
"type" => "params"
35
+
},
36
+
"type" => "query"
37
+
},
38
+
"recordBlob" => %{
39
+
"properties" => %{
40
+
"cid" => %{"format" => "cid", "type" => "string"},
41
+
"recordUri" => %{"format" => "at-uri", "type" => "string"}
42
+
},
43
+
"required" => ["cid", "recordUri"],
44
+
"type" => "object"
45
+
}
46
+
},
47
+
"id" => "com.atproto.repo.listMissingBlobs",
48
+
"lexicon" => 1
49
+
})
50
+
end
+67
lib/atproto/com/atproto/repo/listRecords.ex
+67
lib/atproto/com/atproto/repo/listRecords.ex
···
1
+
defmodule Com.Atproto.Repo.ListRecords do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"List a range of records in a repository, matching a specific collection. Does not require auth.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"cursor" => %{"type" => "string"},
15
+
"records" => %{
16
+
"items" => %{"ref" => "#record", "type" => "ref"},
17
+
"type" => "array"
18
+
}
19
+
},
20
+
"required" => ["records"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"parameters" => %{
25
+
"properties" => %{
26
+
"collection" => %{
27
+
"description" => "The NSID of the record type.",
28
+
"format" => "nsid",
29
+
"type" => "string"
30
+
},
31
+
"cursor" => %{"type" => "string"},
32
+
"limit" => %{
33
+
"default" => 50,
34
+
"description" => "The number of records to return.",
35
+
"maximum" => 100,
36
+
"minimum" => 1,
37
+
"type" => "integer"
38
+
},
39
+
"repo" => %{
40
+
"description" => "The handle or DID of the repo.",
41
+
"format" => "at-identifier",
42
+
"type" => "string"
43
+
},
44
+
"reverse" => %{
45
+
"description" => "Flag to reverse the order of the returned records.",
46
+
"type" => "boolean"
47
+
}
48
+
},
49
+
"required" => ["repo", "collection"],
50
+
"type" => "params"
51
+
},
52
+
"type" => "query"
53
+
},
54
+
"record" => %{
55
+
"properties" => %{
56
+
"cid" => %{"format" => "cid", "type" => "string"},
57
+
"uri" => %{"format" => "at-uri", "type" => "string"},
58
+
"value" => %{"type" => "unknown"}
59
+
},
60
+
"required" => ["uri", "cid", "value"],
61
+
"type" => "object"
62
+
}
63
+
},
64
+
"id" => "com.atproto.repo.listRecords",
65
+
"lexicon" => 1
66
+
})
67
+
end
+82
lib/atproto/com/atproto/repo/putRecord.ex
+82
lib/atproto/com/atproto/repo/putRecord.ex
···
1
+
defmodule Com.Atproto.Repo.PutRecord do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS.",
10
+
"errors" => [%{"name" => "InvalidSwap"}],
11
+
"input" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"nullable" => ["swapRecord"],
15
+
"properties" => %{
16
+
"collection" => %{
17
+
"description" => "The NSID of the record collection.",
18
+
"format" => "nsid",
19
+
"type" => "string"
20
+
},
21
+
"record" => %{
22
+
"description" => "The record to write.",
23
+
"type" => "unknown"
24
+
},
25
+
"repo" => %{
26
+
"description" => "The handle or DID of the repo (aka, current account).",
27
+
"format" => "at-identifier",
28
+
"type" => "string"
29
+
},
30
+
"rkey" => %{
31
+
"description" => "The Record Key.",
32
+
"format" => "record-key",
33
+
"maxLength" => 512,
34
+
"type" => "string"
35
+
},
36
+
"swapCommit" => %{
37
+
"description" => "Compare and swap with the previous commit by CID.",
38
+
"format" => "cid",
39
+
"type" => "string"
40
+
},
41
+
"swapRecord" => %{
42
+
"description" =>
43
+
"Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation",
44
+
"format" => "cid",
45
+
"type" => "string"
46
+
},
47
+
"validate" => %{
48
+
"description" =>
49
+
"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.",
50
+
"type" => "boolean"
51
+
}
52
+
},
53
+
"required" => ["repo", "collection", "rkey", "record"],
54
+
"type" => "object"
55
+
}
56
+
},
57
+
"output" => %{
58
+
"encoding" => "application/json",
59
+
"schema" => %{
60
+
"properties" => %{
61
+
"cid" => %{"format" => "cid", "type" => "string"},
62
+
"commit" => %{
63
+
"ref" => "com.atproto.repo.defs#commitMeta",
64
+
"type" => "ref"
65
+
},
66
+
"uri" => %{"format" => "at-uri", "type" => "string"},
67
+
"validationStatus" => %{
68
+
"knownValues" => ["valid", "unknown"],
69
+
"type" => "string"
70
+
}
71
+
},
72
+
"required" => ["uri", "cid"],
73
+
"type" => "object"
74
+
}
75
+
},
76
+
"type" => "procedure"
77
+
}
78
+
},
79
+
"id" => "com.atproto.repo.putRecord",
80
+
"lexicon" => 1
81
+
})
82
+
end
+20
lib/atproto/com/atproto/repo/strongRef.ex
+20
lib/atproto/com/atproto/repo/strongRef.ex
···
1
+
defmodule Com.Atproto.Repo.StrongRef do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"properties" => %{
9
+
"cid" => %{"format" => "cid", "type" => "string"},
10
+
"uri" => %{"format" => "at-uri", "type" => "string"}
11
+
},
12
+
"required" => ["uri", "cid"],
13
+
"type" => "object"
14
+
}
15
+
},
16
+
"description" => "A URI with a content-hash fingerprint.",
17
+
"id" => "com.atproto.repo.strongRef",
18
+
"lexicon" => 1
19
+
})
20
+
end
+25
lib/atproto/com/atproto/repo/uploadBlob.ex
+25
lib/atproto/com/atproto/repo/uploadBlob.ex
···
1
+
defmodule Com.Atproto.Repo.UploadBlob do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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.",
10
+
"input" => %{"encoding" => "*/*"},
11
+
"output" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{"blob" => %{"type" => "blob"}},
15
+
"required" => ["blob"],
16
+
"type" => "object"
17
+
}
18
+
},
19
+
"type" => "procedure"
20
+
}
21
+
},
22
+
"id" => "com.atproto.repo.uploadBlob",
23
+
"lexicon" => 1
24
+
})
25
+
end
+16
lib/atproto/com/atproto/server/activateAccount.ex
+16
lib/atproto/com/atproto/server/activateAccount.ex
···
1
+
defmodule Com.Atproto.Server.ActivateAccount do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup.",
10
+
"type" => "procedure"
11
+
}
12
+
},
13
+
"id" => "com.atproto.server.activateAccount",
14
+
"lexicon" => 1
15
+
})
16
+
end
+44
lib/atproto/com/atproto/server/checkAccountStatus.ex
+44
lib/atproto/com/atproto/server/checkAccountStatus.ex
···
1
+
defmodule Com.Atproto.Server.CheckAccountStatus do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"activated" => %{"type" => "boolean"},
15
+
"expectedBlobs" => %{"type" => "integer"},
16
+
"importedBlobs" => %{"type" => "integer"},
17
+
"indexedRecords" => %{"type" => "integer"},
18
+
"privateStateValues" => %{"type" => "integer"},
19
+
"repoBlocks" => %{"type" => "integer"},
20
+
"repoCommit" => %{"format" => "cid", "type" => "string"},
21
+
"repoRev" => %{"type" => "string"},
22
+
"validDid" => %{"type" => "boolean"}
23
+
},
24
+
"required" => [
25
+
"activated",
26
+
"validDid",
27
+
"repoCommit",
28
+
"repoRev",
29
+
"repoBlocks",
30
+
"indexedRecords",
31
+
"privateStateValues",
32
+
"expectedBlobs",
33
+
"importedBlobs"
34
+
],
35
+
"type" => "object"
36
+
}
37
+
},
38
+
"type" => "query"
39
+
}
40
+
},
41
+
"id" => "com.atproto.server.checkAccountStatus",
42
+
"lexicon" => 1
43
+
})
44
+
end
+33
lib/atproto/com/atproto/server/confirmEmail.ex
+33
lib/atproto/com/atproto/server/confirmEmail.ex
···
1
+
defmodule Com.Atproto.Server.ConfirmEmail do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Confirm an email using a token from com.atproto.server.requestEmailConfirmation.",
10
+
"errors" => [
11
+
%{"name" => "AccountNotFound"},
12
+
%{"name" => "ExpiredToken"},
13
+
%{"name" => "InvalidToken"},
14
+
%{"name" => "InvalidEmail"}
15
+
],
16
+
"input" => %{
17
+
"encoding" => "application/json",
18
+
"schema" => %{
19
+
"properties" => %{
20
+
"email" => %{"type" => "string"},
21
+
"token" => %{"type" => "string"}
22
+
},
23
+
"required" => ["email", "token"],
24
+
"type" => "object"
25
+
}
26
+
},
27
+
"type" => "procedure"
28
+
}
29
+
},
30
+
"id" => "com.atproto.server.confirmEmail",
31
+
"lexicon" => 1
32
+
})
33
+
end
+84
lib/atproto/com/atproto/server/createAccount.ex
+84
lib/atproto/com/atproto/server/createAccount.ex
···
1
+
defmodule Com.Atproto.Server.CreateAccount do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Create an account. Implemented by PDS.",
9
+
"errors" => [
10
+
%{"name" => "InvalidHandle"},
11
+
%{"name" => "InvalidPassword"},
12
+
%{"name" => "InvalidInviteCode"},
13
+
%{"name" => "HandleNotAvailable"},
14
+
%{"name" => "UnsupportedDomain"},
15
+
%{"name" => "UnresolvableDid"},
16
+
%{"name" => "IncompatibleDidDoc"}
17
+
],
18
+
"input" => %{
19
+
"encoding" => "application/json",
20
+
"schema" => %{
21
+
"properties" => %{
22
+
"did" => %{
23
+
"description" => "Pre-existing atproto DID, being imported to a new account.",
24
+
"format" => "did",
25
+
"type" => "string"
26
+
},
27
+
"email" => %{"type" => "string"},
28
+
"handle" => %{
29
+
"description" => "Requested handle for the account.",
30
+
"format" => "handle",
31
+
"type" => "string"
32
+
},
33
+
"inviteCode" => %{"type" => "string"},
34
+
"password" => %{
35
+
"description" =>
36
+
"Initial account password. May need to meet instance-specific password strength requirements.",
37
+
"type" => "string"
38
+
},
39
+
"plcOp" => %{
40
+
"description" =>
41
+
"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.",
42
+
"type" => "unknown"
43
+
},
44
+
"recoveryKey" => %{
45
+
"description" =>
46
+
"DID PLC rotation key (aka, recovery key) to be included in PLC creation operation.",
47
+
"type" => "string"
48
+
},
49
+
"verificationCode" => %{"type" => "string"},
50
+
"verificationPhone" => %{"type" => "string"}
51
+
},
52
+
"required" => ["handle"],
53
+
"type" => "object"
54
+
}
55
+
},
56
+
"output" => %{
57
+
"encoding" => "application/json",
58
+
"schema" => %{
59
+
"description" => "Account login session returned on successful account creation.",
60
+
"properties" => %{
61
+
"accessJwt" => %{"type" => "string"},
62
+
"did" => %{
63
+
"description" => "The DID of the new account.",
64
+
"format" => "did",
65
+
"type" => "string"
66
+
},
67
+
"didDoc" => %{
68
+
"description" => "Complete DID document.",
69
+
"type" => "unknown"
70
+
},
71
+
"handle" => %{"format" => "handle", "type" => "string"},
72
+
"refreshJwt" => %{"type" => "string"}
73
+
},
74
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
75
+
"type" => "object"
76
+
}
77
+
},
78
+
"type" => "procedure"
79
+
}
80
+
},
81
+
"id" => "com.atproto.server.createAccount",
82
+
"lexicon" => 1
83
+
})
84
+
end
+48
lib/atproto/com/atproto/server/createAppPassword.ex
+48
lib/atproto/com/atproto/server/createAppPassword.ex
···
1
+
defmodule Com.Atproto.Server.CreateAppPassword do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"appPassword" => %{
8
+
"properties" => %{
9
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
10
+
"name" => %{"type" => "string"},
11
+
"password" => %{"type" => "string"},
12
+
"privileged" => %{"type" => "boolean"}
13
+
},
14
+
"required" => ["name", "password", "createdAt"],
15
+
"type" => "object"
16
+
},
17
+
"main" => %{
18
+
"description" => "Create an App Password.",
19
+
"errors" => [%{"name" => "AccountTakedown"}],
20
+
"input" => %{
21
+
"encoding" => "application/json",
22
+
"schema" => %{
23
+
"properties" => %{
24
+
"name" => %{
25
+
"description" => "A short name for the App Password, to help distinguish them.",
26
+
"type" => "string"
27
+
},
28
+
"privileged" => %{
29
+
"description" =>
30
+
"If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients.",
31
+
"type" => "boolean"
32
+
}
33
+
},
34
+
"required" => ["name"],
35
+
"type" => "object"
36
+
}
37
+
},
38
+
"output" => %{
39
+
"encoding" => "application/json",
40
+
"schema" => %{"ref" => "#appPassword", "type" => "ref"}
41
+
},
42
+
"type" => "procedure"
43
+
}
44
+
},
45
+
"id" => "com.atproto.server.createAppPassword",
46
+
"lexicon" => 1
47
+
})
48
+
end
+34
lib/atproto/com/atproto/server/createInviteCode.ex
+34
lib/atproto/com/atproto/server/createInviteCode.ex
···
1
+
defmodule Com.Atproto.Server.CreateInviteCode do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Create an invite code.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"forAccount" => %{"format" => "did", "type" => "string"},
14
+
"useCount" => %{"type" => "integer"}
15
+
},
16
+
"required" => ["useCount"],
17
+
"type" => "object"
18
+
}
19
+
},
20
+
"output" => %{
21
+
"encoding" => "application/json",
22
+
"schema" => %{
23
+
"properties" => %{"code" => %{"type" => "string"}},
24
+
"required" => ["code"],
25
+
"type" => "object"
26
+
}
27
+
},
28
+
"type" => "procedure"
29
+
}
30
+
},
31
+
"id" => "com.atproto.server.createInviteCode",
32
+
"lexicon" => 1
33
+
})
34
+
end
+51
lib/atproto/com/atproto/server/createInviteCodes.ex
+51
lib/atproto/com/atproto/server/createInviteCodes.ex
···
1
+
defmodule Com.Atproto.Server.CreateInviteCodes do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"accountCodes" => %{
8
+
"properties" => %{
9
+
"account" => %{"type" => "string"},
10
+
"codes" => %{"items" => %{"type" => "string"}, "type" => "array"}
11
+
},
12
+
"required" => ["account", "codes"],
13
+
"type" => "object"
14
+
},
15
+
"main" => %{
16
+
"description" => "Create invite codes.",
17
+
"input" => %{
18
+
"encoding" => "application/json",
19
+
"schema" => %{
20
+
"properties" => %{
21
+
"codeCount" => %{"default" => 1, "type" => "integer"},
22
+
"forAccounts" => %{
23
+
"items" => %{"format" => "did", "type" => "string"},
24
+
"type" => "array"
25
+
},
26
+
"useCount" => %{"type" => "integer"}
27
+
},
28
+
"required" => ["codeCount", "useCount"],
29
+
"type" => "object"
30
+
}
31
+
},
32
+
"output" => %{
33
+
"encoding" => "application/json",
34
+
"schema" => %{
35
+
"properties" => %{
36
+
"codes" => %{
37
+
"items" => %{"ref" => "#accountCodes", "type" => "ref"},
38
+
"type" => "array"
39
+
}
40
+
},
41
+
"required" => ["codes"],
42
+
"type" => "object"
43
+
}
44
+
},
45
+
"type" => "procedure"
46
+
}
47
+
},
48
+
"id" => "com.atproto.server.createInviteCodes",
49
+
"lexicon" => 1
50
+
})
51
+
end
+64
lib/atproto/com/atproto/server/createSession.ex
+64
lib/atproto/com/atproto/server/createSession.ex
···
1
+
defmodule Com.Atproto.Server.CreateSession do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Create an authentication session.",
9
+
"errors" => [
10
+
%{"name" => "AccountTakedown"},
11
+
%{"name" => "AuthFactorTokenRequired"}
12
+
],
13
+
"input" => %{
14
+
"encoding" => "application/json",
15
+
"schema" => %{
16
+
"properties" => %{
17
+
"allowTakendown" => %{
18
+
"description" =>
19
+
"When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned",
20
+
"type" => "boolean"
21
+
},
22
+
"authFactorToken" => %{"type" => "string"},
23
+
"identifier" => %{
24
+
"description" =>
25
+
"Handle or other identifier supported by the server for the authenticating user.",
26
+
"type" => "string"
27
+
},
28
+
"password" => %{"type" => "string"}
29
+
},
30
+
"required" => ["identifier", "password"],
31
+
"type" => "object"
32
+
}
33
+
},
34
+
"output" => %{
35
+
"encoding" => "application/json",
36
+
"schema" => %{
37
+
"properties" => %{
38
+
"accessJwt" => %{"type" => "string"},
39
+
"active" => %{"type" => "boolean"},
40
+
"did" => %{"format" => "did", "type" => "string"},
41
+
"didDoc" => %{"type" => "unknown"},
42
+
"email" => %{"type" => "string"},
43
+
"emailAuthFactor" => %{"type" => "boolean"},
44
+
"emailConfirmed" => %{"type" => "boolean"},
45
+
"handle" => %{"format" => "handle", "type" => "string"},
46
+
"refreshJwt" => %{"type" => "string"},
47
+
"status" => %{
48
+
"description" =>
49
+
"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.",
50
+
"knownValues" => ["takendown", "suspended", "deactivated"],
51
+
"type" => "string"
52
+
}
53
+
},
54
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
55
+
"type" => "object"
56
+
}
57
+
},
58
+
"type" => "procedure"
59
+
}
60
+
},
61
+
"id" => "com.atproto.server.createSession",
62
+
"lexicon" => 1
63
+
})
64
+
end
+30
lib/atproto/com/atproto/server/deactivateAccount.ex
+30
lib/atproto/com/atproto/server/deactivateAccount.ex
···
1
+
defmodule Com.Atproto.Server.DeactivateAccount do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"deleteAfter" => %{
15
+
"description" =>
16
+
"A recommendation to server as to how long they should hold onto the deactivated account before deleting.",
17
+
"format" => "datetime",
18
+
"type" => "string"
19
+
}
20
+
},
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"type" => "procedure"
25
+
}
26
+
},
27
+
"id" => "com.atproto.server.deactivateAccount",
28
+
"lexicon" => 1
29
+
})
30
+
end
+43
lib/atproto/com/atproto/server/defs.ex
+43
lib/atproto/com/atproto/server/defs.ex
···
1
+
defmodule Com.Atproto.Server.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"inviteCode" => %{
8
+
"properties" => %{
9
+
"available" => %{"type" => "integer"},
10
+
"code" => %{"type" => "string"},
11
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
12
+
"createdBy" => %{"type" => "string"},
13
+
"disabled" => %{"type" => "boolean"},
14
+
"forAccount" => %{"type" => "string"},
15
+
"uses" => %{
16
+
"items" => %{"ref" => "#inviteCodeUse", "type" => "ref"},
17
+
"type" => "array"
18
+
}
19
+
},
20
+
"required" => [
21
+
"code",
22
+
"available",
23
+
"disabled",
24
+
"forAccount",
25
+
"createdBy",
26
+
"createdAt",
27
+
"uses"
28
+
],
29
+
"type" => "object"
30
+
},
31
+
"inviteCodeUse" => %{
32
+
"properties" => %{
33
+
"usedAt" => %{"format" => "datetime", "type" => "string"},
34
+
"usedBy" => %{"format" => "did", "type" => "string"}
35
+
},
36
+
"required" => ["usedBy", "usedAt"],
37
+
"type" => "object"
38
+
}
39
+
},
40
+
"id" => "com.atproto.server.defs",
41
+
"lexicon" => 1
42
+
})
43
+
end
+29
lib/atproto/com/atproto/server/deleteAccount.ex
+29
lib/atproto/com/atproto/server/deleteAccount.ex
···
1
+
defmodule Com.Atproto.Server.DeleteAccount do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth.",
10
+
"errors" => [%{"name" => "ExpiredToken"}, %{"name" => "InvalidToken"}],
11
+
"input" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"did" => %{"format" => "did", "type" => "string"},
16
+
"password" => %{"type" => "string"},
17
+
"token" => %{"type" => "string"}
18
+
},
19
+
"required" => ["did", "password", "token"],
20
+
"type" => "object"
21
+
}
22
+
},
23
+
"type" => "procedure"
24
+
}
25
+
},
26
+
"id" => "com.atproto.server.deleteAccount",
27
+
"lexicon" => 1
28
+
})
29
+
end
+15
lib/atproto/com/atproto/server/deleteSession.ex
+15
lib/atproto/com/atproto/server/deleteSession.ex
···
1
+
defmodule Com.Atproto.Server.DeleteSession do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Delete the current session. Requires auth.",
9
+
"type" => "procedure"
10
+
}
11
+
},
12
+
"id" => "com.atproto.server.deleteSession",
13
+
"lexicon" => 1
14
+
})
15
+
end
+62
lib/atproto/com/atproto/server/describeServer.ex
+62
lib/atproto/com/atproto/server/describeServer.ex
···
1
+
defmodule Com.Atproto.Server.DescribeServer do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"contact" => %{
8
+
"properties" => %{"email" => %{"type" => "string"}},
9
+
"type" => "object"
10
+
},
11
+
"links" => %{
12
+
"properties" => %{
13
+
"privacyPolicy" => %{"format" => "uri", "type" => "string"},
14
+
"termsOfService" => %{"format" => "uri", "type" => "string"}
15
+
},
16
+
"type" => "object"
17
+
},
18
+
"main" => %{
19
+
"description" =>
20
+
"Describes the server's account creation requirements and capabilities. Implemented by PDS.",
21
+
"output" => %{
22
+
"encoding" => "application/json",
23
+
"schema" => %{
24
+
"properties" => %{
25
+
"availableUserDomains" => %{
26
+
"description" => "List of domain suffixes that can be used in account handles.",
27
+
"items" => %{"type" => "string"},
28
+
"type" => "array"
29
+
},
30
+
"contact" => %{
31
+
"description" => "Contact information",
32
+
"ref" => "#contact",
33
+
"type" => "ref"
34
+
},
35
+
"did" => %{"format" => "did", "type" => "string"},
36
+
"inviteCodeRequired" => %{
37
+
"description" =>
38
+
"If true, an invite code must be supplied to create an account on this instance.",
39
+
"type" => "boolean"
40
+
},
41
+
"links" => %{
42
+
"description" => "URLs of service policy documents.",
43
+
"ref" => "#links",
44
+
"type" => "ref"
45
+
},
46
+
"phoneVerificationRequired" => %{
47
+
"description" =>
48
+
"If true, a phone verification token must be supplied to create an account on this instance.",
49
+
"type" => "boolean"
50
+
}
51
+
},
52
+
"required" => ["did", "availableUserDomains"],
53
+
"type" => "object"
54
+
}
55
+
},
56
+
"type" => "query"
57
+
}
58
+
},
59
+
"id" => "com.atproto.server.describeServer",
60
+
"lexicon" => 1
61
+
})
62
+
end
+44
lib/atproto/com/atproto/server/getAccountInviteCodes.ex
+44
lib/atproto/com/atproto/server/getAccountInviteCodes.ex
···
1
+
defmodule Com.Atproto.Server.GetAccountInviteCodes do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get all invite codes for the current account. Requires auth.",
9
+
"errors" => [%{"name" => "DuplicateCreate"}],
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"codes" => %{
15
+
"items" => %{
16
+
"ref" => "com.atproto.server.defs#inviteCode",
17
+
"type" => "ref"
18
+
},
19
+
"type" => "array"
20
+
}
21
+
},
22
+
"required" => ["codes"],
23
+
"type" => "object"
24
+
}
25
+
},
26
+
"parameters" => %{
27
+
"properties" => %{
28
+
"createAvailable" => %{
29
+
"default" => true,
30
+
"description" =>
31
+
"Controls whether any new 'earned' but not 'created' invites should be created.",
32
+
"type" => "boolean"
33
+
},
34
+
"includeUsed" => %{"default" => true, "type" => "boolean"}
35
+
},
36
+
"type" => "params"
37
+
},
38
+
"type" => "query"
39
+
}
40
+
},
41
+
"id" => "com.atproto.server.getAccountInviteCodes",
42
+
"lexicon" => 1
43
+
})
44
+
end
+53
lib/atproto/com/atproto/server/getServiceAuth.ex
+53
lib/atproto/com/atproto/server/getServiceAuth.ex
···
1
+
defmodule Com.Atproto.Server.GetServiceAuth do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get a signed token on behalf of the requesting DID for the requested service.",
10
+
"errors" => [
11
+
%{
12
+
"description" =>
13
+
"Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes.",
14
+
"name" => "BadExpiration"
15
+
}
16
+
],
17
+
"output" => %{
18
+
"encoding" => "application/json",
19
+
"schema" => %{
20
+
"properties" => %{"token" => %{"type" => "string"}},
21
+
"required" => ["token"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"parameters" => %{
26
+
"properties" => %{
27
+
"aud" => %{
28
+
"description" =>
29
+
"The DID of the service that the token will be used to authenticate with",
30
+
"format" => "did",
31
+
"type" => "string"
32
+
},
33
+
"exp" => %{
34
+
"description" =>
35
+
"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.",
36
+
"type" => "integer"
37
+
},
38
+
"lxm" => %{
39
+
"description" => "Lexicon (XRPC) method to bind the requested token to",
40
+
"format" => "nsid",
41
+
"type" => "string"
42
+
}
43
+
},
44
+
"required" => ["aud"],
45
+
"type" => "params"
46
+
},
47
+
"type" => "query"
48
+
}
49
+
},
50
+
"id" => "com.atproto.server.getServiceAuth",
51
+
"lexicon" => 1
52
+
})
53
+
end
+37
lib/atproto/com/atproto/server/getSession.ex
+37
lib/atproto/com/atproto/server/getSession.ex
···
1
+
defmodule Com.Atproto.Server.GetSession do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Get information about the current auth session. Requires auth.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{
13
+
"active" => %{"type" => "boolean"},
14
+
"did" => %{"format" => "did", "type" => "string"},
15
+
"didDoc" => %{"type" => "unknown"},
16
+
"email" => %{"type" => "string"},
17
+
"emailAuthFactor" => %{"type" => "boolean"},
18
+
"emailConfirmed" => %{"type" => "boolean"},
19
+
"handle" => %{"format" => "handle", "type" => "string"},
20
+
"status" => %{
21
+
"description" =>
22
+
"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.",
23
+
"knownValues" => ["takendown", "suspended", "deactivated"],
24
+
"type" => "string"
25
+
}
26
+
},
27
+
"required" => ["handle", "did"],
28
+
"type" => "object"
29
+
}
30
+
},
31
+
"type" => "query"
32
+
}
33
+
},
34
+
"id" => "com.atproto.server.getSession",
35
+
"lexicon" => 1
36
+
})
37
+
end
+38
lib/atproto/com/atproto/server/listAppPasswords.ex
+38
lib/atproto/com/atproto/server/listAppPasswords.ex
···
1
+
defmodule Com.Atproto.Server.ListAppPasswords do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"appPassword" => %{
8
+
"properties" => %{
9
+
"createdAt" => %{"format" => "datetime", "type" => "string"},
10
+
"name" => %{"type" => "string"},
11
+
"privileged" => %{"type" => "boolean"}
12
+
},
13
+
"required" => ["name", "createdAt"],
14
+
"type" => "object"
15
+
},
16
+
"main" => %{
17
+
"description" => "List all App Passwords.",
18
+
"errors" => [%{"name" => "AccountTakedown"}],
19
+
"output" => %{
20
+
"encoding" => "application/json",
21
+
"schema" => %{
22
+
"properties" => %{
23
+
"passwords" => %{
24
+
"items" => %{"ref" => "#appPassword", "type" => "ref"},
25
+
"type" => "array"
26
+
}
27
+
},
28
+
"required" => ["passwords"],
29
+
"type" => "object"
30
+
}
31
+
},
32
+
"type" => "query"
33
+
}
34
+
},
35
+
"id" => "com.atproto.server.listAppPasswords",
36
+
"lexicon" => 1
37
+
})
38
+
end
+38
lib/atproto/com/atproto/server/refreshSession.ex
+38
lib/atproto/com/atproto/server/refreshSession.ex
···
1
+
defmodule Com.Atproto.Server.RefreshSession do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt').",
10
+
"errors" => [%{"name" => "AccountTakedown"}],
11
+
"output" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"accessJwt" => %{"type" => "string"},
16
+
"active" => %{"type" => "boolean"},
17
+
"did" => %{"format" => "did", "type" => "string"},
18
+
"didDoc" => %{"type" => "unknown"},
19
+
"handle" => %{"format" => "handle", "type" => "string"},
20
+
"refreshJwt" => %{"type" => "string"},
21
+
"status" => %{
22
+
"description" =>
23
+
"Hosting status of the account. If not specified, then assume 'active'.",
24
+
"knownValues" => ["takendown", "suspended", "deactivated"],
25
+
"type" => "string"
26
+
}
27
+
},
28
+
"required" => ["accessJwt", "refreshJwt", "handle", "did"],
29
+
"type" => "object"
30
+
}
31
+
},
32
+
"type" => "procedure"
33
+
}
34
+
},
35
+
"id" => "com.atproto.server.refreshSession",
36
+
"lexicon" => 1
37
+
})
38
+
end
+15
lib/atproto/com/atproto/server/requestAccountDelete.ex
+15
lib/atproto/com/atproto/server/requestAccountDelete.ex
···
1
+
defmodule Com.Atproto.Server.RequestAccountDelete do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Initiate a user account deletion via email.",
9
+
"type" => "procedure"
10
+
}
11
+
},
12
+
"id" => "com.atproto.server.requestAccountDelete",
13
+
"lexicon" => 1
14
+
})
15
+
end
+15
lib/atproto/com/atproto/server/requestEmailConfirmation.ex
+15
lib/atproto/com/atproto/server/requestEmailConfirmation.ex
···
1
+
defmodule Com.Atproto.Server.RequestEmailConfirmation do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Request an email with a code to confirm ownership of email.",
9
+
"type" => "procedure"
10
+
}
11
+
},
12
+
"id" => "com.atproto.server.requestEmailConfirmation",
13
+
"lexicon" => 1
14
+
})
15
+
end
+23
lib/atproto/com/atproto/server/requestEmailUpdate.ex
+23
lib/atproto/com/atproto/server/requestEmailUpdate.ex
···
1
+
defmodule Com.Atproto.Server.RequestEmailUpdate do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Request a token in order to update email.",
9
+
"output" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{"tokenRequired" => %{"type" => "boolean"}},
13
+
"required" => ["tokenRequired"],
14
+
"type" => "object"
15
+
}
16
+
},
17
+
"type" => "procedure"
18
+
}
19
+
},
20
+
"id" => "com.atproto.server.requestEmailUpdate",
21
+
"lexicon" => 1
22
+
})
23
+
end
+23
lib/atproto/com/atproto/server/requestPasswordReset.ex
+23
lib/atproto/com/atproto/server/requestPasswordReset.ex
···
1
+
defmodule Com.Atproto.Server.RequestPasswordReset do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Initiate a user account password reset via email.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{"email" => %{"type" => "string"}},
13
+
"required" => ["email"],
14
+
"type" => "object"
15
+
}
16
+
},
17
+
"type" => "procedure"
18
+
}
19
+
},
20
+
"id" => "com.atproto.server.requestPasswordReset",
21
+
"lexicon" => 1
22
+
})
23
+
end
+43
lib/atproto/com/atproto/server/reserveSigningKey.ex
+43
lib/atproto/com/atproto/server/reserveSigningKey.ex
···
1
+
defmodule Com.Atproto.Server.ReserveSigningKey do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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.",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"did" => %{
15
+
"description" => "The DID to reserve a key for.",
16
+
"format" => "did",
17
+
"type" => "string"
18
+
}
19
+
},
20
+
"type" => "object"
21
+
}
22
+
},
23
+
"output" => %{
24
+
"encoding" => "application/json",
25
+
"schema" => %{
26
+
"properties" => %{
27
+
"signingKey" => %{
28
+
"description" =>
29
+
"The public key for the reserved signing key, in did:key serialization.",
30
+
"type" => "string"
31
+
}
32
+
},
33
+
"required" => ["signingKey"],
34
+
"type" => "object"
35
+
}
36
+
},
37
+
"type" => "procedure"
38
+
}
39
+
},
40
+
"id" => "com.atproto.server.reserveSigningKey",
41
+
"lexicon" => 1
42
+
})
43
+
end
+27
lib/atproto/com/atproto/server/resetPassword.ex
+27
lib/atproto/com/atproto/server/resetPassword.ex
···
1
+
defmodule Com.Atproto.Server.ResetPassword do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Reset a user account password using a token.",
9
+
"errors" => [%{"name" => "ExpiredToken"}, %{"name" => "InvalidToken"}],
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"password" => %{"type" => "string"},
15
+
"token" => %{"type" => "string"}
16
+
},
17
+
"required" => ["token", "password"],
18
+
"type" => "object"
19
+
}
20
+
},
21
+
"type" => "procedure"
22
+
}
23
+
},
24
+
"id" => "com.atproto.server.resetPassword",
25
+
"lexicon" => 1
26
+
})
27
+
end
+23
lib/atproto/com/atproto/server/revokeAppPassword.ex
+23
lib/atproto/com/atproto/server/revokeAppPassword.ex
···
1
+
defmodule Com.Atproto.Server.RevokeAppPassword do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Revoke an App Password by name.",
9
+
"input" => %{
10
+
"encoding" => "application/json",
11
+
"schema" => %{
12
+
"properties" => %{"name" => %{"type" => "string"}},
13
+
"required" => ["name"],
14
+
"type" => "object"
15
+
}
16
+
},
17
+
"type" => "procedure"
18
+
}
19
+
},
20
+
"id" => "com.atproto.server.revokeAppPassword",
21
+
"lexicon" => 1
22
+
})
23
+
end
+36
lib/atproto/com/atproto/server/updateEmail.ex
+36
lib/atproto/com/atproto/server/updateEmail.ex
···
1
+
defmodule Com.Atproto.Server.UpdateEmail do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "Update an account's email.",
9
+
"errors" => [
10
+
%{"name" => "ExpiredToken"},
11
+
%{"name" => "InvalidToken"},
12
+
%{"name" => "TokenRequired"}
13
+
],
14
+
"input" => %{
15
+
"encoding" => "application/json",
16
+
"schema" => %{
17
+
"properties" => %{
18
+
"email" => %{"type" => "string"},
19
+
"emailAuthFactor" => %{"type" => "boolean"},
20
+
"token" => %{
21
+
"description" =>
22
+
"Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed.",
23
+
"type" => "string"
24
+
}
25
+
},
26
+
"required" => ["email"],
27
+
"type" => "object"
28
+
}
29
+
},
30
+
"type" => "procedure"
31
+
}
32
+
},
33
+
"id" => "com.atproto.server.updateEmail",
34
+
"lexicon" => 1
35
+
})
36
+
end
+15
lib/atproto/com/atproto/sync/defs.ex
+15
lib/atproto/com/atproto/sync/defs.ex
···
1
+
defmodule Com.Atproto.Sync.Defs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"hostStatus" => %{
8
+
"knownValues" => ["active", "idle", "offline", "throttled", "banned"],
9
+
"type" => "string"
10
+
}
11
+
},
12
+
"id" => "com.atproto.sync.defs",
13
+
"lexicon" => 1
14
+
})
15
+
end
+40
lib/atproto/com/atproto/sync/getBlob.ex
+40
lib/atproto/com/atproto/sync/getBlob.ex
···
1
+
defmodule Com.Atproto.Sync.GetBlob do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS.",
10
+
"errors" => [
11
+
%{"name" => "BlobNotFound"},
12
+
%{"name" => "RepoNotFound"},
13
+
%{"name" => "RepoTakendown"},
14
+
%{"name" => "RepoSuspended"},
15
+
%{"name" => "RepoDeactivated"}
16
+
],
17
+
"output" => %{"encoding" => "*/*"},
18
+
"parameters" => %{
19
+
"properties" => %{
20
+
"cid" => %{
21
+
"description" => "The CID of the blob to fetch",
22
+
"format" => "cid",
23
+
"type" => "string"
24
+
},
25
+
"did" => %{
26
+
"description" => "The DID of the account.",
27
+
"format" => "did",
28
+
"type" => "string"
29
+
}
30
+
},
31
+
"required" => ["did", "cid"],
32
+
"type" => "params"
33
+
},
34
+
"type" => "query"
35
+
}
36
+
},
37
+
"id" => "com.atproto.sync.getBlob",
38
+
"lexicon" => 1
39
+
})
40
+
end
+39
lib/atproto/com/atproto/sync/getBlocks.ex
+39
lib/atproto/com/atproto/sync/getBlocks.ex
···
1
+
defmodule Com.Atproto.Sync.GetBlocks do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS.",
10
+
"errors" => [
11
+
%{"name" => "BlockNotFound"},
12
+
%{"name" => "RepoNotFound"},
13
+
%{"name" => "RepoTakendown"},
14
+
%{"name" => "RepoSuspended"},
15
+
%{"name" => "RepoDeactivated"}
16
+
],
17
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
18
+
"parameters" => %{
19
+
"properties" => %{
20
+
"cids" => %{
21
+
"items" => %{"format" => "cid", "type" => "string"},
22
+
"type" => "array"
23
+
},
24
+
"did" => %{
25
+
"description" => "The DID of the repo.",
26
+
"format" => "did",
27
+
"type" => "string"
28
+
}
29
+
},
30
+
"required" => ["did", "cids"],
31
+
"type" => "params"
32
+
},
33
+
"type" => "query"
34
+
}
35
+
},
36
+
"id" => "com.atproto.sync.getBlocks",
37
+
"lexicon" => 1
38
+
})
39
+
end
+27
lib/atproto/com/atproto/sync/getCheckout.ex
+27
lib/atproto/com/atproto/sync/getCheckout.ex
···
1
+
defmodule Com.Atproto.Sync.GetCheckout do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "DEPRECATED - please use com.atproto.sync.getRepo instead",
9
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
10
+
"parameters" => %{
11
+
"properties" => %{
12
+
"did" => %{
13
+
"description" => "The DID of the repo.",
14
+
"format" => "did",
15
+
"type" => "string"
16
+
}
17
+
},
18
+
"required" => ["did"],
19
+
"type" => "params"
20
+
},
21
+
"type" => "query"
22
+
}
23
+
},
24
+
"id" => "com.atproto.sync.getCheckout",
25
+
"lexicon" => 1
26
+
})
27
+
end
+35
lib/atproto/com/atproto/sync/getHead.ex
+35
lib/atproto/com/atproto/sync/getHead.ex
···
1
+
defmodule Com.Atproto.Sync.GetHead do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" => "DEPRECATED - please use com.atproto.sync.getLatestCommit instead",
9
+
"errors" => [%{"name" => "HeadNotFound"}],
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{"root" => %{"format" => "cid", "type" => "string"}},
14
+
"required" => ["root"],
15
+
"type" => "object"
16
+
}
17
+
},
18
+
"parameters" => %{
19
+
"properties" => %{
20
+
"did" => %{
21
+
"description" => "The DID of the repo.",
22
+
"format" => "did",
23
+
"type" => "string"
24
+
}
25
+
},
26
+
"required" => ["did"],
27
+
"type" => "params"
28
+
},
29
+
"type" => "query"
30
+
}
31
+
},
32
+
"id" => "com.atproto.sync.getHead",
33
+
"lexicon" => 1
34
+
})
35
+
end
+51
lib/atproto/com/atproto/sync/getHostStatus.ex
+51
lib/atproto/com/atproto/sync/getHostStatus.ex
···
1
+
defmodule Com.Atproto.Sync.GetHostStatus do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Returns information about a specified upstream host, as consumed by the server. Implemented by relays.",
10
+
"errors" => [%{"name" => "HostNotFound"}],
11
+
"output" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"accountCount" => %{
16
+
"description" =>
17
+
"Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts.",
18
+
"type" => "integer"
19
+
},
20
+
"hostname" => %{"type" => "string"},
21
+
"seq" => %{
22
+
"description" =>
23
+
"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).",
24
+
"type" => "integer"
25
+
},
26
+
"status" => %{
27
+
"ref" => "com.atproto.sync.defs#hostStatus",
28
+
"type" => "ref"
29
+
}
30
+
},
31
+
"required" => ["hostname"],
32
+
"type" => "object"
33
+
}
34
+
},
35
+
"parameters" => %{
36
+
"properties" => %{
37
+
"hostname" => %{
38
+
"description" => "Hostname of the host (eg, PDS or relay) being queried.",
39
+
"type" => "string"
40
+
}
41
+
},
42
+
"required" => ["hostname"],
43
+
"type" => "params"
44
+
},
45
+
"type" => "query"
46
+
}
47
+
},
48
+
"id" => "com.atproto.sync.getHostStatus",
49
+
"lexicon" => 1
50
+
})
51
+
end
+44
lib/atproto/com/atproto/sync/getLatestCommit.ex
+44
lib/atproto/com/atproto/sync/getLatestCommit.ex
···
1
+
defmodule Com.Atproto.Sync.GetLatestCommit do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get the current commit CID & revision of the specified repo. Does not require auth.",
10
+
"errors" => [
11
+
%{"name" => "RepoNotFound"},
12
+
%{"name" => "RepoTakendown"},
13
+
%{"name" => "RepoSuspended"},
14
+
%{"name" => "RepoDeactivated"}
15
+
],
16
+
"output" => %{
17
+
"encoding" => "application/json",
18
+
"schema" => %{
19
+
"properties" => %{
20
+
"cid" => %{"format" => "cid", "type" => "string"},
21
+
"rev" => %{"format" => "tid", "type" => "string"}
22
+
},
23
+
"required" => ["cid", "rev"],
24
+
"type" => "object"
25
+
}
26
+
},
27
+
"parameters" => %{
28
+
"properties" => %{
29
+
"did" => %{
30
+
"description" => "The DID of the repo.",
31
+
"format" => "did",
32
+
"type" => "string"
33
+
}
34
+
},
35
+
"required" => ["did"],
36
+
"type" => "params"
37
+
},
38
+
"type" => "query"
39
+
}
40
+
},
41
+
"id" => "com.atproto.sync.getLatestCommit",
42
+
"lexicon" => 1
43
+
})
44
+
end
+41
lib/atproto/com/atproto/sync/getRecord.ex
+41
lib/atproto/com/atproto/sync/getRecord.ex
···
1
+
defmodule Com.Atproto.Sync.GetRecord do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth.",
10
+
"errors" => [
11
+
%{"name" => "RecordNotFound"},
12
+
%{"name" => "RepoNotFound"},
13
+
%{"name" => "RepoTakendown"},
14
+
%{"name" => "RepoSuspended"},
15
+
%{"name" => "RepoDeactivated"}
16
+
],
17
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
18
+
"parameters" => %{
19
+
"properties" => %{
20
+
"collection" => %{"format" => "nsid", "type" => "string"},
21
+
"did" => %{
22
+
"description" => "The DID of the repo.",
23
+
"format" => "did",
24
+
"type" => "string"
25
+
},
26
+
"rkey" => %{
27
+
"description" => "Record Key",
28
+
"format" => "record-key",
29
+
"type" => "string"
30
+
}
31
+
},
32
+
"required" => ["did", "collection", "rkey"],
33
+
"type" => "params"
34
+
},
35
+
"type" => "query"
36
+
}
37
+
},
38
+
"id" => "com.atproto.sync.getRecord",
39
+
"lexicon" => 1
40
+
})
41
+
end
+39
lib/atproto/com/atproto/sync/getRepo.ex
+39
lib/atproto/com/atproto/sync/getRepo.ex
···
1
+
defmodule Com.Atproto.Sync.GetRepo do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS.",
10
+
"errors" => [
11
+
%{"name" => "RepoNotFound"},
12
+
%{"name" => "RepoTakendown"},
13
+
%{"name" => "RepoSuspended"},
14
+
%{"name" => "RepoDeactivated"}
15
+
],
16
+
"output" => %{"encoding" => "application/vnd.ipld.car"},
17
+
"parameters" => %{
18
+
"properties" => %{
19
+
"did" => %{
20
+
"description" => "The DID of the repo.",
21
+
"format" => "did",
22
+
"type" => "string"
23
+
},
24
+
"since" => %{
25
+
"description" => "The revision ('rev') of the repo to create a diff from.",
26
+
"format" => "tid",
27
+
"type" => "string"
28
+
}
29
+
},
30
+
"required" => ["did"],
31
+
"type" => "params"
32
+
},
33
+
"type" => "query"
34
+
}
35
+
},
36
+
"id" => "com.atproto.sync.getRepo",
37
+
"lexicon" => 1
38
+
})
39
+
end
+57
lib/atproto/com/atproto/sync/getRepoStatus.ex
+57
lib/atproto/com/atproto/sync/getRepoStatus.ex
···
1
+
defmodule Com.Atproto.Sync.GetRepoStatus do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay.",
10
+
"errors" => [%{"name" => "RepoNotFound"}],
11
+
"output" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"active" => %{"type" => "boolean"},
16
+
"did" => %{"format" => "did", "type" => "string"},
17
+
"rev" => %{
18
+
"description" => "Optional field, the current rev of the repo, if active=true",
19
+
"format" => "tid",
20
+
"type" => "string"
21
+
},
22
+
"status" => %{
23
+
"description" =>
24
+
"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.",
25
+
"knownValues" => [
26
+
"takendown",
27
+
"suspended",
28
+
"deleted",
29
+
"deactivated",
30
+
"desynchronized",
31
+
"throttled"
32
+
],
33
+
"type" => "string"
34
+
}
35
+
},
36
+
"required" => ["did", "active"],
37
+
"type" => "object"
38
+
}
39
+
},
40
+
"parameters" => %{
41
+
"properties" => %{
42
+
"did" => %{
43
+
"description" => "The DID of the repo.",
44
+
"format" => "did",
45
+
"type" => "string"
46
+
}
47
+
},
48
+
"required" => ["did"],
49
+
"type" => "params"
50
+
},
51
+
"type" => "query"
52
+
}
53
+
},
54
+
"id" => "com.atproto.sync.getRepoStatus",
55
+
"lexicon" => 1
56
+
})
57
+
end
+59
lib/atproto/com/atproto/sync/listBlobs.ex
+59
lib/atproto/com/atproto/sync/listBlobs.ex
···
1
+
defmodule Com.Atproto.Sync.ListBlobs do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS.",
10
+
"errors" => [
11
+
%{"name" => "RepoNotFound"},
12
+
%{"name" => "RepoTakendown"},
13
+
%{"name" => "RepoSuspended"},
14
+
%{"name" => "RepoDeactivated"}
15
+
],
16
+
"output" => %{
17
+
"encoding" => "application/json",
18
+
"schema" => %{
19
+
"properties" => %{
20
+
"cids" => %{
21
+
"items" => %{"format" => "cid", "type" => "string"},
22
+
"type" => "array"
23
+
},
24
+
"cursor" => %{"type" => "string"}
25
+
},
26
+
"required" => ["cids"],
27
+
"type" => "object"
28
+
}
29
+
},
30
+
"parameters" => %{
31
+
"properties" => %{
32
+
"cursor" => %{"type" => "string"},
33
+
"did" => %{
34
+
"description" => "The DID of the repo.",
35
+
"format" => "did",
36
+
"type" => "string"
37
+
},
38
+
"limit" => %{
39
+
"default" => 500,
40
+
"maximum" => 1000,
41
+
"minimum" => 1,
42
+
"type" => "integer"
43
+
},
44
+
"since" => %{
45
+
"description" => "Optional revision of the repo to list blobs since.",
46
+
"format" => "tid",
47
+
"type" => "string"
48
+
}
49
+
},
50
+
"required" => ["did"],
51
+
"type" => "params"
52
+
},
53
+
"type" => "query"
54
+
}
55
+
},
56
+
"id" => "com.atproto.sync.listBlobs",
57
+
"lexicon" => 1
58
+
})
59
+
end
+64
lib/atproto/com/atproto/sync/listHosts.ex
+64
lib/atproto/com/atproto/sync/listHosts.ex
···
1
+
defmodule Com.Atproto.Sync.ListHosts do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"host" => %{
8
+
"properties" => %{
9
+
"accountCount" => %{"type" => "integer"},
10
+
"hostname" => %{
11
+
"description" => "hostname of server; not a URL (no scheme)",
12
+
"type" => "string"
13
+
},
14
+
"seq" => %{
15
+
"description" =>
16
+
"Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor).",
17
+
"type" => "integer"
18
+
},
19
+
"status" => %{
20
+
"ref" => "com.atproto.sync.defs#hostStatus",
21
+
"type" => "ref"
22
+
}
23
+
},
24
+
"required" => ["hostname"],
25
+
"type" => "object"
26
+
},
27
+
"main" => %{
28
+
"description" =>
29
+
"Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays.",
30
+
"output" => %{
31
+
"encoding" => "application/json",
32
+
"schema" => %{
33
+
"properties" => %{
34
+
"cursor" => %{"type" => "string"},
35
+
"hosts" => %{
36
+
"description" =>
37
+
"Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first.",
38
+
"items" => %{"ref" => "#host", "type" => "ref"},
39
+
"type" => "array"
40
+
}
41
+
},
42
+
"required" => ["hosts"],
43
+
"type" => "object"
44
+
}
45
+
},
46
+
"parameters" => %{
47
+
"properties" => %{
48
+
"cursor" => %{"type" => "string"},
49
+
"limit" => %{
50
+
"default" => 200,
51
+
"maximum" => 1000,
52
+
"minimum" => 1,
53
+
"type" => "integer"
54
+
}
55
+
},
56
+
"type" => "params"
57
+
},
58
+
"type" => "query"
59
+
}
60
+
},
61
+
"id" => "com.atproto.sync.listHosts",
62
+
"lexicon" => 1
63
+
})
64
+
end
+69
lib/atproto/com/atproto/sync/listRepos.ex
+69
lib/atproto/com/atproto/sync/listRepos.ex
···
1
+
defmodule Com.Atproto.Sync.ListRepos do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"cursor" => %{"type" => "string"},
15
+
"repos" => %{
16
+
"items" => %{"ref" => "#repo", "type" => "ref"},
17
+
"type" => "array"
18
+
}
19
+
},
20
+
"required" => ["repos"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"parameters" => %{
25
+
"properties" => %{
26
+
"cursor" => %{"type" => "string"},
27
+
"limit" => %{
28
+
"default" => 500,
29
+
"maximum" => 1000,
30
+
"minimum" => 1,
31
+
"type" => "integer"
32
+
}
33
+
},
34
+
"type" => "params"
35
+
},
36
+
"type" => "query"
37
+
},
38
+
"repo" => %{
39
+
"properties" => %{
40
+
"active" => %{"type" => "boolean"},
41
+
"did" => %{"format" => "did", "type" => "string"},
42
+
"head" => %{
43
+
"description" => "Current repo commit CID",
44
+
"format" => "cid",
45
+
"type" => "string"
46
+
},
47
+
"rev" => %{"format" => "tid", "type" => "string"},
48
+
"status" => %{
49
+
"description" =>
50
+
"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.",
51
+
"knownValues" => [
52
+
"takendown",
53
+
"suspended",
54
+
"deleted",
55
+
"deactivated",
56
+
"desynchronized",
57
+
"throttled"
58
+
],
59
+
"type" => "string"
60
+
}
61
+
},
62
+
"required" => ["did", "head", "rev"],
63
+
"type" => "object"
64
+
}
65
+
},
66
+
"id" => "com.atproto.sync.listRepos",
67
+
"lexicon" => 1
68
+
})
69
+
end
+51
lib/atproto/com/atproto/sync/listReposByCollection.ex
+51
lib/atproto/com/atproto/sync/listReposByCollection.ex
···
1
+
defmodule Com.Atproto.Sync.ListReposByCollection do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Enumerates all the DIDs which have records with the given collection NSID.",
10
+
"output" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"cursor" => %{"type" => "string"},
15
+
"repos" => %{
16
+
"items" => %{"ref" => "#repo", "type" => "ref"},
17
+
"type" => "array"
18
+
}
19
+
},
20
+
"required" => ["repos"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"parameters" => %{
25
+
"properties" => %{
26
+
"collection" => %{"format" => "nsid", "type" => "string"},
27
+
"cursor" => %{"type" => "string"},
28
+
"limit" => %{
29
+
"default" => 500,
30
+
"description" =>
31
+
"Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists.",
32
+
"maximum" => 2000,
33
+
"minimum" => 1,
34
+
"type" => "integer"
35
+
}
36
+
},
37
+
"required" => ["collection"],
38
+
"type" => "params"
39
+
},
40
+
"type" => "query"
41
+
},
42
+
"repo" => %{
43
+
"properties" => %{"did" => %{"format" => "did", "type" => "string"}},
44
+
"required" => ["did"],
45
+
"type" => "object"
46
+
}
47
+
},
48
+
"id" => "com.atproto.sync.listReposByCollection",
49
+
"lexicon" => 1
50
+
})
51
+
end
+30
lib/atproto/com/atproto/sync/notifyOfUpdate.ex
+30
lib/atproto/com/atproto/sync/notifyOfUpdate.ex
···
1
+
defmodule Com.Atproto.Sync.NotifyOfUpdate do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"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",
10
+
"input" => %{
11
+
"encoding" => "application/json",
12
+
"schema" => %{
13
+
"properties" => %{
14
+
"hostname" => %{
15
+
"description" =>
16
+
"Hostname of the current service (usually a PDS) that is notifying of update.",
17
+
"type" => "string"
18
+
}
19
+
},
20
+
"required" => ["hostname"],
21
+
"type" => "object"
22
+
}
23
+
},
24
+
"type" => "procedure"
25
+
}
26
+
},
27
+
"id" => "com.atproto.sync.notifyOfUpdate",
28
+
"lexicon" => 1
29
+
})
30
+
end
+31
lib/atproto/com/atproto/sync/requestCrawl.ex
+31
lib/atproto/com/atproto/sync/requestCrawl.ex
···
1
+
defmodule Com.Atproto.Sync.RequestCrawl do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"main" => %{
8
+
"description" =>
9
+
"Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth.",
10
+
"errors" => [%{"name" => "HostBanned"}],
11
+
"input" => %{
12
+
"encoding" => "application/json",
13
+
"schema" => %{
14
+
"properties" => %{
15
+
"hostname" => %{
16
+
"description" =>
17
+
"Hostname of the current service (eg, PDS) that is requesting to be crawled.",
18
+
"type" => "string"
19
+
}
20
+
},
21
+
"required" => ["hostname"],
22
+
"type" => "object"
23
+
}
24
+
},
25
+
"type" => "procedure"
26
+
}
27
+
},
28
+
"id" => "com.atproto.sync.requestCrawl",
29
+
"lexicon" => 1
30
+
})
31
+
end
+238
lib/atproto/com/atproto/sync/subscribeRepos.ex
+238
lib/atproto/com/atproto/sync/subscribeRepos.ex
···
1
+
defmodule Com.Atproto.Sync.SubscribeRepos do
2
+
@moduledoc false
3
+
use Atex.Lexicon
4
+
5
+
deflexicon(%{
6
+
"defs" => %{
7
+
"account" => %{
8
+
"description" =>
9
+
"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.",
10
+
"properties" => %{
11
+
"active" => %{
12
+
"description" =>
13
+
"Indicates that the account has a repository which can be fetched from the host that emitted this event.",
14
+
"type" => "boolean"
15
+
},
16
+
"did" => %{"format" => "did", "type" => "string"},
17
+
"seq" => %{"type" => "integer"},
18
+
"status" => %{
19
+
"description" =>
20
+
"If active=false, this optional field indicates a reason for why the account is not active.",
21
+
"knownValues" => [
22
+
"takendown",
23
+
"suspended",
24
+
"deleted",
25
+
"deactivated",
26
+
"desynchronized",
27
+
"throttled"
28
+
],
29
+
"type" => "string"
30
+
},
31
+
"time" => %{"format" => "datetime", "type" => "string"}
32
+
},
33
+
"required" => ["seq", "did", "time", "active"],
34
+
"type" => "object"
35
+
},
36
+
"commit" => %{
37
+
"description" =>
38
+
"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.",
39
+
"nullable" => ["since"],
40
+
"properties" => %{
41
+
"blobs" => %{
42
+
"items" => %{
43
+
"description" =>
44
+
"DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit.",
45
+
"type" => "cid-link"
46
+
},
47
+
"type" => "array"
48
+
},
49
+
"blocks" => %{
50
+
"description" =>
51
+
"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.",
52
+
"maxLength" => 2_000_000,
53
+
"type" => "bytes"
54
+
},
55
+
"commit" => %{
56
+
"description" => "Repo commit object CID.",
57
+
"type" => "cid-link"
58
+
},
59
+
"ops" => %{
60
+
"items" => %{
61
+
"description" =>
62
+
"List of repo mutation operations in this commit (eg, records created, updated, or deleted).",
63
+
"ref" => "#repoOp",
64
+
"type" => "ref"
65
+
},
66
+
"maxLength" => 200,
67
+
"type" => "array"
68
+
},
69
+
"prevData" => %{
70
+
"description" =>
71
+
"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.",
72
+
"type" => "cid-link"
73
+
},
74
+
"rebase" => %{
75
+
"description" => "DEPRECATED -- unused",
76
+
"type" => "boolean"
77
+
},
78
+
"repo" => %{
79
+
"description" =>
80
+
"The repo this event comes from. Note that all other message types name this field 'did'.",
81
+
"format" => "did",
82
+
"type" => "string"
83
+
},
84
+
"rev" => %{
85
+
"description" =>
86
+
"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.",
87
+
"format" => "tid",
88
+
"type" => "string"
89
+
},
90
+
"seq" => %{
91
+
"description" => "The stream sequence number of this message.",
92
+
"type" => "integer"
93
+
},
94
+
"since" => %{
95
+
"description" => "The rev of the last emitted commit from this repo (if any).",
96
+
"format" => "tid",
97
+
"type" => "string"
98
+
},
99
+
"time" => %{
100
+
"description" => "Timestamp of when this message was originally broadcast.",
101
+
"format" => "datetime",
102
+
"type" => "string"
103
+
},
104
+
"tooBig" => %{
105
+
"description" =>
106
+
"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.",
107
+
"type" => "boolean"
108
+
}
109
+
},
110
+
"required" => [
111
+
"seq",
112
+
"rebase",
113
+
"tooBig",
114
+
"repo",
115
+
"commit",
116
+
"rev",
117
+
"since",
118
+
"blocks",
119
+
"ops",
120
+
"blobs",
121
+
"time"
122
+
],
123
+
"type" => "object"
124
+
},
125
+
"identity" => %{
126
+
"description" =>
127
+
"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.",
128
+
"properties" => %{
129
+
"did" => %{"format" => "did", "type" => "string"},
130
+
"handle" => %{
131
+
"description" =>
132
+
"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.",
133
+
"format" => "handle",
134
+
"type" => "string"
135
+
},
136
+
"seq" => %{"type" => "integer"},
137
+
"time" => %{"format" => "datetime", "type" => "string"}
138
+
},
139
+
"required" => ["seq", "did", "time"],
140
+
"type" => "object"
141
+
},
142
+
"info" => %{
143
+
"properties" => %{
144
+
"message" => %{"type" => "string"},
145
+
"name" => %{"knownValues" => ["OutdatedCursor"], "type" => "string"}
146
+
},
147
+
"required" => ["name"],
148
+
"type" => "object"
149
+
},
150
+
"main" => %{
151
+
"description" =>
152
+
"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.",
153
+
"errors" => [
154
+
%{"name" => "FutureCursor"},
155
+
%{
156
+
"description" =>
157
+
"If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection.",
158
+
"name" => "ConsumerTooSlow"
159
+
}
160
+
],
161
+
"message" => %{
162
+
"schema" => %{
163
+
"refs" => ["#commit", "#sync", "#identity", "#account", "#info"],
164
+
"type" => "union"
165
+
}
166
+
},
167
+
"parameters" => %{
168
+
"properties" => %{
169
+
"cursor" => %{
170
+
"description" => "The last known event seq number to backfill from.",
171
+
"type" => "integer"
172
+
}
173
+
},
174
+
"type" => "params"
175
+
},
176
+
"type" => "subscription"
177
+
},
178
+
"repoOp" => %{
179
+
"description" => "A repo operation, ie a mutation of a single record.",
180
+
"nullable" => ["cid"],
181
+
"properties" => %{
182
+
"action" => %{
183
+
"knownValues" => ["create", "update", "delete"],
184
+
"type" => "string"
185
+
},
186
+
"cid" => %{
187
+
"description" => "For creates and updates, the new record CID. For deletions, null.",
188
+
"type" => "cid-link"
189
+
},
190
+
"path" => %{"type" => "string"},
191
+
"prev" => %{
192
+
"description" =>
193
+
"For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined.",
194
+
"type" => "cid-link"
195
+
}
196
+
},
197
+
"required" => ["action", "path", "cid"],
198
+
"type" => "object"
199
+
},
200
+
"sync" => %{
201
+
"description" =>
202
+
"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.",
203
+
"properties" => %{
204
+
"blocks" => %{
205
+
"description" =>
206
+
"CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'.",
207
+
"maxLength" => 10000,
208
+
"type" => "bytes"
209
+
},
210
+
"did" => %{
211
+
"description" =>
212
+
"The account this repo event corresponds to. Must match that in the commit object.",
213
+
"format" => "did",
214
+
"type" => "string"
215
+
},
216
+
"rev" => %{
217
+
"description" =>
218
+
"The rev of the commit. This value must match that in the commit object.",
219
+
"type" => "string"
220
+
},
221
+
"seq" => %{
222
+
"description" => "The stream sequence number of this message.",
223
+
"type" => "integer"
224
+
},
225
+
"time" => %{
226
+
"description" => "Timestamp of when this message was originally broadcast.",
227
+
"format" => "datetime",
228
+
"type" => "string"
229
+
}
230
+
},
231
+
"required" => ["seq", "did", "blocks", "rev", "time"],
232
+
"type" => "object"
233
+
}
234
+
},
235
+
"id" => "com.atproto.sync.subscribeRepos",
236
+
"lexicon" => 1
237
+
})
238
+
end
+1
-1
lib/mix/tasks/atex.lexicons.ex
+1
-1
lib/mix/tasks/atex.lexicons.ex
+19
-4
mix.exs
+19
-4
mix.exs
···
1
1
defmodule Atex.MixProject do
2
2
use Mix.Project
3
3
4
-
@version "0.4.0"
4
+
@version "0.7.0"
5
5
@github "https://github.com/cometsh/atex"
6
6
@tangled "https://tangled.sh/@comet.sh/atex"
7
7
···
35
35
{:typedstruct, "~> 0.5"},
36
36
{:ex_cldr, "~> 2.42"},
37
37
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
38
-
{:ex_doc, "~> 0.34", only: :dev, runtime: false, warn_if_outdated: true}
38
+
{:ex_doc, "~> 0.39", only: :dev, runtime: false, warn_if_outdated: true},
39
+
{:plug, "~> 1.18"},
40
+
{:jason, "~> 1.4"},
41
+
{:jose, "~> 1.11"},
42
+
{:bandit, "~> 1.0", only: [:dev, :test]},
43
+
{:con_cache, "~> 1.1"},
44
+
{:mutex, "~> 3.0"},
45
+
{:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}
39
46
]
40
47
end
41
48
···
50
57
[
51
58
extras: [
52
59
LICENSE: [title: "License"],
53
-
"README.md": [title: "Overview"]
60
+
"README.md": [title: "Overview"],
61
+
"CHANGELOG.md": [title: "Changelog"]
54
62
],
55
63
main: "readme",
56
64
source_url: @github,
57
65
source_ref: "v#{@version}",
58
-
formatters: ["html"]
66
+
formatters: ["html"],
67
+
groups_for_modules: [
68
+
"Data types": [Atex.AtURI, Atex.DID, Atex.Handle, Atex.NSID, Atex.TID],
69
+
XRPC: ~r/^Atex\.XRPC/,
70
+
OAuth: [Atex.Config.OAuth, Atex.OAuth, Atex.OAuth.Plug],
71
+
Lexicons: ~r/^Atex\.Lexicon/,
72
+
Identity: ~r/^Atex\.IdentityResolver/
73
+
]
59
74
]
60
75
end
61
76
end
+19
-9
mix.lock
+19
-9
mix.lock
···
1
1
%{
2
+
"bandit": {:hex, :bandit, "1.10.0", "f8293b4a4e6c06b31655ae10bd3462f59d8c5dbd1df59028a4984f10c5961147", [: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", "43ebceb7060a4d8273e47d83e703d01b112198624ba0826980caa3f5091243c4"},
2
3
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
3
-
"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"},
4
-
"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"},
4
+
"cldr_utils": {:hex, :cldr_utils, "2.29.1", "11ff0a50a36a7e5f3bd9fc2fb8486a4c1bcca3081d9c080bf9e48fe0e6742e2d", [: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", "3844a0a0ed7f42e6590ddd8bd37eb4b1556b112898f67dea3ba068c29aabd6c2"},
5
+
"con_cache": {:hex, :con_cache, "1.1.1", "9f47a68dfef5ac3bbff8ce2c499869dbc5ba889dadde6ac4aff8eb78ddaf6d82", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "1def4d1bec296564c75b5bbc60a19f2b5649d81bfa345a2febcc6ae380e8ae15"},
6
+
"credo": {:hex, :credo, "1.7.15", "283da72eeb2fd3ccf7248f4941a0527efb97afa224bcdef30b4b580bc8258e1c", [: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", "291e8645ea3fea7481829f1e1eb0881b8395db212821338e577a90bf225c5607"},
5
7
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
8
+
"dialyxir": {:hex, :dialyxir, "1.4.7", "dda948fcee52962e4b6c5b4b16b2d8fa7d50d8645bbae8b8685c3f9ecb7f5f4d", [:mix], [{:erlex, ">= 0.2.8", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b34527202e6eb8cee198efec110996c25c5898f43a4094df157f8d28f27d9efe"},
6
9
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
7
-
"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"},
8
-
"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"},
9
-
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
10
+
"erlex": {:hex, :erlex, "0.2.8", "cd8116f20f3c0afe376d1e8d1f0ae2452337729f68be016ea544a72f767d9c12", [:mix], [], "hexpm", "9d66ff9fedf69e49dc3fd12831e12a8a37b76f8651dd21cd45fcf5561a8a7590"},
11
+
"ex_cldr": {:hex, :ex_cldr, "2.44.1", "0d220b175874e1ce77a0f7213bdfe700b9be11aefbf35933a0e98837803ebdc5", [: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 or ~> 1.0", [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", "3880cd6137ea21c74250cd870d3330c4a9fdec07fabd5e37d1b239547929e29b"},
12
+
"ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [: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", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"},
13
+
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
10
14
"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"},
11
15
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
12
16
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
17
+
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
13
18
"makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},
14
19
"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"},
15
20
"makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"},
16
21
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
17
22
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
18
23
"multiformats_ex": {:hex, :multiformats_ex, "0.2.0", "5b0a3faa1a770dc671aa8a89b6323cc20b0ecf67dc93dcd21312151fbea6b4ee", [:mix], [{:varint, "~> 1.4", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "aa406d9addb06dc197e0e92212992486af6599158d357680f29f2d11e08d0423"},
24
+
"mutex": {:hex, :mutex, "3.0.2", "528877fd0dbc09fc93ad667e10ea0d35a2126fa85205822f9dca85e87d732245", [:mix], [], "hexpm", "0a8f2ed3618160dca6a1e3520b293dc3c2ae53116265e71b4a732d35d29aa3c6"},
19
25
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
20
26
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
21
27
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
22
-
"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"},
23
-
"recase": {:hex, :recase, "0.9.0", "437982693fdfbec125f11c8868eb3b4d32e9aa6995d3a68ac8686f3e2bf5d8d1", [:mix], [], "hexpm", "efa7549ebd128988d1723037a6f6a61948055aec107db6288f1c52830cb6501c"},
24
-
"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"},
28
+
"peri": {:hex, :peri, "0.6.2", "3c043bfb6aa18eb1ea41d80981d19294c5e943937b1311e8e958da3581139061", [: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", "5e0d8e0bd9de93d0f8e3ad6b9a5bd143f7349c025196ef4a3591af93ce6ecad9"},
29
+
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [: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", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
30
+
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
31
+
"recase": {:hex, :recase, "0.9.1", "82d2e2e2d4f9e92da1ce5db338ede2e4f15a50ac1141fc082b80050b9f49d96e", [:mix], [], "hexpm", "19ba03ceb811750e6bec4a015a9f9e45d16a8b9e09187f6d72c3798f454710f3"},
32
+
"req": {:hex, :req, "0.5.16", "99ba6a36b014458e52a8b9a0543bfa752cb0344b2a9d756651db1281d4ba4450", [: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", "974a7a27982b9b791df84e8f6687d21483795882a7840e8309abdbe08bb06f09"},
25
33
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
26
-
"typedstruct": {:hex, :typedstruct, "0.5.3", "d68ae424251a41b81a8d0c485328ab48edbd3858f3565bbdac21b43c056fc9b4", [:make, :mix], [], "hexpm", "b53b8186701417c0b2782bf02a2db5524f879b8488f91d1d83b97d84c2943432"},
34
+
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
35
+
"typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"},
27
36
"varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"},
37
+
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
28
38
}
+1
priv/templates/lexicon.eex
+1
priv/templates/lexicon.eex
+50
test/atex/oauth/permission_test.exs
+50
test/atex/oauth/permission_test.exs
···
1
+
defmodule Atex.OAuth.PermissionTest do
2
+
use ExUnit.Case, async: true
3
+
alias Atex.OAuth.Permission
4
+
doctest Permission
5
+
6
+
describe "account/1" do
7
+
test "requires `:attr`" do
8
+
assert_raise ArgumentError, ~r/`:attr` must be provided/, fn ->
9
+
Permission.account()
10
+
end
11
+
end
12
+
13
+
test "requires valid `:attr`" do
14
+
assert_raise ArgumentError, ~r/`:attr` must be `:email` or `:repo`/, fn ->
15
+
Permission.account(attr: :foobar)
16
+
end
17
+
18
+
assert Permission.account(attr: :email)
19
+
end
20
+
21
+
test "requires valid `:action`" do
22
+
assert_raise ArgumentError, ~r/`:action` must be `:read`, `:manage`, or `nil`/, fn ->
23
+
Permission.account(attr: :email, action: :foobar)
24
+
end
25
+
26
+
assert Permission.account(attr: :email, action: :manage)
27
+
assert Permission.account(attr: :repo, action: nil)
28
+
end
29
+
end
30
+
31
+
describe "rpc/2" do
32
+
test "requires at least `:aud` or `:inherit_aud`" do
33
+
assert_raise ArgumentError, ~r/must specify either/, fn ->
34
+
Permission.rpc("com.example.getProfile")
35
+
end
36
+
end
37
+
38
+
test "disallows `:aud` and `:inherit_aud` at the same time" do
39
+
assert_raise ArgumentError, ~r/cannot specify both/, fn ->
40
+
Permission.rpc("com.example.getProfile", aud: "example", inherit_aud: true)
41
+
end
42
+
end
43
+
44
+
test "disallows wildcard for `lxm` and `aud` at the same time" do
45
+
assert_raise ArgumentError, ~r/wildcard `lxm` and wildcard `aud`/, fn ->
46
+
Permission.rpc("*", aud: "*")
47
+
end
48
+
end
49
+
end
50
+
end