Document embedded API surface
This commit is contained in:
20
docs/ARCH.md
20
docs/ARCH.md
@@ -82,16 +82,20 @@ Configured WS/HTTP Listeners (Bandit/Plug)
|
||||
|
||||
## 4) OTP supervision design
|
||||
|
||||
`Parrhesia.Application` children (top-level):
|
||||
`Parrhesia.Runtime` children (top-level):
|
||||
|
||||
1. `Parrhesia.Telemetry` – metric definitions/reporters
|
||||
2. `Parrhesia.Config` – runtime config cache (ETS-backed)
|
||||
3. `Parrhesia.Storage.Supervisor` – adapter processes (`Repo`, pools)
|
||||
4. `Parrhesia.Subscriptions.Supervisor` – subscription index + fanout workers
|
||||
5. `Parrhesia.Auth.Supervisor` – AUTH challenge/session tracking
|
||||
6. `Parrhesia.Policy.Supervisor` – rate limiters / ACL caches
|
||||
7. `Parrhesia.Web.Endpoint` – supervises configured WS + HTTP listeners
|
||||
8. `Parrhesia.Tasks.Supervisor` – background jobs (expiry purge, maintenance)
|
||||
2. `Parrhesia.ConnectionStats` – per-listener connection/subscription counters
|
||||
3. `Parrhesia.Config` – runtime config cache (ETS-backed)
|
||||
4. `Parrhesia.Web.EventIngestLimiter` – relay-wide event ingest rate limiter
|
||||
5. `Parrhesia.Web.IPEventIngestLimiter` – per-IP event ingest rate limiter
|
||||
6. `Parrhesia.Storage.Supervisor` – adapter processes (`Repo`, pools)
|
||||
7. `Parrhesia.Subscriptions.Supervisor` – subscription index + fanout workers
|
||||
8. `Parrhesia.Auth.Supervisor` – AUTH challenge/session tracking
|
||||
9. `Parrhesia.Sync.Supervisor` – outbound relay sync workers
|
||||
10. `Parrhesia.Policy.Supervisor` – rate limiters / ACL caches
|
||||
11. `Parrhesia.Web.Endpoint` – supervises configured WS + HTTP listeners
|
||||
12. `Parrhesia.Tasks.Supervisor` – background jobs (expiry purge, maintenance)
|
||||
|
||||
Failure model:
|
||||
|
||||
|
||||
140
docs/KHATRU.md
140
docs/KHATRU.md
@@ -1,140 +0,0 @@
|
||||
# Khatru-Inspired Runtime Improvements
|
||||
|
||||
This document collects refactoring and extension ideas learned from studying Khatru-style relay design.
|
||||
|
||||
It is intentionally **not** about the new public API surface or the sync ACL model. Those live in `docs/slop/LOCAL_API.md` and `docs/SYNC.md`.
|
||||
|
||||
The focus here is runtime shape, protocol behavior, and operator-visible relay features.
|
||||
|
||||
---
|
||||
|
||||
## 1. Why This Matters
|
||||
|
||||
Khatru appears mature mainly because it exposes clearer relay pipeline stages.
|
||||
|
||||
That gives three practical benefits:
|
||||
|
||||
- less policy drift between storage, websocket, and management code,
|
||||
- easier feature addition without hard-coding more branches into one connection module,
|
||||
- better composability for relay profiles with different trust and traffic models.
|
||||
|
||||
Parrhesia should borrow that clarity without copying Khatru's code-first hook model wholesale.
|
||||
|
||||
---
|
||||
|
||||
## 2. Proposed Runtime Refactors
|
||||
|
||||
### 2.1 Staged policy pipeline
|
||||
|
||||
Parrhesia should stop treating policy as one coarse `EventPolicy` module plus scattered special cases.
|
||||
|
||||
Recommended internal stages:
|
||||
|
||||
1. connection admission
|
||||
2. authentication challenge and validation
|
||||
3. publish/write authorization
|
||||
4. query/count authorization
|
||||
5. stream subscription authorization
|
||||
6. negentropy authorization
|
||||
7. response shaping
|
||||
8. broadcast/fanout suppression
|
||||
|
||||
This is an internal runtime refactor. It does not imply a new public API.
|
||||
|
||||
### 2.2 Richer internal request context
|
||||
|
||||
The runtime should carry a structured request context through all stages.
|
||||
|
||||
Useful fields:
|
||||
|
||||
- authenticated pubkeys
|
||||
- caller kind
|
||||
- remote IP
|
||||
- subscription id
|
||||
- peer id
|
||||
- negentropy session flag
|
||||
- internal-call flag
|
||||
|
||||
This reduces ad-hoc branching and makes audit/telemetry more coherent.
|
||||
|
||||
### 2.3 Separate policy from storage presence tables
|
||||
|
||||
Moderation state should remain data.
|
||||
|
||||
Runtime enforcement should be a first-class layer that consumes that data, not a side effect of whether a table exists.
|
||||
|
||||
This is especially important for:
|
||||
|
||||
- blocked IP enforcement,
|
||||
- pubkey allowlists,
|
||||
- future kind- or tag-scoped restrictions.
|
||||
|
||||
---
|
||||
|
||||
## 3. Protocol and Relay Features
|
||||
|
||||
### 3.1 Real COUNT sketches
|
||||
|
||||
Parrhesia currently returns a synthetic `hll` payload for NIP-45-style count responses.
|
||||
|
||||
If approximate count exchange matters, implement a real reusable HLL sketch path instead of hashing `filters + count`.
|
||||
|
||||
### 3.2 Relay identity in NIP-11
|
||||
|
||||
Once Parrhesia owns a stable server identity, NIP-11 should expose the relay pubkey instead of returning `nil`.
|
||||
|
||||
This is useful beyond sync:
|
||||
|
||||
- operator visibility,
|
||||
- relay fingerprinting,
|
||||
- future trust tooling.
|
||||
|
||||
### 3.3 Connection-level IP enforcement
|
||||
|
||||
Blocked IP support should be enforced on actual connection admission, not only stored in management tables.
|
||||
|
||||
This should happen early, before expensive protocol handling.
|
||||
|
||||
### 3.4 Better response shaping
|
||||
|
||||
Introduce a narrow internal response shaping layer for cases where returned events or counts need controlled rewriting or suppression.
|
||||
|
||||
Examples:
|
||||
|
||||
- hide fields for specific relay profiles,
|
||||
- suppress rebroadcast of locally-ingested remote sync traffic,
|
||||
- shape relay notices consistently.
|
||||
|
||||
This should stay narrow and deterministic. It should not become arbitrary app semantics.
|
||||
|
||||
---
|
||||
|
||||
## 4. Suggested Extension Points
|
||||
|
||||
These should be internal runtime seams, not necessarily public interfaces:
|
||||
|
||||
- `ConnectionPolicy`
|
||||
- `AuthPolicy`
|
||||
- `ReadPolicy`
|
||||
- `WritePolicy`
|
||||
- `NegentropyPolicy`
|
||||
- `ResponsePolicy`
|
||||
- `BroadcastPolicy`
|
||||
|
||||
They may initially be plain modules with well-defined callbacks or functions.
|
||||
|
||||
The point is not pluggability for its own sake. The point is to make policy stages explicit and testable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Near-Term Priority
|
||||
|
||||
Recommended order:
|
||||
|
||||
1. enforce blocked IPs and any future connection-gating on the real connection path
|
||||
2. split the current websocket flow into explicit read/write/negentropy policy stages
|
||||
3. enrich runtime request context and telemetry metadata
|
||||
4. expose relay pubkey in NIP-11 once identity lands
|
||||
5. replace fake HLL payloads with a real approximate-count implementation if NIP-45 support matters operationally
|
||||
|
||||
This keeps the runtime improvements incremental and independent from the ongoing API and ACL implementation.
|
||||
147
docs/LOCAL_API.md
Normal file
147
docs/LOCAL_API.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# Parrhesia Local API
|
||||
|
||||
Parrhesia can run as a normal standalone relay application, but it also exposes a stable
|
||||
in-process API for Elixir callers that want to embed the relay inside a larger OTP system.
|
||||
|
||||
This document describes that embedding surface. The runtime is still alpha, so treat the API
|
||||
as usable but not yet frozen.
|
||||
|
||||
## What embedding means today
|
||||
|
||||
Embedding currently means:
|
||||
|
||||
- the host app adds `:parrhesia` as a dependency and OTP application
|
||||
- the host app provides `config :parrhesia, ...` explicitly
|
||||
- the host app migrates the Parrhesia database schema
|
||||
- callers interact with the relay through `Parrhesia.API.*`
|
||||
|
||||
Current operational assumptions:
|
||||
|
||||
- Parrhesia runs one runtime per BEAM node
|
||||
- core processes use global module names such as `Parrhesia.Config` and `Parrhesia.Web.Endpoint`
|
||||
- the config defaults in this repo's `config/*.exs` are not imported automatically by a host app
|
||||
|
||||
If you want multiple isolated relay instances inside one VM, Parrhesia does not support that
|
||||
cleanly yet.
|
||||
|
||||
## Minimal host setup
|
||||
|
||||
Add the dependency in your host app:
|
||||
|
||||
```elixir
|
||||
defp deps do
|
||||
[
|
||||
{:parrhesia, path: "../parrhesia"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Configure the runtime in your host app. At minimum you should carry over:
|
||||
|
||||
```elixir
|
||||
import Config
|
||||
|
||||
config :postgrex, :json_library, JSON
|
||||
|
||||
config :parrhesia,
|
||||
relay_url: "wss://relay.example.com/relay",
|
||||
listeners: %{},
|
||||
storage: [backend: :postgres]
|
||||
|
||||
config :parrhesia, Parrhesia.Repo,
|
||||
url: System.fetch_env!("DATABASE_URL"),
|
||||
pool_size: 10,
|
||||
types: Parrhesia.PostgresTypes
|
||||
|
||||
config :parrhesia, Parrhesia.ReadRepo,
|
||||
url: System.fetch_env!("DATABASE_URL"),
|
||||
pool_size: 10,
|
||||
types: Parrhesia.PostgresTypes
|
||||
|
||||
config :parrhesia, ecto_repos: [Parrhesia.Repo]
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Set `listeners: %{}` if you only want the in-process API and no HTTP/WebSocket ingress.
|
||||
- If you do want ingress, copy the listener shape from the config reference in
|
||||
[README.md](../README.md).
|
||||
- Production runtime overrides still use the `PARRHESIA_*` environment variables described in
|
||||
[README.md](../README.md).
|
||||
|
||||
Migrate before serving traffic:
|
||||
|
||||
```elixir
|
||||
Parrhesia.Release.migrate()
|
||||
```
|
||||
|
||||
In development, `mix ecto.migrate -r Parrhesia.Repo` works too.
|
||||
|
||||
## Starting the runtime
|
||||
|
||||
In the common case, letting OTP start the `:parrhesia` application is enough.
|
||||
|
||||
If you need to start the runtime explicitly under your own supervision tree, use
|
||||
`Parrhesia.Runtime`:
|
||||
|
||||
```elixir
|
||||
children = [
|
||||
{Parrhesia.Runtime, name: Parrhesia.Supervisor}
|
||||
]
|
||||
```
|
||||
|
||||
## Primary modules
|
||||
|
||||
The in-process surface is centered on these modules:
|
||||
|
||||
- `Parrhesia.API.Events` for publish, query, and count
|
||||
- `Parrhesia.API.Stream` for REQ-like local subscriptions
|
||||
- `Parrhesia.API.Auth` for event validation and NIP-98 auth parsing
|
||||
- `Parrhesia.API.Admin` for management operations
|
||||
- `Parrhesia.API.Identity` for relay-owned key management
|
||||
- `Parrhesia.API.ACL` for protected sync ACLs
|
||||
- `Parrhesia.API.Sync` for outbound relay sync management
|
||||
|
||||
Generated ExDoc groups these modules under `Embedded API`.
|
||||
|
||||
## Request context
|
||||
|
||||
Most calls take a `Parrhesia.API.RequestContext`. This carries authenticated pubkeys and
|
||||
caller metadata through policy checks.
|
||||
|
||||
```elixir
|
||||
%Parrhesia.API.RequestContext{
|
||||
caller: :local,
|
||||
authenticated_pubkeys: MapSet.new()
|
||||
}
|
||||
```
|
||||
|
||||
If your host app has already authenticated a user or peer, put that pubkey into
|
||||
`authenticated_pubkeys` before calling the API.
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
alias Parrhesia.API.Events
|
||||
alias Parrhesia.API.RequestContext
|
||||
alias Parrhesia.API.Stream
|
||||
|
||||
context = %RequestContext{caller: :local}
|
||||
|
||||
{:ok, publish_result} = Events.publish(event, context: context)
|
||||
{:ok, events} = Events.query([%{"kinds" => [1]}], context: context)
|
||||
{:ok, ref} = Stream.subscribe(self(), "local-sub", [%{"kinds" => [1]}], context: context)
|
||||
|
||||
receive do
|
||||
{:parrhesia, :event, ^ref, "local-sub", event} -> event
|
||||
{:parrhesia, :eose, ^ref, "local-sub"} -> :ok
|
||||
end
|
||||
|
||||
:ok = Stream.unsubscribe(ref)
|
||||
```
|
||||
|
||||
## Where to look next
|
||||
|
||||
- [README.md](../README.md) for setup and the full config reference
|
||||
- [docs/SYNC.md](./SYNC.md) for relay-to-relay sync semantics
|
||||
- module docs under `Parrhesia.API.*` for per-function behavior
|
||||
Reference in New Issue
Block a user