* fix(auth): treat a signed token with wrong claims as malformed
decode_token caught jwt.PyJWTError but not pydantic.ValidationError, so a
correctly signed token without sub/exp raised out of the function. The
OIDC state cookie shares jwt_secret and has exactly that shape, and the
OAuth start endpoint is public, so an unauthenticated caller could turn
it into a 500 (or a broken WebSocket handshake) instead of a 401.
Catch ValidationError and return TokenError.MALFORMED.
The regression test signs an OIDC-state-shaped payload and asserts
MALFORMED; it raises on main.
* fix(auth): treat a signed OIDC state cookie with bad claims as unusable
_verify_state_signed had the same latent shape as decode_token: it caught
only jwt.PyJWTError, so a state cookie signed with jwt_secret but missing
required claims raised pydantic.ValidationError straight into the public
/api/v1/auth/callback/{provider} endpoint instead of returning None and
letting the handler answer 403.
Also pin the decode_token precedence a reviewer raised: an expired token
that is missing sub reports EXPIRED because jwt.decode raises before the
payload model runs.
* docs: reduce inherited backend guidance size
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>