From 28c47ab435220d1b81870093aaf50225d7b8f05c Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Fri, 20 Mar 2026 04:10:06 +0100 Subject: [PATCH 1/2] test/build: Stability, compatibility --- justfile | 11 +-- .../storage/adapters/memory/store.ex | 76 ++++++++++++++++--- lib/parrhesia/storage/supervisor.ex | 10 ++- .../storage/adapters/memory/adapter_test.exs | 6 ++ 4 files changed, 86 insertions(+), 17 deletions(-) diff --git a/justfile b/justfile index 7f06cd9..08f3dcc 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,4 @@ set shell := ["bash", "-euo", "pipefail", "-c"] -set script-interpreter := ["bash", "-euo", "pipefail"] repo_root := justfile_directory() @@ -12,8 +11,9 @@ help topic="": @cd "{{repo_root}}" && ./scripts/just_help.sh "{{topic}}" # Raw e2e harness commands. -[script] -e2e subcommand="help" *args: +e2e subcommand *args: + #!/usr/bin/env bash + set -euo pipefail cd "{{repo_root}}" subcommand="{{subcommand}}" @@ -40,8 +40,9 @@ e2e subcommand="help" *args: fi # Benchmark flows (local/cloud/history + direct relay targets). -[script] -bench subcommand="help" *args: +bench subcommand *args: + #!/usr/bin/env bash + set -euo pipefail cd "{{repo_root}}" subcommand="{{subcommand}}" diff --git a/lib/parrhesia/storage/adapters/memory/store.ex b/lib/parrhesia/storage/adapters/memory/store.ex index 1a74d90..625a15b 100644 --- a/lib/parrhesia/storage/adapters/memory/store.ex +++ b/lib/parrhesia/storage/adapters/memory/store.ex @@ -22,7 +22,17 @@ defmodule Parrhesia.Storage.Adapters.Memory.Store do audit_logs: [] } - def ensure_started, do: start_store() + def start_link(opts \\ []) do + name = Keyword.get(opts, :name, @name) + Agent.start_link(&init_state/0, name: name) + end + + def ensure_started do + case Process.whereis(@name) do + pid when is_pid(pid) -> :ok + nil -> start_store() + end + end def put_event(event_id, event) when is_binary(event_id) and is_map(event) do :ok = ensure_started() @@ -159,28 +169,72 @@ defmodule Parrhesia.Storage.Adapters.Memory.Store do defp normalize_reduce_result(next_acc), do: next_acc def get(fun) do - :ok = ensure_started() - Agent.get(@name, fun) + with_store(fn pid -> Agent.get(pid, fun) end) end def update(fun) do - :ok = ensure_started() - Agent.update(@name, fun) + with_store(fn pid -> Agent.update(pid, fun) end) end def get_and_update(fun) do - :ok = ensure_started() - Agent.get_and_update(@name, fun) + with_store(fn pid -> Agent.get_and_update(pid, fun) end) end defp start_store do - case Agent.start_link(&init_state/0, name: @name) do - {:ok, _pid} -> :ok - {:error, {:already_started, _pid}} -> :ok - {:error, reason} -> {:error, reason} + case start_link() do + {:ok, _pid} -> + :ok + + {:error, {:already_started, pid}} -> + if Process.alive?(pid) do + :ok + else + wait_for_store_exit(pid) + end + + {:error, reason} -> + {:error, reason} end end + defp with_store(fun, attempts \\ 2) + + defp with_store(fun, attempts) when attempts > 0 do + :ok = ensure_started() + + case Process.whereis(@name) do + pid when is_pid(pid) -> + try do + fun.(pid) + catch + :exit, reason -> + if noproc_exit?(reason) and attempts > 1 do + with_store(fun, attempts - 1) + else + exit(reason) + end + end + + nil -> + with_store(fun, attempts - 1) + end + end + + defp with_store(_fun, 0), do: exit(:noproc) + + defp wait_for_store_exit(pid) do + ref = Process.monitor(pid) + + receive do + {:DOWN, ^ref, :process, ^pid, _reason} -> start_store() + after + 100 -> start_store() + end + end + + defp noproc_exit?({:noproc, _details}), do: true + defp noproc_exit?(_reason), do: false + defp init_state do ensure_tables_started() diff --git a/lib/parrhesia/storage/supervisor.ex b/lib/parrhesia/storage/supervisor.ex index 3e73332..adf4423 100644 --- a/lib/parrhesia/storage/supervisor.ex +++ b/lib/parrhesia/storage/supervisor.ex @@ -13,11 +13,19 @@ defmodule Parrhesia.Storage.Supervisor do @impl true def init(_init_arg) do - children = moderation_cache_children() ++ PostgresRepos.started_repos() + children = + memory_store_children() ++ moderation_cache_children() ++ PostgresRepos.started_repos() Supervisor.init(children, strategy: :one_for_one) end + defp memory_store_children do + case Application.get_env(:parrhesia, :storage, [])[:backend] do + :memory -> [Parrhesia.Storage.Adapters.Memory.Store] + _other -> [] + end + end + defp moderation_cache_children do if PostgresRepos.postgres_enabled?() and Application.get_env(:parrhesia, :moderation_cache_enabled, true) do diff --git a/test/parrhesia/storage/adapters/memory/adapter_test.exs b/test/parrhesia/storage/adapters/memory/adapter_test.exs index 27bbce7..e238ed8 100644 --- a/test/parrhesia/storage/adapters/memory/adapter_test.exs +++ b/test/parrhesia/storage/adapters/memory/adapter_test.exs @@ -6,6 +6,12 @@ defmodule Parrhesia.Storage.Adapters.Memory.AdapterTest do alias Parrhesia.Storage.Adapters.Memory.Events alias Parrhesia.Storage.Adapters.Memory.Groups alias Parrhesia.Storage.Adapters.Memory.Moderation + alias Parrhesia.Storage.Adapters.Memory.Store + + setup do + start_supervised!(Store) + :ok + end test "memory adapter supports basic behavior contract operations" do event_id = String.duplicate("a", 64) From bbcaa00f0b5cbea942ee7f9c4965a3a49f16e017 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Fri, 20 Mar 2026 03:44:24 +0100 Subject: [PATCH 2/2] chore: Bump version to 0.7.0, 1st beta --- .github/workflows/ci.yaml | 10 ++++++++-- .github/workflows/release.yaml | 9 +++++++-- CHANGELOG.md | 18 ++++++++++++++++++ README.md | 8 ++++++-- default.nix | 2 +- docs/LOCAL_API.md | 4 ++-- mix.exs | 2 +- mix.lock | 2 +- 8 files changed, 44 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d8cdc90..4fd5585 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -69,6 +69,11 @@ jobs: with: node-version: 24 + - name: Install just + run: | + sudo apt-get update + sudo apt-get install -y just + # Cache deps/ directory — keyed on mix.lock - name: Cache Mix deps uses: actions/cache@v4 @@ -115,7 +120,8 @@ jobs: - name: Run Node Sync E2E tests if: ${{ matrix.main }} - run: mix test.node_sync_e2e + run: just e2e node-sync - name: Run Marmot E2E tests - run: mix test.marmot_e2e + if: ${{ matrix.main }} + run: just e2e marmot diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d7f1f2d..dbe2fe4 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -68,6 +68,11 @@ jobs: with: node-version: 24 + - name: Install just + run: | + sudo apt-get update + sudo apt-get install -y just + - name: Cache Mix deps uses: actions/cache@v4 id: deps-cache @@ -113,10 +118,10 @@ jobs: run: mix test --color - name: Run Node Sync E2E - run: mix test.node_sync_e2e + run: just e2e node-sync - name: Run Marmot E2E - run: mix test.marmot_e2e + run: just e2e marmot - name: Check for unused locked deps run: | diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0f3edad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,18 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [0.7.0] - 2026-03-20 + +First beta release! + +### Added +- Configurable WebSocket keepalive support in `Parrhesia.Web.Connection`: + - server-initiated `PING` frames + - `PONG` timeout handling with connection close on timeout +- New runtime limit settings: + - `:websocket_ping_interval_seconds` (`PARRHESIA_LIMITS_WEBSOCKET_PING_INTERVAL_SECONDS`) + - `:websocket_pong_timeout_seconds` (`PARRHESIA_LIMITS_WEBSOCKET_PONG_TIMEOUT_SECONDS`) + +### Changed +- NIP-42 challenge validation now uses constant-time comparison via `Plug.Crypto.secure_compare/2`. diff --git a/README.md b/README.md index 66748d2..fc33d02 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ Parrhesia is a Nostr relay server written in Elixir/OTP. +**BETA CONDITION – BREAKING CHANGES MAY STILL HAPPEN!** + Supported storage backends: - PostgreSQL, which is the primary and production-oriented backend - in-memory storage, which is useful for tests, local experiments, and benchmarks -**ALPHA CONDITION – BREAKING CHANGES MIGHT HAPPEN!** +Advanced Nostr features: - Advanced Querying: Full-text search (NIP-50) and COUNT queries (NIP-45). - Secure Messaging: First-class support for Marmot MLS-encrypted groups and NIP-17/44/59 gift-wrapped DMs. @@ -146,7 +148,7 @@ Start with: Important caveats for host applications: -- Parrhesia is pre-beta; expect some public API and config churn while we prepare for beta. +- Parrhesia is beta software; expect some API and config churn as the runtime stabilizes. - 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`. @@ -665,3 +667,5 @@ For Marmot client end-to-end checks (TypeScript/Node suite using `marmot-ts`, in ```bash just e2e marmot ``` + +``` diff --git a/default.nix b/default.nix index ff6ef48..5dfcc07 100644 --- a/default.nix +++ b/default.nix @@ -10,7 +10,7 @@ vips, }: let pname = "parrhesia"; - version = "0.6.0"; + version = "0.7.0"; beamPackages = beam.packages.erlang_28.extend ( final: _prev: { diff --git a/docs/LOCAL_API.md b/docs/LOCAL_API.md index d1757c4..c879fdb 100644 --- a/docs/LOCAL_API.md +++ b/docs/LOCAL_API.md @@ -3,8 +3,8 @@ 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 pre-beta, so treat the API -as usable but not yet frozen. +This document describes that embedding surface. The runtime is now beta, so treat the API +as usable with minor churn possible while it stabilizes. ## What embedding means today diff --git a/mix.exs b/mix.exs index 898f2c4..81281fc 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Parrhesia.MixProject do def project do [ app: :parrhesia, - version: "0.6.0", + version: "0.7.0", elixir: "~> 1.18", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, diff --git a/mix.lock b/mix.lock index 3cf315d..02243cd 100644 --- a/mix.lock +++ b/mix.lock @@ -17,7 +17,7 @@ "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"}, - "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.6", "687d622c735e020f13cf480c83d0fce1cc899f4fbed547f5254b960ea82d3525", [: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", "424f41a41273fce0f7424008405ee073b5bd06359ca9396e841f83a669c01619"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "lib_secp256k1": {:hex, :lib_secp256k1, "0.7.1", "53cad778b8da3a29e453a7a477517d99fb5f13f615c8050eb2db8fd1dce7a1db", [:make, :mix], [{:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "78bdd3661a17448aff5aeec5ca74c8ddbc09b01f0ecfa3ba1aba3e8ae47ab2b3"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"},