Webhook Publisher
Webhook Publisher
Section titled “Webhook Publisher”Webhook Publisher (Koa) přijímá webhooky ze Strapi a routuje eventy do RabbitMQ exchangů pro asynchronní zpracování.
Klíčové údaje
Section titled “Klíčové údaje”- Port: 1347
- Tech: Koa, JavaScript (bez TypeScriptu)
- Role: Event bridge (Strapi → RabbitMQ)
- Messaging: AMQP do RabbitMQ
Strapi vyvolává webhooky na lifecycle eventech (create, update, delete). Webhook Publisher:
- Přijme webhook ze Strapi (nebo od jiného interního volajícího)
- Extrahuje typ eventu (
user.create,organisation.updateatd.) - Publikuje do odpovídajícího RabbitMQ exchange
- Vrátí
200okamžitě (fire-and-forget), nebo202, pokud musela být zpráva zařazena do in-memory fronty - RabbitMQ dál řeší doručení zprávy consumerům
Průběh eventu
Section titled “Průběh eventu”Strapi (lifecycle hook) / interní volající ↓POST http://webhook-publisher:1347/webhook-publisherContent-Type: application/jsonX-Webhook-Signature: <hmac-sha256-hex>{ "event": "user.create", "data": { ... }} ↓Webhook Publisher ├─ Ověří HMAC podpis (fail-closed) ├─ Namapuje typ eventu na exchange + routing key └─ Publikuje do RabbitMQ (nebo zařadí do in-memory fronty, pokud je RabbitMQ nedostupné) ↓RabbitMQ ├─ exchange `user` (direct, routing key: create/update/delete) ├─ exchange `organisation` (direct, routing key: create/update/delete) ├─ `cloud.events` (topic, routing key: celý název eventu) ├─ `git.events` (topic, routing key: celý název eventu) └─ `notification.events` (topic, routing key: celý název eventu) ↓Consumeři přihlášení na exchange/routing key ├─ auth-service-consumer (exchange user, organisation) ├─ git-connector (git.events) ├─ cloud-connector (cloud.events) └─ notification-service (notification.events)Autentizace
Section titled “Autentizace”POST /webhook-publisher vyžaduje povinnou hlavičku X-Webhook-Signature.
Služba ověřuje HMAC-SHA256 podpis počítaný nad raw request body pomocí
sdíleného secretu WEBHOOK_PUBLISHER_SECRET, porovnávaný přes
crypto.timingSafeEqual (bez časového side-channelu).
Endpoint je fail-closed — odmítne požadavek s 401, pokud platí cokoliv
z následujícího:
WEBHOOK_PUBLISHER_SECRETnení v prostředí nastaven (služba nikdy nespadne zpět na přijímání nepodepsaných požadavků),- hlavička
X-Webhook-Signaturechybí, - vypočítaný podpis neodpovídá hodnotě v hlavičce.
Hodnota hlavičky může volitelně nést prefix sha256=<hex> (běžná konvence
u webhook podpisů) — prefix je před porovnáním odstraněn, pokud je přítomen.
Volající podepisují požadavek takto:
const crypto = require('crypto');
const signature = crypto .createHmac('sha256', process.env.WEBHOOK_PUBLISHER_SECRET) .update(rawBodyString) // musí být přesný string odeslaný jako tělo požadavku .digest('hex');
// hlavička odeslaná s požadavkem:// X-Webhook-Signature: <signature>Všichni interní volající (registration controller, lifecycle hooky,
lifecycle hooky cloud-instance a souvisejících content-types, SCIM
controller atd.) musí podepisovat své požadavky stejným
WEBHOOK_PUBLISHER_SECRET.
Webhook endpoint
Section titled “Webhook endpoint”POST http://webhook-publisher:1347/webhook-publisherContent-Type: application/jsonX-Webhook-Signature: <hmac-sha256-hex-z-raw-body>
{ "event": "user.create", "data": { "id": 1, "email": "user@example.com", "role": { "name": "provisioned" } }}Odpověď (zpráva publikována okamžitě):
{ "success": true}Odpověď (zpráva zařazena do in-memory fronty, protože je RabbitMQ nedostupné):
{ "success": true, "queued": true, "message": "Message accepted and queued for later delivery"}Mapování eventů
Section titled “Mapování eventů”Webhook Publisher určuje cílový exchange a routing key z řetězce event.
Routovací logika žije v index.js v kořeni repozitáře (žádný soubor
src/routes/webhook.js neexistuje).
| Vzor eventu | Exchange | Typ | Routing key | Podmínka |
|---|---|---|---|---|
organisation.* | organisation | direct | suffix za tečkou (create/update/…) | vždy |
registration.create | user | direct | create | vždy — před publikací je transformován na payload user.create |
user.create | user | direct | create | pouze pokud data.role.name === 'provisioned' |
user.update | user | direct | update | pouze pokud data.role.name === 'provisioned', nebo pokud update nese emailVerified, confirmed či blocked (administrátorská úprava) |
cloud-instance.*, cloud-resource.*, inventory.*, cloud-network.*, cloud-subnet.*, cloud-sg.*, cloud-vpn.*, cloud-nat.*, cloud-route-table.*, backup-policy.* | cloud.events | topic | celý název eventu | vždy |
git-repository.* | git.events | topic | celý název eventu | vždy |
organisation.invitation.*, dpa.*, sponsorship.*, onboarding.*, feedback.* | notification.events | topic | celý název eventu | vždy |
Eventy, které neodpovídají žádnému z výše uvedených vzorů, jsou odmítnuty
s 400 Unsupported event type. Eventy user.*, které nesplňují podmínku
provisioned/administrátorské úpravy, jsou přijaty s 200, ale explicitně
odfiltrovány (nejsou publikovány).
Topic vs. direct routing key: u topic exchangů (cloud.events,
git.events, notification.events) je routing key celý název eventu
(např. cloud-instance.provision), takže se fronty consumerů mohou
připojovat přes wildcard patterny. U direct exchangů (user,
organisation) je routing key jen suffix za první tečkou.
Transformace registration.create → user.create
Section titled “Transformace registration.create → user.create”registration.create je speciální případ: nerouti se tak, jak přišel.
Payload je přepsán na event user.create a publikován do exchange user
s routing key create:
// Příchozí{ event: 'registration.create', data: { username, email, password, firstName, enabled, emailVerified } }
// Transformováno a publikováno{ event: 'user.create', data: { username, email, password, enabled, emailVerified, userInfo: { name, enabled, emailVerified }, requiredActions: ['VERIFY_EMAIL'], },}RabbitMQ exchange
Section titled “RabbitMQ exchange”| Exchange | Typ | Subscriberi | Účel |
|---|---|---|---|
user | direct | auth-service-consumer | Synchronizace uživatelů (KC ↔ Strapi) |
organisation | direct | auth-service-consumer | Změny organizací |
cloud.events | topic | cloud-connector | Cloud provisioning / inventář / networking / backup policy |
git.events | topic | git-connector | Operace nad git repozitáři |
notification.events | topic | notification-service | Pozvánky, DPA, sponsorship, onboarding, feedback e-maily/alerty |
platform.events | fanout | event-store-consumer | Sjednocený event log — zrcadlí každou úspěšně publikovanou zprávu |
Degradovaný režim (žádné fallback fronty na straně RabbitMQ)
Section titled “Degradovaný režim (žádné fallback fronty na straně RabbitMQ)”Webhook Publisher se nespoléhá na dead-letter/fallback fronty na
straně RabbitMQ. Pokud je RabbitMQ při startu nedostupné nebo spojení
spadne, služba běží dál (pokud není RABBITMQ_REQUIRED=true) a odchozí
zprávy zařazuje in-memory, přímo v Node procesu:
- Pokusy o spojení se opakují až
RABBITMQ_RETRY_ATTEMPTS-krát (výchozí5), s čekánímRABBITMQ_RETRY_DELAYms (výchozí5000) a exponenciálním backoffem mezi pokusy. - Během odpojení je každá zpráva, která by jinak byla publikována, místo
toho zařazena do in-memory fronty a HTTP odpověď je
202 Acceptedsqueued: true. - Po úspěšném znovupřipojení je in-memory fronta zpětně odeslána do RabbitMQ v původním pořadí.
RABBITMQ_REQUIRED(výchozífalse) určuje, zda je vyčerpaný retry rozpočet fatální:falseumožní službě nastartovat a obsluhovat požadavky v degradovaném režimu, i kdyby RabbitMQ nebylo nikdy dostupné;truezpůsobí, že start selže.
To znamená, že in-memory fronta se ztratí, pokud se proces restartuje dřív, než se stihne znovu připojit — pro dosud nepublikované zprávy neexistuje žádná perzistence na disku ani na straně RabbitMQ.
Konfigurace
Section titled “Konfigurace”.env proměnné:
# RabbitMQ spojeníRABBITMQ_HOST=rabbitmqRABBITMQ_PORT=5672RABBITMQ_USER=guestRABBITMQ_PASS=guest
# Chování při znovupřipojení (degradovaný režim)RABBITMQ_RETRY_ATTEMPTS=5RABBITMQ_RETRY_DELAY=5000RABBITMQ_REQUIRED=false
# Port serveruPORT=1347
# Sdílený HMAC-SHA256 secret pro POST /webhook-publisher (fail-closed, pokud není nastaven)WEBHOOK_PUBLISHER_SECRET=Endpointy
Section titled “Endpointy”Health Check
Section titled “Health Check”GET /health
Response:{ "status": "ok" | "degraded" | "error", "statusMessage": "...", "timestamp": "2024-01-01T12:00:00Z", "rabbitmq": { "connected": true, "host": "rabbitmq", "port": "5672", "queuedMessages": 0 }}status je degraded (HTTP 200), když je RabbitMQ nedostupné, ale
RABBITMQ_REQUIRED je false, a error (HTTP 500), když je RabbitMQ
nedostupné a RABBITMQ_REQUIRED je true.
Webhook (od Strapi / interních volajících)
Section titled “Webhook (od Strapi / interních volajících)”POST /webhook-publisherX-Webhook-Signature: <hmac-sha256-hex-z-raw-body>
Request:{ "event": "user.create", "data": { ... }}
Response:{ "success": true}# Zobrazení logůdocker logs -f webhook-publisher
# Ukázka výstupu:2024-01-01T12:00:00Z [INFO] Received webhook request: {"event":"user.create",...}2024-01-01T12:00:00Z [INFO] Event "user.create" mapped to "user" exchange2024-01-01T12:00:01Z [INFO] Successfully published message to exchange "user" with routing key "create"Troubleshooting
Section titled “Troubleshooting”„AMQP connection refused”
Section titled “„AMQP connection refused””A: RabbitMQ neběží nebo není dostupné:
docker logs sencai-mq# Ověřte, že profil mq běží: ./run-local.sh mqSlužba bude dál obsluhovat požadavky v degradovaném režimu (zprávy
zařazuje in-memory), pokud není nastaveno RABBITMQ_REQUIRED=true.
401 „Missing or invalid webhook signature”
Section titled “401 „Missing or invalid webhook signature””A: Zkontrolujte:
WEBHOOK_PUBLISHER_SECRETje nastaven a shoduje se mezi webhook-publisher a volajícím- Volající podepisuje přesně raw bajty těla odeslané v požadavku (ne znovu serializovaný objekt)
- Název hlavičky je
X-Webhook-Signature(case-insensitive)
Webhook eventy se nedostávají ke consumerům
Section titled “Webhook eventy se nedostávají ke consumerům”A: Zkontrolujte:
- Logy Webhook Publisheru:
docker logs webhook-publisher - RabbitMQ UI (localhost:15672): zkontrolujte exchange a fronty
- Logy consumeru:
docker logs auth-service-consumer
/health ukazuje queuedMessages > 0
Section titled “/health ukazuje queuedMessages > 0”A: RabbitMQ bylo v nějakém okamžiku nedostupné; zprávy čekají v in-memory frontě a při znovupřipojení se automaticky odešlou. Pokud se proces restartuje dřív, než se stihne znovu připojit, tyto zprávy ve frontě se ztratí.
„Unsupported event type” (400)
Section titled “„Unsupported event type” (400)”A: Řetězec event neodpovídá žádnému z rozpoznaných prefixů v index.js
(organisation.*, registration.create, user.*,
cloud-instance.*/cloud-resource.*/inventory.*/cloud-network.*/
cloud-subnet.*/cloud-sg.*/cloud-vpn.*/cloud-nat.*/
cloud-route-table.*/backup-policy.*, git-repository.*,
organisation.invitation.*/dpa.*/sponsorship.*/onboarding.*/feedback.*).
Pokud je potřeba nový prefix eventu, přidejte novou větev do routovací
logiky v index.js.