Equiply, 1 сентября 2026 года. Разбор про себя — по правилу «мы попадаем в разборы наравне».
Оговорка сразу: репозиторий приватный, поэтому ссылку на коммит читатель не откроет. Хеши и тексты сообщений приводятся дословно, но проверить их снаружи нельзя. Это слабое место разбора, и делать вид, что его нет, было бы против самого метода.
8 июня 2026 — рождается замок.
Первый коммит проекта, 9434579, называется длинно и по-хозяйски: AEP — портал приводчиков (оборудование, склад, руководства, наряды, ИИ-чат).
Внутри — функция, которая на каждом запросе решает, кто перед нами:
Заблокировали человека — он перестаёт входить со следующего же запроса. Одна строка, и она работает два с половиной месяца без единой правки.
23 августа, 23:32 — строится вторая дверь.
Приезжает коммит c1ab6db: feat(mcp): персональные токены доступа в профиле пользователя.
Раньше машины ходили в данные портала одним общим ключом из окружения. Теперь каждый выписывает себе личный.
Это не короткое сообщение на строку. Оно на пять абзацев, и почти все — про безопасность:
Токен —
aep_плюс 32 случайных байта; в базе только SHA-256 от него, поэтому утечка таблицы доступа не даёт, а пользователю он показывается один раз. Хеш без соли и не bcrypt намеренно: это не пароль, а 256 бит энтропии — перебирать нечего, а быстрый хеш нужен для поиска индексом. Отзыв мягкий: строка остаётся, чтобы не терять историю «кто ходил».
Продумано хранение. Продуман отзыв. Продуман журнал. Продумано даже то, чего намеренно не делают и почему.
А проверка на предъявлении получилась такая:
Токен не отозван — проходи.
Про то, жив ли ещё человек, за которым этот токен закреплён, не сказано ни слова. Ни в коде, ни в сообщении.
1 сентября — первый проход ревью.
Одна линза за проход, свой вопрос, находка доказывается прогоном, а не чтением. Линза первая — доступ и права. Обход начинается с машинных поверхностей: там цена ошибки выше.
Проба дёргает настоящий verifyToken, а не свою копию его логики:
Администратор нажимает «Заблокировать». Человек перестаёт входить на сайт в ту же секунду — замок с 8 июня работает.
Его ИИ-клиент в это время продолжает выкачивать реестр оборудования, все 2624 позиции, склад, движения ТМЦ и ежедневник. Ровно так же, как вчера. И будет завтра.
Отозвать нечего: блокировка токены не трогает, удаление тоже. А посмотреть, какие токены выписаны у сотрудника, администратор не может — чужие токены в интерфейсе не показываются.
Между постройкой второй двери и находкой — девять дней.
Дверь, которую никто не открывал.
Дальше та часть, без которой разбор был бы враньём.
Ни одного токена не выписано ни разу. Дыра существовала девять дней в коде и ноль секунд в жизни: чтобы в неё пройти, нужно было сначала завести ключ, а ключей нет.
Это не делает находку менее настоящей — первый же выписанный токен пережил бы увольнение своего хозяина. Но разницу полагается называть вслух: нашли до того, как понадобилось, а не после того, как утекло.
Починка.
Напрашивалось отзывать токены при блокировке. Не стали: массовый отзыв работает только там, где о нём вспомнили, обязан быть повторён в каждом новом пути блокировки — и не закрывает токен, выписанный уже после неё.
Вместо этого условие переехало туда, где ему и место, — в саму проверку предъявления:
Та же строка, что на 8 июня. Теперь она есть у обеих дверей.
Почему это разбор про машину.
Оба коммита — и тот, что построил дверь, и тот, что навесил замок, — подписаны Co-Authored-By: Claude Opus 5.
Обычно в таких разборах самое дорогое — восстановить ход мысли: люди объясняют себя скупо. Здесь наоборот. Модель объясняет себя избыточно: пять абзацев рассуждений там, где человек написал бы «добавил токены». Ход мысли не надо восстанавливать — он выписан целиком.
И он был не поверхностным. Ошибка не в том, что не подумали. Думали много и вслух.
Ошибка в том, что думали про свою дверь. Проверка, жившая в соседнем модуле с 8 июня, в поле зрения не попала — не потому что её сочли ненужной, а потому что её не увидели. Ревью ветки прочитало бы те же пять абзацев и согласилось бы с каждым.
И последнее.
Тот самый коммит от 23 августа, третьим абзацем, объясняет, зачем вообще заводились личные токены:
…а уход сотрудника не требует менять ключ всем сразу.
Уход сотрудника был единственным сценарием, ради которого всё это писалось. Он же оказался единственным, который не работал.
Выписать ВСЕ пути, которыми снаружи попадают к одним и тем же данным: сессия, личный токен, служебный ключ, публичная ссылка, вебхук, очередь.
grep -rn "getCurrentUser\|verifyToken\|Bearer\|shareToken" src/ | cut -d: -f1 | sort -uНе «при блокировке отозвать всё выданное», а «на входе спросить, жив ли владелец».
Перечитать последний большой коммит про безопасность и спросить: сколько из этого рассуждения выходит за пределы изменённого файла?
Написать пробу, которая дёргает НАСТОЯЩУЮ функцию проверки прав и прогоняет её по состояниям владельца: активен, заблокирован, удалён.