Split 3/4 of the companion bridge (#863, #871 landed 1/4 and 2/4). Adds admin-only device pairing to the companion router. - GET /api/companion/pair -- renders a form; never mints (a GET must not mint a credential: SameSite=Lax session cookies ride top-level GET navigations, so GET-minting would be CSRF-triggerable via a link/<img>) - POST /api/companion/pair -- mints a one-time chat-scoped token. Admin-cookie only; CSRF-safe because a SameSite=Lax cookie is not sent on a cross-site POST, the same protection POST /api/tokens relies on. ?format=json returns the pairing payload for an in-app screen. Minting invalidates the auth middleware's token cache so the code works on the next request with no restart. companion/pairing.py holds the mint/LAN/QR helpers; the token is shown once and stored only as a bcrypt hash + prefix (mirrors routes/api_token_routes.py). Tests (tests/test_companion_pairing.py): - a bearer/'api' caller and a non-admin user are rejected by require_admin (403); an admin passes - the token is returned once and persisted only as a hash - minting invalidates the cache (works without restart) - minting is exposed on POST, never GET (CSRF)
457 B
457 B