@k-msg/webhook 0.6.0 → 0.7.0

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 CHANGED
@@ -1,418 +1,176 @@
1
- # @k-msg/webhook-system
1
+ # @k-msg/webhook
2
2
 
3
- 실시간 메시지 이벤트 알림을 위한 엔터프라이즈급 Webhook 시스템입니다.
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-system
18
- # 또는
19
- bun add @k-msg/webhook-system
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
- ### 1. 기본 설정
24
+ ```ts
25
+ import {
26
+ WebhookEventType,
27
+ WebhookService,
28
+ type WebhookConfig,
29
+ } from "@k-msg/webhook";
25
30
 
26
- ```typescript
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
- timeoutMs: 30000,
34
+ // Optional: maxDelayMs, backoffMultiplier, jitter
35
+ timeoutMs: 30_000,
46
36
  enableSecurity: true,
47
- secretKey: 'your-webhook-secret',
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.TEMPLATE_APPROVED
43
+ WebhookEventType.MESSAGE_FAILED,
52
44
  ],
53
45
  batchSize: 10,
54
- batchTimeoutMs: 5000
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
- await webhookService.registerEndpoint(endpoint);
88
- ```
49
+ const service = new WebhookService(config);
89
50
 
90
- ### 3. 이벤트 발송
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
- ```typescript
93
- // 단일 이벤트 발송
94
- const event = {
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
- messageId: 'msg_456',
100
- templateId: 'tmpl_789',
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
- - **5xx 서버 오류**: 일시적 서버 문제
216
- - **429 Too Many Requests**: 요청 제한
217
- - **408 Request Timeout**: 요청 타임아웃
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
- ```typescript
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
- ```typescript
268
- const endpoint = {
269
- id: 'filtered-endpoint',
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
- ## 🛠️ Express/Hono와 통합
106
+ To verify a webhook, you must use the exact raw request body string.
313
107
 
314
- ### Express 미들웨어
108
+ ### Hono Example
315
109
 
316
- ```typescript
317
- import express from 'express';
110
+ ```ts
111
+ import { Hono } from "hono";
112
+ import { SecurityManager } from "@k-msg/webhook";
318
113
 
319
- const app = express();
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
- app.post('/webhooks', verifyWebhook, (req, res) => {
334
- const event = req.body;
335
-
336
- console.log(`Received ${event.type} event:`, event.data);
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
- ```typescript
347
- import { Hono } from 'hono';
122
+ app.post("/webhooks/k-msg", async (c) => {
123
+ const payload = await c.req.text();
348
124
 
349
- const app = new Hono();
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
- app.post('/webhooks', async (c) => {
352
- const signature = c.req.header('X-Webhook-Signature');
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
- 전체 예제는 [examples](./examples/) 디렉토리를 참조하세요:
143
+ `WebhookDispatcher` retries failed deliveries with exponential backoff.
400
144
 
401
- - [기본 사용법](./examples/basic-usage.ts)
402
- - [Express 통합](./examples/express-integration.ts)
403
- - [대량 처리](./examples/batch-processing.ts)
404
- - [고급 필터링](./examples/advanced-filtering.ts)
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
- 기여를 환영합니다! [CONTRIBUTING.md](../../CONTRIBUTING.md)를 참조하세요.
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
- MIT License - [LICENSE](../../LICENSE) 파일을 참조하세요.
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
- **K-Message** - 한국형 멀티채널 메시징 플랫폼
174
+ ## License
417
175
 
418
- 🌟 [GitHub](https://github.com/imjlk/k-msg) | 📖 [Documentation](https://k-msg.dev) | 💬 [Discord](https://discord.gg/k-msg)
176
+ MIT