@k-msg/webhook 0.19.1 → 0.21.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
@@ -2,17 +2,13 @@
2
2
 
3
3
  > Canonical docs: [k-msg.and.guide](https://k-msg.and.guide)
4
4
 
5
- Webhook delivery helpers for emitting real-time message events to HTTP endpoints.
5
+ Runtime-first webhook package for message events.
6
6
 
7
- This package provides:
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
- Note:
14
- - The default `WebhookService` storage is in-memory. For persistence/advanced workflows, see the exported building blocks such as `EndpointManager` and `DeliveryStore`.
15
- - This package is runtime-neutral (Edge/Web/Node). Node built-ins are not required by default.
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
- ## Quickstart (WebhookService)
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
- WebhookService,
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: 1000,
38
- // Optional: maxDelayMs, backoffMultiplier, jitter
47
+ retryDelayMs: 1_000,
39
48
  timeoutMs: 30_000,
40
- enableSecurity: true,
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 service = new WebhookService(config);
59
+ const runtime = new WebhookRuntimeService({
60
+ delivery: config,
61
+ persistence: createInMemoryWebhookPersistence(),
62
+ });
54
63
 
55
- const endpoint = await service.registerEndpoint({
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
- // Asynchronous emit (batched)
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
- // Synchronous emit (returns delivery attempts)
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
- ### Endpoint Registration Behavior
82
+ ## D1 quickstart (same runtime API)
95
83
 
96
- `registerEndpoint()` validates the URL and sends a test webhook once (a `system.maintenance` event via `testEndpoint()`).
97
-
98
- ## Security (HMAC Signatures)
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
- When security is enabled and a secret is available (`endpoint.secret` or `config.secretKey`), outgoing requests include:
101
- - `X-Webhook-Timestamp`: unix epoch seconds (string)
102
- - `X-Webhook-Signature`: HMAC signature (default: `sha256=<hex>`)
92
+ type Env = {
93
+ DB: D1Database;
94
+ };
103
95
 
104
- Signature input is:
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
- ${timestamp}.${rawBody}
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
- To verify a webhook, you must use the exact raw request body string.
117
+ `createD1WebhookPersistence()` initializes schema automatically by default.
111
118
 
112
- ### Hono Example
119
+ ## Schema helpers (Cloudflare)
113
120
 
114
121
  ```ts
115
- import { Hono } from "hono";
116
- import { readRuntimeEnv } from "@k-msg/core";
117
- import { SecurityManager } from "@k-msg/webhook";
122
+ import {
123
+ buildWebhookSchemaSql,
124
+ initializeWebhookSchema,
125
+ } from "@k-msg/webhook/adapters/cloudflare";
118
126
 
119
- const app = new Hono();
127
+ const statements = buildWebhookSchemaSql();
128
+ // run statements in your migration system, or:
129
+ await initializeWebhookSchema(env.DB);
130
+ ```
120
131
 
121
- const security = new SecurityManager({
122
- algorithm: "sha256",
123
- signatureHeader: "X-Webhook-Signature",
124
- signaturePrefix: "sha256=",
125
- });
132
+ ## SQLite / Drizzle(Postgres) snippets
126
133
 
127
- app.post("/webhooks/k-msg", async (c) => {
128
- const payload = await c.req.text();
134
+ `WebhookRuntimeService` accepts custom stores via `endpointStore` + `deliveryStore`.
135
+ Implement the same interfaces to plug any backend:
129
136
 
130
- const signature = c.req.header("X-Webhook-Signature") ?? "";
131
- const timestamp = c.req.header("X-Webhook-Timestamp") ?? "";
132
- const secret = readRuntimeEnv("WEBHOOK_SECRET") ?? "";
137
+ ```ts
138
+ import type {
139
+ WebhookDeliveryStore,
140
+ WebhookEndpointStore,
141
+ } from "@k-msg/webhook";
133
142
 
134
- if (!security.verifyTimestamp(timestamp, 300)) {
135
- return c.json({ error: "Request too old" }, 401);
143
+ class SqliteEndpointStore implements WebhookEndpointStore {
144
+ async add() {}
145
+ async update() {}
146
+ async remove() {}
147
+ async get() {
148
+ return null;
136
149
  }
137
- if (!security.verifySignatureWithTimestamp(payload, timestamp, signature, secret)) {
138
- return c.json({ error: "Invalid signature" }, 401);
150
+ async list() {
151
+ return [];
139
152
  }
153
+ }
140
154
 
141
- const event = JSON.parse(payload);
142
- return c.json({ ok: true, type: event.type });
143
- });
155
+ class SqliteDeliveryStore implements WebhookDeliveryStore {
156
+ async add() {}
157
+ async list() {
158
+ return [];
159
+ }
160
+ }
144
161
  ```
145
162
 
146
- ## Retries and Delivery Status
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
- await service.registerEndpoint({
161
- url: "https://example.com/webhooks/k-msg",
162
- active: true,
163
- events: [WebhookEventType.MESSAGE_SENT],
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
- ## File Storage Adapter (for `type: "file"`)
173
+ ## Security defaults
173
174
 
174
- `EndpointManager`, `EventStore`, `DeliveryStore`, and `QueueManager` no longer import Node `fs/path` directly.
175
- When using file persistence, provide `fileAdapter`.
175
+ - Private hosts are blocked by default
176
+ - `http://localhost` style URLs require explicit allowance (runtime security options)
176
177
 
177
- ```ts
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
- const store = new DeliveryStore({
191
- type: "file",
192
- filePath: "./data/deliveries.log",
193
- fileAdapter: nodeFileAdapter,
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
- ## Zod Schemas
187
+ ## Toolkit subpath
198
188
 
199
- This package exports Zod schemas for validation:
200
- - `WebhookEventSchema` (timestamp is coerced from string/number to `Date`)
201
- - `WebhookEndpointSchema`
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
- HTTP 엔드포인트로 실시간 메시지 이벤트를 전달하기 위한 웹훅(Webhook) 전송 도구입니다.
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
- - 기본 `WebhookService` 저장소는 in-memory입니다. 영속 저장/고급 워크플로우가 필요하면 `EndpointManager`, `DeliveryStore` 같은 빌딩 블록을 활용하세요.
15
- - 이 패키지는 런타임 중립(Edge/Web/Node)으로 동작하며, 기본적으로 Node 내장 모듈에 의존하지 않습니다.
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
- ## 빠른 시작 (WebhookService)
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
- WebhookService,
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: 1000,
38
- // Optional: maxDelayMs, backoffMultiplier, jitter
47
+ retryDelayMs: 1_000,
39
48
  timeoutMs: 30_000,
40
- enableSecurity: true,
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 service = new WebhookService(config);
59
+ const runtime = new WebhookRuntimeService({
60
+ delivery: config,
61
+ persistence: createInMemoryWebhookPersistence(),
62
+ });
54
63
 
55
- const endpoint = await service.registerEndpoint({
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
- // 비동기 emit (배치 처리됨)
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
- // 동기 emit (딜리버리 결과를 반환)
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
- `registerEndpoint()`는 URL 유효성 검증 후, `testEndpoint()`를 통해 테스트 웹훅(`system.maintenance`)을 1회 전송합니다.
97
-
98
- ## 보안 (HMAC 서명)
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
- 보안이 활성화되어 있고 secret이 존재하면(`endpoint.secret` 또는 `config.secretKey`), 아래 헤더가 포함됩니다.
101
- - `X-Webhook-Timestamp`: unix epoch seconds (string)
102
- - `X-Webhook-Signature`: HMAC 서명 (기본: `sha256=<hex>`)
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
- ${timestamp}.${rawBody}
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
- 수신 측에서는 반드시 "raw body 문자열" 그대로 검증해야 합니다.
117
+ `createD1WebhookPersistence()`는 기본값으로 스키마 초기화를 자동 수행합니다.
111
118
 
112
- ### Hono 예시
119
+ ## Cloudflare 스키마 헬퍼
113
120
 
114
121
  ```ts
115
- import { Hono } from "hono";
116
- import { readRuntimeEnv } from "@k-msg/core";
117
- import { SecurityManager } from "@k-msg/webhook";
118
-
119
- const app = new Hono();
120
- const security = new SecurityManager({
121
- algorithm: "sha256",
122
- signatureHeader: "X-Webhook-Signature",
123
- signaturePrefix: "sha256=",
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
- app.post("/webhooks/k-msg", async (c) => {
127
- const payload = await c.req.text();
132
+ ## SQLite / Drizzle(Postgres) 스니펫
128
133
 
129
- const signature = c.req.header("X-Webhook-Signature") ?? "";
130
- const timestamp = c.req.header("X-Webhook-Timestamp") ?? "";
131
- const secret = readRuntimeEnv("WEBHOOK_SECRET") ?? "";
134
+ `WebhookRuntimeService`는 `endpointStore` + `deliveryStore` 주입을 지원합니다.
135
+ 동일 인터페이스만 구현하면 백엔드를 교체할 수 있습니다.
132
136
 
133
- if (!security.verifyTimestamp(timestamp, 300)) {
134
- return c.json({ error: "Request too old" }, 401);
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
- if (!security.verifySignatureWithTimestamp(payload, timestamp, signature, secret)) {
137
- return c.json({ error: "Invalid signature" }, 401);
150
+ async list() {
151
+ return [];
138
152
  }
153
+ }
139
154
 
140
- const event = JSON.parse(payload);
141
- return c.json({ ok: true, type: event.type });
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
- await service.registerEndpoint({
160
- url: "https://example.com/webhooks/k-msg",
161
- active: true,
162
- events: [WebhookEventType.MESSAGE_SENT],
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
- ## File Storage Adapter (`type: "file"` 사용 시)
173
+ ## 보안 기본값
172
174
 
173
- `EndpointManager`, `EventStore`, `DeliveryStore`, `QueueManager`는 Node `fs/path`를 직접 import하지 않습니다.
174
- 파일 영속화를 사용하려면 `fileAdapter`를 주입해야 합니다.
175
+ - 기본적으로 private host 차단
176
+ - `http://localhost` 류 URL은 옵션으로 명시 허용해야 사용 가능
175
177
 
176
- ```ts
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
- const store = new DeliveryStore({
190
- type: "file",
191
- filePath: "./data/deliveries.log",
192
- fileAdapter: nodeFileAdapter,
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
- ## Zod 스키마
187
+ ## Toolkit subpath
197
188
 
198
- 이 패키지는 검증용 Zod 스키마를 export 합니다.
199
- - `WebhookEventSchema` (timestamp는 string/number 입력을 `Date`로 coerce)
200
- - `WebhookEndpointSchema`
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
+ }