Přeskočit na obsah

Webhook Publisher

Webhook Publisher (Koa) přijímá webhooky ze Strapi a routuje eventy do RabbitMQ exchangů pro asynchronní zpracování.

  • 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:

  1. Přijme webhook ze Strapi (nebo od jiného interního volajícího)
  2. Extrahuje typ eventu (user.create, organisation.update atd.)
  3. Publikuje do odpovídajícího RabbitMQ exchange
  4. Vrátí 200 okamžitě (fire-and-forget), nebo 202, pokud musela být zpráva zařazena do in-memory fronty
  5. RabbitMQ dál řeší doručení zprávy consumerům
Strapi (lifecycle hook) / interní volající
POST http://webhook-publisher:1347/webhook-publisher
Content-Type: application/json
X-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)

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_SECRET není v prostředí nastaven (služba nikdy nespadne zpět na přijímání nepodepsaných požadavků),
  • hlavička X-Webhook-Signature chybí,
  • 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.

Terminál
POST http://webhook-publisher:1347/webhook-publisher
Content-Type: application/json
X-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"
}

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 eventuExchangeTypRouting keyPodmínka
organisation.*organisationdirectsuffix za tečkou (create/update/…)vždy
registration.createuserdirectcreatevždy — před publikací je transformován na payload user.create
user.createuserdirectcreatepouze pokud data.role.name === 'provisioned'
user.updateuserdirectupdatepouze 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.eventstopiccelý název eventuvždy
git-repository.*git.eventstopiccelý název eventuvždy
organisation.invitation.*, dpa.*, sponsorship.*, onboarding.*, feedback.*notification.eventstopiccelý název eventuvž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.createuser.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'],
},
}
ExchangeTypSubscriberiÚčel
userdirectauth-service-consumerSynchronizace uživatelů (KC ↔ Strapi)
organisationdirectauth-service-consumerZměny organizací
cloud.eventstopiccloud-connectorCloud provisioning / inventář / networking / backup policy
git.eventstopicgit-connectorOperace nad git repozitáři
notification.eventstopicnotification-servicePozvánky, DPA, sponsorship, onboarding, feedback e-maily/alerty
platform.eventsfanoutevent-store-consumerSjednocený 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ím RABBITMQ_RETRY_DELAY ms (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 Accepted s queued: 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í: false umožní službě nastartovat a obsluhovat požadavky v degradovaném režimu, i kdyby RabbitMQ nebylo nikdy dostupné; true způ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.

.env proměnné:

Terminál
# RabbitMQ spojení
RABBITMQ_HOST=rabbitmq
RABBITMQ_PORT=5672
RABBITMQ_USER=guest
RABBITMQ_PASS=guest
# Chování při znovupřipojení (degradovaný režim)
RABBITMQ_RETRY_ATTEMPTS=5
RABBITMQ_RETRY_DELAY=5000
RABBITMQ_REQUIRED=false
# Port serveru
PORT=1347
# Sdílený HMAC-SHA256 secret pro POST /webhook-publisher (fail-closed, pokud není nastaven)
WEBHOOK_PUBLISHER_SECRET=
Terminál
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)”
Terminál
POST /webhook-publisher
X-Webhook-Signature: <hmac-sha256-hex-z-raw-body>
Request:
{
"event": "user.create",
"data": { ... }
}
Response:
{
"success": true
}
Terminál
# 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" exchange
2024-01-01T12:00:01Z [INFO] Successfully published message to exchange "user" with routing key "create"

A: RabbitMQ neběží nebo není dostupné:

Terminál
docker logs sencai-mq
# Ověřte, že profil mq běží: ./run-local.sh mq

Služ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:

  1. WEBHOOK_PUBLISHER_SECRET je nastaven a shoduje se mezi webhook-publisher a volajícím
  2. Volající podepisuje přesně raw bajty těla odeslané v požadavku (ne znovu serializovaný objekt)
  3. 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:

  1. Logy Webhook Publisheru: docker logs webhook-publisher
  2. RabbitMQ UI (localhost:15672): zkontrolujte exchange a fronty
  3. Logy consumeru: docker logs auth-service-consumer

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í.

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.