Document embedded API surface
Some checks failed
CI / Test (OTP 27.2 / Elixir 1.18.2) (push) Failing after 0s
CI / Test (OTP 28.4 / Elixir 1.19.4 + Marmot E2E) (push) Failing after 0s

This commit is contained in:
2026-03-18 20:01:12 +01:00
parent 7a43ebd395
commit 970cee2c0e
14 changed files with 311 additions and 165 deletions

View File

@@ -45,7 +45,7 @@ Current `supported_nips` list:
## Requirements ## Requirements
- Elixir `~> 1.19` - Elixir `~> 1.18`
- Erlang/OTP 28 - Erlang/OTP 28
- PostgreSQL (18 used in the dev environment; 16+ recommended) - 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 - 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 ## Production configuration
### Minimal setup ### Minimal setup

View File

@@ -82,16 +82,20 @@ Configured WS/HTTP Listeners (Bandit/Plug)
## 4) OTP supervision design ## 4) OTP supervision design
`Parrhesia.Application` children (top-level): `Parrhesia.Runtime` children (top-level):
1. `Parrhesia.Telemetry` metric definitions/reporters 1. `Parrhesia.Telemetry` metric definitions/reporters
2. `Parrhesia.Config` runtime config cache (ETS-backed) 2. `Parrhesia.ConnectionStats` per-listener connection/subscription counters
3. `Parrhesia.Storage.Supervisor` adapter processes (`Repo`, pools) 3. `Parrhesia.Config` runtime config cache (ETS-backed)
4. `Parrhesia.Subscriptions.Supervisor` subscription index + fanout workers 4. `Parrhesia.Web.EventIngestLimiter` relay-wide event ingest rate limiter
5. `Parrhesia.Auth.Supervisor` AUTH challenge/session tracking 5. `Parrhesia.Web.IPEventIngestLimiter` per-IP event ingest rate limiter
6. `Parrhesia.Policy.Supervisor` rate limiters / ACL caches 6. `Parrhesia.Storage.Supervisor` adapter processes (`Repo`, pools)
7. `Parrhesia.Web.Endpoint` supervises configured WS + HTTP listeners 7. `Parrhesia.Subscriptions.Supervisor` subscription index + fanout workers
8. `Parrhesia.Tasks.Supervisor` background jobs (expiry purge, maintenance) 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: Failure model:

View File

@@ -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.

147
docs/LOCAL_API.md Normal file
View File

@@ -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

View File

@@ -1,17 +1,27 @@
defmodule Parrhesia do defmodule Parrhesia do
@moduledoc """ @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 """ @doc false
Hello world.
## Examples
iex> Parrhesia.hello()
:world
"""
def hello do def hello do
:world :world
end end

View File

@@ -1,5 +1,10 @@
defmodule Parrhesia.ConnectionStats do 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 use GenServer

View File

@@ -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`."
)

View File

@@ -1,10 +1,18 @@
defmodule Parrhesia.Release do defmodule Parrhesia.Release do
@moduledoc """ @moduledoc """
Helpers for running Ecto tasks from a production release. 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 @app :parrhesia
@doc """
Runs all pending Ecto migrations for every configured repo.
"""
def migrate do def migrate do
load_app() load_app()
@@ -16,6 +24,9 @@ defmodule Parrhesia.Release do
end end
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 def rollback(repo, version) when is_atom(repo) and is_integer(version) do
load_app() load_app()

View File

@@ -1,6 +1,9 @@
defmodule Parrhesia.Repo do defmodule Parrhesia.Repo do
@moduledoc """ @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, use Ecto.Repo,

View File

@@ -1,8 +1,25 @@
defmodule Parrhesia.Runtime do 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 use Supervisor
@doc """
Starts the Parrhesia runtime supervisor.
Accepts a `:name` option (defaults to `Parrhesia.Supervisor`).
"""
def start_link(opts \\ []) do def start_link(opts \\ []) do
name = Keyword.get(opts, :name, Parrhesia.Supervisor) name = Keyword.get(opts, :name, Parrhesia.Supervisor)
Supervisor.start_link(__MODULE__, opts, name: name) Supervisor.start_link(__MODULE__, opts, name: name)
@@ -13,6 +30,9 @@ defmodule Parrhesia.Runtime do
Supervisor.init(children(), strategy: :one_for_one) Supervisor.init(children(), strategy: :one_for_one)
end end
@doc """
Returns the list of child specifications started by the runtime supervisor.
"""
def children do def children do
[ [
Parrhesia.Telemetry, Parrhesia.Telemetry,

View File

@@ -1,6 +1,10 @@
defmodule Parrhesia.Telemetry do defmodule Parrhesia.Telemetry do
@moduledoc """ @moduledoc """
Supervision entrypoint and helpers for relay telemetry. 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 use Supervisor

View File

@@ -1,6 +1,10 @@
defmodule Parrhesia.Web.RelayInfo do defmodule Parrhesia.Web.RelayInfo do
@moduledoc """ @moduledoc """
NIP-11 relay information document. 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 alias Parrhesia.API.Identity
@@ -8,7 +12,7 @@ defmodule Parrhesia.Web.RelayInfo do
alias Parrhesia.NIP43 alias Parrhesia.NIP43
alias Parrhesia.Web.Listener alias Parrhesia.Web.Listener
@spec document(Listener.t()) :: map() @spec document(map()) :: map()
def document(listener) do def document(listener) do
document = %{ document = %{
"name" => Metadata.name(), "name" => Metadata.name(),

39
mix.exs
View File

@@ -9,7 +9,8 @@ defmodule Parrhesia.MixProject do
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,
deps: deps(), deps: deps(),
aliases: aliases() aliases: aliases(),
docs: docs()
] ]
end end
@@ -53,6 +54,7 @@ defmodule Parrhesia.MixProject do
# Project tooling # Project tooling
{:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.34", only: :dev, runtime: false},
{:deps_changelog, "~> 0.3"}, {:deps_changelog, "~> 0.3"},
{:igniter, "~> 0.6", only: [:dev, :test]} {:igniter, "~> 0.6", only: [:dev, :test]}
] ]
@@ -82,4 +84,39 @@ defmodule Parrhesia.MixProject do
] ]
] ]
end 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 end

View File

@@ -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"}, "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"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
"deps_changelog": {:hex, :deps_changelog, "0.3.5", "65981997d9bc893b8027a0c03da093a4083328c00b17f562df269c2b61d44073", [:mix], [], "hexpm", "298fcd7794395d8e61dba8d29ce8fcee09f1df4d48adb273a41e8f4a1736491e"}, "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": {: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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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"}, "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_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"}, "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"}, "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"}, "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"},