Enterprise edisi
Webhook — Referensi Mendalam
Sekilas pandang
Bagian berjudul “Sekilas pandang”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.
Ketersediaan & lisensi
Bagian berjudul “Ketersediaan & lisensi”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.
Permukaan API publik
Bagian berjudul “Permukaan API publik”| Simbol | Parameter | Perilaku default | Mengembalikan | Melempar atau gagal dengan | Catatan |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | Membuat manager dengan indeks registrasi in-memory yang kosong | WebhookManager baru | Tidak melempar | Registrasi diindeks per tenant |
WebhookManager::register | TenantContext $tenant, WebhookRegistration $registration | Menambahkan registrasi ke indeks tenant pemanggil | void | InvalidArgumentException ketika tenant registrasi tidak cocok dengan tenant konteks | Registrasi lintas-tenant ditolak sebelum penyimpanan |
WebhookManager::unregister | TenantContext $tenant, string $registrationId | Mengganti registrasi yang cocok dengan salinan yang dinonaktifkan | bool | Tidak melempar; mengembalikan false ketika id tidak ditemukan | Deaktivasi lunak; riwayat dipertahankan |
WebhookManager::activeRegistrations | TenantContext $tenant | Menyaring registrasi tenant menjadi yang aktif saja | list<WebhookRegistration> | Tidak melempar | Hanya registrasi milik tenant pemanggil yang terlihat |
WebhookManager::dispatch | TenantContext $tenant, JobEvent $event | Mengirim event ke setiap registrasi aktif yang berlangganan tipe event tersebut | int (pengiriman berhasil) | Meneruskan JsonException ketika data event tidak dapat di-encode ke JSON; kegagalan pengiriman tidak melempar | Sebuah delivery id 32-hex baru dihasilkan per pengiriman registrasi |
WebhookRegistration::__construct | string $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = null | Menyimpan nilai yang disuplai apa adanya | WebhookRegistration baru | Tidak ada @throws yang dideklarasikan; PHP memunculkan TypeError pada tipe argumen yang tidak cocok di bawah strict_types | final readonly; $events kosong berarti berlangganan semua |
WebhookRegistration::subscribesTo | JobEventType $eventType | true ketika $events kosong atau memuat tipe tersebut | bool | Tidak melempar | Perbandingan identitas ketat |
WebhookRegistration::deactivate | — | Mengembalikan salinan yang tidak aktif | self | Tidak melempar | Instans asli tidak berubah |
WebhookPayload::fromJobEvent | JobEvent $event, string $tenantId, string $deliveryId | Menyalin job id, tipe event, data, dan timestamp dari event | self | Tidak melempar | Factory statis yang digunakan oleh dispatch |
WebhookPayload::toJson | — | Menserialisasi body enam-field dengan slash tanpa escape | non-empty-string | JsonException ketika data event tidak dapat di-encode ke JSON | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | Mengembalikan body sebagai array asosiatif | array<string, mixed> | Tidak melempar | Timestamp diformat sebagai RFC 3339 extended |
WebhookPayload::signedTimestamp | — | Waktu event dalam detik-Unix, dijepit ke nol atau lebih besar | int<0, max> | Tidak melempar | Dipancarkan sebagai X-NextPDF-Timestamp dan diikat ke dalam MAC |
WebhookPayload::sign | string $secret | HMAC-SHA256 atas base string {signedTimestamp}.{jsonBody} | non-empty-string (hex) | JsonException via toJson() ketika body tidak dapat di-encode | Mengikat header timestamp ke body secara kriptografis |
WebhookDelivery::__construct | ClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null | Engine pengiriman PSR-18/PSR-17 dengan dead-letter queue kosong | WebhookDelivery baru | Tidak melempar | Policy default: 5 percobaan, base 1 s, cap 300 s |
WebhookDelivery::deliver | WebhookRegistration $registration, WebhookPayload $payload | POST payload yang ditandatangani dengan validasi egress SSRF per-percobaan dan exponential backoff | bool | JsonException sebelum percobaan pertama ketika body tidak dapat di-encode; selain itu tidak melempar — false berarti payload dirutekan ke dead-letter queue | true hanya pada respons 2xx |
WebhookDelivery::deadLetters | — | Mengembalikan semua entri yang tercatat | list<DeadLetterEntry> | Tidak melempar | In-memory, bercakupan proses |
WebhookDelivery::clearDeadLetters | — | Mengosongkan dead-letter queue | void | Tidak melempar | Tidak dapat dibatalkan; ekspor entri lebih dulu jika replay dibutuhkan |
WebhookRetryPolicy::__construct | int $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300 | Menyimpan nilai policy | WebhookRetryPolicy baru | Tidak ada @throws yang dideklarasikan; parameter didokumentasikan positive-int | $maxRetries menghitung total percobaan |
WebhookRetryPolicy::delayForAttempt | int $attempt | baseDelaySeconds × 2^(attempt − 1), dijepit pada maxDelaySeconds | positive-int | Tidak melempar | Nomor percobaan berbasis 1 |
WebhookRetryPolicy::shouldRetry | int $currentAttempt | true selama percobaan saat ini di bawah maksimum | bool | Tidak melempar | Jeda dilewati setelah percobaan terakhir |
WebhookRetryPolicy::default | — | 5 percobaan, base 1 s, cap 300 s | self | Tidak melempar | Factory statis; default produksi |
WebhookRetryPolicy::aggressive | — | 10 percobaan, base 2 s, cap 600 s | self | Tidak melempar | Factory statis untuk endpoint kritis |
DeadLetterEntry::__construct | string $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false | Menyimpan catatan kegagalan apa adanya | DeadLetterEntry baru | Tidak ada @throws yang dideklarasikan; TypeError di bawah strict_types | final readonly; $lastHttpStatus null berarti kegagalan transport |
DeadLetterEntry::markReplayed | — | Mengembalikan salinan dengan replayed = true | self | Tidak melempar | Id 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): 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(): selfKontrak perilaku
Bagian berjudul “Kontrak perilaku”- 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, danX-NextPDF-Event. - Field-field body JSON adalah
delivery_id,job_id,event_type,data,timestamp(RFC 3339 extended), dantenant_id, diserialisasi dengan slash tanpa escape. Nilai tipe event berasal dariJobEventTypedinextpdf/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. NilaiX-NextPDF-Timestampadalah komponen timestamp dari MAC, sehingga header timestamp yang dirusak atau di-replay membatalkan tanda tangan. - Verifikasi penerima: baca header
X-NextPDF-TimestampT; tolak ketikaTberada di luar jendela kesegaran yang dapat diterima (misalnya 300 s); hitung ulanghash_hmac('sha256', T . '.' . rawBody, secret)atas byte mentah yang diterima; bandingkan dalam waktu konstan terhadap nilai header setelah menghapus prefikssha256=. - 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 errorBlocked 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 padamaxDelaySeconds. Jeda dilewati setelah percobaan terakhir. - Ketika tidak ada percobaan yang berhasil, sebuah
DeadLetterEntrymencatat 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.
Kasus tepi & mode kegagalan
Bagian berjudul “Kasus tepi & mode kegagalan”- 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
lastHttpStatusyang 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 konsumsiX-NextPDF-Timestamp. - Data event yang tidak dapat di-encode.
toJson()dansign()melemparJsonException, yang merambat keluar darideliver()dandispatch()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 memanggilclearDeadLetters()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.
Perilaku mode FIPS
Bagian berjudul “Perilaku mode FIPS”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.
Konformansi
Bagian berjudul “Konformansi”- 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.
Catatan pengembangan
Bagian berjudul “Catatan pengembangan”- Semua kelas mendeklarasikan
strict_types=1dan bersifatfinal;WebhookRegistration,WebhookPayload,WebhookRetryPolicy, danDeadLetterEntrybersifatfinal readonlydengan properti publik yang dipromosikan. - Modul membawa anotasi
@sincebernilai2.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 padaX-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.
Batas publikasi
Bagian berjudul “Batas publikasi”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.
Lihat juga
Bagian berjudul “Lihat juga”- Webhook — NextPDF Enterprise — halaman kapabilitas: alur kerja, konfigurasi, dan contoh registrasi terkerjakan.
- SaaS — Referensi Mendalam — identitas tenant, API key, dan kuota; sumber
TenantContext. - Metering — Referensi Mendalam — fan-out metering penggunaan dengan disiplin pengiriman PSR-18 yang sama.