Enterprise edición
Webhook — Referencia detallada
De un vistazo
Sección titulada «De un vistazo»El espacio de nombres NextPDF\Enterprise\Webhook ofrece entrega de webhooks con ámbito de tenant para eventos de trabajos. La superficie pública consta de seis símbolos: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy y DeadLetterEntry. El gestor registra endpoints por tenant y despacha los eventos de trabajos a los registros suscritos. El motor de entrega envía por POST una carga JSON firmada con HMAC-SHA256, valida cada destino contra la puerta de egreso SSRF de Core, reintenta con retroceso exponencial y registra los fallos permanentes en una cola de mensajes fallidos en memoria. A partir de 3.1.0, la firma vincula la cabecera X-NextPDF-Timestamp a la cadena base del MAC, de modo que los receptores verifican frescura e integridad de forma conjunta. Para la guía a nivel de flujo de trabajo, véase Webhook.
Disponibilidad y licencias
Sección titulada «Disponibilidad y licencias»Esta capacidad se incluye en NextPDF Enterprise (nextpdf/enterprise) y se activa con un sobre de licencia de nivel Enterprise. Un despliegue sin esa habilitación no carga las clases de la capacidad. Comparar ediciones y obtener una licencia.
La superficie de webhooks es una capacidad base de Enterprise, disponible una vez instalado el paquete Enterprise; no hay un indicador por función independiente. NextPDF Core (Apache-2.0) y NextPDF Pro no tienen superficie de registro ni de entrega de webhooks; el gestor, el registro, la carga, el motor de entrega, la política de reintentos y la entrada de mensajes fallidos se incluyen únicamente en nextpdf/enterprise.
Superficie de la API pública
Sección titulada «Superficie de la API pública»| Símbolo | Parámetros | Comportamiento predeterminado | Devuelve | Lanza o falla con | Notas |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Crea un gestor con un índice de registros en memoria vacío | Nuevo WebhookManager | No lanza | Los registros se indexan por tenant |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Añade el registro al índice del tenant que realiza la llamada | void | InvalidArgumentException cuando el tenant del registro no coincide con el tenant del contexto | El registro entre tenants se rechaza antes del almacenamiento |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Reemplaza el registro coincidente por una copia desactivada | bool | No lanza; devuelve false cuando no se encuentra el id | Desactivación lógica; se conserva el historial |
WebhookManager::activeRegistrations | TenantContext $tenant | Filtra los registros del tenant para quedarse con los activos | list<WebhookRegistration> | No lanza | Solo son visibles los registros del tenant que realiza la llamada |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Entrega el evento a cada registro activo que esté suscrito al tipo de evento | int (entregas correctas) | Propaga JsonException cuando los datos del evento no son codificables en JSON; los fallos de entrega no lanzan | Se genera un nuevo id de entrega de 32 hex por cada entrega de registro |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Almacena los valores suministrados tal cual | Nuevo WebhookRegistration | Sin @throws declarado; PHP lanza TypeError ante tipos de argumento no coincidentes bajo strict_types | final readonly; $events vacío significa suscribirse a todos |
WebhookRegistration::subscribesTo | JobEventType $eventType | true cuando $events está vacío o contiene el tipo | bool | No lanza | Comparación de identidad estricta |
WebhookRegistration::deactivate | — | Devuelve una copia inactiva | self | No lanza | La instancia original no se modifica |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Copia el id del trabajo, el tipo de evento, los datos y la marca de tiempo del evento | self | No lanza | Fábrica estática usada por dispatch |
WebhookPayload::toJson | — | Serializa el cuerpo de seis campos con las barras sin escapar | non-empty-string | JsonException cuando los datos del evento no son codificables en JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Devuelve el cuerpo como un array asociativo | array<string, mixed> | No lanza | Marca de tiempo formateada como RFC 3339 extendido |
WebhookPayload::signedTimestamp | — | Tiempo del evento en segundos Unix, acotado a cero o superior | int<0, max> | No lanza | Se emite como X-NextPDF-Timestamp y se vincula al MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 sobre la cadena base {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException mediante toJson() cuando el cuerpo no es codificable | Vincula criptográficamente la cabecera de marca de tiempo al cuerpo |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | Motor de entrega PSR-18/PSR-17 con una cola de mensajes fallidos vacía | Nuevo WebhookDelivery | No lanza | Política predeterminada: 5 intentos, 1 s base, 300 s de tope |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | Envía por POST la carga firmada con validación de egreso SSRF por intento y retroceso exponencial | bool | JsonException antes del primer intento cuando el cuerpo no es codificable; en caso contrario no lanza — false significa que la carga se enrutó a la cola de mensajes fallidos | true solo ante una respuesta 2xx |
WebhookDelivery::deadLetters | — | Devuelve todas las entradas registradas | list<DeadLetterEntry> | No lanza | En memoria, con ámbito de proceso |
WebhookDelivery::clearDeadLetters | — | Vacía la cola de mensajes fallidos | void | No lanza | Irreversible; exportar las entradas primero si se requiere reproducción |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Almacena los valores de la política | Nuevo WebhookRetryPolicy | Sin @throws declarado; los parámetros están documentados como positive-int | $maxRetries cuenta el total de intentos |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), con tope en maxDelaySeconds | positive-int | No lanza | Los números de intento empiezan en 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true mientras el intento actual esté por debajo del máximo | bool | No lanza | La espera se omite tras el intento final |
WebhookRetryPolicy::default | — | 5 intentos, 1 s base, 300 s de tope | self | No lanza | Fábrica estática; predeterminado de producción |
WebhookRetryPolicy::aggressive | — | 10 intentos, 2 s base, 600 s de tope | self | No lanza | Fábrica estática para endpoints críticos |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Almacena el registro de fallo tal cual | Nuevo DeadLetterEntry | Sin @throws declarado; TypeError bajo strict_types | final readonly; $lastHttpStatus nulo significa fallo de transporte |
DeadLetterEntry::markReplayed | — | Devuelve una copia con replayed = true | self | No lanza | Mismo id; la entrada original no se modifica |
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 comportamiento
Sección titulada «Contrato de comportamiento»- Los registros se indexan por tenant.
register()rechaza un registro cuyo identificador de tenant no coincide con el contexto que realiza la llamada.unregister()es una desactivación lógica: el registro se reemplaza por una copia inactiva, conservando el historial y excluyéndolo del despacho futuro. dispatch()itera únicamente los registros activos del tenant que realiza la llamada que están suscritos al tipo de evento despachado. Una lista de eventos suscritos vacía significa suscribirse a todos. El valor de retorno cuenta las entregas correctas.- Cada entrega es un POST HTTP con un cuerpo JSON y cinco cabeceras:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(segundos Unix),X-NextPDF-Delivery-IdyX-NextPDF-Event. - Los campos del cuerpo JSON son
delivery_id,job_id,event_type,data,timestamp(RFC 3339 extendido) ytenant_id, serializados con las barras sin escapar. Los valores del tipo de evento provienen deJobEventTypeennextpdf/core:progress,completed,failed,cancelled. - Esquema de firma (modificado en 3.1.0, con ruptura). La cadena base de HMAC-SHA256 es
{signedTimestamp}.{jsonBody}, con la clave del secreto del registro, no el cuerpo por sí solo. El valor deX-NextPDF-Timestampes el componente de marca de tiempo del MAC, de modo que una cabecera de marca de tiempo manipulada o reproducida invalida la firma. - Verificación en el receptor: leer la cabecera
X-NextPDF-TimestampT; rechazar cuandoTesté fuera de una ventana de frescura aceptable (por ejemplo 300 s); recalcularhash_hmac('sha256', T . '.' . rawBody, secret)sobre los bytes recibidos en bruto; comparar en tiempo constante con el valor de la cabecera tras eliminar el prefijosha256=. - El cuerpo, la firma y el id de entrega se calculan una vez por entrega y permanecen constantes a lo largo de los intentos de reintento.
- Puerta de egreso SSRF. Antes de cada intento, la URL de destino pasa por la puerta
UrlValidator::validateExternalUrl()de Core: únicamente esquema HTTPS; se bloquean los rangos de loopback, privados, reservados, carrier-grade-NAT, de metadatos de nube y de transición IPv6 con IPv4 embebida; los nombres de host se resuelven por DNS (A y AAAA) y los hosts no resolubles se rechazan a prueba de fallos. Una URL bloqueada nunca se envía: el bucle de intentos se aborta y la carga se enruta directamente a la cola de mensajes fallidos con un último errorBlocked SSRF destination:y un estado HTTP nulo. - Clasificación del resultado por intento: 2xx es éxito y devuelve de inmediato; un 4xx distinto de 429 es terminal y va directamente a mensajes fallidos; cualquier otro resultado —3xx, 429, 5xx o una excepción de transporte— es reintentable hasta el total de intentos de la política.
- El retroceso es exponencial: la espera antes del siguiente intento es
baseDelaySeconds × 2^(attempt − 1), con tope enmaxDelaySeconds. La espera se omite tras el intento final. - Cuando ningún intento tiene éxito, una
DeadLetterEntryregistra un id único, el id del registro, la carga original, el recuento de intentos (acotado al máximo de la política), el último mensaje de error, el último estado HTTP (nulo ante un fallo de transporte o un bloqueo SSRF) y la marca de tiempo del fallo. - La cola de mensajes fallidos está en memoria y su ámbito es la vida del proceso.
markReplayed()produce una copia marcada; no reenvía, y la cola conserva la entrada original.
Casos límite y modos de fallo
Sección titulada «Casos límite y modos de fallo»- Lista de eventos vacía. El registro recibe todos los tipos de evento. Acotar la lista de forma explícita cuando el receptor no deba ver todos los eventos.
- 4xx terminal frente a fallo de transporte. Un rechazo 4xx registra un
lastHttpStatuscon valor; un fallo de conexión registra nulo. Usar el valor nulo para distinguir el rechazo del receptor del fallo de transporte. - Destino bloqueado por SSRF. Un registro que apunta a una dirección HTTP, privada, loopback o de metadatos pasa a mensajes fallidos en el primer intento con un error
Blocked SSRF destination:y estado nulo. No se realiza ninguna solicitud saliente. Corregir la URL y volver a registrar. - Receptores heredados tras la actualización. Un receptor que aún verifica el HMAC solo sobre el cuerpo previo a 3.1.0 falla a prueba de fallos frente a las entregas de 3.1.0. Migrar el receptor a la cadena base
{timestamp}.{body}y consumirX-NextPDF-Timestamp. - Datos de evento no codificables.
toJson()ysign()lanzanJsonException, que se propaga fuera dedeliver()ydispatch()antes de realizar cualquier intento. - Bloqueo síncrono.
deliver()espera de forma incorporada entre intentos. El retroceso acumulado alcanza 15 s con la política predeterminada y alrededor de 17 minutos con la política agresiva. Despachar desde un worker de cola cuando la latencia del receptor no sea de confianza. - Acotación del recuento de intentos. El recuento de intentos registrado nunca supera el máximo de la política, aunque el contador interno del bucle lo sobrepase al agotarse.
- Crecimiento y durabilidad de la cola. La cola de mensajes fallidos crece sin límite dentro del proceso y desaparece al reiniciar. Exportar las entradas mediante
deadLetters()y persistirlas externamente antes de llamar aclearDeadLetters()cuando se requiera una reproducción duradera. - La reproducción la dirige el operador. Reentregar significa volver a llamar a
deliver()con la carga de la entrada;markReplayed()solo registra el hecho en una copia. - Riesgo residual de DNS rebinding. La URL se revalida en cada intento, lo que reduce pero no cierra la ventana de rebinding: la abstracción PSR-18 no puede fijar la conexión a la IP validada. Añadir controles de egreso a nivel de red allí donde este riesgo residual importe.
- Manejo del secreto. El secreto del registro es una credencial. El HMAC autentica únicamente integridad y origen; no aporta confidencialidad. No colocar en la carga del evento datos que el receptor no deba ver.
Comportamiento en modo FIPS
Sección titulada «Comportamiento en modo FIPS»La firma de la carga es HMAC-SHA256 mediante hash_hmac() de PHP, por lo que depende del proveedor criptográfico del host. En una compilación restringida a FIPS, una primitiva no aprobada falla en la frontera criptográfica en lugar de degradarse. La capa de webhooks no añade ninguna política criptográfica propia.
Conformidad
Sección titulada «Conformidad»- La autenticación de la carga implementa HMAC, el código de autenticación de mensajes basado en hash con clave de FIPS PUB 198-1 §1, instanciado con SHA-256.
- La protección contra reproducción sigue la guía de seguridad de webhooks de OWASP Cheat Sheet Series: la marca de tiempo del evento viaja en una cabecera dedicada y se incorpora al cálculo de la firma, de modo que una marca de tiempo manipulada falla la verificación.
- Las marcas de tiempo del cuerpo usan el formato de fecha y hora extendido RFC 3339. Declarado en código: RFC 3339 no se recuperó del corpus RAG para esta página.
- Estas son afirmaciones de capacidad fundamentadas en el código fuente del producto y en las cláusulas citadas. NextPDF no realiza ninguna afirmación de conformidad ni de certificación para esta superficie.
Notas de desarrollo
Sección titulada «Notas de desarrollo»- Todas las clases declaran
strict_types=1y sonfinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyyDeadLetterEntrysonfinal readonlycon propiedades públicas promovidas. - El módulo lleva una anotación
@sincede2.2.0; el esquema de firma vinculado a la marca de tiempo es un cambio con ruptura documentado en 3.1.0. - El motor de entrega toma abstracciones PSR-18/PSR-17, de modo que un cliente HTTP simulado ejercita sin conexión toda la ruta de envío, reintento y mensajes fallidos. El logger es nulo por defecto; inyectar un logger PSR-3 en producción o los fallos solo se manifestarán a través de los valores de retorno.
- Las implementaciones del receptor deberían usar
hash_equals()para la comparación de la firma e imponer una ventana de frescura sobreX-NextPDF-Timestamp. - Pruebas de límite recomendadas: registro con tenant no coincidente, difusión con lista de eventos vacía, 4xx terminal, agotamiento de reintentos, URL bloqueada por SSRF, rechazo de firma con marca de tiempo manipulada frente a un vector fijo y acotación del recuento de intentos en mensajes fallidos.
Límite de publicación
Sección titulada «Límite de publicación»Esta página documenta únicamente el comportamiento observable externamente y la superficie de la API pública admitida. Las rutas internas de espacios de nombres, las clases auxiliares, las tablas de mecanismos, los nombres de archivo de runbooks y los prefijos de tickets quedan fuera de alcance.
Véase también
Sección titulada «Véase también»- Webhook — NextPDF Enterprise — la página de la capacidad: flujo de trabajo, configuración y ejemplos prácticos de registro.
- SaaS — Referencia detallada — identidad de tenant, claves de API y cuotas; la fuente de
TenantContext. - Metering — Referencia detallada — difusión de medición de uso con la misma disciplina de entrega PSR-18.