Improve public API documentation
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 \\ [])
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user