docs: Diplodoc setup + полное руководство интеграции по API
Новая документация-as-code на платформе Diplodoc (Yandex open-source) с YFM (Yandex Flavored Markdown). Build → static HTML, deployable куда угодно. Что добавлено: - package.json + .yfm + toc.yaml — Diplodoc setup, навигация для sidebar - index.md — главная страница с tabs для разных audiences - README.md — инструкция для contributors Раздел integration/api/ — 11 страниц про интеграцию consumers: - index — обзор + глоссарий - auth — Keycloak service accounts, scope mapping - read-endpoints — GET /dictionaries, list/by-key, spatial filters - events — Kafka outbox топики, partition keys, idempotency, DLQ - webhooks — HMAC, SSRF guard, retry policy, test ping endpoint - bitemporal — validFrom/validTo + history, future-dated entries, bulk close/export - x-references — FK syntax, validation errors, scope hiding, orphan scanner, v2 cascade preview - caching — TTL / push / snapshot strategies, hybrid pattern - errors — full error code reference (validation/auth/conflict/server), retry strategy - best-practices — 33 numbered rules для consumers - bundle-cuod — все 40 справочников с FK графом и сценариями Обновлены: - ops/README.md — link на новую integration/api/, убран inline <br> - ops/slo.md — broken cross-repo links заменены на text references Build: cd docs && pnpm install && pnpm build → ./_dist (20 HTML files, 7.1M) pnpm serve # → http://localhost:8088 CI deploy job для GitLab Pages — в roadmap (см. docs/README.md). Существующий tech/api-integration.md оставлен в toc как legacy (содержимое мигрировано + расширено в integration/api/).
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
# Webhooks
|
||||
|
||||
Альтернатива Kafka events для consumer'ов которые не хотят / не могут
|
||||
поддерживать Kafka client. Ordinis отправляет HTTP POST на зарегистрированный
|
||||
URL при изменениях справочников.
|
||||
|
||||
{% note info %}
|
||||
|
||||
Если у тебя есть Kafka access — используй [события](events.md). Webhooks
|
||||
проще в развёртывании, но менее надёжны (HTTP retry vs Kafka durable log)
|
||||
и медленнее (HTTP overhead vs Kafka batch).
|
||||
|
||||
{% endnote %}
|
||||
|
||||
## Регистрация подписки
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/subscriptions \
|
||||
-d '{
|
||||
"name": "altum-cache-invalidation",
|
||||
"url": "https://altum-backend.altum.local/webhooks/ordinis",
|
||||
"secret": "<HMAC_SECRET_GENERATED_BY_YOU>",
|
||||
"scopes": ["PUBLIC", "INTERNAL"],
|
||||
"dictionaries": ["spacecraft", "satellite_type", "instrument"],
|
||||
"actions": ["created", "updated", "closed"],
|
||||
"active": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Поля
|
||||
|
||||
| Поле | Тип | Назначение |
|
||||
|---|---|---|
|
||||
| `name` | string | Человекочитаемое имя для admin UI. |
|
||||
| `url` | string | HTTPS endpoint. HTTP блокируется SSRF guard'ом. |
|
||||
| `secret` | string | HMAC-SHA256 секрет для подписи payload (см. ниже). |
|
||||
| `scopes` | array | Фильтр по scope (subset из доступных consumer'у). |
|
||||
| `dictionaries` | array | Фильтр по dictionary names. `null`/`[]` = все доступные. |
|
||||
| `actions` | array | `created` / `updated` / `closed`. `null` = все. |
|
||||
| `active` | bool | Можно временно отключить без удаления. |
|
||||
|
||||
## SSRF guard
|
||||
|
||||
Webhook URLs валидируются перед отправкой:
|
||||
|
||||
- **Запрещено:** private CIDR (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16),
|
||||
localhost, link-local (169.254/16), metadata IPs.
|
||||
- **Запрещено:** non-HTTPS scheme. Только `https://`.
|
||||
- **Запрещено:** URL > 2048 chars.
|
||||
|
||||
Метрика `nsi_webhook_ssrf_rejected_total` increments при попытках. См.
|
||||
prometheus rule `OrdinisWebhookSsrfRejected`.
|
||||
|
||||
## Payload
|
||||
|
||||
POST на зарегистрированный URL с body:
|
||||
|
||||
```json
|
||||
{
|
||||
"deliveryId": "wd-abc123-xyz789",
|
||||
"subscriptionName": "altum-cache-invalidation",
|
||||
"occurredAt": "2026-05-07T15:23:45.123+03:00",
|
||||
"events": [
|
||||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"eventType": "dictionary.changed",
|
||||
"dictionary": "spacecraft",
|
||||
"businessKey": "RESURS-P-3",
|
||||
"action": "updated",
|
||||
"version": 3,
|
||||
"validFrom": "2026-05-07T00:00:00+03:00",
|
||||
"data": { ... }
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
{% note tip %}
|
||||
|
||||
Payload может содержать **несколько events** (`events` array) если они
|
||||
произошли в одной transaction (bulk close, bundle import). Не предполагай
|
||||
single-event payload.
|
||||
|
||||
{% endnote %}
|
||||
|
||||
## HMAC подпись
|
||||
|
||||
Каждый POST имеет header:
|
||||
|
||||
```
|
||||
X-Ordinis-Signature: sha256=<HEX>
|
||||
X-Ordinis-Delivery: wd-abc123-xyz789
|
||||
X-Ordinis-Subscription: altum-cache-invalidation
|
||||
```
|
||||
|
||||
Где `<HEX>` = HMAC-SHA256 от raw body bytes с `secret` который ты задал
|
||||
при регистрации.
|
||||
|
||||
### Verify (Python)
|
||||
|
||||
```python
|
||||
import hmac, hashlib
|
||||
|
||||
def verify(body: bytes, signature_header: str, secret: str) -> bool:
|
||||
expected = "sha256=" + hmac.new(
|
||||
secret.encode(), body, hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac.compare_digest(expected, signature_header)
|
||||
```
|
||||
|
||||
### Verify (Node.js)
|
||||
|
||||
```javascript
|
||||
import crypto from 'crypto'
|
||||
|
||||
function verify(body, signatureHeader, secret) {
|
||||
const expected = 'sha256=' + crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(body)
|
||||
.digest('hex')
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expected),
|
||||
Buffer.from(signatureHeader)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
{% note warning %}
|
||||
|
||||
Verify подпись **до** парсинга JSON. Если invalid — `403 Forbidden`,
|
||||
не трогай payload. Без verify ты уязвим к подделке events от любого, кто
|
||||
знает твой URL.
|
||||
|
||||
{% endnote %}
|
||||
|
||||
## Response contract
|
||||
|
||||
Consumer должен ответить `2xx` в течение 10 секунд:
|
||||
|
||||
| Status | Семантика |
|
||||
|---|---|
|
||||
| `200` / `201` / `202` / `204` | Принято. Ordinis помечает delivered. |
|
||||
| `4xx` | Permanent failure (твой bug). Ordinis НЕ retry. |
|
||||
| `5xx` / timeout | Transient. Ordinis retry с exponential backoff. |
|
||||
|
||||
## Retry policy
|
||||
|
||||
| Попытка | Интервал |
|
||||
|---|---|
|
||||
| 1 | сразу |
|
||||
| 2 | через 30s |
|
||||
| 3 | через 2 мин |
|
||||
| 4 | через 10 мин |
|
||||
| 5 | через 1ч |
|
||||
| 6-10 | каждые 6ч |
|
||||
| 1000+ | DLQ → ручной разбор |
|
||||
|
||||
После 1000 неудач delivery попадает в DLQ (`/api/v1/admin/webhooks/deliveries/dlq`).
|
||||
Alert: `OrdinisWebhookDLQNonEmpty`.
|
||||
|
||||
## Test ping
|
||||
|
||||
Endpoint для проверки доступности URL **до** регистрации:
|
||||
|
||||
```bash
|
||||
curl -X POST -H "Authorization: Bearer $ADMIN_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://ordinis.k8s.264.nstart.cloud/api/v1/admin/webhooks/test-ping \
|
||||
-d '{
|
||||
"url": "https://altum-backend.altum.local/webhooks/ordinis",
|
||||
"secret": "<TEST_SECRET>"
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"statusCode": 200,
|
||||
"responseBody": "OK",
|
||||
"elapsedMs": 142
|
||||
}
|
||||
```
|
||||
|
||||
Либо details про failure (DNS issue, timeout, SSRF reject и т.д.).
|
||||
|
||||
## Best practices
|
||||
|
||||
1. **Idempotency** — webhook может прийти **дважды** (network retry, partial
|
||||
failure). Используй `deliveryId` как dedup key.
|
||||
2. **Async обработка** — отвечай `202 Accepted` сразу, обрабатывай в
|
||||
background. 10s timeout жёсткий — если БД медленная, лучше принять и
|
||||
сложить в очередь.
|
||||
3. **HMAC verify первым** — до парсинга JSON.
|
||||
4. **Metrics** — track received / processed / errors для своего side.
|
||||
Сравнивай с Ordinis metrics при расследовании.
|
||||
5. **Whitelist Ordinis IP** на firewall если строгие правила. IP cluster.142
|
||||
= `192.168.100.142`, но в production будет ingress IP. Спросить у Ordinis
|
||||
team при registration.
|
||||
|
||||
## Alerting (Ordinis side)
|
||||
|
||||
| Alert | Trigger |
|
||||
|---|---|
|
||||
| `OrdinisWebhookHighFailureRate` | failure rate > 50% за 5 мин |
|
||||
| `OrdinisWebhookDLQNonEmpty` | events в DLQ > 0 |
|
||||
| `OrdinisWebhookDispatcherIdle` | 0 deliveries при > 0 active subscriptions |
|
||||
| `OrdinisWebhookSsrfRejected` | SSRF guard сработал > 0 раз за 15 мин |
|
||||
|
||||
## Дальше
|
||||
|
||||
- [События (Kafka)](events.md) — primary delivery channel
|
||||
- [Caching](caching.md) — webhook invalidation pattern
|
||||
Reference in New Issue
Block a user