Enterprise エディション
Webhook — 詳細リファレンス
NextPDF\Enterprise\Webhook 名前空間は、ジョブイベント向けのテナントスコープの webhook 配信を提供します。パブリックサーフェスは 6 つのシンボルです。WebhookManager、WebhookRegistration、WebhookPayload、WebhookDelivery、WebhookRetryPolicy、DeadLetterEntry。マネージャーはテナントごとにエンドポイントを登録し、ジョブイベントをサブスクライブしている登録へディスパッチします。配信エンジンは HMAC-SHA256 署名済みの JSON ペイロードを POST し、すべての宛先を 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 | スローしない。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 | — | 6 フィールドのボディをスラッシュ非エスケープでシリアライズ | non-empty-string | イベントデータが JSON エンコード不能な場合は JsonException | JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES |
WebhookPayload::toArray | — | ボディを連想配列として返す | array<string, mixed> | スローしない | タイムスタンプは RFC 3339 extended 形式 |
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 回試行、1s ベース、300s 上限 |
WebhookDelivery::deliver | WebhookRegistration $registration、WebhookPayload $payload | 試行ごとの SSRF イグレス検証と指数バックオフで署名済みペイロードを POST | 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 回試行、1s ベース、300s 上限 | self | スローしない | 静的ファクトリ。本番デフォルト |
WebhookRetryPolicy::aggressive | — | 10 回試行、2s ベース、600s 上限 | 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 ボディと 5 つのヘッダーを持つ 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 extended)、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が許容される鮮度ウィンドウ(例: 300s)の外にある場合は拒否します。受信した生バイト列に対してhash_hmac('sha256', T . '.' . rawBody, secret)を再計算し、sha256=プレフィックスを除去したヘッダー値と定数時間で比較します。 - ボディ、署名、配信 id は配信ごとに一度計算され、再試行をまたいで一定に保たれます。
- SSRF イグレスゲート。 すべての試行の前に、宛先 URL は Core の
UrlValidator::validateExternalUrl()ゲートを通過します。HTTPS スキームのみ。ループバック、プライベート、予約、キャリアグレード NAT、クラウドメタデータ、IPv4 埋め込み IPv6 移行レンジはブロックされます。ホスト名は DNS 解決(A および AAAA)され、解決不能なホストはフェイルクローズで拒否されます。ブロックされた URL は決して送信されません。試行ループは中止され、ペイロードはBlocked SSRF destination:の last error と 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、プライベート、ループバック、またはメタデータアドレスを指す登録は、初回試行で
Blocked SSRF destination:エラーと null ステータスを伴ってデッドレターされます。アウトバウンドリクエストは行われません。URL を修正して再登録してください。 - アップグレード後のレガシー受信側。 3.1.0 以前のボディ単独 HMAC をまだ検証している受信側は、3.1.0 の配信に対してフェイルクローズします。受信側を
{timestamp}.{body}ベース文字列へ移行し、X-NextPDF-Timestampを消費してください。 - エンコード不能なイベントデータ。
toJson()とsign()はJsonExceptionをスローし、これはいずれの試行が行われる前にdeliver()とdispatch()の外へ伝播します。 - 同期ブロッキング。
deliver()は試行の間にインラインでスリープします。累積バックオフは、デフォルトポリシー下で 15s、aggressive ポリシー下で約 17 分に達します。受信側のレイテンシが信頼できない場合は、キューワーカーからディスパッチしてください。 - 試行回数のクランプ。 記録される試行回数は、内部ループカウンターが枯渇時にそれを超えて進んだ場合でも、ポリシー最大値を決して超えません。
- キューの増大と耐久性。 デッドレターキューはプロセス内で無制限に増大し、再起動で消えます。耐久的なリプレイが必要な場合は、
clearDeadLetters()を呼ぶ前にdeadLetters()でエントリをエクスポートし、外部に永続化してください。 - リプレイはオペレーター駆動。 再配信とは、エントリのペイロードで再び
deliver()を呼ぶことを意味します。markReplayed()はコピー上に事実を記録するだけです。 - DNS リバインディングの残余。 URL はすべての試行で再検証されますが、これはリバインディングウィンドウを狭めるものの閉じはしません。PSR-18 抽象は接続を検証済み IP にピン留めできません。この残余が問題になる箇所ではネットワークレイヤーのイグレス制御を追加してください。
- シークレットの取り扱い。 登録シークレットは認証情報です。HMAC は完全性と起源のみを認証し、機密性ではありません。受信側が見るべきでないデータをイベントペイロードに含めないでください。
FIPS モードの挙動
「FIPS モードの挙動」という見出しのセクションペイロードの署名は PHP の hash_hmac() を通じた HMAC-SHA256 であるため、ホストの暗号プロバイダーに依存します。FIPS 制約付きのビルドでは、承認されていないプリミティブはダウングレードするのではなく暗号境界で失敗します。Webhook レイヤーは独自の暗号ポリシーを一切追加しません。
- ペイロード認証は、FIPS PUB 198-1 §1 のキー付きハッシュ MAC である HMAC を実装し、SHA-256 でインスタンス化されています。
- リプレイ保護は OWASP Cheat Sheet Series の webhook セキュリティガイダンスに従います。イベントタイムスタンプは専用ヘッダーで伝わり、署名計算にシードされるため、改ざんされたタイムスタンプは検証に失敗します。
- ボディのタイムスタンプは RFC 3339 extended date-time 形式を使用します。コード宣言: RFC 3339 はこのページでは RAG コーパスから取得していません。
- これらは製品ソースと引用クローズに基づくケイパビリティの記述です。NextPDF はこのサーフェスについて適合性または認証の主張を一切行いません。
開発上の注意
「開発上の注意」という見出しのセクション- すべてのクラスは
strict_types=1を宣言しfinalです。WebhookRegistration、WebhookPayload、WebhookRetryPolicy、DeadLetterEntryは昇格されたパブリックプロパティを持つfinal readonlyです。 - モジュールは
2.2.0の@sinceアノテーションを持ちます。タイムスタンプ束縛の署名スキームは 3.1.0 で文書化された破壊的変更です。 - 配信エンジンは PSR-18/PSR-17 抽象を取るため、モック HTTP クライアントで送信、再試行、デッドレターの全経路をオフラインで実行できます。ロガーはデフォルトで null です。本番では PSR-3 ロガーを注入してください。さもなければ失敗は戻り値でしか表面化しません。
- 受信側の実装は、署名比較に
hash_equals()を使用し、X-NextPDF-Timestampに鮮度ウィンドウを強制すべきです。 - 推奨される境界テスト: テナント不一致登録、空イベントリストのファンアウト、終端 4xx、再試行枯渇、SSRF ブロック URL、固定ベクトルに対するタイムスタンプ改ざん署名の拒否、デッドレター試行回数のクランプ。
公開範囲の境界
「公開範囲の境界」という見出しのセクションこのページは、外部から観測可能な挙動とサポートされるパブリック API サーフェスのみを記載します。内部名前空間パス、ヘルパークラス、メカニズム表、ランブックのファイル名、チケットプレフィックスは対象外です。
- Webhook — NextPDF Enterprise — ケイパビリティページ。ワークフロー、設定、登録例。
- SaaS — 詳細リファレンス — テナント識別、API キー、クォータ。
TenantContextの出所。 - Metering — 詳細リファレンス — 同じ PSR-18 配信規律による使用量メータリングのファンアウト。