Mobidoo

웹훅 검증

@sume-com/client의 verifyWebhook은 Sume 전달의 sume-v1 서명을 확인합니다. 원본 본문, 헤더 형태, 재전송 허용 시간, 시크릿 교체를 살펴보세요.

이 문서는 영문 원고를 AI로 번역한 내용이라 표현이 어색할 수 있습니다.

Sume는 모든 웹훅 전달에 <timestamp>.<raw_body>에 대한 HMAC-SHA256 서명을 붙여 sume-v1=<hex> 형태로 보냅니다. @sume-com/client의 verifyWebhook이 그 확인을 대신하므로 직접 구현하지 않아도 됩니다.

서명 시크릿은 대시보드의 웹훅 탭(/dashboard/webhooks — 표시 후 복사)에서 직접 확인하거나, account:read 범위를 가진 API 키로 GET /v1/webhooks/signing-secret을 호출해 받을 수 있습니다. 워크스페이스별로 파생된 값이므로, 서명이 유효하다는 것은 공용 시크릿을 가진 누군가가 아니라 여러분에게 서명되었다는 뜻입니다. 로컬 샘플이 Sume 워커의 서명과 맞도록 환경변수 이름은 SUME_COM_WEBHOOK_SIGNING_SECRET을 쓰고, API 키와 같은 수준으로 보관하세요. API 키와는 다른 값입니다. verifyWebhook은 클라이언트가 필요 없고 요청도 보내지 않습니다.

대시보드에는 시크릿의 지문도 함께 표시되고, 모든 전송에 x-sume-webhook-secret-fingerprint 헤더로 같은 값이 실립니다. 서명 검증이 계속 실패하면 지문을 비교하세요. 이 과정에서 티켓에 붙여넣어도 안전한 값은 지문뿐입니다.

시크릿 교체

시크릿이 유출되었을 수 있다면 교체하세요. 대시보드의 웹훅 → 시크릿 교체, 또는 account:write 범위를 가진 키로 POST /v1/webhooks/signing-secret/rotate를 호출하면 됩니다.

교체는 즉시 전환이 아닙니다. 이후 24시간 동안 Sume는 모든 전송을 두 시크릿 모두로 서명하고, x-sume-webhook-signature 헤더에 새 서명부터 순서대로 쉼표로 이어 보냅니다.

둘 중 하나만 갖고 있어도 검증되므로, 버튼을 누른 순간에 맞춰 배포할 필요 없이 편한 일정으로 수신 서버를 교체하면 됩니다. 이 기간이 지나면 이전 시크릿은 더 이상 검증되지 않습니다. 기간이 열려 있는 동안 대시보드에 만료 시각이 표시되고, 두 API 응답 모두 rotation.previous_valid_until로 같은 값을 전달합니다.

<Callout type="warn"> **`@sume-com/client` 0.3.0**의 `verifyWebhook`은 이미 다중 서명 헤더를 처리합니다. 서명 중 하나만 맞아도 전달을 받아들입니다. 헤더를 문자열 일치로 비교하는 자체 구현은 이 기간 동안 모든 전송에서 실패합니다. 교체하기 **전에** 수신 서버를 먼저 올려 주세요. 기간이 아닐 때는 서명이 하나만 실리므로, 교체하지 않는 분들에게는 아무 변화가 없습니다. </Callout>

x-sume-webhook-secret-fingerprint는 교체한 순간부터 이 기간 중에도 새 시크릿을 가리킵니다. 지금 어떤 시크릿이 함께 허용되는지가 아니라, 어떤 시크릿으로 옮겨가야 하는지를 알려 주는 값입니다. 한 기간 안에서 두 번 교체하면 두 단계 전 시크릿은 즉시 폐기되며, 유출이 실제로 멈추는 지점이 바로 이 동작입니다.

입력

verifyWebhook(rawBody, timestamp, signature, secret, toleranceSeconds?)

인자설명
rawBody원본 본문입니다. string 또는 Buffer입니다.
timestampx-sume-webhook-timestamp 헤더입니다. Node의 req.headers[...] 값을 그대로 넘겨도 됩니다.
signaturex-sume-webhook-signature 헤더입니다.
secretSume 웹훅 서명 시크릿입니다. 빈 시크릿은 BadInputError를 던집니다.
toleranceSeconds재전송 허용 시간입니다. 기본값은 300입니다.

두 헤더는 이렇게 생겼습니다.

동작을 좌우하는 네 가지 규칙

  • 원본 본문을 넘기세요. 파싱했다가 다시 직렬화한 객체는 검증되지 않습니다. 키 순서와 공백도 서명 대상의 일부입니다. JSON을 대신 파싱해 주는 프레임워크는 이미 바이트를 망가뜨린 상태입니다. Express에서는 웹훅 라우트에만 express.raw({ type: "application/json" })을 붙이세요. Next.js App Router에서는 무엇보다 먼저 await request.text()를 호출하세요.
  • Node.js에서 동작합니다. 확인에 node:crypto를 쓰고 바로 결과를 돌려주므로 await할 것이 없습니다. Workers나 Deno에서는 Run 웹훅의 원본 스킴대로 WebCrypto로 검증하세요.
  • 잘못된 전달에는 throw하지 않고 false를 반환합니다. 헤더 누락, 엉뚱하거나 오래된 타임스탬프, 잘못된 서명은 모두 그냥 검증 실패입니다. 분기할 지점은 하나입니다.
  • 비교는 상수 시간으로 이뤄지고, 재전송 허용 시간은 HMAC보다 먼저 확인합니다.

검증기 하나, 표면 둘

Run 웹훅(*.run.terminal, run_id 포함)과 생성 Job 웹훅(job.*, job_id 포함)은 sume-v1 스킴을 그대로 공유합니다. 페이로드는 다르지만 서명은 같습니다. 그래서 검증기 하나로 둘 다 처리할 수 있습니다. event로 분기하고, 본문에 run_id가 있다고 가정하지 마세요.

알 수 없는 이벤트를 204로 처리하는 것이, 새로 추가된 이벤트 타입이 500과 재시도 폭주로 번지는 것을 막아 줍니다.

필요하다면 쓰는 상수

verifyWebhook이 일반적인 경우를 처리합니다. 수신기를 직접 통제하지 못할 때 — 예를 들어 게이트웨이가 코드 실행 전에 검증하는 경우 — 를 위해 구성 요소도 내보냅니다.

Export값
WEBHOOK_SIGNATURE_VERSION"sume-v1"
WEBHOOK_SIGNATURE_HEADER"x-sume-webhook-signature"
WEBHOOK_TIMESTAMP_HEADER"x-sume-webhook-timestamp"
WEBHOOK_TOLERANCE_SECONDS300

sume-client(Python)에는 검증기가 없습니다. 완성된 Python 수신기는 쿡북에 있습니다.

다음

  • Run 웹훅 — 전달, 이벤트, 페이로드, 재시도, 그리고 JavaScript가 아닌 수신기를 위한 원본 스킴입니다
  • 웹훅 — 다른 표면인 생성 Job 웹훅입니다
  • 실행과 Job 기다리기 — 선택적 폴백인 폴링 경로입니다(프로덕션에서도 전달이 켜져 있습니다)