PD-180 — Expression de besoin : Webhooks pour événements utilisateur
| Champ | Valeur |
| Story | PD-180 |
| Epic | PD-186 (Backend Core) |
| Projet | ProbatioVault-backend |
| Auteur | Claude (étape 0 — gouvernance IA) |
| Date | 2026-03-07 |
1. Contexte
ProbatioVault s'industrialise vers le marché B2B : cabinets RH, garages automobiles, assurances, SIRH, legaltechs. Ces partenaires intègrent ProbatioVault dans leurs systèmes d'information existants (ERP, SIRH, GED) et ont besoin d'être informés en temps réel lorsqu'un événement métier survient — dépôt de document, scellement probatoire, génération de preuve, partage, révocation.
Aujourd'hui, la seule option pour un système tiers est le polling : interroger périodiquement l'API REST pour détecter les changements. Ce mécanisme est inefficace, coûteux en bande passante, et introduit une latence incompatible avec les workflows automatisés des partenaires (ex. : notification immédiate d'un scellement pour déclencher un workflow d'archivage légal côté client).
Les webhooks sortants sont le standard de l'industrie (Stripe, GitHub, Twilio) pour résoudre ce problème : ProbatioVault notifie proactivement les systèmes tiers en envoyant une requête HTTPS signée vers un endpoint configuré par le partenaire.
2. Problème
Pour les partenaires B2B
- Le polling continu génère un trafic réseau disproportionné (99% de requêtes sans changement)
- La latence de détection dépend de la fréquence de polling (compromis coût/réactivité)
- Chaque partenaire doit implémenter et maintenir sa propre boucle de polling
- Pas de garantie de détection exhaustive (fenêtre entre deux polls)
Pour ProbatioVault
- Le polling massif surcharge l'API REST (amplification N partenaires × M fréquence)
- Aucun mécanisme de notification push n'existe dans l'architecture actuelle
- L'absence de webhooks est un bloqueur d'adoption pour les intégrateurs B2B qui exigent ce standard
3. Objectif
Implémenter un système de webhooks sortants permettant à ProbatioVault de notifier automatiquement les systèmes tiers lors de la survenue d'événements métier, avec les propriétés suivantes :
- Sécurisé — Signature HMAC SHA-256, HTTPS obligatoire, anti-rejeu par timestamp
- Robuste — Livraison asynchrone via BullMQ, retry exponentiel, tolérance aux pannes
- Auditable — Journal append-only de chaque tentative de livraison, rétention 30 jours
- Multi-tenant — Isolation stricte par organisation via RLS PostgreSQL
- Zero-knowledge — Aucune donnée sensible, aucune clé, aucun contenu en clair dans les payloads
4. Périmètre fonctionnel
4.1 Événements couverts (v1 — 7 événements)
| Événement | Déclencheur |
document.created | Document déposé dans le coffre |
document.sealed | Document scellé WORM + ancrage blockchain initié |
document.anchored | Preuve composite générée (ancrage confirmé) |
proof.generated | Export de la preuve composite |
document.shared | Partage PRE (Proxy Re-Encryption) effectué |
document.revoked | Accès à un document révoqué |
account.device.revoked | Révocation d'un device sur le compte |
4.2 Gestion des abonnements (API REST)
- Création d'un webhook : URL cible, sélection des événements, génération automatique du secret HMAC
- Lecture : liste des webhooks de l'organisation, détail d'un webhook
- Mise à jour : URL, événements souscrits, activation/désactivation
- Suppression : désactivation puis suppression logique ou physique
- Rotation du secret : génération d'un nouveau secret HMAC (l'ancien est invalidé immédiatement)
- Endpoint de test (ping) : envoi d'un événement
webhook.ping pour valider la connectivité - Replay : ré-émission d'un événement déjà livré (à partir de son
event_id)
{
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"event_type": "document.sealed",
"timestamp": "2026-03-07T14:30:00.000Z",
"data": {
"doc_id": "...",
"hash": "SHA3-256 du document",
"user_id": "...",
"metadata": {}
}
}
Principe zero-knowledge : le champ data ne contient que des identifiants et métadonnées neutres. Aucun contenu de document, aucune clé cryptographique, aucun blob chiffré.
4.4 Signature et vérification
- Header :
X-ProbatioVault-Signature: t=<timestamp>,v1=<HMAC_SHA256(timestamp.payload, secret)> - Le destinataire vérifie : recalcul du HMAC, comparaison timing-safe, rejet si
|t - now| > 5 minutes
4.5 Livraison et retry
- Livraison asynchrone via worker BullMQ dédié
- Retry exponentiel : 1 min → 5 min → 15 min → 1 h (puis toutes les heures)
- Maximum 10 tentatives
- Timeout de livraison : 5 secondes (configurable)
- Pas de suivi de redirection HTTP (prévention SSRF)
- Échec final (10 tentatives épuisées) : statut
FAILED, notification interne
4.6 Journal des livraisons
- Enregistrement append-only de chaque tentative : timestamp, event_id, webhook_id, statut HTTP, durée, tentative N/10
- Rétention : 30 jours
- Consultable via API REST (historique de livraison par webhook)
5. Contraintes
| Contrainte | Valeur |
| Webhooks par organisation | Maximum 5 |
| Rate limit événements | 100/min par organisation (excédentaires en file) |
| Rétention logs livraison | 30 jours |
| Protocole | HTTPS obligatoire (HTTP refusé) |
| Timeout livraison | 5 secondes (configurable) |
| Redirections HTTP | Refusées (prévention SSRF) |
| Retry max | 10 tentatives, backoff exponentiel |
| Scope abonnements | Organisation (tenant B2B), pas utilisateur B2C |
| Architecture | Stateless NestJS + workers BullMQ |
| Isolation données | RLS PostgreSQL par organisation |
6. Invariants de sécurité
| ID | Invariant |
| INV-01 | Aucune donnée sensible (contenu document, clé crypto, blob chiffré) dans le payload webhook |
| INV-02 | Tout payload est signé HMAC SHA-256 avant envoi — aucune livraison sans signature |
| INV-03 | Le secret HMAC est généré côté serveur, stocké chiffré, jamais exposé après création |
| INV-04 | Toute tentative de livraison est journalisée en append-only AVANT l'envoi |
| INV-05 | Un webhook ne peut accéder qu'aux événements de son organisation (RLS) |
| INV-06 | L'émission d'un webhook ne doit JAMAIS précéder la journalisation probatoire de l'événement |
| INV-07 | Les webhooks ne dépendent pas du HSM et ne modifient aucun état probatoire |
| INV-08 | La comparaison du secret lors de la rotation utilise crypto.timingSafeEqual |
| INV-09 | Le binding organisationnel est extrait du JWT (req.user.orgId), jamais du body |
| INV-10 | HTTPS obligatoire — toute URL en HTTP est rejetée à l'enregistrement et à la livraison |
7. Hors périmètre (v1)
| Exclusion | Justification |
| UI d'administration des webhooks | Story séparée (front-end) |
| IP allowlist | Complexité réseau — sécurité v1 couverte par HMAC + HTTPS |
| Filtrage avancé par metadata | Sélection par event_type suffit pour v1 |
| Abonnements utilisateur B2C | Webhooks destinés aux intégrations système B2B |
| Visualisation DLQ dans UI | Consultable via API, UI future |
| Événements custom (définis par le partenaire) | Périmètre fermé aux 7 événements définis |
8. Critères de succès
| # | Critère |
| 1 | Les 7 événements déclenchent correctement un webhook vers l'URL configurée |
| 2 | La signature HMAC est vérifiable par le destinataire (documentation + exemple de vérification) |
| 3 | Le retry exponentiel fonctionne jusqu'à 10 tentatives puis marque FAILED |
| 4 | L'isolation RLS empêche tout accès cross-tenant (test d'accès croisé) |
| 5 | Le journal des livraisons est consultable et retrace chaque tentative |
| 6 | L'endpoint ping valide la connectivité de bout en bout |
| 7 | Le replay ré-émet un événement existant avec une nouvelle tentative de livraison |
| 8 | La rotation du secret invalide immédiatement l'ancien secret |
| 9 | Aucune donnée sensible ne fuit dans les payloads (audit zero-knowledge) |
| 10 | Le rate limit à 100 evt/min par org est respecté (excédentaires en file, pas perdus) |
9. Risques identifiés
| # | Risque | Impact | Mitigation |
| R1 | SSRF via URL webhook malveillante | Élevé | HTTPS only, pas de redirection, validation URL, timeout strict |
| R2 | Secret HMAC compromis | Élevé | Rotation de secret, stockage chiffré, timing-safe comparison |
| R3 | Surcharge du worker BullMQ (pic d'événements) | Moyen | Rate limit 100/min par org, concurrence configurable du worker |
| R4 | Endpoint destinataire lent ou indisponible | Moyen | Timeout 5s, retry exponentiel, FAILED après 10 tentatives |
| R5 | Fuite de données dans le payload | Élevé | INV-01 strict, review systématique de chaque event builder |
| R6 | Ordering des événements non garanti | Faible | Timestamp dans le payload, event_id pour idempotence côté client |
| R7 | Replay abusif (amplification) | Moyen | Rate limit sur l'API replay, journalisation de chaque replay |
| R8 | Race condition journal/émission (INV-06) | Élevé | Pattern Outbox : écriture DB transactionnelle puis polling worker |
10. Dépendances
| Story/Module | Nature de la dépendance |
| PD-21 (BullMQ) | Infrastructure de jobs asynchrones — worker webhook dédié |
| PD-3 (Redis) | Backend de file BullMQ + rate limiting |
| PD-15 (Users/RLS) | Schéma organisations + RLS pour isolation multi-tenant |
| PD-17 (Audit log) | Journal append-only — le webhook ne s'émet qu'APRÈS journalisation probatoire |
| PD-19 (CORS/Headers) | Security headers pour l'API REST de gestion des webhooks |
| PD-172 (Rate limiting) | Mécanisme de rate limiting Redis à réutiliser pour le 100 evt/min |
| PD-28 (Session/JWT) | Extraction du binding organisationnel depuis le JWT |
Ce document constitue l'entrée pour l'étape 1 (spécification technique).