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
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-11
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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-токенов).
|
||||
@@ -0,0 +1,36 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
`JWT_SECRET` сейчас обязателен в окружении: его нужно задавать вручную и одинаково на
|
||||
всех инстансах. Хранение секрета в слое метаданных с автогенерацией убирает обязательный
|
||||
секрет из env и даёт стабильное значение на инсталляцию.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Убрать обязательный `JWT_SECRET` из конфига и окружения.
|
||||
- Добавить таблицу `app_settings` (key/value) в оба бэкенда метаданных (PostgreSQL и
|
||||
SQLite) и репозиторий чтения/создания секрета.
|
||||
- При старте после открытия метаданных читать секрет или генерировать его
|
||||
(`crypto/rand`) и сохранять; использовать для `TokenManager` и как fallback при подписи
|
||||
URL объектов.
|
||||
- `URL_SECRET` остаётся необязательным env-оверрайдом.
|
||||
- Обновить `.env.example`, `project.md`, `README.md`.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `app-secrets`: хранение секретов приложения (JWT) в слое метаданных с автогенерацией при первом использовании.
|
||||
|
||||
### Modified Capabilities
|
||||
<!-- Нет изменений требований существующих спек: для клиента поведение аутентификации не меняется. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- `backend/internal/config` (убрать `JWTSecret` и его обязательность).
|
||||
- `backend/internal/app`, `backend/cmd/server` (порядок старта: метаданные → секрет →
|
||||
storage/app).
|
||||
- `backend/internal/storage` (fallback-секрет для подписи URL).
|
||||
- Новая settings-репозиция (`internal/settings`), миграции PostgreSQL и SQLite.
|
||||
- Тесты (контрактные, e2e), `.env.example`, `project.md`, `README.md`.
|
||||
- Не затрагивает формат данных клиента.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Хранение секретов приложения в слое метаданных с автогенерацией: единый стабильный
|
||||
секрет на инсталляцию без обязательной настройки окружения.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Хранение секрета приложения
|
||||
Слой метаданных SHALL хранить секрет подписи токенов приложения и SHALL возвращать один
|
||||
и тот же секрет при последующих запусках.
|
||||
|
||||
#### Scenario: Первый запуск
|
||||
- **WHEN** секрет ещё не сохранён
|
||||
- **THEN** система генерирует случайный секрет и сохраняет его в слое метаданных
|
||||
|
||||
#### Scenario: Повторный запуск
|
||||
- **WHEN** секрет уже сохранён
|
||||
- **THEN** система использует сохранённый секрет и не генерирует новый
|
||||
|
||||
### Requirement: Отсутствие ручного секрета
|
||||
Система SHALL работать без секрета, заданного в окружении, и SHALL не требовать его
|
||||
наличия при старте.
|
||||
|
||||
#### Scenario: Секрет не задан в окружении
|
||||
- **WHEN** переменная секрета не задана
|
||||
- **THEN** система запускается, используя секрет из слоя метаданных
|
||||
|
||||
### Requirement: Конкурентный первый запуск
|
||||
При одновременном первом запуске нескольких инстансов на общей базе метаданных система
|
||||
SHALL обеспечивать единый сохранённый секрет для всех инстансов.
|
||||
|
||||
#### Scenario: Гонка первого запуска
|
||||
- **WHEN** два инстанса одновременно обнаруживают отсутствие секрета
|
||||
- **THEN** сохраняется ровно один секрет, и оба инстанса используют его
|
||||
|
||||
### Requirement: Использование секрета для подписи URL
|
||||
При отсутствии отдельного секрета подписи URL система SHALL использовать секрет из слоя
|
||||
метаданных.
|
||||
|
||||
#### Scenario: URL_SECRET не задан
|
||||
- **WHEN** отдельный секрет подписи URL не задан
|
||||
- **THEN** подпись URL объектов использует секрет приложения из слоя метаданных
|
||||
|
||||
#### Scenario: URL_SECRET задан
|
||||
- **WHEN** отдельный секрет подписи URL задан
|
||||
- **THEN** он используется независимо от секрета приложения
|
||||
@@ -0,0 +1,15 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Репозиторий секретов и миграции
|
||||
|
||||
- [x] 1.1 Добавить таблицу `app_settings` миграциями: PostgreSQL `000010_app_settings` и SQLite `000002_app_settings`; проверка — применение на временных БД и идемпотентность.
|
||||
- [x] 1.2 Реализовать `internal/settings` (`Get`/`Set`/`Ensure`) для pg и sqlite и добавить в `store.Repos`; проверка — контрактный тест: `Ensure` идемпотентен и возвращает одно значение.
|
||||
|
||||
## 2. Проводка и конфиг
|
||||
|
||||
- [x] 2.1 Убрать `Auth.JWTSecret` и его обязательность из конфига; `app.New` принимает готовый `*auth.TokenManager`; `storage.Open` принимает секрет-fallback; проверка — `go build ./...`.
|
||||
- [x] 2.2 В `cmd/server` после открытия метаданных получить/сгенерировать секрет и собрать `TokenManager` и storage; проверка — e2e: перезапуск процесса даёт тот же секрет (токен, выпущенный до перезапуска, валиден после).
|
||||
|
||||
## 3. Документация
|
||||
|
||||
- [x] 3.1 Обновить `.env.example`, `project.md`, `README.md`: убрать `JWT_SECRET`, описать автогенерацию секрета в слое метаданных и роль `URL_SECRET`; проверка — документированный запуск без `JWT_SECRET` стартует.
|
||||
Reference in New Issue
Block a user