ข้ามไปยังเนื้อหา
getnextpdf.com

Enterprise รุ่น

Webhook — เอกสารอ้างอิงเชิงลึก

เนมสเปซ NextPDF\Enterprise\Webhook มาพร้อมการส่ง webhook ที่จำกัดขอบเขตต่อ tenant สำหรับ job event พื้นผิวสาธารณะประกอบด้วยหกสัญลักษณ์ ได้แก่ WebhookManager, WebhookRegistration, WebhookPayload, WebhookDelivery, WebhookRetryPolicy และ DeadLetterEntry ตัวจัดการลงทะเบียน endpoint ต่อ tenant และ dispatch job event ไปยังการลงทะเบียนที่สมัครรับ delivery engine จะ POST JSON payload ที่ลงนามแบบ HMAC-SHA256, ตรวจสอบทุกปลายทางกับเกต egress แบบ SSRF ของ Core, ลองใหม่ด้วย exponential backoff และบันทึกความล้มเหลวถาวรไว้ในคิว dead-letter ในหน่วยความจำ ตั้งแต่ 3.1.0 ลายเซ็นจะผูก header X-NextPDF-Timestamp เข้ากับ base string ของ MAC ผู้รับจึงตรวจสอบความสดใหม่และความครบถ้วนพร้อมกัน สำหรับคู่มือระดับเวิร์กโฟลว์ ดู Webhook

ความสามารถนี้มาในแพ็กเกจ NextPDF Enterprise (nextpdf/enterprise) และเปิดใช้งานด้วย license envelope ระดับ Enterprise การปรับใช้ที่ไม่มีสิทธิ์ดังกล่าวจะไม่โหลดคลาสของความสามารถนี้ เปรียบเทียบรุ่นและขอรับสิทธิ์การใช้งาน

พื้นผิว webhook เป็นความสามารถพื้นฐานของ Enterprise ที่พร้อมใช้งานเมื่อติดตั้งแพ็กเกจ Enterprise แล้ว ไม่มีแฟล็กต่อฟีเจอร์แยกต่างหาก NextPDF Core (Apache-2.0) และ NextPDF Pro ไม่มีพื้นผิวการลงทะเบียนหรือการส่ง webhook ตัวจัดการ การลงทะเบียน payload, delivery engine, นโยบายการลองใหม่ และรายการ dead-letter มาในแพ็กเกจ nextpdf/enterprise เท่านั้น

สัญลักษณ์พารามิเตอร์พฤติกรรมเริ่มต้นคืนค่าโยนหรือล้มเหลวด้วยหมายเหตุ
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = nullสร้างตัวจัดการที่มีดัชนีการลงทะเบียนในหน่วยความจำที่ว่างเปล่าWebhookManager ใหม่ไม่โยนข้อยกเว้นการลงทะเบียนถูกจัดทำดัชนีต่อ tenant
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registrationเพิ่มการลงทะเบียนต่อท้ายดัชนีของ tenant ที่เรียกใช้voidInvalidArgumentException เมื่อ tenant ของการลงทะเบียนไม่ตรงกับ tenant ของ contextการลงทะเบียนข้าม tenant ถูกปฏิเสธก่อนการจัดเก็บ
WebhookManager::unregisterTenantContext $tenant, string $registrationIdแทนที่การลงทะเบียนที่ตรงกันด้วยสำเนาที่ปิดใช้งานboolไม่โยนข้อยกเว้น คืนค่า false เมื่อไม่พบ idปิดใช้งานแบบ soft รักษาประวัติไว้
WebhookManager::activeRegistrationsTenantContext $tenantกรองการลงทะเบียนของ tenant ให้เหลือเฉพาะที่ใช้งานอยู่list<WebhookRegistration>ไม่โยนข้อยกเว้นมองเห็นเฉพาะการลงทะเบียนของ tenant ที่เรียกใช้เท่านั้น
WebhookManager::dispatchTenantContext $tenant, JobEvent $eventส่ง event ไปยังทุกการลงทะเบียนที่ใช้งานอยู่ซึ่งสมัครรับ event type นั้นint (จำนวนการส่งที่สำเร็จ)ส่งต่อ JsonException เมื่อ event data ไม่สามารถเข้ารหัสเป็น JSON ได้ ความล้มเหลวของการส่งไม่โยนข้อยกเว้นสร้าง delivery id แบบ 32-hex ใหม่ต่อการส่งของแต่ละการลงทะเบียน
WebhookRegistration::__constructstring $id, string $tenantId, string $url, array $events, string $secret, bool $active = true, ?string $description = nullจัดเก็บค่าที่ให้มาตามที่เป็นWebhookRegistration ใหม่ไม่มี @throws ที่ประกาศไว้ PHP จะยก TypeError เมื่อชนิดของอาร์กิวเมนต์ไม่ตรงกันภายใต้ strict_typesfinal readonly $events ที่ว่างเปล่าหมายถึงสมัครรับทั้งหมด
WebhookRegistration::subscribesToJobEventType $eventTypetrue เมื่อ $events ว่างเปล่าหรือมี type นั้นอยู่boolไม่โยนข้อยกเว้นการเปรียบเทียบเอกลักษณ์แบบเข้มงวด
WebhookRegistration::deactivateคืนสำเนาที่ไม่ใช้งานselfไม่โยนข้อยกเว้นอินสแตนซ์เดิมไม่เปลี่ยนแปลง
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryIdคัดลอก job id, event type, data และ timestamp จาก eventselfไม่โยนข้อยกเว้นstatic factory ที่ dispatch ใช้
WebhookPayload::toJsonserialize body หกฟิลด์โดยไม่ escape slashnon-empty-stringJsonException เมื่อ event data ไม่สามารถเข้ารหัสเป็น JSON ได้JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArrayคืน body เป็น associative arrayarray<string, mixed>ไม่โยนข้อยกเว้นtimestamp จัดรูปแบบเป็น RFC 3339 extended
WebhookPayload::signedTimestampเวลาของ event เป็นหน่วยวินาที Unix จำกัดค่าให้เป็นศูนย์หรือมากกว่าint<0, max>ไม่โยนข้อยกเว้นปล่อยออกเป็น X-NextPDF-Timestamp และผูกเข้ากับ MAC
WebhookPayload::signstring $secretHMAC-SHA256 เหนือ base string {signedTimestamp}.{jsonBody}non-empty-string (hex)JsonException ผ่าน toJson() เมื่อ body ไม่สามารถเข้ารหัสได้ผูก header timestamp เข้ากับ body ด้วยวิธีการเข้ารหัส
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = nulldelivery engine แบบ PSR-18/PSR-17 ที่มีคิว dead-letter ว่างเปล่าWebhookDelivery ใหม่ไม่โยนข้อยกเว้นนโยบายเริ่มต้น: 5 ความพยายาม, base 1 s, จำกัด 300 s
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payloadPOST payload ที่ลงนามพร้อมการตรวจสอบ egress แบบ SSRF ต่อความพยายามและ exponential backoffboolJsonException ก่อนความพยายามครั้งแรกเมื่อ body ไม่สามารถเข้ารหัสได้ มิฉะนั้นจะไม่โยนข้อยกเว้น — false หมายถึง payload ถูกส่งไปยังคิว dead-lettertrue เฉพาะเมื่อตอบกลับด้วย 2xx เท่านั้น
WebhookDelivery::deadLettersคืนรายการที่บันทึกไว้ทั้งหมดlist<DeadLetterEntry>ไม่โยนข้อยกเว้นในหน่วยความจำ จำกัดขอบเขตต่อกระบวนการ
WebhookDelivery::clearDeadLettersล้างคิว dead-lettervoidไม่โยนข้อยกเว้นย้อนกลับไม่ได้ ส่งออกรายการก่อนหากต้องการการ replay
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300จัดเก็บค่าของนโยบายWebhookRetryPolicy ใหม่ไม่มี @throws ที่ประกาศไว้ พารามิเตอร์ระบุเป็น positive-int$maxRetries นับจำนวนความพยายามทั้งหมด
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1) จำกัดที่ maxDelaySecondspositive-intไม่โยนข้อยกเว้นหมายเลขความพยายามเริ่มนับจาก 1
WebhookRetryPolicy::shouldRetryint $currentAttempttrue ขณะที่ความพยายามปัจจุบันยังต่ำกว่าค่าสูงสุดboolไม่โยนข้อยกเว้นการรอจะถูกข้ามหลังจากความพยายามครั้งสุดท้าย
WebhookRetryPolicy::default5 ความพยายาม, base 1 s, จำกัด 300 sselfไม่โยนข้อยกเว้นstatic factory ค่าเริ่มต้นสำหรับการใช้งานจริง
WebhookRetryPolicy::aggressive10 ความพยายาม, base 2 s, จำกัด 600 sselfไม่โยนข้อยกเว้นstatic factory สำหรับ endpoint ที่สำคัญยิ่ง
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = falseจัดเก็บระเบียนความล้มเหลวตามที่เป็นDeadLetterEntry ใหม่ไม่มี @throws ที่ประกาศไว้ TypeError ภายใต้ strict_typesfinal readonly $lastHttpStatus ที่เป็น null หมายถึงความล้มเหลวของการขนส่ง
DeadLetterEntry::markReplayedคืนสำเนาที่มี replayed = trueselfไม่โยนข้อยกเว้น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): 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
  • การลงทะเบียนถูกจัดทำดัชนีต่อ tenant register() ปฏิเสธการลงทะเบียนที่ tenant identifier ไม่ตรงกับ context ที่เรียกใช้ unregister() เป็นการปิดใช้งานแบบ soft คือการลงทะเบียนถูกแทนที่ด้วยสำเนาที่ไม่ใช้งาน รักษาประวัติไว้ในขณะที่กันออกจากการ dispatch ในอนาคต
  • dispatch() วนซ้ำเฉพาะการลงทะเบียนที่ใช้งานอยู่ของ tenant ที่เรียกใช้ซึ่งสมัครรับ event type ที่ถูก dispatch รายการ subscribed-event ที่ว่างเปล่าหมายถึงสมัครรับทั้งหมด ค่าที่คืนนับจำนวนการส่งที่สำเร็จ
  • การส่งแต่ละครั้งเป็น HTTP POST ที่มี JSON body และ header ห้าตัว ได้แก่ Content-Type: application/json, X-NextPDF-Signature (sha256=<hex>), X-NextPDF-Timestamp (วินาที Unix), X-NextPDF-Delivery-Id และ X-NextPDF-Event
  • ฟิลด์ใน JSON body ได้แก่ delivery_id, job_id, event_type, data, timestamp (RFC 3339 extended) และ tenant_id โดย serialize แบบไม่ escape slash ค่า event-type มาจาก JobEventType ใน nextpdf/core ได้แก่ progress, completed, failed, cancelled
  • สคีมาลายเซ็น (เปลี่ยนใน 3.1.0 เป็น breaking) base string ของ HMAC-SHA256 คือ {signedTimestamp}.{jsonBody} โดยใช้คีย์เป็น registration secret ไม่ใช่ body เพียงอย่างเดียว ค่า X-NextPDF-Timestamp เป็นองค์ประกอบ timestamp ของ MAC ดังนั้น header timestamp ที่ถูกดัดแปลงหรือ replay จะทำให้ลายเซ็นใช้ไม่ได้
  • การตรวจสอบฝั่งผู้รับ: อ่าน header X-NextPDF-Timestamp เป็น T ปฏิเสธเมื่อ T อยู่นอกหน้าต่างความสดใหม่ที่ยอมรับได้ (เช่น 300 s) คำนวณ hash_hmac('sha256', T . '.' . rawBody, secret) ใหม่เหนือไบต์ดิบที่ได้รับ แล้วเปรียบเทียบในเวลาคงที่กับค่าใน header หลังจากตัดคำนำหน้า sha256= ออก
  • body, ลายเซ็น และ delivery id ถูกคำนวณเพียงครั้งเดียวต่อการส่งและคงที่ตลอดการลองใหม่
  • เกต egress แบบ SSRF ก่อนทุกความพยายาม URL ปลายทางต้องผ่านเกต UrlValidator::validateExternalUrl() ของ Core คือ อนุญาตเฉพาะสคีม HTTPS เท่านั้น บล็อกช่วง loopback, private, reserved, carrier-grade-NAT, cloud-metadata และช่วง transition ของ IPv6 ที่ฝัง IPv4 hostname จะถูก resolve ผ่าน DNS (A และ AAAA) และ host ที่ resolve ไม่ได้จะถูกปฏิเสธแบบ fail-closed URL ที่ถูกบล็อกจะไม่ถูกส่งเลย ลูปความพยายามจะยกเลิกและ payload จะถูกส่งตรงไปยังคิว dead-letter พร้อมข้อผิดพลาดล่าสุด Blocked SSRF destination: และสถานะ HTTP เป็น null
  • การจำแนกผลลัพธ์ต่อความพยายาม: 2xx คือสำเร็จและคืนค่าทันที 4xx ที่ไม่ใช่ 429 เป็น terminal และไปยัง dead-letter โดยตรง ผลลัพธ์อื่นทุกแบบ — 3xx, 429, 5xx หรือข้อยกเว้นด้านการขนส่ง — เป็น retryable ได้สูงสุดตามจำนวนความพยายามทั้งหมดของนโยบาย
  • backoff เป็นแบบ exponential คือเวลารอก่อนความพยายามถัดไปคือ baseDelaySeconds × 2^(attempt − 1) จำกัดที่ maxDelaySeconds การรอจะถูกข้ามหลังจากความพยายามครั้งสุดท้าย
  • เมื่อไม่มีความพยายามใดสำเร็จ DeadLetterEntry จะบันทึก id ที่ไม่ซ้ำ, registration id, payload เดิม, จำนวนความพยายาม (จำกัดที่ค่าสูงสุดของนโยบาย), ข้อความข้อผิดพลาดล่าสุด, สถานะ HTTP ล่าสุด (null เมื่อความล้มเหลวของการขนส่งหรือถูกบล็อกแบบ SSRF) และ timestamp ความล้มเหลว
  • คิว dead-letter อยู่ในหน่วยความจำและจำกัดขอบเขตต่ออายุของกระบวนการ markReplayed() สร้างสำเนาที่ติดเครื่องหมาย ไม่ได้ส่งซ้ำ และคิวยังคงเก็บรายการเดิมไว้
  • รายการ event ว่างเปล่า การลงทะเบียนจะรับ event type ทุกแบบ กำหนดขอบเขตรายการอย่างชัดเจนเมื่อผู้รับไม่ควรเห็น event ทั้งหมด
  • 4xx แบบ terminal เทียบกับความล้มเหลวของการขนส่ง การปฏิเสธด้วย 4xx บันทึก lastHttpStatus ที่มีค่า ส่วนความล้มเหลวของการเชื่อมต่อบันทึกเป็น null ใช้ค่า null เพื่อแยกการปฏิเสธของผู้รับออกจากความล้มเหลวของการขนส่ง
  • ปลายทางที่ถูกบล็อกแบบ SSRF การลงทะเบียนที่ชี้ไปยังที่อยู่ HTTP, private, loopback หรือ metadata จะเข้าสู่ dead-letter ในความพยายามครั้งแรกพร้อมข้อผิดพลาด Blocked SSRF destination: และสถานะ null ไม่มีการส่งคำขอออกไป แก้ไข URL แล้วลงทะเบียนใหม่
  • ผู้รับรุ่นเก่าหลังการอัปเกรด ผู้รับที่ยังตรวจสอบ HMAC แบบ body-only ก่อน 3.1.0 จะล้มเหลวแบบ fail closed กับการส่งของ 3.1.0 ย้ายผู้รับไปใช้ base string {timestamp}.{body} และรับค่า X-NextPDF-Timestamp
  • event data ที่เข้ารหัสไม่ได้ toJson() และ sign() โยน JsonException ซึ่งส่งต่อออกจาก deliver() และ dispatch() ก่อนที่จะมีความพยายามใด ๆ
  • การบล็อกแบบซิงโครนัส deliver() หน่วงเวลาแบบ inline ระหว่างความพยายาม backoff สะสมถึง 15 s ภายใต้นโยบาย default และประมาณ 17 นาทีภายใต้นโยบาย aggressive ให้ dispatch จาก queue worker เมื่อ latency ของผู้รับไม่น่าเชื่อถือ
  • การจำกัดจำนวนความพยายาม จำนวนความพยายามที่บันทึกไม่เกินค่าสูงสุดของนโยบาย แม้ว่าตัวนับลูปภายในจะเลื่อนเกินไปเมื่อความพยายามหมดลง
  • การเติบโตและความคงทนของคิว คิว dead-letter เติบโตแบบไม่จำกัดภายในกระบวนการและหายไปเมื่อรีสตาร์ท ส่งออกรายการผ่าน deadLetters() และเก็บไว้ภายนอกก่อนเรียก clearDeadLetters() เมื่อต้องการการ replay แบบคงทน
  • การ replay ขับเคลื่อนโดยผู้ดำเนินการ การส่งซ้ำหมายถึงการเรียก deliver() อีกครั้งด้วย payload ของรายการนั้น markReplayed() เพียงบันทึกข้อเท็จจริงลงบนสำเนาเท่านั้น
  • ความเสี่ยงตกค้างจาก DNS-rebinding URL ถูกตรวจสอบซ้ำในทุกความพยายาม ซึ่งลดแต่ไม่ปิดหน้าต่างของการ rebinding คือ abstraction แบบ PSR-18 ไม่สามารถตรึงการเชื่อมต่อไว้กับ IP ที่ตรวจสอบแล้วได้ เพิ่มการควบคุม egress ในระดับเครือข่ายเมื่อความเสี่ยงตกค้างนี้มีนัยสำคัญ
  • การจัดการ secret registration secret เป็นข้อมูลรับรอง HMAC ยืนยันความครบถ้วนและที่มาเท่านั้น — ไม่ใช่ความลับ อย่าใส่ข้อมูลที่ผู้รับไม่ควรเห็นลงใน event payload

การลงนาม payload เป็น HMAC-SHA256 ผ่าน hash_hmac() ของ PHP จึงพึ่งพา crypto provider ของ host ใน build ที่ถูกจำกัดด้วย FIPS ไพรมิทีฟที่ไม่ผ่านการอนุมัติจะล้มเหลวที่ขอบเขตทางการเข้ารหัสแทนที่จะลดระดับลง เลเยอร์ webhook ไม่ได้เพิ่มนโยบายทางการเข้ารหัสของตัวเอง

  • การตรวจสอบสิทธิ์ payload ใช้ HMAC ซึ่งเป็น keyed-hash message authentication code ของ FIPS PUB 198-1 §1 โดยสร้างอินสแตนซ์ด้วย SHA-256
  • การป้องกัน replay เป็นไปตามแนวทางความปลอดภัย webhook ของ OWASP Cheat Sheet Series คือ timestamp ของ event เดินทางใน header เฉพาะและถูกป้อนเข้าสู่การคำนวณลายเซ็น ดังนั้น timestamp ที่ถูกดัดแปลงจะตรวจสอบไม่ผ่าน
  • timestamp ใน body ใช้รูปแบบ RFC 3339 extended date-time ประกาศไว้ในโค้ด: RFC 3339 ไม่ได้ถูกดึงมาจากคลังข้อมูล RAG สำหรับหน้านี้
  • ข้อความเหล่านี้เป็นคำแถลงความสามารถที่อิงจากซอร์สของผลิตภัณฑ์และข้อกำหนดที่อ้างอิง NextPDF ไม่ได้อ้างความสอดคล้องหรือการรับรองใด ๆ สำหรับพื้นผิวนี้
  • ทุกคลาสประกาศ strict_types=1 และเป็น final WebhookRegistration, WebhookPayload, WebhookRetryPolicy และ DeadLetterEntry เป็น final readonly พร้อม promoted public property
  • โมดูลมี annotation @since เป็น 2.2.0 สคีมาลายเซ็นที่ผูกกับ timestamp เป็น breaking change ที่บันทึกไว้ใน 3.1.0
  • delivery engine รับ abstraction แบบ PSR-18/PSR-17 ดังนั้น HTTP client จำลองสามารถทดสอบเส้นทางการส่ง การลองใหม่ และ dead-letter ทั้งหมดแบบออฟไลน์ได้ logger มีค่าเริ่มต้นเป็น null ให้ inject logger แบบ PSR-3 ในการใช้งานจริง มิฉะนั้นความล้มเหลวจะปรากฏผ่านค่าที่คืนเท่านั้น
  • การนำ implementation ฝั่งผู้รับควรใช้ hash_equals() สำหรับการเปรียบเทียบลายเซ็นและบังคับใช้หน้าต่างความสดใหม่กับ X-NextPDF-Timestamp
  • การทดสอบขอบเขตที่แนะนำ: การลงทะเบียนที่ tenant ไม่ตรงกัน, การกระจายเมื่อรายการ event ว่างเปล่า, 4xx แบบ terminal, การลองใหม่จนหมด, URL ที่ถูกบล็อกแบบ SSRF, การปฏิเสธลายเซ็นที่ timestamp ถูกดัดแปลงเทียบกับ vector คงที่ และการจำกัดจำนวนความพยายามใน dead-letter

หน้านี้อธิบายเฉพาะพฤติกรรมที่สังเกตได้จากภายนอกและพื้นผิว API สาธารณะที่รองรับเท่านั้น เส้นทาง namespace ภายใน, คลาสตัวช่วย, ตารางกลไก, ชื่อไฟล์ runbook และคำนำหน้า ticket อยู่นอกขอบเขต