From 970cee2c0ee6640621b918b9444bb9f4e5c826b2 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 18 Mar 2026 20:01:12 +0100 Subject: [PATCH] Document embedded API surface --- README.md | 34 ++++++- docs/ARCH.md | 20 ++-- docs/KHATRU.md | 140 ---------------------------- docs/LOCAL_API.md | 147 ++++++++++++++++++++++++++++++ lib/parrhesia.ex | 30 ++++-- lib/parrhesia/connection_stats.ex | 7 +- lib/parrhesia/postgres_types.ex | 5 +- lib/parrhesia/release.ex | 11 +++ lib/parrhesia/repo.ex | 5 +- lib/parrhesia/runtime.ex | 22 ++++- lib/parrhesia/telemetry.ex | 4 + lib/parrhesia/web/relay_info.ex | 6 +- mix.exs | 39 +++++++- mix.lock | 6 ++ 14 files changed, 311 insertions(+), 165 deletions(-) delete mode 100644 docs/KHATRU.md create mode 100644 docs/LOCAL_API.md diff --git a/README.md b/README.md index 328f48c..b81ea8e 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Current `supported_nips` list: ## Requirements -- Elixir `~> 1.19` +- Elixir `~> 1.18` - Erlang/OTP 28 - PostgreSQL (18 used in the dev environment; 16+ recommended) - Docker or Podman plus Docker Compose support if you want to run the published container image @@ -114,6 +114,38 @@ GitHub CI currently runs the non-Docker node-sync e2e on the main Linux matrix j --- +## Embedding in another Elixir app + +Parrhesia is usable as an embedded OTP dependency, not just as a standalone relay process. +The intended in-process surface is `Parrhesia.API.*`, especially: + +- `Parrhesia.API.Events` for publish, query, and count +- `Parrhesia.API.Stream` for local REQ-like subscriptions +- `Parrhesia.API.Admin` for management operations +- `Parrhesia.API.Identity`, `Parrhesia.API.ACL`, and `Parrhesia.API.Sync` for relay identity, protected sync ACLs, and outbound relay sync + +Start with: + +- [`docs/LOCAL_API.md`](./docs/LOCAL_API.md) for the embedding model and a minimal host setup +- generated ExDoc for the `Embedded API` module group when running `mix docs` + +Important caveats for host applications: + +- Parrhesia is still alpha; expect some public API and config churn. +- Parrhesia currently assumes a single runtime per BEAM node and uses globally registered process names. +- The defaults in this repo's `config/*.exs` are not imported automatically when Parrhesia is used as a dependency. A host app must set `config :parrhesia, ...` explicitly. +- The host app is responsible for migrating Parrhesia's schema, for example with `Parrhesia.Release.migrate()` or `mix ecto.migrate -r Parrhesia.Repo`. + +If you only want the in-process API and not the HTTP/WebSocket edge, configure: + +```elixir +config :parrhesia, :listeners, %{} +``` + +The config reference below still applies when embedded. That is the primary place to document basic setup and runtime configuration changes. + +--- + ## Production configuration ### Minimal setup diff --git a/docs/ARCH.md b/docs/ARCH.md index 2a74c3c..3c173d3 100644 --- a/docs/ARCH.md +++ b/docs/ARCH.md @@ -82,16 +82,20 @@ Configured WS/HTTP Listeners (Bandit/Plug) ## 4) OTP supervision design -`Parrhesia.Application` children (top-level): +`Parrhesia.Runtime` children (top-level): 1. `Parrhesia.Telemetry` – metric definitions/reporters -2. `Parrhesia.Config` – runtime config cache (ETS-backed) -3. `Parrhesia.Storage.Supervisor` – adapter processes (`Repo`, pools) -4. `Parrhesia.Subscriptions.Supervisor` – subscription index + fanout workers -5. `Parrhesia.Auth.Supervisor` – AUTH challenge/session tracking -6. `Parrhesia.Policy.Supervisor` – rate limiters / ACL caches -7. `Parrhesia.Web.Endpoint` – supervises configured WS + HTTP listeners -8. `Parrhesia.Tasks.Supervisor` – background jobs (expiry purge, maintenance) +2. `Parrhesia.ConnectionStats` – per-listener connection/subscription counters +3. `Parrhesia.Config` – runtime config cache (ETS-backed) +4. `Parrhesia.Web.EventIngestLimiter` – relay-wide event ingest rate limiter +5. `Parrhesia.Web.IPEventIngestLimiter` – per-IP event ingest rate limiter +6. `Parrhesia.Storage.Supervisor` – adapter processes (`Repo`, pools) +7. `Parrhesia.Subscriptions.Supervisor` – subscription index + fanout workers +8. `Parrhesia.Auth.Supervisor` – AUTH challenge/session tracking +9. `Parrhesia.Sync.Supervisor` – outbound relay sync workers +10. `Parrhesia.Policy.Supervisor` – rate limiters / ACL caches +11. `Parrhesia.Web.Endpoint` – supervises configured WS + HTTP listeners +12. `Parrhesia.Tasks.Supervisor` – background jobs (expiry purge, maintenance) Failure model: diff --git a/docs/KHATRU.md b/docs/KHATRU.md deleted file mode 100644 index 83c172f..0000000 --- a/docs/KHATRU.md +++ /dev/null @@ -1,140 +0,0 @@ -# Khatru-Inspired Runtime Improvements - -This document collects refactoring and extension ideas learned from studying Khatru-style relay design. - -It is intentionally **not** about the new public API surface or the sync ACL model. Those live in `docs/slop/LOCAL_API.md` and `docs/SYNC.md`. - -The focus here is runtime shape, protocol behavior, and operator-visible relay features. - ---- - -## 1. Why This Matters - -Khatru appears mature mainly because it exposes clearer relay pipeline stages. - -That gives three practical benefits: - -- less policy drift between storage, websocket, and management code, -- easier feature addition without hard-coding more branches into one connection module, -- better composability for relay profiles with different trust and traffic models. - -Parrhesia should borrow that clarity without copying Khatru's code-first hook model wholesale. - ---- - -## 2. Proposed Runtime Refactors - -### 2.1 Staged policy pipeline - -Parrhesia should stop treating policy as one coarse `EventPolicy` module plus scattered special cases. - -Recommended internal stages: - -1. connection admission -2. authentication challenge and validation -3. publish/write authorization -4. query/count authorization -5. stream subscription authorization -6. negentropy authorization -7. response shaping -8. broadcast/fanout suppression - -This is an internal runtime refactor. It does not imply a new public API. - -### 2.2 Richer internal request context - -The runtime should carry a structured request context through all stages. - -Useful fields: - -- authenticated pubkeys -- caller kind -- remote IP -- subscription id -- peer id -- negentropy session flag -- internal-call flag - -This reduces ad-hoc branching and makes audit/telemetry more coherent. - -### 2.3 Separate policy from storage presence tables - -Moderation state should remain data. - -Runtime enforcement should be a first-class layer that consumes that data, not a side effect of whether a table exists. - -This is especially important for: - -- blocked IP enforcement, -- pubkey allowlists, -- future kind- or tag-scoped restrictions. - ---- - -## 3. Protocol and Relay Features - -### 3.1 Real COUNT sketches - -Parrhesia currently returns a synthetic `hll` payload for NIP-45-style count responses. - -If approximate count exchange matters, implement a real reusable HLL sketch path instead of hashing `filters + count`. - -### 3.2 Relay identity in NIP-11 - -Once Parrhesia owns a stable server identity, NIP-11 should expose the relay pubkey instead of returning `nil`. - -This is useful beyond sync: - -- operator visibility, -- relay fingerprinting, -- future trust tooling. - -### 3.3 Connection-level IP enforcement - -Blocked IP support should be enforced on actual connection admission, not only stored in management tables. - -This should happen early, before expensive protocol handling. - -### 3.4 Better response shaping - -Introduce a narrow internal response shaping layer for cases where returned events or counts need controlled rewriting or suppression. - -Examples: - -- hide fields for specific relay profiles, -- suppress rebroadcast of locally-ingested remote sync traffic, -- shape relay notices consistently. - -This should stay narrow and deterministic. It should not become arbitrary app semantics. - ---- - -## 4. Suggested Extension Points - -These should be internal runtime seams, not necessarily public interfaces: - -- `ConnectionPolicy` -- `AuthPolicy` -- `ReadPolicy` -- `WritePolicy` -- `NegentropyPolicy` -- `ResponsePolicy` -- `BroadcastPolicy` - -They may initially be plain modules with well-defined callbacks or functions. - -The point is not pluggability for its own sake. The point is to make policy stages explicit and testable. - ---- - -## 5. Near-Term Priority - -Recommended order: - -1. enforce blocked IPs and any future connection-gating on the real connection path -2. split the current websocket flow into explicit read/write/negentropy policy stages -3. enrich runtime request context and telemetry metadata -4. expose relay pubkey in NIP-11 once identity lands -5. replace fake HLL payloads with a real approximate-count implementation if NIP-45 support matters operationally - -This keeps the runtime improvements incremental and independent from the ongoing API and ACL implementation. diff --git a/docs/LOCAL_API.md b/docs/LOCAL_API.md new file mode 100644 index 0000000..ace7a68 --- /dev/null +++ b/docs/LOCAL_API.md @@ -0,0 +1,147 @@ +# Parrhesia Local API + +Parrhesia can run as a normal standalone relay application, but it also exposes a stable +in-process API for Elixir callers that want to embed the relay inside a larger OTP system. + +This document describes that embedding surface. The runtime is still alpha, so treat the API +as usable but not yet frozen. + +## What embedding means today + +Embedding currently means: + +- the host app adds `:parrhesia` as a dependency and OTP application +- the host app provides `config :parrhesia, ...` explicitly +- the host app migrates the Parrhesia database schema +- callers interact with the relay through `Parrhesia.API.*` + +Current operational assumptions: + +- Parrhesia runs one runtime per BEAM node +- core processes use global module names such as `Parrhesia.Config` and `Parrhesia.Web.Endpoint` +- the config defaults in this repo's `config/*.exs` are not imported automatically by a host app + +If you want multiple isolated relay instances inside one VM, Parrhesia does not support that +cleanly yet. + +## Minimal host setup + +Add the dependency in your host app: + +```elixir +defp deps do + [ + {:parrhesia, path: "../parrhesia"} + ] +end +``` + +Configure the runtime in your host app. At minimum you should carry over: + +```elixir +import Config + +config :postgrex, :json_library, JSON + +config :parrhesia, + relay_url: "wss://relay.example.com/relay", + listeners: %{}, + storage: [backend: :postgres] + +config :parrhesia, Parrhesia.Repo, + url: System.fetch_env!("DATABASE_URL"), + pool_size: 10, + types: Parrhesia.PostgresTypes + +config :parrhesia, Parrhesia.ReadRepo, + url: System.fetch_env!("DATABASE_URL"), + pool_size: 10, + types: Parrhesia.PostgresTypes + +config :parrhesia, ecto_repos: [Parrhesia.Repo] +``` + +Notes: + +- Set `listeners: %{}` if you only want the in-process API and no HTTP/WebSocket ingress. +- If you do want ingress, copy the listener shape from the config reference in + [README.md](../README.md). +- Production runtime overrides still use the `PARRHESIA_*` environment variables described in + [README.md](../README.md). + +Migrate before serving traffic: + +```elixir +Parrhesia.Release.migrate() +``` + +In development, `mix ecto.migrate -r Parrhesia.Repo` works too. + +## Starting the runtime + +In the common case, letting OTP start the `:parrhesia` application is enough. + +If you need to start the runtime explicitly under your own supervision tree, use +`Parrhesia.Runtime`: + +```elixir +children = [ + {Parrhesia.Runtime, name: Parrhesia.Supervisor} +] +``` + +## Primary modules + +The in-process surface is centered on these modules: + +- `Parrhesia.API.Events` for publish, query, and count +- `Parrhesia.API.Stream` for REQ-like local subscriptions +- `Parrhesia.API.Auth` for event validation and NIP-98 auth parsing +- `Parrhesia.API.Admin` for management operations +- `Parrhesia.API.Identity` for relay-owned key management +- `Parrhesia.API.ACL` for protected sync ACLs +- `Parrhesia.API.Sync` for outbound relay sync management + +Generated ExDoc groups these modules under `Embedded API`. + +## Request context + +Most calls take a `Parrhesia.API.RequestContext`. This carries authenticated pubkeys and +caller metadata through policy checks. + +```elixir +%Parrhesia.API.RequestContext{ + caller: :local, + authenticated_pubkeys: MapSet.new() +} +``` + +If your host app has already authenticated a user or peer, put that pubkey into +`authenticated_pubkeys` before calling the API. + +## Example + +```elixir +alias Parrhesia.API.Events +alias Parrhesia.API.RequestContext +alias Parrhesia.API.Stream + +context = %RequestContext{caller: :local} + +{:ok, publish_result} = Events.publish(event, context: context) +{:ok, events} = Events.query([%{"kinds" => [1]}], context: context) +{:ok, ref} = Stream.subscribe(self(), "local-sub", [%{"kinds" => [1]}], context: context) + +receive do + {:parrhesia, :event, ^ref, "local-sub", event} -> event + {:parrhesia, :eose, ^ref, "local-sub"} -> :ok +end + +:ok = Stream.unsubscribe(ref) +``` + +## Where to look next + +- [README.md](../README.md) for setup and the full config reference +- [docs/SYNC.md](./SYNC.md) for relay-to-relay sync semantics +- module docs under `Parrhesia.API.*` for per-function behavior diff --git a/lib/parrhesia.ex b/lib/parrhesia.ex index 5cdfc95..c28cb07 100644 --- a/lib/parrhesia.ex +++ b/lib/parrhesia.ex @@ -1,17 +1,27 @@ defmodule Parrhesia do @moduledoc """ - Documentation for `Parrhesia`. + Parrhesia is a Nostr relay runtime that can run standalone or as an embedded OTP service. + + For embedded use, the main developer-facing surface is `Parrhesia.API.*`. + Start with: + + - `Parrhesia.API.Events` + - `Parrhesia.API.Stream` + - `Parrhesia.API.Admin` + - `Parrhesia.API.Identity` + - `Parrhesia.API.ACL` + - `Parrhesia.API.Sync` + + The host application is responsible for: + + - setting `config :parrhesia, ...` + - migrating the configured Parrhesia repos + - deciding whether to expose listeners or use only the in-process API + + See `README.md` and `docs/LOCAL_API.md` for the embedding model and configuration guide. """ - @doc """ - Hello world. - - ## Examples - - iex> Parrhesia.hello() - :world - - """ + @doc false def hello do :world end diff --git a/lib/parrhesia/connection_stats.ex b/lib/parrhesia/connection_stats.ex index fc0f615..9795765 100644 --- a/lib/parrhesia/connection_stats.ex +++ b/lib/parrhesia/connection_stats.ex @@ -1,5 +1,10 @@ defmodule Parrhesia.ConnectionStats do - @moduledoc false + @moduledoc """ + Per-listener connection and subscription counters. + + Tracks active connection and subscription counts per listener and emits + `[:parrhesia, :listener, :population]` telemetry events on each change. + """ use GenServer diff --git a/lib/parrhesia/postgres_types.ex b/lib/parrhesia/postgres_types.ex index 1fdaecd..bd05795 100644 --- a/lib/parrhesia/postgres_types.ex +++ b/lib/parrhesia/postgres_types.ex @@ -1 +1,4 @@ -Postgrex.Types.define(Parrhesia.PostgresTypes, [], json: JSON) +Postgrex.Types.define(Parrhesia.PostgresTypes, [], + json: JSON, + moduledoc: "Custom Postgrex type definitions used by `Parrhesia.Repo` and `Parrhesia.ReadRepo`." +) diff --git a/lib/parrhesia/release.ex b/lib/parrhesia/release.ex index 6b48d20..c205b22 100644 --- a/lib/parrhesia/release.ex +++ b/lib/parrhesia/release.ex @@ -1,10 +1,18 @@ defmodule Parrhesia.Release do @moduledoc """ Helpers for running Ecto tasks from a production release. + + Intended for use from a release `eval` command where Mix is not available: + + bin/parrhesia eval "Parrhesia.Release.migrate()" + bin/parrhesia eval "Parrhesia.Release.rollback(Parrhesia.Repo, 20260101000000)" """ @app :parrhesia + @doc """ + Runs all pending Ecto migrations for every configured repo. + """ def migrate do load_app() @@ -16,6 +24,9 @@ defmodule Parrhesia.Release do end end + @doc """ + Rolls back the given `repo` to the specified migration `version`. + """ def rollback(repo, version) when is_atom(repo) and is_integer(version) do load_app() diff --git a/lib/parrhesia/repo.ex b/lib/parrhesia/repo.ex index 0ee82ef..6956909 100644 --- a/lib/parrhesia/repo.ex +++ b/lib/parrhesia/repo.ex @@ -1,6 +1,9 @@ defmodule Parrhesia.Repo do @moduledoc """ - PostgreSQL repository for storage adapter persistence. + PostgreSQL repository for write traffic and storage adapter persistence. + + Separated from `Parrhesia.ReadRepo` so that ingest writes and read-heavy + queries use independent connection pools. """ use Ecto.Repo, diff --git a/lib/parrhesia/runtime.ex b/lib/parrhesia/runtime.ex index f03edc1..960009e 100644 --- a/lib/parrhesia/runtime.ex +++ b/lib/parrhesia/runtime.ex @@ -1,8 +1,25 @@ defmodule Parrhesia.Runtime do - @moduledoc false + @moduledoc """ + Top-level Parrhesia supervisor. + + In normal standalone use, the `:parrhesia` application starts this supervisor automatically. + Host applications can also embed it directly under their own supervision tree: + + children = [ + {Parrhesia.Runtime, name: Parrhesia.Supervisor} + ] + + Parrhesia currently assumes a single runtime per BEAM node and uses globally registered + process names for core services. + """ use Supervisor + @doc """ + Starts the Parrhesia runtime supervisor. + + Accepts a `:name` option (defaults to `Parrhesia.Supervisor`). + """ def start_link(opts \\ []) do name = Keyword.get(opts, :name, Parrhesia.Supervisor) Supervisor.start_link(__MODULE__, opts, name: name) @@ -13,6 +30,9 @@ defmodule Parrhesia.Runtime do Supervisor.init(children(), strategy: :one_for_one) end + @doc """ + Returns the list of child specifications started by the runtime supervisor. + """ def children do [ Parrhesia.Telemetry, diff --git a/lib/parrhesia/telemetry.ex b/lib/parrhesia/telemetry.ex index 5a5958d..0d3c083 100644 --- a/lib/parrhesia/telemetry.ex +++ b/lib/parrhesia/telemetry.ex @@ -1,6 +1,10 @@ defmodule Parrhesia.Telemetry do @moduledoc """ Supervision entrypoint and helpers for relay telemetry. + + Starts the Prometheus reporter and telemetry poller as supervised children. + All relay metrics are namespaced under `parrhesia.*` and exposed through the + `/metrics` endpoint in Prometheus exposition format. """ use Supervisor diff --git a/lib/parrhesia/web/relay_info.ex b/lib/parrhesia/web/relay_info.ex index 0113da6..f60cff8 100644 --- a/lib/parrhesia/web/relay_info.ex +++ b/lib/parrhesia/web/relay_info.ex @@ -1,6 +1,10 @@ defmodule Parrhesia.Web.RelayInfo do @moduledoc """ NIP-11 relay information document. + + `document/1` builds the JSON-serialisable relay info map served on + `GET /relay` with `Accept: application/nostr+json`, including supported NIPs, + limitations, and the relay's advertised public key. """ alias Parrhesia.API.Identity @@ -8,7 +12,7 @@ defmodule Parrhesia.Web.RelayInfo do alias Parrhesia.NIP43 alias Parrhesia.Web.Listener - @spec document(Listener.t()) :: map() + @spec document(map()) :: map() def document(listener) do document = %{ "name" => Metadata.name(), diff --git a/mix.exs b/mix.exs index cf5137f..d053292 100644 --- a/mix.exs +++ b/mix.exs @@ -9,7 +9,8 @@ defmodule Parrhesia.MixProject do elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, deps: deps(), - aliases: aliases() + aliases: aliases(), + docs: docs() ] end @@ -53,6 +54,7 @@ defmodule Parrhesia.MixProject do # Project tooling {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, + {:ex_doc, "~> 0.34", only: :dev, runtime: false}, {:deps_changelog, "~> 0.3"}, {:igniter, "~> 0.6", only: [:dev, :test]} ] @@ -82,4 +84,39 @@ defmodule Parrhesia.MixProject do ] ] end + + defp docs do + [ + main: "readme", + output: "_build/doc", + extras: [ + "README.md", + "docs/LOCAL_API.md", + "docs/SYNC.md", + "docs/ARCH.md", + "docs/CLUSTER.md", + "BENCHMARK.md" + ], + groups_for_modules: [ + "Embedded API": [ + Parrhesia.API.ACL, + Parrhesia.API.Admin, + Parrhesia.API.Auth, + Parrhesia.API.Auth.Context, + Parrhesia.API.Events, + Parrhesia.API.Events.PublishResult, + Parrhesia.API.Identity, + Parrhesia.API.RequestContext, + Parrhesia.API.Stream, + Parrhesia.API.Sync + ], + Runtime: [ + Parrhesia, + Parrhesia.Release, + Parrhesia.Runtime + ] + ], + nest_modules_by_prefix: [Parrhesia.API] + ] + end end diff --git a/mix.lock b/mix.lock index f824eca..3cf315d 100644 --- a/mix.lock +++ b/mix.lock @@ -8,9 +8,11 @@ "db_connection": {:hex, :db_connection, "2.9.0", "a6a97c5c958a2d7091a58a9be40caf41ab496b0701d21e1d1abff3fa27a7f371", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "17d502eacaf61829db98facf6f20808ed33da6ccf495354a41e64fe42f9c509c"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deps_changelog": {:hex, :deps_changelog, "0.3.5", "65981997d9bc893b8027a0c03da093a4083328c00b17f562df269c2b61d44073", [:mix], [], "hexpm", "298fcd7794395d8e61dba8d29ce8fcee09f1df4d48adb273a41e8f4a1736491e"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"}, "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [: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", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [: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", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, @@ -18,9 +20,13 @@ "igniter": {:hex, :igniter, "0.7.4", "b5f9dd512eb1e672f1c141b523142b5b4602fcca231df5b4e362999df4b88e14", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "971b240ee916a06b1af56381a262d9eeaff9610eddc299d61a213cd7a9d79efd"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lib_secp256k1": {:hex, :lib_secp256k1, "0.7.1", "53cad778b8da3a29e453a7a477517d99fb5f13f615c8050eb2db8fd1dce7a1db", [:make, :mix], [{:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "78bdd3661a17448aff5aeec5ca74c8ddbc09b01f0ecfa3ba1aba3e8ae47ab2b3"}, + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, "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"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"}, "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"},