diff --git a/lib/parrhesia/api/acl.ex b/lib/parrhesia/api/acl.ex index d845308..609c6ae 100644 --- a/lib/parrhesia/api/acl.ex +++ b/lib/parrhesia/api/acl.ex @@ -1,12 +1,37 @@ defmodule Parrhesia.API.ACL do @moduledoc """ Public ACL API and rule matching for protected sync traffic. + + ACL checks are only applied when the requested subject overlaps with + `config :parrhesia, :acl, protected_filters: [...]`. + + The intended flow is: + + 1. mark a subset of sync traffic as protected with `protected_filters` + 2. persist pubkey-based grants with `grant/2` + 3. call `check/3` during sync reads and writes + + Unprotected subjects always return `:ok`. """ alias Parrhesia.API.RequestContext alias Parrhesia.Protocol.Filter alias Parrhesia.Storage + @doc """ + Persists an ACL rule. + + A typical rule looks like: + + ```elixir + %{ + principal_type: :pubkey, + principal: "...64 hex chars...", + capability: :sync_read, + match: %{"kinds" => [5000], "#r" => ["tribes.accounts.user"]} + } + ``` + """ @spec grant(map(), keyword()) :: :ok | {:error, term()} def grant(rule, _opts \\ []) do with {:ok, _stored_rule} <- Storage.acl().put_rule(%{}, normalize_rule(rule)) do @@ -14,16 +39,39 @@ defmodule Parrhesia.API.ACL do end end + @doc """ + Deletes ACL rules matching the given selector. + + The selector is passed through to the configured storage adapter, which typically accepts an + id-based selector such as `%{id: rule_id}`. + """ @spec revoke(map(), keyword()) :: :ok | {:error, term()} def revoke(rule, _opts \\ []) do Storage.acl().delete_rule(%{}, normalize_delete_selector(rule)) end + @doc """ + Lists persisted ACL rules. + + Supported filters are: + + - `:principal_type` + - `:principal` + - `:capability` + """ @spec list(keyword()) :: {:ok, [map()]} | {:error, term()} def list(opts \\ []) do Storage.acl().list_rules(%{}, normalize_list_opts(opts)) end + @doc """ + Authorizes a protected sync read or write subject for the given request context. + + Supported capabilities are `:sync_read` and `:sync_write`. + + `opts[:context]` defaults to an empty `Parrhesia.API.RequestContext`, which means protected + subjects will fail with `{:error, :auth_required}` until authenticated pubkeys are present. + """ @spec check(atom(), map(), keyword()) :: :ok | {:error, term()} def check(capability, subject, opts \\ []) @@ -44,6 +92,9 @@ defmodule Parrhesia.API.ACL do def check(_capability, _subject, _opts), do: {:error, :invalid_acl_capability} + @doc """ + Returns `true` when a filter overlaps the configured protected read surface. + """ @spec protected_read?(map()) :: boolean() def protected_read?(filter) when is_map(filter) do case protected_filters() do @@ -57,6 +108,9 @@ defmodule Parrhesia.API.ACL do def protected_read?(_filter), do: false + @doc """ + Returns `true` when an event matches the configured protected write surface. + """ @spec protected_write?(map()) :: boolean() def protected_write?(event) when is_map(event) do case protected_filters() do diff --git a/lib/parrhesia/api/admin.ex b/lib/parrhesia/api/admin.ex index 259bf0a..103550c 100644 --- a/lib/parrhesia/api/admin.ex +++ b/lib/parrhesia/api/admin.ex @@ -1,6 +1,14 @@ defmodule Parrhesia.API.Admin do @moduledoc """ Public management API facade. + + This module exposes the DX-friendly control plane for administrative tasks. It wraps + storage-backed management methods and a set of built-in helpers for ACL, identity, sync, + and listener management. + + `execute/3` accepts the same method names used by NIP-86 style management endpoints, while + the dedicated functions (`stats/1`, `health/1`, `list_audit_logs/1`) are easier to call + from Elixir code. """ alias Parrhesia.API.ACL @@ -26,6 +34,22 @@ defmodule Parrhesia.API.Admin do sync_sync_now ) + @doc """ + Executes a management method by name. + + Built-in methods include: + + - `supportedmethods` + - `stats` + - `health` + - `list_audit_logs` + - `acl_grant`, `acl_revoke`, `acl_list` + - `identity_get`, `identity_ensure`, `identity_import`, `identity_rotate` + - `listener_reload` + - `sync_*` + + Unknown methods are delegated to the configured `Parrhesia.Storage.Admin` implementation. + """ @spec execute(String.t() | atom(), map(), keyword()) :: {:ok, map()} | {:error, term()} def execute(method, params, opts \\ []) @@ -41,6 +65,9 @@ defmodule Parrhesia.API.Admin do def execute(method, _params, _opts), do: {:error, {:unsupported_method, normalize_method_name(method)}} + @doc """ + Returns aggregate relay stats plus nested sync stats. + """ @spec stats(keyword()) :: {:ok, map()} | {:error, term()} def stats(opts \\ []) do with {:ok, relay_stats} <- relay_stats(), @@ -49,6 +76,12 @@ defmodule Parrhesia.API.Admin do end end + @doc """ + Returns the overall management health payload. + + The top-level `"status"` is currently derived from sync health, while relay-specific health + details remain delegated to storage-backed management methods. + """ @spec health(keyword()) :: {:ok, map()} | {:error, term()} def health(opts \\ []) do with {:ok, sync_health} <- Sync.sync_health(opts) do @@ -60,6 +93,12 @@ defmodule Parrhesia.API.Admin do end end + @doc """ + Lists persisted audit log entries from the configured admin storage backend. + + Supported options are storage-adapter specific. The built-in admin execution path forwards + `:limit`, `:method`, and `:actor_pubkey`. + """ @spec list_audit_logs(keyword()) :: {:ok, [map()]} | {:error, term()} def list_audit_logs(opts \\ []) do Storage.admin().list_audit_logs(%{}, opts) diff --git a/lib/parrhesia/api/auth.ex b/lib/parrhesia/api/auth.ex index 8fbf498..193f7e1 100644 --- a/lib/parrhesia/api/auth.ex +++ b/lib/parrhesia/api/auth.ex @@ -1,6 +1,15 @@ defmodule Parrhesia.API.Auth do @moduledoc """ - Shared auth and event validation helpers. + Public helpers for event validation and NIP-98 HTTP authentication. + + This module is intended for callers that need a programmatic API surface: + + - `validate_event/1` returns validator reason atoms. + - `compute_event_id/1` computes the canonical Nostr event id. + - `validate_nip98/3` and `validate_nip98/4` turn an `Authorization` header into a + shared auth context that can be reused by the rest of the API surface. + + For transport-facing validation messages, see `Parrhesia.Protocol.validate_event/1`. """ alias Parrhesia.API.Auth.Context @@ -8,18 +17,46 @@ defmodule Parrhesia.API.Auth do alias Parrhesia.Auth.Nip98 alias Parrhesia.Protocol.EventValidator + @doc """ + Validates a Nostr event and returns validator-friendly error atoms. + + This is the low-level validation entrypoint used by the API surface. Unlike + `Parrhesia.Protocol.validate_event/1`, it preserves the raw validator reason so callers + can branch on it directly. + """ @spec validate_event(map()) :: :ok | {:error, term()} def validate_event(event), do: EventValidator.validate(event) + @doc """ + Computes the canonical Nostr event id for an event payload. + + The event does not need to be persisted first. This is useful when building or signing + events locally. + """ @spec compute_event_id(map()) :: String.t() def compute_event_id(event), do: EventValidator.compute_id(event) + @doc """ + Validates a NIP-98 `Authorization` header using default options. + """ @spec validate_nip98(String.t() | nil, String.t(), String.t()) :: {:ok, Context.t()} | {:error, term()} def validate_nip98(authorization, method, url) do validate_nip98(authorization, method, url, []) end + @doc """ + Validates a NIP-98 `Authorization` header and returns a shared auth context. + + The returned `Parrhesia.API.Auth.Context` includes: + + - the decoded auth event + - the authenticated pubkey + - a `Parrhesia.API.RequestContext` with `caller: :http` + + Supported options are forwarded to `Parrhesia.Auth.Nip98.validate_authorization_header/4`, + including `:max_age_seconds` and `:replay_cache`. + """ @spec validate_nip98(String.t() | nil, String.t(), String.t(), keyword()) :: {:ok, Context.t()} | {:error, term()} def validate_nip98(authorization, method, url, opts) diff --git a/lib/parrhesia/api/auth/context.ex b/lib/parrhesia/api/auth/context.ex index 9d7bdad..59a5d94 100644 --- a/lib/parrhesia/api/auth/context.ex +++ b/lib/parrhesia/api/auth/context.ex @@ -1,6 +1,10 @@ defmodule Parrhesia.API.Auth.Context do @moduledoc """ Authenticated request details returned by shared auth helpers. + + This is the higher-level result returned by `Parrhesia.API.Auth.validate_nip98/3` and + `validate_nip98/4`. The nested `request_context` is ready to be passed into the rest of the + public API surface. """ alias Parrhesia.API.RequestContext diff --git a/lib/parrhesia/api/events.ex b/lib/parrhesia/api/events.ex index c795744..b1df301 100644 --- a/lib/parrhesia/api/events.ex +++ b/lib/parrhesia/api/events.ex @@ -1,6 +1,17 @@ defmodule Parrhesia.API.Events do @moduledoc """ Canonical event publish, query, and count API. + + This is the main in-process API for working with Nostr events. It applies the same core + validation and policy checks used by the relay edge, but without going through a socket or + HTTP transport. + + All public functions expect `opts[:context]` to contain a `Parrhesia.API.RequestContext`. + That context drives authorization, caller attribution, and downstream policy behavior. + + `publish/2` intentionally returns `{:ok, %PublishResult{accepted: false}}` for policy and + storage rejections so callers can mirror relay `OK` semantics without treating a rejected + event as a process error. """ alias Parrhesia.API.Events.PublishResult @@ -29,6 +40,24 @@ defmodule Parrhesia.API.Events do 449 ]) + @doc """ + Validates, authorizes, persists, and fans out an event. + + Required options: + + - `:context` - a `Parrhesia.API.RequestContext` + + Supported options: + + - `:max_event_bytes` - overrides the configured max encoded event size + - `:path`, `:private_key`, `:configured_private_key` - forwarded to the NIP-43 helper flow + + Return semantics: + + - `{:ok, %PublishResult{accepted: true}}` for accepted events + - `{:ok, %PublishResult{accepted: false}}` for rejected or duplicate events + - `{:error, :invalid_context}` only when the call itself is malformed + """ @spec publish(map(), keyword()) :: {:ok, PublishResult.t()} | {:error, term()} def publish(event, opts \\ []) @@ -87,6 +116,22 @@ defmodule Parrhesia.API.Events do def publish(_event, _opts), do: {:error, :invalid_event} + @doc """ + Queries stored events plus any dynamic NIP-43 events visible to the caller. + + Required options: + + - `:context` - a `Parrhesia.API.RequestContext` + + Supported options: + + - `:max_filter_limit` - overrides the configured per-filter limit + - `:validate_filters?` - skips filter validation when `false` + - `:authorize_read?` - skips read policy checks when `false` + + The skip flags are primarily for internal composition, such as `Parrhesia.API.Stream`. + External callers should normally leave them enabled. + """ @spec query([map()], keyword()) :: {:ok, [map()]} | {:error, term()} def query(filters, opts \\ []) @@ -118,6 +163,22 @@ defmodule Parrhesia.API.Events do def query(_filters, _opts), do: {:error, :invalid_filters} + @doc """ + Counts events matching the given filters. + + Required options: + + - `:context` - a `Parrhesia.API.RequestContext` + + Supported options: + + - `:validate_filters?` - skips filter validation when `false` + - `:authorize_read?` - skips read policy checks when `false` + - `:options` - when set to a map, returns a NIP-45-style payload instead of a bare integer + + When `opts[:options]` is a map, the result shape is `%{"count" => count, "approximate" => false}`. + If `opts[:options]["hll"]` is `true` and the feature is enabled, an `"hll"` field is included. + """ @spec count([map()], keyword()) :: {:ok, non_neg_integer() | map()} | {:error, term()} def count(filters, opts \\ []) diff --git a/lib/parrhesia/api/events/publish_result.ex b/lib/parrhesia/api/events/publish_result.ex index e115d2b..a2fc293 100644 --- a/lib/parrhesia/api/events/publish_result.ex +++ b/lib/parrhesia/api/events/publish_result.ex @@ -1,6 +1,14 @@ defmodule Parrhesia.API.Events.PublishResult do @moduledoc """ Result shape for event publish attempts. + + This mirrors relay `OK` semantics: + + - `accepted: true` means the event was accepted + - `accepted: false` means the event was rejected or identified as a duplicate + + The surrounding call still returns `{:ok, result}` in both cases so callers can surface the + rejection message without treating it as a transport or process failure. """ defstruct [:event_id, :accepted, :message, :reason] diff --git a/lib/parrhesia/api/identity.ex b/lib/parrhesia/api/identity.ex index c8355d2..782d316 100644 --- a/lib/parrhesia/api/identity.ex +++ b/lib/parrhesia/api/identity.ex @@ -1,15 +1,40 @@ defmodule Parrhesia.API.Identity do @moduledoc """ Server-auth identity management. + + Parrhesia uses a single server identity for flows that need the relay to sign events or + prove control of a pubkey. + + Identity resolution follows this order: + + 1. `opts[:private_key]` or `opts[:configured_private_key]` + 2. `Application.get_env(:parrhesia, :identity)` + 3. the persisted file on disk + + Supported options across this module: + + - `:path` - overrides the identity file path + - `:private_key` / `:configured_private_key` - uses an explicit hex secret key + + A configured private key is treated as read-only input and therefore cannot be rotated. """ alias Parrhesia.API.Auth + @typedoc """ + Public identity metadata returned to callers. + """ @type identity_metadata :: %{ pubkey: String.t(), source: :configured | :persisted | :generated | :imported } + @doc """ + Returns the current server identity metadata. + + This does not generate a new identity. If no configured or persisted identity exists, it + returns `{:error, :identity_not_found}`. + """ @spec get(keyword()) :: {:ok, identity_metadata()} | {:error, term()} def get(opts \\ []) do with {:ok, identity} <- fetch_existing_identity(opts) do @@ -17,6 +42,9 @@ defmodule Parrhesia.API.Identity do end end + @doc """ + Returns the current identity, generating and persisting one when necessary. + """ @spec ensure(keyword()) :: {:ok, identity_metadata()} | {:error, term()} def ensure(opts \\ []) do with {:ok, identity} <- ensure_identity(opts) do @@ -24,6 +52,12 @@ defmodule Parrhesia.API.Identity do end end + @doc """ + Imports an explicit secret key and persists it as the server identity. + + The input map must contain `:secret_key` or `"secret_key"` as a 64-character lowercase or + uppercase hex string. + """ @spec import(map(), keyword()) :: {:ok, identity_metadata()} | {:error, term()} def import(identity, opts \\ []) @@ -37,6 +71,12 @@ defmodule Parrhesia.API.Identity do def import(_identity, _opts), do: {:error, :invalid_identity} + @doc """ + Generates and persists a fresh server identity. + + Rotation is rejected with `{:error, :configured_identity_cannot_rotate}` when the active + identity comes from configuration rather than the persisted file. + """ @spec rotate(keyword()) :: {:ok, identity_metadata()} | {:error, term()} def rotate(opts \\ []) do with :ok <- ensure_rotation_allowed(opts), @@ -46,6 +86,18 @@ defmodule Parrhesia.API.Identity do end end + @doc """ + Signs an event with the current server identity. + + The incoming event must already include the fields required to compute a Nostr id: + + - `"created_at"` + - `"kind"` + - `"tags"` + - `"content"` + + On success the returned event includes `"pubkey"`, `"id"`, and `"sig"`. + """ @spec sign_event(map(), keyword()) :: {:ok, map()} | {:error, term()} def sign_event(event, opts \\ []) @@ -59,6 +111,9 @@ defmodule Parrhesia.API.Identity do def sign_event(_event, _opts), do: {:error, :invalid_event} + @doc """ + Returns the default filesystem path for the persisted server identity. + """ def default_path do Path.join([default_data_dir(), "server_identity.json"]) end diff --git a/lib/parrhesia/api/request_context.ex b/lib/parrhesia/api/request_context.ex index 976eab9..3bd002c 100644 --- a/lib/parrhesia/api/request_context.ex +++ b/lib/parrhesia/api/request_context.ex @@ -1,6 +1,15 @@ defmodule Parrhesia.API.RequestContext do @moduledoc """ Shared request context used across API and policy surfaces. + + This struct carries caller identity and transport metadata through authorization and storage + boundaries. + + The most important field for external callers is `authenticated_pubkeys`. For example: + + - `Parrhesia.API.Events` uses it for read and write policy checks + - `Parrhesia.API.Stream` uses it for subscription authorization + - `Parrhesia.API.ACL` uses it when evaluating protected sync traffic """ defstruct authenticated_pubkeys: MapSet.new(), @@ -23,6 +32,11 @@ defmodule Parrhesia.API.RequestContext do metadata: map() } + @doc """ + Merges arbitrary metadata into the context. + + Existing keys are overwritten by the incoming map. + """ @spec put_metadata(t(), map()) :: t() def put_metadata(%__MODULE__{} = context, metadata) when is_map(metadata) do %__MODULE__{context | metadata: Map.merge(context.metadata, metadata)} diff --git a/lib/parrhesia/api/stream.ex b/lib/parrhesia/api/stream.ex index 7dd0413..f5b600c 100644 --- a/lib/parrhesia/api/stream.ex +++ b/lib/parrhesia/api/stream.ex @@ -1,6 +1,15 @@ defmodule Parrhesia.API.Stream do @moduledoc """ In-process subscription API with relay-equivalent catch-up and live fanout semantics. + + Subscriptions are process-local bridges. After subscribing, the caller receives messages in + the same order a relay client would expect: + + - `{:parrhesia, :event, ref, subscription_id, event}` for catch-up and live events + - `{:parrhesia, :eose, ref, subscription_id}` after the initial replay finishes + + This API requires a `Parrhesia.API.RequestContext` so read policies are applied exactly as + they would be for a transport-backed subscriber. """ alias Parrhesia.API.Events @@ -9,6 +18,16 @@ defmodule Parrhesia.API.Stream do alias Parrhesia.Policy.EventPolicy alias Parrhesia.Protocol.Filter + @doc """ + Starts an in-process subscription for a subscriber pid. + + `opts[:context]` must be a `Parrhesia.API.RequestContext`. + + On success the returned reference is both: + + - the subscription handle used by `unsubscribe/1` + - the value embedded in emitted subscriber messages + """ @spec subscribe(pid(), String.t(), [map()], keyword()) :: {:ok, reference()} | {:error, term()} def subscribe(subscriber, subscription_id, filters, opts \\ []) @@ -42,6 +61,11 @@ defmodule Parrhesia.API.Stream do def subscribe(_subscriber, _subscription_id, _filters, _opts), do: {:error, :invalid_subscription} + @doc """ + Stops a subscription previously created with `subscribe/4`. + + This function is idempotent. Unknown or already-stopped references return `:ok`. + """ @spec unsubscribe(reference()) :: :ok def unsubscribe(ref) when is_reference(ref) do case Registry.lookup(Parrhesia.API.Stream.Registry, ref) do diff --git a/lib/parrhesia/api/sync.ex b/lib/parrhesia/api/sync.ex index 3820a95..216072c 100644 --- a/lib/parrhesia/api/sync.ex +++ b/lib/parrhesia/api/sync.ex @@ -1,12 +1,45 @@ defmodule Parrhesia.API.Sync do @moduledoc """ Sync server control-plane API. + + This module manages outbound relay sync definitions and exposes runtime status for each + configured sync worker. + + The main entrypoint is `put_server/2`. Accepted server maps are normalized into a stable + internal shape and persisted by the sync manager. The expected input shape is: + + ```elixir + %{ + "id" => "tribes-primary", + "url" => "wss://relay-a.example/relay", + "enabled?" => true, + "auth_pubkey" => "...64 hex chars...", + "filters" => [%{"kinds" => [5000]}], + "mode" => "req_stream", + "overlap_window_seconds" => 300, + "auth" => %{"type" => "nip42"}, + "tls" => %{ + "mode" => "required", + "hostname" => "relay-a.example", + "pins" => [%{"type" => "spki_sha256", "value" => "..."}] + }, + "metadata" => %{} + } + ``` + + Most functions accept `:manager` or `:name` in `opts` to target a non-default manager. """ alias Parrhesia.API.Sync.Manager + @typedoc """ + Normalized sync server configuration returned by the sync manager. + """ @type server :: map() + @doc """ + Creates or replaces a sync server definition. + """ @spec put_server(map(), keyword()) :: {:ok, server()} | {:error, term()} def put_server(server, opts \\ []) @@ -16,6 +49,9 @@ defmodule Parrhesia.API.Sync do def put_server(_server, _opts), do: {:error, :invalid_server} + @doc """ + Removes a stored sync server definition and stops its worker if it is running. + """ @spec remove_server(String.t(), keyword()) :: :ok | {:error, term()} def remove_server(server_id, opts \\ []) @@ -25,6 +61,11 @@ defmodule Parrhesia.API.Sync do def remove_server(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Fetches a single normalized sync server definition. + + Returns `:error` when the server id is unknown. + """ @spec get_server(String.t(), keyword()) :: {:ok, server()} | :error | {:error, term()} def get_server(server_id, opts \\ []) @@ -34,11 +75,17 @@ defmodule Parrhesia.API.Sync do def get_server(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Lists all configured sync servers, including their runtime state. + """ @spec list_servers(keyword()) :: {:ok, [server()]} | {:error, term()} def list_servers(opts \\ []) when is_list(opts) do Manager.list_servers(manager_name(opts)) end + @doc """ + Marks a sync server as running and reconciles its worker state. + """ @spec start_server(String.t(), keyword()) :: :ok | {:error, term()} def start_server(server_id, opts \\ []) @@ -48,6 +95,9 @@ defmodule Parrhesia.API.Sync do def start_server(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Stops a sync server and records a disconnect timestamp in runtime state. + """ @spec stop_server(String.t(), keyword()) :: :ok | {:error, term()} def stop_server(server_id, opts \\ []) @@ -57,6 +107,9 @@ defmodule Parrhesia.API.Sync do def stop_server(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Triggers an immediate sync run for a server. + """ @spec sync_now(String.t(), keyword()) :: :ok | {:error, term()} def sync_now(server_id, opts \\ []) @@ -66,6 +119,11 @@ defmodule Parrhesia.API.Sync do def sync_now(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Returns runtime counters and timestamps for a single sync server. + + Returns `:error` when the server id is unknown. + """ @spec server_stats(String.t(), keyword()) :: {:ok, map()} | :error | {:error, term()} def server_stats(server_id, opts \\ []) @@ -75,16 +133,25 @@ defmodule Parrhesia.API.Sync do def server_stats(_server_id, _opts), do: {:error, :invalid_server_id} + @doc """ + Returns aggregate counters across all configured sync servers. + """ @spec sync_stats(keyword()) :: {:ok, map()} | {:error, term()} def sync_stats(opts \\ []) when is_list(opts) do Manager.sync_stats(manager_name(opts)) end + @doc """ + Returns a health summary for the sync subsystem. + """ @spec sync_health(keyword()) :: {:ok, map()} | {:error, term()} def sync_health(opts \\ []) when is_list(opts) do Manager.sync_health(manager_name(opts)) end + @doc """ + Returns the default filesystem path for persisted sync server state. + """ def default_path do Path.join([default_data_dir(), "sync_servers.json"]) end diff --git a/lib/parrhesia/config.ex b/lib/parrhesia/config.ex index b536aa3..7fc7cee 100644 --- a/lib/parrhesia/config.ex +++ b/lib/parrhesia/config.ex @@ -1,6 +1,9 @@ defmodule Parrhesia.Config do @moduledoc """ Runtime configuration cache backed by ETS. + + The application environment is copied into ETS at startup so hot-path reads do not need to + traverse the application environment repeatedly. """ use GenServer @@ -8,6 +11,9 @@ defmodule Parrhesia.Config do @table __MODULE__ @root_key :config + @doc """ + Starts the config cache server. + """ def start_link(init_arg \\ []) do GenServer.start_link(__MODULE__, init_arg, name: __MODULE__) end @@ -26,6 +32,9 @@ defmodule Parrhesia.Config do {:ok, %{}} end + @doc """ + Returns the cached top-level Parrhesia application config. + """ @spec all() :: map() | keyword() def all do case :ets.lookup(@table, @root_key) do @@ -34,6 +43,11 @@ defmodule Parrhesia.Config do end end + @doc """ + Reads a nested config value by path. + + The path may traverse maps or keyword lists. Missing paths return `default`. + """ @spec get([atom()], term()) :: term() def get(path, default \\ nil) when is_list(path) do case fetch(path) do diff --git a/lib/parrhesia/protocol.ex b/lib/parrhesia/protocol.ex index 4e9efdf..711bf1e 100644 --- a/lib/parrhesia/protocol.ex +++ b/lib/parrhesia/protocol.ex @@ -1,6 +1,15 @@ defmodule Parrhesia.Protocol do @moduledoc """ Nostr protocol message decode/encode helpers. + + This module is transport-oriented: it turns websocket payloads into structured tuples and + back again. + + For programmatic API calls inside the application, prefer the `Parrhesia.API.*` modules. + In particular: + + - `validate_event/1` returns user-facing error strings + - `Parrhesia.API.Auth.validate_event/1` returns machine-friendly validator atoms """ alias Parrhesia.Protocol.EventValidator @@ -41,6 +50,9 @@ defmodule Parrhesia.Protocol do @count_options_keys MapSet.new(["hll", "approximate"]) + @doc """ + Decodes a client websocket payload into a structured protocol tuple. + """ @spec decode_client(binary()) :: {:ok, client_message()} | {:error, decode_error()} def decode_client(payload) when is_binary(payload) do with {:ok, decoded} <- decode_json(payload) do @@ -48,6 +60,9 @@ defmodule Parrhesia.Protocol do end end + @doc """ + Validates an event and returns relay-facing error strings. + """ @spec validate_event(event()) :: :ok | {:error, String.t()} def validate_event(event) do case EventValidator.validate(event) do @@ -56,6 +71,9 @@ defmodule Parrhesia.Protocol do end end + @doc """ + Encodes a relay message tuple into the JSON frame sent to clients. + """ @spec encode_relay(relay_message()) :: binary() def encode_relay(message) do message @@ -63,6 +81,9 @@ defmodule Parrhesia.Protocol do |> JSON.encode!() end + @doc """ + Converts a decode error into the relay notice string that should be sent to a client. + """ @spec decode_error_notice(decode_error()) :: String.t() def decode_error_notice(reason) do case reason do diff --git a/lib/parrhesia/storage.ex b/lib/parrhesia/storage.ex index 34e2656..7156217 100644 --- a/lib/parrhesia/storage.ex +++ b/lib/parrhesia/storage.ex @@ -4,6 +4,9 @@ defmodule Parrhesia.Storage do Domain/runtime code should resolve behavior modules through this module instead of depending on concrete adapter implementations directly. + + Each accessor validates that the configured module is loaded and declares the expected + behaviour before returning it. """ @default_modules [ @@ -14,18 +17,33 @@ defmodule Parrhesia.Storage do admin: Parrhesia.Storage.Adapters.Postgres.Admin ] + @doc """ + Returns the configured events storage module. + """ @spec events() :: module() def events, do: fetch_module!(:events, Parrhesia.Storage.Events) + @doc """ + Returns the configured moderation storage module. + """ @spec moderation() :: module() def moderation, do: fetch_module!(:moderation, Parrhesia.Storage.Moderation) + @doc """ + Returns the configured ACL storage module. + """ @spec acl() :: module() def acl, do: fetch_module!(:acl, Parrhesia.Storage.ACL) + @doc """ + Returns the configured groups storage module. + """ @spec groups() :: module() def groups, do: fetch_module!(:groups, Parrhesia.Storage.Groups) + @doc """ + Returns the configured admin storage module. + """ @spec admin() :: module() def admin, do: fetch_module!(:admin, Parrhesia.Storage.Admin)