POC Webhook Emitter — HMAC + outbox + retry exponentiel
POC :
poc-webhook-emitter/(~600 lignes Kotlin pur, 0 dépendance externe runtime). Référence pourwebhook-svcMVP.Status : 18/18 tests passants. Démo CLI 8 cas (sign/verify/replay/retry/404/429/recover/24h-deadline).
Ce POC démontre les invariants critiques du module webhook signé en code exécutable :
- Signature HMAC-SHA256 déterministe (format Stripe
t=<ts>,v1=<hex>) - Vérification côté tenant avec replay protection 5 min + constant-time compare
- Outbox pattern in-memory mimant
webhook_outboxPostgres - Sweeper processant les entrées DUE selon retry policy
- Retry exponentiel 8 tentatives bornées sur 24 h
- Gestion HTTP : 2xx success / 4xx permanent (sauf 408/429) / 5xx retry / 429 avec Retry-After / timeout retry
- Final deadline : entry FAILED après 24 h sans success
1. Architecture
Section intitulée « 1. Architecture »poc-webhook-emitter/├── build.gradle.kts // Kotlin 2.0, JUnit 5, AssertJ├── src/main/kotlin/io/vitakyc/webhook/│ ├── Model.kt // WebhookEvent, WebhookOutboxEntry, DeliveryAttempt, DeliveryStatus│ ├── Signer.kt // HMAC-SHA256 sign() + verify() avec replay 5 min│ ├── RetryPolicy.kt // calendrier exponentiel 1s/5s/30s/2m/10m/1h/6h/24h-final│ ├── Outbox.kt // WebhookOutbox in-memory + queue + fetchDue + stats│ ├── Sweeper.kt // HttpClient interface + sweep() avec gestion HTTP statuses│ └── Main.kt // démo CLI 8 cas└── src/test/kotlin/io/vitakyc/webhook/ └── WebhookTest.kt // 18 tests2. Invariants démontrés
Section intitulée « 2. Invariants démontrés »2.1 Signer (HMAC-SHA256 + replay)
Section intitulée « 2.1 Signer (HMAC-SHA256 + replay) »| Test | Vérifie |
|---|---|
sign produces deterministic header | invariant trivial mais critique — reproductibilité |
verify accepts correct signature within tolerance | round-trip valide |
verify rejects wrong secret | sécurité — mismatch HMAC |
verify rejects altered body | sécurité — body tampering détecté |
verify rejects out-of-tolerance timestamp | replay > 5 min bloqué |
verify rejects missing parts | header malformé bloqué |
verify is case-sensitive on hex hmac | constant-time compare strict |
2.2 Retry policy
Section intitulée « 2.2 Retry policy »| Test | Vérifie |
|---|---|
retry schedule respects exponential backoff | délais 1s, 5s, 30s, 2m, 10m, 1h, 6h |
final deadline reached after 24h | abandon après 24 h |
2.3 Outbox + sweeper end-to-end
Section intitulée « 2.3 Outbox + sweeper end-to-end »| Test | Vérifie |
|---|---|
queue then sweep — 200 OK marks DELIVERED in 1 attempt | happy path |
503 triggers RETRYING with next attempt scheduled | retry transient |
404 marks FAILED immediately (permanent client error) | distinction permanent vs transient |
408 timeout is retried (transient) | exception 408 retry |
429 with Retry-After respects header | rate-limit handling |
8 attempts then FAILED if endpoint stays 503 | borne max attempts |
503 then 200 on retry — final state DELIVERED | recovery scenario |
final deadline 24h — entry marked FAILED | abandon après deadline |
outbox stats reflects all statuses | dashboard fonctionnel |
2.4 Roundtrip
Section intitulée « 2.4 Roundtrip »| Test | Vérifie |
|---|---|
roundtrip — emitter signs, tenant verifies same body | l’émetteur signe, le tenant vérifie ; preuve d’interopérabilité |
3. Output démo CLI
Section intitulée « 3. Output démo CLI »[1] Queue 3 events (case.decided / bio.verdict / aml.alert) outbox stats: {PENDING=3}
[2] Sweep — first attempt processed=3 stats={DELIVERED=3}
[3] Vérification HMAC côté tenant — succès verify=Valid timestamp=1745000000
[4] Vérification HMAC — secret modifié → mismatch verify=Invalid(reason=signature mismatch)
[5] Vérification HMAC — timestamp out of tolerance (1h trop tard) verify=OutOfTolerance(ts=1745000000, now=1745003600)
[6] Endpoint failing 503 → retry exponentiel attempt=1 status=RETRYING nextAt(+s)=1 attempt=2 status=RETRYING nextAt(+s)=5 attempt=3 status=RETRYING nextAt(+s)=30 attempt=4 status=RETRYING nextAt(+s)=120 attempt=5 status=RETRYING nextAt(+s)=600 attempt=6 status=RETRYING nextAt(+s)=3600 attempt=7 status=RETRYING nextAt(+s)=21600 attempt=8 status=FAILED nextAt(+s)=0
[7] Endpoint 404 → FAILED immédiat (client error permanent) status=FAILED
[8] Endpoint 429 rate-limited → retry avec Retry-After 30s status=RETRYING nextAt(+s)=30
─── Résumé final ───DELIVERED : 3, FAILED : 2, RETRYING : 14. Mapping POC → Production
Section intitulée « 4. Mapping POC → Production »| Élément POC | Production |
|---|---|
WebhookOutbox (HashMap) | Postgres webhook_outbox avec RLS multi-tenant + index sur next_attempt_at |
Sweeper.sweep(now) synchrone | Coroutine Kotlin polling 1s + batch 100 entries |
HttpClient interface | OkHttp ou Ktor avec timeout 30s, TLS 1.2+, mTLS opt-in |
secret: String literal | Vault HSM read avec key path tenant/{tenantId}/webhook-secret |
Signer.sign | identique (même HMAC déterministe) |
Signer.verify (côté tenant) | code samples Node/Python/Java/PHP publiés (cf Webhooks spec §3) |
RetryPolicy | identique bit-à-bit |
Le re-test golden : un payload signé par le POC est vérifié OK par le code prod (et vice versa) — garanti par déterminisme HMAC.
5. Hors scope POC (ajouts en prod)
Section intitulée « 5. Hors scope POC (ajouts en prod) »- Persistence Postgres + RLS multi-tenant + sweeper coroutine
- HTTP réel (OkHttp/Ktor avec timeout, TLS, mTLS opt-in)
- Vault lecture secret tenant + rotation
- Notifications email/Slack sur 3 échecs ou FAILED final
- Dashboard livraison UI tenant
- Test endpoint
/v1/webhooks/testpour valider conf tenant - Replay GUI admin tenant pour rejouer manuellement
- Métriques Prometheus (throughput, latency, success rate, FAILED rate)
- Idempotency cache côté tenant documentation pattern
6. Reproduire le POC
Section intitulée « 6. Reproduire le POC »cd poc-webhook-emitter./gradlew test # 18/18 tests verts./gradlew run # démo CLI 8 casDépendances : Kotlin 2.0.20, JVM 17, JUnit 5.11, AssertJ 3.26. Aucune dépendance runtime externe.
7. Références
Section intitulée « 7. Références »- Webhooks signés — spec engineering — protocole complet + code samples Node/Python/Java/PHP
- ADR-032
- Convention industrie : Stripe webhooks, GitHub webhooks
- Standards : RFC 8785 JSON canonicalization, RFC 2104 HMAC
- POCs liés : poc-aml-rules-engine (outbox pattern partagé)