# Ссылка на файл открыта знанием записи, а защищает её отсутствие имени в журнале - **Дата:** 2026-08-12 - **Источник:** [../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md](../../openspec/changes/archive/2026-08-12-pocketbase-storage/design.md), раздел «Поле файла не помечаем защищённым, но ссылка не уезжает в журнал» ## Решение Поле файла в хранилище **не помечается защищённым**: ссылка `/api/files/<коллекция>/<запись>/<имя>` работает без токена, и право пройти по ней даёт знание самой ссылки. Взамен имя файла в хранилище **не пишется в журнал ни в каком виде** — ни на успешном пути, ни в тексте отказа. ## Почему Очевидный подход к файлам, отдаваемым в интернет, — закрыть их: у хранилища для этого есть пометка «защищённое поле», и тогда файл отдаётся только по отдельному файловому токену. Мы от неё отказываемся, и цитата из источника называет причину: > Защищённое поле требует отдельного файлового токена. Не помечаем: сегодня право > прочитать задачу даёт знание её идентификатора, и файл встаёт вровень с > `GET /api/status/:id`, а не ниже. Отказ дешёв ровно до тех пор, пока ссылку неоткуда взять. Перевод хранилища это условие сломал, и вот чем: > Изъятие выписано под путь на диске: `data/files/.ogg` читателю журнала > бесполезен. После перевода имя файла в хранилище — это последняя часть ссылки > `/api/files/...`, по которой запись скачивает кто угодно; строка журнала стала > бы бессрочным ключом к чужому аудио. Путь был построен ревью и прогнан: отказ чтения из хранилища нёс ключ файла целиком, строка уходила в журнал, а анонимный запрос по собранному адресу отвечал `200` с телом записи. Второй путь шёл через отказ выгрузки в Object Storage — тот несёт полный URL объекта. Отсюда вторая половина решения, без которой первая недопустима: **отказы обрываются**. Наружу идёт свой текст с идентификатором записи, а чужая цепочка `%w` — нет. В журнал приёма вместо имени идёт расширение собственным полем; прослеживаемость от этого не страдает. Запись попадает в журнал как **намеренный отказ от очевидного подхода**: закрыть файлы токеном предложат снова, и без записанной причины предложение выглядит бесплатным. ## Последствия - `+` ссылка работает без токена, и приёмка проверяется обычным запросом; будущее приложение получает файл без отдельного механизма выдачи токенов. - `+` изъятие из инварианта приватности не расширилось: в журнале по-прежнему только расширение, а не имя. - `−` ссылка, единожды утёкшая, работает бессрочно: отзыва у неё нет, а файлы не удаляются вовсе. Утечка возможна не только журналом — любой будущий экран, показывающий ссылку, наследует это свойство. - `−` появилась норма, которую держит не построение, а внимание: всякий новый отказ хранилища надо обрывать руками. Норму сторожат требование capability `storage` и проверка журнала, но компилятор — нет. - `−` диагностируемость отказов упала: обрывая цепочку, мы теряем причину. У выгрузки в Object Storage это смягчено — сохраняется класс отказа SDK (`AccessDenied`, `NoSuchBucket`), в котором адреса не бывает. - Решение действует до разграничения доступа: задачи `oidc-login` и `record-ownership` меняют условие, и тогда пометку стоит пересмотреть новой записью.