Enterprise 에디션
Webhook — 심층 참조
한눈에 보기
섹션 제목: “한눈에 보기”NextPDF\Enterprise\Webhook 네임스페이스는 작업 이벤트에 대한 테넌트 범위의 웹훅 전달을 제공합니다. 공개 표면은 여섯 개의 심볼입니다: 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 등급 라이선스 엔벌로프로 활성화됩니다. 해당 자격이 없는 배포는 이 역량의 클래스를 로드하지 않습니다. 에디션을 비교하고 라이선스를 받으십시오.
웹훅 표면은 기본 Enterprise 역량으로, Enterprise 패키지가 설치되면 사용할 수 있으며 별도의 기능별 플래그는 없습니다. NextPDF Core(Apache-2.0)와 NextPDF Pro에는 웹훅 등록이나 전달 표면이 없습니다. 매니저, 등록, 페이로드, 전달 엔진, 재시도 정책, 데드 레터 항목은 nextpdf/enterprise에만 포함됩니다.
공개 API 표면
섹션 제목: “공개 API 표면”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
WebhookManager::__construct | WebhookDelivery $delivery, ?LoggerInterface $logger = null | 비어 있는 인메모리 등록 색인을 가진 매니저를 생성합니다 | New 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 | 제공된 값을 그대로 저장합니다 | New 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 초 이벤트 시각, 0 이상으로 클램프됨 | 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 전달 엔진 | New WebhookDelivery | 던지지 않음 | 기본 정책: 5회 시도, 1s 기본, 300s 상한 |
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 | 정책 값을 저장합니다 | New 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 | 실패 기록을 그대로 저장합니다 | New 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가 허용 신선도 윈도(예: 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:마지막 오류와 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 제약 빌드에서 비승인 프리미티브는 다운그레이드하지 않고 암호화 경계에서 실패합니다. 웹훅 계층은 자체 암호화 정책을 추가하지 않습니다.
적합성
섹션 제목: “적합성”- 페이로드 인증은 FIPS PUB 198-1 §1의 키 해시 메시지 인증 코드인 HMAC를 구현하며, SHA-256으로 인스턴스화됩니다.
- 재생 보호는 OWASP Cheat Sheet Series 웹훅 보안 지침을 따릅니다: 이벤트 타임스탬프가 전용 헤더로 이동하고 서명 계산에 시드되므로, 변조된 타임스탬프는 검증에 실패합니다.
- 본문 타임스탬프는 RFC 3339 확장 날짜-시간 형식을 사용합니다. 코드 선언: 이 페이지에서 RFC 3339는 RAG 코퍼스에서 검색되지 않았습니다.
- 이는 제품 소스와 인용된 조항에 근거한 역량 기술입니다. NextPDF는 이 표면에 대해 어떠한 적합성 또는 인증 주장도 하지 않습니다.
개발 참고
섹션 제목: “개발 참고”- 모든 클래스는
strict_types=1을 선언하고final이며;WebhookRegistration,WebhookPayload,WebhookRetryPolicy,DeadLetterEntry는 승격된 public 프로퍼티를 가진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 전달 규율을 사용하는 사용량 미터링 팬아웃.