Договор о том, как сервисы экосистемы (bugtrail, gntodo, smart-home, Navi-инстансы, gnexus-auth, ad-hoc скрипты) шлют события в Synapse.
Источник знает только «что случилось у меня». В конверте события нет получателей, каналов и подписок — выбор «кому, куда и каким каналом» целиком принадлежит Routing Engine (правилам в админке). Клиент, который «знает про других клиентов» — признак ошибки проектирования. Когда-то понадобятся адресные алерты конкретному человеку (например assignee bugtrail) — путь через payload-переменную в шаблоне ({{ payload.assignee_email }}), не через поля конверта.
POST /api/v1/events Authorization: Bearer syn_<token> # единый паттерн экосистемы (Bearer) Content-Type: application/json
Для пачек: POST /api/v1/events/batch — тот же конверт в массиве, ответ — список id (частичная валидация честная).
{
"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:
{
"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-ендпоинт).Когда правило направляет событие в системный 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))
Проверка на принимающей стороне:
t=…,v1=… из raw body (как оно пришло, до каких-либо парсингов) и своего секрета.hash_equals, Python hmac.compare_digest) — не через ==.|now - t| в пределах допуска (gnexus-auth — 5 минут) → защита от replay.Секрет — свой у каждой цели (per-target, поле token_ref в channel_targets.config), значения в .env/gnexus-creds, в БД только ссылки. Ротация секрета цели не затрагивает правила маршрутизации.
Цепочка доверия: получатель проверил подпись ⇒ целостность и авторство Synapse. Синтезировать чужое событие Synapse не может — на приёме его ключ и source сверились бы с реестром (403), а ретрансляция чужого ключом источника невозможна, поскольку событие должно нести source, совпадающий с ключом (анти-спуфинг).
Тело s2s-доставки — конверт события без изменений (описание источника в него не входит):
{
"event_id": "uuid",
"source": "monitoring",
"subject": "container", "action": "down", "priority": "critical",
"payload": { "container": "nomin-web", "host": "melody" }
}
description — атрибут записи источника в реестре Synapse (админка), а не поля события: конверт и s2s-вебхук его не несут. Получатель видит имя источника в X-Synapse-Source; зачем оно — смотрит в админке Synapse, где у каждого источника/цели прописано описание («что это за сервис» для человека и ИИ-агента, не гадать по названию).
api_keys (хэш, привязка к source) · notification_types(source, subject, action) · routing_rules (условия, действия, шаблон) · events (конверт + payload + статус) · deliveries (канал, цель, статус, попытки, ошибки) · channel_targets.