콘텐츠로 이동
getnextpdf.com

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에만 포함됩니다.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
WebhookManager::__constructWebhookDelivery $delivery, ?LoggerInterface $logger = null비어 있는 인메모리 등록 색인을 가진 매니저를 생성합니다New WebhookManager던지지 않음등록은 테넌트별로 색인됩니다
WebhookManager::registerTenantContext $tenant, WebhookRegistration $registration호출 테넌트의 색인에 등록을 추가합니다void등록 테넌트가 컨텍스트 테넌트와 일치하지 않으면 InvalidArgumentException교차 테넌트 등록은 저장 전에 거부됩니다
WebhookManager::unregisterTenantContext $tenant, string $registrationId일치하는 등록을 비활성화된 사본으로 교체합니다bool던지지 않음; id를 찾지 못하면 false 반환소프트 비활성화; 이력은 보존됩니다
WebhookManager::activeRegistrationsTenantContext $tenant테넌트의 등록을 활성 항목으로 필터링합니다list<WebhookRegistration>던지지 않음호출 테넌트의 등록만 보입니다
WebhookManager::dispatchTenantContext $tenant, JobEvent $event이벤트 유형을 구독하는 모든 활성 등록에 이벤트를 전달합니다int(성공한 전달)이벤트 데이터가 JSON 인코딩 불가일 때 JsonException 전파; 전달 실패는 던지지 않음등록 전달마다 새로운 32-hex 전달 id가 생성됩니다
WebhookRegistration::__constructstring $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::subscribesToJobEventType $eventType$events가 비어 있거나 해당 유형을 포함하면 truebool던지지 않음엄격한 동일성 비교
WebhookRegistration::deactivate비활성 사본을 반환합니다self던지지 않음원본 인스턴스는 변경되지 않습니다
WebhookPayload::fromJobEventJobEvent $event, string $tenantId, string $deliveryId이벤트에서 작업 id, 이벤트 유형, 데이터, 타임스탬프를 복사합니다self던지지 않음dispatch가 사용하는 정적 팩토리
WebhookPayload::toJson슬래시를 이스케이프하지 않고 여섯 필드 본문을 직렬화합니다non-empty-string이벤트 데이터가 JSON 인코딩 불가일 때 JsonExceptionJSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES
WebhookPayload::toArray본문을 연관 배열로 반환합니다array<string, mixed>던지지 않음타임스탬프는 RFC 3339 확장 형식
WebhookPayload::signedTimestampUnix 초 이벤트 시각, 0 이상으로 클램프됨int<0, max>던지지 않음X-NextPDF-Timestamp로 방출되고 MAC에 바인딩됩니다
WebhookPayload::signstring $secret기반 문자열 {signedTimestamp}.{jsonBody}에 대한 HMAC-SHA256non-empty-string(hex)본문이 인코딩 불가일 때 toJson()을 통해 JsonException타임스탬프 헤더를 본문에 암호학적으로 바인딩합니다
WebhookDelivery::__constructClientInterface $httpClient, RequestFactoryInterface $requestFactory, StreamFactoryInterface $streamFactory, WebhookRetryPolicy $retryPolicy = new WebhookRetryPolicy(), ?LoggerInterface $logger = null비어 있는 데드 레터 큐를 가진 PSR-18/PSR-17 전달 엔진New WebhookDelivery던지지 않음기본 정책: 5회 시도, 1s 기본, 300s 상한
WebhookDelivery::deliverWebhookRegistration $registration, WebhookPayload $payload시도별 SSRF 이그레스 검증과 지수 백오프로 서명된 페이로드를 POST합니다bool본문이 인코딩 불가일 때 첫 시도 전에 JsonException; 그 외에는 던지지 않음 — false는 페이로드가 데드 레터 큐로 라우팅되었음을 의미2xx 응답에서만 true
WebhookDelivery::deadLetters기록된 모든 항목을 반환합니다list<DeadLetterEntry>던지지 않음인메모리, 프로세스 범위
WebhookDelivery::clearDeadLetters데드 레터 큐를 비웁니다void던지지 않음되돌릴 수 없음; 재생이 필요하면 먼저 항목을 내보내십시오
WebhookRetryPolicy::__constructint $maxRetries = 5, int $baseDelaySeconds = 1, int $maxDelaySeconds = 300정책 값을 저장합니다New WebhookRetryPolicy선언된 @throws 없음; 파라미터는 positive-int로 문서화됨$maxRetries는 총 시도 횟수를 셉니다
WebhookRetryPolicy::delayForAttemptint $attemptbaseDelaySeconds × 2^(attempt − 1), maxDelaySeconds로 제한됨positive-int던지지 않음시도 번호는 1부터 시작합니다
WebhookRetryPolicy::shouldRetryint $currentAttempt현재 시도가 최댓값 미만인 동안 truebool던지지 않음마지막 시도 후에는 대기를 건너뜁니다
WebhookRetryPolicy::default5회 시도, 1s 기본, 300s 상한self던지지 않음정적 팩토리; 프로덕션 기본값
WebhookRetryPolicy::aggressive10회 시도, 2s 기본, 600s 상한self던지지 않음중요 엔드포인트용 정적 팩토리
DeadLetterEntry::__constructstring $id, string $registrationId, WebhookPayload $payload, int $attempts, string $lastError, ?int $lastHttpStatus, DateTimeImmutable $failedAt, bool $replayed = false실패 기록을 그대로 저장합니다New DeadLetterEntry선언된 @throws 없음; strict_types에서 TypeErrorfinal readonly; null $lastHttpStatus는 전송 실패를 의미
DeadLetterEntry::markReplayedreplayed = 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): 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
  • 등록은 테넌트별로 색인됩니다. 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/coreJobEventType에서 옵니다: 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는 무결성과 출처만 인증하며 — 기밀성이 아닙니다. 수신자가 보아서는 안 되는 데이터를 이벤트 페이로드에 두지 마십시오.

페이로드 서명은 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 표면만 문서화합니다. 내부 네임스페이스 경로, 헬퍼 클래스, 메커니즘 표, 런북 파일명, 티켓 접두사는 범위 밖입니다.