User Story
As a pyry binary opening an outbound WebSocket to the relay, I want a /v1/server endpoint that validates the required headers, registers my connection in the registry under my server-id, and returns WS close 4409 if another binary already holds it, so that the binary side of the routing topology is established cleanly per the protocol spec.
Context
Implements the binary-side handshake from pyrycode/pyrycode/docs/protocol-mobile.md § Authentication → Binary → relay. First-claim-wins enforcement; the registry (#3) provides the underlying primitive. WebSocket library choice: nhooyr.io/websocket (modern, context-aware, idiomatic Go).
Acceptance Criteria
Technical Notes
nhooyr.io/websocket v1.8+ — adds to go.mod. Picked over gorilla/websocket (unmaintained as of 2022), gobwas/ws (lower-level than needed).
- WS close code
4409 is application-defined (4000–4999 range allowed by RFC 6455); use websocket.StatusCode(4409).
- The
Conn wrapper's Send() method serializes via a per-conn mutex (WS writes are not concurrent-safe). Close() is idempotent.
- Out of scope:
Size Estimate
S — ~120 LOC + ~150 LOC tests. Adds nhooyr.io/websocket dep.
Depends on
User Story
As a pyry binary opening an outbound WebSocket to the relay, I want a
/v1/serverendpoint that validates the required headers, registers my connection in the registry under my server-id, and returns WS close4409if another binary already holds it, so that the binary side of the routing topology is established cleanly per the protocol spec.Context
Implements the binary-side handshake from
pyrycode/pyrycode/docs/protocol-mobile.md§ Authentication → Binary → relay. First-claim-wins enforcement; the registry (#3) provides the underlying primitive. WebSocket library choice:nhooyr.io/websocket(modern, context-aware, idiomatic Go).Acceptance Criteria
internal/relay/server_endpoint.goexporting:func ServerHandler(r *Registry, logger *slog.Logger) http.Handler— returns anhttp.Handlerfor/v1/server.x-pyrycode-server(UUID-ish string, non-empty),x-pyrycode-version(non-empty),user-agent(non-empty). Missing/empty → respond400 Bad RequestBEFORE upgrading.nhooyr.io/websocket.AcceptwithOriginPatterns: []string{"*"}(binary connects programmatically; no browser-origin check).Connimplementation (satisfies the registry's interface from relay: connection registry — server-id → binary + server-id → [phone] thread-safe maps #3).ConnID()returns a stable per-connection id (e.g."server-" + serverID + "-" + randomShortID()).registry.ClaimServer(serverID, conn). OnErrServerIDConflict→ close WS with code4409(status code policy code; see protocol spec § Error codes). Reason string:"server-id already claimed".event=server_claimed server_id=<id> binary_version=<x> remote=<host>.registry.ReleaseServer(serverID)and logevent=server_released. The 30-second grace period is layered later (relay: WebSocket heartbeat — RFC 6455 ping/pong every 30s, close at 60s #7); this ticket releases immediately.cmd/pyrycode-relay/main.goupdated: register the handler at/v1/serveron the existing mux. Existing/healthzkeeps working.internal/relay/server_endpoint_test.gocovering:4409.nhooyr.io/websocket.Technical Notes
nhooyr.io/websocketv1.8+ — adds togo.mod. Picked overgorilla/websocket(unmaintained as of 2022),gobwas/ws(lower-level than needed).4409is application-defined (4000–4999 range allowed by RFC 6455); usewebsocket.StatusCode(4409).Connwrapper'sSend()method serializes via a per-conn mutex (WS writes are not concurrent-safe).Close()is idempotent.Size Estimate
S — ~120 LOC + ~150 LOC tests. Adds nhooyr.io/websocket dep.
Depends on