@hostwebhook/node 1.6.0 → 1.6.2
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 +276 -276
- package/dist/index.d.mts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +6 -2
- package/dist/index.mjs +6 -2
- package/llms.txt +167 -167
- package/package.json +43 -43
package/README.md
CHANGED
|
@@ -1,276 +1,276 @@
|
|
|
1
|
-
# @hostwebhook/node
|
|
2
|
-
|
|
3
|
-
> **Early Access** — This SDK is in early access. The API may change between versions. We'd love your feedback — [report an issue](https://www.hostwebhook.com/#issues) if you run into anything.
|
|
4
|
-
|
|
5
|
-
Official Node.js SDK for [HostWebhook](https://www.hostwebhook.com) — verify webhook signatures and send webhooks through the platform.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```sh
|
|
10
|
-
npm install @hostwebhook/node
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Requires Node.js 18+.
|
|
14
|
-
|
|
15
|
-
## Verify Webhook Signatures
|
|
16
|
-
|
|
17
|
-
HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity and prevent replay attacks.
|
|
18
|
-
|
|
19
|
-
### Express
|
|
20
|
-
|
|
21
|
-
```ts
|
|
22
|
-
import express from 'express';
|
|
23
|
-
import { expressMiddleware } from '@hostwebhook/node';
|
|
24
|
-
|
|
25
|
-
const app = express();
|
|
26
|
-
|
|
27
|
-
app.post('/webhooks',
|
|
28
|
-
express.raw({ type: 'application/json' }),
|
|
29
|
-
expressMiddleware(process.env.SIGNING_SECRET!),
|
|
30
|
-
(req, res) => {
|
|
31
|
-
const payload = JSON.parse(req.body.toString());
|
|
32
|
-
// process payload...
|
|
33
|
-
res.json({ received: true });
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
> **Important:** Use `express.raw()` so `req.body` is a Buffer, not parsed JSON.
|
|
39
|
-
|
|
40
|
-
### NestJS
|
|
41
|
-
|
|
42
|
-
```ts
|
|
43
|
-
import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
44
|
-
import { createNestGuard } from '@hostwebhook/node';
|
|
45
|
-
|
|
46
|
-
const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
47
|
-
|
|
48
|
-
@Controller('webhooks')
|
|
49
|
-
export class WebhooksController {
|
|
50
|
-
@Post()
|
|
51
|
-
@UseGuards(WebhookGuard)
|
|
52
|
-
handle(@Req() req: Request) {
|
|
53
|
-
return { received: true };
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Fastify
|
|
59
|
-
|
|
60
|
-
```ts
|
|
61
|
-
import Fastify from 'fastify';
|
|
62
|
-
import { fastifyHook } from '@hostwebhook/node';
|
|
63
|
-
|
|
64
|
-
const app = Fastify();
|
|
65
|
-
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
66
|
-
|
|
67
|
-
app.post('/webhooks', {
|
|
68
|
-
preHandler: fastifyHook(process.env.SIGNING_SECRET!),
|
|
69
|
-
}, (req, reply) => {
|
|
70
|
-
reply.send({ received: true });
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### Manual Verification
|
|
75
|
-
|
|
76
|
-
```ts
|
|
77
|
-
import { verify, VerificationError } from '@hostwebhook/node';
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
verify(rawBody, request.headers, secret);
|
|
81
|
-
// signature valid
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err instanceof VerificationError) {
|
|
84
|
-
// invalid — reject the request
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
Options: `verify(payload, headers, secret, { maxAgeSec: 300 })` — default 300s (5 min). Set `0` to disable replay protection.
|
|
90
|
-
|
|
91
|
-
## Send Webhooks
|
|
92
|
-
|
|
93
|
-
Send webhook payloads through HostWebhook for reliable delivery with retries and monitoring.
|
|
94
|
-
|
|
95
|
-
### Fire & forget (default)
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { HostWebhook } from '@hostwebhook/node';
|
|
99
|
-
|
|
100
|
-
const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
101
|
-
|
|
102
|
-
// Returns immediately with the event ID — you don't know what happened in the pipeline
|
|
103
|
-
const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
### Wait for pipeline result (blocking)
|
|
107
|
-
|
|
108
|
-
Use `waitForResult` to wait for the full pipeline to complete. The SDK opens an SSE stream behind the scenes and returns the result with all pipeline steps. **This blocks until the pipeline finishes** (~120s max).
|
|
109
|
-
|
|
110
|
-
```ts
|
|
111
|
-
const result = await hw.send(
|
|
112
|
-
{ event: 'order.created', orderId: '123' },
|
|
113
|
-
{ waitForResult: true },
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
console.log(result.syncStatus); // 200 = delivered, 422 = validation failed,
|
|
117
|
-
console.log(result.steps); // array of pipeline steps with nodeType, status, durationMs
|
|
118
|
-
console.log(result.response); // response body from target URL
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### Non-blocking result (recommended for UIs)
|
|
122
|
-
|
|
123
|
-
Use `onResult` to get the pipeline result in a background callback. `send()` returns immediately so your UI never blocks. Perfect for forms and user-facing apps.
|
|
124
|
-
|
|
125
|
-
```ts
|
|
126
|
-
const { eventId } = await hw.send(
|
|
127
|
-
{ event: 'order.created', orderId: '123' },
|
|
128
|
-
{
|
|
129
|
-
onResult: (result) => {
|
|
130
|
-
// Fires when the pipeline completes (via SSE in background)
|
|
131
|
-
if (result.awaitingApproval) {
|
|
132
|
-
showToast('Awaiting approval from ' + result.approvalNodeName);
|
|
133
|
-
} else if (result.syncStatus === 200) {
|
|
134
|
-
showToast('Delivered successfully');
|
|
135
|
-
} else {
|
|
136
|
-
showToast('Pipeline failed: ' + result.error);
|
|
137
|
-
}
|
|
138
|
-
},
|
|
139
|
-
onStep: (step) => {
|
|
140
|
-
// Optional: fires for each pipeline node as it executes
|
|
141
|
-
updateProgress(step.nodeName, step.status);
|
|
142
|
-
},
|
|
143
|
-
},
|
|
144
|
-
);
|
|
145
|
-
// UI shows "Sent!" immediately here — never blocks
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
> **Important:** `onResult` requires the endpoint to be in **None** response mode (async). In Sync mode, the server holds the HTTP connection open until the pipeline completes, which blocks `send()` regardless of callbacks. See [Choosing the right mode](#choosing-the-right-mode) below.
|
|
149
|
-
|
|
150
|
-
### Deduplication detection
|
|
151
|
-
|
|
152
|
-
When an event is deduplicated, the response includes `deduplicated: true`:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
const result = await hw.send({ event: 'order.created', orderId: '123' });
|
|
156
|
-
|
|
157
|
-
if (result.deduplicated) {
|
|
158
|
-
console.log('Duplicate! Original event:', result.originalEventId);
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Batch
|
|
163
|
-
|
|
164
|
-
```ts
|
|
165
|
-
const results = await hw.sendBatch([
|
|
166
|
-
{ event: 'user.signup', userId: '1' },
|
|
167
|
-
{ event: 'user.signup', userId: '2' },
|
|
168
|
-
]);
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
### Standalone Function
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
import { send } from '@hostwebhook/node';
|
|
175
|
-
|
|
176
|
-
const { eventId } = await send(
|
|
177
|
-
{ token: 'your_ingress_token' },
|
|
178
|
-
{ event: 'test', data: 'hello' },
|
|
179
|
-
);
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
## Choosing the right mode
|
|
183
|
-
|
|
184
|
-
Your endpoint has a **Response Mode** setting (None or Sync) that affects how `send()` behaves:
|
|
185
|
-
|
|
186
|
-
| Scenario | Endpoint mode | SDK option | Behavior |
|
|
187
|
-
|----------|--------------|------------|----------|
|
|
188
|
-
| **Forms / UIs** | None | `onResult` | `send()` returns instantly. Pipeline result arrives in background callback. **UI never blocks.** |
|
|
189
|
-
| **Backend scripts** | None | `waitForResult` | `send()` blocks until pipeline completes via SSE (~120s max). |
|
|
190
|
-
| **API proxy / Stripe** | Sync | *(none)* | `send()` blocks until
|
|
191
|
-
| **Testing (Postman)** | Sync | *(none)* | Single request, response includes full pipeline result. |
|
|
192
|
-
|
|
193
|
-
> **Warning: Never use Sync mode for user-facing forms or UIs.** Sync mode holds the HTTP connection open on the server, which blocks `send()` and freezes the UI until the pipeline completes. If the pipeline has approval nodes, merge nodes, or long delays, the SDK will timeout. Use **None** mode with `onResult` instead — the user sees "Sent!" immediately and the pipeline result arrives in the background.
|
|
194
|
-
|
|
195
|
-
### When to use Sync mode
|
|
196
|
-
|
|
197
|
-
- The caller is a **server or script**, not a human looking at a UI
|
|
198
|
-
- The caller **needs the pipeline result in the same HTTP request** (e.g., Stripe expects a specific response body)
|
|
199
|
-
- The pipeline is **fast and linear** (filter -> transform -> action) with no human-gated nodes (approval, merge)
|
|
200
|
-
|
|
201
|
-
### When to use None mode + onResult
|
|
202
|
-
|
|
203
|
-
- The caller is a **form, UI, or frontend app**
|
|
204
|
-
- You want **instant feedback** ("Sent!") without waiting for the pipeline
|
|
205
|
-
- The pipeline has **approval nodes, merge nodes, or delays**
|
|
206
|
-
- You want **real-time progress** via `onStep` callbacks
|
|
207
|
-
|
|
208
|
-
## Configuration
|
|
209
|
-
|
|
210
|
-
| Option | Type | Default | Description |
|
|
211
|
-
|--------|------|---------|-------------|
|
|
212
|
-
| `token` | `string` | *required* | Endpoint ingress token |
|
|
213
|
-
| `baseUrl` | `string` | `https://api.hostwebhook.com` | API base URL |
|
|
214
|
-
| `signingSecret` | `string` | — | Signs outgoing requests with HMAC-SHA256 |
|
|
215
|
-
| `signature` | `StaticSignature` | — | Static secret signature (alternative to HMAC) |
|
|
216
|
-
| `defaultHeaders` | `Record<string, string>` | — | Headers added to every request |
|
|
217
|
-
| `timeoutMs` | `number` | `
|
|
218
|
-
|
|
219
|
-
### SendOptions
|
|
220
|
-
|
|
221
|
-
| Option | Type | Default | Description |
|
|
222
|
-
|--------|------|---------|-------------|
|
|
223
|
-
| `headers` | `Record<string, string>` | — | Per-request headers |
|
|
224
|
-
| `contentType` | `string` | `application/json` | Override content type |
|
|
225
|
-
| `rawBody` | `string \| Buffer` | — | Send raw body instead of JSON |
|
|
226
|
-
| `waitForResult` | `boolean` | `false` | Wait for pipeline result via SSE stream (blocking) |
|
|
227
|
-
| `onResult` | `(result) => void` | — | Non-blocking pipeline result callback via SSE |
|
|
228
|
-
| `onStep` | `(step) => void` | — | Real-time pipeline step callback (with `onResult` or `waitForResult`) |
|
|
229
|
-
|
|
230
|
-
## Error Handling
|
|
231
|
-
|
|
232
|
-
```ts
|
|
233
|
-
import { SendError, VerificationError } from '@hostwebhook/node';
|
|
234
|
-
|
|
235
|
-
// Send errors
|
|
236
|
-
try {
|
|
237
|
-
await hw.send(payload);
|
|
238
|
-
} catch (err) {
|
|
239
|
-
if (err instanceof SendError) {
|
|
240
|
-
console.log(err.statusCode, err.responseBody);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Verification errors
|
|
245
|
-
try {
|
|
246
|
-
verify(body, headers, secret);
|
|
247
|
-
} catch (err) {
|
|
248
|
-
if (err instanceof VerificationError) {
|
|
249
|
-
console.log(err.message);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
```
|
|
253
|
-
|
|
254
|
-
## Signature Format
|
|
255
|
-
|
|
256
|
-
```
|
|
257
|
-
X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>
|
|
258
|
-
```
|
|
259
|
-
|
|
260
|
-
HMAC computed as: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
|
|
261
|
-
|
|
262
|
-
## Support
|
|
263
|
-
|
|
264
|
-
Found a bug or have a feature request? [Report an issue](https://www.hostwebhook.com/#issues) on our website.
|
|
265
|
-
|
|
266
|
-
## Privacy
|
|
267
|
-
|
|
268
|
-
Your webhook data is yours. HostWebhook is built with privacy as a core principle:
|
|
269
|
-
|
|
270
|
-
- **Your payloads stay between you and your endpoints** — we never sell, share, or use your data for training.
|
|
271
|
-
- **Encryption in transit** — all traffic between the SDK, the platform, and your endpoints is sent over TLS.
|
|
272
|
-
- **Minimal data retention** — event payloads are stored only for delivery and debugging, and are automatically purged based on your plan's retention window.
|
|
273
|
-
|
|
274
|
-
## License
|
|
275
|
-
|
|
276
|
-
MIT
|
|
1
|
+
# @hostwebhook/node
|
|
2
|
+
|
|
3
|
+
> **Early Access** — This SDK is in early access. The API may change between versions. We'd love your feedback — [report an issue](https://www.hostwebhook.com/#issues) if you run into anything.
|
|
4
|
+
|
|
5
|
+
Official Node.js SDK for [HostWebhook](https://www.hostwebhook.com) — verify webhook signatures and send webhooks through the platform.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @hostwebhook/node
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node.js 18+.
|
|
14
|
+
|
|
15
|
+
## Verify Webhook Signatures
|
|
16
|
+
|
|
17
|
+
HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity and prevent replay attacks.
|
|
18
|
+
|
|
19
|
+
### Express
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import express from 'express';
|
|
23
|
+
import { expressMiddleware } from '@hostwebhook/node';
|
|
24
|
+
|
|
25
|
+
const app = express();
|
|
26
|
+
|
|
27
|
+
app.post('/webhooks',
|
|
28
|
+
express.raw({ type: 'application/json' }),
|
|
29
|
+
expressMiddleware(process.env.SIGNING_SECRET!),
|
|
30
|
+
(req, res) => {
|
|
31
|
+
const payload = JSON.parse(req.body.toString());
|
|
32
|
+
// process payload...
|
|
33
|
+
res.json({ received: true });
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
> **Important:** Use `express.raw()` so `req.body` is a Buffer, not parsed JSON.
|
|
39
|
+
|
|
40
|
+
### NestJS
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
44
|
+
import { createNestGuard } from '@hostwebhook/node';
|
|
45
|
+
|
|
46
|
+
const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
47
|
+
|
|
48
|
+
@Controller('webhooks')
|
|
49
|
+
export class WebhooksController {
|
|
50
|
+
@Post()
|
|
51
|
+
@UseGuards(WebhookGuard)
|
|
52
|
+
handle(@Req() req: Request) {
|
|
53
|
+
return { received: true };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Fastify
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import Fastify from 'fastify';
|
|
62
|
+
import { fastifyHook } from '@hostwebhook/node';
|
|
63
|
+
|
|
64
|
+
const app = Fastify();
|
|
65
|
+
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
66
|
+
|
|
67
|
+
app.post('/webhooks', {
|
|
68
|
+
preHandler: fastifyHook(process.env.SIGNING_SECRET!),
|
|
69
|
+
}, (req, reply) => {
|
|
70
|
+
reply.send({ received: true });
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Manual Verification
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { verify, VerificationError } from '@hostwebhook/node';
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
verify(rawBody, request.headers, secret);
|
|
81
|
+
// signature valid
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err instanceof VerificationError) {
|
|
84
|
+
// invalid — reject the request
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Options: `verify(payload, headers, secret, { maxAgeSec: 300 })` — default 300s (5 min). Set `0` to disable replay protection.
|
|
90
|
+
|
|
91
|
+
## Send Webhooks
|
|
92
|
+
|
|
93
|
+
Send webhook payloads through HostWebhook for reliable delivery with retries and monitoring.
|
|
94
|
+
|
|
95
|
+
### Fire & forget (default)
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { HostWebhook } from '@hostwebhook/node';
|
|
99
|
+
|
|
100
|
+
const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
101
|
+
|
|
102
|
+
// Returns immediately with the event ID — you don't know what happened in the pipeline
|
|
103
|
+
const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Wait for pipeline result (blocking)
|
|
107
|
+
|
|
108
|
+
Use `waitForResult` to wait for the full pipeline to complete. The SDK opens an SSE stream behind the scenes and returns the result with all pipeline steps. **This blocks until the pipeline finishes** (~120s max).
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const result = await hw.send(
|
|
112
|
+
{ event: 'order.created', orderId: '123' },
|
|
113
|
+
{ waitForResult: true },
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
console.log(result.syncStatus); // 200 = delivered, 202 = paused, 422 = validation failed, 500 = failed, 408 = timeout
|
|
117
|
+
console.log(result.steps); // array of pipeline steps with nodeType, status, durationMs
|
|
118
|
+
console.log(result.response); // response body from target URL
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Non-blocking result (recommended for UIs)
|
|
122
|
+
|
|
123
|
+
Use `onResult` to get the pipeline result in a background callback. `send()` returns immediately so your UI never blocks. Perfect for forms and user-facing apps.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
const { eventId } = await hw.send(
|
|
127
|
+
{ event: 'order.created', orderId: '123' },
|
|
128
|
+
{
|
|
129
|
+
onResult: (result) => {
|
|
130
|
+
// Fires when the pipeline completes (via SSE in background)
|
|
131
|
+
if (result.awaitingApproval) {
|
|
132
|
+
showToast('Awaiting approval from ' + result.approvalNodeName);
|
|
133
|
+
} else if (result.syncStatus === 200) {
|
|
134
|
+
showToast('Delivered successfully');
|
|
135
|
+
} else {
|
|
136
|
+
showToast('Pipeline failed: ' + result.error);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
onStep: (step) => {
|
|
140
|
+
// Optional: fires for each pipeline node as it executes
|
|
141
|
+
updateProgress(step.nodeName, step.status);
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
// UI shows "Sent!" immediately here — never blocks
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **Important:** `onResult` requires the endpoint to be in **None** response mode (async). In Sync mode, the server holds the HTTP connection open until the pipeline completes, which blocks `send()` regardless of callbacks. See [Choosing the right mode](#choosing-the-right-mode) below.
|
|
149
|
+
|
|
150
|
+
### Deduplication detection
|
|
151
|
+
|
|
152
|
+
When an event is deduplicated, the response includes `deduplicated: true`:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const result = await hw.send({ event: 'order.created', orderId: '123' });
|
|
156
|
+
|
|
157
|
+
if (result.deduplicated) {
|
|
158
|
+
console.log('Duplicate! Original event:', result.originalEventId);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Batch
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
const results = await hw.sendBatch([
|
|
166
|
+
{ event: 'user.signup', userId: '1' },
|
|
167
|
+
{ event: 'user.signup', userId: '2' },
|
|
168
|
+
]);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Standalone Function
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import { send } from '@hostwebhook/node';
|
|
175
|
+
|
|
176
|
+
const { eventId } = await send(
|
|
177
|
+
{ token: 'your_ingress_token' },
|
|
178
|
+
{ event: 'test', data: 'hello' },
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Choosing the right mode
|
|
183
|
+
|
|
184
|
+
Your endpoint has a **Response Mode** setting (None or Sync) that affects how `send()` behaves:
|
|
185
|
+
|
|
186
|
+
| Scenario | Endpoint mode | SDK option | Behavior |
|
|
187
|
+
|----------|--------------|------------|----------|
|
|
188
|
+
| **Forms / UIs** | None | `onResult` | `send()` returns instantly. Pipeline result arrives in background callback. **UI never blocks.** |
|
|
189
|
+
| **Backend scripts** | None | `waitForResult` | `send()` blocks until pipeline completes via SSE (~120s max). |
|
|
190
|
+
| **API proxy / Stripe** | Sync | *(none)* | `send()` blocks until a Schema Validator or an AI Node in the flow answers (20s at most, 55s with an AI Node; `timeoutMs` is 60s by default, enough for both). A 2xx is the result; a 422, 500 or 408 throws `SendError` with the JSON body in `responseBody`. |
|
|
191
|
+
| **Testing (Postman)** | Sync | *(none)* | Single request, response includes full pipeline result. |
|
|
192
|
+
|
|
193
|
+
> **Warning: Never use Sync mode for user-facing forms or UIs.** Sync mode holds the HTTP connection open on the server, which blocks `send()` and freezes the UI until the pipeline completes. If the pipeline has approval nodes, merge nodes, or long delays, the SDK will timeout. Use **None** mode with `onResult` instead — the user sees "Sent!" immediately and the pipeline result arrives in the background.
|
|
194
|
+
|
|
195
|
+
### When to use Sync mode
|
|
196
|
+
|
|
197
|
+
- The caller is a **server or script**, not a human looking at a UI
|
|
198
|
+
- The caller **needs the pipeline result in the same HTTP request** (e.g., Stripe expects a specific response body)
|
|
199
|
+
- The pipeline is **fast and linear** (filter -> transform -> action) with no human-gated nodes (approval, merge)
|
|
200
|
+
|
|
201
|
+
### When to use None mode + onResult
|
|
202
|
+
|
|
203
|
+
- The caller is a **form, UI, or frontend app**
|
|
204
|
+
- You want **instant feedback** ("Sent!") without waiting for the pipeline
|
|
205
|
+
- The pipeline has **approval nodes, merge nodes, or delays**
|
|
206
|
+
- You want **real-time progress** via `onStep` callbacks
|
|
207
|
+
|
|
208
|
+
## Configuration
|
|
209
|
+
|
|
210
|
+
| Option | Type | Default | Description |
|
|
211
|
+
|--------|------|---------|-------------|
|
|
212
|
+
| `token` | `string` | *required* | Endpoint ingress token |
|
|
213
|
+
| `baseUrl` | `string` | `https://api.hostwebhook.com` | API base URL |
|
|
214
|
+
| `signingSecret` | `string` | — | Signs outgoing requests with HMAC-SHA256 |
|
|
215
|
+
| `signature` | `StaticSignature` | — | Static secret signature (alternative to HMAC) |
|
|
216
|
+
| `defaultHeaders` | `Record<string, string>` | — | Headers added to every request |
|
|
217
|
+
| `timeoutMs` | `number` | `60000` | Request timeout in ms (long enough for a Sync webhook) |
|
|
218
|
+
|
|
219
|
+
### SendOptions
|
|
220
|
+
|
|
221
|
+
| Option | Type | Default | Description |
|
|
222
|
+
|--------|------|---------|-------------|
|
|
223
|
+
| `headers` | `Record<string, string>` | — | Per-request headers |
|
|
224
|
+
| `contentType` | `string` | `application/json` | Override content type |
|
|
225
|
+
| `rawBody` | `string \| Buffer` | — | Send raw body instead of JSON |
|
|
226
|
+
| `waitForResult` | `boolean` | `false` | Wait for pipeline result via SSE stream (blocking) |
|
|
227
|
+
| `onResult` | `(result) => void` | — | Non-blocking pipeline result callback via SSE |
|
|
228
|
+
| `onStep` | `(step) => void` | — | Real-time pipeline step callback (with `onResult` or `waitForResult`) |
|
|
229
|
+
|
|
230
|
+
## Error Handling
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { SendError, VerificationError } from '@hostwebhook/node';
|
|
234
|
+
|
|
235
|
+
// Send errors
|
|
236
|
+
try {
|
|
237
|
+
await hw.send(payload);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (err instanceof SendError) {
|
|
240
|
+
console.log(err.statusCode, err.responseBody);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Verification errors
|
|
245
|
+
try {
|
|
246
|
+
verify(body, headers, secret);
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err instanceof VerificationError) {
|
|
249
|
+
console.log(err.message);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Signature Format
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
HMAC computed as: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
|
|
261
|
+
|
|
262
|
+
## Support
|
|
263
|
+
|
|
264
|
+
Found a bug or have a feature request? [Report an issue](https://www.hostwebhook.com/#issues) on our website.
|
|
265
|
+
|
|
266
|
+
## Privacy
|
|
267
|
+
|
|
268
|
+
Your webhook data is yours. HostWebhook is built with privacy as a core principle:
|
|
269
|
+
|
|
270
|
+
- **Your payloads stay between you and your endpoints** — we never sell, share, or use your data for training.
|
|
271
|
+
- **Encryption in transit** — all traffic between the SDK, the platform, and your endpoints is sent over TLS.
|
|
272
|
+
- **Minimal data retention** — event payloads are stored only for delivery and debugging, and are automatically purged based on your plan's retention window.
|
|
273
|
+
|
|
274
|
+
## License
|
|
275
|
+
|
|
276
|
+
MIT
|
package/dist/index.d.mts
CHANGED
|
@@ -192,7 +192,7 @@ interface HostWebhookConfig {
|
|
|
192
192
|
apiKey?: string;
|
|
193
193
|
/**
|
|
194
194
|
* Default ingress token. When set, `send(payload)` uses this token.
|
|
195
|
-
* For multi-
|
|
195
|
+
* For multi-webhook usage, pass the token per-call: `send(token, payload)`.
|
|
196
196
|
*/
|
|
197
197
|
token?: string;
|
|
198
198
|
/** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
|
|
@@ -200,7 +200,7 @@ interface HostWebhookConfig {
|
|
|
200
200
|
/**
|
|
201
201
|
* HMAC-SHA256 signing secret. When set, every request is signed with the
|
|
202
202
|
* HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
|
|
203
|
-
* Pair with `incomingSignatureType: 'hostwebhook'` on the
|
|
203
|
+
* Pair with `incomingSignatureType: 'hostwebhook'` on the webhook.
|
|
204
204
|
* @deprecated Prefer `apiKey` for authentication.
|
|
205
205
|
*/
|
|
206
206
|
signingSecret?: string;
|
|
@@ -220,7 +220,7 @@ interface HostWebhookConfig {
|
|
|
220
220
|
signature?: StaticSignature;
|
|
221
221
|
/** Custom headers to include in every request. */
|
|
222
222
|
defaultHeaders?: Record<string, string>;
|
|
223
|
-
/** Request timeout in milliseconds. Default:
|
|
223
|
+
/** Request timeout in milliseconds. Default: 60000 (60s), long enough for a Sync webhook with an AI Node (55s at most). */
|
|
224
224
|
timeoutMs?: number;
|
|
225
225
|
}
|
|
226
226
|
/** A single pipeline execution step received via SSE stream. */
|
|
@@ -262,7 +262,7 @@ interface SendOptions {
|
|
|
262
262
|
* it does NOT wait for the approval to be resolved (that can take hours/days)
|
|
263
263
|
* - Delay nodes — if the delay exceeds the 125s timeout, the stream times out
|
|
264
264
|
*
|
|
265
|
-
* For approval workflows, use `responseMode: 'sync'` on the
|
|
265
|
+
* For approval workflows, use `responseMode: 'sync'` on the webhook instead —
|
|
266
266
|
* it returns the approval status inline without blocking the SSE stream.
|
|
267
267
|
*/
|
|
268
268
|
waitForResult?: boolean;
|
|
@@ -295,7 +295,13 @@ interface SendResult {
|
|
|
295
295
|
eventId: string;
|
|
296
296
|
/** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
|
|
297
297
|
streamToken?: string;
|
|
298
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Stream (`waitForResult`, `onResult`, `stream()`): pipeline result status —
|
|
300
|
+
* 200=delivered, 202=paused (approval, merge or a long delay), 422=validation
|
|
301
|
+
* failed, 500=delivery failed, 408=timeout. A webhook in Sync response mode
|
|
302
|
+
* answers with the real HTTP status instead: a non-2xx (422, 500, 408) throws
|
|
303
|
+
* `SendError` with the JSON body in `responseBody`.
|
|
304
|
+
*/
|
|
299
305
|
syncStatus?: number;
|
|
300
306
|
/** Sync/stream: delivery status ('delivered') */
|
|
301
307
|
deliveryStatus?: string;
|
|
@@ -324,13 +330,13 @@ declare class SendError extends Error {
|
|
|
324
330
|
/**
|
|
325
331
|
* HostWebhook client for sending webhooks through the platform.
|
|
326
332
|
*
|
|
327
|
-
* @example Single-
|
|
333
|
+
* @example Single-webhook
|
|
328
334
|
* ```ts
|
|
329
335
|
* const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
|
|
330
336
|
* await hw.send({ event: 'order.created' });
|
|
331
337
|
* ```
|
|
332
338
|
*
|
|
333
|
-
* @example Multi-
|
|
339
|
+
* @example Multi-webhook
|
|
334
340
|
* ```ts
|
|
335
341
|
* const hw = new HostWebhook({ apiKey: 'hwk_...' });
|
|
336
342
|
* await hw.send('token_a', { event: 'order.created' });
|
package/dist/index.d.ts
CHANGED
|
@@ -192,7 +192,7 @@ interface HostWebhookConfig {
|
|
|
192
192
|
apiKey?: string;
|
|
193
193
|
/**
|
|
194
194
|
* Default ingress token. When set, `send(payload)` uses this token.
|
|
195
|
-
* For multi-
|
|
195
|
+
* For multi-webhook usage, pass the token per-call: `send(token, payload)`.
|
|
196
196
|
*/
|
|
197
197
|
token?: string;
|
|
198
198
|
/** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
|
|
@@ -200,7 +200,7 @@ interface HostWebhookConfig {
|
|
|
200
200
|
/**
|
|
201
201
|
* HMAC-SHA256 signing secret. When set, every request is signed with the
|
|
202
202
|
* HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
|
|
203
|
-
* Pair with `incomingSignatureType: 'hostwebhook'` on the
|
|
203
|
+
* Pair with `incomingSignatureType: 'hostwebhook'` on the webhook.
|
|
204
204
|
* @deprecated Prefer `apiKey` for authentication.
|
|
205
205
|
*/
|
|
206
206
|
signingSecret?: string;
|
|
@@ -220,7 +220,7 @@ interface HostWebhookConfig {
|
|
|
220
220
|
signature?: StaticSignature;
|
|
221
221
|
/** Custom headers to include in every request. */
|
|
222
222
|
defaultHeaders?: Record<string, string>;
|
|
223
|
-
/** Request timeout in milliseconds. Default:
|
|
223
|
+
/** Request timeout in milliseconds. Default: 60000 (60s), long enough for a Sync webhook with an AI Node (55s at most). */
|
|
224
224
|
timeoutMs?: number;
|
|
225
225
|
}
|
|
226
226
|
/** A single pipeline execution step received via SSE stream. */
|
|
@@ -262,7 +262,7 @@ interface SendOptions {
|
|
|
262
262
|
* it does NOT wait for the approval to be resolved (that can take hours/days)
|
|
263
263
|
* - Delay nodes — if the delay exceeds the 125s timeout, the stream times out
|
|
264
264
|
*
|
|
265
|
-
* For approval workflows, use `responseMode: 'sync'` on the
|
|
265
|
+
* For approval workflows, use `responseMode: 'sync'` on the webhook instead —
|
|
266
266
|
* it returns the approval status inline without blocking the SSE stream.
|
|
267
267
|
*/
|
|
268
268
|
waitForResult?: boolean;
|
|
@@ -295,7 +295,13 @@ interface SendResult {
|
|
|
295
295
|
eventId: string;
|
|
296
296
|
/** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
|
|
297
297
|
streamToken?: string;
|
|
298
|
-
/**
|
|
298
|
+
/**
|
|
299
|
+
* Stream (`waitForResult`, `onResult`, `stream()`): pipeline result status —
|
|
300
|
+
* 200=delivered, 202=paused (approval, merge or a long delay), 422=validation
|
|
301
|
+
* failed, 500=delivery failed, 408=timeout. A webhook in Sync response mode
|
|
302
|
+
* answers with the real HTTP status instead: a non-2xx (422, 500, 408) throws
|
|
303
|
+
* `SendError` with the JSON body in `responseBody`.
|
|
304
|
+
*/
|
|
299
305
|
syncStatus?: number;
|
|
300
306
|
/** Sync/stream: delivery status ('delivered') */
|
|
301
307
|
deliveryStatus?: string;
|
|
@@ -324,13 +330,13 @@ declare class SendError extends Error {
|
|
|
324
330
|
/**
|
|
325
331
|
* HostWebhook client for sending webhooks through the platform.
|
|
326
332
|
*
|
|
327
|
-
* @example Single-
|
|
333
|
+
* @example Single-webhook
|
|
328
334
|
* ```ts
|
|
329
335
|
* const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
|
|
330
336
|
* await hw.send({ event: 'order.created' });
|
|
331
337
|
* ```
|
|
332
338
|
*
|
|
333
|
-
* @example Multi-
|
|
339
|
+
* @example Multi-webhook
|
|
334
340
|
* ```ts
|
|
335
341
|
* const hw = new HostWebhook({ apiKey: 'hwk_...' });
|
|
336
342
|
* await hw.send('token_a', { event: 'order.created' });
|
package/dist/index.js
CHANGED
|
@@ -213,7 +213,7 @@ var SendError = class extends Error {
|
|
|
213
213
|
|
|
214
214
|
// src/client.ts
|
|
215
215
|
var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
|
|
216
|
-
var DEFAULT_TIMEOUT =
|
|
216
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
217
217
|
var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
|
|
218
218
|
var HostWebhook = class {
|
|
219
219
|
constructor(config) {
|
|
@@ -410,7 +410,7 @@ doSend_fn = async function(token, payload, options) {
|
|
|
410
410
|
}
|
|
411
411
|
const headers = {
|
|
412
412
|
"Content-Type": options?.contentType ?? "application/json",
|
|
413
|
-
"User-Agent": `HostWebhook-SDK/${"1.
|
|
413
|
+
"User-Agent": `HostWebhook-SDK/${"1.6.2"}`,
|
|
414
414
|
"X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
|
|
415
415
|
...tz ? { "X-HostWebhook-Timezone": tz } : {},
|
|
416
416
|
...__privateGet(this, _defaultHeaders),
|
|
@@ -457,6 +457,10 @@ doSend_fn = async function(token, payload, options) {
|
|
|
457
457
|
} catch {
|
|
458
458
|
ingressResult = { eventId: text };
|
|
459
459
|
}
|
|
460
|
+
if (!ingressResult.eventId) {
|
|
461
|
+
const deCabecera = res.headers.get("x-hostwebhook-event-id");
|
|
462
|
+
if (deCabecera) ingressResult = { ...ingressResult, eventId: deCabecera };
|
|
463
|
+
}
|
|
460
464
|
} catch (err) {
|
|
461
465
|
if (err instanceof SendError) throw err;
|
|
462
466
|
if (err instanceof DOMException && err.name === "AbortError") {
|
package/dist/index.mjs
CHANGED
|
@@ -179,7 +179,7 @@ var SendError = class extends Error {
|
|
|
179
179
|
|
|
180
180
|
// src/client.ts
|
|
181
181
|
var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
|
|
182
|
-
var DEFAULT_TIMEOUT =
|
|
182
|
+
var DEFAULT_TIMEOUT = 6e4;
|
|
183
183
|
var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
|
|
184
184
|
var HostWebhook = class {
|
|
185
185
|
constructor(config) {
|
|
@@ -376,7 +376,7 @@ doSend_fn = async function(token, payload, options) {
|
|
|
376
376
|
}
|
|
377
377
|
const headers = {
|
|
378
378
|
"Content-Type": options?.contentType ?? "application/json",
|
|
379
|
-
"User-Agent": `HostWebhook-SDK/${"1.
|
|
379
|
+
"User-Agent": `HostWebhook-SDK/${"1.6.2"}`,
|
|
380
380
|
"X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
|
|
381
381
|
...tz ? { "X-HostWebhook-Timezone": tz } : {},
|
|
382
382
|
...__privateGet(this, _defaultHeaders),
|
|
@@ -423,6 +423,10 @@ doSend_fn = async function(token, payload, options) {
|
|
|
423
423
|
} catch {
|
|
424
424
|
ingressResult = { eventId: text };
|
|
425
425
|
}
|
|
426
|
+
if (!ingressResult.eventId) {
|
|
427
|
+
const deCabecera = res.headers.get("x-hostwebhook-event-id");
|
|
428
|
+
if (deCabecera) ingressResult = { ...ingressResult, eventId: deCabecera };
|
|
429
|
+
}
|
|
426
430
|
} catch (err) {
|
|
427
431
|
if (err instanceof SendError) throw err;
|
|
428
432
|
if (err instanceof DOMException && err.name === "AbortError") {
|
package/llms.txt
CHANGED
|
@@ -1,167 +1,167 @@
|
|
|
1
|
-
# @hostwebhook/node
|
|
2
|
-
|
|
3
|
-
> Official HostWebhook Node.js SDK — verify webhook signatures and send webhooks through the platform.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
npm install @hostwebhook/node
|
|
8
|
-
|
|
9
|
-
## Verify Signatures (receiving webhooks)
|
|
10
|
-
|
|
11
|
-
HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity.
|
|
12
|
-
|
|
13
|
-
### Express
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
import express from 'express';
|
|
17
|
-
import { expressMiddleware } from '@hostwebhook/node';
|
|
18
|
-
|
|
19
|
-
const app = express();
|
|
20
|
-
app.post('/webhooks',
|
|
21
|
-
express.raw({ type: 'application/json' }),
|
|
22
|
-
expressMiddleware(process.env.SIGNING_SECRET!),
|
|
23
|
-
(req, res) => {
|
|
24
|
-
const payload = JSON.parse(req.body.toString());
|
|
25
|
-
// process payload...
|
|
26
|
-
res.json({ received: true });
|
|
27
|
-
},
|
|
28
|
-
);
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
IMPORTANT: Use `express.raw()` so `req.body` is a Buffer, not parsed JSON.
|
|
32
|
-
|
|
33
|
-
### NestJS
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
37
|
-
import { createNestGuard } from '@hostwebhook/node';
|
|
38
|
-
|
|
39
|
-
const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
40
|
-
|
|
41
|
-
@Controller('webhooks')
|
|
42
|
-
export class WebhooksController {
|
|
43
|
-
@Post()
|
|
44
|
-
@UseGuards(WebhookGuard)
|
|
45
|
-
handle(@Req() req: Request) {
|
|
46
|
-
return { received: true };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### Fastify
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import Fastify from 'fastify';
|
|
55
|
-
import { fastifyHook } from '@hostwebhook/node';
|
|
56
|
-
|
|
57
|
-
const app = Fastify();
|
|
58
|
-
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
59
|
-
|
|
60
|
-
app.post('/webhooks', {
|
|
61
|
-
preHandler: fastifyHook(process.env.SIGNING_SECRET!),
|
|
62
|
-
}, (req, reply) => {
|
|
63
|
-
reply.send({ received: true });
|
|
64
|
-
});
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Manual verification
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
import { verify, VerificationError } from '@hostwebhook/node';
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
verify(rawBody, request.headers, secret);
|
|
74
|
-
// signature is valid
|
|
75
|
-
} catch (err) {
|
|
76
|
-
if (err instanceof VerificationError) {
|
|
77
|
-
// invalid signature — reject
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
verify(payload, headers, secret, options?) — throws VerificationError if invalid.
|
|
83
|
-
VerifyOptions: { maxAgeSec?: number } — default 300 (5 min). Set 0 to disable replay protection.
|
|
84
|
-
|
|
85
|
-
## Send Webhooks (sending through HostWebhook)
|
|
86
|
-
|
|
87
|
-
### Class-based client
|
|
88
|
-
|
|
89
|
-
```ts
|
|
90
|
-
import { HostWebhook } from '@hostwebhook/node';
|
|
91
|
-
|
|
92
|
-
const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
93
|
-
|
|
94
|
-
// Send a single event
|
|
95
|
-
const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
96
|
-
|
|
97
|
-
// Send multiple events in parallel
|
|
98
|
-
const results = await hw.sendBatch([
|
|
99
|
-
{ event: 'user.signup', userId: '1' },
|
|
100
|
-
{ event: 'user.signup', userId: '2' },
|
|
101
|
-
]);
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
### Standalone function (no class needed)
|
|
105
|
-
|
|
106
|
-
```ts
|
|
107
|
-
import { send } from '@hostwebhook/node';
|
|
108
|
-
|
|
109
|
-
const { eventId } = await send(
|
|
110
|
-
{ token: 'your_ingress_token' },
|
|
111
|
-
{ event: 'test', data: 'hello' },
|
|
112
|
-
);
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
## Configuration
|
|
116
|
-
|
|
117
|
-
HostWebhookConfig: {
|
|
118
|
-
token: string (required — your endpoint's ingress token),
|
|
119
|
-
baseUrl?: string (default: https://api.hostwebhook.com),
|
|
120
|
-
signingSecret?: string (optional — signs outgoing requests with HMAC-SHA256),
|
|
121
|
-
defaultHeaders?: Record<string, string>,
|
|
122
|
-
timeoutMs?: number (default: 10000),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
SendOptions: {
|
|
126
|
-
headers?: Record<string, string>,
|
|
127
|
-
contentType?: string (default: application/json),
|
|
128
|
-
rawBody?: string | Buffer (skip JSON.stringify, send raw),
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
SendResult: { eventId: string }
|
|
132
|
-
|
|
133
|
-
## Signature Format
|
|
134
|
-
|
|
135
|
-
Header: `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
|
|
136
|
-
HMAC computed as: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
|
|
137
|
-
|
|
138
|
-
## Error Handling
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
import { SendError, VerificationError } from '@hostwebhook/node';
|
|
142
|
-
|
|
143
|
-
// SendError — thrown when API returns non-2xx
|
|
144
|
-
try {
|
|
145
|
-
await hw.send(payload);
|
|
146
|
-
} catch (err) {
|
|
147
|
-
if (err instanceof SendError) {
|
|
148
|
-
console.log(err.statusCode, err.responseBody);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// VerificationError — thrown when signature is invalid
|
|
153
|
-
try {
|
|
154
|
-
verify(body, headers, secret);
|
|
155
|
-
} catch (err) {
|
|
156
|
-
if (err instanceof VerificationError) {
|
|
157
|
-
console.log(err.message); // e.g. "Signature mismatch"
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Exports
|
|
163
|
-
|
|
164
|
-
Functions: verify, expressMiddleware, createNestGuard, fastifyHook, send
|
|
165
|
-
Classes: HostWebhook, SendError, VerificationError
|
|
166
|
-
Types: VerifyOptions, HostWebhookConfig, SendOptions, SendResult, WebhookHeaders
|
|
167
|
-
Helpers: parseHeaders (extract webhook metadata from headers)
|
|
1
|
+
# @hostwebhook/node
|
|
2
|
+
|
|
3
|
+
> Official HostWebhook Node.js SDK — verify webhook signatures and send webhooks through the platform.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
npm install @hostwebhook/node
|
|
8
|
+
|
|
9
|
+
## Verify Signatures (receiving webhooks)
|
|
10
|
+
|
|
11
|
+
HostWebhook signs every delivery with HMAC-SHA256. Use these helpers to verify authenticity.
|
|
12
|
+
|
|
13
|
+
### Express
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import express from 'express';
|
|
17
|
+
import { expressMiddleware } from '@hostwebhook/node';
|
|
18
|
+
|
|
19
|
+
const app = express();
|
|
20
|
+
app.post('/webhooks',
|
|
21
|
+
express.raw({ type: 'application/json' }),
|
|
22
|
+
expressMiddleware(process.env.SIGNING_SECRET!),
|
|
23
|
+
(req, res) => {
|
|
24
|
+
const payload = JSON.parse(req.body.toString());
|
|
25
|
+
// process payload...
|
|
26
|
+
res.json({ received: true });
|
|
27
|
+
},
|
|
28
|
+
);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
IMPORTANT: Use `express.raw()` so `req.body` is a Buffer, not parsed JSON.
|
|
32
|
+
|
|
33
|
+
### NestJS
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
37
|
+
import { createNestGuard } from '@hostwebhook/node';
|
|
38
|
+
|
|
39
|
+
const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
40
|
+
|
|
41
|
+
@Controller('webhooks')
|
|
42
|
+
export class WebhooksController {
|
|
43
|
+
@Post()
|
|
44
|
+
@UseGuards(WebhookGuard)
|
|
45
|
+
handle(@Req() req: Request) {
|
|
46
|
+
return { received: true };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Fastify
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import Fastify from 'fastify';
|
|
55
|
+
import { fastifyHook } from '@hostwebhook/node';
|
|
56
|
+
|
|
57
|
+
const app = Fastify();
|
|
58
|
+
app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
59
|
+
|
|
60
|
+
app.post('/webhooks', {
|
|
61
|
+
preHandler: fastifyHook(process.env.SIGNING_SECRET!),
|
|
62
|
+
}, (req, reply) => {
|
|
63
|
+
reply.send({ received: true });
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Manual verification
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { verify, VerificationError } from '@hostwebhook/node';
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
verify(rawBody, request.headers, secret);
|
|
74
|
+
// signature is valid
|
|
75
|
+
} catch (err) {
|
|
76
|
+
if (err instanceof VerificationError) {
|
|
77
|
+
// invalid signature — reject
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
verify(payload, headers, secret, options?) — throws VerificationError if invalid.
|
|
83
|
+
VerifyOptions: { maxAgeSec?: number } — default 300 (5 min). Set 0 to disable replay protection.
|
|
84
|
+
|
|
85
|
+
## Send Webhooks (sending through HostWebhook)
|
|
86
|
+
|
|
87
|
+
### Class-based client
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { HostWebhook } from '@hostwebhook/node';
|
|
91
|
+
|
|
92
|
+
const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
93
|
+
|
|
94
|
+
// Send a single event
|
|
95
|
+
const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
96
|
+
|
|
97
|
+
// Send multiple events in parallel
|
|
98
|
+
const results = await hw.sendBatch([
|
|
99
|
+
{ event: 'user.signup', userId: '1' },
|
|
100
|
+
{ event: 'user.signup', userId: '2' },
|
|
101
|
+
]);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Standalone function (no class needed)
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { send } from '@hostwebhook/node';
|
|
108
|
+
|
|
109
|
+
const { eventId } = await send(
|
|
110
|
+
{ token: 'your_ingress_token' },
|
|
111
|
+
{ event: 'test', data: 'hello' },
|
|
112
|
+
);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
HostWebhookConfig: {
|
|
118
|
+
token: string (required — your endpoint's ingress token),
|
|
119
|
+
baseUrl?: string (default: https://api.hostwebhook.com),
|
|
120
|
+
signingSecret?: string (optional — signs outgoing requests with HMAC-SHA256),
|
|
121
|
+
defaultHeaders?: Record<string, string>,
|
|
122
|
+
timeoutMs?: number (default: 10000),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
SendOptions: {
|
|
126
|
+
headers?: Record<string, string>,
|
|
127
|
+
contentType?: string (default: application/json),
|
|
128
|
+
rawBody?: string | Buffer (skip JSON.stringify, send raw),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
SendResult: { eventId: string }
|
|
132
|
+
|
|
133
|
+
## Signature Format
|
|
134
|
+
|
|
135
|
+
Header: `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
|
|
136
|
+
HMAC computed as: `HMAC-SHA256(secret, "${timestamp}.${rawBody}")`
|
|
137
|
+
|
|
138
|
+
## Error Handling
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { SendError, VerificationError } from '@hostwebhook/node';
|
|
142
|
+
|
|
143
|
+
// SendError — thrown when API returns non-2xx
|
|
144
|
+
try {
|
|
145
|
+
await hw.send(payload);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
if (err instanceof SendError) {
|
|
148
|
+
console.log(err.statusCode, err.responseBody);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// VerificationError — thrown when signature is invalid
|
|
153
|
+
try {
|
|
154
|
+
verify(body, headers, secret);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err instanceof VerificationError) {
|
|
157
|
+
console.log(err.message); // e.g. "Signature mismatch"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Exports
|
|
163
|
+
|
|
164
|
+
Functions: verify, expressMiddleware, createNestGuard, fastifyHook, send
|
|
165
|
+
Classes: HostWebhook, SendError, VerificationError
|
|
166
|
+
Types: VerifyOptions, HostWebhookConfig, SendOptions, SendResult, WebhookHeaders
|
|
167
|
+
Helpers: parseHeaders (extract webhook metadata from headers)
|
package/package.json
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@hostwebhook/node",
|
|
3
|
-
"version": "1.6.
|
|
4
|
-
"description": "Official HostWebhook SDK — verify signatures, parse headers, and send webhooks",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"module": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.ts",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.mjs",
|
|
12
|
-
"require": "./dist/index.js"
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist",
|
|
17
|
-
"README.md",
|
|
18
|
-
"llms.txt"
|
|
19
|
-
],
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsup",
|
|
22
|
-
"test": "node --test __tests__/sdk.test.mjs",
|
|
23
|
-
"prepublishOnly": "npm run build"
|
|
24
|
-
},
|
|
25
|
-
"keywords": [
|
|
26
|
-
"hostwebhook",
|
|
27
|
-
"webhook",
|
|
28
|
-
"signature",
|
|
29
|
-
"verify",
|
|
30
|
-
"send",
|
|
31
|
-
"hmac",
|
|
32
|
-
"retry"
|
|
33
|
-
],
|
|
34
|
-
"license": "MIT",
|
|
35
|
-
"devDependencies": {
|
|
36
|
-
"@types/node": "^25.3.3",
|
|
37
|
-
"tsup": "^8.0.0",
|
|
38
|
-
"typescript": "^5.3.0"
|
|
39
|
-
},
|
|
40
|
-
"engines": {
|
|
41
|
-
"node": ">=18"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@hostwebhook/node",
|
|
3
|
+
"version": "1.6.2",
|
|
4
|
+
"description": "Official HostWebhook SDK — verify signatures, parse headers, and send webhooks",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"llms.txt"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"test": "node --test __tests__/sdk.test.mjs",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"hostwebhook",
|
|
27
|
+
"webhook",
|
|
28
|
+
"signature",
|
|
29
|
+
"verify",
|
|
30
|
+
"send",
|
|
31
|
+
"hmac",
|
|
32
|
+
"retry"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.3.3",
|
|
37
|
+
"tsup": "^8.0.0",
|
|
38
|
+
"typescript": "^5.3.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
}
|
|
43
|
+
}
|