Enterprise редакция
Webhook — углублённый справочник
Пространство имён NextPDF\Enterprise\Webhook предоставляет доставку webhook с изоляцией по арендаторам для событий задач. Публичная поверхность — это шесть символов: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy и DeadLetterEntry. Менеджер регистрирует конечные точки для каждого арендатора и диспетчеризует события задач в подписанные регистрации. Движок доставки отправляет POST-запросом полезную нагрузку JSON, подписанную HMAC-SHA256, проверяет каждый адрес назначения через шлюз исходящего трафика Core против SSRF, повторяет с экспоненциальной отсрочкой и записывает постоянные сбои в очередь недоставленных сообщений в памяти. Начиная с 3.1.0 подпись привязывает заголовок X-NextPDF-Timestamp к базовой строке MAC, поэтому получатели проверяют свежесть и целостность вместе. Руководство на уровне рабочего процесса см. в Webhook.
Доступность и лицензирование
Заголовок раздела «Доступность и лицензирование»Эта возможность поставляется в NextPDF Enterprise (nextpdf/enterprise) и активируется лицензионным конвертом уровня Enterprise. Развёртывание без этого права не загружает классы возможности. Сравните редакции и получите лицензию.
Поверхность webhook — это базовая возможность Enterprise, доступная после установки пакета Enterprise; отдельного флага для каждой функции нет. У NextPDF Core (Apache-2.0) и NextPDF Pro нет поверхности регистрации или доставки webhook; менеджер, регистрация, полезная нагрузка, движок доставки, политика повторов и запись недоставленных сообщений поставляются только в nextpdf/enterprise.
Публичная поверхность API
Заголовок раздела «Публичная поверхность API»| Символ | Параметры | Поведение по умолчанию | Возвращает | Бросает или завершается сбоем | Примечания |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Создаёт менеджер с пустым индексом регистраций в памяти | Новый WebhookManager | Не бросает | Регистрации индексируются по арендатору |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Добавляет регистрацию в индекс вызывающего арендатора | void | InvalidArgumentException, когда арендатор регистрации не совпадает с арендатором контекста | Межарендаторная регистрация отклоняется до сохранения |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Заменяет совпадающую регистрацию деактивированной копией | bool | Не бросает; возвращает false, когда id не найден | Мягкая деактивация; история сохраняется |
WebhookManager::activeRegistrations | TenantContext $tenant | Отфильтровывает регистрации арендатора до активных | list<WebhookRegistration> | Не бросает | Видны только регистрации вызывающего арендатора |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Доставляет событие каждой активной регистрации, подписанной на этот тип события | int (успешные доставки) | Пробрасывает JsonException, когда данные события не кодируются в JSON; сбои доставки не бросают | Для каждой доставки регистрации генерируется новый 32-символьный шестнадцатеричный id доставки |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Сохраняет переданные значения дословно | Новый WebhookRegistration | Объявленного @throws нет; PHP выбрасывает TypeError при несовпадении типов аргументов под strict_types | final readonly; пустой $events означает подписку на всё |
WebhookRegistration::subscribesTo | JobEventType $eventType | true, когда $events пуст или содержит этот тип | bool | Не бросает | Строгое сравнение по идентичности |
WebhookRegistration::deactivate | — | Возвращает неактивную копию | self | Не бросает | Исходный экземпляр не изменяется |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Копирует id задачи, тип события, данные и временную метку из события | self | Не бросает | Статическая фабрика, используемая dispatch |
WebhookPayload::toJson | — | Сериализует тело из шести полей с неэкранированными слешами | non-empty-string | JsonException, когда данные события не кодируются в JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Возвращает тело как ассоциативный массив | array<string, mixed> | Не бросает | Временная метка в расширенном формате RFC 3339 |
WebhookPayload::signedTimestamp | — | Время события в секундах Unix, ограниченное снизу нулём | int<0, max> | Не бросает | Выдаётся как X-NextPDF-Timestamp и привязывается к MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 по базовой строке {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException через toJson(), когда тело не кодируется | Криптографически привязывает заголовок временной метки к телу |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | Движок доставки PSR-18/PSR-17 с пустой очередью недоставленных сообщений | Новый WebhookDelivery | Не бросает | Политика по умолчанию: 5 попыток, база 1 с, потолок 300 с |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | Отправляет POST-запросом подписанную полезную нагрузку с проверкой исходящего трафика против SSRF на каждой попытке и экспоненциальной отсрочкой | bool | JsonException до первой попытки, когда тело не кодируется; иначе не бросает — false означает, что полезная нагрузка направлена в очередь недоставленных сообщений | true только при ответе 2xx |
WebhookDelivery::deadLetters | — | Возвращает все записанные записи | list<DeadLetterEntry> | Не бросает | В памяти, в рамках процесса |
WebhookDelivery::clearDeadLetters | — | Очищает очередь недоставленных сообщений | void | Не бросает | Необратимо; сначала экспортируйте записи, если требуется повтор |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Сохраняет значения политики | Новый WebhookRetryPolicy | Объявленного @throws нет; параметры документированы как positive-int | $maxRetries считает общее число попыток |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), с ограничением maxDelaySeconds | positive-int | Не бросает | Номера попыток начинаются с 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true, пока текущая попытка ниже максимума | bool | Не бросает | Ожидание пропускается после последней попытки |
WebhookRetryPolicy::default | — | 5 попыток, база 1 с, потолок 300 с | self | Не бросает | Статическая фабрика; значение по умолчанию для продакшена |
WebhookRetryPolicy::aggressive | — | 10 попыток, база 2 с, потолок 600 с | self | Не бросает | Статическая фабрика для критичных конечных точек |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Сохраняет запись о сбое дословно | Новый DeadLetterEntry | Объявленного @throws нет; TypeError под strict_types | final readonly; null в $lastHttpStatus означает сбой транспорта |
DeadLetterEntry::markReplayed | — | Возвращает копию с replayed = true | self | Не бросает | Тот же id; исходная запись не изменяется |
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(): selfКонтракт поведения
Заголовок раздела «Контракт поведения»- Регистрации индексируются по арендатору.
register()отклоняет регистрацию, чей идентификатор арендатора не совпадает с вызывающим контекстом.unregister()— это мягкая деактивация: регистрация заменяется неактивной копией, что сохраняет историю, но исключает её из будущей диспетчеризации. dispatch()перебирает только активные регистрации вызывающего арендатора, подписанные на отправляемый тип события. Пустой список подписанных событий означает подписку на всё. Возвращаемое значение считает успешные доставки.- Каждая доставка — это HTTP POST с телом JSON и пятью заголовками:
Content-Type: application/json,X-NextPDF-Signature(sha256=<hex>),X-NextPDF-Timestamp(секунды Unix),X-NextPDF-Delivery-IdиX-NextPDF-Event. - Поля тела JSON:
delivery_id,job_id,event_type,data,timestamp(расширенный RFC 3339) иtenant_id, сериализованные с неэкранированными слешами. Значения типа события берутся изJobEventTypeвnextpdf/core:progress,completed,failed,cancelled. - Схема подписи (изменена в 3.1.0, ломающее изменение). Базовая строка HMAC-SHA256 — это
{signedTimestamp}.{jsonBody}с ключом — секретом регистрации, а не одно тело. ЗначениеX-NextPDF-Timestamp— это компонент временной метки MAC, поэтому подделанный или воспроизведённый заголовок временной метки делает подпись недействительной. - Проверка на стороне получателя: прочитайте заголовок
X-NextPDF-TimestampT; отклоните, когдаTвыходит за пределы допустимого окна свежести (например, 300 с); пересчитайтеhash_hmac('sha256', T . '.' . rawBody, secret)по сырым полученным байтам; сравните за постоянное время со значением заголовка после удаления префиксаsha256=. - Тело, подпись и идентификатор доставки вычисляются один раз на доставку и остаются постоянными на всех попытках повтора.
- Шлюз исходящего трафика против SSRF. Перед каждой попыткой URL назначения проходит через шлюз Core
UrlValidator::validateExternalUrl(): только схема HTTPS; диапазоны loopback, приватные, зарезервированные, carrier-grade-NAT, облачных метаданных и переходные диапазоны IPv6 со встроенным IPv4 блокируются; имена хостов разрешаются через DNS (A и AAAA), а неразрешимые хосты отклоняются fail-closed. Заблокированный URL никогда не отправляется: цикл попыток прерывается, и полезная нагрузка направляется прямо в очередь недоставленных сообщений с последней ошибкойBlocked SSRF destination:и нулевым статусом HTTP. - Классификация результата каждой попытки: 2xx — успех и немедленный возврат; 4xx, кроме 429, — окончательный и сразу попадает в недоставленные сообщения; любой другой результат — 3xx, 429, 5xx или исключение транспорта — повторяемый вплоть до общего числа попыток политики.
- Отсрочка экспоненциальная: ожидание перед следующей попыткой равно
baseDelaySeconds × 2^(attempt − 1), с ограничениемmaxDelaySeconds. Ожидание пропускается после последней попытки. - Когда ни одна попытка не удаётся,
DeadLetterEntryзаписывает уникальный id, id регистрации, исходную полезную нагрузку, число попыток (ограниченное максимумом политики), последнее сообщение об ошибке, последний статус HTTP (null при сбое транспорта или блокировке SSRF) и временную метку сбоя. - Очередь недоставленных сообщений хранится в памяти и ограничена временем жизни процесса.
markReplayed()создаёт помеченную копию; он не пересылает повторно, и очередь сохраняет исходную запись.
Граничные случаи и режимы сбоя
Заголовок раздела «Граничные случаи и режимы сбоя»- Пустой список событий. Регистрация получает каждый тип события. Ограничьте список явно, если получатель не должен видеть все события.
- Окончательный 4xx против сбоя транспорта. Отклонение 4xx записывает заполненный
lastHttpStatus; сбой соединения записывает null. Используйте null, чтобы отличить отклонение получателем от сбоя транспорта. - Адрес назначения, заблокированный SSRF. Регистрация, указывающая на адрес HTTP, приватный, loopback или метаданных, попадает в недоставленные сообщения на первой попытке с ошибкой
Blocked SSRF destination:и нулевым статусом. Исходящий запрос не выполняется. Исправьте URL и зарегистрируйте заново. - Устаревшие получатели после обновления. Получатель, всё ещё проверяющий HMAC только по телу до версии 3.1.0, завершается сбоем fail-closed против доставок 3.1.0. Переведите получателя на базовую строку
{timestamp}.{body}и обрабатывайтеX-NextPDF-Timestamp. - Некодируемые данные события.
toJson()иsign()бросаютJsonException, который пробрасывается изdeliver()иdispatch()до выполнения любой попытки. - Синхронная блокировка.
deliver()засыпает встроенно между попытками. Суммарная отсрочка достигает 15 с при политике по умолчанию и около 17 минут при агрессивной политике. Диспетчеризуйте из обработчика очереди, когда задержка получателя не заслуживает доверия. - Ограничение числа попыток. Записанное число попыток никогда не превышает максимум политики, хотя внутренний счётчик цикла продвигается за него при исчерпании.
- Рост очереди и устойчивость. Очередь недоставленных сообщений растёт неограниченно в рамках процесса и исчезает при перезапуске. Экспортируйте записи через
deadLetters()и сохраняйте их внешне перед вызовомclearDeadLetters(), когда требуется устойчивый повтор. - Повтор управляется оператором. Повторная доставка означает повторный вызов
deliver()с полезной нагрузкой записи;markReplayed()лишь фиксирует факт на копии. - Остаточный риск DNS-rebinding. URL повторно проверяется на каждой попытке, что сужает, но не закрывает окно rebinding: абстракция PSR-18 не может закрепить соединение за проверенным IP. Добавьте средства контроля исходящего трафика на сетевом уровне там, где этот остаточный риск важен.
- Обращение с секретом. Секрет регистрации — это учётные данные. HMAC аутентифицирует только целостность и происхождение — это не конфиденциальность. Не помещайте в полезную нагрузку события данные, которые получатель не должен видеть.
Поведение в режиме FIPS
Заголовок раздела «Поведение в режиме FIPS»Подписание полезной нагрузки — это HMAC-SHA256 через hash_hmac() в PHP, поэтому оно опирается на криптопровайдер хоста. В сборке с ограничениями FIPS неодобренный примитив завершается сбоем на криптографической границе, а не понижает уровень. Слой webhook не добавляет собственной криптографической политики.
Соответствие
Заголовок раздела «Соответствие»- Аутентификация полезной нагрузки реализует HMAC — код аутентификации сообщений с ключом-хешем из FIPS PUB 198-1 §1, инстанцированный с SHA-256.
- Защита от повторов следует рекомендациям OWASP Cheat Sheet Series по безопасности webhook: временная метка события передаётся в отдельном заголовке и включается в вычисление подписи, поэтому подделанная временная метка не проходит проверку.
- Временные метки тела используют расширенный формат даты-времени RFC 3339. Объявлено в коде: RFC 3339 не извлекался из корпуса RAG для этой страницы.
- Это утверждения о возможностях, основанные на исходном коде продукта и процитированных пунктах. NextPDF не делает заявлений о соответствии или сертификации для этой поверхности.
Заметки по разработке
Заголовок раздела «Заметки по разработке»- Все классы объявляют
strict_types=1и являютсяfinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicyиDeadLetterEntry— этоfinal readonlyс продвинутыми публичными свойствами. - Модуль несёт аннотацию
@sinceсо значением2.2.0; схема подписи с привязкой к временной метке — это документированное ломающее изменение в 3.1.0. - Движок доставки принимает абстракции PSR-18/PSR-17, поэтому мок HTTP-клиента проходит весь путь отправки, повтора и недоставленных сообщений офлайн. Логгер по умолчанию null; внедрите логгер PSR-3 в продакшене, иначе сбои проявляются только через возвращаемые значения.
- Реализации получателей должны использовать
hash_equals()для сравнения подписи и применять окно свежести кX-NextPDF-Timestamp. - Рекомендуемые граничные тесты: регистрация с несовпадающим арендатором, веерная рассылка при пустом списке событий, окончательный 4xx, исчерпание повторов, URL, заблокированный SSRF, отклонение подписи с подделанной временной меткой против фиксированного вектора и ограничение числа попыток в недоставленных сообщениях.
Граница публикации
Заголовок раздела «Граница публикации»Эта страница документирует только внешне наблюдаемое поведение и поддерживаемую публичную поверхность API. Внутренние пути пространств имён, вспомогательные классы, таблицы механизмов, имена файлов runbook и префиксы тикетов выходят за рамки.
См. также
Заголовок раздела «См. также»- Webhook — NextPDF Enterprise — страница возможности: рабочий процесс, конфигурация и проработанные примеры регистрации.
- SaaS — глубокий справочник — идентичность арендатора, API-ключи и квоты; источник
TenantContext. - Metering — углублённый справочник — веерная рассылка учёта использования с той же дисциплиной доставки PSR-18.