Salta ai contenuti
getnextpdf.com

Enterprise edizione

Webhook — Riferimento approfondito

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.

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.

SimboloParametriComportamento predefinitoRestituisceSolleva o fallisce conNote
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullCrea un manager con un indice di registrazioni vuoto in memoriaNuovo WebhookManagerNon sollevaLe registrazioni sono indicizzate per tenant
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationAggiunge la registrazione all’indice del tenant chiamantevoidInvalidArgumentException quando il tenant della registrazione non corrisponde al tenant del contestoLa registrazione cross-tenant è rifiutata prima della memorizzazione
WebhookManager::unregisterTenantContext $tenant, string $registrationIdSostituisce la registrazione corrispondente con una copia disattivataboolNon solleva; restituisce false quando l’id non viene trovatoDisattivazione soft; la storia è preservata
WebhookManager::activeRegistrationsTenantContext $tenantFiltra le registrazioni del tenant a quelle attivelist<WebhookRegistration>Non sollevaSono visibili solo le registrazioni del tenant chiamante
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventRecapita l’evento a ogni registrazione attiva iscritta al tipo di eventoint (recapiti riusciti)Propaga JsonException quando i dati dell’evento non sono codificabili in JSON; i fallimenti di recapito non sollevanoPer ogni recapito di registrazione viene generato un nuovo id di recapito a 32 cifre esadecimali
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullMemorizza i valori forniti verbatimNuovo WebhookRegistrationNessun @throws dichiarato; PHP solleva TypeError su tipi di argomento non corrispondenti sotto strict_typesfinal readonly; $events vuoto significa iscriversi a tutti
WebhookRegistration::subscribesToJobEventType $eventTypetrue quando $events è vuoto o contiene il tipoboolNon sollevaConfronto di identità stretto
WebhookRegistration::deactivateRestituisce una copia inattivaselfNon sollevaL’istanza originale è invariata
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdCopia dall’evento id del job, tipo di evento, dati e timestampselfNon sollevaFactory statica usata da dispatch
WebhookPayload::toJsonSerializza il corpo a sei campi con slash non escapednon-empty-stringJsonException quando i dati dell’evento non sono codificabili in JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayRestituisce il corpo come array associativoarray<string, mixed>Non sollevaTimestamp formattato come RFC 3339 esteso
WebhookPayload::signedTimestampTempo dell’evento in secondi Unix, limitato a zero o superioreint<0, max>Non sollevaEmesso come X-NextPDF-Timestamp e vincolato nel MAC
WebhookPayload::signstring $secretHMAC-SHA256 sulla stringa base {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException tramite toJson() quando il corpo non è codificabileVincola crittograficamente l’header timestamp al corpo
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullMotore di recapito PSR-18/PSR-17 con una coda dead-letter vuotaNuovo WebhookDeliveryNon sollevaPolicy predefinita: 5 tentativi, base 1 s, tetto 300 s
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadEffettua il POST del payload firmato con validazione di egress SSRF per tentativo e backoff esponenzialeboolJsonException prima del primo tentativo quando il corpo non è codificabile; altrimenti non solleva — false significa che il payload è stato instradato nella coda dead-lettertrue solo su una risposta 2xx
WebhookDelivery::deadLettersRestituisce tutte le voci registratelist<DeadLetterEntry>Non sollevaIn memoria, con ambito il processo
WebhookDelivery::clearDeadLettersSvuota la coda dead-lettervoidNon sollevaIrreversibile; esportare prima le voci se è richiesto il replay
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Memorizza i valori della policyNuovo WebhookRetryPolicyNessun @throws dichiarato; i parametri sono documentati positive-int$maxRetries conta i tentativi totali
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), con tetto a maxDelaySecondspositive-intNon sollevaI numeri di tentativo partono da 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue finché il tentativo corrente è inferiore al massimoboolNon sollevaL’attesa è saltata dopo il tentativo finale
WebhookRetryPolicy::default5 tentativi, base 1 s, tetto 300 sselfNon sollevaFactory statica; predefinita di produzione
WebhookRetryPolicy::aggressive10 tentativi, base 2 s, tetto 600 sselfNon sollevaFactory statica per endpoint critici
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseMemorizza il record di fallimento verbatimNuovo DeadLetterEntryNessun @throws dichiarato; TypeError sotto strict_typesfinal readonly; $lastHttpStatus null significa fallimento di trasporto
DeadLetterEntry::markReplayedRestituisce una copia con replayed = trueselfNon sollevaStesso 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): 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
  • 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-Id e X-NextPDF-Event.
  • I campi del corpo JSON sono delivery_id, job_id, event_type, data, timestamp (RFC 3339 esteso) e tenant_id, serializzati con slash non escaped. I valori del tipo di evento provengono da JobEventType in nextpdf/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 valore X-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-Timestamp T; rifiutare quando T è fuori da una finestra di freschezza accettabile (per esempio 300 s); ricalcolare hash_hmac('sha256', T . '.' . rawBody, secret) sui byte grezzi ricevuti; confrontare in tempo costante con il valore dell’header dopo aver rimosso il prefisso sha256=.
  • 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 errore Blocked 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 a maxDelaySeconds. L’attesa è saltata dopo il tentativo finale.
  • Quando nessun tentativo riesce, una DeadLetterEntry registra 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.
  • 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 lastHttpStatus popolato; 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 consumare X-NextPDF-Timestamp.
  • Dati dell’evento non codificabili. toJson() e sign() sollevano JsonException, che si propaga fuori da deliver() e dispatch() 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 chiamare clearDeadLetters() 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.

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.

  • 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.
  • Tutte le classi dichiarano strict_types=1 e sono final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy e DeadLetterEntry sono final readonly con proprietà pubbliche promosse.
  • Il modulo riporta un’annotazione @since pari a 2.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 su X-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.

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.