+1
.formatter.exs
+1
.formatter.exs
-1
.gitignore
-1
.gitignore
+39
AGENTS.md
+39
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
+
- **Error Handling**: Return `{:ok, result}` or `{:error, reason}` tuples; use
25
+
pattern matching in case statements
26
+
- **Pattern Matching**: Prefer pattern matching over conditionals; use guards
27
+
when appropriate
28
+
- **Macros**: Use `deflexicon` macro for lexicon definitions; use `defschema`
29
+
(from Peri) for validation schemas
30
+
- **Tests**: Async by default (`use ExUnit.Case, async: true`), use doctests
31
+
where applicable
32
+
- **Dependencies**: Core deps include Peri (validation), Req (HTTP), JOSE
33
+
(JWT/OAuth), TypedStruct (structs)
34
+
35
+
## Important Notes
36
+
37
+
- **DO NOT modify** `lib/atproto/**/` - autogenerated from official AT Protocol
38
+
lexicons
39
+
- **Update CHANGELOG.md** when adding features, changes, or fixes
+5
CHANGELOG.md
+5
CHANGELOG.md
···
28
- `Atex.OAuth.Error` exception module for OAuth flow errors. Contains both a
29
human-readable `message` string and a machine-readable `reason` atom for error
30
handling.
31
+
- `Atex.OAuth.Cache` module provides TTL caching for OAuth authorization server
32
+
metadata with a 1-hour default TTL to reduce load on third-party PDSs.
33
+
- `Atex.OAuth.get_authorization_server/2` and
34
+
`Atex.OAuth.get_authorization_server_metadata/2` now support an optional
35
+
`fresh` parameter to bypass the cache when needed.
36
37
### Changed
38
+5
-1
lib/atex/application.ex
+5
-1
lib/atex/application.ex
+91
-40
lib/atex/oauth.ex
+91
-40
lib/atex/oauth.ex
···
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.
293
294
## Parameters
295
296
- `pds_host` - Base URL of the PDS (e.g., "https://bsky.social")
297
298
## Returns
299
···
302
- `{:error, :invalid_metadata}` - Server returned invalid metadata
303
- `{:error, reason}` - Error discovering authorization server
304
"""
305
-
@spec get_authorization_server(String.t()) :: {:ok, String.t()} | {:error, any()}
306
-
def get_authorization_server(pds_host) do
307
-
"#{pds_host}/.well-known/oauth-protected-resource"
308
-
|> Req.get()
309
-
|> case do
310
-
# TODO: what to do when multiple authorization servers?
311
-
{:ok, %{body: %{"authorization_servers" => [authz_server | _]}}} -> {:ok, authz_server}
312
-
{:ok, _} -> {:error, :invalid_metadata}
313
-
err -> err
314
end
315
end
316
···
319
320
Retrieves the metadata from the authorization server's
321
`.well-known/oauth-authorization-server` endpoint, providing endpoint URLs
322
-
required for the OAuth flow.
323
324
## Parameters
325
326
- `issuer` - Authorization server issuer URL
327
328
## Returns
329
···
332
- `{:error, :invalid_issuer}` - Issuer mismatch in metadata
333
- `{:error, any()}` - Other error fetching metadata
334
"""
335
-
@spec get_authorization_server_metadata(String.t()) ::
336
{:ok, authorization_metadata()} | {:error, any()}
337
-
def get_authorization_server_metadata(issuer) do
338
-
"#{issuer}/.well-known/oauth-authorization-server"
339
-
|> Req.get()
340
-
|> case do
341
-
{:ok,
342
-
%{
343
-
body: %{
344
-
"issuer" => metadata_issuer,
345
-
"pushed_authorization_request_endpoint" => par_endpoint,
346
-
"token_endpoint" => token_endpoint,
347
-
"authorization_endpoint" => authorization_endpoint
348
-
}
349
-
}} ->
350
-
if issuer != metadata_issuer do
351
-
{:error, :invaild_issuer}
352
-
else
353
-
{:ok,
354
-
%{
355
-
issuer: metadata_issuer,
356
-
par_endpoint: par_endpoint,
357
-
token_endpoint: token_endpoint,
358
-
authorization_endpoint: authorization_endpoint
359
-
}}
360
-
end
361
362
-
{:ok, _} ->
363
-
{:error, :invalid_metadata}
364
365
-
err ->
366
-
err
367
end
368
end
369
···
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
···
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
···
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
···
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
+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
+3
-2
mix.exs
+3
-2
mix.exs
···
35
{:typedstruct, "~> 0.5"},
36
{:ex_cldr, "~> 2.42"},
37
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
38
-
{:ex_doc, "~> 0.34", 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
]
44
end
45
···
35
{:typedstruct, "~> 0.5"},
36
{:ex_cldr, "~> 2.42"},
37
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
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
]
45
end
46
+2
-1
mix.lock
+2
-1
mix.lock
···
2
"bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"},
3
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
4
"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"},
5
"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"},
6
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
7
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
8
"ex_cldr": {:hex, :ex_cldr, "2.43.0", "8700031e30a03501cf65f7ba7c8287bb67339d03559f3108f3c54fe86d926b19", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "1524eb01275b89473ee5f53fcc6169bae16e4a5267ef109229f37694799e0b20"},
9
-
"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"},
10
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
11
"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"},
12
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
···
2
"bandit": {:hex, :bandit, "1.8.0", "c2e93d7e3c5c794272fa4623124f827c6f24b643acc822be64c826f9447d92fb", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "8458ff4eed20ff2a2ea69d4854883a077c33ea42b51f6811b044ceee0fa15422"},
3
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
4
"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"},
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.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"},
7
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
8
"earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"},
9
"ex_cldr": {:hex, :ex_cldr, "2.43.0", "8700031e30a03501cf65f7ba7c8287bb67339d03559f3108f3c54fe86d926b19", [:mix], [{:cldr_utils, "~> 2.28", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "1524eb01275b89473ee5f53fcc6169bae16e4a5267ef109229f37694799e0b20"},
10
+
"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"},
11
"file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"},
12
"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"},
13
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},