Files
md2vk/openspec/specs/publishing/markdown/spec.md
T

134 lines
7.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# publishing/markdown Specification
## Purpose
Перечень требований к сервису md2vk — публикация Markdown на стену VK через официальный VK API,
с защитой доступа, аудитом и инфраструктурой деплоя.
## Requirements
### Requirement: Конвертация Markdown в формат VK
Сервис MUST конвертировать Markdown-текст в VK format_data (bold, italic, link, inline_code) и
разбивать длинные посты на чанки (лимит ~4096 символов, учёт `@` = 2 символа).
#### Scenario: конвертация без публикации
- **WHEN** клиент вызывает `POST /api/v1/convert` с валидным `message_md` и API-ключом
- **THEN** сервис возвращает `{text, format_data:{version:1,items:[...]}}` без обращения к VK
### Requirement: Публикация поста на стене VK
Сервис MUST публиковать посты через официальный VK API `wall.post` (access_token, owner_id,
from_group, format_data), сохраняя запись в архив с статусом published/error.
#### Scenario: успешная публикация
- **WHEN** клиент вызывает `POST /api/v1/publish` с валидным API-ключом, vk_account_id и message_md
- **THEN** сервис вызывает VK API wall.post и возвращает `{success:true, vk_post_id, vk_owner_id, url}`,
а публикация получает статус `published`
#### Scenario: ошибка VK API
- **WHEN** VK API возвращает ошибку (невалидный/отозванный токен, лимиты)
- **THEN** сервис возвращает `{success:false, error}` и сохраняет публикацию со статусом `error`
и текстом ошибки в `error_message`
### Requirement: Управление VK-аккаунтами
Сервис MUST поддерживать несколько VK-аккаунтов на пользователя: добавление (с проверкой токена
через users.get), список, soft-delete; токены хранить только зашифрованными.
#### Scenario: добавление аккаунта с валидным токеном
- **WHEN** клиент вызывает `POST /api/v1/accounts` с api_key и корректным access_token (scope wall)
- **THEN** сервис проверяет токен через VK users.get, шифрует его Fernet и сохраняет аккаунт
#### Scenario: дубликат аккаунта
- **WHEN** клиент добавляет VK-аккаунт, который уже есть у пользователя (тот же vk_user_id)
- **THEN** сервис возвращает 409 и не создаёт дубликат
### Requirement: Аутентификация по API-ключу
Все эндпоинты `/api/v1/*` (кроме `/health`) MUST требовать API-ключ `md2vk_...` (Bearer-заголовок
или поле `api_key` в теле); в БД хранится только SHA-256 хэш; сравнение constant-time.
API-ключ SHOULD NOT позволять прочитать VK-токен.
#### Scenario: запрос без ключа
- **WHEN** клиент вызывает `/api/v1/accounts` без Authorization-заголовка
- **THEN** сервис возвращает 401 с сообщением об использовании Bearer
#### Scenario: неверный ключ
- **WHEN** клиент передаёт ключ, хэш которого не найден в БД
- **THEN** сервис возвращает 401
### Requirement: Шифрование VK-токенов
VK OAuth-токены MUST храниться в БД только в зашифрованном виде (Fernet: AES-128-CBC + HMAC-SHA256);
ключ шифрования — из Docker secret `/run/secrets/token_encryption_key`; расшифровка только в памяти.
#### Scenario: утечка файла БД
- **WHEN** файл `data/md2vk.db` попадает в чужие руки без ключа шифрования
- **THEN** VK-токены не могут быть расшифрованы
### Requirement: Внешний контроль доступа (basic auth + fail2ban)
Прод-домен https://md2vk.nixg.ru MUST быть закрыт HTTP Basic Auth (пользователь estorozhenko,
bcrypt-хэш в Caddyfile). Сервис MUST блокировать IP после 5 неудачных попыток подряд (fail2ban,
jail `md2vk`), с ручной разблокировкой (fail2ban-client unban).
#### Scenario: неверный пароль 5 раз подряд
- **WHEN** клиент 5 раз подряд отправляет запросы с неверным паролем basic auth
- **THEN** fail2ban банит IP (bantime=-1), последующие запросы получают отказ соединения
#### Scenario: ручная разблокировка
- **WHEN** администратор выполняет `fail2ban-client unban <ip>` на vps02
- **THEN** IP снова может обращаться к сервису
### Requirement: Аудит-лог запросов API
Сервис MUST логировать каждый запрос `/api/v1/*` в JSONL (ts, ip, method, path, api_key_prefix,
user_id, status, success, latency_ms, error) с ротацией по дням.
#### Scenario: запрос к API
- **WHEN** любой запрос к `/api/v1/*` завершается
- **THEN** в каталоге логов (AUDIT_LOG_DIR, по умолчанию ./logs) появляется строка JSON
с полями ts, status, latency_ms и др.
### Requirement: Деплой через docker и Caddy
Приложение MUST запускаться docker-контейнером на bigbox, слушать 127.0.0.1:8420, монтировать
./data → /data (SQLite), ./logs → /logs, секрет token_encryption_key; Caddy на vps02 MUST
проксировать md2vk.nixg.ru → 10.8.0.2:8420 с basic auth и TLS (Let's Encrypt).
#### Scenario: проверка здоровья через Caddy
- **WHEN** выполняется `curl https://md2vk.nixg.ru/api/v1/health` с верными учётными данными
- **THEN** возвращается `{"status":"ok"}`
### Requirement: Git-зеркала
Репозиторий MUST иметь истину на gitverse.ru (kpa39l/md2vk) и pull mirror на gitea.nixg.ru
(estorozhenko/md2vk); секреты и данные не коммитятся (.gitignore).
#### Scenario: синхронизация зеркала
- **WHEN** в gitverse появляется новый коммит в main
- **THEN** gitea.nixg.ru подтягивает его по расписанию (mirror interval)
### Requirement: Отложенные посты
Сервис MUST сохранять посты с `publish_date` в статусе `scheduled`. Планировщик (worker,
выполняющий wall.post для scheduled-постов) — открытая задача (SHOULD в будущем).
#### Scenario: отложенная публикация
- **WHEN** клиент вызывает `/api/v1/publish` с publish_date в будущем
- **THEN** публикация сохраняется со статусом `scheduled` и не отправляется в VK сразу