@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.
@@ -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 };
@@ -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
+ }