хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
+86
-18
@@ -72,11 +72,11 @@
|
||||
- вшито то, что собрано этим прогоном, а не то, что осталось от прошлого;
|
||||
- шаг следует словарю кодов: отказ сети и реестра — 3, красная сборка — 1, и он
|
||||
**отказывает, а не висит**;
|
||||
- путь, выбранный анонимом, не уходит ни меткой метрики, ни строкой журнала — и
|
||||
журналов **два**: свой, в вывод контейнера, и журнал хранилища, куда
|
||||
библиотека кладёт путь целиком вместе с адресом отправителя. Второй молчит
|
||||
только на успехе и только потому, что признак отказа от записи поставлен
|
||||
руками: готовая раздача статики ставит его сама, своя — нет.
|
||||
- путь, выбранный анонимом, не уходит ни меткой метрики, ни строкой журнала.
|
||||
Журнал у сервиса с 2026-08-22 **один** — свой, в вывод контейнера: второй
|
||||
ушёл вместе со встроенным хранилищем, которое клало путь целиком вместе с
|
||||
адресом отправителя. Правило при этом расширилось, а не сузилось: путь не
|
||||
пишется дословно ни под каким корнем, включая корень приложения.
|
||||
|
||||
**Клиент внешнего сервиса** (`adapter/recognizer/yandex`):
|
||||
|
||||
@@ -86,17 +86,20 @@
|
||||
- вырожденный ответ (пустой, усечённый, без ожидаемого поля) не превращает в
|
||||
успех молча.
|
||||
|
||||
**Репозиторий хранилища** (`internal/adapter/repo/pocketbase`; шаги схемы —
|
||||
**Репозиторий хранилища** (`internal/adapter/repo/sqlite`; шаги схемы —
|
||||
подпакетом `migrations`):
|
||||
|
||||
- список колонок совпадает в обоих местах — `applyOwnedByPipeline` вместе с
|
||||
`applyToRecord` и `recordToAudioRecord` — и в шаге схемы (инвариант
|
||||
[CLAUDE.md](../CLAUDE.md), «Инварианты»);
|
||||
- список колонок совпадает во всех трёх местах — `writeOwnedByPipeline` вместе с
|
||||
`writeRecord`, `readRecordColumns` и `rowToAudioRecord` — и в шаге схемы
|
||||
(инвариант [CLAUDE.md](../CLAUDE.md), «Инварианты»). Колонки называются
|
||||
**именами**: именованный параметр запроса и место назначения по имени, а не
|
||||
позиция в списке;
|
||||
- захват задачи не выдаёт одну строку двум вызывающим, а результат пишет только
|
||||
держатель захвата;
|
||||
- репозиторий кладёт время в сыром запросе тем же видом, каким хранилище пишет
|
||||
свои `created`/`updated` ([database.md](database.md), «Представление данных»);
|
||||
- отказ хранилища не выходит наружу дословно: он несёт ключ файла целиком.
|
||||
держатель захвата, и держатель узнаётся значением признака;
|
||||
- репозиторий кладёт время тем же видом, каким его кладут остальные, и берёт его
|
||||
из единой точки ([database.md](database.md), «Представление данных»);
|
||||
- отказ хранилища не выходит наружу дословно: он несёт ключ файла и путь к нему
|
||||
целиком.
|
||||
|
||||
**Обёртка над внешним процессом** (`adapter/converter/ffmpeg`,
|
||||
`adapter/metaviewer/ffmpeg`):
|
||||
@@ -256,7 +259,7 @@
|
||||
|
||||
- изменение, трогающее конвейер задач целиком: состояние, воркер, шаг сервиса и
|
||||
колонку разом;
|
||||
- замена хранилища или переход на PocketBase — любой её кусок;
|
||||
- замена хранилища — любой её кусок;
|
||||
- смена модели очереди: захват, повторы и воркеры разом;
|
||||
- каркас приложения: сборка фронтенда, раздача статики и шаг гейта разом;
|
||||
- изменение, убирающее или возвращающее вход приёма целиком.
|
||||
@@ -301,9 +304,10 @@ API и имя не откатываются обратной правкой по
|
||||
**весь** барьер: он обязан заголовки `Remote-*` перезаписывать, а не пропускать
|
||||
пришедшие. Проверить это отсюда нечем — правило живёт в `pet-project-server`
|
||||
([adr/ADR-2026-08-12-access-delegated-to-provider.md](adr/ADR-2026-08-12-access-delegated-to-provider.md));
|
||||
- `security`: поведение браузера с куками. Своих кук сервис больше не ставит
|
||||
(2026-08-22), и класс сузился до кук, которые ставит панель хранилища; браузера
|
||||
в прогоне нет, и находки этого рода остаются гипотезами.
|
||||
- `security`: поведение браузера с куками. Своих кук сервис не ставит с
|
||||
2026-08-22, а вместе со встроенным хранилищем ушли и те, что ставила его
|
||||
панель. Класс опустел, и строка стоит здесь затем, чтобы возврат кук читался
|
||||
как возврат недоступного проверке, а не как обычная работа.
|
||||
|
||||
**Перестали проверять сознательно:**
|
||||
|
||||
@@ -315,7 +319,8 @@ API и имя не откатываются обратной правкой по
|
||||
- **работа сервиса с настоящими внешними собеседниками.** Сам сервис поднять
|
||||
можно: он встаёт своим единственным входом на выдуманных непустых ключах
|
||||
секций `[auth]` и `[yandex]` — наружу они на старте не ходят. Живой прогон —
|
||||
осмотр HTTP, панели, журнала, метрик и остановки — доступен любой задаче.
|
||||
осмотр HTTP, журнала, метрик и остановки — доступен любой задаче; панели среди
|
||||
предметов осмотра нет с 2026-08-22.
|
||||
Прежняя формулировка «всё, что требует поднять сервис целиком» снята задачей
|
||||
`local-run-without-telegram-token` 2026-08-13; рецепт прогона менялся дважды —
|
||||
с пустого ключа доступа на выключенный вход (`telegram-enabled-flag` того же
|
||||
@@ -341,6 +346,69 @@ API и имя не откатываются обратной правкой по
|
||||
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
||||
оракул, и выдумывать оракул задним числом нельзя.
|
||||
|
||||
## 2026-08-23 — путь, выбранный анонимом, уезжал в журнал под корнем приложения [пойман ревью]
|
||||
|
||||
- **Где:** `internal/controller/http/journal.go`, `JournalRoute`; задача
|
||||
`storage-without-pocketbase`
|
||||
- **Симптом:** неузнанный писал в журнал владельца свой текст произвольной длины.
|
||||
Путь под корнем приложения уходил в строку дословно — в том числе при ответе
|
||||
`401`, потому что слой журнала стоит снаружи ограничителя частоты
|
||||
- **Причина:** правило «путь спрашивающего в журнал не идёт» было записано только
|
||||
для запроса, отданного приложению. Путь вида `/app/<текст>` принадлежит
|
||||
сервису, под то правило не подпадал и уезжал целиком, хотя множеством значений
|
||||
под корнем распоряжается тот же аноним
|
||||
- **Чем воспроизведён:** прогон враждебного прохода — путь в 1 044 480 знаков дал
|
||||
прирост журнала в 1 044 632 байта; одно соединение за 1,003 с дало 122 запроса
|
||||
и 121,5 МиБ журнала; 120 отказов ограничителя оставили 240 строк
|
||||
- **Почему не поймали раньше:** правило записали по месту, где его впервые
|
||||
понадобилось применить, а не по признаку «значением распоряжается спрашивающий».
|
||||
Зазор был ровно шириной в корень приложения
|
||||
- **Что меняем:** `JournalRoute` обобщает всё, что накрыто корнем приложения, а
|
||||
длину отдаёт полем `http.path_length`; дословно пишутся только адреса из
|
||||
закрытого перечня. Правило в [conventions/logging.md](conventions/logging.md)
|
||||
переписано на все корни разом — оно стоит теперь у строки о всяком входящем
|
||||
запросе, а не у строки о раздаче приложения
|
||||
|
||||
## 2026-08-23 — ключ бюджета ограничителя выбирал тот, кого ограничивают [пойман ревью]
|
||||
|
||||
- **Где:** `internal/controller/http/rate_limit.go`, `clientAddress`; задача
|
||||
`storage-without-pocketbase`
|
||||
- **Симптом:** ограничитель пропустил 1200 запросов одного спрашивающего при
|
||||
бюджете 120 за окно. Заодно карта счётчиков росла линейно от числа выдуманных
|
||||
адресов
|
||||
- **Причина:** адрес брался из **левого** значения `X-Forwarded-For`, а прокси
|
||||
заголовок дописывает, а не заменяет. Левым значением распоряжается сам
|
||||
спрашивающий, значит он же выбирает и ключ карты — и меняет его на каждом
|
||||
запросе
|
||||
- **Чем воспроизведён:** прогон враждебного прохода — 1200 пропущенных запросов
|
||||
при бюджете 120; 200 000 ключей в карте дали прирост кучи в 19 810 376 байт
|
||||
- **Почему не поймали раньше:** слой писался заново вместе с транспортом, а
|
||||
свойство «ключ бюджета не выбирает тот, кого ограничивают» не стояло ни в
|
||||
конвенции, ни в типовом узле — его держала прежде чужая библиотека
|
||||
- **Что меняем:** цепочка читается справа налево, доверенные адреса
|
||||
отбрасываются, ключом становится первый недоверенный, а заголовок читается
|
||||
всеми строками, а не одной. Требование к контуру этим снято: дописывающий
|
||||
прокси правилом покрыт — [security.md](security.md), «Периметр»
|
||||
|
||||
## 2026-08-23 — инвариант о колонках записи потерял предмет [пойман ревью]
|
||||
|
||||
- **Где:** [CLAUDE.md](../CLAUDE.md), «Инварианты»;
|
||||
`internal/adapter/repo/sqlite/record_mapping.go`, `internal/archrules`
|
||||
- **Симптом:** инвариант называл поимённо `applyOwnedByPipeline`, `applyToRecord`
|
||||
и `recordToAudioRecord` — функций с такими именами в коде уже не было. Сослаться
|
||||
на инвариант как на оракул стало нельзя
|
||||
- **Причина:** сторож и отображение переписаны под новую форму хранилища, а текст
|
||||
инварианта остался от прежней. Мест при этом стало три: что спрошено
|
||||
(`readRecordColumns`), куда лягут (`recordRow`) и что доедет до сущности
|
||||
(`rowToAudioRecord`), — а сверялось правилом одно
|
||||
- **Чем воспроизведён:** `grep` по трём прежним именам — пусто; `grep` по
|
||||
`rowToAudioRecord` в `internal/archrules` — пусто
|
||||
- **Почему не поймали раньше:** инвариант проверяется правилом, а имена в его
|
||||
тексте — ничем. Текст и сторож разошлись молча
|
||||
- **Что меняем:** инвариант назван действующими именами и действительным числом
|
||||
мест; правило `internal/archrules` расширено на `rowToAudioRecord` — перечень
|
||||
колонок чтения сверяется с перечнем присвоений в сущность
|
||||
|
||||
## 2026-08-15 — короткая форма рецепта входа не работала, а проверяли длинную [пойман ревью]
|
||||
|
||||
- **Где:** `cmd/oidcstub` — подставной провайдер OIDC для локального входа;
|
||||
|
||||
Reference in New Issue
Block a user