From cc78558612199aa6bee147959a763ef2f492ff3a Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Fri, 13 Mar 2026 18:50:16 +0100 Subject: [PATCH] build/docs: architecture, deps --- AGENTS.md | 2 + PROGRESS.md | 86 +++++++++++++++++ docs/ARCH.md | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++ mix.exs | 23 ++++- mix.lock | 22 +++++ 5 files changed, 383 insertions(+), 3 deletions(-) create mode 100644 PROGRESS.md create mode 100644 docs/ARCH.md diff --git a/AGENTS.md b/AGENTS.md index 350562f..a4d9b2e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,5 +1,7 @@ This is a Nostr server written using Elixir and PostgreSQL. +NOTE: Nostr and NIP specs are available in `~/nostr/` and `~/nips/`. + ## Project guidelines - Use `mix precommit` alias when you are done with all changes and fix any pending issues diff --git a/PROGRESS.md b/PROGRESS.md new file mode 100644 index 0000000..4c985c0 --- /dev/null +++ b/PROGRESS.md @@ -0,0 +1,86 @@ +# PROGRESS (ephemeral) + +Implementation checklist for Parrhesia relay. + +## Phase 0 — foundation + +- [ ] Confirm architecture doc with final NIP scope (`docs/ARCH.md`) +- [ ] Add core deps (websocket/http server, ecto_sql/postgrex, telemetry, test tooling) +- [ ] Establish application config structure (limits, policies, feature flags) +- [ ] Wire initial supervision tree skeleton + +## Phase 1 — protocol core (NIP-01) + +- [ ] Implement websocket endpoint + per-connection process +- [ ] Implement message decode/encode for `EVENT`, `REQ`, `CLOSE` +- [ ] Implement strict event validation (`id`, `sig`, shape, timestamps) +- [ ] Implement filter evaluation engine (AND/OR semantics) +- [ ] Implement subscription lifecycle + `EOSE` behavior +- [ ] Implement canonical `OK`, `NOTICE`, `CLOSED` responses + prefixes + +## Phase 2 — storage boundary + postgres adapter + +- [ ] Define `Parrhesia.Storage.*` behaviors (events/moderation/groups/admin) +- [ ] Implement Postgres adapter modules behind behaviors +- [ ] Create migrations for events, tags, moderation, membership +- [ ] Implement replaceable/addressable semantics at storage layer +- [ ] Add adapter contract test suite + +## Phase 3 — fanout + performance primitives + +- [ ] Build ETS-backed subscription index +- [ ] Implement candidate narrowing by kind/author/tag +- [ ] Add bounded outbound queues/backpressure per connection +- [ ] Add telemetry for ingest/query/fanout latency + queue depth + +## Phase 4 — relay metadata and auth + +- [ ] NIP-11 endpoint (`application/nostr+json`) +- [ ] NIP-42 challenge/auth flow +- [ ] Enforce NIP-70 protected events (default reject, auth override) +- [ ] Add auth-required/restricted response paths for writes and reqs + +## Phase 5 — lifecycle and moderation features + +- [ ] NIP-09 deletion requests +- [ ] NIP-40 expiration handling + purge worker +- [ ] NIP-62 vanish requests (hard delete semantics) +- [ ] NIP-13 PoW gate (configurable minimum) +- [ ] Moderation tables + policy hooks (ban/allow/event/ip) + +## Phase 6 — query extensions + +- [ ] NIP-45 `COUNT` (exact) +- [ ] Optional HLL response support +- [ ] NIP-50 search (`search` filter + ranking) +- [ ] NIP-77 negentropy (`NEG-OPEN/MSG/CLOSE`) + +## Phase 7 — private messaging, groups, and MLS + +- [ ] NIP-17/59 recipient-protected giftwrap read path (`kind:1059`) +- [ ] NIP-29 group event policy + relay metadata events +- [ ] NIP-43 membership request flow (`28934/28935/28936`, `8000/8001`, `13534`) +- [ ] NIP-EE (feature-flagged): `443`, `445`, `10051` handling +- [ ] MLS retention policy + tests for commit race edge cases + +## Phase 8 — management API + operations + +- [ ] NIP-86 HTTP management endpoint +- [ ] NIP-98 auth validation for management calls +- [ ] Implement supported management methods + audit logging +- [ ] Build health/readiness and Prometheus-compatible `/metrics` endpoints + +## Phase 9 — full test + hardening pass + +- [ ] Unit + integration + property test coverage for all critical modules +- [ ] End-to-end websocket conformance scenarios +- [ ] Load/soak tests with target p95 latency budgets +- [ ] Fault-injection tests (DB outages, high churn, restart recovery) +- [ ] Final precommit run and fix all issues + +## Nice-to-have / backlog + +- [ ] Multi-node fanout via PG LISTEN/NOTIFY or external bus +- [ ] Partitioned event storage + archival strategy +- [ ] Alternate storage adapter prototype (non-Postgres) +- [ ] Compatibility mode for Marmot protocol transition diff --git a/docs/ARCH.md b/docs/ARCH.md new file mode 100644 index 0000000..bd9feab --- /dev/null +++ b/docs/ARCH.md @@ -0,0 +1,253 @@ +# Parrhesia Nostr Relay Architecture + +## 1) Goals + +Build a **robust, high-performance Nostr relay** in Elixir/OTP with PostgreSQL as first adapter, while keeping a strict boundary so storage can be swapped later. + +Primary targets: + +- Broad relay feature support (core + modern relay-facing NIPs) +- Strong correctness around NIP-01 semantics +- Clear OTP supervision and failure isolation +- High fanout throughput and bounded resource usage +- Storage abstraction via behavior-driven ports/adapters +- Full test suite (unit, integration, conformance, perf, fault-injection) +- Support for experimental MLS flow (NIP-EE), behind feature flags + +## 2) NIP support scope + +### Mandatory baseline + +- NIP-01 (includes behavior moved from NIP-12/NIP-16/NIP-20/NIP-33) +- NIP-11 (relay info document) + +### Relay-facing features to include + +- NIP-09 (deletion requests) +- NIP-13 (PoW gating) +- NIP-17 + NIP-44 + NIP-59 (private DMs / gift wraps) +- NIP-40 (expiration) +- NIP-42 (AUTH) +- NIP-43 (relay membership requests/metadata) +- NIP-45 (COUNT, optional HLL) +- NIP-50 (search) +- NIP-62 (request to vanish) +- NIP-66 (relay discovery events; store/serve as normal events) +- NIP-70 (protected events) +- NIP-77 (negentropy sync) +- NIP-86 + NIP-98 (relay management API auth) + +### Experimental MLS + +- NIP-EE (unrecommended/upstream-superseded, but requested): + - kind `443` KeyPackage events + - kind `445` group events (policy-controlled retention/ephemeral treatment) + - kind `10051` keypackage relay lists + - interop with wrapped delivery (`1059`) and auth/privacy policies + +## 3) System architecture (high level) + +```text +WS/HTTP Edge (Bandit/Plug) + -> Protocol Decoder/Encoder + -> Command Router (EVENT/REQ/CLOSE/AUTH/COUNT/NEG-*) + -> Policy Pipeline (validation, auth, ACL, PoW, NIP-70) + -> Event Service / Query Service + -> Storage Port (behavior) + -> Postgres Adapter (Ecto) + -> Subscription Index (ETS) + -> Fanout Dispatcher + -> Telemetry + Metrics + Tracing +``` + +## 4) OTP supervision design + +`Parrhesia.Application` 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` – WS + HTTP ingress +8. `Parrhesia.Tasks.Supervisor` – background jobs (expiry purge, maintenance) + +Failure model: + +- Connection failures are isolated per socket process. +- Storage outages degrade with explicit `OK/CLOSED` error prefixes (`error:`) per NIP-01. +- Non-critical workers are `:transient`; core infra is `:permanent`. + +## 5) Core runtime components + +### 5.1 Connection process + +Per websocket connection: + +- Parse frames, enforce max frame/message limits +- Maintain authenticated pubkeys (NIP-42) +- Track active subscriptions (`sub_id` scoped to connection) +- Handle backpressure (bounded outbound queue + drop/close strategy) + +### 5.2 Command router + +Dispatches: + +- `EVENT` -> ingest pipeline +- `REQ` -> initial DB query + live subscription +- `CLOSE` -> unsubscribe +- `AUTH` -> challenge validation, session update +- `COUNT` -> aggregate path +- `NEG-OPEN`/`NEG-MSG`/`NEG-CLOSE` -> negentropy session engine + +### 5.3 Event ingest pipeline + +Ordered stages: + +1. Decode + schema checks +2. `id` recomputation and signature verification +3. NIP semantic checks (timestamps, tag forms, size limits) +4. Policy checks (banlists, kind allowlists, auth-required, NIP-70, PoW) +5. Storage write (or no-store for ephemeral policy) +6. Live fanout to matching subscriptions +7. Return canonical `OK` response with machine prefix when needed + +### 5.4 Subscription index + fanout + +- ETS-backed inverted indices (`kind`, `author`, single-letter tags) +- Candidate narrowing before full filter evaluation +- OR semantics across filters, AND within filter +- `limit` only for initial query phase; ignored in live phase (NIP-01) + +### 5.5 Query service + +- Compiles NIP filters into adapter-neutral query AST +- Pushes AST to storage adapter +- Deterministic ordering (`created_at` desc, `id` lexical tie-break) +- Emits `EOSE` exactly once per subscription initial catch-up + +## 6) Storage boundary (swap-friendly by design) + +### 6.1 Port/adapter contract + +Define behaviors under `Parrhesia.Storage`: + +- `Parrhesia.Storage.Events` + - `put_event/2`, `get_event/2`, `query/3`, `count/3` + - `delete_by_request/2`, `vanish/2`, `purge_expired/1` +- `Parrhesia.Storage.Moderation` + - pubkey/event bans, allowlists, blocked IPs +- `Parrhesia.Storage.Groups` + - NIP-29/NIP-43 membership + role operations +- `Parrhesia.Storage.Admin` + - backing for NIP-86 methods + +All domain logic depends only on these behaviors. + +### 6.2 Postgres adapter notes + +Initial adapter: `Parrhesia.Storage.Adapters.Postgres` with Ecto. + +Schema outline: + +- `events` (id PK, pubkey, created_at, kind, content, sig, d_tag, deleted_at, expires_at) +- `event_tags` (event_id, name, value, idx) +- moderation tables (banned/allowed pubkeys, banned events, blocked IPs) +- relay/group membership tables +- optional count/HLL helper tables + +Indexing strategy: + +- `(kind, created_at DESC)` +- `(pubkey, created_at DESC)` +- `(created_at DESC)` +- `(name, value, created_at DESC)` on `event_tags` +- partial/unique indexes for replaceable and addressable semantics + +Retention strategy: + +- Optional table partitioning by time for hot pruning +- Periodic purge job for expired/deleted tombstoned rows + +## 7) Feature-specific implementation notes + +### 7.1 NIP-11 + +- Serve on WS URL with `Accept: application/nostr+json` +- Include accurate `supported_nips` and `limitation` + +### 7.2 NIP-42 + NIP-70 + +- Connection-scoped challenge store +- Protected (`["-"]`) events rejected by default unless auth+pubkey match + +### 7.3 NIP-17/59 privacy guardrails + +- Relay can enforce recipient-only reads for kind `1059` (AUTH required) +- Query path validates requester access for wrapped DM fetches + +### 7.4 NIP-45 COUNT + +- Exact count baseline +- Optional approximate mode and HLL payloads for common queries + +### 7.5 NIP-50 search + +- Use Postgres FTS (`tsvector`) with ranking +- Apply `limit` after ranking + +### 7.6 NIP-77 negentropy + +- Track per-negentropy-session state in dedicated GenServer +- Use bounded resources + inactivity timeout + +### 7.7 NIP-62 vanish + +- Hard-delete all events by pubkey up to `created_at` +- Also delete matching gift wraps where feasible (`#p` target) +- Persist minimal audit record if needed for operations/legal trace + +### 7.8 NIP-EE MLS (feature-flagged) + +- Accept/store kind `443` KeyPackage events +- Process kind `445` under configurable retention policy (default short TTL) +- Ensure kind `10051` replaceable semantics +- Keep relay MLS-agnostic cryptographically (no MLS decryption in relay path) + +## 8) Performance model + +- Bounded mailbox and queue limits on connections +- ETS-heavy hot path (subscription match, auth/session cache) +- DB writes batched where safe; reads via prepared plans +- Avoid global locks; prefer partitioned workers and sharded ETS tables +- Telemetry-first tuning: p50/p95/p99 for ingest, query, fanout +- Expose Prometheus-compatible `/metrics` endpoint for scraping + +Targets (initial): + +- p95 EVENT ack < 50ms under nominal load +- p95 REQ initial response start < 120ms on indexed queries +- predictable degradation under overload via rate-limit + backpressure + +## 9) Testing strategy (full suite) + +1. **Unit tests**: parser, filter evaluator, policy predicates, NIP validators +2. **Property tests**: filter semantics, replaceable/addressable conflict resolution +3. **Adapter contract tests**: shared behavior tests run against Postgres adapter +4. **Integration tests**: websocket protocol flows (`EVENT/REQ/CLOSE/AUTH/COUNT/NEG-*`) +5. **NIP conformance tests**: machine-prefix responses, ordering, EOSE behavior +6. **MLS scenario tests**: keypackage/group-event acceptance and policy handling +7. **Performance tests**: soak + burst + large fanout profiles +8. **Fault-injection tests**: DB outage, slow query, connection churn, node restart + +## 10) Implementation principles + +- Keep relay event-kind agnostic by default; special-case only where NIPs require +- Prefer explicit feature flags for expensive/experimental modules +- No direct Ecto usage outside Postgres adapter and migration layer +- Every feature lands with tests + telemetry hooks + +--- + +Implementation task breakdown is tracked in `./PROGRESS.md`. diff --git a/mix.exs b/mix.exs index 366a58f..221c429 100644 --- a/mix.exs +++ b/mix.exs @@ -21,11 +21,28 @@ defmodule Parrhesia.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ecto, "~> 3.0"}, + # Runtime: web + protocol edge + {:bandit, "~> 1.5"}, + {:plug, "~> 1.15"}, + + # Runtime: storage adapter (Postgres first) + {:ecto_sql, "~> 3.12"}, + {:postgrex, ">= 0.0.0"}, + + # Runtime: telemetry + prometheus exporter (/metrics) + {:telemetry_metrics, "~> 1.0"}, + {:telemetry_poller, "~> 1.0"}, + {:telemetry_metrics_prometheus, "~> 1.1"}, + + # Test tooling + {:stream_data, "~> 1.0", only: :test}, + {:mox, "~> 1.1", only: :test}, + {:bypass, "~> 2.1", only: :test}, + {:websockex, "~> 0.4", only: :test}, + + # Project tooling {:deps_changelog, "~> 0.3"}, {:igniter, "~> 0.6", only: [:dev, :test]} - # {:dep_from_hexpm, "~> 0.3.0"}, - # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} ] end end diff --git a/mix.lock b/mix.lock index ebd95bc..439e230 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,14 @@ %{ + "bandit": {:hex, :bandit, "1.10.3", "1e5d168fa79ec8de2860d1b4d878d97d4fbbe2fdbe7b0a7d9315a4359d1d4bb9", [: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", "99a52d909c48db65ca598e1962797659e3c0f1d06e825a50c3d75b74a5e2db18"}, + "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, + "cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"}, + "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"}, "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"}, "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"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, @@ -9,13 +16,28 @@ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "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"}, + "mox": {:hex, :mox, "1.2.0", "a2cd96b4b80a3883e3100a221e8adc1b98e4c3a332a8fc434c39526babafd5b3", [:mix], [{:nimble_ownership, "~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}], "hexpm", "c7b92b3cc69ee24a7eeeaf944cd7be22013c52fcb580c1f33f50845ec821089a"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, + "nimble_ownership": {:hex, :nimble_ownership, "1.0.2", "fa8a6f2d8c592ad4d79b2ca617473c6aefd5869abfa02563a77682038bf916cf", [:mix], [], "hexpm", "098af64e1f6f8609c6672127cfe9e9590a5d3fcdd82bc17a377b8692fd81a879"}, "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"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.8.0", "07789e9c03539ee51bb14a07839cc95aa96999fd8846ebfd28c97f0b50c7b612", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9cbfaaf17463334ca31aed38ea7e08a68ee37cabc077b1e9be6d2fb68e0171d0"}, + "plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"}, + "ranch": {:hex, :ranch, "1.8.1", "208169e65292ac5d333d6cdbad49388c1ae198136e4697ae2f474697140f201c", [:make, :rebar3], [], "hexpm", "aed58910f4e21deea992a67bf51632b6d60114895eb03bb392bb733064594dd0"}, "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [: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", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, "spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"}, + "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"}, + "telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"}, + "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, + "thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"}, + "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, + "websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"}, }