Enterprise 版本
Webhook — 深入參考
NextPDF\Enterprise\Webhook 命名空間為工作事件提供租戶範圍的 webhook 傳遞。公開範圍為六個符號:WebhookManager、WebhookRegistration、WebhookPayload、WebhookDelivery、WebhookRetryPolicy 與 DeadLetterEntry。manager 逐租戶登錄端點,並將工作事件派送給訂閱中的登錄。傳遞引擎會 POST 一份經 HMAC-SHA256 簽署的 JSON 酬載,針對 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 登錄或傳遞範圍;manager、登錄、酬載、傳遞引擎、重試原則與死信項目僅隨附於 nextpdf/enterprise。
公開 API 範圍
標題為「公開 API 範圍」的區段| 符號 | 參數 | 預設行為 | 回傳 | 拋出或失敗於 | 註記 |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery、?LoggerInterface $logger = null | 建立一個帶有空的記憶體登錄索引的 manager | 新的 WebhookManager | 不會拋出 | 登錄是逐租戶索引 |
WebhookManager::register | TenantContext $tenant、WebhookRegistration $registration | 將登錄附加到呼叫租戶的索引 | void | 當登錄的租戶與情境租戶不符時拋出 InvalidArgumentException | 跨租戶登錄在儲存前即被拒絕 |
WebhookManager::unregister | TenantContext $tenant、string $registrationId | 以停用的副本取代相符的登錄 | bool | 不會拋出;找不到 id 時回傳 false | 軟停用;歷史予以保留 |
WebhookManager::activeRegistrations | TenantContext $tenant | 將該租戶的登錄篩選為有效者 | list<WebhookRegistration> | 不會拋出 | 只有呼叫租戶的登錄可見 |
WebhookManager::dispatch | TenantContext $tenant、JobEvent $event | 將事件傳遞給每一個訂閱該事件型別的有效登錄 | int(成功傳遞數) | 當事件資料無法 JSON 編碼時傳播 JsonException;傳遞失敗不會拋出 | 每次登錄傳遞會產生一個全新的 32-hex 傳遞 id |
WebhookRegistration::__construct | string $id、string $tenantId、string $url、array $events、string $secret、bool $active = true、?string $description = null | 原封不動地儲存所提供的值 | 新的 WebhookRegistration | 未宣告 @throws;在 strict_types 下 PHP 會對不符的引數型別拋出 TypeError | final readonly;空的 $events 代表訂閱全部 |
WebhookRegistration::subscribesTo | JobEventType $eventType | 當 $events 為空或包含該型別時為 true | bool | 不會拋出 | 嚴格同一性比較 |
WebhookRegistration::deactivate | — | 回傳一份停用的副本 | self | 不會拋出 | 原始實例維持不變 |
WebhookPayload::fromJobEvent | JobEvent $event、string $tenantId、string $deliveryId | 從事件複製工作 id、事件型別、資料與時間戳記 | self | 不會拋出 | dispatch 所使用的靜態工廠 |
WebhookPayload::toJson | — | 以不轉義斜線序列化六欄位主體 | non-empty-string | 當事件資料無法 JSON 編碼時拋出 JsonException | 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 | 對基礎字串 {signedTimestamp}.{jsonBody} 計算 HMAC-SHA256 | non-empty-string(hex) | 當主體無法編碼時,透過 toJson() 拋出 JsonException | 以密碼學方式將時間戳記標頭綁定至主體 |
WebhookDelivery::__construct | ClientInterface $httpClient、RequestFactoryInterface $requestFactory、StreamFactoryInterface $streamFactory、WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy()、?LoggerInterface $logger = null | 帶有空死信佇列的 PSR-18/PSR-17 傳遞引擎 | 新的 WebhookDelivery | 不會拋出 | 預設原則:5 次嘗試、1 s 基礎、300 s 上限 |
WebhookDelivery::deliver | WebhookRegistration $registration、WebhookPayload $payload | 以逐次嘗試的 SSRF 出向驗證與指數退避 POST 已簽署的酬載 | bool | 當主體無法編碼時,在第一次嘗試前拋出 JsonException;否則不會拋出——false 代表酬載已被送入死信佇列 | 僅在 2xx 回應時為 true |
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 s 基礎、300 s 上限 | self | 不會拋出 | 靜態工廠;正式環境預設 |
WebhookRetryPolicy::aggressive | — | 10 次嘗試、2 s 基礎、600 s 上限 | self | 不會拋出 | 針對關鍵端點的靜態工廠 |
DeadLetterEntry::__construct | string $id、string $registrationId、WebhookPayload $payload、int $attempts、string $lastError、?int $lastHttpStatus、DateTimeImmutable $failedAt、bool $replayed = false | 原封不動地儲存失敗紀錄 | 新的 DeadLetterEntry | 未宣告 @throws;在 strict_types 下拋出 TypeError | 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()只會走訪呼叫租戶中訂閱了所派送事件型別的有效登錄。空的已訂閱事件清單代表訂閱全部。回傳值計數的是成功傳遞數。- 每次傳遞都是一個帶有 JSON 主體與五個標頭的 HTTP POST:
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,以不轉義斜線序列化。事件型別值來自nextpdf/core中的JobEventType:progress、completed、failed、cancelled。 - 簽章方案(於 3.1.0 變更,破壞性)。 HMAC-SHA256 的基礎字串是
{signedTimestamp}.{jsonBody},以登錄密鑰為金鑰——而非僅有主體。X-NextPDF-Timestamp值是 MAC 的時間戳記部分,因此遭竄改或重播的時間戳記標頭會使簽章失效。 - 接收方驗證:讀取
X-NextPDF-Timestamp標頭T;當T落在可接受的新鮮度視窗(例如 300 s)之外時予以拒絕;對原始接收的 bytes 重新計算hash_hmac('sha256', T . '.' . rawBody, secret);剝除sha256=前綴後,以常數時間與標頭值比較。 - 主體、簽章與傳遞 id 每次傳遞只計算一次,並在各次重試嘗試間維持不變。
- SSRF 出向閘門。 每次嘗試前,目的地 URL 都會通過 Core 的
UrlValidator::validateExternalUrl()閘門:僅限 HTTPS scheme;loopback、私有、保留、carrier-grade-NAT、cloud-metadata 與 IPv4 內嵌的 IPv6 轉換範圍皆被封鎖;主機名稱會經 DNS 解析(A 與 AAAA),無法解析的主機以 fail-closed 拒絕。被封鎖的 URL 絕不會送出:嘗試迴圈中止,酬載直接以Blocked SSRF destination:最後錯誤與 null HTTP 狀態送入死信佇列。 - 逐次嘗試的結果分類:2xx 為成功並立即回傳;429 以外的 4xx 為終止,直接進入死信;其他所有結果——3xx、429、5xx 或傳輸例外——皆可重試,直到原則的總嘗試次數。
- 退避為指數式:下一次嘗試前的等待為
baseDelaySeconds × 2^(attempt − 1),以maxDelaySeconds為上限。最後一次嘗試之後會跳過等待。 - 當沒有任何嘗試成功時,
DeadLetterEntry會記錄一個唯一 id、登錄 id、原始酬載、嘗試次數(夾限至原則最大值)、最後一則錯誤訊息、最後的 HTTP 狀態(傳輸失敗或 SSRF 封鎖時為 null),以及失敗時間戳記。 - 死信佇列在記憶體中,且範圍限於程序生命週期。
markReplayed()會產生一份帶標記的副本;它不會重送,且佇列會保留原始項目。
邊界案例與失敗模式
標題為「邊界案例與失敗模式」的區段- 空事件清單。 該登錄會收到每一種事件型別。當接收方不應看到所有事件時,請明確界定清單範圍。
- 終止的 4xx 與傳輸失敗。 4xx 拒絕會記錄一個已填入的
lastHttpStatus;連線失敗則記錄 null。請用 null 來區分接收方拒絕與傳輸失敗。 - SSRF 封鎖的目的地。 指向 HTTP、私有、loopback 或 metadata 位址的登錄會在第一次嘗試就以
Blocked SSRF destination:錯誤與 null 狀態進入死信。不會發出任何對外請求。請修正 URL 後重新登錄。 - 升級後的舊版接收方。 仍在驗證 3.1.0 之前僅主體 HMAC 的接收方,對 3.1.0 的傳遞會 fail closed。請將接收方遷移至
{timestamp}.{body}基礎字串並消費X-NextPDF-Timestamp。 - 無法編碼的事件資料。
toJson()與sign()會拋出JsonException,並在任何嘗試發生之前從deliver()與dispatch()向外傳播。 - 同步阻塞。
deliver()會在各次嘗試間就地 sleep。累積退避在 default 原則下達 15 s,在 aggressive 原則下約 17 分鐘。當接收方延遲不可信時,請自佇列 worker 派送。 - 嘗試次數夾限。 已記錄的嘗試次數絕不會超過原則最大值,即使內部迴圈計數器在耗盡時會推進超過它。
- 佇列成長與持久性。 死信佇列在程序內無界成長,並於重啟時消失。當需要持久重播時,請在呼叫
clearDeadLetters()之前透過deadLetters()匯出項目並於外部持久化。 - 重播由操作者驅動。 重新傳遞意指以項目的酬載再次呼叫
deliver();markReplayed()只是在一份副本上記錄此事實。 - DNS 重新綁定的殘留風險。 URL 會在每次嘗試時重新驗證,這縮小但未關閉重新綁定的視窗:PSR-18 抽象無法將連線釘定到已驗證的 IP。在此殘留風險重要之處,請加上網路層的出向控制。
- 密鑰處理。 登錄密鑰是一項憑證。HMAC 只驗證完整性與來源——它不是機密性。請勿將接收方不應看到的資料放入事件酬載。
FIPS 模式行為
標題為「FIPS 模式行為」的區段酬載簽署是透過 PHP 的 hash_hmac() 進行的 HMAC-SHA256,因此依賴主機的密碼學供應者。在 FIPS 受限的建置中,非核准的原語會在密碼學邊界失敗,而非降級。webhook 層不另加任何自有的密碼學原則。
一致性
標題為「一致性」的區段- 酬載驗證實作了 HMAC,即 FIPS PUB 198-1 §1 的金鑰雜湊訊息驗證碼,以 SHA-256 實例化。
- 重播防護遵循 OWASP Cheat Sheet Series 的 webhook 安全指引:事件時間戳記以專屬標頭傳遞並植入簽章運算,因此遭竄改的時間戳記會驗證失敗。
- 主體時間戳記使用 RFC 3339 延伸日期時間格式。由程式碼宣告:本頁未從 RAG 語料庫取得 RFC 3339。
- 這些是根植於產品原始碼與所引條款的能力陳述。NextPDF 對此範圍不作任何一致性或認證主張。
開發註記
標題為「開發註記」的區段- 所有類別皆宣告
strict_types=1且為final;WebhookRegistration、WebhookPayload、WebhookRetryPolicy與DeadLetterEntry為final readonly,並帶有提升的公開屬性。 - 此模組帶有
2.2.0的@since註解;時間戳記綁定的簽章方案是 3.1.0 中一項已記載的破壞性變更。 - 傳遞引擎接受 PSR-18/PSR-17 抽象,因此一個 mock HTTP 用戶端即可離線演練完整的傳送、重試與死信路徑。logger 預設為 null;請在正式環境注入一個 PSR-3 logger,否則失敗只會透過回傳值浮現。
- 接收方實作應使用
hash_equals()進行簽章比較,並對X-NextPDF-Timestamp強制執行新鮮度視窗。 - 建議的邊界測試:租戶不符的登錄、空事件清單的扇出、終止的 4xx、重試耗盡、SSRF 封鎖的 URL、對固定向量拒絕遭竄改時間戳記的簽章,以及死信嘗試次數的夾限。
發佈邊界
標題為「發佈邊界」的區段本頁僅記載外部可觀察的行為與受支援的公開 API 範圍。內部命名空間路徑、輔助類別、機制表格、runbook 檔名與工單前綴皆不在範圍內。
另請參閱
標題為「另請參閱」的區段- Webhook — NextPDF Enterprise — 能力頁面:工作流程、設定與實作的登錄範例。
- SaaS — 深入參考 — 租戶身分、API 金鑰與配額;
TenantContext的來源。 - Metering — 深入參考 — 採用相同 PSR-18 傳遞規範的用量計量扇出。