Improve public API documentation

This commit is contained in:
2026-03-18 18:08:47 +01:00
parent 9014912e9d
commit 2225dfdc9e
13 changed files with 417 additions and 1 deletions

View File

@@ -1,12 +1,37 @@
defmodule Parrhesia.API.ACL do defmodule Parrhesia.API.ACL do
@moduledoc """ @moduledoc """
Public ACL API and rule matching for protected sync traffic. 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.API.RequestContext
alias Parrhesia.Protocol.Filter alias Parrhesia.Protocol.Filter
alias Parrhesia.Storage 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()} @spec grant(map(), keyword()) :: :ok | {:error, term()}
def grant(rule, _opts \\ []) do def grant(rule, _opts \\ []) do
with {:ok, _stored_rule} <- Storage.acl().put_rule(%{}, normalize_rule(rule)) do with {:ok, _stored_rule} <- Storage.acl().put_rule(%{}, normalize_rule(rule)) do
@@ -14,16 +39,39 @@ defmodule Parrhesia.API.ACL do
end end
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()} @spec revoke(map(), keyword()) :: :ok | {:error, term()}
def revoke(rule, _opts \\ []) do def revoke(rule, _opts \\ []) do
Storage.acl().delete_rule(%{}, normalize_delete_selector(rule)) Storage.acl().delete_rule(%{}, normalize_delete_selector(rule))
end end
@doc """
Lists persisted ACL rules.
Supported filters are:
- `:principal_type`
- `:principal`
- `:capability`
"""
@spec list(keyword()) :: {:ok, [map()]} | {:error, term()} @spec list(keyword()) :: {:ok, [map()]} | {:error, term()}
def list(opts \\ []) do def list(opts \\ []) do
Storage.acl().list_rules(%{}, normalize_list_opts(opts)) Storage.acl().list_rules(%{}, normalize_list_opts(opts))
end 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()} @spec check(atom(), map(), keyword()) :: :ok | {:error, term()}
def check(capability, subject, opts \\ []) def check(capability, subject, opts \\ [])
@@ -44,6 +92,9 @@ defmodule Parrhesia.API.ACL do
def check(_capability, _subject, _opts), do: {:error, :invalid_acl_capability} 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() @spec protected_read?(map()) :: boolean()
def protected_read?(filter) when is_map(filter) do def protected_read?(filter) when is_map(filter) do
case protected_filters() do case protected_filters() do
@@ -57,6 +108,9 @@ defmodule Parrhesia.API.ACL do
def protected_read?(_filter), do: false def protected_read?(_filter), do: false
@doc """
Returns `true` when an event matches the configured protected write surface.
"""
@spec protected_write?(map()) :: boolean() @spec protected_write?(map()) :: boolean()
def protected_write?(event) when is_map(event) do def protected_write?(event) when is_map(event) do
case protected_filters() do case protected_filters() do

View File

@@ -1,6 +1,14 @@
defmodule Parrhesia.API.Admin do defmodule Parrhesia.API.Admin do
@moduledoc """ @moduledoc """
Public management API facade. 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 alias Parrhesia.API.ACL
@@ -26,6 +34,22 @@ defmodule Parrhesia.API.Admin do
sync_sync_now 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()} @spec execute(String.t() | atom(), map(), keyword()) :: {:ok, map()} | {:error, term()}
def execute(method, params, opts \\ []) def execute(method, params, opts \\ [])
@@ -41,6 +65,9 @@ defmodule Parrhesia.API.Admin do
def execute(method, _params, _opts), def execute(method, _params, _opts),
do: {:error, {:unsupported_method, normalize_method_name(method)}} do: {:error, {:unsupported_method, normalize_method_name(method)}}
@doc """
Returns aggregate relay stats plus nested sync stats.
"""
@spec stats(keyword()) :: {:ok, map()} | {:error, term()} @spec stats(keyword()) :: {:ok, map()} | {:error, term()}
def stats(opts \\ []) do def stats(opts \\ []) do
with {:ok, relay_stats} <- relay_stats(), with {:ok, relay_stats} <- relay_stats(),
@@ -49,6 +76,12 @@ defmodule Parrhesia.API.Admin do
end end
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()} @spec health(keyword()) :: {:ok, map()} | {:error, term()}
def health(opts \\ []) do def health(opts \\ []) do
with {:ok, sync_health} <- Sync.sync_health(opts) do with {:ok, sync_health} <- Sync.sync_health(opts) do
@@ -60,6 +93,12 @@ defmodule Parrhesia.API.Admin do
end end
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()} @spec list_audit_logs(keyword()) :: {:ok, [map()]} | {:error, term()}
def list_audit_logs(opts \\ []) do def list_audit_logs(opts \\ []) do
Storage.admin().list_audit_logs(%{}, opts) Storage.admin().list_audit_logs(%{}, opts)

View File

@@ -1,6 +1,15 @@
defmodule Parrhesia.API.Auth do defmodule Parrhesia.API.Auth do
@moduledoc """ @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 alias Parrhesia.API.Auth.Context
@@ -8,18 +17,46 @@ defmodule Parrhesia.API.Auth do
alias Parrhesia.Auth.Nip98 alias Parrhesia.Auth.Nip98
alias Parrhesia.Protocol.EventValidator 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()} @spec validate_event(map()) :: :ok | {:error, term()}
def validate_event(event), do: EventValidator.validate(event) 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() @spec compute_event_id(map()) :: String.t()
def compute_event_id(event), do: EventValidator.compute_id(event) 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()) :: @spec validate_nip98(String.t() | nil, String.t(), String.t()) ::
{:ok, Context.t()} | {:error, term()} {:ok, Context.t()} | {:error, term()}
def validate_nip98(authorization, method, url) do def validate_nip98(authorization, method, url) do
validate_nip98(authorization, method, url, []) validate_nip98(authorization, method, url, [])
end 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()) :: @spec validate_nip98(String.t() | nil, String.t(), String.t(), keyword()) ::
{:ok, Context.t()} | {:error, term()} {:ok, Context.t()} | {:error, term()}
def validate_nip98(authorization, method, url, opts) def validate_nip98(authorization, method, url, opts)

View File

@@ -1,6 +1,10 @@
defmodule Parrhesia.API.Auth.Context do defmodule Parrhesia.API.Auth.Context do
@moduledoc """ @moduledoc """
Authenticated request details returned by shared auth helpers. 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 alias Parrhesia.API.RequestContext

View File

@@ -1,6 +1,17 @@
defmodule Parrhesia.API.Events do defmodule Parrhesia.API.Events do
@moduledoc """ @moduledoc """
Canonical event publish, query, and count API. 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 alias Parrhesia.API.Events.PublishResult
@@ -29,6 +40,24 @@ defmodule Parrhesia.API.Events do
449 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()} @spec publish(map(), keyword()) :: {:ok, PublishResult.t()} | {:error, term()}
def publish(event, opts \\ []) def publish(event, opts \\ [])
@@ -87,6 +116,22 @@ defmodule Parrhesia.API.Events do
def publish(_event, _opts), do: {:error, :invalid_event} 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()} @spec query([map()], keyword()) :: {:ok, [map()]} | {:error, term()}
def query(filters, opts \\ []) def query(filters, opts \\ [])
@@ -118,6 +163,22 @@ defmodule Parrhesia.API.Events do
def query(_filters, _opts), do: {:error, :invalid_filters} 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()} @spec count([map()], keyword()) :: {:ok, non_neg_integer() | map()} | {:error, term()}
def count(filters, opts \\ []) def count(filters, opts \\ [])

View File

@@ -1,6 +1,14 @@
defmodule Parrhesia.API.Events.PublishResult do defmodule Parrhesia.API.Events.PublishResult do
@moduledoc """ @moduledoc """
Result shape for event publish attempts. 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] defstruct [:event_id, :accepted, :message, :reason]

View File

@@ -1,15 +1,40 @@
defmodule Parrhesia.API.Identity do defmodule Parrhesia.API.Identity do
@moduledoc """ @moduledoc """
Server-auth identity management. 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 alias Parrhesia.API.Auth
@typedoc """
Public identity metadata returned to callers.
"""
@type identity_metadata :: %{ @type identity_metadata :: %{
pubkey: String.t(), pubkey: String.t(),
source: :configured | :persisted | :generated | :imported 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()} @spec get(keyword()) :: {:ok, identity_metadata()} | {:error, term()}
def get(opts \\ []) do def get(opts \\ []) do
with {:ok, identity} <- fetch_existing_identity(opts) do with {:ok, identity} <- fetch_existing_identity(opts) do
@@ -17,6 +42,9 @@ defmodule Parrhesia.API.Identity do
end end
end end
@doc """
Returns the current identity, generating and persisting one when necessary.
"""
@spec ensure(keyword()) :: {:ok, identity_metadata()} | {:error, term()} @spec ensure(keyword()) :: {:ok, identity_metadata()} | {:error, term()}
def ensure(opts \\ []) do def ensure(opts \\ []) do
with {:ok, identity} <- ensure_identity(opts) do with {:ok, identity} <- ensure_identity(opts) do
@@ -24,6 +52,12 @@ defmodule Parrhesia.API.Identity do
end end
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()} @spec import(map(), keyword()) :: {:ok, identity_metadata()} | {:error, term()}
def import(identity, opts \\ []) def import(identity, opts \\ [])
@@ -37,6 +71,12 @@ defmodule Parrhesia.API.Identity do
def import(_identity, _opts), do: {:error, :invalid_identity} 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()} @spec rotate(keyword()) :: {:ok, identity_metadata()} | {:error, term()}
def rotate(opts \\ []) do def rotate(opts \\ []) do
with :ok <- ensure_rotation_allowed(opts), with :ok <- ensure_rotation_allowed(opts),
@@ -46,6 +86,18 @@ defmodule Parrhesia.API.Identity do
end end
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()} @spec sign_event(map(), keyword()) :: {:ok, map()} | {:error, term()}
def sign_event(event, opts \\ []) def sign_event(event, opts \\ [])
@@ -59,6 +111,9 @@ defmodule Parrhesia.API.Identity do
def sign_event(_event, _opts), do: {:error, :invalid_event} def sign_event(_event, _opts), do: {:error, :invalid_event}
@doc """
Returns the default filesystem path for the persisted server identity.
"""
def default_path do def default_path do
Path.join([default_data_dir(), "server_identity.json"]) Path.join([default_data_dir(), "server_identity.json"])
end end

View File

@@ -1,6 +1,15 @@
defmodule Parrhesia.API.RequestContext do defmodule Parrhesia.API.RequestContext do
@moduledoc """ @moduledoc """
Shared request context used across API and policy surfaces. 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(), defstruct authenticated_pubkeys: MapSet.new(),
@@ -23,6 +32,11 @@ defmodule Parrhesia.API.RequestContext do
metadata: map() metadata: map()
} }
@doc """
Merges arbitrary metadata into the context.
Existing keys are overwritten by the incoming map.
"""
@spec put_metadata(t(), map()) :: t() @spec put_metadata(t(), map()) :: t()
def put_metadata(%__MODULE__{} = context, metadata) when is_map(metadata) do def put_metadata(%__MODULE__{} = context, metadata) when is_map(metadata) do
%__MODULE__{context | metadata: Map.merge(context.metadata, metadata)} %__MODULE__{context | metadata: Map.merge(context.metadata, metadata)}

View File

@@ -1,6 +1,15 @@
defmodule Parrhesia.API.Stream do defmodule Parrhesia.API.Stream do
@moduledoc """ @moduledoc """
In-process subscription API with relay-equivalent catch-up and live fanout semantics. 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 alias Parrhesia.API.Events
@@ -9,6 +18,16 @@ defmodule Parrhesia.API.Stream do
alias Parrhesia.Policy.EventPolicy alias Parrhesia.Policy.EventPolicy
alias Parrhesia.Protocol.Filter 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()} @spec subscribe(pid(), String.t(), [map()], keyword()) :: {:ok, reference()} | {:error, term()}
def subscribe(subscriber, subscription_id, filters, opts \\ []) def subscribe(subscriber, subscription_id, filters, opts \\ [])
@@ -42,6 +61,11 @@ defmodule Parrhesia.API.Stream do
def subscribe(_subscriber, _subscription_id, _filters, _opts), def subscribe(_subscriber, _subscription_id, _filters, _opts),
do: {:error, :invalid_subscription} 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 @spec unsubscribe(reference()) :: :ok
def unsubscribe(ref) when is_reference(ref) do def unsubscribe(ref) when is_reference(ref) do
case Registry.lookup(Parrhesia.API.Stream.Registry, ref) do case Registry.lookup(Parrhesia.API.Stream.Registry, ref) do

View File

@@ -1,12 +1,45 @@
defmodule Parrhesia.API.Sync do defmodule Parrhesia.API.Sync do
@moduledoc """ @moduledoc """
Sync server control-plane API. 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 alias Parrhesia.API.Sync.Manager
@typedoc """
Normalized sync server configuration returned by the sync manager.
"""
@type server :: map() @type server :: map()
@doc """
Creates or replaces a sync server definition.
"""
@spec put_server(map(), keyword()) :: {:ok, server()} | {:error, term()} @spec put_server(map(), keyword()) :: {:ok, server()} | {:error, term()}
def put_server(server, opts \\ []) def put_server(server, opts \\ [])
@@ -16,6 +49,9 @@ defmodule Parrhesia.API.Sync do
def put_server(_server, _opts), do: {:error, :invalid_server} 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()} @spec remove_server(String.t(), keyword()) :: :ok | {:error, term()}
def remove_server(server_id, opts \\ []) 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} 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()} @spec get_server(String.t(), keyword()) :: {:ok, server()} | :error | {:error, term()}
def get_server(server_id, opts \\ []) 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} 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()} @spec list_servers(keyword()) :: {:ok, [server()]} | {:error, term()}
def list_servers(opts \\ []) when is_list(opts) do def list_servers(opts \\ []) when is_list(opts) do
Manager.list_servers(manager_name(opts)) Manager.list_servers(manager_name(opts))
end end
@doc """
Marks a sync server as running and reconciles its worker state.
"""
@spec start_server(String.t(), keyword()) :: :ok | {:error, term()} @spec start_server(String.t(), keyword()) :: :ok | {:error, term()}
def start_server(server_id, opts \\ []) 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} 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()} @spec stop_server(String.t(), keyword()) :: :ok | {:error, term()}
def stop_server(server_id, opts \\ []) 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} 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()} @spec sync_now(String.t(), keyword()) :: :ok | {:error, term()}
def sync_now(server_id, opts \\ []) 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} 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()} @spec server_stats(String.t(), keyword()) :: {:ok, map()} | :error | {:error, term()}
def server_stats(server_id, opts \\ []) 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} 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()} @spec sync_stats(keyword()) :: {:ok, map()} | {:error, term()}
def sync_stats(opts \\ []) when is_list(opts) do def sync_stats(opts \\ []) when is_list(opts) do
Manager.sync_stats(manager_name(opts)) Manager.sync_stats(manager_name(opts))
end end
@doc """
Returns a health summary for the sync subsystem.
"""
@spec sync_health(keyword()) :: {:ok, map()} | {:error, term()} @spec sync_health(keyword()) :: {:ok, map()} | {:error, term()}
def sync_health(opts \\ []) when is_list(opts) do def sync_health(opts \\ []) when is_list(opts) do
Manager.sync_health(manager_name(opts)) Manager.sync_health(manager_name(opts))
end end
@doc """
Returns the default filesystem path for persisted sync server state.
"""
def default_path do def default_path do
Path.join([default_data_dir(), "sync_servers.json"]) Path.join([default_data_dir(), "sync_servers.json"])
end end

View File

@@ -1,6 +1,9 @@
defmodule Parrhesia.Config do defmodule Parrhesia.Config do
@moduledoc """ @moduledoc """
Runtime configuration cache backed by ETS. 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 use GenServer
@@ -8,6 +11,9 @@ defmodule Parrhesia.Config do
@table __MODULE__ @table __MODULE__
@root_key :config @root_key :config
@doc """
Starts the config cache server.
"""
def start_link(init_arg \\ []) do def start_link(init_arg \\ []) do
GenServer.start_link(__MODULE__, init_arg, name: __MODULE__) GenServer.start_link(__MODULE__, init_arg, name: __MODULE__)
end end
@@ -26,6 +32,9 @@ defmodule Parrhesia.Config do
{:ok, %{}} {:ok, %{}}
end end
@doc """
Returns the cached top-level Parrhesia application config.
"""
@spec all() :: map() | keyword() @spec all() :: map() | keyword()
def all do def all do
case :ets.lookup(@table, @root_key) do case :ets.lookup(@table, @root_key) do
@@ -34,6 +43,11 @@ defmodule Parrhesia.Config do
end end
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() @spec get([atom()], term()) :: term()
def get(path, default \\ nil) when is_list(path) do def get(path, default \\ nil) when is_list(path) do
case fetch(path) do case fetch(path) do

View File

@@ -1,6 +1,15 @@
defmodule Parrhesia.Protocol do defmodule Parrhesia.Protocol do
@moduledoc """ @moduledoc """
Nostr protocol message decode/encode helpers. 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 alias Parrhesia.Protocol.EventValidator
@@ -41,6 +50,9 @@ defmodule Parrhesia.Protocol do
@count_options_keys MapSet.new(["hll", "approximate"]) @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()} @spec decode_client(binary()) :: {:ok, client_message()} | {:error, decode_error()}
def decode_client(payload) when is_binary(payload) do def decode_client(payload) when is_binary(payload) do
with {:ok, decoded} <- decode_json(payload) do with {:ok, decoded} <- decode_json(payload) do
@@ -48,6 +60,9 @@ defmodule Parrhesia.Protocol do
end end
end end
@doc """
Validates an event and returns relay-facing error strings.
"""
@spec validate_event(event()) :: :ok | {:error, String.t()} @spec validate_event(event()) :: :ok | {:error, String.t()}
def validate_event(event) do def validate_event(event) do
case EventValidator.validate(event) do case EventValidator.validate(event) do
@@ -56,6 +71,9 @@ defmodule Parrhesia.Protocol do
end end
end end
@doc """
Encodes a relay message tuple into the JSON frame sent to clients.
"""
@spec encode_relay(relay_message()) :: binary() @spec encode_relay(relay_message()) :: binary()
def encode_relay(message) do def encode_relay(message) do
message message
@@ -63,6 +81,9 @@ defmodule Parrhesia.Protocol do
|> JSON.encode!() |> JSON.encode!()
end 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() @spec decode_error_notice(decode_error()) :: String.t()
def decode_error_notice(reason) do def decode_error_notice(reason) do
case reason do case reason do

View File

@@ -4,6 +4,9 @@ defmodule Parrhesia.Storage do
Domain/runtime code should resolve behavior modules through this module instead of Domain/runtime code should resolve behavior modules through this module instead of
depending on concrete adapter implementations directly. 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 [ @default_modules [
@@ -14,18 +17,33 @@ defmodule Parrhesia.Storage do
admin: Parrhesia.Storage.Adapters.Postgres.Admin admin: Parrhesia.Storage.Adapters.Postgres.Admin
] ]
@doc """
Returns the configured events storage module.
"""
@spec events() :: module() @spec events() :: module()
def events, do: fetch_module!(:events, Parrhesia.Storage.Events) def events, do: fetch_module!(:events, Parrhesia.Storage.Events)
@doc """
Returns the configured moderation storage module.
"""
@spec moderation() :: module() @spec moderation() :: module()
def moderation, do: fetch_module!(:moderation, Parrhesia.Storage.Moderation) def moderation, do: fetch_module!(:moderation, Parrhesia.Storage.Moderation)
@doc """
Returns the configured ACL storage module.
"""
@spec acl() :: module() @spec acl() :: module()
def acl, do: fetch_module!(:acl, Parrhesia.Storage.ACL) def acl, do: fetch_module!(:acl, Parrhesia.Storage.ACL)
@doc """
Returns the configured groups storage module.
"""
@spec groups() :: module() @spec groups() :: module()
def groups, do: fetch_module!(:groups, Parrhesia.Storage.Groups) def groups, do: fetch_module!(:groups, Parrhesia.Storage.Groups)
@doc """
Returns the configured admin storage module.
"""
@spec admin() :: module() @spec admin() :: module()
def admin, do: fetch_module!(:admin, Parrhesia.Storage.Admin) def admin, do: fetch_module!(:admin, Parrhesia.Storage.Admin)