# 웹훅

동의 철회·등급 변경 같은 이벤트를 파트너 서버로 자동 알림합니다. 엔드포인트 등록은 운영팀을 통해
이루어지며, 등록 시 서명 시크릿이 1회 평문으로 발급됩니다.

## 이벤트 종류 (4종)

| eventType | 발생 시점 | 필요 동의 카테고리 |
|---|---|---|
| `consent.revoked` | 동의 철회 시. 철회 후에도 이 이벤트만 발송 | 없음(카테고리 무관) |
| `score.updated` | 점수 재산출·저장 시 | `monitoring` |
| `tier.changed` | 등급 변경 시. `direction`은 UP/DOWN만 | `monitoring` |
| `attestation.expiring` | 증명서 7일 내 만료 예정(일 1회 스캔) | — |

점수 원값은 웹훅에 절대 포함되지 않습니다. 이벤트는 해당 pairwise 동의가 ACTIVE인 클라이언트의
ACTIVE 엔드포인트에만 발송됩니다(`consent.revoked`는 철회 직전 동의 보유 클라이언트에).

## 페이로드

```json
{
  "deliveryId": "d2f1a3b4-5c6d-7e8f-9a0b-example",
  "eventType": "tier.changed",
  "occurredAt": "2026-08-24T02:15:00Z",
  "data": { "pairwiseSub": "b41c9d2f-8e77-4a10-9c3e-example", "direction": "DOWN" }
}
```

## 서명 검증

| 헤더 | 내용 |
|---|---|
| `X-HACS-Signature` | `t=<unixSeconds>,v1=<hex hmacSHA256(secret, t + "." + rawBody)>` |
| `X-HACS-Delivery-Id` | 전달 고유 ID — 중복 수신(at-least-once) 멱등 처리용 |

절차: `t`/`v1` 파싱 → `t + "." + rawBody`를 HMAC-SHA256 → hex를 `v1`과 상수시간 비교 →
`t`가 현재 시각 ±5분을 벗어나면 거부. HMAC은 반드시 원문 바디(raw body)로 계산해야 합니다.

Java:

```java
String signedPayload = t + "." + rawBody;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
// hex(raw)를 v1과 MessageDigest.isEqual로 비교
```

TypeScript (Node.js):

```ts
import { createHmac, timingSafeEqual } from "node:crypto";
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const ok = timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
```

## 수신 규칙과 재시도

- 3초 안에 2xx 응답(타임아웃 3초) — 무거운 처리는 큐로
- at-least-once 전달 — `X-HACS-Delivery-Id`로 중복 제거
- 실패 시 재시도 사다리: 1분 → 5분 → 30분 → 2시간, 소진 시 EXHAUSTED
- 연속 10회 실패 시 엔드포인트 자동 비활성화(DISABLED) — 재활성화는 운영팀에 요청
- https만 허용, 사설/루프백/링크로컬 IP로 resolve되는 호스트 거부

## 테스트

운영팀에 테스트 이벤트 발송을 요청하면 실제 서명이 붙은 합성 이벤트가 전달됩니다.
최근 50건의 전달 기록도 운영팀을 통해 확인할 수 있습니다.
