@hostwebhook/node 1.0.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/dist/index.d.mts +234 -0
- package/dist/index.d.ts +234 -0
- package/dist/index.js +300 -0
- package/dist/index.mjs +265 -0
- package/package.json +41 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when signature verification fails.
|
|
3
|
+
*/
|
|
4
|
+
declare class VerificationError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
interface VerifyOptions {
|
|
8
|
+
/** Max allowed age of the signature timestamp in seconds. Default: 300 (5 min). Set 0 to disable. */
|
|
9
|
+
maxAgeSec?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify a HostWebhook signature.
|
|
13
|
+
*
|
|
14
|
+
* Matches the format produced by HostWebhook's delivery processor:
|
|
15
|
+
* `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
|
|
16
|
+
*
|
|
17
|
+
* The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
|
|
18
|
+
*
|
|
19
|
+
* @param payload - The raw request body (string or Buffer)
|
|
20
|
+
* @param headers - The request headers object
|
|
21
|
+
* @param secret - Your signing secret (e.g. `whsec_...`)
|
|
22
|
+
* @param options - Optional configuration
|
|
23
|
+
* @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
|
|
24
|
+
*/
|
|
25
|
+
declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Typed representation of HostWebhook delivery headers.
|
|
29
|
+
*/
|
|
30
|
+
interface WebhookHeaders {
|
|
31
|
+
/** Unique event ID (`X-HostWebhook-Event-Id`) */
|
|
32
|
+
eventId: string;
|
|
33
|
+
/** Delivery attempt number (`X-HostWebhook-Attempt`) */
|
|
34
|
+
attempt: number;
|
|
35
|
+
/** Signature timestamp in milliseconds */
|
|
36
|
+
timestamp: number;
|
|
37
|
+
/** Raw HMAC-SHA256 hex signature */
|
|
38
|
+
signature: string;
|
|
39
|
+
/** Chain step number, if this is a chained delivery */
|
|
40
|
+
chainStep?: number;
|
|
41
|
+
/** Original chain payload (base64-decoded JSON) */
|
|
42
|
+
chainOriginal?: unknown;
|
|
43
|
+
/** Previous chain step response (base64-decoded JSON) */
|
|
44
|
+
chainPrevious?: unknown;
|
|
45
|
+
/** Chain response metadata (base64-decoded JSON) */
|
|
46
|
+
chainResponse?: {
|
|
47
|
+
status: number;
|
|
48
|
+
body: unknown;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
/** Whether the chain response was truncated */
|
|
52
|
+
chainResponseTruncated?: boolean;
|
|
53
|
+
/** Original size of the chain response before truncation */
|
|
54
|
+
chainResponseOriginalSize?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse all `X-HostWebhook-*` headers into a typed object.
|
|
58
|
+
*
|
|
59
|
+
* @param headers - The raw request headers
|
|
60
|
+
* @returns Parsed webhook headers
|
|
61
|
+
*/
|
|
62
|
+
declare function parseHeaders(headers: Record<string, string | string[] | undefined>): WebhookHeaders;
|
|
63
|
+
|
|
64
|
+
type NextFn = (err?: unknown) => void;
|
|
65
|
+
interface ExpressRequest {
|
|
66
|
+
body: string | Buffer;
|
|
67
|
+
headers: Record<string, string | string[] | undefined>;
|
|
68
|
+
}
|
|
69
|
+
interface ExpressResponse {
|
|
70
|
+
status(code: number): ExpressResponse;
|
|
71
|
+
json(body: unknown): void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Express middleware that verifies HostWebhook signatures.
|
|
75
|
+
*
|
|
76
|
+
* IMPORTANT: You must use `express.raw({ type: 'application/json' })` before
|
|
77
|
+
* this middleware so that `req.body` is a Buffer (not parsed JSON).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import express from 'express';
|
|
82
|
+
* import { expressMiddleware } from '@hostwebhook/node';
|
|
83
|
+
*
|
|
84
|
+
* const app = express();
|
|
85
|
+
* app.post('/webhooks',
|
|
86
|
+
* express.raw({ type: 'application/json' }),
|
|
87
|
+
* expressMiddleware(process.env.SIGNING_SECRET!),
|
|
88
|
+
* (req, res) => {
|
|
89
|
+
* const payload = JSON.parse(req.body.toString());
|
|
90
|
+
* res.status(200).json({ received: true });
|
|
91
|
+
* },
|
|
92
|
+
* );
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function expressMiddleware(secret: string, options?: VerifyOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFn) => void;
|
|
96
|
+
/**
|
|
97
|
+
* NestJS guard factory. Returns a class that can be used with @UseGuards().
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
102
|
+
* import { createNestGuard } from '@hostwebhook/node';
|
|
103
|
+
*
|
|
104
|
+
* const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
105
|
+
*
|
|
106
|
+
* @Controller('webhooks')
|
|
107
|
+
* export class WebhooksController {
|
|
108
|
+
* @Post()
|
|
109
|
+
* @UseGuards(WebhookGuard)
|
|
110
|
+
* handle(@Req() req: Request) {
|
|
111
|
+
* return { received: true };
|
|
112
|
+
* }
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare function createNestGuard(secret: string, options?: VerifyOptions): {
|
|
117
|
+
new (): {
|
|
118
|
+
canActivate(context: {
|
|
119
|
+
switchToHttp(): {
|
|
120
|
+
getRequest(): ExpressRequest;
|
|
121
|
+
};
|
|
122
|
+
}): boolean;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Fastify preHandler hook that verifies HostWebhook signatures.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import Fastify from 'fastify';
|
|
131
|
+
* import { fastifyHook } from '@hostwebhook/node';
|
|
132
|
+
*
|
|
133
|
+
* const app = Fastify({ bodyLimit: 1_048_576 });
|
|
134
|
+
* app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
135
|
+
*
|
|
136
|
+
* app.post('/webhooks', { preHandler: fastifyHook(process.env.SIGNING_SECRET!) }, (req, reply) => {
|
|
137
|
+
* reply.send({ received: true });
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
declare function fastifyHook(secret: string, options?: VerifyOptions): (req: ExpressRequest, reply: ExpressResponse, done: NextFn) => void;
|
|
142
|
+
|
|
143
|
+
/** Configuration for the HostWebhook client. */
|
|
144
|
+
interface HostWebhookConfig {
|
|
145
|
+
/** Ingress token from your endpoint settings. */
|
|
146
|
+
token: string;
|
|
147
|
+
/** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Signing secret for incoming signature verification.
|
|
151
|
+
* When provided, every request is signed with HMAC-SHA256 using the `custom`
|
|
152
|
+
* format (`X-Webhook-Signature: sha256=<hex>`), which HostWebhook verifies
|
|
153
|
+
* if the endpoint has `incomingSignatureType: 'custom'` configured.
|
|
154
|
+
*/
|
|
155
|
+
signingSecret?: string;
|
|
156
|
+
/** Custom headers to include in every request. */
|
|
157
|
+
defaultHeaders?: Record<string, string>;
|
|
158
|
+
/** Request timeout in milliseconds. Default: 10000 (10s). */
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
}
|
|
161
|
+
/** Options for a single send call. */
|
|
162
|
+
interface SendOptions {
|
|
163
|
+
/** Custom headers for this specific request (merged with defaultHeaders). */
|
|
164
|
+
headers?: Record<string, string>;
|
|
165
|
+
/** Override the Content-Type. Default: `application/json`. */
|
|
166
|
+
contentType?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Send raw string/Buffer body instead of JSON-stringifying a payload object.
|
|
169
|
+
* When set, the `payload` parameter to `send()` is ignored.
|
|
170
|
+
*/
|
|
171
|
+
rawBody?: string | Buffer;
|
|
172
|
+
}
|
|
173
|
+
/** Result returned after a successful send. */
|
|
174
|
+
interface SendResult {
|
|
175
|
+
/** The event ID assigned by HostWebhook. */
|
|
176
|
+
eventId: string;
|
|
177
|
+
}
|
|
178
|
+
/** Thrown when the HostWebhook API returns a non-2xx response. */
|
|
179
|
+
declare class SendError extends Error {
|
|
180
|
+
readonly statusCode: number;
|
|
181
|
+
readonly responseBody: string;
|
|
182
|
+
constructor(message: string, statusCode: number, responseBody: string);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* HostWebhook client for sending webhooks through the platform.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
191
|
+
* const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
declare class HostWebhook {
|
|
195
|
+
private readonly token;
|
|
196
|
+
private readonly baseUrl;
|
|
197
|
+
private readonly signingSecret?;
|
|
198
|
+
private readonly defaultHeaders;
|
|
199
|
+
private readonly timeoutMs;
|
|
200
|
+
constructor(config: HostWebhookConfig);
|
|
201
|
+
/**
|
|
202
|
+
* Send a webhook payload through HostWebhook.
|
|
203
|
+
*
|
|
204
|
+
* The payload is JSON-stringified and POST'd to the ingress endpoint.
|
|
205
|
+
* HostWebhook then handles delivery to the target URL with retries,
|
|
206
|
+
* monitoring, transforms, and signing.
|
|
207
|
+
*
|
|
208
|
+
* @param payload - The webhook payload (will be JSON.stringify'd)
|
|
209
|
+
* @param options - Optional per-request configuration
|
|
210
|
+
* @returns The event ID assigned by HostWebhook
|
|
211
|
+
* @throws {SendError} If the API returns a non-2xx status
|
|
212
|
+
*/
|
|
213
|
+
send(payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Convenience: send multiple payloads in parallel.
|
|
216
|
+
*
|
|
217
|
+
* @param payloads - Array of payloads to send
|
|
218
|
+
* @param options - Optional per-request configuration (shared for all)
|
|
219
|
+
* @returns Array of results (in same order as payloads)
|
|
220
|
+
*/
|
|
221
|
+
sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Standalone send function (no class instantiation needed).
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* import { send } from '@hostwebhook/node';
|
|
229
|
+
* await send({ token: 'your_token' }, { event: 'test' });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
|
|
233
|
+
|
|
234
|
+
export { HostWebhook, type HostWebhookConfig, SendError, type SendOptions, type SendResult, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown when signature verification fails.
|
|
3
|
+
*/
|
|
4
|
+
declare class VerificationError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
interface VerifyOptions {
|
|
8
|
+
/** Max allowed age of the signature timestamp in seconds. Default: 300 (5 min). Set 0 to disable. */
|
|
9
|
+
maxAgeSec?: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Verify a HostWebhook signature.
|
|
13
|
+
*
|
|
14
|
+
* Matches the format produced by HostWebhook's delivery processor:
|
|
15
|
+
* `X-HostWebhook-Signature: t=<timestamp_ms>,v1=<hmac_hex>`
|
|
16
|
+
*
|
|
17
|
+
* The HMAC is computed as: HMAC-SHA256(secret, `${timestamp}.${rawBody}`)
|
|
18
|
+
*
|
|
19
|
+
* @param payload - The raw request body (string or Buffer)
|
|
20
|
+
* @param headers - The request headers object
|
|
21
|
+
* @param secret - Your signing secret (e.g. `whsec_...`)
|
|
22
|
+
* @param options - Optional configuration
|
|
23
|
+
* @throws {VerificationError} If the signature is missing, malformed, expired, or invalid
|
|
24
|
+
*/
|
|
25
|
+
declare function verify(payload: string | Buffer, headers: Record<string, string | string[] | undefined>, secret: string, options?: VerifyOptions): void;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Typed representation of HostWebhook delivery headers.
|
|
29
|
+
*/
|
|
30
|
+
interface WebhookHeaders {
|
|
31
|
+
/** Unique event ID (`X-HostWebhook-Event-Id`) */
|
|
32
|
+
eventId: string;
|
|
33
|
+
/** Delivery attempt number (`X-HostWebhook-Attempt`) */
|
|
34
|
+
attempt: number;
|
|
35
|
+
/** Signature timestamp in milliseconds */
|
|
36
|
+
timestamp: number;
|
|
37
|
+
/** Raw HMAC-SHA256 hex signature */
|
|
38
|
+
signature: string;
|
|
39
|
+
/** Chain step number, if this is a chained delivery */
|
|
40
|
+
chainStep?: number;
|
|
41
|
+
/** Original chain payload (base64-decoded JSON) */
|
|
42
|
+
chainOriginal?: unknown;
|
|
43
|
+
/** Previous chain step response (base64-decoded JSON) */
|
|
44
|
+
chainPrevious?: unknown;
|
|
45
|
+
/** Chain response metadata (base64-decoded JSON) */
|
|
46
|
+
chainResponse?: {
|
|
47
|
+
status: number;
|
|
48
|
+
body: unknown;
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
};
|
|
51
|
+
/** Whether the chain response was truncated */
|
|
52
|
+
chainResponseTruncated?: boolean;
|
|
53
|
+
/** Original size of the chain response before truncation */
|
|
54
|
+
chainResponseOriginalSize?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Parse all `X-HostWebhook-*` headers into a typed object.
|
|
58
|
+
*
|
|
59
|
+
* @param headers - The raw request headers
|
|
60
|
+
* @returns Parsed webhook headers
|
|
61
|
+
*/
|
|
62
|
+
declare function parseHeaders(headers: Record<string, string | string[] | undefined>): WebhookHeaders;
|
|
63
|
+
|
|
64
|
+
type NextFn = (err?: unknown) => void;
|
|
65
|
+
interface ExpressRequest {
|
|
66
|
+
body: string | Buffer;
|
|
67
|
+
headers: Record<string, string | string[] | undefined>;
|
|
68
|
+
}
|
|
69
|
+
interface ExpressResponse {
|
|
70
|
+
status(code: number): ExpressResponse;
|
|
71
|
+
json(body: unknown): void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Express middleware that verifies HostWebhook signatures.
|
|
75
|
+
*
|
|
76
|
+
* IMPORTANT: You must use `express.raw({ type: 'application/json' })` before
|
|
77
|
+
* this middleware so that `req.body` is a Buffer (not parsed JSON).
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```ts
|
|
81
|
+
* import express from 'express';
|
|
82
|
+
* import { expressMiddleware } from '@hostwebhook/node';
|
|
83
|
+
*
|
|
84
|
+
* const app = express();
|
|
85
|
+
* app.post('/webhooks',
|
|
86
|
+
* express.raw({ type: 'application/json' }),
|
|
87
|
+
* expressMiddleware(process.env.SIGNING_SECRET!),
|
|
88
|
+
* (req, res) => {
|
|
89
|
+
* const payload = JSON.parse(req.body.toString());
|
|
90
|
+
* res.status(200).json({ received: true });
|
|
91
|
+
* },
|
|
92
|
+
* );
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
declare function expressMiddleware(secret: string, options?: VerifyOptions): (req: ExpressRequest, res: ExpressResponse, next: NextFn) => void;
|
|
96
|
+
/**
|
|
97
|
+
* NestJS guard factory. Returns a class that can be used with @UseGuards().
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* import { Controller, Post, UseGuards, Req } from '@nestjs/common';
|
|
102
|
+
* import { createNestGuard } from '@hostwebhook/node';
|
|
103
|
+
*
|
|
104
|
+
* const WebhookGuard = createNestGuard(process.env.SIGNING_SECRET!);
|
|
105
|
+
*
|
|
106
|
+
* @Controller('webhooks')
|
|
107
|
+
* export class WebhooksController {
|
|
108
|
+
* @Post()
|
|
109
|
+
* @UseGuards(WebhookGuard)
|
|
110
|
+
* handle(@Req() req: Request) {
|
|
111
|
+
* return { received: true };
|
|
112
|
+
* }
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare function createNestGuard(secret: string, options?: VerifyOptions): {
|
|
117
|
+
new (): {
|
|
118
|
+
canActivate(context: {
|
|
119
|
+
switchToHttp(): {
|
|
120
|
+
getRequest(): ExpressRequest;
|
|
121
|
+
};
|
|
122
|
+
}): boolean;
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Fastify preHandler hook that verifies HostWebhook signatures.
|
|
127
|
+
*
|
|
128
|
+
* @example
|
|
129
|
+
* ```ts
|
|
130
|
+
* import Fastify from 'fastify';
|
|
131
|
+
* import { fastifyHook } from '@hostwebhook/node';
|
|
132
|
+
*
|
|
133
|
+
* const app = Fastify({ bodyLimit: 1_048_576 });
|
|
134
|
+
* app.addContentTypeParser('application/json', { parseAs: 'buffer' }, (req, body, done) => done(null, body));
|
|
135
|
+
*
|
|
136
|
+
* app.post('/webhooks', { preHandler: fastifyHook(process.env.SIGNING_SECRET!) }, (req, reply) => {
|
|
137
|
+
* reply.send({ received: true });
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
declare function fastifyHook(secret: string, options?: VerifyOptions): (req: ExpressRequest, reply: ExpressResponse, done: NextFn) => void;
|
|
142
|
+
|
|
143
|
+
/** Configuration for the HostWebhook client. */
|
|
144
|
+
interface HostWebhookConfig {
|
|
145
|
+
/** Ingress token from your endpoint settings. */
|
|
146
|
+
token: string;
|
|
147
|
+
/** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
|
|
148
|
+
baseUrl?: string;
|
|
149
|
+
/**
|
|
150
|
+
* Signing secret for incoming signature verification.
|
|
151
|
+
* When provided, every request is signed with HMAC-SHA256 using the `custom`
|
|
152
|
+
* format (`X-Webhook-Signature: sha256=<hex>`), which HostWebhook verifies
|
|
153
|
+
* if the endpoint has `incomingSignatureType: 'custom'` configured.
|
|
154
|
+
*/
|
|
155
|
+
signingSecret?: string;
|
|
156
|
+
/** Custom headers to include in every request. */
|
|
157
|
+
defaultHeaders?: Record<string, string>;
|
|
158
|
+
/** Request timeout in milliseconds. Default: 10000 (10s). */
|
|
159
|
+
timeoutMs?: number;
|
|
160
|
+
}
|
|
161
|
+
/** Options for a single send call. */
|
|
162
|
+
interface SendOptions {
|
|
163
|
+
/** Custom headers for this specific request (merged with defaultHeaders). */
|
|
164
|
+
headers?: Record<string, string>;
|
|
165
|
+
/** Override the Content-Type. Default: `application/json`. */
|
|
166
|
+
contentType?: string;
|
|
167
|
+
/**
|
|
168
|
+
* Send raw string/Buffer body instead of JSON-stringifying a payload object.
|
|
169
|
+
* When set, the `payload` parameter to `send()` is ignored.
|
|
170
|
+
*/
|
|
171
|
+
rawBody?: string | Buffer;
|
|
172
|
+
}
|
|
173
|
+
/** Result returned after a successful send. */
|
|
174
|
+
interface SendResult {
|
|
175
|
+
/** The event ID assigned by HostWebhook. */
|
|
176
|
+
eventId: string;
|
|
177
|
+
}
|
|
178
|
+
/** Thrown when the HostWebhook API returns a non-2xx response. */
|
|
179
|
+
declare class SendError extends Error {
|
|
180
|
+
readonly statusCode: number;
|
|
181
|
+
readonly responseBody: string;
|
|
182
|
+
constructor(message: string, statusCode: number, responseBody: string);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* HostWebhook client for sending webhooks through the platform.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```ts
|
|
190
|
+
* const hw = new HostWebhook({ token: 'your_ingress_token' });
|
|
191
|
+
* const { eventId } = await hw.send({ event: 'order.created', orderId: '123' });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
declare class HostWebhook {
|
|
195
|
+
private readonly token;
|
|
196
|
+
private readonly baseUrl;
|
|
197
|
+
private readonly signingSecret?;
|
|
198
|
+
private readonly defaultHeaders;
|
|
199
|
+
private readonly timeoutMs;
|
|
200
|
+
constructor(config: HostWebhookConfig);
|
|
201
|
+
/**
|
|
202
|
+
* Send a webhook payload through HostWebhook.
|
|
203
|
+
*
|
|
204
|
+
* The payload is JSON-stringified and POST'd to the ingress endpoint.
|
|
205
|
+
* HostWebhook then handles delivery to the target URL with retries,
|
|
206
|
+
* monitoring, transforms, and signing.
|
|
207
|
+
*
|
|
208
|
+
* @param payload - The webhook payload (will be JSON.stringify'd)
|
|
209
|
+
* @param options - Optional per-request configuration
|
|
210
|
+
* @returns The event ID assigned by HostWebhook
|
|
211
|
+
* @throws {SendError} If the API returns a non-2xx status
|
|
212
|
+
*/
|
|
213
|
+
send(payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
|
|
214
|
+
/**
|
|
215
|
+
* Convenience: send multiple payloads in parallel.
|
|
216
|
+
*
|
|
217
|
+
* @param payloads - Array of payloads to send
|
|
218
|
+
* @param options - Optional per-request configuration (shared for all)
|
|
219
|
+
* @returns Array of results (in same order as payloads)
|
|
220
|
+
*/
|
|
221
|
+
sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Standalone send function (no class instantiation needed).
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* import { send } from '@hostwebhook/node';
|
|
229
|
+
* await send({ token: 'your_token' }, { event: 'test' });
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
|
|
233
|
+
|
|
234
|
+
export { HostWebhook, type HostWebhookConfig, SendError, type SendOptions, type SendResult, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
HostWebhook: () => HostWebhook,
|
|
24
|
+
SendError: () => SendError,
|
|
25
|
+
VerificationError: () => VerificationError,
|
|
26
|
+
createNestGuard: () => createNestGuard,
|
|
27
|
+
expressMiddleware: () => expressMiddleware,
|
|
28
|
+
fastifyHook: () => fastifyHook,
|
|
29
|
+
parseHeaders: () => parseHeaders,
|
|
30
|
+
send: () => send,
|
|
31
|
+
verify: () => verify
|
|
32
|
+
});
|
|
33
|
+
module.exports = __toCommonJS(index_exports);
|
|
34
|
+
|
|
35
|
+
// src/verify.ts
|
|
36
|
+
var import_crypto = require("crypto");
|
|
37
|
+
var VerificationError = class extends Error {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "VerificationError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
function verify(payload, headers, secret, options) {
|
|
44
|
+
const sigHeader = getHeader(headers, "x-hostwebhook-signature");
|
|
45
|
+
if (!sigHeader) {
|
|
46
|
+
throw new VerificationError("Missing X-HostWebhook-Signature header");
|
|
47
|
+
}
|
|
48
|
+
const parts = {};
|
|
49
|
+
for (const part of sigHeader.split(",")) {
|
|
50
|
+
const eq = part.indexOf("=");
|
|
51
|
+
if (eq > 0) {
|
|
52
|
+
parts[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const { t, v1 } = parts;
|
|
56
|
+
if (!t || !v1) {
|
|
57
|
+
throw new VerificationError(
|
|
58
|
+
"Malformed X-HostWebhook-Signature: expected t=<timestamp>,v1=<signature>"
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const timestamp = parseInt(t, 10);
|
|
62
|
+
if (isNaN(timestamp)) {
|
|
63
|
+
throw new VerificationError("Malformed timestamp in signature header");
|
|
64
|
+
}
|
|
65
|
+
const maxAge = options?.maxAgeSec ?? 300;
|
|
66
|
+
if (maxAge > 0) {
|
|
67
|
+
const drift = Math.abs(Date.now() - timestamp);
|
|
68
|
+
if (drift > maxAge * 1e3) {
|
|
69
|
+
throw new VerificationError(
|
|
70
|
+
`Signature timestamp is too old (drift: ${Math.round(drift / 1e3)}s, max: ${maxAge}s)`
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const body = typeof payload === "string" ? payload : payload.toString("utf8");
|
|
75
|
+
const expected = (0, import_crypto.createHmac)("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
76
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
77
|
+
const receivedBuf = Buffer.from(v1, "hex");
|
|
78
|
+
if (expectedBuf.length !== receivedBuf.length || !(0, import_crypto.timingSafeEqual)(expectedBuf, receivedBuf)) {
|
|
79
|
+
throw new VerificationError("Signature mismatch");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function getHeader(headers, key) {
|
|
83
|
+
const val = headers[key] ?? headers[key.toLowerCase()];
|
|
84
|
+
if (Array.isArray(val)) return val[0];
|
|
85
|
+
return val;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/headers.ts
|
|
89
|
+
function parseHeaders(headers) {
|
|
90
|
+
const get = (key) => {
|
|
91
|
+
const val = headers[key] ?? headers[key.toLowerCase()];
|
|
92
|
+
if (Array.isArray(val)) return val[0];
|
|
93
|
+
return val;
|
|
94
|
+
};
|
|
95
|
+
const sigHeader = get("x-hostwebhook-signature") ?? "";
|
|
96
|
+
const sigParts = {};
|
|
97
|
+
for (const part of sigHeader.split(",")) {
|
|
98
|
+
const eq = part.indexOf("=");
|
|
99
|
+
if (eq > 0) {
|
|
100
|
+
sigParts[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const result = {
|
|
104
|
+
eventId: get("x-hostwebhook-event-id") ?? "",
|
|
105
|
+
attempt: parseInt(get("x-hostwebhook-attempt") ?? "1", 10) || 1,
|
|
106
|
+
timestamp: parseInt(sigParts["t"] ?? "0", 10),
|
|
107
|
+
signature: sigParts["v1"] ?? ""
|
|
108
|
+
};
|
|
109
|
+
const chainStep = get("x-hostwebhook-chain-step");
|
|
110
|
+
if (chainStep) {
|
|
111
|
+
result.chainStep = parseInt(chainStep, 10) || void 0;
|
|
112
|
+
}
|
|
113
|
+
const chainOriginal = get("x-hostwebhook-chain-original");
|
|
114
|
+
if (chainOriginal) {
|
|
115
|
+
try {
|
|
116
|
+
result.chainOriginal = JSON.parse(
|
|
117
|
+
Buffer.from(chainOriginal, "base64").toString("utf8")
|
|
118
|
+
);
|
|
119
|
+
} catch {
|
|
120
|
+
result.chainOriginal = chainOriginal;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const chainPrevious = get("x-hostwebhook-chain-previous");
|
|
124
|
+
if (chainPrevious) {
|
|
125
|
+
try {
|
|
126
|
+
result.chainPrevious = JSON.parse(
|
|
127
|
+
Buffer.from(chainPrevious, "base64").toString("utf8")
|
|
128
|
+
);
|
|
129
|
+
} catch {
|
|
130
|
+
result.chainPrevious = chainPrevious;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const chainResponse = get("x-hostwebhook-chain-response");
|
|
134
|
+
if (chainResponse) {
|
|
135
|
+
try {
|
|
136
|
+
result.chainResponse = JSON.parse(
|
|
137
|
+
Buffer.from(chainResponse, "base64").toString("utf8")
|
|
138
|
+
);
|
|
139
|
+
} catch {
|
|
140
|
+
result.chainResponse = void 0;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (get("x-hostwebhook-chain-response-truncated") === "true") {
|
|
144
|
+
result.chainResponseTruncated = true;
|
|
145
|
+
const origSize = get("x-hostwebhook-chain-response-original-size");
|
|
146
|
+
if (origSize) result.chainResponseOriginalSize = parseInt(origSize, 10);
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/middleware.ts
|
|
152
|
+
function expressMiddleware(secret, options) {
|
|
153
|
+
return (req, res, next) => {
|
|
154
|
+
try {
|
|
155
|
+
verify(req.body, req.headers, secret, options);
|
|
156
|
+
next();
|
|
157
|
+
} catch (err) {
|
|
158
|
+
if (err instanceof VerificationError) {
|
|
159
|
+
res.status(401).json({ error: err.message });
|
|
160
|
+
} else {
|
|
161
|
+
next(err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function createNestGuard(secret, options) {
|
|
167
|
+
return class HostWebhookGuard {
|
|
168
|
+
canActivate(context) {
|
|
169
|
+
const req = context.switchToHttp().getRequest();
|
|
170
|
+
verify(req.body, req.headers, secret, options);
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
function fastifyHook(secret, options) {
|
|
176
|
+
return (req, _reply, done) => {
|
|
177
|
+
try {
|
|
178
|
+
verify(req.body, req.headers, secret, options);
|
|
179
|
+
done();
|
|
180
|
+
} catch (err) {
|
|
181
|
+
done(err);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/client.ts
|
|
187
|
+
var import_crypto2 = require("crypto");
|
|
188
|
+
|
|
189
|
+
// src/send-types.ts
|
|
190
|
+
var SendError = class extends Error {
|
|
191
|
+
constructor(message, statusCode, responseBody) {
|
|
192
|
+
super(message);
|
|
193
|
+
this.statusCode = statusCode;
|
|
194
|
+
this.responseBody = responseBody;
|
|
195
|
+
this.name = "SendError";
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// src/client.ts
|
|
200
|
+
var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
|
|
201
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
202
|
+
var HostWebhook = class {
|
|
203
|
+
constructor(config) {
|
|
204
|
+
if (!config.token) {
|
|
205
|
+
throw new Error("HostWebhook: token is required");
|
|
206
|
+
}
|
|
207
|
+
this.token = config.token;
|
|
208
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
209
|
+
this.signingSecret = config.signingSecret;
|
|
210
|
+
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
211
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Send a webhook payload through HostWebhook.
|
|
215
|
+
*
|
|
216
|
+
* The payload is JSON-stringified and POST'd to the ingress endpoint.
|
|
217
|
+
* HostWebhook then handles delivery to the target URL with retries,
|
|
218
|
+
* monitoring, transforms, and signing.
|
|
219
|
+
*
|
|
220
|
+
* @param payload - The webhook payload (will be JSON.stringify'd)
|
|
221
|
+
* @param options - Optional per-request configuration
|
|
222
|
+
* @returns The event ID assigned by HostWebhook
|
|
223
|
+
* @throws {SendError} If the API returns a non-2xx status
|
|
224
|
+
*/
|
|
225
|
+
async send(payload, options) {
|
|
226
|
+
const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
|
|
227
|
+
const headers = {
|
|
228
|
+
"Content-Type": options?.contentType ?? "application/json",
|
|
229
|
+
"User-Agent": "HostWebhook-SDK/1.0",
|
|
230
|
+
...this.defaultHeaders,
|
|
231
|
+
...options?.headers
|
|
232
|
+
};
|
|
233
|
+
if (this.signingSecret) {
|
|
234
|
+
const sig = (0, import_crypto2.createHmac)("sha256", this.signingSecret).update(body).digest("hex");
|
|
235
|
+
headers["X-Webhook-Signature"] = `sha256=${sig}`;
|
|
236
|
+
}
|
|
237
|
+
const url = `${this.baseUrl}/api/in/${this.token}`;
|
|
238
|
+
const controller = new AbortController();
|
|
239
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
240
|
+
try {
|
|
241
|
+
const res = await fetch(url, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers,
|
|
244
|
+
body,
|
|
245
|
+
signal: controller.signal
|
|
246
|
+
});
|
|
247
|
+
const text = await res.text();
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
throw new SendError(
|
|
250
|
+
`HostWebhook ingress error (${res.status}): ${text}`,
|
|
251
|
+
res.status,
|
|
252
|
+
text
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
return JSON.parse(text);
|
|
257
|
+
} catch {
|
|
258
|
+
return { eventId: text };
|
|
259
|
+
}
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof SendError) throw err;
|
|
262
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
263
|
+
throw new SendError(
|
|
264
|
+
`HostWebhook request timed out after ${this.timeoutMs}ms`,
|
|
265
|
+
0,
|
|
266
|
+
""
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
throw err;
|
|
270
|
+
} finally {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Convenience: send multiple payloads in parallel.
|
|
276
|
+
*
|
|
277
|
+
* @param payloads - Array of payloads to send
|
|
278
|
+
* @param options - Optional per-request configuration (shared for all)
|
|
279
|
+
* @returns Array of results (in same order as payloads)
|
|
280
|
+
*/
|
|
281
|
+
async sendBatch(payloads, options) {
|
|
282
|
+
return Promise.all(payloads.map((p) => this.send(p, options)));
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
async function send(config, payload, options) {
|
|
286
|
+
const client = new HostWebhook(config);
|
|
287
|
+
return client.send(payload, options);
|
|
288
|
+
}
|
|
289
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
290
|
+
0 && (module.exports = {
|
|
291
|
+
HostWebhook,
|
|
292
|
+
SendError,
|
|
293
|
+
VerificationError,
|
|
294
|
+
createNestGuard,
|
|
295
|
+
expressMiddleware,
|
|
296
|
+
fastifyHook,
|
|
297
|
+
parseHeaders,
|
|
298
|
+
send,
|
|
299
|
+
verify
|
|
300
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// src/verify.ts
|
|
2
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
3
|
+
var VerificationError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "VerificationError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function verify(payload, headers, secret, options) {
|
|
10
|
+
const sigHeader = getHeader(headers, "x-hostwebhook-signature");
|
|
11
|
+
if (!sigHeader) {
|
|
12
|
+
throw new VerificationError("Missing X-HostWebhook-Signature header");
|
|
13
|
+
}
|
|
14
|
+
const parts = {};
|
|
15
|
+
for (const part of sigHeader.split(",")) {
|
|
16
|
+
const eq = part.indexOf("=");
|
|
17
|
+
if (eq > 0) {
|
|
18
|
+
parts[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const { t, v1 } = parts;
|
|
22
|
+
if (!t || !v1) {
|
|
23
|
+
throw new VerificationError(
|
|
24
|
+
"Malformed X-HostWebhook-Signature: expected t=<timestamp>,v1=<signature>"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const timestamp = parseInt(t, 10);
|
|
28
|
+
if (isNaN(timestamp)) {
|
|
29
|
+
throw new VerificationError("Malformed timestamp in signature header");
|
|
30
|
+
}
|
|
31
|
+
const maxAge = options?.maxAgeSec ?? 300;
|
|
32
|
+
if (maxAge > 0) {
|
|
33
|
+
const drift = Math.abs(Date.now() - timestamp);
|
|
34
|
+
if (drift > maxAge * 1e3) {
|
|
35
|
+
throw new VerificationError(
|
|
36
|
+
`Signature timestamp is too old (drift: ${Math.round(drift / 1e3)}s, max: ${maxAge}s)`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const body = typeof payload === "string" ? payload : payload.toString("utf8");
|
|
41
|
+
const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
|
|
42
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
43
|
+
const receivedBuf = Buffer.from(v1, "hex");
|
|
44
|
+
if (expectedBuf.length !== receivedBuf.length || !timingSafeEqual(expectedBuf, receivedBuf)) {
|
|
45
|
+
throw new VerificationError("Signature mismatch");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function getHeader(headers, key) {
|
|
49
|
+
const val = headers[key] ?? headers[key.toLowerCase()];
|
|
50
|
+
if (Array.isArray(val)) return val[0];
|
|
51
|
+
return val;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// src/headers.ts
|
|
55
|
+
function parseHeaders(headers) {
|
|
56
|
+
const get = (key) => {
|
|
57
|
+
const val = headers[key] ?? headers[key.toLowerCase()];
|
|
58
|
+
if (Array.isArray(val)) return val[0];
|
|
59
|
+
return val;
|
|
60
|
+
};
|
|
61
|
+
const sigHeader = get("x-hostwebhook-signature") ?? "";
|
|
62
|
+
const sigParts = {};
|
|
63
|
+
for (const part of sigHeader.split(",")) {
|
|
64
|
+
const eq = part.indexOf("=");
|
|
65
|
+
if (eq > 0) {
|
|
66
|
+
sigParts[part.slice(0, eq).trim()] = part.slice(eq + 1).trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const result = {
|
|
70
|
+
eventId: get("x-hostwebhook-event-id") ?? "",
|
|
71
|
+
attempt: parseInt(get("x-hostwebhook-attempt") ?? "1", 10) || 1,
|
|
72
|
+
timestamp: parseInt(sigParts["t"] ?? "0", 10),
|
|
73
|
+
signature: sigParts["v1"] ?? ""
|
|
74
|
+
};
|
|
75
|
+
const chainStep = get("x-hostwebhook-chain-step");
|
|
76
|
+
if (chainStep) {
|
|
77
|
+
result.chainStep = parseInt(chainStep, 10) || void 0;
|
|
78
|
+
}
|
|
79
|
+
const chainOriginal = get("x-hostwebhook-chain-original");
|
|
80
|
+
if (chainOriginal) {
|
|
81
|
+
try {
|
|
82
|
+
result.chainOriginal = JSON.parse(
|
|
83
|
+
Buffer.from(chainOriginal, "base64").toString("utf8")
|
|
84
|
+
);
|
|
85
|
+
} catch {
|
|
86
|
+
result.chainOriginal = chainOriginal;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const chainPrevious = get("x-hostwebhook-chain-previous");
|
|
90
|
+
if (chainPrevious) {
|
|
91
|
+
try {
|
|
92
|
+
result.chainPrevious = JSON.parse(
|
|
93
|
+
Buffer.from(chainPrevious, "base64").toString("utf8")
|
|
94
|
+
);
|
|
95
|
+
} catch {
|
|
96
|
+
result.chainPrevious = chainPrevious;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const chainResponse = get("x-hostwebhook-chain-response");
|
|
100
|
+
if (chainResponse) {
|
|
101
|
+
try {
|
|
102
|
+
result.chainResponse = JSON.parse(
|
|
103
|
+
Buffer.from(chainResponse, "base64").toString("utf8")
|
|
104
|
+
);
|
|
105
|
+
} catch {
|
|
106
|
+
result.chainResponse = void 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (get("x-hostwebhook-chain-response-truncated") === "true") {
|
|
110
|
+
result.chainResponseTruncated = true;
|
|
111
|
+
const origSize = get("x-hostwebhook-chain-response-original-size");
|
|
112
|
+
if (origSize) result.chainResponseOriginalSize = parseInt(origSize, 10);
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/middleware.ts
|
|
118
|
+
function expressMiddleware(secret, options) {
|
|
119
|
+
return (req, res, next) => {
|
|
120
|
+
try {
|
|
121
|
+
verify(req.body, req.headers, secret, options);
|
|
122
|
+
next();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof VerificationError) {
|
|
125
|
+
res.status(401).json({ error: err.message });
|
|
126
|
+
} else {
|
|
127
|
+
next(err);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function createNestGuard(secret, options) {
|
|
133
|
+
return class HostWebhookGuard {
|
|
134
|
+
canActivate(context) {
|
|
135
|
+
const req = context.switchToHttp().getRequest();
|
|
136
|
+
verify(req.body, req.headers, secret, options);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function fastifyHook(secret, options) {
|
|
142
|
+
return (req, _reply, done) => {
|
|
143
|
+
try {
|
|
144
|
+
verify(req.body, req.headers, secret, options);
|
|
145
|
+
done();
|
|
146
|
+
} catch (err) {
|
|
147
|
+
done(err);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/client.ts
|
|
153
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
154
|
+
|
|
155
|
+
// src/send-types.ts
|
|
156
|
+
var SendError = class extends Error {
|
|
157
|
+
constructor(message, statusCode, responseBody) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.statusCode = statusCode;
|
|
160
|
+
this.responseBody = responseBody;
|
|
161
|
+
this.name = "SendError";
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/client.ts
|
|
166
|
+
var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
|
|
167
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
168
|
+
var HostWebhook = class {
|
|
169
|
+
constructor(config) {
|
|
170
|
+
if (!config.token) {
|
|
171
|
+
throw new Error("HostWebhook: token is required");
|
|
172
|
+
}
|
|
173
|
+
this.token = config.token;
|
|
174
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
175
|
+
this.signingSecret = config.signingSecret;
|
|
176
|
+
this.defaultHeaders = config.defaultHeaders ?? {};
|
|
177
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Send a webhook payload through HostWebhook.
|
|
181
|
+
*
|
|
182
|
+
* The payload is JSON-stringified and POST'd to the ingress endpoint.
|
|
183
|
+
* HostWebhook then handles delivery to the target URL with retries,
|
|
184
|
+
* monitoring, transforms, and signing.
|
|
185
|
+
*
|
|
186
|
+
* @param payload - The webhook payload (will be JSON.stringify'd)
|
|
187
|
+
* @param options - Optional per-request configuration
|
|
188
|
+
* @returns The event ID assigned by HostWebhook
|
|
189
|
+
* @throws {SendError} If the API returns a non-2xx status
|
|
190
|
+
*/
|
|
191
|
+
async send(payload, options) {
|
|
192
|
+
const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
|
|
193
|
+
const headers = {
|
|
194
|
+
"Content-Type": options?.contentType ?? "application/json",
|
|
195
|
+
"User-Agent": "HostWebhook-SDK/1.0",
|
|
196
|
+
...this.defaultHeaders,
|
|
197
|
+
...options?.headers
|
|
198
|
+
};
|
|
199
|
+
if (this.signingSecret) {
|
|
200
|
+
const sig = createHmac2("sha256", this.signingSecret).update(body).digest("hex");
|
|
201
|
+
headers["X-Webhook-Signature"] = `sha256=${sig}`;
|
|
202
|
+
}
|
|
203
|
+
const url = `${this.baseUrl}/api/in/${this.token}`;
|
|
204
|
+
const controller = new AbortController();
|
|
205
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
206
|
+
try {
|
|
207
|
+
const res = await fetch(url, {
|
|
208
|
+
method: "POST",
|
|
209
|
+
headers,
|
|
210
|
+
body,
|
|
211
|
+
signal: controller.signal
|
|
212
|
+
});
|
|
213
|
+
const text = await res.text();
|
|
214
|
+
if (!res.ok) {
|
|
215
|
+
throw new SendError(
|
|
216
|
+
`HostWebhook ingress error (${res.status}): ${text}`,
|
|
217
|
+
res.status,
|
|
218
|
+
text
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(text);
|
|
223
|
+
} catch {
|
|
224
|
+
return { eventId: text };
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if (err instanceof SendError) throw err;
|
|
228
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
229
|
+
throw new SendError(
|
|
230
|
+
`HostWebhook request timed out after ${this.timeoutMs}ms`,
|
|
231
|
+
0,
|
|
232
|
+
""
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
throw err;
|
|
236
|
+
} finally {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Convenience: send multiple payloads in parallel.
|
|
242
|
+
*
|
|
243
|
+
* @param payloads - Array of payloads to send
|
|
244
|
+
* @param options - Optional per-request configuration (shared for all)
|
|
245
|
+
* @returns Array of results (in same order as payloads)
|
|
246
|
+
*/
|
|
247
|
+
async sendBatch(payloads, options) {
|
|
248
|
+
return Promise.all(payloads.map((p) => this.send(p, options)));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
async function send(config, payload, options) {
|
|
252
|
+
const client = new HostWebhook(config);
|
|
253
|
+
return client.send(payload, options);
|
|
254
|
+
}
|
|
255
|
+
export {
|
|
256
|
+
HostWebhook,
|
|
257
|
+
SendError,
|
|
258
|
+
VerificationError,
|
|
259
|
+
createNestGuard,
|
|
260
|
+
expressMiddleware,
|
|
261
|
+
fastifyHook,
|
|
262
|
+
parseHeaders,
|
|
263
|
+
send,
|
|
264
|
+
verify
|
|
265
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hostwebhook/node",
|
|
3
|
+
"version": "1.0.0",
|
|
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
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
20
|
+
"test": "node --test __tests__/sdk.test.mjs",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"hostwebhook",
|
|
25
|
+
"webhook",
|
|
26
|
+
"signature",
|
|
27
|
+
"verify",
|
|
28
|
+
"send",
|
|
29
|
+
"hmac",
|
|
30
|
+
"retry"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/node": "^25.3.3",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.3.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
}
|
|
41
|
+
}
|