# 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_<token>       # единый паттерн экосистемы (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-ендпоинт).

## Доставка s2s: подпись и верификация

Когда правило направляет событие в системный webhook (Navi и другие сервисы), Synapse подписывает доставку. Схема — **ровно та же, что у вебхуков gnexus-auth** (`WebhookSignature.php`), так что принимающий код в экосистеме один и тот же.

Заголовки (как у gnexus-auth, плюс один):

```
X-Gnexus-Event-Id: <uuid события Synapse>
X-Gnexus-Event-Type: monitoring.container.down      # "<source>.<subject>.<action>"
X-Gnexus-Event-Timestamp: 1759483200
X-Gnexus-Signature: t=1759483200,v1=<hex>
X-Synapse-Source: monitoring                        # доп. заголовок Synapse
```

Подпись — HMAC-SHA256 по «сырому телу» запроса (то, что уйдёт в сеть, байт в байт):

```
sig = "t=" + unix_timestamp + ",v1=" + hex(hmac_sha256(unix_timestamp + "." + raw_body, secret))
```

Проверка на принимающей стороне:

1. Вычислить ожидаемую строку `t=…,v1=…` из **raw body** (как оно пришло, до каких-либо парсингов) и своего секрета.
2. Сравнить через **константное время** (PHP `hash_equals`, Python `hmac.compare_digest`) — не через `==`.
3. Свежесть: `|now - t|` в пределах допуска (gnexus-auth — 5 минут) → защита от replay.

Секрет — **свой у каждой цели** (per-target, поле `token_ref` в `channel_targets.config`), значения в `.env`/gnexus-creds, в БД только ссылки. Ротация секрета цели не затрагивает правила маршрутизации.

Цепочка доверия: получатель проверил подпись ⇒ целостность и авторство Synapse. Синтезировать чужое событие Synapse не может — на приёме его ключ и `source` сверились бы с реестром (403), а ретрансляция чужого ключом источника невозможна, поскольку событие должно нести `source`, совпадающий с ключом (анти-спуфинг).

Тело s2s-доставки — конверт события плюс описание источника (и человек, и ИИ-агент получателя видят, кто это, без гадания по имени):

```json
{
  "event_id": "uuid",
  "source": "monitoring",
  "source_description": "Демон-контроля контейнеров на swarm-хостах — следит за здоровьем сервисов",
  "subject": "container", "action": "down", "priority": "critical",
  "payload": { "container": "nomin-web", "host": "melody" }
}
```

## Таблицы БД (постановка #34)

`api_keys` (хэш, привязка к source) · `notification_types(source, subject, action)` · `routing_rules` (условия, действия, шаблон) · `events` (конверт + payload + статус) · `deliveries` (канал, цель, статус, попытки, ошибки) · `channel_targets`.