# rust-link-sidecar Configuration reference and endpoint list. For what this component *is*, see the [repo README](../README.md). ## Configuration `sidecar.toml`, resolved in this order: `--config `, else `$RUSTLINK_CONFIG`, else `./sidecar.toml`. Environment variables override the file; the file overrides the defaults. | Key | Env | Default | What it is | |---|---|---|---| | `[game].bind` | `RUSTLINK_GAME_BIND` | `127.0.0.1:7799` | Where the Oxide plugin dials in | | `[game].server_id` | `RUSTLINK_SERVER_ID` | *(empty)* | Optional cross-check against the plugin's own `serverId` | | `[web].bind` | `RUSTLINK_WEB_BIND` | `127.0.0.1:8090` | Where the website reaches this sidecar | | `[web].auth_token` | `RUSTLINK_WEB_TOKEN` | *(generated)* | The shared secret the website presents | | `[store].path` | `RUSTLINK_DB_PATH` | `rust-link.db` | SQLite file | | `[store].retain_days` | `RUSTLINK_RETAIN_DAYS` | `14` | Days of event history to keep. `0` keeps everything | Two things about those defaults are load-bearing: - **`[game].bind` is loopback, and there is no token on that link.** The plugin and the sidecar share a host; `127.0.0.1` *is* the authentication. Binding it to a routable address puts an unauthenticated command channel on the network. - **A relative `[store].path` resolves against the directory holding `sidecar.toml`**, not the working directory. A service manager's working directory must not decide where the database lands — on Windows that can be `%SystemRoot%\System32`, or a silently redirected VirtualStore copy. `[game].server_id` is a **cross-check, not a second source of truth**. The plugin announces its own `serverId` and that is the authority; when both are set and they disagree, the sidecar logs the disagreement loudly and keeps the plugin's. Two game servers pointed at one sidecar by a copied config is the mistake this catches, and it is silent in every other design. ### Reading the token back ```bash rust-link-sidecar --print-config ``` Resolves the configuration exactly as a normal start would — writing the file and generating the token if they are missing — and prints it as JSON on stdout, **including the token in clear text**. That is the supported way for an installer to obtain it; the alternative is scraping a log. ## Endpoints Everything except `/health` requires the token, as `Authorization: Bearer `, `X-Api-Key: `, or `?token=` (the last so browser WebSocket clients, which cannot set handshake headers, can still authenticate). Every response carries `X-RustLink-Version`. | Route | Backed by | Notes | |---|---|---| | `GET /health` | — | Unauthenticated, so monitoring can reach it | | `GET /server` | store | The last `server.hello`. **`204` when the game has never connected** | | `GET /boards` | store | Every board, keyed by kind. `200` with an empty object when there are none | | `GET /events?kind=&wipe=&limit=` | store | Newest first; `limit` clamped to 1–1000. For a human | | `GET /feed?since=&limit=` | store | **Oldest first, from a cursor.** For a consumer that must not miss a row. Omitting `since` asks where the end is | | `GET /status` | plugin (RPC) | A live round trip. `503` with no plugin, `504` on no reply | | `GET /ws` | broadcast | The live feed. Sends `ws.hello` on connect | The split is the point: the store-backed reads answer while the game is off, which is what lets the website render a server list during a wipe or a restart. `/status` is the one route that fails when the game is down, because "what is it doing right now" has no stale answer worth giving. `/server` answers `204`, not `200` with a null, when the game has never connected. "We have never heard from this server" and "this server reports nothing" are different answers, and a client that cannot tell them apart renders a server that does not exist. ## The protocol Newline-delimited JSON over TCP, both directions. Outbound frames (plugin → sidecar) carry `kind`; inbound frames (sidecar → plugin) carry `cmd`. Lines are capped at 1 MiB; an over-long line is discarded and the connection stays up. **This process files a frame by its `type`, and never by its `kind`** — which is what keeps it a dumb forwarder while the catalogue grows. Ten new event kinds are no change here at all. | `type` | Kept | Broadcast | Example | |---|---|---|---| | `event` | appended to the history | yes | `player.death` | | `snapshot` | **replaces** the board of that kind | yes | `players.online` | | `reply` | no | no | `server.status`, routed by `reqId` | | `control` | no | yes | `pong`, `link.down` | A frame with no `type` this build knows is **dropped and counted**, never guessed at, and the count is on `/health` as `untyped_frames`. The game link has no version handshake, so a plugin and a sidecar on different protocol versions show up there and nowhere else. | Frame | Direction | Purpose | |---|---|---| | `server.hello` | plugin → sidecar | A board. Sent on **every connect**, not once at server start — this process restarts independently of the game. Carries `serverId`, `bootId` and `wipeId` | | `players.online` | plugin → sidecar | The other board: who is connected, re-sent on connect and every 60s | | `ping` / `pong` | sidecar → plugin → sidecar | The heartbeat, every 30s. A `pong` is never persisted; it only moves `last_event` | | `server.status` | sidecar → plugin → sidecar | The one request/reply verb, correlated by `reqId` | | the read path | plugin → sidecar | Presence, deaths, chat, tallies, moderation, the wipe — the catalogue is `PROTOCOL.md` §8.4 | `bootId` is how a game restart is told apart from a sidecar reconnect — the distinction the event system's `reconcile` hangs off later. `wipeId` is how a wipe splits the history instead of ending it; the plugin derives it, and every frame carries it. **History is bounded, boards are not.** `[store].retain_days` (default 14) prunes events hourly; a board is one row per kind holding what is true now, and pruning it would make a server the site has rendered for weeks look like one that has never connected. The permanent record is the website's. **The RPC reply timeout (`rpc::REPLY_TIMEOUT`, 10s) is a ceiling every later command budget sits under.** Core classifies a budget overrun as retryable unconditionally, because it cannot ask the game while the action is still awaiting a socket. An action whose `budgetMs` exceeds this can never report `retry: false`. ## Checks ```bash cargo fmt --all -- --check cargo clippy --all-targets -- -D warnings cargo test ```