Enterprise editie
Webhook — Diepe referentie
In het kort
Sectie met titel “In het kort”De namespace NextPDF\Enterprise\Webhook levert tenant-gescopete webhook-aflevering voor taakgebeurtenissen. Het publieke oppervlak bestaat uit zes symbolen: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy en DeadLetterEntry. De manager registreert endpoints per tenant en verzendt taakgebeurtenissen naar de abonnerende registraties. De delivery engine POST een met HMAC-SHA256 ondertekende JSON-payload, valideert elke bestemming tegen de Core SSRF-egress-gate, probeert opnieuw met exponentiële backoff en registreert permanente fouten in een in-memory dead-letter-wachtrij. Vanaf 3.1.0 bindt de handtekening de header X-NextPDF-Timestamp in de MAC-basisstring, zodat ontvangers versheid en integriteit samen verifiëren. Voor de gids op workflowniveau, zie Webhook.
Beschikbaarheid en licentiëring
Sectie met titel “Beschikbaarheid en licentiëring”Deze mogelijkheid wordt geleverd in NextPDF Enterprise (nextpdf/enterprise) en activeert met een licentie-envelop op Enterprise-niveau. Een deployment zonder dat recht laadt de klassen van de mogelijkheid niet. Vergelijk edities en vraag een licentie aan.
Het webhook-oppervlak is een basismogelijkheid van Enterprise, beschikbaar zodra het Enterprise-pakket is geïnstalleerd; er is geen aparte vlag per functie. NextPDF Core (Apache-2.0) en NextPDF Pro hebben geen webhook-registratie- of afleveringsoppervlak; de manager, registratie, payload, delivery engine, retry policy en dead-letter-vermelding worden alleen geleverd in nextpdf/enterprise.
Publiek API-oppervlak
Sectie met titel “Publiek API-oppervlak”| Symbool | Parameters | Standaardgedrag | Retourneert | Gooit of faalt met | Opmerkingen |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Maakt een manager met een lege in-memory registratie-index | Nieuwe WebhookManager | Gooit niet | Registraties worden per tenant geïndexeerd |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Voegt de registratie toe aan de index van de aanroepende tenant | void | InvalidArgumentException wanneer de tenant van de registratie niet overeenkomt met de tenant van de context | Cross-tenant-registratie wordt afgewezen vóór opslag |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Vervangt de overeenkomende registratie door een gedeactiveerde kopie | bool | Gooit niet; retourneert false wanneer de id niet wordt gevonden | Soft deactivate; historie blijft behouden |
WebhookManager::activeRegistrations | TenantContext $tenant | Filtert de registraties van de tenant op de actieve | list<WebhookRegistration> | Gooit niet | Alleen de registraties van de aanroepende tenant zijn zichtbaar |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Levert de gebeurtenis af aan elke actieve registratie die zich op het gebeurtenistype abonneert | int (geslaagde afleveringen) | Propageert JsonException wanneer gebeurtenisgegevens niet JSON-encodeerbaar zijn; afleveringsfouten gooien niet | Er wordt een verse 32-hex afleverings-id gegenereerd per registratieaflevering |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Slaat de aangeleverde waarden letterlijk op | Nieuwe WebhookRegistration | Geen gedeclareerde @throws; PHP werpt TypeError bij niet-overeenkomende argumenttypes onder strict_types | final readonly; lege $events betekent abonneer-op-alles |
WebhookRegistration::subscribesTo | JobEventType $eventType | true wanneer $events leeg is of het type bevat | bool | Gooit niet | Strikte identiteitsvergelijking |
WebhookRegistration::deactivate | — | Retourneert een inactieve kopie | self | Gooit niet | De originele instantie blijft ongewijzigd |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Kopieert taak-id, gebeurtenistype, gegevens en tijdstempel uit de gebeurtenis | self | Gooit niet | Statische factory gebruikt door dispatch |
WebhookPayload::toJson | — | Serialiseert de body met zes velden met niet-geëscapete slashes | non-empty-string | JsonException wanneer gebeurtenisgegevens niet JSON-encodeerbaar zijn | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Retourneert de body als een associatieve array | array<string, mixed> | Gooit niet | Tijdstempel geformatteerd als RFC 3339 extended |
WebhookPayload::signedTimestamp | — | Gebeurtenistijd in Unix-seconden, begrensd op nul of hoger | int<0, max> | Gooit niet | Uitgezonden als X-NextPDF-Timestamp en gebonden in de MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 over de basisstring {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException via toJson() wanneer de body niet encodeerbaar is | Bindt de tijdstempel-header cryptografisch aan de body |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | PSR-18/PSR-17 delivery engine met een lege dead-letter-wachtrij | Nieuwe WebhookDelivery | Gooit niet | Standaardbeleid: 5 pogingen, 1 s basis, 300 s cap |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | POST de ondertekende payload met SSRF-egress-validatie per poging en exponentiële backoff | bool | JsonException vóór de eerste poging wanneer de body niet encodeerbaar is; anders gooit het niet — false betekent dat de payload naar de dead-letter-wachtrij is geleid | true alleen bij een 2xx-respons |
WebhookDelivery::deadLetters | — | Retourneert alle geregistreerde vermeldingen | list<DeadLetterEntry> | Gooit niet | In-memory, proces-gescoped |
WebhookDelivery::clearDeadLetters | — | Leegt de dead-letter-wachtrij | void | Gooit niet | Onomkeerbaar; exporteer de vermeldingen eerst als replay vereist is |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Slaat de beleidswaarden op | Nieuwe WebhookRetryPolicy | Geen gedeclareerde @throws; parameters zijn gedocumenteerd als positive-int | $maxRetries telt het totale aantal pogingen |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), begrensd op maxDelaySeconds | positive-int | Gooit niet | Pogingnummers zijn 1-gebaseerd |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true zolang de huidige poging onder het maximum ligt | bool | Gooit niet | De wachttijd wordt na de laatste poging overgeslagen |
WebhookRetryPolicy::default | — | 5 pogingen, 1 s basis, 300 s cap | self | Gooit niet | Statische factory; productiestandaard |
WebhookRetryPolicy::aggressive | — | 10 pogingen, 2 s basis, 600 s cap | self | Gooit niet | Statische factory voor kritieke endpoints |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Slaat het faalrecord letterlijk op | Nieuwe DeadLetterEntry | Geen gedeclareerde @throws; TypeError onder strict_types | final readonly; null $lastHttpStatus betekent transportfout |
DeadLetterEntry::markReplayed | — | Retourneert een kopie met replayed = true | self | Gooit niet | Zelfde id; de originele vermelding blijft ongewijzigd |
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): intpublic 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(): selfpublic 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): stringpublic 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(): voidpublic 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(): selfpublic 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(): selfGedragscontract
Sectie met titel “Gedragscontract”- Registraties worden per tenant geïndexeerd.
register()wijst een registratie af waarvan de tenant-identificator niet overeenkomt met de aanroepende context.unregister()is een soft deactivate: de registratie wordt vervangen door een inactieve kopie, die de historie behoudt en haar uitsluit van toekomstige dispatch. dispatch()itereert alleen over de actieve registraties van de aanroepende tenant die zich op het verzonden gebeurtenistype abonneren. Een lege geabonneerde-gebeurtenissenlijst betekent abonneer-op-alles. De retourwaarde telt de geslaagde afleveringen.- Elke aflevering is een HTTP POST met een JSON-body en vijf headers:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(unix-seconden),X-NextPDF-Delivery-IdenX-NextPDF-Event. - De velden van de JSON-body zijn
delivery_id,job_id,event_type,data,timestamp(RFC 3339 extended) entenant_id, geserialiseerd met niet-geëscapete slashes. Gebeurtenistype-waarden komen uitJobEventTypeinnextpdf/core:progress,completed,failed,cancelled. - Handtekeningschema (gewijzigd in 3.1.0, breaking). De HMAC-SHA256-basisstring is
{signedTimestamp}.{jsonBody}, gekeyd met het registratiegeheim — niet de body alleen. De waardeX-NextPDF-Timestampis de tijdstempelcomponent van de MAC, dus een gemanipuleerde of hergebruikte tijdstempel-header maakt de handtekening ongeldig. - Verificatie door de ontvanger: lees de header
X-NextPDF-TimestampT; wijs af wanneerTbuiten een aanvaardbaar versheidsvenster valt (bijvoorbeeld 300 s); herberekenhash_hmac('sha256', T . '.' . rawBody, secret)over de ruwe ontvangen bytes; vergelijk in constante tijd met de header-waarde na het verwijderen van de prefixsha256=. - De body, handtekening en afleverings-id worden eenmaal per aflevering berekend en blijven constant over de retry-pogingen heen.
- SSRF-egress-gate. Vóór elke poging passeert de bestemmings-URL de Core
UrlValidator::validateExternalUrl()-gate: alleen HTTPS-schema; loopback-, private, gereserveerde, carrier-grade-NAT-, cloud-metadata- en in IPv4 ingebedde IPv6-transitiebereiken worden geblokkeerd; hostnamen worden via DNS geresolveerd (A en AAAA) en niet-resolveerbare hosts worden fail-closed afgewezen. Een geblokkeerde URL wordt nooit verzonden: de pogingslus breekt af en de payload gaat rechtstreeks naar de dead-letter-wachtrij met eenBlocked SSRF destination:als laatste fout en een null HTTP-status. - Uitkomstclassificatie per poging: 2xx is succes en retourneert onmiddellijk; een 4xx anders dan 429 is terminaal en gaat rechtstreeks naar dead-letter; elke andere uitkomst — 3xx, 429, 5xx of een transportuitzondering — is retryable tot het totale aantal pogingen van het beleid.
- Backoff is exponentieel: de wachttijd vóór de volgende poging is
baseDelaySeconds × 2^(attempt − 1), begrensd opmaxDelaySeconds. De wachttijd wordt na de laatste poging overgeslagen. - Wanneer geen enkele poging slaagt, registreert een
DeadLetterEntryeen unieke id, de registratie-id, de originele payload, het aantal pogingen (begrensd op het beleidsmaximum), het laatste foutbericht, de laatste HTTP-status (null bij transportfout of SSRF-blokkade) en het faaltijdstempel. - De dead-letter-wachtrij is in-memory en gescoped op de levensduur van het proces.
markReplayed()produceert een gemarkeerde kopie; het verstuurt niet opnieuw, en de wachtrij behoudt de originele vermelding.
Randgevallen en faalmodi
Sectie met titel “Randgevallen en faalmodi”- Lege gebeurtenissenlijst. De registratie ontvangt elk gebeurtenistype. Scope de lijst expliciet wanneer de ontvanger niet alle gebeurtenissen mag zien.
- Terminale 4xx versus transportfout. Een 4xx-afwijzing registreert een gevulde
lastHttpStatus; een verbindingsfout registreert null. Gebruik de null om ontvangerafwijzing van transportfout te onderscheiden. - SSRF-geblokkeerde bestemming. Een registratie die wijst naar een HTTP-, private, loopback- of metadata-adres komt bij de eerste poging in dead-letter terecht met een
Blocked SSRF destination:-fout en null-status. Er wordt geen uitgaand verzoek gedaan. Corrigeer de URL en registreer opnieuw. - Legacy-ontvangers na upgrade. Een ontvanger die nog de body-only HMAC van vóór 3.1.0 verifieert, faalt closed tegen 3.1.0-afleveringen. Migreer de ontvanger naar de basisstring
{timestamp}.{body}en consumeerX-NextPDF-Timestamp. - Niet-encodeerbare gebeurtenisgegevens.
toJson()ensign()gooienJsonException, die uitdeliver()endispatch()propageert voordat er een poging wordt gedaan. - Synchroon blokkeren.
deliver()slaapt inline tussen pogingen. Cumulatieve backoff bereikt 15 s onder het standaardbeleid en ongeveer 17 minuten onder het aggressive-beleid. Verzend vanuit een queue-worker wanneer de latentie van de ontvanger niet vertrouwd is. - Begrenzing aantal pogingen. Het geregistreerde aantal pogingen overschrijdt nooit het beleidsmaximum, ook al gaat de interne lusteller daar bij uitputting voorbij.
- Wachtrijgroei en duurzaamheid. De dead-letter-wachtrij groeit onbegrensd binnen het proces en verdwijnt bij een herstart. Exporteer de vermeldingen via
deadLetters()en persisteer ze extern voordat jeclearDeadLetters()aanroept wanneer duurzame replay vereist is. - Replay is operatorgedreven. Heraflevering betekent
deliver()opnieuw aanroepen met de payload van de vermelding;markReplayed()legt het feit alleen vast op een kopie. - DNS-rebinding-restrisico. De URL wordt bij elke poging opnieuw gevalideerd, wat het rebinding-venster verkleint maar niet sluit: de PSR-18-abstractie kan de verbinding niet vastpinnen op het gevalideerde IP. Voeg egress-controles op netwerkniveau toe waar dit restrisico van belang is.
- Omgaan met het geheim. Het registratiegeheim is een referentie. De HMAC authenticeert alleen integriteit en herkomst — het is geen vertrouwelijkheid. Plaats geen gegevens in de gebeurtenis-payload die de ontvanger niet mag zien.
FIPS-modusgedrag
Sectie met titel “FIPS-modusgedrag”Payload-ondertekening is HMAC-SHA256 via PHP’s hash_hmac(), dus het steunt op de host-cryptoprovider. In een FIPS-beperkte build faalt een niet-goedgekeurde primitieve op de cryptografische grens in plaats van te degraderen. De webhook-laag voegt geen eigen cryptografisch beleid toe.
Conformiteit
Sectie met titel “Conformiteit”- Payload-authenticatie implementeert HMAC, de keyed-hash message authentication code van FIPS PUB 198-1 §1, geïnstantieerd met SHA-256.
- Replaybescherming volgt de webhook-securityrichtlijnen van de OWASP Cheat Sheet Series: het gebeurtenistijdstempel reist in een dedicated header en wordt geseed in de berekening van de handtekening, zodat een gemanipuleerd tijdstempel de verificatie doet mislukken.
- Body-tijdstempels gebruiken het RFC 3339 extended date-time-formaat. Code-declared: RFC 3339 is niet opgehaald uit het RAG-corpus voor deze pagina.
- Dit zijn capability-uitspraken gegrond in de productbron en de geciteerde clausules. NextPDF doet geen conformiteits- of certificeringsclaim voor dit oppervlak.
Ontwikkelingsnotities
Sectie met titel “Ontwikkelingsnotities”- Alle klassen declareren
strict_types=1en zijnfinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyenDeadLetterEntryzijnfinal readonlymet promoted public properties. - De module draagt een
@since-annotatie van2.2.0; het aan een tijdstempel gebonden handtekeningschema is een gedocumenteerde breaking change in 3.1.0. - De delivery engine neemt PSR-18/PSR-17-abstracties, dus een mock-HTTP-client oefent het volledige verzend-, retry- en dead-letter-pad offline uit. De logger is standaard null; injecteer een PSR-3-logger in productie, anders komen fouten alleen via retourwaarden naar boven.
- Ontvangerimplementaties zouden
hash_equals()moeten gebruiken voor de handtekeningvergelijking en een versheidsvenster opX-NextPDF-Timestampmoeten afdwingen. - Aanbevolen grenstests: tenant-mismatch-registratie, fan-out met lege gebeurtenissenlijst, terminale 4xx, uitputting van retries, SSRF-geblokkeerde URL, afwijzing van een handtekening met gemanipuleerd tijdstempel tegen een vast vector, en begrenzing van het aantal pogingen bij dead-letter.
Publicatiegrens
Sectie met titel “Publicatiegrens”Deze pagina documenteert alleen extern waarneembaar gedrag en het ondersteunde publieke API-oppervlak. Interne namespace-paden, helperklassen, mechanismetabellen, runbook-bestandsnamen en ticketprefixen vallen buiten de scope.
Zie ook
Sectie met titel “Zie ook”- Webhook — NextPDF Enterprise — de capability-pagina: workflow, configuratie en uitgewerkte registratievoorbeelden.
- SaaS — Diepe referentie — tenant-identiteit, API keys en quota; de bron van
TenantContext. - Metering — Diepe referentie — usage-metering-fan-out met dezelfde PSR-18-afleveringsdiscipline.