Files
iu 8638d1bbfc chore(openspec): archive completed changes and sync main specs
- archive local-fs-storage-backend, sqlite-metadata-backend,
  storage-orphan-reconciliation, metadata-managed-jwt-secret, default-local-backends
- sync specs: storage-backends, metadata-backends, storage-reconciliation,
  app-secrets; update photos/share-links
- defaults reflected in specs: SQLite and local FS
2026-10-11 10:19:44 +03:00

77 lines
5.0 KiB
Markdown

# 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-токенов).