Enterprise edição
Webhook — Referência Profunda
Visão geral
Seção intitulada “Visão geral”O namespace NextPDF\Enterprise\Webhook fornece entrega de webhook com escopo por locatário para eventos de job. A superfície pública são seis símbolos: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy e DeadLetterEntry. O gerenciador registra endpoints por locatário e despacha eventos de job para os registros assinantes. O engine de entrega faz POST de um payload JSON assinado com HMAC-SHA256, valida cada destino contra o gate de egress SSRF do Core, tenta novamente com backoff exponencial e registra falhas permanentes em uma fila de dead-letter em memória. A partir da 3.1.0, a assinatura vincula o cabeçalho X-NextPDF-Timestamp na base string do MAC, de modo que os receptores verificam frescor e integridade juntos. Para o guia em nível de workflow, consulte Webhook.
Disponibilidade e licenciamento
Seção intitulada “Disponibilidade e licenciamento”Esta capacidade é fornecida no NextPDF Enterprise (nextpdf/enterprise) e é ativada com um envelope de licença de nível Enterprise. Uma implantação sem essa titularidade não carrega as classes da capacidade. Compare edições e obtenha uma licença.
A superfície de webhook é uma capacidade base do Enterprise, disponível assim que o pacote Enterprise é instalado; não há nenhum sinalizador por recurso separado. O NextPDF Core (Apache-2.0) e o NextPDF Pro não têm nenhuma superfície de registro ou entrega de webhook; o gerenciador, o registro, o payload, o engine de entrega, a política de retry e a entrada de dead-letter são fornecidos apenas em nextpdf/enterprise.
Superfície da API pública
Seção intitulada “Superfície da API pública”| Símbolo | Parâmetros | Comportamento padrão | Retorna | Lança ou falha com | Notas |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Cria um gerenciador com um índice de registros vazio em memória | Novo WebhookManager | Não lança | Os registros são indexados por locatário |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Anexa o registro ao índice do locatário chamador | void | InvalidArgumentException quando o locatário do registro não corresponde ao locatário do contexto | O registro entre locatários é rejeitado antes do armazenamento |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Substitui o registro correspondente por uma cópia desativada | bool | Não lança; retorna false quando o id não é encontrado | Desativação suave; o histórico é preservado |
WebhookManager::activeRegistrations | TenantContext $tenant | Filtra os registros do locatário para os ativos | list<WebhookRegistration> | Não lança | Apenas os registros do locatário chamador são visíveis |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Entrega o evento a cada registro ativo que assina o tipo de evento | int (entregas bem-sucedidas) | Propaga JsonException quando os dados do evento não são codificáveis em JSON; falhas de entrega não lançam | Um novo id de entrega de 32 hex é gerado por entrega de registro |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Armazena os valores fornecidos literalmente | Novo WebhookRegistration | Nenhum @throws declarado; o PHP lança TypeError em tipos de argumento incompatíveis sob strict_types | final readonly; $events vazio significa assinar-todos |
WebhookRegistration::subscribesTo | JobEventType $eventType | true quando $events está vazio ou contém o tipo | bool | Não lança | Comparação de identidade estrita |
WebhookRegistration::deactivate | — | Retorna uma cópia inativa | self | Não lança | A instância original permanece inalterada |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Copia id do job, tipo de evento, dados e timestamp do evento | self | Não lança | Factory estático usado por dispatch |
WebhookPayload::toJson | — | Serializa o corpo de seis campos com barras não escapadas | non-empty-string | JsonException quando os dados do evento não são codificáveis em JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Retorna o corpo como um array associativo | array<string, mixed> | Não lança | Timestamp formatado como RFC 3339 estendido |
WebhookPayload::signedTimestamp | — | Tempo do evento em segundos Unix, fixado em zero ou maior | int<0, max> | Não lança | Emitido como X-NextPDF-Timestamp e vinculado ao MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 sobre a base string {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException via toJson() quando o corpo não é codificável | Vincula o cabeçalho de timestamp ao corpo criptograficamente |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | Engine de entrega PSR-18/PSR-17 com uma fila de dead-letter vazia | Novo WebhookDelivery | Não lança | Política padrão: 5 tentativas, 1 s base, 300 s limite |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | Faz POST do payload assinado com validação de egress SSRF por tentativa e backoff exponencial | bool | JsonException antes da primeira tentativa quando o corpo não é codificável; caso contrário não lança — false significa que o payload foi roteado para a fila de dead-letter | true apenas em uma resposta 2xx |
WebhookDelivery::deadLetters | — | Retorna todas as entradas registradas | list<DeadLetterEntry> | Não lança | Em memória, com escopo de processo |
WebhookDelivery::clearDeadLetters | — | Esvazia a fila de dead-letter | void | Não lança | Irreversível; exporte as entradas primeiro se o replay for necessário |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Armazena os valores da política | Novo WebhookRetryPolicy | Nenhum @throws declarado; os parâmetros são documentados como positive-int | $maxRetries conta o total de tentativas |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), limitado a maxDelaySeconds | positive-int | Não lança | Os números de tentativa começam em 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true enquanto a tentativa atual está abaixo do máximo | bool | Não lança | A espera é pulada após a tentativa final |
WebhookRetryPolicy::default | — | 5 tentativas, 1 s base, 300 s limite | self | Não lança | Factory estático; padrão de produção |
WebhookRetryPolicy::aggressive | — | 10 tentativas, 2 s base, 600 s limite | self | Não lança | Factory estático para endpoints críticos |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Armazena o registro de falha literalmente | Novo DeadLetterEntry | Nenhum @throws declarado; TypeError sob strict_types | final readonly; $lastHttpStatus nulo significa falha de transporte |
DeadLetterEntry::markReplayed | — | Retorna uma cópia com replayed = true | self | Não lança | Mesmo id; a entrada original permanece inalterada |
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(): selfContrato de comportamento
Seção intitulada “Contrato de comportamento”- Os registros são indexados por locatário.
register()rejeita um registro cujo identificador de locatário não corresponde ao contexto chamador.unregister()é uma desativação suave: o registro é substituído por uma cópia inativa, preservando o histórico enquanto o exclui de dispatches futuros. dispatch()itera apenas os registros ativos do locatário chamador que assinam o tipo de evento despachado. Uma lista de eventos assinados vazia significa assinar-todos. O valor de retorno conta as entregas bem-sucedidas.- Cada entrega é um HTTP POST com um corpo JSON e cinco cabeçalhos:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(segundos unix),X-NextPDF-Delivery-IdeX-NextPDF-Event. - Os campos do corpo JSON são
delivery_id,job_id,event_type,data,timestamp(RFC 3339 estendido) etenant_id, serializados com barras não escapadas. Os valores de tipo de evento vêm deJobEventTypeemnextpdf/core:progress,completed,failed,cancelled. - Esquema de assinatura (alterado na 3.1.0, breaking). A base string do HMAC-SHA256 é
{signedTimestamp}.{jsonBody}, com chave igual ao segredo do registro — não apenas o corpo. O valor deX-NextPDF-Timestampé o componente de timestamp do MAC, de modo que um cabeçalho de timestamp adulterado ou reproduzido invalida a assinatura. - Verificação do receptor: leia o cabeçalho
X-NextPDF-TimestampT; rejeite quandoTestiver fora de uma janela de frescor aceitável (por exemplo, 300 s); recalculehash_hmac('sha256', T . '.' . rawBody, secret)sobre os bytes brutos recebidos; compare em tempo constante com o valor do cabeçalho após remover o prefixosha256=. - O corpo, a assinatura e o id de entrega são calculados uma vez por entrega e permanecem constantes entre as tentativas de retry.
- Gate de egress SSRF. Antes de cada tentativa, a URL de destino passa pelo gate
UrlValidator::validateExternalUrl()do Core: apenas esquema HTTPS; faixas de loopback, privadas, reservadas, carrier-grade-NAT, de metadados de nuvem e de transição IPv6 com IPv4 embutido são bloqueadas; os hostnames são resolvidos por DNS (A e AAAA) e hosts não resolvíveis são rejeitados com fail-closed. Uma URL bloqueada nunca é enviada: o loop de tentativas aborta e o payload é roteado diretamente para a fila de dead-letter com um último erroBlocked SSRF destination:e um status HTTP nulo. - Classificação de resultado por tentativa: 2xx é sucesso e retorna imediatamente; um 4xx diferente de 429 é terminal e vai direto para dead-letter; todo outro resultado — 3xx, 429, 5xx ou uma exceção de transporte — é passível de retry até a contagem total de tentativas da política.
- O backoff é exponencial: a espera antes da próxima tentativa é
baseDelaySeconds × 2^(attempt − 1), limitada amaxDelaySeconds. A espera é pulada após a tentativa final. - Quando nenhuma tentativa é bem-sucedida, um
DeadLetterEntryregistra um id único, o id do registro, o payload original, a contagem de tentativas (fixada no máximo da política), a última mensagem de erro, o último status HTTP (nulo em falha de transporte ou bloqueio SSRF) e o timestamp da falha. - A fila de dead-letter é em memória e tem escopo da duração do processo.
markReplayed()produz uma cópia sinalizada; ela não reenvia, e a fila mantém a entrada original.
Casos extremos e modos de falha
Seção intitulada “Casos extremos e modos de falha”- Lista de eventos vazia. O registro recebe todo tipo de evento. Defina o escopo da lista explicitamente quando o receptor não deve ver todos os eventos.
- 4xx terminal versus falha de transporte. Uma rejeição 4xx registra um
lastHttpStatuspreenchido; uma falha de conexão registra nulo. Use o nulo para distinguir a rejeição do receptor da falha de transporte. - Destino bloqueado por SSRF. Um registro apontando para um endereço HTTP, privado, loopback ou de metadados vai para dead-letter na primeira tentativa com um erro
Blocked SSRF destination:e status nulo. Nenhuma requisição de saída é feita. Corrija a URL e registre novamente. - Receptores legados após a atualização. Um receptor que ainda verifica o HMAC apenas-do-corpo anterior à 3.1.0 falha com fail-closed contra as entregas 3.1.0. Migre o receptor para a base string
{timestamp}.{body}e consumaX-NextPDF-Timestamp. - Dados de evento não codificáveis.
toJson()esign()lançamJsonException, que se propaga para fora dedeliver()edispatch()antes que qualquer tentativa seja feita. - Bloqueio síncrono.
deliver()dorme inline entre as tentativas. O backoff cumulativo chega a 15 s sob a política default e cerca de 17 minutos sob a política aggressive. Despache a partir de um queue worker quando a latência do receptor não é confiável. - Fixação da contagem de tentativas. A contagem de tentativas registrada nunca excede o máximo da política, mesmo que o contador interno do loop avance além dele no esgotamento.
- Crescimento e durabilidade da fila. A fila de dead-letter cresce sem limites dentro do processo e desaparece na reinicialização. Exporte as entradas via
deadLetters()e persista-as externamente antes de chamarclearDeadLetters()quando o replay durável for necessário. - O replay é conduzido pelo operador. A reentrega significa chamar
deliver()novamente com o payload da entrada;markReplayed()apenas registra o fato em uma cópia. - Resíduo de DNS-rebinding. A URL é revalidada em cada tentativa, o que estreita mas não fecha a janela de rebinding: a abstração PSR-18 não pode fixar a conexão ao IP validado. Adicione controles de egress em nível de rede onde esse resíduo importa.
- Tratamento do segredo. O segredo do registro é uma credencial. O HMAC autentica apenas integridade e origem — ele não é confidencialidade. Não coloque no payload do evento dados que o receptor não deve ver.
Comportamento em modo FIPS
Seção intitulada “Comportamento em modo FIPS”A assinatura do payload é HMAC-SHA256 através de hash_hmac() do PHP, portanto ela depende do provedor de criptografia do host. Em um build com restrição FIPS, uma primitiva não aprovada falha no limite criptográfico em vez de fazer downgrade. A camada de webhook não acrescenta nenhuma política criptográfica própria.
Conformidade
Seção intitulada “Conformidade”- A autenticação de payload implementa HMAC, o código de autenticação de mensagem com hash e chave do FIPS PUB 198-1 §1, instanciado com SHA-256.
- A proteção contra replay segue a orientação de segurança de webhook da OWASP Cheat Sheet Series: o timestamp do evento viaja em um cabeçalho dedicado e é semeado no cálculo da assinatura, de modo que um timestamp adulterado falha na verificação.
- Os timestamps do corpo usam o formato de data-hora estendido RFC 3339. Declarado em código: RFC 3339 não foi recuperado do corpus RAG para esta página.
- Estas são declarações de capacidade fundamentadas no código-fonte do produto e nas cláusulas citadas. A NextPDF não faz nenhuma alegação de conformidade ou certificação para esta superfície.
Notas de desenvolvimento
Seção intitulada “Notas de desenvolvimento”- Todas as classes declaram
strict_types=1e sãofinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyeDeadLetterEntrysãofinal readonlycom propriedades públicas promovidas. - O módulo carrega uma anotação
@sincede2.2.0; o esquema de assinatura vinculado a timestamp é uma mudança breaking documentada na 3.1.0. - O engine de entrega recebe abstrações PSR-18/PSR-17, então um mock de HTTP client exercita todo o caminho de envio, retry e dead-letter offline. O logger tem padrão null; injete um logger PSR-3 em produção ou as falhas aparecem apenas através dos valores de retorno.
- As implementações do receptor devem usar
hash_equals()para a comparação da assinatura e impor uma janela de frescor emX-NextPDF-Timestamp. - Testes de limite recomendados: registro com locatário incompatível, fan-out de lista de eventos vazia, 4xx terminal, esgotamento de retry, URL bloqueada por SSRF, rejeição de assinatura com timestamp adulterado contra um vetor fixo e fixação da contagem de tentativas de dead-letter.
Limite de publicação
Seção intitulada “Limite de publicação”Esta página documenta apenas o comportamento observável externamente e a superfície da API pública suportada. Caminhos de namespace internos, classes auxiliares, tabelas de mecanismos, nomes de arquivo de runbook e prefixos de tíquete estão fora do escopo.
Consulte também
Seção intitulada “Consulte também”- Webhook — NextPDF Enterprise — a página da capacidade: workflow, configuração e exemplos de registro trabalhados.
- SaaS — Referência Profunda — identidade de locatário, API keys e cotas; a origem de
TenantContext. - Metering — Referência Profunda — fan-out de medição de uso com a mesma disciplina de entrega PSR-18.