You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As a phone connecting to the relay, I want a /v1/client endpoint that validates my headers, looks up the server-id I'm targeting, and registers me as a phone connection on that binary's slot — or closes with 4404 if no binary holds the server-id — so that the phone-side of the routing topology is established per the protocol spec.
Context
Implements the phone-side handshake from pyrycode/pyrycode/docs/protocol-mobile.md § Phone → relay → binary. The relay does NOT validate the device token — it forwards everything to the binary, which validates. The relay's job is purely routing: "is there a binary holding this server-id? if yes, connect; if no, close 4404."
Acceptance Criteria
New file internal/relay/client_endpoint.go exporting:
func ClientHandler(r *Registry, logger *slog.Logger) http.Handler — returns an http.Handler for /v1/client.
On request:
Read required headers: x-pyrycode-server (non-empty), x-pyrycode-token (non-empty — validated by binary, not relay; just must be present), user-agent (non-empty). Missing/empty → respond 400 Bad Request before upgrading.
Read optional header: x-pyrycode-device-name (used in logs; not required).
On WS close (any reason), call registry.UnregisterPhone(serverID, connID) and log event=phone_unregistered.
cmd/pyrycode-relay/main.go updated: register the handler at /v1/client on the existing mux. Existing routes (/healthz, /v1/server) keep working.
Tests in internal/relay/client_endpoint_test.go covering:
Successful upgrade when a binary holds the server-id → registry.PhonesFor(serverID) contains the new connection.
Missing each required header → 400 (table-driven, one row per missing/empty header).
No binary holds the server-id → WS close 4404.
Closing the phone WS unregisters it from the registry.
Multiple phones on the same server-id register and unregister independently.
gosec and govulncheck clean.
Technical Notes
The x-pyrycode-token is treated as opaque by the relay. The relay neither parses nor validates it — that's the binary's responsibility. Per the protocol spec, the relay's authorization model is "is there a binary holding this server-id?" Period.
The relay MUST NOT log token values, full headers, or message payloads. The structured event includes only server-id (public), conn-id (public; opaque), device name (user-supplied label), and remote host (for ops).
User Story
As a phone connecting to the relay, I want a
/v1/clientendpoint that validates my headers, looks up the server-id I'm targeting, and registers me as a phone connection on that binary's slot — or closes with4404if no binary holds the server-id — so that the phone-side of the routing topology is established per the protocol spec.Context
Implements the phone-side handshake from
pyrycode/pyrycode/docs/protocol-mobile.md§ Phone → relay → binary. The relay does NOT validate the device token — it forwards everything to the binary, which validates. The relay's job is purely routing: "is there a binary holding this server-id? if yes, connect; if no, close 4404."Acceptance Criteria
internal/relay/client_endpoint.goexporting:func ClientHandler(r *Registry, logger *slog.Logger) http.Handler— returns anhttp.Handlerfor/v1/client.x-pyrycode-server(non-empty),x-pyrycode-token(non-empty — validated by binary, not relay; just must be present),user-agent(non-empty). Missing/empty → respond400 Bad Requestbefore upgrading.x-pyrycode-device-name(used in logs; not required).nhooyr.io/websocket.Accept(dep introduced in relay: WS upgrade for /v1/server — accept binary connection, validate headers, claim server-id #4).Connimplementation satisfying the registry interface (same pattern as/v1/serverin relay: WS upgrade for /v1/server — accept binary connection, validate headers, claim server-id #4).ConnID()returns"client-" + serverID + "-" + randomShortID()— opaque per-connection id used by the routing envelope (relay: routing-envelope wrapper type — marshal, unmarshal, tests #1).registry.RegisterPhone(serverID, conn). OnErrNoServer→ close WS with code4404. Reason string:"no server with that id".event=phone_registered server_id=<id> conn_id=<id> device_name=<name> remote=<host>.registry.UnregisterPhone(serverID, connID)and logevent=phone_unregistered.cmd/pyrycode-relay/main.goupdated: register the handler at/v1/clienton the existing mux. Existing routes (/healthz,/v1/server) keep working.internal/relay/client_endpoint_test.gocovering:registry.PhonesFor(serverID)contains the new connection.4404.gosecandgovulncheckclean.Technical Notes
x-pyrycode-tokenis treated as opaque by the relay. The relay neither parses nor validates it — that's the binary's responsibility. Per the protocol spec, the relay's authorization model is "is there a binary holding this server-id?" Period.4404is application-defined (4000–4999 range per RFC 6455); usewebsocket.StatusCode(4404). Mirrors the4409pattern already established in/v1/server(relay: WS upgrade for /v1/server — accept binary connection, validate headers, claim server-id #4).hello) to the binary — that's the frame-forwarding ticket (relay: frame forwarding loop — wrap/unwrap routing envelope, route by server-id and conn_id #6).Size Estimate
S — ~100 LOC + ~120 LOC tests.
Depends on
/v1/serverendpoint — introduces thenhooyr.io/websocketdep and theConnwrapper pattern this ticket mirrors).