- шаг схемы закрывает поверхность, которую хранилище приносит открытой: собственную регистрацию, вход по паролю и одноразовый код — без этого закрытие приёма обходилось двумя запросами - продление сессии выключено, срок семь суток: иначе отзыв доступа у провайдера до сервиса не доходит никогда - файл записи отдаётся вошедшему по токену файла — пересмотр ADR-2026-08-12-file-link-open-but-not-logged
50 KiB
Ревью кода: oidc-login — триаж
База диффа: origin/master, изменение целиком в рабочем дереве. Дата прогона: 2026-08-12.
Что сделано по итогам (дописано оркестратором после отработки)
Все семь пунктов закрыты; три развилки решены владельцем 2026-08-12.
| Пункт | Исход |
|---|---|
| 1. Войти не может никто | Исправлено: CreateRule = @request.context = "oauth2" правкой неуехавшего шага. Оракул — TestLoginCreatesAccountAndSession: вход целиком через подставного провайдера, учётная запись заводится, сессия выдаётся |
| 2. Утечка обработчиков | Исправлено: роутер хранилища собирается один раз через sync.Once. Оракул — TestCallbackDoesNotLeakHooks: 20 возвратов не меняют длины очереди |
| 3. Файл не отдаётся вошедшему | Решение владельца: открыть вошедшим. Назначено files.ViewRule = @request.auth.id != "", спека дополнена порядком «сессия → токен файла → ссылка». Оракул — TestRecordFileNeedsSessionAndToken |
| 4. Носитель состояния не убирался | Исправлено: уборка перенесена до записи ответа; все проверки файла судят по w.Result(), а не по живой карте заголовков |
5. Опечатка в [auth] роняет процесс |
Решение владельца: проверять форму адреса на старте. Validate разбирает адреса и требует схему и хост. Оракул — TestAuthConfigValidateRejectsMalformedURL |
| 6. Отзыв доступа не доходит | Решение владельца: выключить продление. Заведён слой BlockSessionRefresh; спека и модель угроз дополнены. Оракул — TestSessionRefreshIsClosed |
| 7. Сердцевина не покрыта | Исправлено: заведён login_test.go (вход целиком, отказ обмена, утечка, продление, файл) и internal/config/config_test.go; закрыты ветки выхода |
Сверх семи, из срезанного потолком, исправлено там же, где окно закрывается мерджем:
- G — срок жизни сессии перенесён из шага схемы в приведение настроек при подъёме;
- L — откат шага больше не падает на валидации и не возвращает открытую регистрацию;
- O —
docs/database.mdприведён к коду, коллекцияusersописана, три числа внесены в таблицу; - Q — уровни журнала разведены по адресату, добавлено поле
capability; - R —
auth.client_secretвнесён в перечень секретов и в инвариантCLAUDE.mdс изъятием про базу; - N — причина отказа провайдера приводится к перечню известных кодов;
- язык ответов пользователю переведён на русский.
Не сделано намеренно, ушло в урожай: P (настройка docs/.docs.json указывает на несуществующий каталог миграций — дефект гейта, не этого изменения), M (код провайдера в журнале запросов хранилища), S (начало входа собрано руками), T/U (рантбук выкладки), V (проверка конфига не в норме), гипотезы без пути и остаток пункта 4 (серверный учёт употреблённых состояний).
Сводка
Размер, сложность, метка. Размер — крупное: 24 подзадачи в 6 группах, 5 слоёв кода, 8 узлов в «Затрагивает», 2 capability (одна ADDED целиком). Сложность — незнакомое: docs/review.md, «Триггеры метки», «Незнакомое здесь» называет вход через OIDC дословно первой строкой. Метка large (максимум по осям), режим — по графу, опиниативные проходы открыты.
Состояние гейта: ЗЕЛЁНЫЙ. Проверено собственным прогоном триажа, а не только отчётом прохода: task gate BASE=origin/master → exit 0, все девять шагов. Унаследованное замечание tasks.py (any-audio-source) шаг не роняет и к диффу отношения не имеет.
Зелёный гейт здесь — часть находки, а не свидетельство. Два теста в internal/controller/http/auth_test.go утверждают проверенным то, что не работает (пункты 3 и 4), и оба зелёные. Два пункта tasks.md — 5.5 и 5.8 — отмечены [x] за проверки, которых в файле нет.
План разметки задачи с исходом по каждой теме
| тема | дом | глубина | кто закрывает | исход |
|---|---|---|---|---|
| requirements | openspec/changes/oidc-login/specs/{access,intake,storage}/spec.md |
разбор | specs | закрыта, 5 находок (C, D, E, M, V) + 3 блока наблюдений |
| autotests | CLAUDE.md, «Гейт» |
— | autotests | закрыта, 3 находки (H, I, J) + отчёт гейта и govulncheck |
| conventions | docs/conventions/{config,database,errors,logging}.md |
разбор | code | закрыта, 7 находок (A, L, O, P, Q, R + доля в B) |
| architecture | docs/architecture.md; источник docs/passport.md |
доказательство | architecture | закрыта, 3 находки (B, G, S) + 5 пунктов «дешевле переделать» |
| security | docs/security.md |
доказательство | adversary | закрыта, 6 находок (A, B, E, F, M, N) + 5 свойств без пути |
| operations | docs/architecture.md «Эксплуатация»; источник docs/database.md |
доказательство | ops | закрыта, 3 находки (K, T, U) |
Тем без отчёта нет. Все шесть тем ядра вернули отчёты. review-basics не запускался — по решению review-scope: своих тем сверх ядра у проекта нет. Тем, унесённых непроверенными, нет.
Сигнал о заниженной метке: не поступил, и провенанс у этого один. review-code подал строку прямо: «метка large соответствует изменению, понижения не вижу». review-basics не запускался, поэтому второго независимого корректора метки у прогона не было — согласия двух проходов нет, есть отсутствие возражения от одного.
Находок на входе: 22 нумерованных (A–V) плюс 23 ненумерованных содержательных пункта (8 «поведение вне спеки», 5 «границы спеки», 5 «свойства без построенного пути», 5 «дешевле переделать») = 45 позиций. Осталось в основных секциях: 7 — 3 блокирующие и 4 «стоит исправить сейчас». Слито по причине 4 группы, понижено до гипотез 5, уехало в promote 6, выброшено как вкусовщина 3, срезано потолком с поимённым перечислением 13.
Блокирует мердж
1. После выкладки войти не может никто, включая владельца — первый вход не заводит учётной записи
- Файл:
internal/adapter/repo/pocketbase/migrations.go:149 - Severity:
critical· Confidence:high - Найдено проходами: code, adversary (2 прохода, оракула два разных; согласие приоритет повышает,
confidence— нет) - Оракул — мой собственный, добыт на этом прогоне. Тест против настоящего PocketBase с подставным провайдером OIDC (
httptest, token + userinfo), запущен черезgo test -overlay=…без записи в дерево проекта:
users.CreateRule = <nil>
ИСХОД: код=401 обращений к токен-эндпоинту=1 учётных записей=0
тело={"error":"Login failed"}
ERROR Failed to exchange provider code error="storage rejected the exchange with code 403"
Причина изолирована тем же прогоном: с CreateRule = @request.context = "oauth2" результат код=302 учётных записей=1 Location="/", при этом анонимный POST /api/collections/users/records по-прежнему получает 400. Подтверждение по исходникам библиотеки: apis/record_crud.go:230-232 (!hasSuperuserAuth && collection.CreateRule == nil → Forbidden); внутренний запрос обмена идёт без авторизации.
- Последствие: шаг схемы закрывает создание записи для всех, кроме суперпользователя, а запись при первом входе заводит именно внутренний запрос обмена. Ни одной записи
usersшаг схемы не создаёт, приём и опрос закрыты сессией. После выкладки HTTP-вход не работает ни у кого; жив только Telegram. Лечится руками в панели — ровно то, от чего задача уходила. - Предложение:
users.CreateRule = ptr("@request.context = \"oauth2\"")правкой самого шагаup202608120001: он ещё не уезжал на сервер, и окно закрывается мерджем — после выкладки то же изменение потребует нового шага. Не чинить открытиемCreateRule = "": публичныйauth-with-oauth2принимаетcreateData, и всякий владелец учётной записи Authelia задаст поля новой записи сам. - Действие: инлайн
2. Всякий анонимный запрос на возврат навсегда добавляет обработчики приложению и дёргает Authelia нашим секретом
- Файл:
internal/controller/http/auth.go:224-234 - Severity:
critical· Confidence:high - Найдено проходами: architecture, adversary (
critical), specs и code (minor) — 4 прохода, оракула два - Оракул — мой собственный, тот же прогон:
OnModelAfterCreateSuccess: до=5 после 50 возвратов=55
OnModelAfterUpdateSuccess: до=6 после=106
обращений к провайдеру за 50 анонимных возвратов: 50
провайдер недоступен: обработчиков до=5 после 10 возвратов=15
Последняя строка важна: утечка происходит раньше сетевого обращения и работает при мёртвом провайдере. Путь анонимный: атакующий сам ставит себе куку transcriber_login=S:V и зовёт /auth/callback?state=S&code=x — сверка сравнивает две его же величины. Причина: apis.NewRouter (apis/base.go:47,53) зовёт bindRealtimeEvents и bindUIExtensions, которые вешают обработчики на приложение без поля Id; hook.Bind (tools/hook/hook.go:64) дописывает, а не заменяет.
- Последствие: рост линейный и не освобождается до перезапуска; каждое сохранение задачи конвейером проходит по всем накопленным замыканиям (замер adversary: 3000 хитов → 100 сохранений 3.86ms → 59.8ms, куча +5013 КиБ). Побочно каждый анонимный запрос гонит обмен к Authelia нашими
client_id/client_secret. - Предложение: строить роутер один раз (при подъёме либо
sync.Once), держатьhttp.HandlerполемAuthHandler. Решение владельца о петле внутри процесса не пересматривается. - Остаток, который правка не закрывает и который я не заказываю: анонимный запрос по-прежнему вызывает исходящее обращение к провайдеру. Ограничение числа запросов в scope изменения не входит; названо, чтобы не потерялось.
- Действие: инлайн
3. Файл записи не отдаётся ни одному вошедшему — сценарий дельты не исполняется, а docs/security.md уже утверждает обратное
- Файл:
internal/adapter/repo/pocketbase/migrations.go:168-179; тестinternal/controller/http/auth_test.go:405-431;docs/security.md:112-114;openspec/changes/oidc-login/tasks.md:68 - Severity:
major· Confidence:high - Найдено проходами: specs, code, adversary
- Оракул — мой собственный, тот же прогон:
files.ViewRule = <nil>
вошедший кукой → 404
вошедший заголовком → 404
файловый токен получен: код=200 непусто=true
вошедший файловым токеном → 404
аноним → 404
Причина: Protected=true включает проверку по файловому токену и по ViewRule коллекции (apis/file.go:109-134); ViewRule у files не назначался ни одним шагом → nil → доступ только суперпользователю (core/record_query.go:606-608).
-
Последствие: сценарий дельты
storage«GIVEN забирающий предъявил сессию THEN приходит тот же файл» не исполняется. Пунктtasks.md5.8 («без сессии отдаёт отказ, а с сессией — тот же файл») отмечен сделанным, а тест проверяет только отказ анониму.docs/security.md:113уже переписан утверждением «пройти по ссылке теперь можно только с сессией»: сегодня это ложно. Заведённая задача про прослушивание записи упрётся сюда и, вероятнее всего, «починит» снятиемProtected, вернув «знание ссылки = доступ». -
Действие: развилка
Вопрос владельцу. Файл записи защищён так, что его не получает никто, кроме владельца панели. Что делаем:
- назначить
files.ViewRuleдля вошедших и описать в спеке шаг «сессия → файловый токен → ссылка» (цена: правка шага схемы + новый абзац нормы + тест; окно на правку неуехавшего шага закрывается мерджем); - переписать требование как «файл виден только владельцу в панели», снять сценарий из дельты
storageи поправитьdocs/security.md(цена: правка нормы, задача про прослушивание записи начинается с этого же вопроса); - оставить как есть и записать расхождение (цена: норма и код разошлись сознательно, следующий проход найдёт то же самое).
- назначить
Стоит исправить сейчас
4. Носитель состояния входа живёт 10 минут вместо одного входа, и стерегущий его тест зелёный ложно
- Файл:
internal/controller/http/auth.go:131(defer),293-303; тестinternal/controller/http/auth_test.go:272 - Severity:
major· Confidence:high - Найдено проходами: specs, code (сведено с находкой «одноразовость состояния не реализована ничем» — причина одна: единственным механизмом одноразовости была уборка куки)
- Оракул — мой собственный, тот же прогон, настоящий сервер против recorder'а:
НАСТОЯЩИЙ СЕРВЕР: код=401 Set-Cookie=[]
RECORDER rec.Header()=[transcriber_login=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax]
RECORDER rec.Result().Header=[]
УСПЕХ: код=302 Location="/" Set-Cookie=[transcriber_session=…; Max-Age=604800; HttpOnly; Secure; SameSite=Lax]
На успешной ветке уборки состояния тоже нет — в ответе только кука сессии. Причина: e.SetCookie правит карту заголовков, а defer исполняется после e.JSON/e.Redirect, которые уже позвали WriteHeader. net/http при WriteHeader фиксирует снимок; httptest.ResponseRecorder наоборот — Header() отдаёт живую карту, а снимок лежит в snapHeader, который читает Result().
- Последствие: спека требует «MUST убираться на возврате — и на успешном, и на отказном»; носитель не убирается ни там, ни там и остаётся годным 10 минут. Повторно поданный URL возврата проходит сверку и уходит в обмен; отказ наступает только потому, что код у провайдера одноразовый — гарантия перенесена на внешнюю систему, чего спека не допускает. Тест утверждает обратное и проходит, потому что читает
w.Header(). - Предложение: убирать куку до записи ответа (как уже сделано в
Logout); всем тестам этого файла судить поw.Result(). - Остаток:
tasks.md5.5 обещает ещё и «повторный возврат с уже употреблённым состоянием», то есть учёт употреблённых состояний. Уборка куки закрывает переигрывание тем же браузером, но не учёт как таковой. Нужен ли учёт — вопрос владельцу, отдельно от этой правки; в код его сейчас не заказываю. - Действие: инлайн
5. Опечатка в [auth] кладёт весь сервис детерминированно, и /health в этот момент ещё не зарегистрирован
-
Файл:
main.go:220-234;internal/config/config.go:69-90 -
Severity:
major· Confidence:high -
Найдено проходом: ops
-
Оракул: эксперимент прохода —
ApplyProviderSettingsс непустым, но негодным URL →oauth2: (providers: (0: (authURL: must be a valid URL; tokenURL: …).).). Проверено мной по коду:AuthConfig.Validate(config.go:69-90) сверяет только непустоту шести ключей;ApplyProviderSettingsзовётся внутри хукаOnServeвыше регистрации/healthи/metrics(main.go:227-234противmain.go:239+), а ошибка хука прерываетapis.Serveдо открытия порта (apis/serve.go:216-269). Далее общий shutdown останавливает бот и все три воркера. -
Последствие: пробел от шаблона или отсутствующая схема в адресе роняет сервис целиком при каждом перезапуске, и у владельца нет даже кода состояния — только текст в журнале контейнера. Это первая выкладка этой секции конфига, шесть новых ключей.
-
Действие: развилка
Вопрос владельцу. Негодный адрес провайдера сегодня валит процесс молча. Что делаем:
- проверять форму URL в
Validate()— отказ переезжает на старт, называет ключ поимённо и виден в журнале сразу (цена: три строки, поведение «не поднимаемся с кривым входом» сохраняется); - регистрировать
/healthи/metricsдоApplyProviderSettings— сервис поднимается и честно отвечает о своём состоянии (цена: появляется состояние «сервис жив, вход сломан», которого спека не описывает); - и то и другое.
- проверять форму URL в
6. Отзыв доступа в Authelia не доходит до сервиса никогда: предъявитель продлевает сессию сам
- Файл:
internal/adapter/repo/pocketbase/migrations.go:126-130(комментарий);openspec/changes/oidc-login/specs/access/spec.md:185-188;docs/security.md:145 - Severity:
major· Confidence:high - Найдено проходом: adversary
- Оракул — мой собственный, тот же прогон:
auth-refresh #1 → код=200 новый токен непуст=true
auth-refresh #2 → код=200 новый токен непуст=true
auth-refresh #3 → код=200 новый токен непуст=true
продлённое значение на /api/status → 404 (401 значило бы, что не работает)
Замер adversary добавляет растущий exp (13:32:59 → 13:33:00 → 13:33:01 → 13:33:03) и claim refreshable=true.
-
Последствие: спека дельты
accessобъявляет срок сессии «единственным, что доносит до сервиса отзыв доступа у провайдера», и на этом утверждении стоит ссылка паспорта на отзыв в Authelia как на способ остановить перерасход. Канала нет: предъявитель одного живого значения продлевает себе доступ бессрочно, никуда не входя. Аноним так не может — нужен живой токен. -
Действие: развилка
Вопрос владельцу. Что делаем с продлением сессии:
- выключить продление (закрыть
auth-refreshдляusers) — отзыв начинает доходить за семь суток, как обещает норма (цена: человек перевходит раз в неделю); - сверяться с провайдером по расписанию (цена: новая связь с Authelia, обработка её недоступности, вне текущего scope);
- принять как есть и сейчас же убрать из
specs/access/spec.md,docs/security.mdи комментария шага схемы утверждение про канал отзыва (цена: паспорт теряет способ остановить перерасход, и это надо записать явно).
При любом варианте утверждение о канале отзыва сегодня ложно — правка нормы обязательна во всех трёх.
- выключить продление (закрыть
7. Сердцевина входа не исполнялась ни одним тестом: обмен кода, выдача куки сессии и проверка конфига
- Файл:
internal/controller/http/auth.go:199-252, 269-279;internal/config/config.go:69-90;internal/controller/http/auth.go:174-195 - Severity:
major· Confidence:high - Найдено проходами: autotests, specs (сведены три находки: покрытие
exchangeиsetSessionCookie, непокрытаяAuthConfig.Validate, непокрытые ветки отказаLogout— причина одна: проверки останавливаются раньше сердцевины) - Оракул — мой собственный прогон покрытия:
auth.go:199 exchange 0.0%
auth.go:269 setSessionCookie 0.0%
auth.go:128 Callback 54.5%
auth.go:174 Logout 63.6%
config.go:69 Validate 0.0% (у пакета internal/config нет файла тестов вовсе)
- Последствие: единственный код, который меняет код провайдера на сессию, и код, который выдаёт сессию браузеру, не проверены ни на успех, ни на отказ. Спека объявляет имя
transcriber_sessionнормативным именно потому, что «тест, ставящий и читающий одно и то же имя, этого не замечает» — потеряHttpOnly/Secure/SameSite, смена имени или срока пройдут гейт зелёными. Класс не новый:docs/review.md, журнал, записи 2026-08-10 («тесты http-обработчика ни разу не были зелёными») и 2026-08-11 («проверка приёма не могла упасть») — тот же род, третье появление. - Предложение: поднять подставного провайдера
httptestи пройтиCallbackдо302+ куки, судя поw.Result().Cookies(), сверяя литерал имени, флаги иMaxAge; завести файл тестовinternal/config; закрыть обе ветки отказаLogout. - Действие: инлайн
Гипотезы без доказательства
- Чужая страница гасит сессию.
POST /auth/logoutсессии не требует и на кросс-сайтовом запросе отвечает200сSet-Cookie Max-Age=-1. Браузера в прогоне нет, применениеSameSite=Laxк POST не проверялось. Confidencelow→minor(adversary). - Две учётные записи провайдера с одной почтой сливаются в одну нашу. Обмен ищет по
sub, не найдя — по почте. Требует, чтобы Authelia выдала двум субъектам один адрес; живого провайдера нет. Confidencemedium, без оракула → вышеmajorне поднимается и в основные секции не идёт (adversary). - Претензия
pictureтянет до 32 МиБ на вход.MappedFields.AvatarURLзаставляет библиотеку скачать URL из ответа провайдера. Confidencelow→minor(adversary). - Анонимный
POST /api/collections/users/request-verificationзаставляет сервис слать почту. Почта не настроена, потолка запросов нет. Confidencemedium→minor(adversary). - Вошедший читает, правит и удаляет свою запись
users(системные правилаid = @request.auth.idоставлены), тогда какdocs/security.mdутверждает, что правила коллекций пусты и отдают403. Своим прогоном не проверял — бюджет попытки израсходован на пункты 1–4, 6, 7 (adversary).
Promote candidates
- Проверка ответа судит по
w.Result(), а не поw.Header(). Ровно этим различием держался ложно зелёный тест пункта 4. Место —docs/review.md, «Типовые узлы», абзац про способность проверки упасть: род механизируем grep'ом и уже дал дефект. docs/.docs.jsonобъявляет механизированную сверку миграций, которой нет. Ключ"migrations": "migrations", аcheck_migrationsфильтрует изменённые файлы по префиксуmigrations/; такого каталога в репозитории нет (git ls-files | grep -c "^migrations/"→0), шаги схемы лежат вinternal/adapter/repo/pocketbase/. Шаг гейта зелен при изменённомmigrations.goи нетронутомdocs/database.md. Правило есть, механизации нет:"migrations": "internal/adapter/repo/pocketbase"либо снять пометку «механизировано» вdocs/conventions/README.md.- Откат бинаря не откатывает шаг схемы — назвать в
CLAUDE.md, «Необратимое», рядом с «применённой миграцией» (эксперимент ops: два прогонаNew()разных ревизий над одним каталогом,files.file.Protected=trueсохраняется). - Новый секрет
auth.client_secretне назван ни в перечне секретных полейdocs/conventions/config.md:94-96, ни в инвариантеCLAUDE.md. Перечень поимённый и закрытый — это и делает его правилом. - Ввод пользователя в журнале приводится к закрытому перечню, а не пишется как есть — обобщение приёма задачи
no-user-filename-in-log. Повод:providerErrorиз query уходит вlogger.Warnцеликом (падающий тест adversary: 204806 байт запроса → 204902 байта журнала). - Имя провайдера
oidc— это значение в связи учётной записи с провайдером. Смена имени после выкладки отвяжет всех заведённых людей.CLAUDE.md, «Необратимое», знает имя ключа конфига, но не знает имени провайдера.
Границы покрытия
План: темы, глубины, дома
| тема | дом | глубина | закрыта |
|---|---|---|---|
| requirements | дельта-спеки access, intake, storage |
разбор | specs |
| autotests | CLAUDE.md, «Гейт» |
— | autotests |
| conventions | docs/conventions/{config,database,errors,logging}.md |
разбор | code |
| architecture | docs/architecture.md; источник docs/passport.md |
доказательство | architecture |
| security | docs/security.md |
доказательство | adversary |
| operations | docs/architecture.md «Эксплуатация»; источник docs/database.md |
доказательство | ops |
Тем без дома нет. Тем без отчёта нет.
Что запускалось и что нет
- Запущены на метке
large, режим «по графу»:specs,code,architecture,adversary,ops,autotests. basicsне запускался: решениеreview-scope— своих тем сверх ядра нет. Это решение, а не бюджет.- Триаж запускал сам:
task gate BASE=origin/master(exit 0), покрытиеinternal/controller/httpиinternal/config, шесть собственных тестов-оракулов черезgo test -overlay=…(в дерево проекта не писал), чтение исходников PocketBase v0.39.10 иdocs.py.
Чего запущенные проходы не могли проверить в принципе
- Живой вход у настоящей Authelia не воспроизводился ни одним проходом и мной: провайдера нет, поднять нечем. Всё, что известно о протоколе, получено против подставного провайдера и исходников библиотеки.
- Поведенческая верификация живым запуском сервиса не проводилась: адаптер Telegram роняет старт при негодном токене, а боевым токеном запускаться запрещено (
CLAUDE.md, «Запреты»). - Поведение браузера с куками — применение
SameSite, приёмSet-Cookieкросс-сайтом — не проверялось: браузера в прогоне нет. - Панель
/_/в тестовом роутере отсутствует (её вешаетapis.Serve); закрывает её обратный прокси, то есть выкладка, а она вне модели. govulncheckдал две уязвимости (GO-2026-6061 grpc, GO-2026-5764 aws eventstream/s3), обе унаследованы отorigin/master, в цепочке распознавания, не в этом коде.- Замер утечки обработчиков сделан на 50 и 3000 итерациях в тесте; поведение под настоящим потоком не замерялось ничем.
Что осталось целиком на человеке
Из docs/review.md, «Недоступно проверке». Списки не сливаются: при следующем промахе первый вопрос — «не тот ли это класс, который мы перестали проверять».
Не проверит ни один проход:
operations: поведение внешних сервисов под нагрузкой и на границах — SpeechKit и Object Storage поднять в тесте нечем;operations: реальный профиль нагрузки — проект работает на единицах записей в день, и утверждения о росте остаются условиями, а не замерами;security: стойкостьffmpegк вредоносному входу — разбор чужого формата отдан внешней программе, и она вне нашей границы.
Перестали проверять сознательно:
autotests: разбор вывода настоящегоffprobe— проверки приёма получают длительность от подставного источника; своего теста уadapter/metaviewer/ffmpegнет (docs/adr/ADR-2026-08-11-stub-adapters-in-tests.md).
Сверх проектного перечня — общее: история инцидентов, поведение под реальным потоком, поведение внешних систем в их версиях (здесь — конкретной Authelia владельца и её правила на этого клиента), завязка потребителей на текущее поведение и вопрос «а нужна ли эта функциональность вообще».
Каких документов проекта не хватило
docs/review.md, «Типовые ложноположительные»: раздел есть и непуст (4 пункта), но ни один не относится к области этого изменения — все четыре про конвейер, очередь и открытый HTTP. Отсев для темы входа и веб-поверхности шёл по общим критериям, проектных ложноположительных этой области я не знал.docs/review.md, «Вопросы по темам»: вопросов по теме входа нет — раздел писан до появления этой поверхности. Вопросыsecurityпро журнал и метки применялись, вопросы про конвейер неприменимы.docs/conventions/web-ui.mdсуществует (104 строки), требование к языку пользовательского текста в нём есть; конвенции по форме HTTP-ответов входа (коды, тело отказа) в нём нет — поэтому «Login failed» судилось только по требованию русского языка, а форма ответа не судилась ничем.- Прочих пробелов проходы не заявляли;
CLAUDE.mdс разделом инвариантов,docs/security.md,docs/architecture.md,docs/database.md,docs/passport.mdи четыре конвенции были на месте и использовались.
Потолки проходов
code: 4 из 4 — срез сработал. За срезом остались два рода, названы самим проходом: (1) язык пользовательского текста на новой публичной поверхности («Login failed», «Logout failed» по-английски противweb-ui.md); (2) продолжение известных «Расхождений» новым кодом —msgпредложением, «failed to» в обёртках, третья точка трансляции доменной ошибки в ответ.architecture: 3 из 3 — потолок выбран полностью, за срезом ничего не заявлено.specs,adversary,ops,autotests: свои потолки не сообщили. Это находка о прогоне: сколько находок каждый показал против своего лимита и что осталось за срезом, установить нечем. Из четверых пришло 5, 6, 3 и 3 находки — то есть по крайней мереadversaryшёл близко к типичному лимиту, и молчание о срезе здесь дороже всего.
Срезано потолком триажа — названо поимённо
Тринадцать позиций с оракулами не попали в секции 1–2 и к правке не заказаны. Строки ниже — не задание; они здесь потому, что ничего не выбрасывается молча.
- G (
major, architecture):SessionDurationпитает две точки разной обратимости —AuthToken.Durationв применяемом однажды шаге схемы иMaxAgeкуки, перечитываемый каждый подъём. Первая же правка константы уедет только в куку. Оракул — инвариантCLAUDE.md«Миграция, уехавшая на сервер, не переписывается» и собственный доводdesign.md. Правка (перенестиAuthToken.DurationвApplyProviderSettings) стоит трёх строк сейчас и требует нового шага схемы после выкладки: окно закрывается мерджем. Первый кандидат на восьмое место. - M (
minor): код провайдера оседает в журнале запросов хранилища на пять суток. Проверено мной по исходникам:activityLoggerпишетevent.Request.URL.RequestURI()(со строкой запроса) полемurl(apis/middlewares.go:391,422), ретеншенMaxDays: 5(core/settings_model.go:158), проект его не переопределяет. Спека требует «код MUST не попадать в журнал»; нашslogчист, и тест смотрит только в него. - N (
minor): аноним пишет в журнал контейнера мегабайты (?error=<1 МиБ>→Warnцеликом; падающий тест adversary: 204806 → 204902 байта). Журнал — единственное место наблюдения двух инвариантов о молчаливой потере задачи. - L (
minor):down202608120001падает на валидации — проверено моим прогоном:Save(users)сDuration=0→authToken: (duration: cannot be blank.). Путь «шаг обратим своим down» изdesign.mdне работает, снятиеProtectedне выполняется вовсе, а самdownвозвращаетCreateRule = ""— открытую регистрацию, то самое, что чинитup. - O (
minor):docs/database.md:98-101противоречит коду («поле файла не помечено защищённым»), коллекцияusersне описана, три новых числа (7 суток, 10 минут, 15 секунд) не попали в таблицу «Настройки с числовым значением». Оракул —docs/conventions/database.md, «Прочее». - Q (
minor): уровни журнала не по адресату — отказ человека у Authelia даётWARN, возврат по старой ссылкеERROR. Оракул —docs/conventions/logging.md, «Уровни», дословно. Плюс ни одна новая запись не несёт поляcapability. - S (
minor): начало входа собрано руками поверх того, что библиотека экспортирует (InitProvider,BuildAuthURL,PKCE) — протокол разрезан пополам,auth_urlиclient_idполучают второго потребителя мимо настроек коллекции. - U (
minor): пустая секция[auth]роняет сервис целиком, тогда как пустой токен бота лишь деградировал до «работает без Telegram». Асимметрия осознанная, но в рантбуке выкладки не названа. - V (
minor): новое безусловное условие отказа старта по шести ключам живёт вtasks.mdи конвенции, но не в норме. - Поведение вне спеки (8 пунктов от
specs, ни один не заказан):stateCookieMaxAge10 минут; редирект успешного входа на/; выход без сессии отвечает200; отказ загрузки записи при выходе оставляет куку; составscope; склейкаauthURLчерез?/&;ApplyProviderSettingsперетирает список провайдеров целиком (провайдер, заведённый владельцем в панели, исчезнет при подъёме);downне возвращаетOTP.Enabled. - Границы спеки (5 пунктов): два входа одновременно в двух вкладках; поведение при отказе приведения настроек провайдера; остальная поверхность аутентификации хранилища (
confirm-password-reset,request-verification,confirm-verification,request-email-change— перечень «что выключено» в спеке закрыт тремя пунктами, а поверхность шире); отзыв доступа внутри срока сессии; «владелец закрывает чужие сессии немедленно» существует только как ручная правка в панели. - Имя куки состояния
transcriber_loginв спеке не нормировано, в отличие отtranscriber_session; комментарий шагаup202608110001до сих пор утверждает «Защищённым поле не помечено намеренно» — после выкладки два шага противоречат друг другу в исходнике. - Литерал
"users"живёт в четырёх местах при существующих константах имён коллекций (FilesCollection,JobsCollection).
Выброшено как вкусовщина (3): имена ключей auth.* как таковые (CLAUDE.md уже относит имя ключа конфига к необратимому — повторение записанного, а не находка); замечание про JSON-404 катч-олла на / (поведение хранилища, не этого кода, последствие не названо); предложение обобщить сборку адреса согласия сверх пункта S (работающий частный случай, последствия сверх S нет).
Четыре строки, которых не принесёт ни один проход
- Решения проекта не сверялись.
docs/adr/— процессный документ, прогон его не открывает. Расхождение изменения с записанным решением ловит скиллav-dev-docs:healthcheck, а не ревью. В этом изменении решений владельца названо минимум три (архив бессрочный, петля обмена внутри процесса, семь суток сессии) — ни одно против ADR не сверено. - Записанные наблюдения проекта не использовались.
docs/research/прогон не открывал. Всякое число в этом отчёте снято на этом прогоне и сопровождено командой или выводом; чисел из записанных наблюдений здесь нет. - Поимённая сверка с руководствами по стилю Go не задавалась ни одним проходом. Различение «идиоматично против распространено» не спрашивает никто с тех пор, как упразднён проход про идиоматичность; к новому коду (
auth.go,session.go,provider.go) это относится целиком. - Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет. Проход независимой реализации снят по стоимости, а не по замеру. «Не знаю, чего не знаю» про форму решения входа — а форма здесь нащупывалась по ходу, это и подняло метку до
large— не достаёт никто.
Метка large, поэтому пятая строка (про small) не применяется — дома всех трёх тем security, operations, architecture открывались.