Обработчики IntegrityError (409) и SQLAlchemyError (500) молча отдавали
ответ. Теперь каждая такая ошибка — одна запись ERROR: путь и метод
запроса, классы ошибок SQLAlchemy и драйвера, SQLSTATE, SQL-текст с
плейсхолдерами (усечён до 200 символов) и стек вызовов. Текст исключения
в лог не идёт: str() ошибки asyncpg/SQLAlchemy содержит параметры запроса
и DETAIL PostgreSQL со значениями ключей (email, ИНН). Ответы прежние.
JwksVerifier.verify: TypeError/ValueError, превращаемые в AuthError,
теперь пишут warning в том же санированном виде repr(str(exc))[:200].
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
I-1: в jwt.decode передаётся PyJWK (а не голый ключ), рассогласование alg и типа ключа даёт 401,
а не TypeError/500; страховка на (TypeError, ValueError).
I-2: cache_keys=False — кэш ключей по kid не имеет срока жизни и держал бы отозванный ключ;
остаётся кэш набора JWKS на 300 с.
R-17: Claims.email = strip().lower().
R-18: запредельный exp не даёт 500; пустой sub отвергается; kid из заголовка пишется в лог через repr и усекается.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev HS256 включается только при AUTH_DEV_ISSUER и DEBUG одновременно; РАИСА — RS256/ES256 по
публичному ключу или JWKS с кэшем. PyJWT[crypto] добавлен в requirements.txt как прод-зависимость.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>