From cc9c18b38cf7ad344a1d612752c6438fd6721fd2 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Fri, 13 Mar 2026 22:54:18 +0100 Subject: [PATCH] docs+nix: add deployment README and align release packaging --- README.md | 170 ++++++++++++++++-- config/prod.exs | 8 +- config/runtime.exs | 14 ++ default.nix | 6 +- devenv.nix | 2 + lib/parrhesia/auth/nip98.ex | 2 +- lib/parrhesia/protocol.ex | 4 +- lib/parrhesia/protocol/event_validator.ex | 2 +- lib/parrhesia/web/connection.ex | 2 +- lib/parrhesia/web/management.ex | 2 +- lib/parrhesia/web/router.ex | 4 +- test/parrhesia/auth/nip98_test.exs | 4 +- .../fault_injection_group_flow_test.exs | 18 +- test/parrhesia/fault_injection_test.exs | 8 +- test/parrhesia/performance/load_soak_test.exs | 2 +- test/parrhesia/protocol_test.exs | 32 ++-- test/parrhesia/web/conformance_test.exs | 42 ++--- test/parrhesia/web/connection_test.exs | 76 ++++---- test/parrhesia/web/router_test.exs | 12 +- 19 files changed, 282 insertions(+), 128 deletions(-) create mode 100644 config/runtime.exs diff --git a/README.md b/README.md index 7f4eac5..06535c4 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,165 @@ # Parrhesia -**TODO: Add description** +Parrhesia is a Nostr relay server written in Elixir/OTP with PostgreSQL storage. -## Installation +It exposes: +- a WebSocket relay endpoint at `/relay` +- NIP-11 relay info on `GET /relay` with `Accept: application/nostr+json` +- operational HTTP endpoints (`/health`, `/ready`, `/metrics`) +- a NIP-86-style management API at `POST /management` (NIP-98 auth) -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `parrhesia` to your list of dependencies in `mix.exs`: +## Supported NIPs -```elixir -def deps do - [ - {:parrhesia, "~> 0.1.0"} - ] -end +Current `supported_nips` list: + +`1, 9, 11, 13, 17, 40, 42, 43, 44, 45, 50, 59, 62, 66, 70, 77, 86, 98` + +## Requirements + +- Elixir `~> 1.19` +- Erlang/OTP 28 +- PostgreSQL (18 used in the dev environment; 16+ recommended) + +--- + +## Run locally + +### 1) Prepare the database + +Parrhesia uses these defaults in `dev`: +- `PGDATABASE=parrhesia_dev` +- `PGHOST=localhost` +- `PGPORT=5432` +- `PGUSER=$USER` + +Create the DB and run migrations/seeds: + +```bash +mix setup ``` -Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) -and published on [HexDocs](https://hexdocs.pm). Once published, the docs can -be found at . +### 2) Start the server +```bash +mix run --no-halt +``` + +Server listens on `http://localhost:4000` by default. + +WebSocket clients should connect to: + +```text +ws://localhost:4000/relay +``` + +### Useful endpoints + +- `GET /health` -> `ok` +- `GET /ready` -> readiness status +- `GET /metrics` -> Prometheus metrics +- `GET /relay` + `Accept: application/nostr+json` -> NIP-11 document +- `POST /management` -> management API (requires NIP-98 auth) + +--- + +## Production configuration + +In `prod`, these environment variables are used: + +- `DATABASE_URL` (**required**), e.g. `ecto://USER:PASS@HOST/parrhesia_prod` +- `POOL_SIZE` (optional, default `10`) +- `PORT` (optional, default `4000`) + +`config/runtime.exs` reads these values at runtime in production releases. + +### Typical relay config + +Add/override in config files (for example in `config/prod.exs` or a `config/runtime.exs`): + +```elixir +config :parrhesia, Parrhesia.Web.Endpoint, + ip: {0, 0, 0, 0}, + port: 4000 + +config :parrhesia, + limits: [ + max_frame_bytes: 1_048_576, + max_event_bytes: 262_144, + max_filters_per_req: 16, + max_filter_limit: 500, + max_subscriptions_per_connection: 32, + max_event_future_skew_seconds: 900, + max_outbound_queue: 256, + outbound_drain_batch_size: 64, + outbound_overflow_strategy: :close + ], + policies: [ + auth_required_for_writes: false, + auth_required_for_reads: false, + min_pow_difficulty: 0, + accept_ephemeral_events: true, + mls_group_event_ttl_seconds: 300, + marmot_require_h_for_group_queries: true, + marmot_group_max_h_values_per_filter: 32, + marmot_group_max_query_window_seconds: 2_592_000, + marmot_media_max_imeta_tags_per_event: 8, + marmot_media_max_field_value_bytes: 1024, + marmot_media_max_url_bytes: 2048, + marmot_media_allowed_mime_prefixes: [], + marmot_media_reject_mip04_v1: true, + marmot_push_server_pubkeys: [], + marmot_push_max_relay_tags: 16, + marmot_push_max_payload_bytes: 65_536, + marmot_push_max_trigger_age_seconds: 120, + marmot_push_require_expiration: true, + marmot_push_max_expiration_window_seconds: 120, + marmot_push_max_server_recipients: 1 + ], + features: [ + nip_45_count: true, + nip_50_search: true, + nip_77_negentropy: true, + marmot_push_notifications: false + ] +``` + +--- + +## Deploy + +### Option A: Elixir release + +```bash +export MIX_ENV=prod +export DATABASE_URL="ecto://USER:PASS@HOST/parrhesia_prod" +export POOL_SIZE=20 + +mix deps.get --only prod +mix compile +mix ecto.migrate +mix release + +_build/prod/rel/parrhesia/bin/parrhesia foreground +``` + +For systemd/process managers, run the release command in foreground mode. + +### Option B: Nix package (`default.nix`) + +Build: + +```bash +nix-build +``` + +Run the built release from `./result/bin/parrhesia` (release command interface). + +--- + +## Development quality checks + +Before opening a PR: + +```bash +mix precommit +``` diff --git a/config/prod.exs b/config/prod.exs index d4763a8..f8e5e5a 100644 --- a/config/prod.exs +++ b/config/prod.exs @@ -1,9 +1,3 @@ import Config -database_url = - System.get_env("DATABASE_URL") || - raise "environment variable DATABASE_URL is missing. Example: ecto://USER:PASS@HOST/DATABASE" - -config :parrhesia, Parrhesia.Repo, - url: database_url, - pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") +# Production runtime configuration lives in config/runtime.exs. diff --git a/config/runtime.exs b/config/runtime.exs new file mode 100644 index 0000000..6c197c7 --- /dev/null +++ b/config/runtime.exs @@ -0,0 +1,14 @@ +import Config + +if config_env() == :prod do + database_url = + System.get_env("DATABASE_URL") || + raise "environment variable DATABASE_URL is missing. Example: ecto://USER:PASS@HOST/DATABASE" + + config :parrhesia, Parrhesia.Repo, + url: database_url, + pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10") + + config :parrhesia, Parrhesia.Web.Endpoint, + port: String.to_integer(System.get_env("PORT") || "4000") +end diff --git a/default.nix b/default.nix index bf15e68..ef99230 100644 --- a/default.nix +++ b/default.nix @@ -30,7 +30,7 @@ "_build" "deps" "node_modules" - "complement" + "marmot-ts" ]; in lib.cleanSourceFilter path type @@ -43,7 +43,7 @@ beamPackages.fetchMixDeps { pname = "${pname}-mix-deps"; inherit version src; - hash = "sha256-eAk6FVjVPGcJl3adBSoQvPIva7w4xz4GuZ+DZaz6SYY="; + hash = "sha256-1v2+Q1MHbu09r5OBaLehiR+JfMP0Q5OHaWuwrQDzZJU="; } else null; in @@ -65,7 +65,7 @@ in ''; meta = with lib; { - description = "Parrhesia Matrix homeserver"; + description = "Parrhesia Nostr relay server"; license = licenses.asl20; platforms = platforms.unix; }; diff --git a/devenv.nix b/devenv.nix index 2c787a2..7504951 100644 --- a/devenv.nix +++ b/devenv.nix @@ -87,6 +87,8 @@ in { vips-mozjpeg # Mermaid diagram generator mermaid-cli + # Nostr CLI client + nak ]; # https://devenv.sh/tests/ diff --git a/lib/parrhesia/auth/nip98.ex b/lib/parrhesia/auth/nip98.ex index 5e425df..ba4d487 100644 --- a/lib/parrhesia/auth/nip98.ex +++ b/lib/parrhesia/auth/nip98.ex @@ -14,7 +14,7 @@ defmodule Parrhesia.Auth.Nip98 do def validate_authorization_header("Nostr " <> encoded_event, method, url) when is_binary(method) and is_binary(url) do with {:ok, event_json} <- decode_base64(encoded_event), - {:ok, event} <- Jason.decode(event_json), + {:ok, event} <- JSON.decode(event_json), :ok <- validate_event_shape(event), :ok <- validate_http_binding(event, method, url) do {:ok, event} diff --git a/lib/parrhesia/protocol.ex b/lib/parrhesia/protocol.ex index 6c057e2..aeb06c3 100644 --- a/lib/parrhesia/protocol.ex +++ b/lib/parrhesia/protocol.ex @@ -59,7 +59,7 @@ defmodule Parrhesia.Protocol do def encode_relay(message) do message |> relay_frame() - |> Jason.encode!() + |> JSON.encode!() end @spec decode_error_notice(decode_error()) :: String.t() @@ -77,7 +77,7 @@ defmodule Parrhesia.Protocol do end defp decode_json(payload) do - case Jason.decode(payload) do + case JSON.decode(payload) do {:ok, decoded} -> {:ok, decoded} {:error, _reason} -> {:error, :invalid_json} end diff --git a/lib/parrhesia/protocol/event_validator.ex b/lib/parrhesia/protocol/event_validator.ex index b871d51..5aa472c 100644 --- a/lib/parrhesia/protocol/event_validator.ex +++ b/lib/parrhesia/protocol/event_validator.ex @@ -71,7 +71,7 @@ defmodule Parrhesia.Protocol.EventValidator do event["tags"], event["content"] ] - |> Jason.encode!() + |> JSON.encode!() |> then(&:crypto.hash(:sha256, &1)) |> Base.encode16(case: :lower) end diff --git a/lib/parrhesia/web/connection.ex b/lib/parrhesia/web/connection.ex index a40ec87..f3ceefb 100644 --- a/lib/parrhesia/web/connection.ex +++ b/lib/parrhesia/web/connection.ex @@ -493,7 +493,7 @@ defmodule Parrhesia.Web.Connection do defp generate_hll_payload(filters, count) do filters - |> Jason.encode!() + |> JSON.encode!() |> then(&"#{&1}:#{count}") |> then(&:crypto.hash(:sha256, &1)) |> Base.encode64() diff --git a/lib/parrhesia/web/management.ex b/lib/parrhesia/web/management.ex index eda4d28..667c10f 100644 --- a/lib/parrhesia/web/management.ex +++ b/lib/parrhesia/web/management.ex @@ -76,7 +76,7 @@ defmodule Parrhesia.Web.Management do defp normalize_result(result), do: %{"value" => inspect(result)} defp send_json(conn, status, body) do - encoded = Jason.encode!(body) + encoded = JSON.encode!(body) conn |> put_resp_content_type("application/json") diff --git a/lib/parrhesia/web/router.ex b/lib/parrhesia/web/router.ex index 8641bf0..ed7db7e 100644 --- a/lib/parrhesia/web/router.ex +++ b/lib/parrhesia/web/router.ex @@ -11,7 +11,7 @@ defmodule Parrhesia.Web.Router do plug(Plug.Parsers, parsers: [:json], pass: ["application/json"], - json_decoder: Jason + json_decoder: JSON ) plug(:match) @@ -43,7 +43,7 @@ defmodule Parrhesia.Web.Router do get "/relay" do if accepts_nip11?(conn) do - body = Jason.encode!(RelayInfo.document()) + body = JSON.encode!(RelayInfo.document()) conn |> put_resp_content_type("application/nostr+json") diff --git a/test/parrhesia/auth/nip98_test.exs b/test/parrhesia/auth/nip98_test.exs index ed29f9d..9d0accf 100644 --- a/test/parrhesia/auth/nip98_test.exs +++ b/test/parrhesia/auth/nip98_test.exs @@ -7,7 +7,7 @@ defmodule Parrhesia.Auth.Nip98Test do test "validates authorization header with matching method and url tags" do url = "http://example.com/management" event = nip98_event("POST", url) - header = "Nostr " <> Base.encode64(Jason.encode!(event)) + header = "Nostr " <> Base.encode64(JSON.encode!(event)) assert {:ok, parsed_event} = Nip98.validate_authorization_header(header, "POST", url) assert parsed_event["id"] == event["id"] @@ -16,7 +16,7 @@ defmodule Parrhesia.Auth.Nip98Test do test "rejects mismatched method and url" do url = "http://example.com/management" event = nip98_event("POST", url) - header = "Nostr " <> Base.encode64(Jason.encode!(event)) + header = "Nostr " <> Base.encode64(JSON.encode!(event)) assert {:error, :invalid_method_tag} = Nip98.validate_authorization_header(header, "GET", url) diff --git a/test/parrhesia/fault_injection_group_flow_test.exs b/test/parrhesia/fault_injection_group_flow_test.exs index 913e5fc..07c3b2f 100644 --- a/test/parrhesia/fault_injection_group_flow_test.exs +++ b/test/parrhesia/fault_injection_group_flow_test.exs @@ -41,12 +41,12 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do "content" => Base.encode64("commit") }) - payload = Jason.encode!(["EVENT", group_event]) + payload = JSON.encode!(["EVENT", group_event]) assert {:push, {:text, error_response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(error_response) == ["OK", group_event["id"], false, "error: :db_down"] + assert JSON.decode!(error_response) == ["OK", group_event["id"], false, "error: :db_down"] Application.put_env( :parrhesia, @@ -57,7 +57,7 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do assert {:push, {:text, ok_response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(ok_response) == ["OK", group_event["id"], true, "ok: event stored"] + assert JSON.decode!(ok_response) == ["OK", group_event["id"], true, "ok: event stored"] assert {:ok, persisted_group_event} = Storage.events().get_event(%{}, group_event["id"]) assert persisted_group_event["id"] == group_event["id"] @@ -89,11 +89,11 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do assert {:push, {:text, outage_response}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", older_event]), [opcode: :text]}, + {JSON.encode!(["EVENT", older_event]), [opcode: :text]}, state ) - assert Jason.decode!(outage_response) == ["OK", older_event["id"], false, "error: :db_down"] + assert JSON.decode!(outage_response) == ["OK", older_event["id"], false, "error: :db_down"] Application.put_env( :parrhesia, @@ -103,19 +103,19 @@ defmodule Parrhesia.FaultInjectionGroupFlowTest do assert {:push, {:text, newer_response}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", newer_event]), [opcode: :text]}, + {JSON.encode!(["EVENT", newer_event]), [opcode: :text]}, state ) - assert Jason.decode!(newer_response) == ["OK", newer_event["id"], true, "ok: event stored"] + assert JSON.decode!(newer_response) == ["OK", newer_event["id"], true, "ok: event stored"] assert {:push, {:text, older_response}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", older_event]), [opcode: :text]}, + {JSON.encode!(["EVENT", older_event]), [opcode: :text]}, state ) - assert Jason.decode!(older_response) == ["OK", older_event["id"], true, "ok: event stored"] + assert JSON.decode!(older_response) == ["OK", older_event["id"], true, "ok: event stored"] assert {:ok, results} = Storage.events().query( diff --git a/test/parrhesia/fault_injection_test.exs b/test/parrhesia/fault_injection_test.exs index 3136d91..0060966 100644 --- a/test/parrhesia/fault_injection_test.exs +++ b/test/parrhesia/fault_injection_test.exs @@ -30,19 +30,19 @@ defmodule Parrhesia.FaultInjectionTest do event = valid_event() assert {:push, {:text, response}, ^state} = - Connection.handle_in({Jason.encode!(["EVENT", event]), [opcode: :text]}, state) + Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state) - assert Jason.decode!(response) == ["OK", event["id"], false, "error: :db_down"] + assert JSON.decode!(response) == ["OK", event["id"], false, "error: :db_down"] end test "REQ closes with storage error when query fails" do {:ok, state} = Connection.init(subscription_index: nil) - payload = Jason.encode!(["REQ", "sub-db-down", %{"kinds" => [1]}]) + payload = JSON.encode!(["REQ", "sub-db-down", %{"kinds" => [1]}]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == ["CLOSED", "sub-db-down", "error: :db_down"] + assert JSON.decode!(response) == ["CLOSED", "sub-db-down", "error: :db_down"] end defp valid_event do diff --git a/test/parrhesia/performance/load_soak_test.exs b/test/parrhesia/performance/load_soak_test.exs index 2947187..a6a267b 100644 --- a/test/parrhesia/performance/load_soak_test.exs +++ b/test/parrhesia/performance/load_soak_test.exs @@ -11,7 +11,7 @@ defmodule Parrhesia.Performance.LoadSoakTest do {:ok, state} = Connection.init(subscription_index: nil, max_outbound_queue: 10_000) - req_payload = Jason.encode!(["REQ", "sub-load", %{"kinds" => [1]}]) + req_payload = JSON.encode!(["REQ", "sub-load", %{"kinds" => [1]}]) assert {:push, _frames, subscribed_state} = Connection.handle_in({req_payload, [opcode: :text]}, state) diff --git a/test/parrhesia/protocol_test.exs b/test/parrhesia/protocol_test.exs index 990965c..72adbc6 100644 --- a/test/parrhesia/protocol_test.exs +++ b/test/parrhesia/protocol_test.exs @@ -5,7 +5,7 @@ defmodule Parrhesia.ProtocolTest do alias Parrhesia.Protocol.EventValidator test "decodes EVENT frame shape" do - payload = Jason.encode!(["EVENT", valid_event()]) + payload = JSON.encode!(["EVENT", valid_event()]) assert {:ok, {:event, event}} = Protocol.decode_client(payload) assert event["kind"] == 1 @@ -13,9 +13,9 @@ defmodule Parrhesia.ProtocolTest do end test "decodes valid REQ, COUNT and CLOSE frames" do - req_payload = Jason.encode!(["REQ", "sub-1", %{"authors" => [String.duplicate("a", 64)]}]) - count_payload = Jason.encode!(["COUNT", "sub-1", %{"kinds" => [1]}, %{"hll" => true}]) - close_payload = Jason.encode!(["CLOSE", "sub-1"]) + req_payload = JSON.encode!(["REQ", "sub-1", %{"authors" => [String.duplicate("a", 64)]}]) + count_payload = JSON.encode!(["COUNT", "sub-1", %{"kinds" => [1]}, %{"hll" => true}]) + close_payload = JSON.encode!(["CLOSE", "sub-1"]) assert {:ok, {:req, "sub-1", [%{"authors" => [_author]}]}} = Protocol.decode_client(req_payload) @@ -27,8 +27,8 @@ defmodule Parrhesia.ProtocolTest do end test "rejects invalid subscription ids" do - empty_sub_payload = Jason.encode!(["REQ", "", %{"kinds" => [1]}]) - long_sub_payload = Jason.encode!(["CLOSE", String.duplicate("x", 65)]) + empty_sub_payload = JSON.encode!(["REQ", "", %{"kinds" => [1]}]) + long_sub_payload = JSON.encode!(["CLOSE", String.duplicate("x", 65)]) assert {:error, :invalid_subscription_id} = Protocol.decode_client(empty_sub_payload) assert {:error, :invalid_subscription_id} = Protocol.decode_client(long_sub_payload) @@ -39,25 +39,25 @@ defmodule Parrhesia.ProtocolTest do auth_event = Map.put(auth_event, "id", EventValidator.compute_id(auth_event)) assert {:ok, {:auth, ^auth_event}} = - Protocol.decode_client(Jason.encode!(["AUTH", auth_event])) + Protocol.decode_client(JSON.encode!(["AUTH", auth_event])) assert {:ok, {:neg_open, "sub-neg", %{"cursor" => 0}}} = - Protocol.decode_client(Jason.encode!(["NEG-OPEN", "sub-neg", %{"cursor" => 0}])) + Protocol.decode_client(JSON.encode!(["NEG-OPEN", "sub-neg", %{"cursor" => 0}])) assert {:ok, {:neg_msg, "sub-neg", %{"delta" => "abc"}}} = - Protocol.decode_client(Jason.encode!(["NEG-MSG", "sub-neg", %{"delta" => "abc"}])) + Protocol.decode_client(JSON.encode!(["NEG-MSG", "sub-neg", %{"delta" => "abc"}])) assert {:ok, {:neg_close, "sub-neg"}} = - Protocol.decode_client(Jason.encode!(["NEG-CLOSE", "sub-neg"])) + Protocol.decode_client(JSON.encode!(["NEG-CLOSE", "sub-neg"])) end test "returns decode errors for malformed messages" do assert {:error, :invalid_json} = Protocol.decode_client("not-json") - assert {:error, :invalid_filters} = Protocol.decode_client(Jason.encode!(["REQ", "sub-1"])) - assert {:error, :invalid_count} = Protocol.decode_client(Jason.encode!(["COUNT", "sub-1"])) + assert {:error, :invalid_filters} = Protocol.decode_client(JSON.encode!(["REQ", "sub-1"])) + assert {:error, :invalid_count} = Protocol.decode_client(JSON.encode!(["COUNT", "sub-1"])) assert {:error, :invalid_event} = - Protocol.decode_client(Jason.encode!(["EVENT", "not-a-map"])) + Protocol.decode_client(JSON.encode!(["EVENT", "not-a-map"])) end test "validates strict NIP-01 event fields" do @@ -83,13 +83,13 @@ defmodule Parrhesia.ProtocolTest do test "encodes relay messages" do frame = Protocol.encode_relay({:closed, "sub-1", "error: subscription closed"}) - assert Jason.decode!(frame) == ["CLOSED", "sub-1", "error: subscription closed"] + assert JSON.decode!(frame) == ["CLOSED", "sub-1", "error: subscription closed"] auth_frame = Protocol.encode_relay({:auth, "challenge"}) - assert Jason.decode!(auth_frame) == ["AUTH", "challenge"] + assert JSON.decode!(auth_frame) == ["AUTH", "challenge"] count_frame = Protocol.encode_relay({:count, "sub-1", %{"count" => 1}}) - assert Jason.decode!(count_frame) == ["COUNT", "sub-1", %{"count" => 1}] + assert JSON.decode!(count_frame) == ["COUNT", "sub-1", %{"count" => 1}] end defp valid_event do diff --git a/test/parrhesia/web/conformance_test.exs b/test/parrhesia/web/conformance_test.exs index 5504a47..de4d708 100644 --- a/test/parrhesia/web/conformance_test.exs +++ b/test/parrhesia/web/conformance_test.exs @@ -15,20 +15,20 @@ defmodule Parrhesia.Web.ConformanceTest do test "REQ -> EOSE emitted once and CLOSE emits CLOSED" do {:ok, state} = Connection.init(subscription_index: nil) - req_payload = Jason.encode!(["REQ", "sub-e2e", %{"kinds" => [1]}]) + req_payload = JSON.encode!(["REQ", "sub-e2e", %{"kinds" => [1]}]) assert {:push, frames, subscribed_state} = Connection.handle_in({req_payload, [opcode: :text]}, state) - decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) assert ["EOSE", "sub-e2e"] = List.last(decoded) - close_payload = Jason.encode!(["CLOSE", "sub-e2e"]) + close_payload = JSON.encode!(["CLOSE", "sub-e2e"]) assert {:push, {:text, closed_frame}, closed_state} = Connection.handle_in({close_payload, [opcode: :text]}, subscribed_state) - assert Jason.decode!(closed_frame) == ["CLOSED", "sub-e2e", "error: subscription closed"] + assert JSON.decode!(closed_frame) == ["CLOSED", "sub-e2e", "error: subscription closed"] refute Map.has_key?(closed_state.subscriptions, "sub-e2e") end @@ -38,9 +38,9 @@ defmodule Parrhesia.Web.ConformanceTest do event = valid_event() assert {:push, {:text, frame}, ^state} = - Connection.handle_in({Jason.encode!(["EVENT", event]), [opcode: :text]}, state) + Connection.handle_in({JSON.encode!(["EVENT", event]), [opcode: :text]}, state) - assert Jason.decode!(frame) == ["OK", event["id"], true, "ok: event stored"] + assert JSON.decode!(frame) == ["OK", event["id"], true, "ok: event stored"] end test "wrapped kind 1059 welcome delivery is recipient-gated" do @@ -56,19 +56,19 @@ defmodule Parrhesia.Web.ConformanceTest do assert {:push, {:text, ok_frame}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, + {JSON.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, state ) - assert Jason.decode!(ok_frame) == ["OK", wrapped_welcome["id"], true, "ok: event stored"] + assert JSON.decode!(ok_frame) == ["OK", wrapped_welcome["id"], true, "ok: event stored"] - req_payload = Jason.encode!(["REQ", "sub-welcome", %{"kinds" => [1059], "#p" => [recipient]}]) + req_payload = JSON.encode!(["REQ", "sub-welcome", %{"kinds" => [1059], "#p" => [recipient]}]) assert {:push, restricted_frames, ^state} = Connection.handle_in({req_payload, [opcode: :text]}, state) decoded_restricted = - Enum.map(restricted_frames, fn {:text, frame} -> Jason.decode!(frame) end) + Enum.map(restricted_frames, fn {:text, frame} -> JSON.decode!(frame) end) assert [ "CLOSED", @@ -80,14 +80,14 @@ defmodule Parrhesia.Web.ConformanceTest do auth_event = valid_auth_event(state.auth_challenge, recipient) assert {:push, {:text, auth_frame}, authed_state} = - Connection.handle_in({Jason.encode!(["AUTH", auth_event]), [opcode: :text]}, state) + Connection.handle_in({JSON.encode!(["AUTH", auth_event]), [opcode: :text]}, state) - assert Jason.decode!(auth_frame) == ["OK", auth_event["id"], true, "ok: auth accepted"] + assert JSON.decode!(auth_frame) == ["OK", auth_event["id"], true, "ok: auth accepted"] assert {:push, frames, _next_state} = Connection.handle_in({req_payload, [opcode: :text]}, authed_state) - decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) assert ["EVENT", "sub-welcome", result_event] = Enum.find(decoded, fn frame -> List.first(frame) == "EVENT" end) @@ -108,11 +108,11 @@ defmodule Parrhesia.Web.ConformanceTest do assert {:push, {:text, commit_ok_frame}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", commit_event]), [opcode: :text]}, + {JSON.encode!(["EVENT", commit_event]), [opcode: :text]}, state ) - assert Jason.decode!(commit_ok_frame) == ["OK", commit_event["id"], true, "ok: event stored"] + assert JSON.decode!(commit_ok_frame) == ["OK", commit_event["id"], true, "ok: event stored"] assert {:ok, persisted_commit} = Storage.events().get_event(%{}, commit_event["id"]) assert persisted_commit["id"] == commit_event["id"] @@ -126,11 +126,11 @@ defmodule Parrhesia.Web.ConformanceTest do assert {:push, {:text, welcome_ok_frame}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, + {JSON.encode!(["EVENT", wrapped_welcome]), [opcode: :text]}, state ) - assert Jason.decode!(welcome_ok_frame) == [ + assert JSON.decode!(welcome_ok_frame) == [ "OK", wrapped_welcome["id"], true, @@ -189,11 +189,11 @@ defmodule Parrhesia.Web.ConformanceTest do assert {:push, {:text, relay_ok_frame}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", relay_list_event]), [opcode: :text]}, + {JSON.encode!(["EVENT", relay_list_event]), [opcode: :text]}, state ) - assert Jason.decode!(relay_ok_frame) == [ + assert JSON.decode!(relay_ok_frame) == [ "OK", relay_list_event["id"], true, @@ -202,11 +202,11 @@ defmodule Parrhesia.Web.ConformanceTest do assert {:push, {:text, trigger_ok_frame}, ^state} = Connection.handle_in( - {Jason.encode!(["EVENT", push_trigger]), [opcode: :text]}, + {JSON.encode!(["EVENT", push_trigger]), [opcode: :text]}, state ) - assert Jason.decode!(trigger_ok_frame) == ["OK", push_trigger["id"], true, "ok: event stored"] + assert JSON.decode!(trigger_ok_frame) == ["OK", push_trigger["id"], true, "ok: event stored"] assert {:ok, persisted_relay_list} = Storage.events().get_event(%{}, relay_list_event["id"]) assert persisted_relay_list["id"] == relay_list_event["id"] diff --git a/test/parrhesia/web/connection_test.exs b/test/parrhesia/web/connection_test.exs index 18480df..6203ed0 100644 --- a/test/parrhesia/web/connection_test.exs +++ b/test/parrhesia/web/connection_test.exs @@ -14,7 +14,7 @@ defmodule Parrhesia.Web.ConnectionTest do test "REQ registers subscription, streams initial events and replies with EOSE" do state = connection_state() - req_payload = Jason.encode!(["REQ", "sub-123", %{"kinds" => [1]}]) + req_payload = JSON.encode!(["REQ", "sub-123", %{"kinds" => [1]}]) assert {:push, responses, next_state} = Connection.handle_in({req_payload, [opcode: :text]}, state) @@ -23,7 +23,7 @@ defmodule Parrhesia.Web.ConnectionTest do assert next_state.subscriptions["sub-123"].filters == [%{"kinds" => [1]}] assert next_state.subscriptions["sub-123"].eose_sent? - assert List.last(Enum.map(responses, fn {:text, frame} -> Jason.decode!(frame) end)) == [ + assert List.last(Enum.map(responses, fn {:text, frame} -> JSON.decode!(frame) end)) == [ "EOSE", "sub-123" ] @@ -32,12 +32,12 @@ defmodule Parrhesia.Web.ConnectionTest do test "COUNT returns exact count payload" do state = connection_state() - payload = Jason.encode!(["COUNT", "sub-count", %{"kinds" => [1]}]) + payload = JSON.encode!(["COUNT", "sub-count", %{"kinds" => [1]}]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert ["COUNT", "sub-count", payload] = Jason.decode!(response) + assert ["COUNT", "sub-count", payload] = JSON.decode!(response) assert payload["count"] >= 0 assert payload["approximate"] == false end @@ -46,12 +46,12 @@ defmodule Parrhesia.Web.ConnectionTest do state = connection_state() auth_event = valid_auth_event(state.auth_challenge) - payload = Jason.encode!(["AUTH", auth_event]) + payload = JSON.encode!(["AUTH", auth_event]) assert {:push, {:text, response}, next_state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == ["OK", auth_event["id"], true, "ok: auth accepted"] + assert JSON.decode!(response) == ["OK", auth_event["id"], true, "ok: auth accepted"] assert MapSet.member?(next_state.authenticated_pubkeys, auth_event["pubkey"]) refute next_state.auth_challenge == state.auth_challenge end @@ -60,11 +60,11 @@ defmodule Parrhesia.Web.ConnectionTest do state = connection_state() auth_event = valid_auth_event("wrong-challenge") - payload = Jason.encode!(["AUTH", auth_event]) + payload = JSON.encode!(["AUTH", auth_event]) assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) assert Enum.any?(decoded, fn frame -> frame == ["AUTH", state.auth_challenge] end) @@ -81,11 +81,11 @@ defmodule Parrhesia.Web.ConnectionTest do |> Map.put("tags", [["-"]]) |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, frames, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) assert ["OK", _, false, "auth-required: protected events require authenticated pubkey"] = Enum.find(decoded, fn frame -> List.first(frame) == "OK" end) @@ -96,11 +96,11 @@ defmodule Parrhesia.Web.ConnectionTest do test "kind 445 REQ without #h is rejected" do state = connection_state() - req_payload = Jason.encode!(["REQ", "sub-445", %{"kinds" => [445]}]) + req_payload = JSON.encode!(["REQ", "sub-445", %{"kinds" => [445]}]) assert {:push, frames, ^state} = Connection.handle_in({req_payload, [opcode: :text]}, state) - decoded = Enum.map(frames, fn {:text, frame} -> Jason.decode!(frame) end) + decoded = Enum.map(frames, fn {:text, frame} -> JSON.decode!(frame) end) assert ["CLOSED", "sub-445", "restricted: kind 445 queries must include a #h tag"] = Enum.find(decoded, fn frame -> List.first(frame) == "CLOSED" end) @@ -110,24 +110,24 @@ defmodule Parrhesia.Web.ConnectionTest do state = connection_state() event = valid_event() - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == ["OK", event["id"], true, "ok: event stored"] + assert JSON.decode!(response) == ["OK", event["id"], true, "ok: event stored"] end test "invalid EVENT replies with OK false invalid prefix" do state = connection_state() event = valid_event() |> Map.put("sig", "nope") - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == [ + assert JSON.decode!(response) == [ "OK", event["id"], false, @@ -145,12 +145,12 @@ defmodule Parrhesia.Web.ConnectionTest do |> Map.put("content", "ciphertext") |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == [ + assert JSON.decode!(response) == [ "OK", event["id"], false, @@ -168,12 +168,12 @@ defmodule Parrhesia.Web.ConnectionTest do |> Map.put("content", "not-base64") |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == [ + assert JSON.decode!(response) == [ "OK", event["id"], false, @@ -202,12 +202,12 @@ defmodule Parrhesia.Web.ConnectionTest do ]) |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == [ + assert JSON.decode!(response) == [ "OK", event["id"], false, @@ -253,12 +253,12 @@ defmodule Parrhesia.Web.ConnectionTest do |> Map.put("content", "encrypted") |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(response) == [ + assert JSON.decode!(response) == [ "OK", event["id"], false, @@ -304,17 +304,17 @@ defmodule Parrhesia.Web.ConnectionTest do |> Map.put("content", "encrypted") |> then(&Map.put(&1, "id", EventValidator.compute_id(&1))) - payload = Jason.encode!(["EVENT", event]) + payload = JSON.encode!(["EVENT", event]) assert {:push, {:text, first_response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(first_response) == ["OK", event["id"], true, "ok: event stored"] + assert JSON.decode!(first_response) == ["OK", event["id"], true, "ok: event stored"] assert {:push, {:text, second_response}, ^state} = Connection.handle_in({payload, [opcode: :text]}, state) - assert Jason.decode!(second_response) == [ + assert JSON.decode!(second_response) == [ "OK", event["id"], false, @@ -325,32 +325,32 @@ defmodule Parrhesia.Web.ConnectionTest do test "NEG sessions open and close" do state = connection_state() - open_payload = Jason.encode!(["NEG-OPEN", "neg-1", %{"cursor" => 0}]) + open_payload = JSON.encode!(["NEG-OPEN", "neg-1", %{"cursor" => 0}]) assert {:push, {:text, open_response}, ^state} = Connection.handle_in({open_payload, [opcode: :text]}, state) assert ["NEG-MSG", "neg-1", %{"status" => "open", "cursor" => 0}] = - Jason.decode!(open_response) + JSON.decode!(open_response) - close_payload = Jason.encode!(["NEG-CLOSE", "neg-1"]) + close_payload = JSON.encode!(["NEG-CLOSE", "neg-1"]) assert {:push, {:text, close_response}, ^state} = Connection.handle_in({close_payload, [opcode: :text]}, state) - assert Jason.decode!(close_response) == ["NEG-MSG", "neg-1", %{"status" => "closed"}] + assert JSON.decode!(close_response) == ["NEG-MSG", "neg-1", %{"status" => "closed"}] end test "CLOSE removes subscription and replies with CLOSED" do state = subscribed_connection_state([]) - close_payload = Jason.encode!(["CLOSE", "sub-1"]) + close_payload = JSON.encode!(["CLOSE", "sub-1"]) assert {:push, {:text, response}, next_state} = Connection.handle_in({close_payload, [opcode: :text]}, state) refute Map.has_key?(next_state.subscriptions, "sub-1") - assert Jason.decode!(response) == ["CLOSED", "sub-1", "error: subscription closed"] + assert JSON.decode!(response) == ["CLOSED", "sub-1", "error: subscription closed"] end test "fanout_event enqueues and drains matching events" do @@ -366,7 +366,7 @@ defmodule Parrhesia.Web.ConnectionTest do Connection.handle_info(:drain_outbound_queue, queued_state) assert drained_state.outbound_queue_size == 0 - assert Jason.decode!(payload) == ["EVENT", "sub-1", event] + assert JSON.decode!(payload) == ["EVENT", "sub-1", event] end test "high-volume kind 445 fanout drains in order across batches" do @@ -392,7 +392,7 @@ defmodule Parrhesia.Web.ConnectionTest do delivered_ids = frames - |> Enum.map(fn {:text, payload} -> Jason.decode!(payload) end) + |> Enum.map(fn {:text, payload} -> JSON.decode!(payload) end) |> Enum.map(fn ["EVENT", "sub-group", event] -> event["id"] end) assert delivered_ids == Enum.map(events, & &1["id"]) @@ -418,12 +418,12 @@ defmodule Parrhesia.Web.ConnectionTest do ) assert message == "rate-limited: outbound queue overflow" - assert Jason.decode!(notice_payload) == ["NOTICE", message] + assert JSON.decode!(notice_payload) == ["NOTICE", message] end defp subscribed_connection_state(opts) do state = connection_state(opts) - req_payload = Jason.encode!(["REQ", "sub-1", %{"kinds" => [1]}]) + req_payload = JSON.encode!(["REQ", "sub-1", %{"kinds" => [1]}]) assert {:push, _, subscribed_state} = Connection.handle_in({req_payload, [opcode: :text]}, state) @@ -433,7 +433,7 @@ defmodule Parrhesia.Web.ConnectionTest do defp subscribed_group_connection_state(group_id, opts) do state = connection_state(opts) - req_payload = Jason.encode!(["REQ", "sub-group", %{"kinds" => [445], "#h" => [group_id]}]) + req_payload = JSON.encode!(["REQ", "sub-group", %{"kinds" => [445], "#h" => [group_id]}]) assert {:push, _, subscribed_state} = Connection.handle_in({req_payload, [opcode: :text]}, state) diff --git a/test/parrhesia/web/router_test.exs b/test/parrhesia/web/router_test.exs index 9f9210f..b8632bf 100644 --- a/test/parrhesia/web/router_test.exs +++ b/test/parrhesia/web/router_test.exs @@ -37,7 +37,7 @@ defmodule Parrhesia.Web.RouterTest do assert conn.status == 200 assert get_resp_header(conn, "content-type") == ["application/nostr+json; charset=utf-8"] - body = Jason.decode!(conn.resp_body) + body = JSON.decode!(conn.resp_body) assert body["name"] == "Parrhesia" assert 11 in body["supported_nips"] @@ -52,29 +52,29 @@ defmodule Parrhesia.Web.RouterTest do test "POST /management requires authorization" do conn = - conn(:post, "/management", Jason.encode!(%{"method" => "ping", "params" => %{}})) + conn(:post, "/management", JSON.encode!(%{"method" => "ping", "params" => %{}})) |> put_req_header("content-type", "application/json") |> Router.call([]) assert conn.status == 401 - assert Jason.decode!(conn.resp_body) == %{"ok" => false, "error" => "auth-required"} + assert JSON.decode!(conn.resp_body) == %{"ok" => false, "error" => "auth-required"} end test "POST /management accepts valid NIP-98 header" do management_url = "http://www.example.com/management" auth_event = nip98_event("POST", management_url) - authorization = "Nostr " <> Base.encode64(Jason.encode!(auth_event)) + authorization = "Nostr " <> Base.encode64(JSON.encode!(auth_event)) conn = - conn(:post, "/management", Jason.encode!(%{"method" => "ping", "params" => %{}})) + conn(:post, "/management", JSON.encode!(%{"method" => "ping", "params" => %{}})) |> put_req_header("content-type", "application/json") |> put_req_header("authorization", authorization) |> Router.call([]) assert conn.status == 200 - assert Jason.decode!(conn.resp_body) == %{ + assert JSON.decode!(conn.resp_body) == %{ "ok" => true, "result" => %{"status" => "ok"} }