Channel Webhook¶
L'adapter bidirectionnel channel/webhook reçoit un JSON HTTP générique, vérifie
son authenticité sur les octets bruts, le transforme en
ChannelIncomingMessage, puis passe par le ChannelDispatcher. Il n'impose ni
fournisseur, ni SDK, ni logique métier.
Intention¶
Utiliser cet adapter pour raccorder un outil interne, une automatisation ou un prototype qui sait émettre un webhook HTTP. Préférer un adapter fournisseur dédié lorsqu'un protocole impose ses propres challenges, événements ou règles d'accusé de réception.
Le webhook transporte un message. Il ne devient pas pour autant un historique de chat : consulter la distinction Channel n'est pas Chat.
Position Hexagonale¶
| Text Only | |
|---|---|
FastAPI reste dans l'adapter. Le handler applicatif reçoit uniquement les
modèles communs channel, et domain/ ne dépend ni de FastAPI ni de httpx.
Quickstart¶
Installer L'adapter¶
| Bash | |
|---|---|
La commande :
- crée
config/adapters/bidirectional/webhook.yaml; - ajoute l'extra
channelà la dépendance Arclith du projet ; - crée dans
config/secrets.yamlle mappingadapters.channel.webhook.secret: ARCLITH_WEBHOOK_SECRET; - ne place aucune valeur de secret dans un fichier versionné.
Le YAML généré utilise le mode synchrone :
Pour un endpoint exposé, fournir un secret aléatoire d'au moins 32 octets via
le resolver secrets. secret: null désactive volontairement HMAC et convient
uniquement à un test local ou à une autre frontière déjà authentifiée.
Brancher Le Router¶
Ce POC utilise les implémentations mémoire afin de rester exécutable. En production, remplacer le resolver et surtout le store d'événements par des adapters partagés entre replicas.
Dans une application Arclith, utiliser normalement les settings déjà chargés :
| Python | |
|---|---|
Envoyer Un Événement Signé¶
La signature canonique est :
| Text Only | |
|---|---|
Le corps signé doit être exactement celui envoyé, octet pour octet. Cet exemple évite de mettre le secret dans la ligne de commande :
Une réponse synchrone réussie contient les messages provider-neutral à rendre au client :
Formation¶
Le quickstart Channel explique d'abord les quatre ports avec le fake mémoire. Cette page ajoute ensuite le geste HTTP, HMAC et les contraintes de production.
Payload Entrant¶
Le JSON est strict : tout champ inconnu est rejeté.
| Champ | Requis | Description |
|---|---|---|
sender_id |
oui | identifiant externe, résolu ensuite par ChannelIdentityResolver |
conversation_id |
oui | conversation externe stable |
text |
conditionnel | texte non vide ; requis si aucune pièce jointe |
sender_display_name |
non | libellé externe non fiable |
external_tenant_id |
non | tenant affirmé par l'émetteur, à mapper explicitement |
external_workspace_id |
non | workspace affirmé par l'émetteur |
thread_id |
non | thread externe, sans persistance implicite |
attachments |
non | métadonnées ChannelAttachment, jamais des octets embarqués |
metadata |
non | JSON fini ; seules les clés de metadata_allowlist sont transmises |
L'identifiant d'événement n'est pas accepté dans le JSON. Il vient uniquement
du header idempotency_header, afin de conserver une source stable et
configurée côté serveur.
Modes De Réponse¶
| Mode | Résultat handler valide | HTTP | Livraison |
|---|---|---|---|
sync |
completed |
200 |
les réponses sont retournées dans le corps HTTP |
accepted |
accepted |
202 |
le handler confirme avoir persisté ou publié durablement la suite |
callback |
completed |
200 |
les réponses sont POSTées immédiatement vers l'URL serveur |
callback |
accepted |
202 |
le handler a pris le travail durablement et livrera plus tard via WebhookCallbackSender |
Un résultat accepted ne déclenche aucune tâche FastAPI locale. Le handler doit
avoir obtenu une garantie durable avant de le retourner. Une combinaison
incompatible, par exemple accepted en mode sync, retourne
500 response_mode_error.
Pour configurer un callback depuis la CLI :
| Bash | |
|---|---|
Arclith.channel_sender("webhook") construit un WebhookCallbackSender
uniquement lorsque ce mode est complètement configuré. Le même sender peut être
injecté dans un worker durable. Sans client injecté, sa construction vérifie
immédiatement la présence de httpx : une installation sans extra channel
échoue donc au montage du router ou du sender, avant la première requête.
Idempotence Et Échecs Sortants¶
Le store réserve atomiquement ("webhook", event_id) avant la résolution
d'identité et le handler. Un doublon retourne 200 avec status: duplicate
sans réexécuter l'application.
Une erreur de résolution ou de handler libère le claim. Après la réussite du handler, le claim est conservé même si le callback sortant échoue : rejouer le handler pourrait dupliquer ses effets métier. Un callback critique doit donc être écrit dans un outbox ou une file durable par le handler, puis livré et retenté indépendamment. Un retry identique de l'émetteur ne rejouera pas le callback perdu.
Le fake MemoryChannel fournit un claim atomique dans un seul processus, mais
perd son état au redémarrage et ne coordonne pas plusieurs replicas.
Statuts HTTP¶
Le router déclare explicitement ses réponses dans OpenAPI :
| HTTP | Code JSON | Cause |
|---|---|---|
200 |
— | traitement terminé ou doublon |
202 |
— | prise en charge durable confirmée par le handler |
400 |
missing_event_id |
header d'événement absent ou vide |
401 |
invalid_signature |
signature absente, invalide ou périmée |
403 |
identity_not_resolved, channel_unauthorized |
identité non mappée ou refusée |
413 |
payload_too_large |
corps au-delà de max_payload_bytes |
415 |
unsupported_media_type |
media type non JSON |
422 |
invalid_payload, unsupported_event |
schéma ou événement non supporté |
429 |
callback_rate_limited |
callback limité ; Retry-After numérique est propagé |
500 |
response_mode_error |
contrat handler/mode incohérent |
502 |
callback_rejected |
callback HTTP non-2xx et non-429/5xx |
503 |
callback_unavailable |
timeout, transport ou erreur 5xx du callback |
Les réponses d'erreur utilisent des détails stables. Elles ne reprennent ni corps fournisseur, ni URL, ni signature, ni secret.
Sécurité¶
- La taille est bornée pendant la lecture du stream, puis contrôlée à nouveau avant parsing.
- Si HMAC est activé, le corps brut est authentifié avant le parsing JSON.
- Le timestamp signé doit rester dans
signature_tolerance_secondset la comparaison utilisehmac.compare_digest. - L'URL de callback ne vient jamais du payload. Elle doit être HTTPS, sans
credentials, query ni fragment, et son hostname doit correspondre exactement
à
callback_allowed_host. - Les IP littérales privées, loopback ou non globales et
localhostsont refusées. Les redirects HTTP ne sont jamais suivies. - La validation applicative ne remplace pas une politique egress : en production, contrôler également DNS, réseau sortant et résolution privée pour réduire les risques de rebinding ou de compromission de configuration.
- Les pièces jointes ne sont pas téléchargées par l'adapter.
sender_id, tenant et workspace restent des affirmations externes jusqu'au mapping explicite du resolver.
Production¶
Avant exposition publique :
- activer HMAC avec un secret distinct par intégration et le faire tourner via
la capability
secrets; - utiliser un
ChannelEventStoreatomique partagé et dimensionner son TTL ; - définir une
metadata_allowlistminimale ; - injecter un resolver d'identité tenant-aware ;
- préférer
acceptedavec file/outbox durable pour les traitements longs ; - borner timeout, retries et egress du callback ;
- journaliser uniquement event ID, statut, latence et corrélation technique.
Validation¶
| Bash | |
|---|---|
Vérifier ensuite make quality, make precommit, les deux lockfiles et
make docs.
Troubleshooting¶
| Symptôme | Diagnostic | Correction |
|---|---|---|
401 invalid_signature |
timestamp hors fenêtre ou corps modifié après signature | signer les octets transmis avec le même timestamp |
400 missing_event_id |
header configuré absent | envoyer un ID stable, identique sur les retries |
403 identity_not_resolved |
identité externe inconnue | créer le mapping sans utiliser directement sender_id comme user métier |
duplicate |
le couple provider/event ID est déjà claim | ne pas générer un nouvel ID pour le même événement ; inspecter le store |
500 response_mode_error |
le handler n'honore pas le mode | aligner completed/accepted et la garantie durable |
502 ou 503 callback |
endpoint rejeté, indisponible ou expiré | consulter la télémétrie technique et utiliser une reprise outbound durable |
Projet¶
Dans un service consommateur, garder le handler dans application/, le
resolver et le store dans les ports/adapters appropriés, et inclure le router
depuis le bootstrap FastAPI. Ne placer aucune logique métier, URL de callback
issue de l'utilisateur ou appel HTTP dans domain/.