diff --git a/CLAUDE.md b/CLAUDE.md index f85d598..7ccd648 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,6 @@ 1. Хост и домен деплоя (synapse.gnexus.space?) — VM на libvirt или существующий VPS. 2. Telegram-бот — новый или существующий токен. -3. Единый JSON-контракт событий — черновик в задаче #32. ## Интеграция с gnexus-auth (SSO) @@ -57,6 +56,8 @@ ## Принятые решения +- **Контракт Ingestion (v1 принят, 2026-10-03)** — `docs/05-ingestion-api.md`. Инвариант: источник знает только «что случилось у него» — в конверте нет recipients/topics/каналов; маршрутизация целиком правилами в админке. Конверт: `source` (явное поле; ключ — только удостоверение источника, правила матчают имя), `subject`/`action` (два явных поля, не dotted-строка), `priority` (шкала low/normal/high/critical), `payload` (жёсткая валидация конверта, мягкая — payload, в воркере), `dedup_key`, `ttl_seconds`, `scheduled_at` (резерв). `tags` из v1 убраны. Приём — `POST /api/v1/events` с `Authorization: Bearer syn_*`, ответ `202` мгновенно, статус — `GET /api/v1/events/{id}`. + - **FastAPI** для API (не Django) — обоснование описать в задаче #32 при проектировании API. - **Админ-панель** — SPA на Vue 3 с Gnexus UI Kit; раздаётся как статика контейнером FastAPI. Всё конфигурирование проекта — через админку, UI первичен для настроек. Минимум своего, максимум из кита. - **Доступ к админке** — роли gnexus-auth (SSO): только `admin` и выше. Пользователя с ролью ниже — отсекать явной ошибкой (403 «недостаточно прав»), как на уровне API, так и в UI (гейт после логина). Своих паролей Synapse не хранит. diff --git a/docs/05-ingestion-api.md b/docs/05-ingestion-api.md new file mode 100644 index 0000000..e027f70 --- /dev/null +++ b/docs/05-ingestion-api.md @@ -0,0 +1,81 @@ +# 05 — Ingestion API: контракт приёма событий (v1, принят) + +Договор о том, как сервисы экосистемы (bugtrail, gntodo, smart-home, Navi-инстансы, gnexus-auth, ad-hoc скрипты) шлют события в Synapse. + +## Инвариант + +**Источник знает только «что случилось у меня».** В конверте события нет получателей, каналов и подписок — выбор «кому, куда и каким каналом» целиком принадлежит Routing Engine (правилам в админке). Клиент, который «знает про других клиентов» — признак ошибки проектирования. Когда-то понадобятся адресные алерты конкретному человеку (например assignee bugtrail) — путь через payload-переменную в шаблоне (`{{ payload.assignee_email }}`), не через поля конверта. + +## Endpoint + +``` +POST /api/v1/events +Authorization: Bearer syn_ # единый паттерн экосистемы (Bearer) +Content-Type: application/json +``` + +Для пачек: `POST /api/v1/events/batch` — тот же конверт в массиве, ответ — список id (частичная валидация честная). + +## Конверт + +```json +{ + "source": "monitoring", + "subject": "container", + "action": "down", + "priority": "critical", + "payload": { "container": "nomin-web", "host": "melody", "exit_code": 137 }, + "dedup_key": "container-nomin-web-down-2026-10-03", + "ttl_seconds": 3600, + "scheduled_at": null +} +``` + +| Поле | Обяз. | Смысл | +|---|---|---| +| `source` | да | Явное имя источника (см. «Источник и ключ»). Правила маршрутизации матч **имя**, не токен — ротация ключа не трогает правила. | +| `subject` | да | ЧТО: сущность события — `issue`, `container`, `build`, `motion`… | +| `action` | да | Что с ней случилось: `created`, `down`, `resolved`… | +| `priority` | нет | `low \| normal \| high \| critical`, дефолт `normal`. Фиксированная шкала, не число. `critical` в будущем = обход шумодавов, отдельная очередь. | +| `payload` | нет | Любой JSON. Опциональная JSON Schema типа enforced'ится воркером (не на приёме). Шаблоны каналов должны быть устойчивы к отсутствию полей. | +| `dedup_key` | нет | Ретраи источника: тот же ключ в TTL-окне → `deduplicated: true, id: <первый>`. Дубли в каналах раздражают сильнее всего. | +| `ttl_seconds` | нет | Событие «контейнер упал» бесполезно через час. Дефолт — «вечно», поле обязательно с первого дня — ретрофит дороже. | +| `scheduled_at` | резерв | Не реализуется в MVP; поле зарезервировано. | + +Не существует и не появится в клиентском контракте: `recipients`, `topics`, `channel` — получатели/каналы/подписки живут только внутри Synapse (правила в админке). `tags` из обсуждений **убраны** из v1; вернутся, когда появится реальное правило, требующее тега (решение обсуждалось 2026-10-03). + +## Ответы + +| Код | Когда | +|---|---| +| `401` | Нет/битый ключ | +| `403` | Ключ валиден, но не принадлежит `source` из тела (анти-спуфинг) | +| `422` | Неизвестная тройка `(source, subject, action)` — «тип не зарегистрирован». Payload при приёме не валидируется — deep-валидация в воркере, её исход — Delivery Log, не откат приёма. | +| `202` | Принято → `{"id": "uuid", "status": "queued", "deduplicated": false}` | + +Валидация конверта жёсткая и мгновенная, payload — мягкая. Схема «принял мгновенно → разберусь» ломается, если требовать на входе знание всех payload-схем. + +## Статус события + +`GET /api/v1/events/{id}` — только источнику события, чужой id → `404`: + +```json +{ + "id": "uuid", "status": "delivered", + "deliveries": [ + {"channel": "telegram", "target": "infra-squad", "status": "delivered", "attempts": 1, "error": null}, + {"channel": "internal_log", "status": "delivered", "attempts": 1} + ] +} +``` + +Обратный канал (webhook об изменении статуса) позже; контракт поллинга стабилен — подписки докинутся сверху. Для MVP источники либо fire-and-forget, либо поллят этот GET. + +## Типы и правила (админка) + +- **Реестр типов**: тройка `(source, subject, action)`, опционально JSON Schema payload'а. Заводит админ Synapse в UI. +- **Правило маршрутизации**: условия (`source`, `subject`, набор `actions`, `priority >= X`, позже — payload-матчинг и теги) → действия (каналы + шаблон + цели: TG-чат, SMTP, s2s-ендпоинт). + +## Таблицы БД (постановка #34) + +`api_keys` (хэш, привязка к source) · `notification_types(source, subject, action)` · `routing_rules` (условия, действия, шаблон) · `events` (конверт + payload + статус) · `deliveries` (канал, цель, статус, попытки, ошибки) · `channel_targets`. \ No newline at end of file