mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-06 03:38:23 +00:00
A relay can now answer one client command per HTTP request (POST /req,
/count, /event, /neg) with its own NIP-01 frames, one per line, ending
on the command's answer, through the same session, policies and backend
as its websocket. The spec draft is NIP-FE, "Relay Commands over HTTP".
Engine (nip01Core/relay/server), source- and binary-compatible:
- SessionSink: where a session's frames go, typed (Message) wherever the
engine built one and raw for the spliced EVENT frames. A transport can
now end an answer at its EOSE or map a CLOSED onto a status without
re-parsing wire JSON. SessionSink.of(send) is the old string callback,
and RelaySession/RelayServerBase keep every (String) -> Unit overload.
- RelaySession.initialAuthenticatedUsers, RelayServerBase.connect(sink,
authenticatedUsers) and serve(sink, authenticatedUsers, incoming): a
transport that proved a key itself (NIP-98) opens the session signed
in, recorded exactly where a NIP-42 AUTH would record it, so every
policy and backend that reads RequestContext.authenticatedUsers sees
it with no side table.
- RelaySession.receive(Command): dispatch an already-parsed command. The
string overload parses and calls it; acceptMessage still guards text.
NIP-FE (nipFERelayOverHttp), common code, no HTTP library:
- HttpRelayCommand: the four paths, each body parsed into its typed
Command through a JSON tree (so a body can only ever be arguments),
and which Message ends each answer.
- HttpRelayStatus: the status an answer's first frame gives it, from
its NIP-01 prefix; a duplicate: OK is a 200.
- HttpRelayHandler: size, parse, NIP-98 (payload-bound, single-use, its
own replay cache, any of the relay's addresses, other schemes
ignored), then the exchange: first frame decides the status, the rest
streams through HttpRelayResponse/HttpRelayLines, flushed whenever no
frame is waiting. The deadline is read between frames and never
interrupts a write; a reader that stops reading is HttpRelayReaderStalled
past a grace, and the host drops it. Admission stays with the host, in
front of handle(), so a refused request never spends a token.
- NEG is one stateless NIP-77 round, [filter, message]: the responder's
only state is its sealed snapshot, which the backend already caches
per filter, so nothing is held between rounds and no NEG-CLOSE exists.
Not in this change: backpressure from the socket into the backend. The
store callbacks cannot suspend, so a reader more than maxQueuedFrames
behind is cut with a CLOSED line, the websocket's bound.
Verified with :quartz:jvmTest (HttpRelayHandlerTest, 13 cases over a fake
in-memory SessionBackend), and earlier against the a8e8778265 jar that
vespa-relay pins, where every call the rest of the jar makes into
RelaySession/RelayServerBase still links.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jDNhr6a3J4VC5TaG3Yd48