Lewati ke konten
getnextpdf.com

Enterprise edisi

Webhook — Referensi Mendalam

Namespace NextPDF\Enterprise\Webhook menyediakan pengiriman webhook bercakupan tenant untuk event job. Permukaan publiknya terdiri dari enam simbol: WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy, dan DeadLetterEntry. Manager meregistrasi endpoint per tenant dan mendispatch event job ke registrasi yang berlangganan. Engine pengiriman melakukan POST payload JSON yang ditandatangani HMAC-SHA256, memvalidasi setiap destinasi terhadap gate egress SSRF Core, melakukan retry dengan exponential backoff, dan mencatat kegagalan permanen di dead-letter queue in-memory. Sejak 3.1.0 tanda tangan mengikat header X-NextPDF-Timestamp ke dalam MAC base string, sehingga penerima memverifikasi kesegaran dan integritas secara bersamaan. Untuk panduan tingkat alur kerja, lihat Webhook.

Kapabilitas ini disertakan dalam NextPDF Enterprise (nextpdf/enterprise) dan diaktifkan dengan envelope lisensi tier Enterprise. Deployment tanpa entitlement tersebut tidak memuat kelas-kelas kapabilitas ini. Bandingkan edisi dan dapatkan lisensi.

Permukaan webhook adalah kapabilitas dasar Enterprise, tersedia begitu paket Enterprise terpasang; tidak ada flag per-fitur terpisah. NextPDF Core (Apache-2.0) dan NextPDF Pro tidak memiliki permukaan registrasi atau pengiriman webhook; manager, registration, payload, engine pengiriman, retry policy, dan dead-letter entry hanya disertakan di nextpdf/enterprise.

SimbolParameterPerilaku defaultMengembalikanMelempar atau gagal denganCatatan
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullMembuat manager dengan indeks registrasi in-memory yang kosongWebhookManager baruTidak melemparRegistrasi diindeks per tenant
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationMenambahkan registrasi ke indeks tenant pemanggilvoidInvalidArgumentException ketika tenant registrasi tidak cocok dengan tenant konteksRegistrasi lintas-tenant ditolak sebelum penyimpanan
WebhookManager::unregisterTenantContext $tenant, string $registrationIdMengganti registrasi yang cocok dengan salinan yang dinonaktifkanboolTidak melempar; mengembalikan false ketika id tidak ditemukanDeaktivasi lunak; riwayat dipertahankan
WebhookManager::activeRegistrationsTenantContext $tenantMenyaring registrasi tenant menjadi yang aktif sajalist<WebhookRegistration>Tidak melemparHanya registrasi milik tenant pemanggil yang terlihat
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventMengirim event ke setiap registrasi aktif yang berlangganan tipe event tersebutint (pengiriman berhasil)Meneruskan JsonException ketika data event tidak dapat di-encode ke JSON; kegagalan pengiriman tidak melemparSebuah delivery id 32-hex baru dihasilkan per pengiriman registrasi
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullMenyimpan nilai yang disuplai apa adanyaWebhookRegistration baruTidak ada @throws yang dideklarasikan; PHP memunculkan TypeError pada tipe argumen yang tidak cocok di bawah strict_typesfinal readonly; $events kosong berarti berlangganan semua
WebhookRegistration::subscribesToJobEventType $eventTypetrue ketika $events kosong atau memuat tipe tersebutboolTidak melemparPerbandingan identitas ketat
WebhookRegistration::deactivateMengembalikan salinan yang tidak aktifselfTidak melemparInstans asli tidak berubah
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdMenyalin job id, tipe event, data, dan timestamp dari eventselfTidak melemparFactory statis yang digunakan oleh dispatch
WebhookPayload::toJsonMenserialisasi body enam-field dengan slash tanpa escapenon-empty-stringJsonException ketika data event tidak dapat di-encode ke JSONJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayMengembalikan body sebagai array asosiatifarray<string, mixed>Tidak melemparTimestamp diformat sebagai RFC 3339 extended
WebhookPayload::signedTimestampWaktu event dalam detik-Unix, dijepit ke nol atau lebih besarint<0, max>Tidak melemparDipancarkan sebagai X-NextPDF-Timestamp dan diikat ke dalam MAC
WebhookPayload::signstring $secretHMAC-SHA256 atas base string {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException via toJson() ketika body tidak dapat di-encodeMengikat header timestamp ke body secara kriptografis
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nullEngine pengiriman PSR-18/PSR-17 dengan dead-letter queue kosongWebhookDelivery baruTidak melemparPolicy default: 5 percobaan, base 1 s, cap 300 s
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadPOST payload yang ditandatangani dengan validasi egress SSRF per-percobaan dan exponential backoffboolJsonException sebelum percobaan pertama ketika body tidak dapat di-encode; selain itu tidak melempar — false berarti payload dirutekan ke dead-letter queuetrue hanya pada respons 2xx
WebhookDelivery::deadLettersMengembalikan semua entri yang tercatatlist<DeadLetterEntry>Tidak melemparIn-memory, bercakupan proses
WebhookDelivery::clearDeadLettersMengosongkan dead-letter queuevoidTidak melemparTidak dapat dibatalkan; ekspor entri lebih dulu jika replay dibutuhkan
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300Menyimpan nilai policyWebhookRetryPolicy baruTidak ada @throws yang dideklarasikan; parameter didokumentasikan positive-int$maxRetries menghitung total percobaan
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), dijepit pada maxDelaySecondspositive-intTidak melemparNomor percobaan berbasis 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue selama percobaan saat ini di bawah maksimumboolTidak melemparJeda dilewati setelah percobaan terakhir
WebhookRetryPolicy::default5 percobaan, base 1 s, cap 300 sselfTidak melemparFactory statis; default produksi
WebhookRetryPolicy::aggressive10 percobaan, base 2 s, cap 600 sselfTidak melemparFactory statis untuk endpoint kritis
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseMenyimpan catatan kegagalan apa adanyaDeadLetterEntry baruTidak ada @throws yang dideklarasikan; TypeError di bawah strict_typesfinal readonly; $lastHttpStatus null berarti kegagalan transport
DeadLetterEntry::markReplayedMengembalikan salinan dengan replayed = trueselfTidak melemparId sama; entri asli tidak berubah
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
  • Registrasi diindeks per tenant. register() menolak registrasi yang identifier tenant-nya tidak cocok dengan konteks pemanggil. unregister() adalah deaktivasi lunak: registrasi diganti dengan salinan yang tidak aktif, mempertahankan riwayat sekaligus mengecualikannya dari dispatch mendatang.
  • dispatch() melakukan iterasi hanya atas registrasi aktif milik tenant pemanggil yang berlangganan tipe event yang didispatch. Daftar event berlangganan yang kosong berarti berlangganan semua. Nilai kembalian menghitung pengiriman yang berhasil.
  • Setiap pengiriman adalah HTTP POST dengan body JSON dan lima header: Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (detik unix), X-NextPDF-Delivery-Id, dan X-NextPDF-Event.
  • Field-field body JSON adalah delivery_id, job_id, event_type, data, timestamp (RFC 3339 extended), dan tenant_id, diserialisasi dengan slash tanpa escape. Nilai tipe event berasal dari JobEventType di nextpdf/core: progress, completed, failed, cancelled.
  • Skema tanda tangan (berubah di 3.1.0, breaking). Base string HMAC-SHA256 adalah {signedTimestamp}.{jsonBody}, dikunci oleh secret registrasi — bukan body saja. Nilai X-NextPDF-Timestamp adalah komponen timestamp dari MAC, sehingga header timestamp yang dirusak atau di-replay membatalkan tanda tangan.
  • Verifikasi penerima: baca header X-NextPDF-Timestamp T; tolak ketika T berada di luar jendela kesegaran yang dapat diterima (misalnya 300 s); hitung ulang hash_hmac('sha256', T . '.' . rawBody, secret) atas byte mentah yang diterima; bandingkan dalam waktu konstan terhadap nilai header setelah menghapus prefiks sha256=.
  • Body, tanda tangan, dan delivery id dihitung sekali per pengiriman dan tetap konstan di seluruh percobaan retry.
  • Gate egress SSRF. Sebelum setiap percobaan, URL destinasi melewati gate UrlValidator::validateExternalUrl() Core: hanya skema HTTPS; rentang loopback, private, reserved, carrier-grade-NAT, cloud-metadata, dan transisi IPv6 yang menyematkan IPv4 diblokir; hostname di-resolve DNS (A dan AAAA) dan host yang tidak dapat di-resolve ditolak secara fail-closed. URL yang diblokir tidak pernah dikirim: loop percobaan dibatalkan dan payload langsung dirutekan ke dead-letter queue dengan last error Blocked SSRF destination: dan status HTTP null.
  • Klasifikasi hasil per percobaan: 2xx adalah sukses dan langsung kembali; 4xx selain 429 bersifat terminal dan langsung menuju dead-letter; setiap hasil lain — 3xx, 429, 5xx, atau eksepsi transport — dapat di-retry hingga total jumlah percobaan policy.
  • Backoff bersifat eksponensial: jeda sebelum percobaan berikutnya adalah baseDelaySeconds × 2^(attempt − 1), dijepit pada maxDelaySeconds. Jeda dilewati setelah percobaan terakhir.
  • Ketika tidak ada percobaan yang berhasil, sebuah DeadLetterEntry mencatat id unik, id registrasi, payload asli, jumlah percobaan (dijepit ke maksimum policy), pesan last error, status HTTP terakhir (null pada kegagalan transport atau blokir SSRF), dan timestamp kegagalan.
  • Dead-letter queue bersifat in-memory dan bercakupan masa hidup proses. markReplayed() menghasilkan salinan yang ditandai; ia tidak mengirim ulang, dan queue mempertahankan entri asli.
  • Daftar event kosong. Registrasi menerima setiap tipe event. Batasi daftar secara eksplisit ketika penerima tidak boleh melihat semua event.
  • 4xx terminal versus kegagalan transport. Penolakan 4xx mencatat lastHttpStatus yang terisi; kegagalan koneksi mencatat null. Gunakan null untuk membedakan penolakan penerima dari kegagalan transport.
  • Destinasi terblokir SSRF. Registrasi yang mengarah ke alamat HTTP, private, loopback, atau metadata masuk dead-letter pada percobaan pertama dengan error Blocked SSRF destination: dan status null. Tidak ada request keluar yang dilakukan. Perbaiki URL dan registrasi ulang.
  • Penerima lawas setelah upgrade. Penerima yang masih memverifikasi HMAC body-only pra-3.1.0 gagal secara fail-closed terhadap pengiriman 3.1.0. Migrasikan penerima ke base string {timestamp}.{body} dan konsumsi X-NextPDF-Timestamp.
  • Data event yang tidak dapat di-encode. toJson() dan sign() melempar JsonException, yang merambat keluar dari deliver() dan dispatch() sebelum ada percobaan dilakukan.
  • Pemblokiran sinkron. deliver() melakukan sleep secara inline di antara percobaan. Backoff kumulatif mencapai 15 s di bawah policy default dan sekitar 17 menit di bawah policy aggressive. Dispatch dari queue worker ketika latensi penerima tidak terpercaya.
  • Penjepitan jumlah percobaan. Jumlah percobaan yang tercatat tidak pernah melebihi maksimum policy, meskipun penghitung loop internal bergerak melampauinya saat kehabisan.
  • Pertumbuhan dan durabilitas queue. Dead-letter queue tumbuh tanpa batas di dalam proses dan lenyap saat restart. Ekspor entri via deadLetters() dan persistkan secara eksternal sebelum memanggil clearDeadLetters() ketika replay yang durabel dibutuhkan.
  • Replay digerakkan operator. Pengiriman ulang berarti memanggil deliver() lagi dengan payload entri; markReplayed() hanya mencatat fakta tersebut pada sebuah salinan.
  • Sisa DNS-rebinding. URL divalidasi ulang pada setiap percobaan, yang mempersempit tetapi tidak menutup jendela rebinding: abstraksi PSR-18 tidak dapat menyematkan koneksi ke IP yang tervalidasi. Tambahkan kontrol egress lapisan jaringan di mana sisa ini penting.
  • Penanganan secret. Secret registrasi adalah kredensial. HMAC mengautentikasi integritas dan asal saja — bukan kerahasiaan. Jangan menempatkan data dalam payload event yang tidak boleh dilihat penerima.

Penandatanganan payload adalah HMAC-SHA256 melalui hash_hmac() PHP, sehingga bergantung pada penyedia kripto host. Pada build yang dibatasi FIPS, primitif yang tidak disetujui gagal di batas kriptografis alih-alih diturunkan. Lapisan webhook tidak menambahkan policy kriptografis apa pun sendiri.

  • Autentikasi payload mengimplementasikan HMAC, keyed-hash message authentication code dari FIPS PUB 198-1 §1, diinstansiasi dengan SHA-256.
  • Proteksi replay mengikuti panduan keamanan webhook OWASP Cheat Sheet Series: timestamp event dibawa dalam header khusus dan disemai ke dalam komputasi tanda tangan, sehingga timestamp yang dirusak gagal verifikasi.
  • Timestamp body menggunakan format tanggal-waktu RFC 3339 extended. Dideklarasikan-kode: RFC 3339 tidak diambil dari korpus RAG untuk halaman ini.
  • Ini adalah pernyataan kapabilitas yang berlandaskan pada source produk dan klausul yang dikutip. NextPDF tidak membuat klaim konformansi atau sertifikasi apa pun untuk permukaan ini.
  • Semua kelas mendeklarasikan strict_types=1 dan bersifat final; WebhookRegistration, WebhookPayload, WebhookRetryPolicy, dan DeadLetterEntry bersifat final readonly dengan properti publik yang dipromosikan.
  • Modul membawa anotasi @since bernilai 2.2.0; skema tanda tangan yang terikat timestamp adalah perubahan breaking yang terdokumentasi di 3.1.0.
  • Engine pengiriman menerima abstraksi PSR-18/PSR-17, sehingga mock HTTP client menjalankan jalur kirim, retry, dan dead-letter secara penuh secara offline. Logger default null; injeksikan logger PSR-3 di produksi atau kegagalan hanya muncul melalui nilai kembalian.
  • Implementasi penerima sebaiknya menggunakan hash_equals() untuk perbandingan tanda tangan dan menegakkan jendela kesegaran pada X-NextPDF-Timestamp.
  • Pengujian batas yang direkomendasikan: registrasi tenant-mismatch, fan-out daftar-event-kosong, 4xx terminal, kehabisan retry, URL terblokir SSRF, penolakan tanda tangan timestamp-tampered terhadap vektor tetap, dan penjepitan jumlah-percobaan dead-letter.

Halaman ini hanya mendokumentasikan perilaku yang teramati secara eksternal dan permukaan API publik yang didukung. Jalur namespace internal, kelas helper, tabel mekanisme, nama file runbook, dan prefiks tiket berada di luar cakupan.