Enterprise edizione
Webhook — Riferimento approfondito
In sintesi
Sezione intitolata “In sintesi”Il namespace NextPDF\Enterprise\Webhook fornisce il recapito di webhook con ambito per tenant per gli eventi dei job. La superficie pubblica è composta da sei simboli: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy e DeadLetterEntry. Il manager registra gli endpoint per tenant e dispatcha gli eventi dei job alle registrazioni iscritte. Il motore di recapito effettua il POST di un payload JSON firmato con HMAC-SHA256, valida ogni destinazione rispetto al gate di egress SSRF di Core, ritenta con backoff esponenziale e registra i fallimenti permanenti in una coda dead-letter in memoria. A partire dalla 3.1.0 la firma vincola l’header X-NextPDF-Timestamp nella stringa base del MAC, così i ricevitori verificano insieme freschezza e integrità. Per la guida a livello di workflow, vedere Webhook.
Disponibilità e licenza
Sezione intitolata “Disponibilità e licenza”Questa capacità è inclusa in NextPDF Enterprise (nextpdf/enterprise) e si attiva con un envelope di licenza di tier Enterprise. Un deployment privo di tale entitlement non carica le classi della capacità. Confronta le edizioni e ottieni una licenza.
La superficie webhook è una capacità Enterprise di base, disponibile una volta installato il pacchetto Enterprise; non esiste un flag per-funzionalità separato. NextPDF Core (Apache-2.0) e NextPDF Pro non hanno alcuna superficie di registrazione o recapito di webhook; il manager, la registrazione, il payload, il motore di recapito, la retry policy e la voce dead-letter sono inclusi solo in nextpdf/enterprise.
Superficie dell’API pubblica
Sezione intitolata “Superficie dell’API pubblica”| Simbolo | Parametri | Comportamento predefinito | Restituisce | Solleva o fallisce con | Note |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Crea un manager con un indice di registrazioni vuoto in memoria | Nuovo WebhookManager | Non solleva | Le registrazioni sono indicizzate per tenant |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Aggiunge la registrazione all’indice del tenant chiamante | void | InvalidArgumentException quando il tenant della registrazione non corrisponde al tenant del contesto | La registrazione cross-tenant è rifiutata prima della memorizzazione |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Sostituisce la registrazione corrispondente con una copia disattivata | bool | Non solleva; restituisce false quando l’id non viene trovato | Disattivazione soft; la storia è preservata |
WebhookManager::activeRegistrations | TenantContext $tenant | Filtra le registrazioni del tenant a quelle attive | list<WebhookRegistration> | Non solleva | Sono visibili solo le registrazioni del tenant chiamante |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Recapita l’evento a ogni registrazione attiva iscritta al tipo di evento | int (recapiti riusciti) | Propaga JsonException quando i dati dell’evento non sono codificabili in JSON; i fallimenti di recapito non sollevano | Per ogni recapito di registrazione viene generato un nuovo id di recapito a 32 cifre esadecimali |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Memorizza i valori forniti verbatim | Nuovo WebhookRegistration | Nessun @throws dichiarato; PHP solleva TypeError su tipi di argomento non corrispondenti sotto strict_types | final readonly; $events vuoto significa iscriversi a tutti |
WebhookRegistration::subscribesTo | JobEventType $eventType | true quando $events è vuoto o contiene il tipo | bool | Non solleva | Confronto di identità stretto |
WebhookRegistration::deactivate | — | Restituisce una copia inattiva | self | Non solleva | L’istanza originale è invariata |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Copia dall’evento id del job, tipo di evento, dati e timestamp | self | Non solleva | Factory statica usata da dispatch |
WebhookPayload::toJson | — | Serializza il corpo a sei campi con slash non escaped | non-empty-string | JsonException quando i dati dell’evento non sono codificabili in JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Restituisce il corpo come array associativo | array<string, mixed> | Non solleva | Timestamp formattato come RFC 3339 esteso |
WebhookPayload::signedTimestamp | — | Tempo dell’evento in secondi Unix, limitato a zero o superiore | int<0, max> | Non solleva | Emesso come X-NextPDF-Timestamp e vincolato nel MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 sulla stringa base {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException tramite toJson() quando il corpo non è codificabile | Vincola crittograficamente l’header timestamp al corpo |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | Motore di recapito PSR-18/PSR-17 con una coda dead-letter vuota | Nuovo WebhookDelivery | Non solleva | Policy predefinita: 5 tentativi, base 1 s, tetto 300 s |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | Effettua il POST del payload firmato con validazione di egress SSRF per tentativo e backoff esponenziale | bool | JsonException prima del primo tentativo quando il corpo non è codificabile; altrimenti non solleva — false significa che il payload è stato instradato nella coda dead-letter | true solo su una risposta 2xx |
WebhookDelivery::deadLetters | — | Restituisce tutte le voci registrate | list<DeadLetterEntry> | Non solleva | In memoria, con ambito il processo |
WebhookDelivery::clearDeadLetters | — | Svuota la coda dead-letter | void | Non solleva | Irreversibile; esportare prima le voci se è richiesto il replay |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Memorizza i valori della policy | Nuovo WebhookRetryPolicy | Nessun @throws dichiarato; i parametri sono documentati positive-int | $maxRetries conta i tentativi totali |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), con tetto a maxDelaySeconds | positive-int | Non solleva | I numeri di tentativo partono da 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true finché il tentativo corrente è inferiore al massimo | bool | Non solleva | L’attesa è saltata dopo il tentativo finale |
WebhookRetryPolicy::default | — | 5 tentativi, base 1 s, tetto 300 s | self | Non solleva | Factory statica; predefinita di produzione |
WebhookRetryPolicy::aggressive | — | 10 tentativi, base 2 s, tetto 600 s | self | Non solleva | Factory statica per endpoint critici |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Memorizza il record di fallimento verbatim | Nuovo DeadLetterEntry | Nessun @throws dichiarato; TypeError sotto strict_types | final readonly; $lastHttpStatus null significa fallimento di trasporto |
DeadLetterEntry::markReplayed | — | Restituisce una copia con replayed = true | self | Non solleva | Stesso id; la voce originale è invariata |
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(): selfContratto di comportamento
Sezione intitolata “Contratto di comportamento”- Le registrazioni sono indicizzate per tenant.
register()rifiuta una registrazione il cui identificatore di tenant non corrisponde al contesto chiamante.unregister()è una disattivazione soft: la registrazione viene sostituita con una copia inattiva, preservando la storia ed escludendola dai dispatch futuri. dispatch()itera solo le registrazioni attive del tenant chiamante iscritte al tipo di evento dispatchato. Un elenco di eventi sottoscritti vuoto significa iscriversi a tutti. Il valore restituito conta i recapiti riusciti.- Ogni recapito è un HTTP POST con un corpo JSON e cinque header:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(secondi Unix),X-NextPDF-Delivery-IdeX-NextPDF-Event. - I campi del corpo JSON sono
delivery_id,job_id,event_type,data,timestamp(RFC 3339 esteso) etenant_id, serializzati con slash non escaped. I valori del tipo di evento provengono daJobEventTypeinnextpdf/core:progress,completed,failed,cancelled. - Schema di firma (modificato nella 3.1.0, breaking). La stringa base HMAC-SHA256 è
{signedTimestamp}.{jsonBody}, con chiave il secret della registrazione — non il solo corpo. Il valoreX-NextPDF-Timestampè la componente timestamp del MAC, quindi un header timestamp manomesso o rigiocato invalida la firma. - Verifica del ricevitore: leggere l’header
X-NextPDF-TimestampT; rifiutare quandoTè fuori da una finestra di freschezza accettabile (per esempio 300 s); ricalcolarehash_hmac('sha256', T . '.' . rawBody, secret)sui byte grezzi ricevuti; confrontare in tempo costante con il valore dell’header dopo aver rimosso il prefissosha256=. - Il corpo, la firma e l’id di recapito sono calcolati una sola volta per recapito e restano costanti attraverso i tentativi di retry.
- Gate di egress SSRF. Prima di ogni tentativo l’URL di destinazione supera il gate
UrlValidator::validateExternalUrl()di Core: solo schema HTTPS; gli intervalli loopback, privati, riservati, carrier-grade-NAT, cloud-metadata e di transizione IPv6 con IPv4 incorporato sono bloccati; gli hostname vengono risolti via DNS (A e AAAA) e gli host non risolvibili sono rifiutati fail-closed. Un URL bloccato non viene mai inviato: il ciclo dei tentativi si interrompe e il payload viene instradato direttamente nella coda dead-letter con un ultimo erroreBlocked SSRF destination:e uno status HTTP null. - Classificazione dell’esito per tentativo: 2xx è successo e ritorna immediatamente; un 4xx diverso da 429 è terminale e va direttamente in dead-letter; ogni altro esito — 3xx, 429, 5xx o un’eccezione di trasporto — è retryable fino al numero totale di tentativi della policy.
- Il backoff è esponenziale: l’attesa prima del tentativo successivo è
baseDelaySeconds × 2^(attempt − 1), con tetto amaxDelaySeconds. L’attesa è saltata dopo il tentativo finale. - Quando nessun tentativo riesce, una
DeadLetterEntryregistra un id univoco, l’id della registrazione, il payload originale, il conteggio dei tentativi (limitato al massimo della policy), l’ultimo messaggio di errore, l’ultimo status HTTP (null su fallimento di trasporto o blocco SSRF) e il timestamp del fallimento. - La coda dead-letter è in memoria e ha ambito la durata del processo.
markReplayed()produce una copia contrassegnata; non effettua un nuovo invio e la coda conserva la voce originale.
Casi limite e modalità di errore
Sezione intitolata “Casi limite e modalità di errore”- Elenco di eventi vuoto. La registrazione riceve ogni tipo di evento. Limitare esplicitamente l’ambito dell’elenco quando il ricevitore non deve vedere tutti gli eventi.
- 4xx terminale rispetto a fallimento di trasporto. Un rifiuto 4xx registra un
lastHttpStatuspopolato; un fallimento di connessione registra null. Usare il null per distinguere il rifiuto del ricevitore dal fallimento di trasporto. - Destinazione bloccata da SSRF. Una registrazione che punta a un indirizzo HTTP, privato, loopback o di metadata va in dead-letter al primo tentativo con un errore
Blocked SSRF destination:e status null. Non viene effettuata alcuna richiesta in uscita. Correggere l’URL e registrare di nuovo. - Ricevitori legacy dopo l’aggiornamento. Un ricevitore che verifica ancora l’HMAC solo sul corpo precedente alla 3.1.0 fallisce fail-closed rispetto ai recapiti 3.1.0. Migrare il ricevitore alla stringa base
{timestamp}.{body}e consumareX-NextPDF-Timestamp. - Dati dell’evento non codificabili.
toJson()esign()sollevanoJsonException, che si propaga fuori dadeliver()edispatch()prima che venga effettuato alcun tentativo. - Blocco sincrono.
deliver()attende inline tra i tentativi. Il backoff cumulativo raggiunge 15 s con la policy predefinita e circa 17 minuti con la policy aggressive. Dispatchare da un queue worker quando la latenza del ricevitore non è affidabile. - Limitazione del conteggio dei tentativi. Il conteggio dei tentativi registrato non supera mai il massimo della policy, anche se il contatore del ciclo interno lo oltrepassa all’esaurimento.
- Crescita e durabilità della coda. La coda dead-letter cresce senza limiti all’interno del processo e scompare al riavvio. Esportare le voci tramite
deadLetters()e persisterle esternamente prima di chiamareclearDeadLetters()quando è richiesto un replay durevole. - Il replay è guidato dall’operatore. Il nuovo recapito significa chiamare di nuovo
deliver()con il payload della voce;markReplayed()registra solo il fatto su una copia. - Residuo di DNS-rebinding. L’URL viene ri-validato a ogni tentativo, il che restringe ma non chiude la finestra di rebinding: l’astrazione PSR-18 non può vincolare la connessione all’IP validato. Aggiungere controlli di egress a livello di rete dove questo residuo è rilevante.
- Gestione del secret. Il secret della registrazione è una credenziale. L’HMAC autentica solo integrità e origine — non è confidenzialità. Non inserire nel payload dell’evento dati che il ricevitore non deve vedere.
Comportamento in modalità FIPS
Sezione intitolata “Comportamento in modalità FIPS”La firma del payload è HMAC-SHA256 tramite hash_hmac() di PHP, quindi si affida al provider crittografico dell’host. In una build con vincoli FIPS una primitiva non approvata fallisce al confine crittografico anziché degradare. Lo strato webhook non aggiunge alcuna politica crittografica propria.
Conformità
Sezione intitolata “Conformità”- L’autenticazione del payload implementa HMAC, il codice di autenticazione dei messaggi con hash a chiave di FIPS PUB 198-1 §1, istanziato con SHA-256.
- La protezione dal replay segue le linee guida di sicurezza per i webhook della OWASP Cheat Sheet Series: il timestamp dell’evento viaggia in un header dedicato ed è inserito nel calcolo della firma, così un timestamp manomesso fallisce la verifica.
- I timestamp del corpo usano il formato data-ora esteso RFC 3339. Dichiarato nel codice: RFC 3339 non è stato recuperato dal corpus RAG per questa pagina.
- Queste sono dichiarazioni di capacità fondate sul codice sorgente del prodotto e sulle clausole citate. NextPDF non avanza alcuna dichiarazione di conformità o certificazione per questa superficie.
Note di sviluppo
Sezione intitolata “Note di sviluppo”- Tutte le classi dichiarano
strict_types=1e sonofinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyeDeadLetterEntrysonofinal readonlycon proprietà pubbliche promosse. - Il modulo riporta un’annotazione
@sincepari a2.2.0; lo schema di firma vincolato al timestamp è un breaking change documentato nella 3.1.0. - Il motore di recapito accetta astrazioni PSR-18/PSR-17, quindi un client HTTP mock esercita offline l’intero percorso di invio, retry e dead-letter. Il logger è null per impostazione predefinita; iniettare un logger PSR-3 in produzione, altrimenti i fallimenti emergono solo attraverso i valori restituiti.
- Le implementazioni del ricevitore dovrebbero usare
hash_equals()per il confronto della firma e imporre una finestra di freschezza suX-NextPDF-Timestamp. - Test di confine consigliati: registrazione con tenant non corrispondente, fan-out con elenco di eventi vuoto, 4xx terminale, esaurimento dei retry, URL bloccato da SSRF, rifiuto della firma con timestamp manomesso rispetto a un vettore fisso e limitazione del conteggio dei tentativi in dead-letter.
Confine di pubblicazione
Sezione intitolata “Confine di pubblicazione”Questa pagina documenta solo il comportamento osservabile dall’esterno e la superficie dell’API pubblica supportata. I percorsi di namespace interni, le classi helper, le tabelle dei meccanismi, i nomi dei file di runbook e i prefissi dei ticket sono fuori ambito.
Vedere anche
Sezione intitolata “Vedere anche”- Webhook — NextPDF Enterprise — la pagina della capacità: workflow, configurazione ed esempi di registrazione svolti.
- SaaS — Riferimento approfondito — identità del tenant, API key e quote; l’origine di
TenantContext. - Metering — Riferimento approfondito — fan-out di metering d’uso con la stessa disciplina di recapito PSR-18.