Aller au contenu
getnextpdf.com

Enterprise édition

Webhook — Référence approfondie

L’espace de noms NextPDF\Enterprise\Webhook fournit une livraison de webhooks à portée de locataire pour les événements de tâche. La surface publique compte six symboles : WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy et DeadLetterEntry. Le gestionnaire enregistre des points de terminaison par locataire et distribue les événements de tâche aux enregistrements abonnés. Le moteur de livraison émet en POST un contenu JSON signé en HMAC-SHA256, valide chaque destination au regard de la barrière de sortie SSRF de Core, effectue de nouvelles tentatives avec un backoff exponentiel, et enregistre les échecs permanents dans une file d’attente de lettres mortes en mémoire. Depuis la version 3.1.0, la signature lie l’en-tête X-NextPDF-Timestamp dans la chaîne de base du MAC, de sorte que les récepteurs vérifient conjointement la fraîcheur et l’intégrité. Pour le guide au niveau du workflow, consulte Webhook.

Cette capacité est fournie dans NextPDF Enterprise (nextpdf/enterprise) et s’active avec une enveloppe de licence de niveau Enterprise. Un déploiement dépourvu de ce droit ne charge pas les classes de la capacité. Compare les éditions et obtiens une licence.

La surface webhook est une capacité Enterprise de base, disponible dès que le paquet Enterprise est installé ; il n’existe pas de drapeau distinct par fonctionnalité. NextPDF Core (Apache-2.0) et NextPDF Pro n’ont aucune surface d’enregistrement ni de livraison de webhooks ; le gestionnaire, l’enregistrement, le contenu, le moteur de livraison, la politique de nouvelles tentatives et l’entrée de lettre morte sont fournis uniquement dans nextpdf/enterprise.

SymboleParamètresComportement par défautRetourneLève ou échoue avecNotes
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullCrée un gestionnaire avec un index d’enregistrements en mémoire videNouveau WebhookManagerNe lève pasLes enregistrements sont indexés par locataire
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationAjoute l’enregistrement à l’index du locataire appelantvoidInvalidArgumentException lorsque le locataire de l’enregistrement ne correspond pas au locataire du contexteL’enregistrement inter-locataires est rejeté avant stockage
WebhookManager::unregisterTenantContext $tenant, string $registrationIdRemplace l’enregistrement correspondant par une copie désactivéeboolNe lève pas ; retourne false lorsque l’id est introuvableDésactivation en douceur ; l’historique est préservé
WebhookManager::activeRegistrationsTenantContext $tenantFiltre les enregistrements du locataire pour ne garder que les actifslist<WebhookRegistration>Ne lève pasSeuls les enregistrements du locataire appelant sont visibles
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventLivre l’événement à chaque enregistrement actif abonné au type d’événementint (livraisons réussies)Propage JsonException lorsque les données de l’événement ne sont pas encodables en JSON ; les échecs de livraison ne lèvent pasUn id de livraison de 32 caractères hexadécimaux est généré à chaque livraison d’enregistrement
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullStocke les valeurs fournies telles quellesNouveau WebhookRegistrationAucun @throws déclaré ; PHP lève TypeError en cas de types d’arguments incompatibles sous strict_typesfinal readonly ; un $events vide signifie l’abonnement à tout
WebhookRegistration::subscribesToJobEventType $eventTypetrue lorsque $events est vide ou contient le typeboolNe lève pasComparaison d’identité stricte
WebhookRegistration::deactivateRetourne une copie inactiveselfNe lève pasL’instance d’origine reste inchangée
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdCopie l’id de tâche, le type d’événement, les données et l’horodatage depuis l’événementselfNe lève pasFabrique statique utilisée par dispatch
WebhookPayload::toJsonSérialise le contenu à six champs avec des barres obliques non échappéesnon-empty-stringJsonException lorsque les données de l’événement ne sont pas encodables en JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayRetourne le contenu sous forme de tableau associatifarray<string, mixed>Ne lève pasHorodatage formaté en RFC 3339 étendu
WebhookPayload::signedTimestampHeure de l’événement en secondes Unix, bornée à zéro ou plusint<0, max>Ne lève pasÉmis sous forme d’en-tête X-NextPDF-Timestamp et lié au MAC
WebhookPayload::signstring $secretHMAC-SHA256 sur la chaîne de base {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException via toJson() lorsque le contenu n’est pas encodableLie cryptographiquement l’en-tête d’horodatage au contenu
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullMoteur de livraison PSR-18/PSR-17 avec une file d’attente de lettres mortes videNouveau WebhookDeliveryNe lève pasPolitique par défaut : 5 tentatives, base de 1 s, plafond de 300 s
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadÉmet en POST le contenu signé avec validation de la sortie SSRF à chaque tentative et backoff exponentielboolJsonException avant la première tentative lorsque le contenu n’est pas encodable ; sinon ne lève pas — false signifie que le contenu a été routé vers la file d’attente de lettres mortestrue uniquement en cas de réponse 2xx
WebhookDelivery::deadLettersRetourne toutes les entrées enregistréeslist<DeadLetterEntry>Ne lève pasEn mémoire, à portée du processus
WebhookDelivery::clearDeadLettersVide la file d’attente de lettres mortesvoidNe lève pasIrréversible ; exporte d’abord les entrées si un rejeu est requis
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Stocke les valeurs de la politiqueNouveau WebhookRetryPolicyAucun @throws déclaré ; les paramètres sont documentés positive-int$maxRetries compte le nombre total de tentatives
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), plafonné à maxDelaySecondspositive-intNe lève pasLes numéros de tentative commencent à 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue tant que la tentative en cours est inférieure au maximumboolNe lève pasL’attente est ignorée après la dernière tentative
WebhookRetryPolicy::default5 tentatives, base de 1 s, plafond de 300 sselfNe lève pasFabrique statique ; valeur par défaut en production
WebhookRetryPolicy::aggressive10 tentatives, base de 2 s, plafond de 600 sselfNe lève pasFabrique statique pour les points de terminaison critiques
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseStocke l’enregistrement d’échec tel quelNouveau DeadLetterEntryAucun @throws déclaré ; TypeError sous strict_typesfinal readonly ; un $lastHttpStatus null signifie un échec de transport
DeadLetterEntry::markReplayedRetourne une copie avec replayed = trueselfNe lève pasMême id ; l’entrée d’origine reste inchangée
public function __construct(
private readonly WebhookDelivery $delivery,
private readonly ?LoggerInterface $logger = null,
) {}
public function register(TenantContext $tenant, WebhookRegistration $registration): void
public function unregister(TenantContext $tenant, string $registrationId): bool
public function activeRegistrations(TenantContext $tenant): array
public function dispatch(TenantContext $tenant, JobEvent $event): int
public function __construct(
public string $id,
public string $tenantId,
public string $url,
public array $events,
public string $secret,
public bool $active = true,
public ?string $description = null,
) {}
public function subscribesTo(JobEventType $eventType): bool
public function deactivate(): self
public static function fromJobEvent(
JobEvent $event,
string $tenantId,
string $deliveryId,
): self
public function toJson(): string
public function toArray(): array
public function signedTimestamp(): int
public function sign(string $secret): string
public function __construct(
private readonly ClientInterface $httpClient,
private readonly RequestFactoryInterface $requestFactory,
private readonly StreamFactoryInterface $streamFactory,
private readonly WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(),
private readonly ?LoggerInterface $logger = null,
) {}
public function deliver(WebhookRegistration $registration, WebhookPayload $payload): bool
public function deadLetters(): array
public function clearDeadLetters(): void
public function __construct(
public int $maxRetries = 5,
public int $baseDelaySeconds = 1,
public int $maxDelaySeconds = 300,
) {}
public function delayForAttempt(int $attempt): int
public function shouldRetry(int $currentAttempt): bool
public static function default(): self
public static function aggressive(): self
public function __construct(
public string $id,
public string $registrationId,
public WebhookPayload $payload,
public int $attempts,
public string $lastError,
public ?int $lastHttpStatus,
public DateTimeImmutable $failedAt,
public bool $replayed = false,
) {}
public function markReplayed(): self
  • Les enregistrements sont indexés par locataire. register() rejette un enregistrement dont l’identifiant de locataire ne correspond pas au contexte appelant. unregister() est une désactivation en douceur : l’enregistrement est remplacé par une copie inactive, ce qui préserve l’historique tout en l’excluant des distributions futures.
  • dispatch() ne parcourt que les enregistrements actifs du locataire appelant qui sont abonnés au type d’événement distribué. Une liste d’événements abonnés vide signifie l’abonnement à tout. La valeur de retour compte les livraisons réussies.
  • Chaque livraison est un POST HTTP avec un contenu JSON et cinq en-têtes : Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (secondes unix), X-NextPDF-Delivery-Id et X-NextPDF-Event.
  • Les champs du contenu JSON sont delivery_id, job_id, event_type, data, timestamp (RFC 3339 étendu) et tenant_id, sérialisés avec des barres obliques non échappées. Les valeurs de type d’événement proviennent de JobEventType dans nextpdf/core : progress, completed, failed, cancelled.
  • Schéma de signature (modifié en 3.1.0, rupture). La chaîne de base HMAC-SHA256 est {signedTimestamp}.{jsonBody}, avec le secret d’enregistrement comme clé — et non le contenu seul. La valeur X-NextPDF-Timestamp est la composante d’horodatage du MAC, de sorte qu’un en-tête d’horodatage falsifié ou rejoué invalide la signature.
  • Vérification côté récepteur : lis l’en-tête X-NextPDF-Timestamp T ; rejette lorsque T est en dehors d’une fenêtre de fraîcheur acceptable (par exemple 300 s) ; recalcule hash_hmac('sha256', T . '.' . rawBody, secret) sur les octets bruts reçus ; compare en temps constant à la valeur de l’en-tête après avoir retiré le préfixe sha256=.
  • Le contenu, la signature et l’id de livraison sont calculés une seule fois par livraison et restent constants d’une tentative à l’autre.
  • Barrière de sortie SSRF. Avant chaque tentative, l’URL de destination franchit la barrière UrlValidator::validateExternalUrl() de Core : schéma HTTPS uniquement ; les plages loopback, privées, réservées, carrier-grade-NAT, de métadonnées cloud et de transition IPv6 avec IPv4 embarquée sont bloquées ; les noms d’hôte sont résolus via DNS (A et AAAA) et les hôtes non résolus sont rejetés en échec fermé. Une URL bloquée n’est jamais envoyée : la boucle de tentatives s’interrompt et le contenu est routé directement vers la file d’attente de lettres mortes avec une dernière erreur Blocked SSRF destination: et un statut HTTP null.
  • Classification du résultat par tentative : un 2xx est un succès et retourne immédiatement ; un 4xx autre que 429 est terminal et part directement en lettre morte ; tout autre résultat — 3xx, 429, 5xx ou une exception de transport — peut faire l’objet d’une nouvelle tentative jusqu’au nombre total de tentatives de la politique.
  • Le backoff est exponentiel : l’attente avant la tentative suivante est baseDelaySeconds × 2^(attempt − 1), plafonnée à maxDelaySeconds. L’attente est ignorée après la dernière tentative.
  • Lorsqu’aucune tentative n’aboutit, un DeadLetterEntry enregistre un id unique, l’id d’enregistrement, le contenu original, le nombre de tentatives (borné au maximum de la politique), le dernier message d’erreur, le dernier statut HTTP (null en cas d’échec de transport ou de blocage SSRF) et l’horodatage de l’échec.
  • La file d’attente de lettres mortes est en mémoire et limitée à la durée de vie du processus. markReplayed() produit une copie marquée ; elle ne renvoie rien, et la file conserve l’entrée d’origine.
  • Liste d’événements vide. L’enregistrement reçoit tous les types d’événement. Restreins explicitement la liste lorsque le récepteur ne doit pas voir tous les événements.
  • 4xx terminal versus échec de transport. Un rejet 4xx enregistre un lastHttpStatus renseigné ; un échec de connexion enregistre null. Utilise le null pour distinguer un rejet du récepteur d’un échec de transport.
  • Destination bloquée par SSRF. Un enregistrement pointant vers une adresse HTTP, privée, loopback ou de métadonnées part en lettre morte dès la première tentative avec une erreur Blocked SSRF destination: et un statut null. Aucune requête sortante n’est émise. Corrige l’URL et enregistre de nouveau.
  • Récepteurs hérités après mise à niveau. Un récepteur qui vérifie encore le HMAC portant uniquement sur le contenu, antérieur à la 3.1.0, échoue en mode fermé face aux livraisons 3.1.0. Fais migrer le récepteur vers la chaîne de base {timestamp}.{body} et consomme X-NextPDF-Timestamp.
  • Données d’événement non encodables. toJson() et sign() lèvent JsonException, qui se propage hors de deliver() et dispatch() avant toute tentative.
  • Blocage synchrone. deliver() s’endort en ligne entre les tentatives. Le backoff cumulé atteint 15 s sous la politique par défaut et environ 17 minutes sous la politique agressive. Distribue depuis un worker de file d’attente lorsque la latence du récepteur n’est pas de confiance.
  • Bornage du nombre de tentatives. Le nombre de tentatives enregistré ne dépasse jamais le maximum de la politique, même si le compteur de boucle interne le dépasse à l’épuisement.
  • Croissance et durabilité de la file. La file d’attente de lettres mortes croît sans limite au sein du processus et disparaît au redémarrage. Exporte les entrées via deadLetters() et conserve-les en externe avant d’appeler clearDeadLetters() lorsqu’un rejeu durable est requis.
  • Le rejeu est piloté par l’opérateur. La re-livraison consiste à rappeler deliver() avec le contenu de l’entrée ; markReplayed() ne fait qu’enregistrer le fait sur une copie.
  • Résidu de re-liaison DNS. L’URL est revalidée à chaque tentative, ce qui réduit sans la fermer la fenêtre de re-liaison : l’abstraction PSR-18 ne peut pas épingler la connexion à l’IP validée. Ajoute des contrôles de sortie au niveau réseau là où ce résidu importe.
  • Gestion du secret. Le secret d’enregistrement est un identifiant de connexion. Le HMAC authentifie uniquement l’intégrité et l’origine — il n’assure pas la confidentialité. Ne place pas dans le contenu de l’événement des données que le récepteur ne doit pas voir.

La signature du contenu est un HMAC-SHA256 via la fonction hash_hmac() de PHP, elle repose donc sur le fournisseur cryptographique de l’hôte. Dans une build sous contrainte FIPS, une primitive non approuvée échoue à la frontière cryptographique plutôt que de se rétrograder. La couche webhook n’ajoute aucune politique cryptographique propre.

  • L’authentification du contenu implémente HMAC, le code d’authentification de message à hachage à clé de FIPS PUB 198-1 §1, instancié avec SHA-256.
  • La protection contre le rejeu suit les recommandations de sécurité des webhooks de l’OWASP Cheat Sheet Series : l’horodatage de l’événement voyage dans un en-tête dédié et est intégré au calcul de la signature, de sorte qu’un horodatage falsifié échoue à la vérification.
  • Les horodatages du contenu utilisent le format date-heure étendu RFC 3339. Déclaré par le code : RFC 3339 n’a pas été récupéré depuis le corpus RAG pour cette page.
  • Il s’agit d’énoncés de capacité fondés sur le code source du produit et les clauses citées. NextPDF ne formule aucune revendication de conformité ou de certification pour cette surface.
  • Toutes les classes déclarent strict_types=1 et sont final ; WebhookRegistration, WebhookPayload, WebhookRetryPolicy et DeadLetterEntry sont final readonly avec des propriétés publiques promues.
  • Le module porte une annotation @since de 2.2.0 ; le schéma de signature lié à l’horodatage est une rupture documentée en 3.1.0.
  • Le moteur de livraison prend des abstractions PSR-18/PSR-17, de sorte qu’un client HTTP simulé exerce hors ligne l’intégralité du chemin d’envoi, de nouvelle tentative et de lettre morte. Le logger vaut null par défaut ; injecte un logger PSR-3 en production, sinon les échecs ne se manifestent qu’à travers les valeurs de retour.
  • Les implémentations côté récepteur devraient utiliser hash_equals() pour la comparaison de signature et imposer une fenêtre de fraîcheur sur X-NextPDF-Timestamp.
  • Tests aux limites recommandés : enregistrement avec locataire non concordant, diffusion sur liste d’événements vide, 4xx terminal, épuisement des tentatives, URL bloquée par SSRF, rejet de signature à horodatage falsifié face à un vecteur fixe, et bornage du nombre de tentatives des lettres mortes.

Cette page documente uniquement le comportement observable de l’extérieur et la surface d’API publique prise en charge. Les chemins d’espaces de noms internes, les classes utilitaires, les tables de mécanismes, les noms de fichiers de runbook et les préfixes de tickets sont hors périmètre.