- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
45 KiB
Модель угроз
Периметр
Сервис открыт наружу, но не анонимен: HTTP-порт опубликован в интернет через
обратный прокси, а приём записи, чтение её карточки и текста и файл записи
требуют, чтобы пришедшего назвала Authelia. С 2026-08-22, задачей
trusted-header-login, называет она его заголовком, который ставит обратный
прокси: своего входа у сервиса не осталось — ни адреса к провайдеру, ни
возврата, ни куки, ни выхода. Прежде сервис вёл вход сам (oidc-login
2026-08-12) и потом семь суток верил выданной куке; теперь Authelia судит
каждый запрос, и отзыв доступа действует со следующего.
Без узнавания открыты проба здоровья, метрики и — с 2026-08-15, задачей spa-skeleton —
само приложение: его разметка и её ресурсы, а вместе с ними всякий путь, не
принадлежащий ни одному корню сервиса. Причина внешняя: заголовок ставит прокси,
и человек, которого прокси не назвал, до приложения дошёл бы только мимо него —
а закрытая разметка выглядела бы поломкой сервиса, а не отказом входа. Данных
открытость не касается — всякий адрес под корнем приложения узнанного
по-прежнему требует. Находки строятся против этого — сегодняшнего — периметра.
Состав того, что отдаётся анонимно, задаёт содержимое собранного приложения,
а каталог его лежит в .gitignore и не судится ничем: всё, что окажется там у
собирающего, уезжает в бинарник и раздаётся. Под каталогом ресурсов оно ещё и
отдаётся с годовым сроком хранения и пометкой «неизменяемо» — отозвать выданное
браузеру сервису нечем.
Целевой периметр добавляет к нему отдельный вход для программ по личным токенам
и два уровня доступа — пользователь видит свои записи, владелец сервиса ещё и
страницу расхода. Разграничение по владельцу записи заведено 2026-08-14
задачей record-ownership: и чтение записи, и файл записи сужены владельцем
записи, а чужая отвечает «не найдено». Целевому периметру недостаёт теперь второго уровня
доступа — страницы расхода для владельца сервиса.
Ничьих записей у сервиса больше не бывает: колонка владельца пустого значения не принимает, и держит это схема хранилища. Прежде такие записи заводил вход Telegram — связи чата с учётной записью сервис не вёл, — и 2026-08-14 вход убран вместе с этим исключением.
Целевой периметр шире сегодняшнего не только входом. Содержимое записи начинает уходить на три новые стороны — языковой модели, в канал уведомлений и на почту. Записи и тексты хранятся бессрочно: решение паспорта от 2026-08-11 сделало сервис архивом. Оба сдвига описаны ниже разделами «Куда уходит содержимое записи» и «Что вне модели».
Третьего сдвига — панели администратора — больше нет, и это снятие. Решением
от 2026-08-11 хранилищем становилась PocketBase, и вместе с ней на том же порту
появлялась панель /_/: доступ ко всем записям, всем файлам и всем пользователям
разом, закрываемый не приложением, а правилом обратного прокси. 2026-08-22,
задачей storage-without-pocketbase, встроенное хранилище убрано целиком:
панели не существует, второго периметра на порту сервиса не осталось, и правилу
прокси нечего закрывать.
Вместе с панелью снят и дефект подменённого знака. Маршрутизатор сравнивал
сегменты пути после раскодирования, поэтому /%5f/ попадал в ту же группу, что и
/_/, а правило прокси, написанное на литерал, такой формы не видело — весь
клиент панели грузился анониму (проверено прогоном 2026-08-15 ревью задачи
spa-skeleton). Лечится он теперь тем, что за обоими адресами не стоит ничего:
оба попадают под общее правило неизвестного пути и отдают разметку приложения.
Проверено прогоном 2026-08-22: /_/, /%5f/ и всякий путь под /api/ отвечают
байт в байт тем же, чем отвечает выдуманный путь вне корней сервиса.
Четвёртый сдвиг был — секрет клиента в базе, — и он снят. Задача
oidc-login 2026-08-12 клала адреса провайдера, идентификатор клиента и его
секрет в настройки коллекции пользователей, и чтение файла базы становилось
равносильно чтению секрета. 2026-08-22 секрета не стало вовсе: обменивать код не
на что, и изъятие из инварианта «Секрет не покидает конфиг» снято вместе с ним.
Вместо него — новый и главный: барьер держится на том, что прокси ставит
заголовок сам. Сервис верит Remote-User, пришедшему с адреса из объявленного
перечня, а перечень этот и есть адрес прокси. Прокси, настроенный добавлять
заголовок вместо замены, оставит рядом со своим значением присланное анонимом —
и аноним войдёт под любым именем. Половину этой беды сервис закрывает сам:
запрос с двумя значениями Remote-User не узнаёт никого. Вторую половину
проверить отсюда нечем: правило живёт в files/caddyproxy/Caddyfile.template
репозитория pet-project-server, и требование к нему такое — заголовки
Remote-* прокси обязан перезаписывать, а не пропускать. Выкладку запускает
человек.
X-Forwarded-For сервис читает сам, и правило чтения закрывает дописывание.
С 2026-08-22 адрес спрашивающего ограничитель частоты берёт из этого заголовка:
иначе счётчик ведётся по адресу пира, а пир теперь всегда один — прокси, — и
бюджет становится общим на весь сервис. Цепочка читается справа налево,
доверенные адреса отбрасываются, и ключом становится первый недоверенный: левым
значением распоряжается сам спрашивающий, а правое приписал ближайший к нам
прокси. Заголовок читается всеми строками, а не одной: цепочка законно приходит
несколькими. Прокси, дописывающий X-Forwarded-For к присланному, этим правилом
покрыт, и требования «перезаписывать, а не дописывать» у сервиса к нему нет — в
отличие от Remote-*. Барьером узнавания заголовок при этом не служит: кто
пришёл, решает адрес самого соединения.
Ширина перечня доверенных адресов — тоже цена, и она принимается сознательно.
Перечень задаёт, чьему Remote-User верить, и всякий, кто дотянулся до сервиса
с такого адреса, называет себя кем угодно. Перечень поэтому обязан покрывать
адрес прокси, а не весь частный диапазон: сеть докера целиком означает «любой
контейнер на хосте», включая чужие. Образец конфига называет узкий пример
именно поэтому.
Пятый сдвиг — логин у провайдера переиспользуем. Ключ учётной записи —
Remote-User, то есть логин человека у Authelia. Логин можно выдать заново
после ухода прежнего владельца, и тогда новый человек при первом же обращении
попадает в существующую запись и получает весь её архив — самое
чувствительное, что у сервиса есть. Сервис этого не различает и различить не
может: неизменяемого признака заголовок не приносит. Не допускать
переиспользования — работа провайдера, и это принятая цена, записанная в
access. Обратная сторона той же цены:
переименование заводит новую запись, а прежняя остаётся с архивом, который
нечем ни слить, ни убрать.
Отсюда главное следствие, из которого читается всё остальное: POST /app/audiorecords требует входа, а размер файла ограничен потолком записи, число
же запросов ограничено только частотой. Вошедший тратит наши деньги на
распознавание столько, сколько захочет: ограничитель частоты под корнем
приложения заведён 2026-08-15 и режет темп, а не общий объём. Квоты по объёму
по-прежнему нет — её заводит per-user-size-quota.
Недоверенный вход
Что приходит извне и каким каналом.
| Вход | Канал | Кто может слать |
|---|---|---|
| Имя пришедшего, имя для показа и почта | Заголовки Remote-User, Remote-Name, Remote-Email |
Обратный прокси — и всякий, кто дотянулся до сервиса с доверенного адреса. Значение принимается: пустое, пробельное, длиннее 255 знаков и с управляющими знаками не узнают никого; два значения одного заголовка не узнают никого тоже. С недоверенного адреса заголовок не действует, и это идёт в журнал предупреждением с адресом пира, но без значения |
| Аудиофайл и его имя | POST /app/audiorecords, multipart-поле audio |
Любой узнанный; неузнанному — 401 до чтения тела. Имя доходит до колонки записи обрезанным по пределу и без управляющих знаков |
| Идентификатор записи | GET /app/audiorecords/{id} и /text |
Любой узнанный; неузнанному — 401, одинаковый для заведённой и незаведённой записи |
| Ключ страницы, размер страницы, состояние отбора | GET /app/audiorecords, параметры запроса |
Любой узнанный; нечитаемый ключ и негодный размер дают 400, а не молчаливую первую страницу |
| Вид текста | GET /app/audiorecords/{id}/text, параметр view |
Любой узнанный; значение вне закрытого перечня даёт 400 |
| Содержимое аудио | Файл, скармливаемый ffmpeg и ffprobe |
Отправитель |
| Текст расшифровки | Поток gRPC от SpeechKit | Yandex, а через него — содержимое записи |
Что добавится вместе с целевым периметром — каждый вход появляется своей задачей, и до неё его нет:
| Вход | Канал | Кто может слать | Чья задача |
|---|---|---|---|
| Токен доступа | Заголовок запроса к /api/ |
Любой из интернета | api-tokens |
| Заголовок, темы, пересказ | Ответ языковой модели | Внешняя модель, а через неё — содержимое записи | llm-insights-adapter |
| Вычитанный текст | Ответ той же модели | То же | literary-text-level |
| Настройки пользователя | Эндпоинт записи своих настроек | Вошедший пользователь | settings-screen |
| Хеш-сумма файла | Поле запроса приёма | Отправитель — и она же решает, отдать ли прежнюю запись | dedup-by-content-hash |
Ответ языковой модели опаснее прочего в этом списке: он приходит текстом, идёт в заголовок записи и оттуда на экран — то есть внешний сервис пишет то, что увидит человек.
Куда уходит содержимое записи
Сегодня запись покидает наш сервер двумя путями: файл уезжает в Yandex Object Storage, оттуда его читает SpeechKit. Третий путь — ответ в Telegram — исчез 2026-08-14 вместе с убранным входом: текст теперь достаётся только своим адресом приложения.
Целевой периметр добавляет три пути, каждый — своей задачей:
| Куда | Что уходит | Чья задача |
|---|---|---|
| Языковая модель за шлюзом bifrost | Текст расшифровки целиком | llm-insights-adapter, затем literary-text-level |
| Канал уведомлений (ntfy через apprise) | Готовый текст либо причина отказа | ntfy-delivery |
| Почтовый сервер | Готовый текст либо причина отказа, на адрес из учётной записи | email-notification |
Каждая из названных задач обязана оставить строку в этом разделе — там это записано их «Затрагивает». Отказ любой из трёх сторон задачу не роняет: текст остаётся в приложении.
Из чего строятся пути и ключи
Раскладка файлов на диске, состав пути к файлу и ключа объекта, имя каталога. Отсюда возможен выход за пределы каталога хранения — запись файла туда, куда путь не предполагался.
- Путь на диске выбирает сервис:
data/records/<ULID записи>/<имя>. Обе части задаёт он сам — подкаталог назван идентификатором записи, имя файла это<ULID><расширение>, — и имя, данное отправителем, не попадает ни в одну из них. Расширение берётся из имени отправителя черезfilepath.Extбез проверки списком;filepath.Extрежет по последней точке и не пропускает разделитель каталогов, но это единственное, что стоит между входом и именем файла. Длина расширения при этом ограничена числом — иначеx.с четырьмястами знаками роняет заведение временного файла. - Ключ объекта в Object Storage — то же имя файла, то есть UUID с расширением. Бакет один на все записи, префикса по пользователю нет. С 2026-08-14 копия там файлом записи не считается: она существует лишь потому, что провайдер читает аудио по адресу, и её ключ живёт в строке попытки распознавания.
- Сохранённый ответ провайдера лежит третьим файлом в том же подкаталоге записи, под именем, которое задаёт сервис. Содержимое там — полный текст речи, а не метаданные, поэтому закрыт он наравне с расшифровкой: адреса, которым его читают снаружи, у сервиса нет вовсе, а путь к нему не пишется ни в журнал, ни в метку метрики, ни в ответ.
- Адрес файла —
GET /app/audiorecords/{id}/file?copy=original|normalized. Право пройти по нему даёт узнавание пришедшего и владение записью, и судится оно там же, где отдаётся файл. Значений на предъявителя сервис не выдаёт вовсе: короткий токен файла ушёл 2026-08-22 вместе со встроенным хранилищем, и отзыв доступа доходит до файла сразу, а не через срок жизни выданного значения. Запрет при этом остаётся: имя файла на диске в журнал не пишется — строка журнала стала бы бессрочным ключом к чужой записи. В журнал идёт расширение своим полем. - Идентификатор записи — ULID, 26 знаков, выдаёт приложение. Он же единственное, что защищает карточку записи, её текст и её файл сверх владения.
- Чужой поверхности на порту сервиса нет. Адреса
/api/collections/...,/api/logs,/api/backups,/api/settings,/api/cronsи панель/_/ушли вместе со встроенным хранилищем 2026-08-22. Отвечает сервис только своими адресами, а всё прочее идёт общим правилом неизвестного пути — норму держит webapp. Что содержимое записи закрыто везде, где лежит, нормирует storage.
Целевой периметр добавляет сюда три вещи, и все три — от новых задач:
- Хеш-сумма содержимого (
dedup-by-content-hash) становится ключом поиска прежней записи. Ищется она в пределах одного пользователя: глобальный поиск отдавал бы чужую расшифровку тому, кто угадал или добыл тот же файл, и заодно сообщал бы, что запись у кого-то уже есть. - Файлы фрагментов (
long-audio-chunking) ложатся рядом с исходным в тот же плоский каталог — раскладка каталога данных меняется, и это необратимо. - Имя отправляемого документа (
long-text-delivery) собирается из идентификатора задачи: имя, данное пользователем, в него не попадает.
Что разграничивает доступ
- HTTP API — заголовок
Remote-User, пришедший с адреса из объявленного перечня доверенных. Адрес берётся у самого соединения, а не из пересылаемого заголовка: пересылаемым распоряжается тот, кто шлёт запрос. Значения, переживающего запрос, сервис не выдаёт вовсе — ни куки, ни токена, — и потому отзыв доступа у Authelia действует со следующего обращения. Собственных токенов сервис не принимает вовсе: значения, предъявленного запросом и дающего доступ помимо заголовка, у него не существует. Прежде такое значение било заголовок — им работал владелец панели; панели нет, и правило приоритета осталось бы правилом без предмета. Область узнавания — корень приложения, и выводится она из объявленного адресного пространства сервиса: слои одеты на корень целиком, вторым списком адресов область не описывается. Проба здоровья, метрики и ресурсы приложения под неё не подпадают — иначе запрос за каждой картинкой стоил бы обращения к базе, а первый такой запрос с новым именем — записи в неё. - Учётная запись — заводится первым обращением с новым логином и находится
по нему же дальше. Ключ — колонка
provider_login, уникальная; править её снаружи нельзя, потому что адреса правки учётной записи у сервиса нет вовсе: своих экранов профиля он не заводит, а поверхности хранилища, правившей запись библиотечным правилом, не осталось. - Файл записи — узнавание пришедшего и владение записью, судимые в самом обработчике отдачи. Отказ наступает на обращении за файлом: другого места, где он мог бы наступить, у сервиса не осталось. Значений, переживающих запрос, сервис не выдаёт ни одного, поэтому отзыв доступа доходит и до файла.
- Кто допущен — решает Authelia, а не сервис. Своей проверки группы приложение не делает: кого пускать, определяет правило провайдера на этого клиента. Правило живёт вне репозитория, в настройках выкладки, и по коду его не проверить. Клиент, настроенный слишком широко, открывает сервис всякому, у кого есть учётная запись в общей Authelia. Решение владельца от 2026-08-12.
- Собственного входа у сервиса нет вовсе. Создание записи, вход по паролю, одноразовый код, обмен кода у внешнего провайдера, восстановление доступа и продление принадлежали встроенному хранилищу и ушли вместе с ним: закрывать больше нечего, и адресов этих не существует.
- Метрики и здоровье —
GET /metricsиGET /healthоткрыты неузнанному: учётной записи нет ни у пробы, ни у сборщика. Заголовок их ответа не меняет и учётной записи на них не заводит. Наружу их закрывает правило обратного прокси — работа выкладки, и сервис на неё не полагается: содержимого записей эти адреса не несут. - Приложение — его разметка и ресурсы открыты неузнанному, и ограничителя частоты на них нет: правило заведено под корень приложения, а раздача стоит вне его. Содержимого записей ни разметка, ни ресурсы не несут: они одинаковы для всех и собраны до всякого запроса. По ответу нельзя узнать, узнан ли кто-то, — узнанному и неузнанному отдаётся одно и то же.
Владение записью в модели данных появилось 2026-08-14: у задачи и у её файла есть владелец. Знание идентификатора задачи правом её читать больше не является — читает её тот, кто её принёс.
Целевой периметр заводит четыре механизма вместо одного белого списка; первый из них уже стоит:
| Механизм | Что даёт | Чья задача |
|---|---|---|
| Заголовок от Authelia через прокси | Право открыть приложение и его эндпоинты — сделано 2026-08-22; прежде то же давала сессия OIDC, с 2026-08-12 | trusted-header-login |
| Владелец у задачи и файла | Чужая запись по её идентификатору отвечает «не найдено» — сделано 2026-08-14 | record-ownership |
| Личный токен | Права своего владельца программе, которой прокси заголовка не ставит | api-tokens |
| Признак владельца сервиса | Страницу расхода и сводку по всем пользователям | admin-stats-screen |
Признак владельца сервиса — второй уровень доступа, которого в сегодняшней
модели нет вовсе: до него всё разграничение сводилось к «свой или чужой».
Откуда он берётся — из группы OIDC или из конфигурации — не решено
(admin-stats-screen).
Панели администратора в этой таблице нет, и это снятие, а не пропуск. До 2026-08-22 суперпользователь встроенного хранилища видел все записи, все файлы и всех пользователей мимо любого из механизмов разграничения, а пускал его свой пароль, а не Authelia. Хранилище ушло, панели не существует, и разграничение у сервиса осталось одно — владение записью.
Владелец сервиса взамен получил одно действие и один инструмент: подкоманда
cmd/devtools resume возвращает остановленную запись в работу. Она ходит в тот
же каталог данных, то есть требует доступа к файлам сервера, а не к сети:
поверхности, открытой в интернет, у неё нет вовсе.
Что чувствительнее чего
- Содержимое записей и расшифровок. Голосовые сообщения — личная переписка;
это самое чувствительное, что здесь есть. С 2026-08-14 оно живёт не одной
колонкой, а шестью таблицами: сама запись (заголовок и краткое описание),
texts(расшифровка и вычитанный текст),structures(реплики со временем),recognitions(попытка распознавания; сохранённый ответ провайдера — полный текст речи — лежит файлом в подкаталоге записи),record_events(журнал событий, содержимого не несёт) иtopics(словарь тем человека). Всякая новая таблица, куда содержимое переезжает, закрывается наравне с записью — норму держит спекаstorage. - Ключи Yandex Cloud —
speech_kit_api_keyи пара ключей Object Storage. Утечка оплачивается деньгами и доступом к бакету. Секрета клиента OIDC в этом списке больше нет: 2026-08-22 он ушёл из конфига и из базы вместе с собственным входом.
Всё перечисленное лежит в config.toml. Файл в .gitignore, на сервер его
кладёт Ansible; gitleaks на pre-commit смотрит только индекс коммита.
Целевой периметр добавляет к списку пять записей, и первая из них — новый вид секрета, которого сегодня в проекте нет вовсе:
- Токены пользователей (
api-tokens). Токен даёт права своего владельца целиком. Срока жизни у него нет. В базе лежит только отпечаток, полное значение показывается один раз при выпуске. Это первый секрет, который хранится в базе, а не в конфигурации. - Ключ языковой модели и адрес шлюза bifrost (
llm-insights-adapter). Утечка оплачивается деньгами. - Пароль почтового сервера (
email-notification). - Адрес почты пользователя — приходит от Authelia и хранится у нас
(
oidc-login,email-notification). - Статистика потребления (
usage-accounting). Текста записей не содержит, но говорит, кто и когда пользовался сервисом и сколько; страница расхода открыта только владельцу. Пароля владельца от панели в этом списке больше нет: он ушёл 2026-08-22 вместе с самой панелью. Секрет, появившийся только ради перевода на встроенное хранилище, пропал, и ключа под него в конфигурации не заводится по той простой причине, что заводить нечего.
Тексты расшифровок в логи не пишутся — логируется длина текста и
идентификаторы. Имя файла, данное отправителем, из журнала приёма убрано
2026-08-11 задачей no-user-filename-in-log; запрет проверяют тесты приёма по HTTP на
успешном пути и на пути отказа — они ищут значение, а не имя поля.
Хвост после последней точки остаётся в журнале, и это объявленное изъятие
инварианта приватности из ../CLAUDE.md, а не незакрытый остаток.
Расширение берётся из имени отправителя дословно (filepath.Ext), поэтому имя
запись.тайное-слово отдаёт тайное-слово, а Разговор с Петровым 11.08 —
08. В журнал оно идёт собственным полем, а не в составе имени файла: по нему
прослеживается путь записи. Читает этот журнал владелец сервиса. Нормализация
расширения в хранилище — отдельная работа, задачи на неё пока нет: формат имени
файла объявлен необратимым и меняется решением человека.
Наружу хвост не выходит. Метки метрик (file_extension у
transcriber_input_file_size_bytes, source_format у
transcriber_conversion_duration_seconds) несут расширение, только приведённое к
закрытому перечню известных форматов; всё прочее заменяется значением other.
Это закрыто задачей no-user-filename-in-log 2026-08-11 вместе с самим именем.
Заодно у метки размера принятой записи пропала ведущая точка (.mp3 стало
mp3) — форма выровнялась с меткой конвертации, которая точку не носила
никогда. Ряды, собранные до выкладки, перестают пополняться: график, отобранный
по старому значению, покажет пустоту, и это не поломка.
Требование важно тем, что GET /metrics открыт вместе с остальным: без
приведения хвост читал бы кто угодно из интернета, а множеством значений метки
распоряжался бы анонимный отправитель.
Два пути утечки токена бота — адрес Bot API в отказе транспорта и отказ сборки
клиента — закрыты задачами no-user-filename-in-log и
local-run-without-telegram-token 2026-08-13 и потеряли предмет 2026-08-14
вместе с убранным входом: ни клиента, ни токена у сервиса больше нет. Разбор
случая остался в review.md — он про класс, а не про Telegram.
Путь, который остался, закрыт задачей telegram-enabled-flag 2026-08-13, и он
шире всякого одного ключа: до неё утечь мог любой секрет конфига. Отказ разбора файла
настроек пересказывался как есть, а библиотека разбора собирает текст отказа из
разбираемого куска — toml.ParseError кладёт в сообщение само значение. Строка
секретного ключа с оборванной кавычкой — типовая поломка криво собранного
шаблона выкладки — уносила ключ в журнал контейнера целиком. Теперь такой отказ
пересобирается своими словами: путь, строка, столбец и последний ключ, без текста
библиотеки; прочие отказы декодера собраны из имён ключей и типов и потому
проходят как есть. Нашло это ревью дизайна, чинилось решением владельца в той же
работе. Правило — conventions/config.md, «Секреты»;
оракулы — internal/config/config_test.go, проверки поломанного файла настроек.
Остаточный риск назван там же: разрез опирается на то, какое семейство отказов
несёт значения в нынешней версии библиотеки.
Что вне модели
Перечислить явно.
-
Атака на сам сервер и на контур. Компрометация хоста, прокси, Docker и Ansible — не наша граница.
-
Машина разработчика и то, что он на ней поднимает. В репозитории лежит
cmd/devtools— оснастка разработчика; её подкомандаproxyвстаёт на место контура: ставит заголовокRemote-Userи переправляет запрос сервису, не проверяя ничего. С 2026-08-15 по 2026-08-22 ту же роль игралcmd/oidcstub, подставной провайдер OIDC. Двух вещей это не отменяет, и обе проверяемы: в образ оснастка не едет (ступень собирает./cmd/transcriberпоимённо), а слушает петлевой адрес. Периметра выкладки она поэтому не касается; кто поднял её у себя в чужой сети, отвечает за это сам. -
Злоупотребление со стороны пользователя из белого списка. Приглашённому доверяем полностью.
-
Достоверность расшифровки. Подмена или искажение текста на стороне SpeechKit не рассматривается.
-
Стойкость к целенаправленной нагрузке. Ограничения по числу запросов и по размеру файла нет, и защищаться от исчерпания диска мы сейчас не пытаемся.
-
Исчерпание диска приглашёнными. Записи и тексты хранятся бессрочно (паспорт, 2026-08-11). Шестичасовая запись весит единицы гигабайт — оценка, а не замер: распределения длин у сервиса нет, а самая длинная проверенная запись — 9,6 МБ (research/pocketbase-defaults.md). Потолок длины стоит открытым вопросом
architecture.md, «Долгие записи». Квот нет — это граница домена, passport.md, «Учёт денег»; расход считаютusage-accountingиadmin-stats-screen. Для модели угроз отсюда следует одно: ни числом запросов, ни размером записи вошедший не ограничен, и защищаться от исчерпания диска мы не пытаемся. Рост каталога данных при этом ничем не наблюдается — открытый вопросarchitecture.md. -
Перерасход денег на внешних сервисах. Распознавание и языковая модель оплачиваются по факту; потолка на пользователя нет по тому же решению.
-
Стойкость
ffmpegк вредоносному входу. Разбор чужого формата отдан внешней программе, своей песочницы вокруг неё нет. -
Удаление данных по требованию. Ни файлы, ни расшифровки не удаляются вовсе. С 2026-08-11 это уже не недосмотр, а следствие решения хранить бессрочно, и тем же днём заведена задача
delete-record: своя запись убирается вместе с файлом, объектом в Object Storage и всеми уровнями текста. Учёт расхода удалению не подлежит по решению человека: деньги потрачены, а строки потребления текста не содержат.Руками запись сегодня убирается только запросом к базе, и порядок в нём несущий. Содержимое живёт в таблицах, перечисленных выше («Что чувствительнее чего»), связи приложений с записью обязательны и каскада не имеют, поэтому удаление самой строки отвергается базой, пока живы приложения. Порядок такой: сперва строки приложений — журнал событий, попытка распознавания, структура, тексты, связи с темами, — потом сама запись, потом её файлы. Файлы при этом убираются одним движением: подкаталог записи под её идентификатором. Тот, кто убрал только файлы, стирает аудио и оставляет полный текст речи — расшифровку, разбивку по репликам и сохранённый ответ провайдера. До
delete-recordэто единственный способ, и он ручной целиком.