# 09 · Гайд интеграции: сервис шлёт события в Synapse

Для разработчика (или ИИ-агента), подключающего сервис экосистемы (bugtrail,
gntodo, smart-home, gnexus-auth, Navi…) к хабу уведомлений. Контракт приёма —
`docs/05-ingestion-api.md`; этот документ — порядок внедрения и паттерны.
Клиентские библиотеки: `gn-synapse-client-py` / `gn-synapse-client-php`
(README последних — полный справочник API).

## 0. Кто что делает (роли)

- **Владелец/админ Synapse** (админка или MCP-тулы, docs/07): создаёт
  источник, выдаёт API-ключ (plaintext показывается один раз), регистрирует
  типы (тройки `source,subject,action`), цели и правила.
- **Сервис** (потребитель): хранит ключ в своём env (и в gnexus-creds),
  шлёт события через клиентскую библиотеку, документирует свои тройки.

Админка — единственный способ конфигурировать Synapse: тип не зарегистрирован →
`422` на каждое событие. Подключение сервиса начинается **не с кода, а с
регистрации**: сначала в Synapse, потом код.

## 1. Чеклист подключения нового сервиса

1. Согласовать с владельцем:
   - имя источника (`^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$`, 1..64, латиница
     строчными) — обычно имя репо/сервиса (`bugtrail`, `gntodo`, `navi-rei`);
   - список трой `(subject, action)` — что сервис будет сообщать (см. §4),
     с приоритетом по умолчанию каждой тройки;
   - кому и куда: целевые каналы (`internal_log`, `push`, `telegram`,
     s2s в Navi) и «о ком» — нужен ли `payload.user_id`.
2. Владелец: `source_create` (или §админка → источники) → `key_issue`
   → plaintext-ключ **в .env сервиса и gnexus-creds, нигде больше**;
   `type_register` для каждой тройки; `rule_create`/`rule_set`.
3. Вставить в env сервиса:
   ```
   SYNAPSE_URL=https://synapse.gnexus.space        # или :8013 в LAN
   SYNAPSE_API_KEY=syn_...
   SYNAPSE_DEFAULT_SOURCE=<имя источника>
   ```
4. Код: установить либу, инжектнуть в DI (один инстанс на процесс),
   шлать через `emit`/`send`. Быстрая проверка после первого отправа —
   админка → поток (лог доставок), или `send_test_event` (MCP).

## 2. Установка и init

Python:

```bash
pip install "git+https://git.gnexus.space/git/root/gn-synapse-client-py.git@v0.1.2"
```

```python
from gnexus_synapse import SynapseClient
sync = SynapseClient()                      # конфиг из SYNAPSE_*
async_client = AsyncSynapseClient()         # для FastAPI-контекста
```

PHP (composer, repositories vcs — см. README либы):

```php
$client = new \GNexus\Synapse\SynapseClient(
    \GNexus\Synapse\Config\SynapseConfig::fromGlobals(),
    new \GuzzleHttp\Client(['timeout' => 10.0]),
    new \GuzzleHttp\Psr7\HttpFactory(),
    new \GuzzleHttp\Psr7\HttpFactory(),
);
```

Правило: **один клиент-инстанс на процесс** (переиспользует http-соединения);
при ручном создании — таймаут обязательно (`SYNAPSE_TIMEOUT` и
`timeoutSeconds` у Guzzle-транспорта), иначе вечный вис при лежащем Synapse.

## 3. Четыре паттерна отправки (какой когда)

| Паттерн | Когда | Возврат/падение |
|---|---|---|
| `emit(source?, subject, action, …)` | событие «потери не страшно», основной путь большинства сервисов | `Event\|None`; `SynapseError`/транспорт → warning в лог, не бросаются |
| `send(...)` | событие, чья потеря = инцидент (уведомление о падении монитора, инцидент безопасности) | бросает типизированные исключения (см. README) — у caller'а есть ретри- или эскалационная политика |
| `send_batch([...])` | прогон «сотни мелких» (импорт, массовая рассылка) | `BatchResult.accepted/rejected`; локально битые и серверные отклонения — по индексам, 202 и 422 парсятся одинаково |
| `status(event_id)` | отладка/приёмочный тест нового источника; в бизнес-коде обычно не нужен | `EventStatus` со списком доставок |
| `health()` / `ready()` | readiness-probe сервиса, старт-самоtest | без ключа |

Конвенции полей:

- `priority` — `low|normal|high|critical`; critical — только действительно
  инциденты (обходит throttle правил).
- `user_id=` — конвенция «о ком событие» сразу параметром SDK (uuid gnexus-auth,
  перебивает `payload["user_id"]`); личный лог и push адресуются по нему.
- `dedup_key` — на «повторяющиеся в окне» события (`ping-<день>`,
  `issue-<id>`) чтобы не спамить; **best-effort** (24ч-окно, повтор вернёт id
  первого события с `deduplicated=true`) — не строить бизнес-логику на нём.
- `ttl_seconds` — забыть событие старше N сек (актуальность), по умолчанию окно
  от настроек Synapse.
- `scheduled_at` — резерв контракта, сервер пока не обрабатывает.

Для приёмки нового источника есть ещё `wait_for_status(event_id, statuses=("done","failed"))`
— поллинг статуса тестового события до терминального или таймаут (§6).

## 4. Как называть тройки (subject/action)

Тройки регистрируются админом один раз каждая — расширение списка это
правка реестра, а не кода; берите предсказуемую схему:

- subject — сущность сервиса (`task`, `issue`, `container`, `user`, `auth`);
- action — то, что случилось (`created`, `completed`, `down`, `password_changed`,
  `login_failed`); глаголы в прошедшем времени или существительные — не повелительное.

Шаблон события держите в репо сервиса (или в описании сервиса gnexus-book),
чтобы при регистрации нового типа не догадываться о смысле payload'а.

## 5. Что уже должно быть в сервисе после интеграции (checklist ревью)

- [ ] Клиент инжектится, не создается в каждом вызове; таймаут задан.
- [ ] Обычные события — `emit`; критичные — `send` + `try/catch` с политикой
      (лог + ручной ретри/эскалация — не автомедицина, Synapse сам ретраит доставки по каналам).
- [ ] Ключ — только в env (и копия в gnexus-creds), не в коде/конфиге репозитория.
- [ ] `dedup_key` не в ролях бизнес-идемпотентности.
- [ ] `payload.user_id` — uuid gnexus-auth (если адресные доставки нужны).
- [ ] Все шлющиеся тройки зарегистрированы (в админке Synapse видно типы;
      `422` на проде = забыли тип — добавить `type_register`, не «обходить» в коде).
- [ ] Свои тройки задокументированы (таблица subject × action × priority ×
      «что в payload»).

## 6. Старты и приёмка (для агента)

Быстрая приёмка после первого деплоя интеграции:

1. В либе клиента — интеграционные смоки:
   - py: `examples/plain/smoke.py`, php: `examples/plain-php/smoke.php`
   (требуют источник `libtest` и тип; для реального сервиса подставьте своё имя).
   Дождаться доставки тестового события — `wait_for_status(id, statuses=("done", "failed"))`
   (`waitForStatus` в php), не ручным циклом.
2. Через MCP: `source_list` → `key_issue` → `type_register` →
   `send_test_event` (прогон правила без кода сервиса) → `deliveries_list`.
3. Отрицательные проверки (дешёвые и полезные): неверное имя источника →
   локальная валидация клиента до HTTP; не зарегистрированная тройка → 422;
   отправка с чужим/отозванным ключом → 401; чужой event_id → 404 (`status`).

## 7. Приём s2s-доставки — тот же SDK

Большинство сервисов-приёмников и шлют события (§3) — поэтому приёмная
половина живёт в **том же SDK**, без второй зависимости. Одна точка входа
`verify_webhook()` проверяет подпись (одна схема на всю экосистему — зеркало
`WebhookSignature.php` gnexus-auth и `app/signature.py`) и распарсивает конверт:

Python:

```python
from gnexus_synapse import verify_webhook

@router.post("/webhooks/synapse")
async def synapse_webhook(request: Request):
    env = verify_webhook(await request.body(), request.headers, S2S_SECRET)
    # env["event_id"], env["subject"], env["action"], env["payload"], ...
```

PHP:

```php
use GNexus\Synapse\Webhook\WebhookVerifier;
$envelope = WebhookVerifier::verify($r->getContent(), $r->headers->all(), $secret);
```

Всё, что не прошло (нет подписи, чужой секрет, replay — тело старше 300 с,
не-JSON), ловится одним `SynapseWebhookError` / `InvalidWebhookException`.
Endpoint обязан возвращать 2xx быстро (первая попытка — сразу при
маршрутизации; провал → ретраи Synapse 30 с → 2 м → 10 м → 30 м) и быть
идемпотентным или не бояться повторов. Секрет per-target —
`S2S_SECRET_<token_ref>` в env приёмника (+ gnexus-creds); в Synapse цель
создаётся с `token_ref` (админка/MCP `target_create`). Заголовки-контекст:
`X-Gnexus-Event-Type` = `<source>.<subject>.<action>`, `X-Synapse-Source`.
Полная спецификация подписи — docs/05 → «Доставка s2s».

## 8. Анти-паттерны (каждое из них уже решено на сервере — не повторять в сервисе)

- Локальные очереди/ретраи отправки — Synapse отвечает 202 мгновенно и сам
  ретраит доставки (backoff 30 с → 2 м → 10 м → 30 м, 5 попыток); клиент
  ничего этого не делает.
- Опрос `status()` «до delivered» в бизнес-коде — события fire-and-forget;
  нужен мониторинг — это дежурные метрики Synapse и админка, не поллинг.
- Формирование routing'а в коде (какой канал, кто получатель) — маршрутизация
  живёт правилами админки; сервис знает только «что случилось».
- Своя таблица «доставлено/не доставлено» — Delivery Log у Synapse с полным
  статусами и ретраем.
- Валидировать тройку на клиенте и молча глотать в рантайме: клиентская
  валидация (`statusCode = null`) — ошибка кода, тесты; `422` сервера —
  вопрос регистрации, админ.
- Своя рассылка по пользователям внутри сервиса (email/телеграм из сервиса) —
  это и есть замена Synapse; если канала не хватает, расширяется канальный
  список Synapse.