@k-msg/webhook 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -357
- package/README_ko.md +176 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +24 -24
- package/dist/index.js.map +7 -7
- package/dist/index.mjs +25 -25
- package/dist/index.mjs.map +7 -7
- package/dist/security/security.manager.d.ts +15 -1
- package/dist/services/webhook.dispatcher.d.ts +3 -1
- package/dist/services/webhook.service.d.ts +0 -2
- package/dist/types/webhook.types.d.ts +7 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,418 +1,176 @@
|
|
|
1
|
-
# @k-msg/webhook
|
|
1
|
+
# @k-msg/webhook
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Webhook delivery helpers for emitting real-time message events to HTTP endpoints.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
This package provides:
|
|
6
|
+
- `WebhookService`: a convenience facade (in-memory endpoint registry + batching)
|
|
7
|
+
- `WebhookDispatcher`: HTTP delivery with retries/backoff
|
|
8
|
+
- `SecurityManager`: HMAC signature generation/verification
|
|
9
|
+
- Zod schemas: `WebhookEventSchema`, `WebhookEndpointSchema`, `WebhookDeliverySchema`
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
-
- 🔒 **보안**: HMAC 서명 및 타임스탬프 검증
|
|
10
|
-
- 📊 **모니터링**: 전송 통계 및 실패 추적
|
|
11
|
-
- 🎯 **필터링**: 이벤트 타입 및 메타데이터 기반 필터링
|
|
12
|
-
- 📦 **배치 처리**: 대량 이벤트 효율적 처리
|
|
11
|
+
Note:
|
|
12
|
+
- The default `WebhookService` storage is in-memory. For persistence/advanced workflows, see the exported building blocks such as `EndpointManager` and `DeliveryStore`.
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Install
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npm install @k-msg/webhook
|
|
18
|
-
#
|
|
19
|
-
bun add @k-msg/webhook
|
|
17
|
+
npm install @k-msg/webhook
|
|
18
|
+
# or
|
|
19
|
+
bun add @k-msg/webhook
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
##
|
|
22
|
+
## Quickstart (WebhookService)
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
```ts
|
|
25
|
+
import {
|
|
26
|
+
WebhookEventType,
|
|
27
|
+
WebhookService,
|
|
28
|
+
type WebhookConfig,
|
|
29
|
+
} from "@k-msg/webhook";
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
WebhookService,
|
|
29
|
-
WebhookRegistry,
|
|
30
|
-
WebhookDispatcher,
|
|
31
|
-
SecurityManager,
|
|
32
|
-
RetryManager,
|
|
33
|
-
WebhookEventType
|
|
34
|
-
} from '@k-msg/webhook-system';
|
|
35
|
-
|
|
36
|
-
// 웹훅 시스템 초기화
|
|
37
|
-
const registry = new WebhookRegistry();
|
|
38
|
-
const securityManager = new SecurityManager();
|
|
39
|
-
const retryManager = new RetryManager();
|
|
40
|
-
const dispatcher = new WebhookDispatcher(registry, securityManager, retryManager);
|
|
41
|
-
|
|
42
|
-
const webhookService = new WebhookService({
|
|
31
|
+
const config: WebhookConfig = {
|
|
43
32
|
maxRetries: 3,
|
|
44
33
|
retryDelayMs: 1000,
|
|
45
|
-
|
|
34
|
+
// Optional: maxDelayMs, backoffMultiplier, jitter
|
|
35
|
+
timeoutMs: 30_000,
|
|
46
36
|
enableSecurity: true,
|
|
47
|
-
|
|
37
|
+
// Optional: used when an endpoint does not provide its own secret
|
|
38
|
+
secretKey: process.env.WEBHOOK_SECRET,
|
|
39
|
+
// Optional: algorithm, signatureHeader, signaturePrefix
|
|
48
40
|
enabledEvents: [
|
|
49
41
|
WebhookEventType.MESSAGE_SENT,
|
|
50
42
|
WebhookEventType.MESSAGE_DELIVERED,
|
|
51
|
-
WebhookEventType.
|
|
43
|
+
WebhookEventType.MESSAGE_FAILED,
|
|
52
44
|
],
|
|
53
45
|
batchSize: 10,
|
|
54
|
-
batchTimeoutMs:
|
|
55
|
-
});
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### 2. 웹훅 엔드포인트 등록
|
|
59
|
-
|
|
60
|
-
```typescript
|
|
61
|
-
const endpoint = {
|
|
62
|
-
id: 'endpoint-1',
|
|
63
|
-
url: 'https://your-app.com/webhooks',
|
|
64
|
-
name: 'My App Webhook',
|
|
65
|
-
description: 'Receives message events',
|
|
66
|
-
active: true,
|
|
67
|
-
events: [
|
|
68
|
-
WebhookEventType.MESSAGE_SENT,
|
|
69
|
-
WebhookEventType.MESSAGE_DELIVERED,
|
|
70
|
-
WebhookEventType.MESSAGE_FAILED
|
|
71
|
-
],
|
|
72
|
-
secret: 'your-endpoint-secret',
|
|
73
|
-
retryConfig: {
|
|
74
|
-
maxRetries: 5,
|
|
75
|
-
retryDelayMs: 2000,
|
|
76
|
-
backoffMultiplier: 2
|
|
77
|
-
},
|
|
78
|
-
filters: {
|
|
79
|
-
providerId: ['iwinv', 'aligo'],
|
|
80
|
-
channelId: ['channel-1']
|
|
81
|
-
},
|
|
82
|
-
createdAt: new Date(),
|
|
83
|
-
updatedAt: new Date(),
|
|
84
|
-
status: 'active' as const
|
|
46
|
+
batchTimeoutMs: 5_000,
|
|
85
47
|
};
|
|
86
48
|
|
|
87
|
-
|
|
88
|
-
```
|
|
49
|
+
const service = new WebhookService(config);
|
|
89
50
|
|
|
90
|
-
|
|
51
|
+
const endpoint = await service.registerEndpoint({
|
|
52
|
+
url: "https://example.com/webhooks/k-msg",
|
|
53
|
+
active: true,
|
|
54
|
+
events: [WebhookEventType.MESSAGE_SENT, WebhookEventType.MESSAGE_FAILED],
|
|
55
|
+
// Optional: endpoint-specific secret (preferred over config.secretKey)
|
|
56
|
+
secret: process.env.WEBHOOK_SECRET,
|
|
57
|
+
// Optional: per-endpoint retry overrides
|
|
58
|
+
retryConfig: { maxRetries: 5, retryDelayMs: 1000, backoffMultiplier: 2 },
|
|
59
|
+
// Optional: metadata-based filters
|
|
60
|
+
filters: { providerId: ["iwinv", "solapi"] },
|
|
61
|
+
});
|
|
91
62
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id: 'evt_123',
|
|
63
|
+
// Asynchronous emit (batched)
|
|
64
|
+
await service.emit({
|
|
65
|
+
id: crypto.randomUUID(),
|
|
96
66
|
type: WebhookEventType.MESSAGE_SENT,
|
|
97
67
|
timestamp: new Date(),
|
|
98
|
-
data: {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
phoneNumber: '01012345678',
|
|
102
|
-
status: 'sent'
|
|
103
|
-
},
|
|
104
|
-
metadata: {
|
|
105
|
-
providerId: 'iwinv',
|
|
106
|
-
channelId: 'channel-1',
|
|
107
|
-
templateId: 'tmpl_789',
|
|
108
|
-
messageId: 'msg_456',
|
|
109
|
-
correlationId: 'req_abc'
|
|
110
|
-
},
|
|
111
|
-
version: '1.0'
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
await webhookService.dispatchEvent(event);
|
|
115
|
-
|
|
116
|
-
// 배치 이벤트 발송
|
|
117
|
-
const events = [event1, event2, event3];
|
|
118
|
-
await webhookService.dispatchEvents(events);
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
## 📋 이벤트 타입
|
|
122
|
-
|
|
123
|
-
웹훅 시스템은 다음과 같은 이벤트 타입을 지원합니다:
|
|
124
|
-
|
|
125
|
-
### 메시지 이벤트
|
|
126
|
-
|
|
127
|
-
- `message.sent` - 메시지 발송 완료
|
|
128
|
-
- `message.delivered` - 메시지 전달 완료
|
|
129
|
-
- `message.failed` - 메시지 발송 실패
|
|
130
|
-
- `message.clicked` - 메시지 클릭
|
|
131
|
-
- `message.read` - 메시지 읽음
|
|
132
|
-
|
|
133
|
-
### 템플릿 이벤트
|
|
134
|
-
|
|
135
|
-
- `template.created` - 템플릿 생성
|
|
136
|
-
- `template.approved` - 템플릿 승인
|
|
137
|
-
- `template.rejected` - 템플릿 거부
|
|
138
|
-
- `template.updated` - 템플릿 수정
|
|
139
|
-
- `template.deleted` - 템플릿 삭제
|
|
140
|
-
|
|
141
|
-
### 채널 이벤트
|
|
142
|
-
|
|
143
|
-
- `channel.created` - 채널 생성
|
|
144
|
-
- `channel.verified` - 채널 인증
|
|
145
|
-
- `sender_number.added` - 발신번호 추가
|
|
146
|
-
- `sender_number.verified` - 발신번호 인증
|
|
147
|
-
|
|
148
|
-
### 시스템 이벤트
|
|
149
|
-
|
|
150
|
-
- `system.quota_warning` - 할당량 경고
|
|
151
|
-
- `system.quota_exceeded` - 할당량 초과
|
|
152
|
-
- `system.provider_error` - 프로바이더 오류
|
|
153
|
-
- `system.maintenance` - 시스템 점검
|
|
154
|
-
|
|
155
|
-
### 분석 이벤트
|
|
156
|
-
|
|
157
|
-
- `analytics.anomaly_detected` - 이상 징후 감지
|
|
158
|
-
- `analytics.threshold_exceeded` - 임계값 초과
|
|
159
|
-
|
|
160
|
-
## 🔒 보안
|
|
161
|
-
|
|
162
|
-
### HMAC 서명 검증
|
|
163
|
-
|
|
164
|
-
웹훅 요청은 HMAC-SHA256으로 서명됩니다:
|
|
165
|
-
|
|
166
|
-
```typescript
|
|
167
|
-
// 서명 생성 (자동)
|
|
168
|
-
const securityManager = new SecurityManager({
|
|
169
|
-
algorithm: 'sha256',
|
|
170
|
-
header: 'X-Webhook-Signature',
|
|
171
|
-
prefix: 'sha256='
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// 수신측에서 서명 검증
|
|
175
|
-
const payload = req.body;
|
|
176
|
-
const signature = req.headers['X-Webhook-Signature'];
|
|
177
|
-
const secret = process.env.WEBHOOK_SECRET;
|
|
178
|
-
|
|
179
|
-
const isValid = securityManager.verifySignature(payload, signature, secret);
|
|
180
|
-
if (!isValid) {
|
|
181
|
-
return res.status(401).json({ error: 'Invalid signature' });
|
|
182
|
-
}
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### 타임스탬프 검증
|
|
186
|
-
|
|
187
|
-
재생 공격을 방지하기 위한 타임스탬프 검증:
|
|
188
|
-
|
|
189
|
-
```typescript
|
|
190
|
-
const timestamp = req.headers['X-Webhook-Timestamp'];
|
|
191
|
-
const isValidTime = securityManager.verifyTimestamp(timestamp, 300); // 5분 허용
|
|
192
|
-
|
|
193
|
-
if (!isValidTime) {
|
|
194
|
-
return res.status(401).json({ error: 'Request too old' });
|
|
195
|
-
}
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
## 🔄 재시도 정책
|
|
199
|
-
|
|
200
|
-
### 설정
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
const retryManager = new RetryManager({
|
|
204
|
-
maxRetries: 3,
|
|
205
|
-
baseDelayMs: 1000,
|
|
206
|
-
maxDelayMs: 300000, // 5분
|
|
207
|
-
backoffMultiplier: 2,
|
|
208
|
-
jitter: true
|
|
68
|
+
data: { messageId: "msg_123", status: "sent" },
|
|
69
|
+
metadata: { providerId: "iwinv", messageId: "msg_123" },
|
|
70
|
+
version: "1.0",
|
|
209
71
|
});
|
|
210
|
-
```
|
|
211
72
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
- **4xx 클라이언트 오류** (429, 408 제외)
|
|
222
|
-
- **최대 재시도 횟수 초과**
|
|
223
|
-
- **성공적 응답** (2xx, 3xx)
|
|
224
|
-
|
|
225
|
-
## 📊 모니터링 및 통계
|
|
226
|
-
|
|
227
|
-
### 웹훅 통계 조회
|
|
228
|
-
|
|
229
|
-
```typescript
|
|
230
|
-
// 전체 통계
|
|
231
|
-
const stats = await webhookService.getStats();
|
|
232
|
-
console.log(stats);
|
|
233
|
-
// {
|
|
234
|
-
// totalEndpoints: 5,
|
|
235
|
-
// activeEndpoints: 4,
|
|
236
|
-
// totalDeliveries: 1250,
|
|
237
|
-
// successfulDeliveries: 1180,
|
|
238
|
-
// failedDeliveries: 70,
|
|
239
|
-
// averageLatency: 245,
|
|
240
|
-
// successRate: 94.4
|
|
241
|
-
// }
|
|
242
|
-
|
|
243
|
-
// 특정 엔드포인트 통계
|
|
244
|
-
const endpointStats = await webhookService.getEndpointStats('endpoint-1', {
|
|
245
|
-
start: new Date('2024-01-01'),
|
|
246
|
-
end: new Date('2024-01-31')
|
|
73
|
+
// Synchronous emit (returns delivery attempts)
|
|
74
|
+
const deliveries = await service.emitSync({
|
|
75
|
+
id: crypto.randomUUID(),
|
|
76
|
+
type: WebhookEventType.MESSAGE_FAILED,
|
|
77
|
+
timestamp: new Date(),
|
|
78
|
+
data: { messageId: "msg_456", status: "failed" },
|
|
79
|
+
metadata: { providerId: "solapi", messageId: "msg_456" },
|
|
80
|
+
version: "1.0",
|
|
247
81
|
});
|
|
248
|
-
```
|
|
249
82
|
|
|
250
|
-
|
|
83
|
+
// Inspect recent deliveries (in-memory)
|
|
84
|
+
const recent = await service.getDeliveries(endpoint.id);
|
|
85
|
+
console.log(deliveries.length, recent.length);
|
|
251
86
|
|
|
252
|
-
|
|
253
|
-
// 실패한 전송 내역
|
|
254
|
-
const failedDeliveries = await webhookService.getFailedDeliveries('endpoint-1');
|
|
255
|
-
|
|
256
|
-
failedDeliveries.forEach(delivery => {
|
|
257
|
-
console.log(`Delivery ${delivery.id} failed:`, delivery.attempts[0].error);
|
|
258
|
-
});
|
|
87
|
+
await service.shutdown();
|
|
259
88
|
```
|
|
260
89
|
|
|
261
|
-
|
|
90
|
+
### Endpoint Registration Behavior
|
|
262
91
|
|
|
263
|
-
|
|
92
|
+
`registerEndpoint()` validates the URL and sends a test webhook once (a `system.maintenance` event via `testEndpoint()`).
|
|
264
93
|
|
|
265
|
-
|
|
94
|
+
## Security (HMAC Signatures)
|
|
266
95
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
url: 'https://app.com/webhooks',
|
|
271
|
-
events: [WebhookEventType.MESSAGE_SENT],
|
|
272
|
-
filters: {
|
|
273
|
-
providerId: ['iwinv'], // IWINV 프로바이더만
|
|
274
|
-
channelId: ['channel-marketing'], // 마케팅 채널만
|
|
275
|
-
templateId: ['welcome-template'] // 환영 템플릿만
|
|
276
|
-
},
|
|
277
|
-
// ... 기타 설정
|
|
278
|
-
};
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### 배치 처리
|
|
96
|
+
When security is enabled and a secret is available (`endpoint.secret` or `config.secretKey`), outgoing requests include:
|
|
97
|
+
- `X-Webhook-Timestamp`: unix epoch seconds (string)
|
|
98
|
+
- `X-Webhook-Signature`: HMAC signature (default: `sha256=<hex>`)
|
|
282
99
|
|
|
283
|
-
|
|
100
|
+
Signature input is:
|
|
284
101
|
|
|
285
|
-
```typescript
|
|
286
|
-
const webhookService = new WebhookService({
|
|
287
|
-
batchSize: 50,
|
|
288
|
-
batchTimeoutMs: 10000, // 10초마다 또는 50개씩 배치 처리
|
|
289
|
-
// ... 기타 설정
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
// 배치가 자동으로 처리됨
|
|
293
|
-
await webhookService.dispatchEvent(event1);
|
|
294
|
-
await webhookService.dispatchEvent(event2);
|
|
295
|
-
// ... 더 많은 이벤트
|
|
296
102
|
```
|
|
297
|
-
|
|
298
|
-
### 엔드포인트 테스트
|
|
299
|
-
|
|
300
|
-
웹훅 엔드포인트의 연결성 테스트:
|
|
301
|
-
|
|
302
|
-
```typescript
|
|
303
|
-
const testResult = await webhookService.testEndpoint('endpoint-1');
|
|
304
|
-
|
|
305
|
-
if (testResult.success) {
|
|
306
|
-
console.log(`✅ Endpoint is healthy (${testResult.responseTime}ms)`);
|
|
307
|
-
} else {
|
|
308
|
-
console.log(`❌ Endpoint failed: ${testResult.error}`);
|
|
309
|
-
}
|
|
103
|
+
${timestamp}.${rawBody}
|
|
310
104
|
```
|
|
311
105
|
|
|
312
|
-
|
|
106
|
+
To verify a webhook, you must use the exact raw request body string.
|
|
313
107
|
|
|
314
|
-
###
|
|
108
|
+
### Hono Example
|
|
315
109
|
|
|
316
|
-
```
|
|
317
|
-
import
|
|
110
|
+
```ts
|
|
111
|
+
import { Hono } from "hono";
|
|
112
|
+
import { SecurityManager } from "@k-msg/webhook";
|
|
318
113
|
|
|
319
|
-
const app =
|
|
320
|
-
|
|
321
|
-
// 웹훅 검증 미들웨어
|
|
322
|
-
const verifyWebhook = (req, res, next) => {
|
|
323
|
-
const signature = req.headers['x-webhook-signature'];
|
|
324
|
-
const payload = JSON.stringify(req.body);
|
|
325
|
-
|
|
326
|
-
if (!securityManager.verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
|
|
327
|
-
return res.status(401).json({ error: 'Invalid signature' });
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
next();
|
|
331
|
-
};
|
|
114
|
+
const app = new Hono();
|
|
332
115
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
// TODO: 이벤트 처리 로직
|
|
339
|
-
|
|
340
|
-
res.json({ success: true });
|
|
116
|
+
const security = new SecurityManager({
|
|
117
|
+
algorithm: "sha256",
|
|
118
|
+
signatureHeader: "X-Webhook-Signature",
|
|
119
|
+
signaturePrefix: "sha256=",
|
|
341
120
|
});
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
### Hono 통합
|
|
345
121
|
|
|
346
|
-
|
|
347
|
-
|
|
122
|
+
app.post("/webhooks/k-msg", async (c) => {
|
|
123
|
+
const payload = await c.req.text();
|
|
348
124
|
|
|
349
|
-
const
|
|
125
|
+
const signature = c.req.header("X-Webhook-Signature") ?? "";
|
|
126
|
+
const timestamp = c.req.header("X-Webhook-Timestamp") ?? "";
|
|
127
|
+
const secret = process.env.WEBHOOK_SECRET ?? "";
|
|
350
128
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const payload = await c.req.text();
|
|
354
|
-
|
|
355
|
-
if (!securityManager.verifySignature(payload, signature, process.env.WEBHOOK_SECRET)) {
|
|
356
|
-
return c.json({ error: 'Invalid signature' }, 401);
|
|
129
|
+
if (!security.verifyTimestamp(timestamp, 300)) {
|
|
130
|
+
return c.json({ error: "Request too old" }, 401);
|
|
357
131
|
}
|
|
358
|
-
|
|
132
|
+
if (!security.verifySignatureWithTimestamp(payload, timestamp, signature, secret)) {
|
|
133
|
+
return c.json({ error: "Invalid signature" }, 401);
|
|
134
|
+
}
|
|
135
|
+
|
|
359
136
|
const event = JSON.parse(payload);
|
|
360
|
-
|
|
361
|
-
// TODO: 이벤트 처리
|
|
362
|
-
console.log(`Processing ${event.type}:`, event.data);
|
|
363
|
-
|
|
364
|
-
return c.json({ success: true });
|
|
137
|
+
return c.json({ ok: true, type: event.type });
|
|
365
138
|
});
|
|
366
139
|
```
|
|
367
140
|
|
|
368
|
-
##
|
|
369
|
-
|
|
370
|
-
### WebhookConfig
|
|
371
|
-
|
|
372
|
-
```typescript
|
|
373
|
-
interface WebhookConfig {
|
|
374
|
-
maxRetries: number; // 최대 재시도 횟수 (기본: 3)
|
|
375
|
-
retryDelayMs: number; // 재시도 기본 지연시간 (기본: 1000ms)
|
|
376
|
-
timeoutMs: number; // 요청 타임아웃 (기본: 30000ms)
|
|
377
|
-
enableSecurity: boolean; // 보안 기능 활성화 (기본: true)
|
|
378
|
-
secretKey?: string; // 기본 시크릿 키
|
|
379
|
-
enabledEvents: WebhookEventType[]; // 활성화할 이벤트 타입들
|
|
380
|
-
batchSize: number; // 배치 크기 (기본: 10)
|
|
381
|
-
batchTimeoutMs: number; // 배치 타임아웃 (기본: 5000ms)
|
|
382
|
-
}
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
### RetryConfig
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
interface RetryConfig {
|
|
389
|
-
maxRetries: number; // 최대 재시도 횟수
|
|
390
|
-
baseDelayMs: number; // 기본 지연시간
|
|
391
|
-
maxDelayMs: number; // 최대 지연시간
|
|
392
|
-
backoffMultiplier: number; // 백오프 배수
|
|
393
|
-
jitter: boolean; // 지터 활성화 (랜덤성 추가)
|
|
394
|
-
}
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
## 📝 예제
|
|
141
|
+
## Retries and Delivery Status
|
|
398
142
|
|
|
399
|
-
|
|
143
|
+
`WebhookDispatcher` retries failed deliveries with exponential backoff.
|
|
400
144
|
|
|
401
|
-
|
|
402
|
-
-
|
|
403
|
-
-
|
|
404
|
-
-
|
|
145
|
+
Delivery status:
|
|
146
|
+
- `success`: received a 2xx response
|
|
147
|
+
- `failed`: non-retryable failure (typically non-retryable 4xx)
|
|
148
|
+
- `exhausted`: retryable failure, but retries were used up
|
|
405
149
|
|
|
406
|
-
##
|
|
150
|
+
## Filtering
|
|
407
151
|
|
|
408
|
-
|
|
152
|
+
Endpoints can filter deliveries based on event metadata:
|
|
409
153
|
|
|
410
|
-
|
|
154
|
+
```ts
|
|
155
|
+
await service.registerEndpoint({
|
|
156
|
+
url: "https://example.com/webhooks/k-msg",
|
|
157
|
+
active: true,
|
|
158
|
+
events: [WebhookEventType.MESSAGE_SENT],
|
|
159
|
+
filters: {
|
|
160
|
+
providerId: ["iwinv"],
|
|
161
|
+
channelId: ["marketing"],
|
|
162
|
+
templateId: ["welcome-template"],
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
```
|
|
411
166
|
|
|
412
|
-
|
|
167
|
+
## Zod Schemas
|
|
413
168
|
|
|
414
|
-
|
|
169
|
+
This package exports Zod schemas for validation:
|
|
170
|
+
- `WebhookEventSchema` (timestamp is coerced from string/number to `Date`)
|
|
171
|
+
- `WebhookEndpointSchema`
|
|
172
|
+
- `WebhookDeliverySchema`
|
|
415
173
|
|
|
416
|
-
|
|
174
|
+
## License
|
|
417
175
|
|
|
418
|
-
|
|
176
|
+
MIT
|