diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index f1fea1d..ec938c0 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -22,7 +22,6 @@ - [🐞 Убирать записанный файл, когда приём отказал на середине](items/orphan-file-on-failed-intake.md) — Отказ чтения метаданных и отказ записи на диск оставляют файл в каталоге хранения без задачи и без учёта: сопоставить его не с чем, удалять приходится руками. - [🧹 Задать таймауты обращениям к внешним сервисам](items/external-call-timeouts.md) — Ни у Telegram, ни у Object Storage, ни у SpeechKit нет таймаута: молчащий собеседник держит шаг конвейера до истечения часового захвата. -- [✨ Пускать в приложение только после входа через OIDC](items/oidc-login.md) — HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. - [✨ Привязать запись к владельцу и отдавать только свои](items/record-ownership.md) — У задачи и файла нет владельца, поэтому знание UUID задачи и есть право её читать. - [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны. - [✨ Свести приём и чтение записей к одному контракту для приложения](items/json-api-for-spa.md) — Сегодняшний API отвечает 404 на любую ошибку чтения и 500 на любую ошибку приёма: строить на нём экраны нечем. @@ -64,4 +63,12 @@ - [🧹 Закрепить версию рантайм-базы образа](items/pin-runtime-image-base.md) — Финальный слой Dockerfile собирается на alpine:latest, а task image идёт с --pull, поэтому два образа из одного коммита с разницей в неделю несут разный ffmpeg — регрессия конвертации после такой пересборки выглядит как задачи в failed при пустом диффе репозитория, и откат на прежний коммит её не чинит. - [🧹 Привести коды выхода шагов гейта к общему словарю](items/gate-step-exit-codes.md) — CLAUDE.md объявляет словарь кодов общим для четырёх проверочных шагов и описывает код 3 как «каталог или файл не найден», но все четыре обёртки в Taskfile.yml при отсутствующем скрипте отдают 1 («дрейф»), из-за чего читатель кода идёт искать разъехавшееся там, где просто неполно дерево. - [🧹 Настроить конвейер ревью по итогам прогона go-1-26-upgrade](items/review-config-from-go-upgrade.md) — Прогон вскрыл две прорехи настройки: «Типовые узлы» знают только рантайм и не знают рода «проверочный шаг набора проверок», а «Триггеры метки» не видят оси «изменение трогает канон» — и именно она дала обе блокирующие находки. +- [🐞 Починить путь миграций в настройке сверки документов](items/fix-migrations-path-in-docs-config.md) — Ключ migrations указывает на каталог migrations/, которого в репозитории нет: шаг гейта зелен при изменённой миграции и нетронутом database.md, а конвенции числят этот род механизированным. +- [🐞 Убрать код провайдера из журнала запросов хранилища](items/provider-code-out-of-storage-log.md) — Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал. +- [🧹 Поднимать сервис локально без действующего токена бота](items/local-run-without-telegram-token.md) — Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача. +- [🧹 Строить адрес входа из настроек коллекции, а не из конфига](items/login-url-from-collection-settings.md) — Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину. +- [🧹 Судить ответ в тестах по готовому ответу](items/response-assertions-judge-result.md) — Проверка, читающая изменяемую карту заголовков обработчика, зелена при неработающем коде: класс всплыл трижды, последний раз на уборке куки входа. +- [🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы](items/rollback-does-not-undo-schema-step.md) — Откат бинаря оставляет применённый шаг схемы в силе, и на этом строятся решения о выкладке: сегодня об этом не сказано нигде. +- [🐞 Вести учёт употреблённых состояний входа на сервере](items/server-side-login-state.md) — Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены. +- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было. - [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. diff --git a/tasks/items/fix-migrations-path-in-docs-config.md b/tasks/items/fix-migrations-path-in-docs-config.md new file mode 100644 index 0000000..1580aac --- /dev/null +++ b/tasks/items/fix-migrations-path-in-docs-config.md @@ -0,0 +1,48 @@ +# 🐞 Починить путь миграций в настройке сверки документов + +- **Тип:** fix +- **Категория:** Очередь +- **Зачем:** Ключ migrations указывает на каталог migrations/, которого в репозитории нет: шаг гейта зелен при изменённой миграции и нетронутом database.md, а конвенции числят этот род механизированным. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +раздел «Promote candidates». + +Проверка сверяет изменённые файлы с префиксом `migrations/`, а шаги схемы лежат +в `internal/adapter/repo/pocketbase/`. Совпадений не бывает никогда, значит шаг +проходит зелёным всегда. Оракул сегодняшнего состояния: +`git ls-files | grep -c "^migrations/"` отдаёт `0`. + +Цена уже заплачена дважды: задача `oidc-login` изменила шаг схемы и не тронула +`docs/database.md`, и гейт этого не заметил — расхождение нашёл человек на +ревью. Так же провалится всякая следующая миграция. + +Развилка внутри задачи: либо поправить путь, либо снять пометку +«механизировано» в `docs/conventions/README.md` и отдать род человеку. Второе +дешевле, но тогда проверять его будет некому. + +## Воспроизведение + +1. Изменить любой файл шагов схемы в `internal/adapter/repo/pocketbase/`. +2. `docs/database.md` не трогать. +3. Прогнать `task docs BASE=origin/master`. +4. Шаг проходит зелёным, хотя должен назвать расхождение. Сегодняшнее состояние + настройки видно командой `git ls-files | grep -c "^migrations/"` — она отдаёт + `0`, то есть каталога с таким именем в репозитории нет. + +## Затрагивает + +- `docs/.docs.json`, ключ `migrations`; +- `docs/conventions/README.md`, таблица «Механизировано», строка про миграцию; +- шаг `docs` в `Taskfile.yml` — его исход меняется. + +## Критерии приёмки + +- Изменённый шаг схемы при нетронутом `docs/database.md` роняет шаг гейта. + Оракул — правка любого файла шагов схемы без правки схемы в документах, затем + `task docs BASE=origin/master`: ненулевой код возврата. +- Изменённый шаг схемы вместе с правкой `docs/database.md` шаг гейта проходит. + Оракул — то же с обеими правками: код возврата 0. +- Строка «Механизировано» в `docs/conventions/README.md` соответствует тому, что + проверка делает на самом деле. Оракул — чтение таблицы против исхода первых + двух проверок. diff --git a/tasks/items/local-run-without-telegram-token.md b/tasks/items/local-run-without-telegram-token.md new file mode 100644 index 0000000..e1d6781 --- /dev/null +++ b/tasks/items/local-run-without-telegram-token.md @@ -0,0 +1,35 @@ +# 🧹 Поднимать сервис локально без действующего токена бота + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Адаптер Telegram проверяет токен обращением к Telegram и роняет старт, а боевым токеном запускаться запрещено: проверить поведение живым прогоном не может ни одна задача. + +Замечено при попытке проверить вход вживую в задаче `oidc-login` 2026-08-12; +подтверждено прогоном: с выдуманным токеном старт кончается отказом создания +отправителя раньше, чем поднимается HTTP-сервер. + +Отсюда следствие, которое стоит дороже самого неудобства: **поведенческая +верификация живым запуском недоступна проекту вовсе**. Всякая задача, меняющая +наблюдаемое поведение, проверяется только тестами, а «поднять и посмотреть» +остаётся человеку с боевым конфигом. + +Запрет запускаться боевым токеном снимать не надо: второй процесс с тем же +токеном перехватывает обновления у работающего. + +## Затрагивает + +- создание отправителя Telegram при старте в `main.go`; +- секция `[telegram]` конфига и её образец; +- раздел «Запреты» в `CLAUDE.md` — строка про боевой токен остаётся, но рядом + появляется способ поднять сервис без него; +- `docs/review.md`, подраздел «Недоступно проверке»: строка про недоступность + живого прогона снимается или сужается. + +## Критерии приёмки + +- Сервис поднимается с пустым токеном бота: HTTP отвечает, воркеры идут, бот не + создан. Оракул — запуск с конфигом без токена и запрос `GET /health`: код 200. +- Отсутствие бота названо в журнале один раз при старте, а не молчанием. Оракул — + тот же запуск: в выводе есть строка о том, что бот не поднят и почему. +- Поведение с настоящим токеном не изменилось. Оракул — тест на создание + отправителя с непустым токеном: прежний путь сохранён. diff --git a/tasks/items/login-surface-hypotheses.md b/tasks/items/login-surface-hypotheses.md new file mode 100644 index 0000000..01bebbd --- /dev/null +++ b/tasks/items/login-surface-hypotheses.md @@ -0,0 +1,44 @@ +# 🔬 Четыре недоказанные гипотезы о поверхности входа + +- **Тип:** research +- **Категория:** Очередь +- **Зачем:** Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было. + +Провенанс — отчёт триажа ревью задачи `oidc-login` 2026-08-12, +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +раздел «Гипотезы без доказательства». Каждая либо становится задачей, либо +закрывается с причиной; сегодня они не то и не другое. + +## Вопрос + +Работает ли хоть один из четырёх путей на самом деле, и если да — чего стоит +каждый? + +1. **Выход по чужой ссылке.** Адрес выхода сессии не требует и на запросе с + чужого сайта отвечает успехом, убирая куку. Применит ли браузер эту куку при + ограничении `SameSite=Lax` — по коду не выяснить. Если применит, человека + выкидывает молча, а унесённое значение остаётся годным. +2. **Слияние двух учётных записей провайдера с одной почтой.** Обмен ищет запись + по неизменяемому признаку провайдера, а не найдя — по адресу почты. Выдаст ли + Authelia двум разным субъектам один адрес, зависит от её настройки. +3. **Поле снимка в ответе провайдера тянет данные наружу.** Оно сопоставлено + файловому полю учётной записи, и библиотека скачивает названный там адрес — + до потолка размера записи. Шлёт ли Authelia это поле и кто им управляет, + неизвестно. +4. **Анонимный запрос подтверждения почты.** Адрес отвечает успехом и заставляет + сервис слать почту. Почта не настроена, и потолка числа запросов нет. + +## Куда ляжет ответ + +- подтверждённый путь — задачей в беклоге, с провенансом этой разведки; +- опровергнутый — строкой в `docs/security.md`, раздел «Что вне модели» либо + «Что разграничивает доступ», чтобы следующее ревью не открывало его заново; +- то, что зависит от настройки Authelia, — строкой там же, с указанием, какая + именно настройка это решает. + +## Рамки + +Смотрим только четыре названных пути. Первый требует настоящего браузера, второй +и третий — настоящей Authelia либо её настройки из `pet-project-server`; +четвёртый воспроизводится своим прогоном без внешних систем и потому берётся +первым. Прогонов на боевом контуре не делаем. diff --git a/tasks/items/login-url-from-collection-settings.md b/tasks/items/login-url-from-collection-settings.md new file mode 100644 index 0000000..85cc9e2 --- /dev/null +++ b/tasks/items/login-url-from-collection-settings.md @@ -0,0 +1,38 @@ +# 🧹 Строить адрес входа из настроек коллекции, а не из конфига + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Первая половина входа собрана руками из конфига и на настройки провайдера не смотрит, вторая берётся из коллекции: обновление библиотеки изменит только вторую половину. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +пункт срезанного потолком под номером 7. + +Сегодня адрес согласия собирается своим кодом: состав запрашиваемых сведений, +способ проверочного кода и признак ответа записаны у нас, а обмен кода берёт +настройки провайдера из коллекции хранилища. Библиотека умеет собирать этот +адрес сама — она делает это своим обработчиком способов входа. + +Цена расхождения отложенная: адреса провайдера и идентификатор клиента получают +второго потребителя мимо единственного места, где настройки живут, а правка +провайдера в панели на начало входа не влияет вовсе. Обновление библиотеки, +тронувшее форму запроса согласия, доедет до половины протокола и разойдётся +молча — отказом на живой выкладке, которого нечем воспроизвести. + +## Затрагивает + +- эндпоинт `GET /auth/login`, где сегодня адрес согласия собирается вручную; +- поля `auth_url` и `client_id` в секции `[auth]` конфига и их проброс — + часть из них перестаёт быть нужной приложению; +- дельта-спека `access`, требование «Вход через внешнего провайдера» — состав + запрашиваемых сведений и способ проверочного кода перестают быть нашими. + +## Критерии приёмки + +- Адрес согласия строится из настроек коллекции: правка провайдера в панели + меняет адрес, куда уводит вход. Оракул — тест: сменить настройки провайдера в + хранилище и убедиться, что адрес перенаправления изменился. +- Проверочный код и состав запрашиваемых сведений берутся у библиотеки, а не + записаны у нас. Оракул — чтение кода: своих литералов состава больше нет. +- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через + подставного провайдера остаётся зелёным. diff --git a/tasks/items/oidc-login.md b/tasks/items/oidc-login.md deleted file mode 100644 index e5d1401..0000000 --- a/tasks/items/oidc-login.md +++ /dev/null @@ -1,67 +0,0 @@ -# ✨ Пускать в приложение только после входа через OIDC - -- **Тип:** feature -- **Категория:** Очередь -- **Зачем:** HTTP API открыт наружу без аутентификации: любой из интернета заводит задачи за наши деньги и читает чужие расшифровки по идентификатору. -- **Теги:** goal:multi-user - -Двигает пункты 1 и 3 «Завершения» цели: неаутентифицированный запрос к записям -не проходит ни к странице, ни к API; вход идёт через OIDC у Authelia, а выход из -сессии работает. - -Провайдер — Authelia по OIDC. Своей регистрации и своих паролей не делаем, это -граница из [паспорта](../../docs/passport.md). Разграничения записей по владельцу -здесь ещё нет: после входа видно всё, что видно сейчас, — этим занимается -`record-ownership`. - -**Ответ провайдера разбирает PocketBase, а не наш код** — решено 2026-08-11 -([adr](../../docs/adr/ADR-2026-08-11-pocketbase-storage-with-admin-panel.md)). -У её коллекции пользователей настраивается провайдер `oidc` с адресами Authelia, -и учётные записи заводятся сами; проверено на версии 0.39.10, -[docs/research/pocketbase.md](../../docs/research/pocketbase.md). Отсюда порядок: -задача идёт после `pocketbase-storage`, до неё настраивать нечего. - -## Затрагивает - -- настройка провайдера `oidc` у коллекции пользователей PocketBase: адреса - Authelia, идентификатор клиента, секрет, соответствие полей учётной записи; -- эндпоинты входа и выхода, которые PocketBase приносит своими; -- `POST /api/audio` и `GET /api/status/:id` — оба уходят за аутентификацию; -- `GET /health` и `GET /metrics` — остаются открытыми и сессии не требуют; -- хранение сессии: её ведёт PocketBase, и решить надо, чем она предъявляется - приложению; -- секция конфигурации под провайдера: адрес, идентификатор клиента, секрет; -- `docs/security.md` — периметр меняется, и первая его строка перестаёт быть - верной; -- `config.dist.toml` и `internal/config`. - -## Критерии приёмки - -- Запрос к `POST /api/audio` и `GET /api/status/:id` без сессии получает отказ, а - не заводит задачу и не отдаёт текст. Оракул — тест на обоих эндпоинтах без - куки: код ответа 401 либо 302 на вход, тело без данных задачи. Тот же тест - проверяет вторую сторону границы: `GET /health` и `GET /metrics` без куки - отвечают 200. -- Сессия переживает перезапуск приложения. Оракул — тест: запрос с прежней кукой - после пересоздания сервера проходит. -- Выход из сессии закрывает доступ. Оракул — тест: после выхода тот же запрос - получает отказ. -- Секрет провайдера не попадает ни в лог, ни в ответ. Оракул — тест на отсутствие - значения секрета в записанном выводе логгера. -- Первая строка `docs/security.md` описывает новый периметр. Оракул — `task - gate`, шаг `docs.py check`. - -## Рамки - -Владельца у записи здесь не заводим и выборку не сужаем: после входа видно -столько же, сколько сейчас. Инвариант «бот отвечает только тем, кто в белом -списке» не трогаем — он живёт до `telegram-account-link`. Панель администратора -тоже не трогаем: наружу её закрывает Authelia на обратном прокси, а это работа -выкладки. - -`GET /health` и `GET /metrics` за аутентификацию не уходят — решено 2026-08-12. -Сессии нет ни у пробы здоровья, ни у сборщика Prometheus, и вход по личным -токенам эта задача не заводит. Наружу их закрывает то же правило обратного -прокси, что и панель администратора, — работа выкладки. Пока правило не -поставлено, `/metrics` отдаёт наружу объёмы работы сервиса: число задач, размеры -и длительности записей; содержимого расшифровок в них нет. diff --git a/tasks/items/provider-code-out-of-storage-log.md b/tasks/items/provider-code-out-of-storage-log.md new file mode 100644 index 0000000..24689e6 --- /dev/null +++ b/tasks/items/provider-code-out-of-storage-log.md @@ -0,0 +1,44 @@ +# 🐞 Убрать код провайдера из журнала запросов хранилища + +- **Тип:** fix +- **Категория:** Очередь +- **Зачем:** Строка запроса с кодом входа целиком уезжает в таблицу _logs и лежит там пять суток, хотя спека access требует, чтобы код в журнал не попадал. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +пункт срезанного потолком под номером 2. + +Наш собственный журнал чист — код туда не пишет ни одна строка приложения. +Пишет его слой хранилища: он логирует всякий запрос вместе со строкой запроса, +а адрес возврата несёт код параметром. Тест на отсутствие значений в журнале +этого не видит, потому что смотрит только в наш логгер. + +Код провайдера одноразовый и живёт минуты, поэтому это не захват сессии, а +расхождение написанного со сделанным: комментарий в коде и норма спеки +утверждают, что код в журнал не идёт. + +## Воспроизведение + +1. Поднять сервис с настроенным провайдером. +2. Пройти вход и вернуться на адрес возврата. +3. Открыть журнал запросов хранилища — панель, `GET /api/logs` либо файл базы. +4. В строке запроса и в поле `url` виден код провайдера целиком. + +## Затрагивает + +- адрес возврата `GET /auth/callback` и слой журналирования запросов хранилища; +- настройка срока хранения журнала запросов (сегодня умолчание, пять суток); +- дельта-спека `access`, требование «Значение, дающее доступ, не печатается» — + либо норма выполняется, либо изъятие называется поимённо; +- `docs/security.md`, перечень мест, где оседает чувствительное. + +## Критерии приёмки + +- После входа код провайдера не встречается в журнале запросов хранилища. + Оракул — тест: пройти вход подставным провайдером, затем отобрать записи + журнала и убедиться, что значения кода в них нет. +- Строка о запросе к адресу возврата в журнале остаётся: пропажа самого следа + не годится, прослеживаемость входа нужна. Оракул — тот же тест: запись о + запросе есть, кода в ней нет. +- Норма и код сошлись: либо спека выполняется буквально, либо в ней названо + изъятие с ценой. Оракул — чтение требования против исхода первого теста. diff --git a/tasks/items/response-assertions-judge-result.md b/tasks/items/response-assertions-judge-result.md new file mode 100644 index 0000000..99372f4 --- /dev/null +++ b/tasks/items/response-assertions-judge-result.md @@ -0,0 +1,36 @@ +# 🧹 Судить ответ в тестах по готовому ответу + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Проверка, читающая изменяемую карту заголовков обработчика, зелена при неработающем коде: класс всплыл трижды, последний раз на уборке куки входа. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +раздел «Promote candidates», первый пункт. + +Инструмент проверки устроен зеркально настоящему серверу: у сервера заголовки +фиксируются в момент, когда ответ начинают писать, а у него живая карта остаётся +доступной и после. Проверка, читающая живую карту, видит то, чего клиент не +получит, и остаётся зелёной при любой регрессии в этом месте. + +Класс повторяется третий раз — записи журнала дефектов от 2026-08-10, 2026-08-11 +и 2026-08-12, — и все три раза стоил зелёного гейта при неработающем поведении. +Отсюда продвижение: правило в конвенции плюс механизация, а после механизации проза +из конвенции убирается. + +## Затрагивает + +- `docs/conventions/` — новая запись либо раздел существующей: чем судят ответ; +- `docs/conventions/README.md`, перечень механизированного; +- набор шагов `task gate` — место, где живёт проверка правила; +- существующие тесты обработчиков: те, что читают живую карту заголовков. + +## Критерии приёмки + +- Правило записано в конвенциях одной формулировкой, и названо место + механизации. Оракул — чтение `docs/conventions/README.md`: строка есть, ссылка + ведёт в существующее место. +- Проверка, читающая живую карту заголовков, роняет гейт. Оракул — завести такую + строку в любом тесте и прогнать гейт: ненулевой код возврата. +- Проза из конвенции убрана после механизации, а не осталась дублем. Оракул — + чтение записи: правило названо один раз, дальше ссылка на механизацию. diff --git a/tasks/items/rollback-does-not-undo-schema-step.md b/tasks/items/rollback-does-not-undo-schema-step.md new file mode 100644 index 0000000..568fb34 --- /dev/null +++ b/tasks/items/rollback-does-not-undo-schema-step.md @@ -0,0 +1,34 @@ +# 🧹 Назвать в необратимом, что откат кода не откатывает шаг схемы + +- **Тип:** chore +- **Категория:** Очередь +- **Зачем:** Откат бинаря оставляет применённый шаг схемы в силе, и на этом строятся решения о выкладке: сегодня об этом не сказано нигде. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +раздел «Promote candidates». + +Проверено прогоном: шаг схемы применили новой ревизией, затем подняли хранилище +кодом прежней — все правки шага остались на месте. Хранилище считает применённое +по имени файла и о шагах, которых не знает, не догадывается. Обратный шаг +запускается только руками отдельной командой. + +Сегодня в разделе «Необратимое» сказано, что применённая миграция не +переписывается. Не сказано главного для выкладки: **откат кода её тоже не +отменяет**, и версия схемы после отката остаётся новее версии бинаря. Задача +`oidc-login` оставила после себя ровно такой случай — защищённое поле файла +переживает откат, а прежний код токена для него не запрашивает. + +## Затрагивает + +- `CLAUDE.md`, раздел «Необратимое» — строка про применённую миграцию; +- `docs/architecture.md`, раздел эксплуатации — что происходит при откате + выкладки. + +## Критерии приёмки + +- В «Необратимом» сказано, что откат кода не отменяет применённый шаг схемы, и + названо следствие: схема остаётся новее бинаря. Оракул — чтение раздела. +- Названо, чем откат схемы делается на самом деле, если он всё же нужен. Оракул — + чтение той же строки: команда или «руками, отдельным шагом» с адресом. +- Гейт зелёный. Оракул — `task gate`. diff --git a/tasks/items/server-side-login-state.md b/tasks/items/server-side-login-state.md new file mode 100644 index 0000000..ca43424 --- /dev/null +++ b/tasks/items/server-side-login-state.md @@ -0,0 +1,54 @@ +# 🐞 Вести учёт употреблённых состояний входа на сервере + +- **Тип:** fix +- **Категория:** Очередь +- **Зачем:** Одноразовость возврата держится на уборке куки, то есть на браузере: сервер не помнит, какие состояния уже потрачены. + +Найдено ревью задачи `oidc-login` 2026-08-12, отчёт триажа — +[review/report.md](../../openspec/changes/archive/2026-08-12-oidc-login/review/report.md), +остаток пункта 4. + +Носитель состояния здесь — кука, которую сервис ставит на время входа: в ней +лежат выданное состояние и проверочный код, и по ней сверяется возврат. + +Спека требует, чтобы состояние было одноразовым: возврат с уже употреблённым +отвергается наравне с невыданным. Сегодня это выполняется тем, что носитель +состояния убирается у браузера на возврате — и для обычного человека этого +достаточно: второй раз тот же адрес возврата сверку не пройдёт. + +Чего это не закрывает: тот, кто носитель контролирует, поставит его себе заново +и повторит возврат. Отказ тогда наступит только потому, что код у провайдера +одноразовый, — то есть гарантия перенесена на внешнюю систему, чего норма не +допускает. + +Цена сегодняшнего состояния невелика, поэтому задача и отложена: код живёт +минуты, а вход у провайдера всё равно нужен. Цена решения — своё хранение +состояний со сроком жизни и его чистка. + +## Воспроизведение + +1. Пройти вход до конца: получить сессию по возврату от провайдера. +2. Поставить носитель состояния заново — тем же значением, которое сервис выдавал + на первом шаге. +3. Повторить тот же запрос возврата. +4. Сверка состояния проходит, и запрос уходит в обмен. Отказ наступает только + потому, что код у провайдера одноразовый, — то есть одноразовость держит + внешняя система, а не сервис. + +## Затрагивает + +- обработчики начала входа и возврата; +- место хранения употреблённых состояний: своя коллекция хранилища либо память + процесса — выбор входит в задачу; +- дельта-спека `access`, требование «Вход через внешнего провайдера» — сценарий + «Возврат нельзя переиграть» получает настоящий оракул. + +## Критерии приёмки + +- Повторный возврат с тем же состоянием отвергается, даже если носитель + восстановлен вручную. Оракул — тест: пройти вход, затем повторить тот же + возврат с заново поставленным носителем; сессия не открывается. +- Состояния не копятся без предела. Оракул — тест либо чтение кода: у записи + состояния есть срок жизни, и просроченные убираются. +- Вход по-прежнему проходит целиком. Оракул — существующий тест входа через + подставного провайдера остаётся зелёным.