@k-msg/webhook 0.19.1 → 0.20.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 +113 -124
- package/README_ko.md +115 -125
- package/dist/adapters/cloudflare/d1-client.d.ts +21 -0
- package/dist/adapters/cloudflare/d1-delivery.store.d.ts +12 -0
- package/dist/adapters/cloudflare/d1-endpoint.store.d.ts +15 -0
- package/dist/adapters/cloudflare/index.d.ts +10 -0
- package/dist/adapters/cloudflare/index.js +82 -0
- package/dist/adapters/cloudflare/index.js.map +86 -0
- package/dist/adapters/cloudflare/index.mjs +82 -0
- package/dist/adapters/cloudflare/index.mjs.map +86 -0
- package/dist/adapters/cloudflare/sql-schema.d.ts +9 -0
- package/dist/dispatcher/load-balancer.d.ts +1 -0
- package/dist/index.d.ts +7 -14
- package/dist/index.js +23 -31
- package/dist/index.js.map +17 -24
- package/dist/index.mjs +23 -31
- package/dist/index.mjs.map +17 -24
- package/dist/runtime/endpoint-validation.d.ts +8 -0
- package/dist/runtime/event-matcher.d.ts +2 -0
- package/dist/runtime/persistence.d.ts +16 -0
- package/dist/runtime/types.d.ts +60 -0
- package/dist/runtime/webhook-runtime.service.d.ts +41 -0
- package/dist/services/webhook.dispatcher.d.ts +4 -2
- package/dist/toolkit/index.d.ts +16 -0
- package/dist/toolkit/index.js +52 -0
- package/dist/toolkit/index.js.map +100 -0
- package/dist/toolkit/index.mjs +52 -0
- package/dist/toolkit/index.mjs.map +100 -0
- package/package.json +14 -4
package/README.md
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Canonical docs: [k-msg.and.guide](https://k-msg.and.guide)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Runtime-first webhook package for message events.
|
|
6
6
|
|
|
7
|
-
This package
|
|
8
|
-
- `WebhookService`: a convenience facade (in-memory endpoint registry + batching)
|
|
9
|
-
- `WebhookDispatcher`: HTTP delivery with retries/backoff
|
|
10
|
-
- `SecurityManager`: HMAC signature generation/verification
|
|
11
|
-
- Zod schemas: `WebhookEventSchema`, `WebhookEndpointSchema`, `WebhookDeliverySchema`
|
|
7
|
+
This package now follows a DX-first flow:
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
1. Start in 5 minutes with in-memory persistence
|
|
10
|
+
2. Move to production by swapping persistence to D1
|
|
11
|
+
3. Extend to SQLite/Drizzle(Postgres) with the same store contract
|
|
16
12
|
|
|
17
13
|
## Install
|
|
18
14
|
|
|
@@ -22,50 +18,56 @@ npm install @k-msg/webhook
|
|
|
22
18
|
bun add @k-msg/webhook
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
##
|
|
21
|
+
## Runtime API (root)
|
|
22
|
+
|
|
23
|
+
`@k-msg/webhook` root exports runtime-only APIs:
|
|
24
|
+
|
|
25
|
+
- `WebhookRuntimeService`
|
|
26
|
+
- `createInMemoryWebhookPersistence`
|
|
27
|
+
- `addEndpoints`, `probeEndpoint`
|
|
28
|
+
- `validateEndpointUrl`
|
|
29
|
+
|
|
30
|
+
Advanced building blocks are now exposed from subpaths:
|
|
31
|
+
|
|
32
|
+
- `@k-msg/webhook/toolkit`
|
|
33
|
+
- `@k-msg/webhook/adapters/cloudflare`
|
|
34
|
+
|
|
35
|
+
## Quickstart (in-memory)
|
|
26
36
|
|
|
27
37
|
```ts
|
|
28
|
-
import { readRuntimeEnv } from "@k-msg/core";
|
|
29
38
|
import {
|
|
30
39
|
WebhookEventType,
|
|
31
|
-
|
|
40
|
+
WebhookRuntimeService,
|
|
41
|
+
createInMemoryWebhookPersistence,
|
|
32
42
|
type WebhookConfig,
|
|
33
43
|
} from "@k-msg/webhook";
|
|
34
44
|
|
|
35
45
|
const config: WebhookConfig = {
|
|
36
46
|
maxRetries: 3,
|
|
37
|
-
retryDelayMs:
|
|
38
|
-
// Optional: maxDelayMs, backoffMultiplier, jitter
|
|
47
|
+
retryDelayMs: 1_000,
|
|
39
48
|
timeoutMs: 30_000,
|
|
40
|
-
enableSecurity:
|
|
41
|
-
// Optional: used when an endpoint does not provide its own secret
|
|
42
|
-
secretKey: readRuntimeEnv("WEBHOOK_SECRET"),
|
|
43
|
-
// Optional: algorithm, signatureHeader, signaturePrefix
|
|
49
|
+
enableSecurity: false,
|
|
44
50
|
enabledEvents: [
|
|
45
51
|
WebhookEventType.MESSAGE_SENT,
|
|
46
|
-
WebhookEventType.MESSAGE_DELIVERED,
|
|
47
52
|
WebhookEventType.MESSAGE_FAILED,
|
|
53
|
+
WebhookEventType.SYSTEM_MAINTENANCE,
|
|
48
54
|
],
|
|
49
55
|
batchSize: 10,
|
|
50
56
|
batchTimeoutMs: 5_000,
|
|
51
57
|
};
|
|
52
58
|
|
|
53
|
-
const
|
|
59
|
+
const runtime = new WebhookRuntimeService({
|
|
60
|
+
delivery: config,
|
|
61
|
+
persistence: createInMemoryWebhookPersistence(),
|
|
62
|
+
});
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
await runtime.addEndpoint({
|
|
56
65
|
url: "https://example.com/webhooks/k-msg",
|
|
57
66
|
active: true,
|
|
58
67
|
events: [WebhookEventType.MESSAGE_SENT, WebhookEventType.MESSAGE_FAILED],
|
|
59
|
-
// Optional: endpoint-specific secret (preferred over config.secretKey)
|
|
60
|
-
secret: readRuntimeEnv("WEBHOOK_SECRET"),
|
|
61
|
-
// Optional: per-endpoint retry overrides
|
|
62
|
-
retryConfig: { maxRetries: 5, retryDelayMs: 1000, backoffMultiplier: 2 },
|
|
63
|
-
// Optional: metadata-based filters
|
|
64
|
-
filters: { providerId: ["iwinv", "solapi"] },
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
await service.emit({
|
|
70
|
+
await runtime.emitSync({
|
|
69
71
|
id: crypto.randomUUID(),
|
|
70
72
|
type: WebhookEventType.MESSAGE_SENT,
|
|
71
73
|
timestamp: new Date(),
|
|
@@ -74,132 +76,119 @@ await service.emit({
|
|
|
74
76
|
version: "1.0",
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
const deliveries = await service.emitSync({
|
|
79
|
-
id: crypto.randomUUID(),
|
|
80
|
-
type: WebhookEventType.MESSAGE_FAILED,
|
|
81
|
-
timestamp: new Date(),
|
|
82
|
-
data: { messageId: "msg_456", status: "failed" },
|
|
83
|
-
metadata: { providerId: "solapi", messageId: "msg_456" },
|
|
84
|
-
version: "1.0",
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// Inspect recent deliveries (in-memory)
|
|
88
|
-
const recent = await service.getDeliveries(endpoint.id);
|
|
89
|
-
console.log(deliveries.length, recent.length);
|
|
90
|
-
|
|
91
|
-
await service.shutdown();
|
|
79
|
+
await runtime.shutdown();
|
|
92
80
|
```
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
## D1 quickstart (same runtime API)
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
WebhookEventType,
|
|
87
|
+
WebhookRuntimeService,
|
|
88
|
+
type WebhookConfig,
|
|
89
|
+
} from "@k-msg/webhook";
|
|
90
|
+
import { createD1WebhookPersistence } from "@k-msg/webhook/adapters/cloudflare";
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
type Env = {
|
|
93
|
+
DB: D1Database;
|
|
94
|
+
};
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
const config: WebhookConfig = {
|
|
97
|
+
maxRetries: 3,
|
|
98
|
+
retryDelayMs: 1_000,
|
|
99
|
+
timeoutMs: 30_000,
|
|
100
|
+
enableSecurity: false,
|
|
101
|
+
enabledEvents: [WebhookEventType.MESSAGE_SENT, WebhookEventType.MESSAGE_FAILED],
|
|
102
|
+
batchSize: 10,
|
|
103
|
+
batchTimeoutMs: 5_000,
|
|
104
|
+
};
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
function createRuntime(env: Env): WebhookRuntimeService {
|
|
107
|
+
return new WebhookRuntimeService({
|
|
108
|
+
delivery: config,
|
|
109
|
+
persistence: createD1WebhookPersistence(env.DB),
|
|
110
|
+
security: {
|
|
111
|
+
allowPrivateHosts: true,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
108
115
|
```
|
|
109
116
|
|
|
110
|
-
|
|
117
|
+
`createD1WebhookPersistence()` initializes schema automatically by default.
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
## Schema helpers (Cloudflare)
|
|
113
120
|
|
|
114
121
|
```ts
|
|
115
|
-
import {
|
|
116
|
-
|
|
117
|
-
|
|
122
|
+
import {
|
|
123
|
+
buildWebhookSchemaSql,
|
|
124
|
+
initializeWebhookSchema,
|
|
125
|
+
} from "@k-msg/webhook/adapters/cloudflare";
|
|
118
126
|
|
|
119
|
-
const
|
|
127
|
+
const statements = buildWebhookSchemaSql();
|
|
128
|
+
// run statements in your migration system, or:
|
|
129
|
+
await initializeWebhookSchema(env.DB);
|
|
130
|
+
```
|
|
120
131
|
|
|
121
|
-
|
|
122
|
-
algorithm: "sha256",
|
|
123
|
-
signatureHeader: "X-Webhook-Signature",
|
|
124
|
-
signaturePrefix: "sha256=",
|
|
125
|
-
});
|
|
132
|
+
## SQLite / Drizzle(Postgres) snippets
|
|
126
133
|
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
`WebhookRuntimeService` accepts custom stores via `endpointStore` + `deliveryStore`.
|
|
135
|
+
Implement the same interfaces to plug any backend:
|
|
129
136
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
```ts
|
|
138
|
+
import type {
|
|
139
|
+
WebhookDeliveryStore,
|
|
140
|
+
WebhookEndpointStore,
|
|
141
|
+
} from "@k-msg/webhook";
|
|
133
142
|
|
|
134
|
-
|
|
135
|
-
|
|
143
|
+
class SqliteEndpointStore implements WebhookEndpointStore {
|
|
144
|
+
async add() {}
|
|
145
|
+
async update() {}
|
|
146
|
+
async remove() {}
|
|
147
|
+
async get() {
|
|
148
|
+
return null;
|
|
136
149
|
}
|
|
137
|
-
|
|
138
|
-
return
|
|
150
|
+
async list() {
|
|
151
|
+
return [];
|
|
139
152
|
}
|
|
153
|
+
}
|
|
140
154
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
155
|
+
class SqliteDeliveryStore implements WebhookDeliveryStore {
|
|
156
|
+
async add() {}
|
|
157
|
+
async list() {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
144
161
|
```
|
|
145
162
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
`WebhookDispatcher` retries failed deliveries with exponential backoff.
|
|
149
|
-
|
|
150
|
-
Delivery status:
|
|
151
|
-
- `success`: received a 2xx response
|
|
152
|
-
- `failed`: non-retryable failure (typically non-retryable 4xx)
|
|
153
|
-
- `exhausted`: retryable failure, but retries were used up
|
|
154
|
-
|
|
155
|
-
## Filtering
|
|
156
|
-
|
|
157
|
-
Endpoints can filter deliveries based on event metadata:
|
|
163
|
+
Then wire it without changing runtime logic:
|
|
158
164
|
|
|
159
165
|
```ts
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
filters: {
|
|
165
|
-
providerId: ["iwinv"],
|
|
166
|
-
channelId: ["marketing"],
|
|
167
|
-
templateId: ["welcome-template"],
|
|
168
|
-
},
|
|
166
|
+
const runtime = new WebhookRuntimeService({
|
|
167
|
+
delivery: config,
|
|
168
|
+
endpointStore: new SqliteEndpointStore(),
|
|
169
|
+
deliveryStore: new SqliteDeliveryStore(),
|
|
169
170
|
});
|
|
170
171
|
```
|
|
171
172
|
|
|
172
|
-
##
|
|
173
|
+
## Security defaults
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
175
|
+
- Private hosts are blocked by default
|
|
176
|
+
- `http://localhost` style URLs require explicit allowance (runtime security options)
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
import { DeliveryStore, type FileStorageAdapter } from "@k-msg/webhook";
|
|
179
|
-
import * as fs from "node:fs/promises";
|
|
180
|
-
import * as path from "node:path";
|
|
181
|
-
|
|
182
|
-
const nodeFileAdapter: FileStorageAdapter = {
|
|
183
|
-
appendFile: (filePath, data) => fs.appendFile(filePath, data, "utf8"),
|
|
184
|
-
readFile: (filePath) => fs.readFile(filePath, "utf8"),
|
|
185
|
-
writeFile: (filePath, data) => fs.writeFile(filePath, data, "utf8"),
|
|
186
|
-
ensureDirForFile: (filePath) =>
|
|
187
|
-
fs.mkdir(path.dirname(filePath), { recursive: true }),
|
|
188
|
-
};
|
|
178
|
+
## Migration notes (breaking)
|
|
189
179
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
180
|
+
| Old usage | New usage |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `WebhookService` (root) | `WebhookRuntimeService` (root) |
|
|
183
|
+
| `registerEndpoint()` auto test call | `addEndpoint()` only; test with `probeEndpoint()` |
|
|
184
|
+
| Advanced classes from root | import from `@k-msg/webhook/toolkit` |
|
|
185
|
+
| Cloudflare persistence from custom wiring | use `@k-msg/webhook/adapters/cloudflare` |
|
|
196
186
|
|
|
197
|
-
##
|
|
187
|
+
## Toolkit subpath
|
|
198
188
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
- `WebhookDeliverySchema`
|
|
189
|
+
```ts
|
|
190
|
+
import { LoadBalancer, QueueManager } from "@k-msg/webhook/toolkit";
|
|
191
|
+
```
|
|
203
192
|
|
|
204
193
|
## License
|
|
205
194
|
|
package/README_ko.md
CHANGED
|
@@ -2,17 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> 공식 문서: [k-msg.and.guide](https://k-msg.and.guide)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
메시지 이벤트 웹훅 전송을 위한 runtime 중심 패키지입니다.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- `WebhookService`: 편의용 Facade (in-memory 엔드포인트 레지스트리 + 배치 처리)
|
|
9
|
-
- `WebhookDispatcher`: HTTP 전송 + 재시도(백오프)
|
|
10
|
-
- `SecurityManager`: HMAC 서명 생성/검증
|
|
11
|
-
- Zod 스키마: `WebhookEventSchema`, `WebhookEndpointSchema`, `WebhookDeliverySchema`
|
|
7
|
+
이번 구조는 DX 기준으로 다음 3단계에 맞춰 설계되었습니다.
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
1. in-memory로 5분 내 시작
|
|
10
|
+
2. 서비스 코드 변경 없이 D1으로 전환
|
|
11
|
+
3. SQLite/Drizzle(Postgres) 확장
|
|
16
12
|
|
|
17
13
|
## 설치
|
|
18
14
|
|
|
@@ -22,50 +18,56 @@ npm install @k-msg/webhook
|
|
|
22
18
|
bun add @k-msg/webhook
|
|
23
19
|
```
|
|
24
20
|
|
|
25
|
-
##
|
|
21
|
+
## Runtime API (루트)
|
|
22
|
+
|
|
23
|
+
`@k-msg/webhook` 루트는 runtime API만 제공합니다.
|
|
24
|
+
|
|
25
|
+
- `WebhookRuntimeService`
|
|
26
|
+
- `createInMemoryWebhookPersistence`
|
|
27
|
+
- `addEndpoints`, `probeEndpoint`
|
|
28
|
+
- `validateEndpointUrl`
|
|
29
|
+
|
|
30
|
+
고급 빌딩 블록은 subpath로 분리되었습니다.
|
|
31
|
+
|
|
32
|
+
- `@k-msg/webhook/toolkit`
|
|
33
|
+
- `@k-msg/webhook/adapters/cloudflare`
|
|
34
|
+
|
|
35
|
+
## 빠른 시작 (in-memory)
|
|
26
36
|
|
|
27
37
|
```ts
|
|
28
|
-
import { readRuntimeEnv } from "@k-msg/core";
|
|
29
38
|
import {
|
|
30
39
|
WebhookEventType,
|
|
31
|
-
|
|
40
|
+
WebhookRuntimeService,
|
|
41
|
+
createInMemoryWebhookPersistence,
|
|
32
42
|
type WebhookConfig,
|
|
33
43
|
} from "@k-msg/webhook";
|
|
34
44
|
|
|
35
45
|
const config: WebhookConfig = {
|
|
36
46
|
maxRetries: 3,
|
|
37
|
-
retryDelayMs:
|
|
38
|
-
// Optional: maxDelayMs, backoffMultiplier, jitter
|
|
47
|
+
retryDelayMs: 1_000,
|
|
39
48
|
timeoutMs: 30_000,
|
|
40
|
-
enableSecurity:
|
|
41
|
-
// Optional: 엔드포인트에 secret이 없을 때 fallback으로 사용됩니다.
|
|
42
|
-
secretKey: readRuntimeEnv("WEBHOOK_SECRET"),
|
|
43
|
-
// Optional: algorithm, signatureHeader, signaturePrefix
|
|
49
|
+
enableSecurity: false,
|
|
44
50
|
enabledEvents: [
|
|
45
51
|
WebhookEventType.MESSAGE_SENT,
|
|
46
|
-
WebhookEventType.MESSAGE_DELIVERED,
|
|
47
52
|
WebhookEventType.MESSAGE_FAILED,
|
|
53
|
+
WebhookEventType.SYSTEM_MAINTENANCE,
|
|
48
54
|
],
|
|
49
55
|
batchSize: 10,
|
|
50
56
|
batchTimeoutMs: 5_000,
|
|
51
57
|
};
|
|
52
58
|
|
|
53
|
-
const
|
|
59
|
+
const runtime = new WebhookRuntimeService({
|
|
60
|
+
delivery: config,
|
|
61
|
+
persistence: createInMemoryWebhookPersistence(),
|
|
62
|
+
});
|
|
54
63
|
|
|
55
|
-
|
|
64
|
+
await runtime.addEndpoint({
|
|
56
65
|
url: "https://example.com/webhooks/k-msg",
|
|
57
66
|
active: true,
|
|
58
67
|
events: [WebhookEventType.MESSAGE_SENT, WebhookEventType.MESSAGE_FAILED],
|
|
59
|
-
// Optional: 엔드포인트별 secret (config.secretKey보다 우선)
|
|
60
|
-
secret: readRuntimeEnv("WEBHOOK_SECRET"),
|
|
61
|
-
// Optional: 엔드포인트별 재시도 설정
|
|
62
|
-
retryConfig: { maxRetries: 5, retryDelayMs: 1000, backoffMultiplier: 2 },
|
|
63
|
-
// Optional: 메타데이터 기반 필터
|
|
64
|
-
filters: { providerId: ["iwinv", "solapi"] },
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
|
|
68
|
-
await service.emit({
|
|
70
|
+
await runtime.emitSync({
|
|
69
71
|
id: crypto.randomUUID(),
|
|
70
72
|
type: WebhookEventType.MESSAGE_SENT,
|
|
71
73
|
timestamp: new Date(),
|
|
@@ -74,131 +76,119 @@ await service.emit({
|
|
|
74
76
|
version: "1.0",
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
const deliveries = await service.emitSync({
|
|
79
|
-
id: crypto.randomUUID(),
|
|
80
|
-
type: WebhookEventType.MESSAGE_FAILED,
|
|
81
|
-
timestamp: new Date(),
|
|
82
|
-
data: { messageId: "msg_456", status: "failed" },
|
|
83
|
-
metadata: { providerId: "solapi", messageId: "msg_456" },
|
|
84
|
-
version: "1.0",
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// 최근 딜리버리 조회(in-memory)
|
|
88
|
-
const recent = await service.getDeliveries(endpoint.id);
|
|
89
|
-
console.log(deliveries.length, recent.length);
|
|
90
|
-
|
|
91
|
-
await service.shutdown();
|
|
79
|
+
await runtime.shutdown();
|
|
92
80
|
```
|
|
93
81
|
|
|
94
|
-
|
|
82
|
+
## D1 전환 (동일 API)
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
84
|
+
```ts
|
|
85
|
+
import {
|
|
86
|
+
WebhookEventType,
|
|
87
|
+
WebhookRuntimeService,
|
|
88
|
+
type WebhookConfig,
|
|
89
|
+
} from "@k-msg/webhook";
|
|
90
|
+
import { createD1WebhookPersistence } from "@k-msg/webhook/adapters/cloudflare";
|
|
99
91
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
type Env = {
|
|
93
|
+
DB: D1Database;
|
|
94
|
+
};
|
|
103
95
|
|
|
104
|
-
|
|
96
|
+
const config: WebhookConfig = {
|
|
97
|
+
maxRetries: 3,
|
|
98
|
+
retryDelayMs: 1_000,
|
|
99
|
+
timeoutMs: 30_000,
|
|
100
|
+
enableSecurity: false,
|
|
101
|
+
enabledEvents: [WebhookEventType.MESSAGE_SENT, WebhookEventType.MESSAGE_FAILED],
|
|
102
|
+
batchSize: 10,
|
|
103
|
+
batchTimeoutMs: 5_000,
|
|
104
|
+
};
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
function createRuntime(env: Env): WebhookRuntimeService {
|
|
107
|
+
return new WebhookRuntimeService({
|
|
108
|
+
delivery: config,
|
|
109
|
+
persistence: createD1WebhookPersistence(env.DB),
|
|
110
|
+
security: {
|
|
111
|
+
allowPrivateHosts: true,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
108
115
|
```
|
|
109
116
|
|
|
110
|
-
|
|
117
|
+
`createD1WebhookPersistence()`는 기본값으로 스키마 초기화를 자동 수행합니다.
|
|
111
118
|
|
|
112
|
-
|
|
119
|
+
## Cloudflare 스키마 헬퍼
|
|
113
120
|
|
|
114
121
|
```ts
|
|
115
|
-
import {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
122
|
+
import {
|
|
123
|
+
buildWebhookSchemaSql,
|
|
124
|
+
initializeWebhookSchema,
|
|
125
|
+
} from "@k-msg/webhook/adapters/cloudflare";
|
|
126
|
+
|
|
127
|
+
const statements = buildWebhookSchemaSql();
|
|
128
|
+
// 마이그레이션 시스템에서 실행하거나:
|
|
129
|
+
await initializeWebhookSchema(env.DB);
|
|
130
|
+
```
|
|
125
131
|
|
|
126
|
-
|
|
127
|
-
const payload = await c.req.text();
|
|
132
|
+
## SQLite / Drizzle(Postgres) 스니펫
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const secret = readRuntimeEnv("WEBHOOK_SECRET") ?? "";
|
|
134
|
+
`WebhookRuntimeService`는 `endpointStore` + `deliveryStore` 주입을 지원합니다.
|
|
135
|
+
동일 인터페이스만 구현하면 백엔드를 교체할 수 있습니다.
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
137
|
+
```ts
|
|
138
|
+
import type {
|
|
139
|
+
WebhookDeliveryStore,
|
|
140
|
+
WebhookEndpointStore,
|
|
141
|
+
} from "@k-msg/webhook";
|
|
142
|
+
|
|
143
|
+
class SqliteEndpointStore implements WebhookEndpointStore {
|
|
144
|
+
async add() {}
|
|
145
|
+
async update() {}
|
|
146
|
+
async remove() {}
|
|
147
|
+
async get() {
|
|
148
|
+
return null;
|
|
135
149
|
}
|
|
136
|
-
|
|
137
|
-
return
|
|
150
|
+
async list() {
|
|
151
|
+
return [];
|
|
138
152
|
}
|
|
153
|
+
}
|
|
139
154
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
class SqliteDeliveryStore implements WebhookDeliveryStore {
|
|
156
|
+
async add() {}
|
|
157
|
+
async list() {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
143
161
|
```
|
|
144
162
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
`WebhookDispatcher`는 실패한 전송을 지수 백오프로 재시도합니다.
|
|
148
|
-
|
|
149
|
-
딜리버리 상태:
|
|
150
|
-
- `success`: 2xx 응답을 받음
|
|
151
|
-
- `failed`: 재시도 대상이 아닌 실패(주로 non-retryable 4xx)
|
|
152
|
-
- `exhausted`: 재시도 가능한 실패였지만, 재시도 횟수 소진
|
|
153
|
-
|
|
154
|
-
## 필터링
|
|
155
|
-
|
|
156
|
-
엔드포인트는 이벤트 메타데이터 기반으로 전달을 필터링할 수 있습니다.
|
|
163
|
+
런타임 연결 코드는 동일합니다.
|
|
157
164
|
|
|
158
165
|
```ts
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
filters: {
|
|
164
|
-
providerId: ["iwinv"],
|
|
165
|
-
channelId: ["marketing"],
|
|
166
|
-
templateId: ["welcome-template"],
|
|
167
|
-
},
|
|
166
|
+
const runtime = new WebhookRuntimeService({
|
|
167
|
+
delivery: config,
|
|
168
|
+
endpointStore: new SqliteEndpointStore(),
|
|
169
|
+
deliveryStore: new SqliteDeliveryStore(),
|
|
168
170
|
});
|
|
169
171
|
```
|
|
170
172
|
|
|
171
|
-
##
|
|
173
|
+
## 보안 기본값
|
|
172
174
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
- 기본적으로 private host 차단
|
|
176
|
+
- `http://localhost` 류 URL은 옵션으로 명시 허용해야 사용 가능
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
import { DeliveryStore, type FileStorageAdapter } from "@k-msg/webhook";
|
|
178
|
-
import * as fs from "node:fs/promises";
|
|
179
|
-
import * as path from "node:path";
|
|
180
|
-
|
|
181
|
-
const nodeFileAdapter: FileStorageAdapter = {
|
|
182
|
-
appendFile: (filePath, data) => fs.appendFile(filePath, data, "utf8"),
|
|
183
|
-
readFile: (filePath) => fs.readFile(filePath, "utf8"),
|
|
184
|
-
writeFile: (filePath, data) => fs.writeFile(filePath, data, "utf8"),
|
|
185
|
-
ensureDirForFile: (filePath) =>
|
|
186
|
-
fs.mkdir(path.dirname(filePath), { recursive: true }),
|
|
187
|
-
};
|
|
178
|
+
## 마이그레이션 (브레이킹)
|
|
188
179
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
180
|
+
| 기존 | 변경 |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| 루트 `WebhookService` | 루트 `WebhookRuntimeService` |
|
|
183
|
+
| `registerEndpoint()` 시 자동 테스트 전송 | `addEndpoint()` + 필요 시 `probeEndpoint()` |
|
|
184
|
+
| 고급 클래스 루트 import | `@k-msg/webhook/toolkit`에서 import |
|
|
185
|
+
| Cloudflare persistence 수동 구성 | `@k-msg/webhook/adapters/cloudflare` 사용 |
|
|
195
186
|
|
|
196
|
-
##
|
|
187
|
+
## Toolkit subpath
|
|
197
188
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
- `WebhookDeliverySchema`
|
|
189
|
+
```ts
|
|
190
|
+
import { LoadBalancer, QueueManager } from "@k-msg/webhook/toolkit";
|
|
191
|
+
```
|
|
202
192
|
|
|
203
193
|
## License
|
|
204
194
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type D1Row = Record<string, unknown>;
|
|
2
|
+
export interface D1PreparedStatementLike {
|
|
3
|
+
bind(...values: unknown[]): D1PreparedStatementLike;
|
|
4
|
+
first<T extends D1Row = D1Row>(): Promise<T | null>;
|
|
5
|
+
all<T extends D1Row = D1Row>(): Promise<{
|
|
6
|
+
results: T[];
|
|
7
|
+
}>;
|
|
8
|
+
run(): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface D1DatabaseLike {
|
|
11
|
+
prepare(query: string): D1PreparedStatementLike;
|
|
12
|
+
exec?(query: string): Promise<unknown>;
|
|
13
|
+
}
|
|
14
|
+
export declare function toDate(value: unknown): Date | undefined;
|
|
15
|
+
export declare function toNumber(value: unknown, fallback?: number): number;
|
|
16
|
+
export declare function toStringValue(value: unknown, fallback?: string): string;
|
|
17
|
+
export declare function safeJsonParse<T>(value: unknown): T | undefined;
|
|
18
|
+
export declare function queryAll<T extends D1Row = D1Row>(db: D1DatabaseLike, sql: string, params?: readonly unknown[]): Promise<T[]>;
|
|
19
|
+
export declare function queryFirst<T extends D1Row = D1Row>(db: D1DatabaseLike, sql: string, params?: readonly unknown[]): Promise<T | null>;
|
|
20
|
+
export declare function runStatement(db: D1DatabaseLike, sql: string, params?: readonly unknown[]): Promise<void>;
|
|
21
|
+
export declare function runStatements(db: D1DatabaseLike, sqlStatements: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { WebhookDeliveryListOptions, WebhookDeliveryStore } from "../../runtime/types";
|
|
2
|
+
import { type WebhookDelivery } from "../../types/webhook.types";
|
|
3
|
+
import { type D1DatabaseLike } from "./d1-client";
|
|
4
|
+
export declare class D1WebhookDeliveryStore implements WebhookDeliveryStore {
|
|
5
|
+
private readonly db;
|
|
6
|
+
private readonly tableName;
|
|
7
|
+
private readonly ensureInitialized;
|
|
8
|
+
constructor(db: D1DatabaseLike, tableName: string, ensureInitialized: () => Promise<void>);
|
|
9
|
+
add(delivery: WebhookDelivery): Promise<void>;
|
|
10
|
+
list(options?: WebhookDeliveryListOptions): Promise<WebhookDelivery[]>;
|
|
11
|
+
private toDelivery;
|
|
12
|
+
}
|