Skip to content

Compare versions

From:To:
+7
11¶ **Equiply, 1 сентября 2026 года. Разбор про себя** — по правилу «мы попадаем в разборы наравне».
22
33 Оговорка сразу: репозиторий приватный, поэтому ссылку на коммит читатель не откроет. Хеши и тексты сообщений приводятся дословно, но проверить их снаружи нельзя. Это слабое место разбора, и делать вид, что его нет, было бы против самого метода.
44¶ **8 июня 2026 — рождается замок.**
55
66 Первый коммит проекта, `9434579`, называется длинно и по-хозяйски: `AEP — портал приводчиков (оборудование, склад, руководства, наряды, ИИ-чат)`.
77
88 Внутри — функция, которая на каждом запросе решает, кто перед нами:
99
1010 ```ts
1111 if (row.user.deletedAt || row.user.status === 'blocked') return null
1212 ```
1313
1414 Заблокировали человека — он перестаёт входить со следующего же запроса. Одна строка, и она работает два с половиной месяца без единой правки.
1515¶ **23 августа, 23:32 — строится вторая дверь.**
1616
1717 Приезжает коммит `c1ab6db`: `feat(mcp): персональные токены доступа в профиле пользователя`.
1818
1919 Раньше машины ходили в данные портала одним общим ключом из окружения. Теперь каждый выписывает себе личный.
2020¶ Это не короткое сообщение на строку. Оно на пять абзацев, и почти все — про безопасность:
2121
2222 > Токен — `aep_` плюс 32 случайных байта; в базе только SHA-256 от него, поэтому утечка таблицы доступа не даёт, а пользователю он показывается один раз. Хеш без соли и не bcrypt намеренно: это не пароль, а 256 бит энтропии — перебирать нечего, а быстрый хеш нужен для поиска индексом. Отзыв мягкий: строка остаётся, чтобы не терять историю «кто ходил».
2323
2424 Продумано хранение. Продуман отзыв. Продуман журнал. Продумано даже то, чего намеренно не делают и почему.
2525¶ А проверка на предъявлении получилась такая:
2626
2727 ```ts
2828 if (!row || row.revokedAt) return null
2929 ```
3030
3131 Токен не отозван — проходи.
3232
3333 Про то, жив ли ещё человек, за которым этот токен закреплён, не сказано ни слова. Ни в коде, ни в сообщении.
3434¶ **1 сентября — первый проход ревью.**
3535
3636 Одна линза за проход, свой вопрос, находка доказывается прогоном, а не чтением. Линза первая — доступ и права. Обход начинается с машинных поверхностей: там цена ошибки выше.
3737
3838 Проба дёргает настоящий `verifyToken`, а не свою копию его логики:
3939
4040 ```
4141 активный → ДОСТУП ЕСТЬ
4242 заблокирован → ДОСТУП ЕСТЬ
4343 мягко удалён → ДОСТУП ЕСТЬ
4444 ```
4545¶ Администратор нажимает «Заблокировать». Человек перестаёт входить на сайт в ту же секунду — замок с 8 июня работает.
4646
4747 Его ИИ-клиент в это время продолжает выкачивать реестр оборудования, все 2624 позиции, склад, движения ТМЦ и ежедневник. Ровно так же, как вчера. И будет завтра.
4848
4949 Отозвать нечего: блокировка токены не трогает, удаление тоже. А посмотреть, какие токены выписаны у сотрудника, администратор не может — чужие токены в интерфейсе не показываются.
5050
5151 Между постройкой второй двери и находкой — девять дней.
5252¶ **Дверь, которую никто не открывал.**
5353
5454 Дальше та часть, без которой разбор был бы враньём.
5555
5656 ```sql
5757 select count(*) from mcp_tokens;
5858 -- 0
5959 ```
6060
6161 Ни одного токена не выписано ни разу. Дыра существовала девять дней в коде и ноль секунд в жизни: чтобы в неё пройти, нужно было сначала завести ключ, а ключей нет.
6262
6363 Это не делает находку менее настоящей — первый же выписанный токен пережил бы увольнение своего хозяина. Но разницу полагается называть вслух: нашли до того, как понадобилось, а не после того, как утекло.
6464¶ **Починка.**
6565
6666 Напрашивалось отзывать токены при блокировке. Не стали: массовый отзыв работает только там, где о нём вспомнили, обязан быть повторён в каждом новом пути блокировки — и не закрывает токен, выписанный уже после неё.
6767
6868 Вместо этого условие переехало туда, где ему и место, — в саму проверку предъявления:
6969
7070 ```ts
7171 if (row.userDeletedAt || row.userStatus === 'blocked') return null
7272 ```
7373
7474 Та же строка, что на 8 июня. Теперь она есть у обеих дверей.
7575¶ **Почему это разбор про машину.**
7676
7777 Оба коммита — и тот, что построил дверь, и тот, что навесил замок, — подписаны `Co-Authored-By: Claude Opus 5`.
7878
7979 Обычно в таких разборах самое дорогое — восстановить ход мысли: люди объясняют себя скупо. Здесь наоборот. Модель объясняет себя избыточно: пять абзацев рассуждений там, где человек написал бы «добавил токены». Ход мысли не надо восстанавливать — он выписан целиком.
8080
8181 И он был не поверхностным. Ошибка не в том, что не подумали. Думали много и вслух.
8282
8383 Ошибка в том, что думали **про свою дверь**. Проверка, жившая в соседнем модуле с 8 июня, в поле зрения не попала — не потому что её сочли ненужной, а потому что её не увидели. Ревью ветки прочитало бы те же пять абзацев и согласилось бы с каждым.
84+¶ И последнее.
85+
86+ Тот самый коммит от 23 августа, третьим абзацем, объясняет, зачем вообще заводились личные токены:
87+
88+ > …а уход сотрудника не требует менять ключ всем сразу.
89+
90+ Уход сотрудника был единственным сценарием, ради которого всё это писалось. Он же оказался единственным, который не работал.
8491## Что это значит для нас
85921. Пересчитать двери к своим данным
8693 Выписать ВСЕ пути, которыми снаружи попадают к одним и тем же данным: сессия, личный токен, служебный ключ, публичная ссылка, вебхук, очередь.
8794 $ grep -rn "getCurrentUser\|verifyToken\|Bearer\|shareToken" src/ | cut -d: -f1 | sort -u
8895 why: Проверка, существующая в одном экземпляре из двух, — не проверка. Если право отзывается в одном месте, а предъявляется в двух, оно отозвано наполовину.
8996 - [ ] Список путей выписан целиком, а не по памяти
9097 - [ ] Для каждого пути назван замок, который на нём стоит
9198 - [ ] Найден хотя бы один путь, о котором вспомнили только при составлении списка
92992. Заменить действие при блокировке на проверку при предъявлении
93100 Не «при блокировке отозвать всё выданное», а «на входе спросить, жив ли владелец».
94101 why: Действие нужно помнить и повторять в каждом новом пути блокировки, который когда-нибудь появится, и оно не закрывает ключ, выданный уже после. Проверка срабатывает всегда, включая случаи, о которых никто не подумал заранее.
95102 - [ ] Состояние владельца проверяется в момент предъявления права
96103 - [ ] Проверка живёт рядом с проверкой отзыва, а не в отдельном месте
97104 - [ ] Новый путь выдачи ключа не требует помнить про отзыв
981053. Не принимать длину объяснения за полноту покрытия
99106 Перечитать последний большой коммит про безопасность и спросить: сколько из этого рассуждения выходит за пределы изменённого файла?
100107 why: Пять абзацев про энтропию, соль и журнал создают ощущение, что безопасность рассмотрена. Рассмотрена была одна её половина. Объём рассуждения говорит о старательности автора, а не о полноте охвата.
101108 - [ ] Названо, какие соседние модули коммит должен был учесть
102109 - [ ] Проверено, упомянут ли хоть один из них
1031104. Доказать прогоном, а не чтением
104111 Написать пробу, которая дёргает НАСТОЯЩУЮ функцию проверки прав и прогоняет её по состояниям владельца: активен, заблокирован, удалён.
105112 why: «Здесь стоит проверка» доказательством не является: проверка бывает второй копией, которая отстала. Проба, повторяющая логику своими словами, проверяет саму себя.
106113 - [ ] Проба вызывает настоящий код, а не копию его логики
107114 - [ ] До правки проба проваливается, после — проходит
108115 - [ ] У пробы есть предохранитель: на боевой базе она отказывается работать