# Design ## Context См. `proposal.md` — Why. Текущее состояние: - `JWT_SECRET` обязателен в env (`config.Load`), используется в `auth.NewTokenManager` (HS256-подпись access-токенов) и как fallback для `URL_SECRET` в `storage.Open`. - Refresh-токены не подписываются секретом (случайные, хранятся хэшем). - Есть два бэкенда метаданных (`postgres`, `sqlite`) за интерфейсом `Repository` и фабрикой `store.NewPostgres/NewSQLite`; миграции раздельные (pg-набор и `migrations/sqlite`). - Порядок старта в `cmd/server`: миграции → метаданные → storage → app. ## Goals / Non-Goals **Goals:** - Хранить секрет приложения в слое метаданных; генерировать при первом использовании. - Убрать обязательный `JWT_SECRET` из окружения. - Единый стабильный секрет на инсталляцию; корректное поведение при конкурентном старте. **Non-Goals:** - Шифрование секрета в БД (БД доверенная). - UI/команда ротации секрета. - Миграция существующих env-секретов в БД. - Изменение формата токенов или наблюдаемого поведения аутентификации для клиента. ## Decisions ### D1. Таблица `app_settings` `app_settings(key TEXT PRIMARY KEY, value TEXT NOT NULL, created_at, updated_at)` в обоих бэкендах. Ключ `jwt_secret`. Отдельные миграции: PostgreSQL `000010_app_settings`, SQLite `000002_app_settings` (консолидированная 000001 уже применена на существующих БД). ### D2. Репозиторий `internal/settings` Интерфейс `Repository { Get(ctx,key) (value string, ok bool, err error); Set(ctx,key,value) error; Ensure(ctx,key, generate func() (string,error)) (string, error) }`. Реализации pg и sqlite. `Ensure`: `SELECT` → если есть, вернуть; иначе сгенерировать, `INSERT ... ON CONFLICT(key) DO NOTHING`, затем `SELECT` (единый секрет при гонке). Добавляется в `store.Repos`. ### D3. Генерация секрета `crypto/rand` 32 байта → base64url (без padding). Значение хранится как строка; в TokenManager и Signer передаётся как есть. ### D4. Порядок старта и проводка `cmd/server`: миграции → метаданные → `secret := repos.Settings.Ensure("jwt_secret", generate)` → `tm := auth.NewTokenManager(secret, accessTTL, refreshTTL)` → `storage.Open(ctx, cfg, secret)` (где `URL_SECRET`, если задан, имеет приоритет, иначе используется `secret`) → `app.New`. `app.New` перестаёт строить TokenManager из конфига и принимает готовый `*auth.TokenManager` (или секрет). ### D5. Конфиг Поле `Auth.JWTSecret` и проверка обязательности удаляются; `JWT_ACCESS_TTL`/`JWT_REFRESH_TTL` остаются. `URL_SECRET` остаётся необязательным. `storage.Open` получает секрет параметром вместо чтения из конфига. ## Risks / Trade-offs - [Смена источника секрета у существующих деплоев] → access-токены инвалидируются, но refresh-токены остаются валидными (не подписаны секретом), клиент молча обновляет access; в худшем случае — перелогин. Отмечаем в доках. - [Конкурентный первый старт на общей БД] → `INSERT ... ON CONFLICT DO NOTHING` + повторный `SELECT` (D2). - [Секрет в открытом виде в БД] → БД доверенная; шифрование — вне рамок. - [Тихая смена поведения] → при `URL_SECRET` не задан подпись URL зависит от секрета из БД; явный `URL_SECRET` снимает зависимость. ## Migration Plan 1. Добавить таблицу и репозиторий секретов; сид отсутствует — генерируется при старте. 2. Переключить `cmd/server`/`app`/`storage` на секрет из метаданных; убрать `JWT_SECRET`. 3. Обновить доки и `.env.example`. 4. Откат: вернуть чтение `JWT_SECRET` из env (код), данные не меняются. ## Open Questions - Нужна ли операция ротации секрета (сброс значения и инвалидация access-токенов).