Nodes and signalling¶
Each account is one hive. Engines (engine) and thin clients (thin) register an Ed25519
public key as a node, then find each other through the server and connect peer to peer.
The server is a directory and signalling relay. It never stores thread content.
Note
coturn (a TURN relay) is not deployed. turn_enabled is off and the ICE configuration
returns STUN servers only. The server-side encrypted relay over the signalling socket is
the only fallback when a direct connection fails.
Identity and registration¶
All encodings are base64url without padding. A node id is n_ plus the lowercase base32
(no padding) of the first 16 bytes of SHA-256 over the public key bytes. The server
recomputes it.
POST /nodes takes public_key, kind, name, platform, app_version,
protocol_version (1), proof and an optional invite_code. The proof is an Ed25519
signature over sc-register-v1\n{user_id}\n{public_key}. Logging in to the account is the
only link: every node is active as soon as it registers, and nodes only see nodes with the
same account. invite_code is deprecated and ignored. Registering the same key again
returns the existing node (a revoked node stays revoked). The pending status exists only
for nodes pairing directly over the LAN, where the server is not involved; a legacy
pending row becomes active when that node itself re-registers or authenticates.
REST endpoints¶
All endpoints need a bearer access token and are scoped to the caller's account. A node id
from another account returns the same 404 as an unknown id.
GET /nodeslistshive_idand nodes.POST /nodes/{id}/approveis deprecated and unnecessary; it returns the node and accepts the old body (approver_node_id,approval_sig) without checking the signature.DELETE /nodes/{id}revokes a node and closes its sockets.POST /nodes/invitesis deprecated: it returns a code that is never needed.POST /nodes/{id}/signal-ticketreturnsticket,challengeandexpires_in(30).POST /nodes/{id}/ice-configreturnsice_servers,ws_relay_enabledandttl.POST /nodes/{id}/pairing-assertionandPOST /nodes/{id}/pairing-assertion/verifypair a browser extension with one of the caller's engines (see below).
Extension pairing¶
A browser extension that signed in to the account asks for a one-time assertion for an
active engine node it owns, hands it to the local engine, and the engine verifies it
with its own account session. The assertion is stored hashed in LMDB, bound to the user,
node, client nonce and extension origin, and is never logged.
POST /nodes/{id}/pairing-assertion with {"client_nonce": "<16-128 chars>", "origin":
"chrome-extension://<32 a-p>" | "moz-extension://<uuid>"} returns
{"assertion": "...", "expires_in": 60}. The nonce may contain letters, digits and
_-.~+/=. A node that is unknown, owned by another account or not an engine gives 404;
a revoked node gives 403; a malformed body gives 422. Audit event: pairing_issued.
POST /nodes/{id}/pairing-assertion/verify with {"assertion", "client_nonce", "origin"}
and the same account's bearer returns {"ok": true, "user_id": "..."}. The assertion is
consumed by the first attempt, so a replay, a wrong nonce, origin, node or user, an
expired assertion or a revoked node all return 404 {"detail": "Invalid assertion"}.
Audit event: pairing_verified.
Both endpoints share the RATE_LIMIT_PAIRING limit (default 10/minute). The assertion
lifetime is PAIRING_ASSERTION_TTL_SECONDS (default 60, 5 to 300).
Signing in from an extension¶
Send POST /auth/email/login?native=true with JSON {"email", "password"}. The response
body holds access_token, expires_in, refresh_token and user; without
native=true the refresh token goes into an httponly cookie instead. Renew with
POST /auth/refresh and JSON {"refresh_token": "..."}: the body form is accepted
without cookies and returns a rotated refresh_token, so store the new one each time.
No Origin or CSRF check applies to these endpoints. Both use the RATE_LIMIT_AUTH limit.
Signalling WebSocket¶
Connect to /signal. Frames are JSON {"v":1,"type":...,"data":{...}}, at most 64 KiB.
The first frame must be auth within 5 seconds, with ticket, node_id and sig, an
Ed25519 signature over sc-signal-v1\n{challenge}\n{sha256_hex(ticket)}\n{node_id}.
Failure sends error with auth_failed and closes with code 4401.
Message types: presence.set, presence.update, presence.snapshot, signal, relay,
auth.refresh, ping and pong. Error codes are auth_failed, unknown_node,
rate_limited, too_large, bad_request and relay_disabled. The socket closes with
4401 when the session expires unless auth.refresh supplies a new ticket.
Limits¶
Each connection has a token bucket (30 messages per 10 seconds by default), at most 5 sockets per node, 20 nodes per account and a 60 second idle timeout.