feat(sidecar): protocol 3 — the first route on this bridge that is not a GET
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 2m30s

`POST /link/confirm` forwards a one-time link code to the plugin and hands back
what it says. Everything before it was the website reading what the game had
already told us; this is the website asking the game a question only the game can
answer.

**It is still a forwarder and holds no authority of its own.** It does not mint
codes, does not store them, does not know what a website user is, and cannot tell
a good code from a bad one. Putting the code table here would give the sidecar a
credential and an opinion, and D2 and the bridge principles say it has neither.

**A refused code is a 200.** `link.ok` and `link.error` are both answers, and the
website has to tell "that code is wrong" from "the game never replied" to say the
right thing to a player. The two transport failures keep the codes `respond`
already gives them: 503 when the game is down, 504 when it is up and silent.

`usable_code` is split out and tested because its two rejections are easy to get
subtly wrong. It trims BEFORE it measures: a player pasting a code out of game
chat brings whitespace with it, a field of nothing but spaces is empty rather
than four characters long, and the length bound belongs on the trimmed value.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-17 07:36:45 -05:00
parent 3aabd2befe
commit fd6efd9a2c
2 changed files with 88 additions and 2 deletions

View File

@@ -73,7 +73,7 @@ use tracing_subscriber::EnvFilter;
///
/// `docs/rust-link/PROTOCOL.md` §8 is the specification; this constant is one of its four
/// declaration sites.
pub const PROTOCOL_VERSION: u32 = 2;
pub const PROTOCOL_VERSION: u32 = 3;
fn main() -> anyhow::Result<()> {
let args = match cli::parse(std::env::args().skip(1)) {