@hostwebhook/node 1.3.0 → 1.4.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,355 @@
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
+ /** Static secret signature — sends the secret as-is in a header (no HMAC). */
144
+ interface StaticSignature {
145
+ type: 'static';
146
+ /** The secret/token value to send. */
147
+ secret: string;
148
+ /**
149
+ * Header name to send the secret in.
150
+ * Default: `Authorization` with `Bearer <secret>` format.
151
+ * @example 'X-My-Custom-Key'
152
+ */
153
+ header?: string;
154
+ }
155
+ /** Configuration for the HostWebhook client. */
156
+ interface HostWebhookConfig {
157
+ /**
158
+ * API key (`hwk_...`) for authentication. Required for file uploads
159
+ * and recommended for all operations. Must have appropriate scopes.
160
+ */
161
+ apiKey?: string;
162
+ /**
163
+ * Default ingress token. When set, `send(payload)` uses this token.
164
+ * For multi-endpoint usage, pass the token per-call: `send(token, payload)`.
165
+ */
166
+ token?: string;
167
+ /** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
168
+ baseUrl?: string;
169
+ /**
170
+ * HMAC-SHA256 signing secret. When set, every request is signed with the
171
+ * HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
172
+ * Pair with `incomingSignatureType: 'hostwebhook'` on the endpoint.
173
+ * @deprecated Prefer `apiKey` for authentication.
174
+ */
175
+ signingSecret?: string;
176
+ /**
177
+ * Signature configuration for incoming webhook verification.
178
+ * Cannot be used together with `signingSecret`.
179
+ *
180
+ * @example Static with default header (Authorization: Bearer)
181
+ * ```ts
182
+ * { type: 'static', secret: 'my-secret' }
183
+ * ```
184
+ * @example Static with custom header
185
+ * ```ts
186
+ * { type: 'static', secret: 'my-secret', header: 'X-My-Custom-Key' }
187
+ * ```
188
+ */
189
+ signature?: StaticSignature;
190
+ /** Custom headers to include in every request. */
191
+ defaultHeaders?: Record<string, string>;
192
+ /** Request timeout in milliseconds. Default: 10000 (10s). */
193
+ timeoutMs?: number;
194
+ }
195
+ /** A single pipeline execution step received via SSE stream. */
196
+ interface PipelineStep {
197
+ eventId?: string;
198
+ nodeType: string;
199
+ nodeId: string;
200
+ nodeName?: string;
201
+ status: 'running' | 'success' | 'error' | 'blocked';
202
+ statusCode?: number;
203
+ durationMs?: number;
204
+ error?: string;
205
+ lastPayload?: Record<string, unknown>;
206
+ }
207
+ /** Options for a single send call. */
208
+ interface SendOptions {
209
+ /** Custom headers for this specific request (merged with defaultHeaders). */
210
+ headers?: Record<string, string>;
211
+ /** Override the Content-Type. Default: `application/json`. */
212
+ contentType?: string;
213
+ /**
214
+ * Send raw string/Buffer body instead of JSON-stringifying a payload object.
215
+ * When set, the `payload` parameter to `send()` is ignored.
216
+ */
217
+ rawBody?: string | Buffer;
218
+ /**
219
+ * Wait for the full pipeline result via SSE streaming (blocking).
220
+ * The SDK will POST the payload, then open an SSE stream to receive
221
+ * pipeline steps and the final result. Returns when the pipeline completes.
222
+ * Timeout: ~125s (slightly above the server's 120s SSE timeout).
223
+ */
224
+ waitForResult?: boolean;
225
+ /**
226
+ * Non-blocking pipeline result callback. `send()` returns immediately with
227
+ * the eventId, and the callback fires in the background when the pipeline
228
+ * completes (via SSE stream). Perfect for UIs that shouldn't block.
229
+ *
230
+ * Requires the endpoint to be in `none` response mode (async).
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const { eventId } = await hw.send(payload, {
235
+ * onResult: (result) => console.log('Pipeline done:', result.syncStatus),
236
+ * });
237
+ * // UI shows "Sent!" immediately here
238
+ * ```
239
+ */
240
+ onResult?: (result: SendResult) => void;
241
+ /**
242
+ * Called for each pipeline step during SSE streaming.
243
+ * Only fires when `onResult` or `waitForResult` is used.
244
+ */
245
+ onStep?: (step: PipelineStep) => void;
246
+ }
247
+ /** Result returned after a successful send. */
248
+ interface SendResult {
249
+ /** The event ID assigned by HostWebhook. */
250
+ eventId: string;
251
+ /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
252
+ streamToken?: string;
253
+ /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
254
+ syncStatus?: number;
255
+ /** Sync/stream: delivery status ('delivered') */
256
+ deliveryStatus?: string;
257
+ /** Sync/stream: HTTP status code from the target URL */
258
+ statusCode?: number;
259
+ /** Sync/stream: response body from the target URL (truncated to 4KB) */
260
+ response?: string;
261
+ /** Sync/stream: delivery latency in ms */
262
+ latencyMs?: number;
263
+ /** Sync/stream: error message if pipeline/delivery failed */
264
+ error?: string;
265
+ /** Sync/stream: validation error messages (schema validator) */
266
+ message?: string[];
267
+ /** SSE stream: pipeline execution steps (only when waitForResult is used) */
268
+ steps?: PipelineStep[];
269
+ /** Any additional fields from the sync response */
270
+ [key: string]: unknown;
271
+ }
272
+ /** Thrown when the HostWebhook API returns a non-2xx response. */
273
+ declare class SendError extends Error {
274
+ readonly statusCode: number;
275
+ readonly responseBody: string;
276
+ constructor(message: string, statusCode: number, responseBody: string);
277
+ }
278
+
279
+ /**
280
+ * HostWebhook client for sending webhooks through the platform.
281
+ *
282
+ * @example Single-endpoint
283
+ * ```ts
284
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
285
+ * await hw.send({ event: 'order.created' });
286
+ * ```
287
+ *
288
+ * @example Multi-endpoint
289
+ * ```ts
290
+ * const hw = new HostWebhook({ apiKey: 'hwk_...' });
291
+ * await hw.send('token_a', { event: 'order.created' });
292
+ * await hw.send('token_b', { event: 'user.updated' });
293
+ * ```
294
+ */
295
+ declare class HostWebhook {
296
+ #private;
297
+ constructor(config: HostWebhookConfig);
298
+ /**
299
+ * Send a webhook payload through HostWebhook.
300
+ *
301
+ * @overload send(payload, options?) — uses default token from constructor
302
+ * @overload send(token, payload, options?) — specify token per call
303
+ *
304
+ * @throws {SendError} If the API returns a non-2xx status
305
+ * @throws {Error} If no token is available
306
+ */
307
+ send(tokenOrPayload: string | Record<string, unknown>, payloadOrOptions?: Record<string, unknown> | SendOptions, maybeOptions?: SendOptions): Promise<SendResult>;
308
+ /**
309
+ * Convenience: send multiple payloads in parallel.
310
+ *
311
+ * @param payloads - Array of payloads to send
312
+ * @param options - Optional per-request configuration (shared for all)
313
+ * @returns Array of results (in same order as payloads)
314
+ */
315
+ sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
316
+ /**
317
+ * Send a webhook with file attachments.
318
+ *
319
+ * Requires `apiKey` with `canvas:write` scope for file operations.
320
+ * Files are uploaded via presigned URLs, then references are injected into the payload.
321
+ *
322
+ * @overload sendWithFiles(payload, files, options?) — uses default token
323
+ * @overload sendWithFiles(token, payload, files, options?) — specify token per call
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
328
+ * await hw.sendWithFiles(
329
+ * { orderId: '123' },
330
+ * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
331
+ * );
332
+ * ```
333
+ */
334
+ sendWithFiles(tokenOrPayload: string | Record<string, unknown>, payloadOrFiles: Record<string, unknown> | Record<string, {
335
+ data: Buffer | Blob | Uint8Array;
336
+ fileName: string;
337
+ mimeType: string;
338
+ }>, filesOrOptions?: Record<string, {
339
+ data: Buffer | Blob | Uint8Array;
340
+ fileName: string;
341
+ mimeType: string;
342
+ }> | SendOptions, maybeOptions?: SendOptions): Promise<SendResult>;
343
+ }
344
+ /**
345
+ * Standalone send function (no class instantiation needed).
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * import { send } from '@hostwebhook/node';
350
+ * await send({ token: 'your_token' }, { event: 'test' });
351
+ * ```
352
+ */
353
+ declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
354
+
355
+ export { HostWebhook, type HostWebhookConfig, type PipelineStep, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
@@ -0,0 +1,355 @@
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
+ /** Static secret signature — sends the secret as-is in a header (no HMAC). */
144
+ interface StaticSignature {
145
+ type: 'static';
146
+ /** The secret/token value to send. */
147
+ secret: string;
148
+ /**
149
+ * Header name to send the secret in.
150
+ * Default: `Authorization` with `Bearer <secret>` format.
151
+ * @example 'X-My-Custom-Key'
152
+ */
153
+ header?: string;
154
+ }
155
+ /** Configuration for the HostWebhook client. */
156
+ interface HostWebhookConfig {
157
+ /**
158
+ * API key (`hwk_...`) for authentication. Required for file uploads
159
+ * and recommended for all operations. Must have appropriate scopes.
160
+ */
161
+ apiKey?: string;
162
+ /**
163
+ * Default ingress token. When set, `send(payload)` uses this token.
164
+ * For multi-endpoint usage, pass the token per-call: `send(token, payload)`.
165
+ */
166
+ token?: string;
167
+ /** Base URL of the HostWebhook API. Default: `https://api.hostwebhook.com` */
168
+ baseUrl?: string;
169
+ /**
170
+ * HMAC-SHA256 signing secret. When set, every request is signed with the
171
+ * HostWebhook signature format (`X-HostWebhook-Signature: t=<ts>,v1=<hmac>`).
172
+ * Pair with `incomingSignatureType: 'hostwebhook'` on the endpoint.
173
+ * @deprecated Prefer `apiKey` for authentication.
174
+ */
175
+ signingSecret?: string;
176
+ /**
177
+ * Signature configuration for incoming webhook verification.
178
+ * Cannot be used together with `signingSecret`.
179
+ *
180
+ * @example Static with default header (Authorization: Bearer)
181
+ * ```ts
182
+ * { type: 'static', secret: 'my-secret' }
183
+ * ```
184
+ * @example Static with custom header
185
+ * ```ts
186
+ * { type: 'static', secret: 'my-secret', header: 'X-My-Custom-Key' }
187
+ * ```
188
+ */
189
+ signature?: StaticSignature;
190
+ /** Custom headers to include in every request. */
191
+ defaultHeaders?: Record<string, string>;
192
+ /** Request timeout in milliseconds. Default: 10000 (10s). */
193
+ timeoutMs?: number;
194
+ }
195
+ /** A single pipeline execution step received via SSE stream. */
196
+ interface PipelineStep {
197
+ eventId?: string;
198
+ nodeType: string;
199
+ nodeId: string;
200
+ nodeName?: string;
201
+ status: 'running' | 'success' | 'error' | 'blocked';
202
+ statusCode?: number;
203
+ durationMs?: number;
204
+ error?: string;
205
+ lastPayload?: Record<string, unknown>;
206
+ }
207
+ /** Options for a single send call. */
208
+ interface SendOptions {
209
+ /** Custom headers for this specific request (merged with defaultHeaders). */
210
+ headers?: Record<string, string>;
211
+ /** Override the Content-Type. Default: `application/json`. */
212
+ contentType?: string;
213
+ /**
214
+ * Send raw string/Buffer body instead of JSON-stringifying a payload object.
215
+ * When set, the `payload` parameter to `send()` is ignored.
216
+ */
217
+ rawBody?: string | Buffer;
218
+ /**
219
+ * Wait for the full pipeline result via SSE streaming (blocking).
220
+ * The SDK will POST the payload, then open an SSE stream to receive
221
+ * pipeline steps and the final result. Returns when the pipeline completes.
222
+ * Timeout: ~125s (slightly above the server's 120s SSE timeout).
223
+ */
224
+ waitForResult?: boolean;
225
+ /**
226
+ * Non-blocking pipeline result callback. `send()` returns immediately with
227
+ * the eventId, and the callback fires in the background when the pipeline
228
+ * completes (via SSE stream). Perfect for UIs that shouldn't block.
229
+ *
230
+ * Requires the endpoint to be in `none` response mode (async).
231
+ *
232
+ * @example
233
+ * ```ts
234
+ * const { eventId } = await hw.send(payload, {
235
+ * onResult: (result) => console.log('Pipeline done:', result.syncStatus),
236
+ * });
237
+ * // UI shows "Sent!" immediately here
238
+ * ```
239
+ */
240
+ onResult?: (result: SendResult) => void;
241
+ /**
242
+ * Called for each pipeline step during SSE streaming.
243
+ * Only fires when `onResult` or `waitForResult` is used.
244
+ */
245
+ onStep?: (step: PipelineStep) => void;
246
+ }
247
+ /** Result returned after a successful send. */
248
+ interface SendResult {
249
+ /** The event ID assigned by HostWebhook. */
250
+ eventId: string;
251
+ /** Short-lived token for SSE stream auth (returned by ingress, used internally by waitForResult). */
252
+ streamToken?: string;
253
+ /** Sync/stream: pipeline result status (200=delivered, 422=validation failed, 502=delivery failed, 408=timeout) */
254
+ syncStatus?: number;
255
+ /** Sync/stream: delivery status ('delivered') */
256
+ deliveryStatus?: string;
257
+ /** Sync/stream: HTTP status code from the target URL */
258
+ statusCode?: number;
259
+ /** Sync/stream: response body from the target URL (truncated to 4KB) */
260
+ response?: string;
261
+ /** Sync/stream: delivery latency in ms */
262
+ latencyMs?: number;
263
+ /** Sync/stream: error message if pipeline/delivery failed */
264
+ error?: string;
265
+ /** Sync/stream: validation error messages (schema validator) */
266
+ message?: string[];
267
+ /** SSE stream: pipeline execution steps (only when waitForResult is used) */
268
+ steps?: PipelineStep[];
269
+ /** Any additional fields from the sync response */
270
+ [key: string]: unknown;
271
+ }
272
+ /** Thrown when the HostWebhook API returns a non-2xx response. */
273
+ declare class SendError extends Error {
274
+ readonly statusCode: number;
275
+ readonly responseBody: string;
276
+ constructor(message: string, statusCode: number, responseBody: string);
277
+ }
278
+
279
+ /**
280
+ * HostWebhook client for sending webhooks through the platform.
281
+ *
282
+ * @example Single-endpoint
283
+ * ```ts
284
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
285
+ * await hw.send({ event: 'order.created' });
286
+ * ```
287
+ *
288
+ * @example Multi-endpoint
289
+ * ```ts
290
+ * const hw = new HostWebhook({ apiKey: 'hwk_...' });
291
+ * await hw.send('token_a', { event: 'order.created' });
292
+ * await hw.send('token_b', { event: 'user.updated' });
293
+ * ```
294
+ */
295
+ declare class HostWebhook {
296
+ #private;
297
+ constructor(config: HostWebhookConfig);
298
+ /**
299
+ * Send a webhook payload through HostWebhook.
300
+ *
301
+ * @overload send(payload, options?) — uses default token from constructor
302
+ * @overload send(token, payload, options?) — specify token per call
303
+ *
304
+ * @throws {SendError} If the API returns a non-2xx status
305
+ * @throws {Error} If no token is available
306
+ */
307
+ send(tokenOrPayload: string | Record<string, unknown>, payloadOrOptions?: Record<string, unknown> | SendOptions, maybeOptions?: SendOptions): Promise<SendResult>;
308
+ /**
309
+ * Convenience: send multiple payloads in parallel.
310
+ *
311
+ * @param payloads - Array of payloads to send
312
+ * @param options - Optional per-request configuration (shared for all)
313
+ * @returns Array of results (in same order as payloads)
314
+ */
315
+ sendBatch(payloads: Record<string, unknown>[], options?: SendOptions): Promise<SendResult[]>;
316
+ /**
317
+ * Send a webhook with file attachments.
318
+ *
319
+ * Requires `apiKey` with `canvas:write` scope for file operations.
320
+ * Files are uploaded via presigned URLs, then references are injected into the payload.
321
+ *
322
+ * @overload sendWithFiles(payload, files, options?) — uses default token
323
+ * @overload sendWithFiles(token, payload, files, options?) — specify token per call
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
328
+ * await hw.sendWithFiles(
329
+ * { orderId: '123' },
330
+ * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
331
+ * );
332
+ * ```
333
+ */
334
+ sendWithFiles(tokenOrPayload: string | Record<string, unknown>, payloadOrFiles: Record<string, unknown> | Record<string, {
335
+ data: Buffer | Blob | Uint8Array;
336
+ fileName: string;
337
+ mimeType: string;
338
+ }>, filesOrOptions?: Record<string, {
339
+ data: Buffer | Blob | Uint8Array;
340
+ fileName: string;
341
+ mimeType: string;
342
+ }> | SendOptions, maybeOptions?: SendOptions): Promise<SendResult>;
343
+ }
344
+ /**
345
+ * Standalone send function (no class instantiation needed).
346
+ *
347
+ * @example
348
+ * ```ts
349
+ * import { send } from '@hostwebhook/node';
350
+ * await send({ token: 'your_token' }, { event: 'test' });
351
+ * ```
352
+ */
353
+ declare function send(config: HostWebhookConfig, payload: Record<string, unknown>, options?: SendOptions): Promise<SendResult>;
354
+
355
+ export { HostWebhook, type HostWebhookConfig, type PipelineStep, SendError, type SendOptions, type SendResult, type StaticSignature, VerificationError, type VerifyOptions, type WebhookHeaders, createNestGuard, expressMiddleware, fastifyHook, parseHeaders, send, verify };
package/dist/index.js CHANGED
@@ -207,24 +207,26 @@ var SendError = class extends Error {
207
207
  // src/client.ts
208
208
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
209
209
  var DEFAULT_TIMEOUT = 1e4;
210
- var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, waitForStream_fn;
210
+ var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
211
211
  var HostWebhook = class {
212
212
  constructor(config) {
213
213
  __privateAdd(this, _HostWebhook_instances);
214
+ __privateAdd(this, _apiKey);
214
215
  __privateAdd(this, _token);
215
216
  __privateAdd(this, _baseUrl);
216
217
  __privateAdd(this, _signingSecret);
217
218
  __privateAdd(this, _signature);
218
219
  __privateAdd(this, _defaultHeaders);
219
220
  __privateAdd(this, _timeoutMs);
220
- if (!config.token) {
221
- throw new Error("HostWebhook: token is required");
221
+ if (!config.apiKey && !config.token) {
222
+ throw new Error("HostWebhook: apiKey or token is required");
222
223
  }
223
224
  if (config.signingSecret && config.signature) {
224
225
  throw new Error(
225
226
  "HostWebhook: use either signingSecret or signature, not both"
226
227
  );
227
228
  }
229
+ __privateSet(this, _apiKey, config.apiKey);
228
230
  __privateSet(this, _token, config.token);
229
231
  __privateSet(this, _baseUrl, (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""));
230
232
  __privateSet(this, _signingSecret, config.signingSecret);
@@ -235,91 +237,29 @@ var HostWebhook = class {
235
237
  /**
236
238
  * Send a webhook payload through HostWebhook.
237
239
  *
238
- * The payload is JSON-stringified and POST'd to the ingress endpoint.
239
- * HostWebhook then handles delivery to the target URL with retries,
240
- * monitoring, transforms, and signing.
240
+ * @overload send(payload, options?) — uses default token from constructor
241
+ * @overload send(token, payload, options?) — specify token per call
241
242
  *
242
- * @param payload - The webhook payload (will be JSON.stringify'd)
243
- * @param options - Optional per-request configuration
244
- * @returns The event ID assigned by HostWebhook
245
243
  * @throws {SendError} If the API returns a non-2xx status
244
+ * @throws {Error} If no token is available
246
245
  */
247
- async send(payload, options) {
248
- const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
249
- let tz = "";
250
- try {
251
- tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
252
- } catch {
253
- }
254
- const headers = {
255
- "Content-Type": options?.contentType ?? "application/json",
256
- "User-Agent": `HostWebhook-SDK/${"1.3.0"}`,
257
- "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
258
- ...tz ? { "X-HostWebhook-Timezone": tz } : {},
259
- ...__privateGet(this, _defaultHeaders),
260
- ...options?.headers
261
- };
262
- if (__privateGet(this, _signature)?.type === "static") {
263
- const { secret, header } = __privateGet(this, _signature);
264
- if (header) {
265
- headers[header] = secret;
266
- } else {
267
- headers["Authorization"] = `Bearer ${secret}`;
246
+ async send(tokenOrPayload, payloadOrOptions, maybeOptions) {
247
+ let token;
248
+ let payload;
249
+ let options;
250
+ if (typeof tokenOrPayload === "string") {
251
+ token = tokenOrPayload;
252
+ payload = payloadOrOptions ?? {};
253
+ options = maybeOptions;
254
+ } else {
255
+ if (!__privateGet(this, _token)) {
256
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument: send(token, payload)");
268
257
  }
269
- } else if (__privateGet(this, _signingSecret)) {
270
- const timestamp = Math.floor(Date.now() / 1e3);
271
- const sig = (0, import_crypto2.createHmac)("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
272
- headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
258
+ token = __privateGet(this, _token);
259
+ payload = tokenOrPayload;
260
+ options = payloadOrOptions;
273
261
  }
274
- const url = `${__privateGet(this, _baseUrl)}/api/in/${__privateGet(this, _token)}`;
275
- const controller = new AbortController();
276
- const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
277
- let ingressResult;
278
- try {
279
- const res = await fetch(url, {
280
- method: "POST",
281
- headers,
282
- body,
283
- signal: controller.signal
284
- });
285
- const text = await res.text();
286
- if (!res.ok) {
287
- throw new SendError(
288
- `HostWebhook ingress error (${res.status}): ${text}`,
289
- res.status,
290
- text
291
- );
292
- }
293
- try {
294
- ingressResult = JSON.parse(text);
295
- } catch {
296
- ingressResult = { eventId: text };
297
- }
298
- } catch (err) {
299
- if (err instanceof SendError) throw err;
300
- if (err instanceof DOMException && err.name === "AbortError") {
301
- throw new SendError(
302
- `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
303
- 0,
304
- ""
305
- );
306
- }
307
- throw err;
308
- } finally {
309
- clearTimeout(timer);
310
- }
311
- if (ingressResult.syncStatus !== void 0) {
312
- if (options?.onResult) options.onResult(ingressResult);
313
- return ingressResult;
314
- }
315
- if (options?.waitForResult && ingressResult.streamToken) {
316
- return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
317
- }
318
- if (options?.onResult && ingressResult.streamToken) {
319
- __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
320
- });
321
- }
322
- return ingressResult;
262
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, payload, options);
323
263
  }
324
264
  /**
325
265
  * Convenience: send multiple payloads in parallel.
@@ -334,41 +274,60 @@ var HostWebhook = class {
334
274
  /**
335
275
  * Send a webhook with file attachments.
336
276
  *
337
- * Files are uploaded directly to storage via presigned URLs (zero memory on API),
338
- * then file references are injected into the payload before sending.
277
+ * Requires `apiKey` with `canvas:write` scope for file operations.
278
+ * Files are uploaded via presigned URLs, then references are injected into the payload.
339
279
  *
340
- * @param payload - The webhook payload
341
- * @param files - Map of field name → file (Buffer or Blob with metadata)
342
- * @param options - Optional per-request configuration
280
+ * @overload sendWithFiles(payload, files, options?) — uses default token
281
+ * @overload sendWithFiles(token, payload, files, options?) — specify token per call
343
282
  *
344
283
  * @example
345
284
  * ```ts
346
- * const hw = new HostWebhook({ token: 'your_token' });
285
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
347
286
  * await hw.sendWithFiles(
348
- * { orderId: '123', type: 'invoice' },
287
+ * { orderId: '123' },
349
288
  * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
350
289
  * );
351
290
  * ```
352
291
  */
353
- async sendWithFiles(payload, files, options) {
292
+ async sendWithFiles(tokenOrPayload, payloadOrFiles, filesOrOptions, maybeOptions) {
293
+ let token;
294
+ let payload;
295
+ let files;
296
+ let options;
297
+ if (typeof tokenOrPayload === "string") {
298
+ token = tokenOrPayload;
299
+ payload = payloadOrFiles;
300
+ files = filesOrOptions ?? {};
301
+ options = maybeOptions;
302
+ } else {
303
+ if (!__privateGet(this, _token)) {
304
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument");
305
+ }
306
+ token = __privateGet(this, _token);
307
+ payload = tokenOrPayload;
308
+ files = payloadOrFiles ?? {};
309
+ options = filesOrOptions;
310
+ }
311
+ if (!__privateGet(this, _apiKey)) {
312
+ throw new Error("HostWebhook: apiKey is required for sendWithFiles (needs canvas:write scope)");
313
+ }
354
314
  const enrichedPayload = { ...payload };
355
315
  for (const [fieldName, file] of Object.entries(files)) {
356
316
  const uploadRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/upload-url`, {
357
317
  method: "POST",
358
318
  headers: {
359
319
  "Content-Type": "application/json",
360
- "Authorization": `Bearer ${__privateGet(this, _token)}`
320
+ "Authorization": `Bearer ${__privateGet(this, _apiKey)}`
361
321
  },
362
322
  body: JSON.stringify({ fileName: file.fileName, mimeType: file.mimeType })
363
323
  });
364
324
  if (!uploadRes.ok) throw new SendError(`Failed to get upload URL for ${fieldName}`, uploadRes.status, await uploadRes.text());
365
325
  const { fileId, uploadUrl, key } = await uploadRes.json();
366
- const uploadBody = file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.mimeType });
367
- const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: uploadBody });
326
+ const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: file.data });
368
327
  if (!putRes.ok) throw new SendError(`Failed to upload ${fieldName} to storage`, putRes.status, await putRes.text());
369
328
  const confirmRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/${fileId}/confirm`, {
370
329
  method: "POST",
371
- headers: { "Authorization": `Bearer ${__privateGet(this, _token)}` }
330
+ headers: { "Authorization": `Bearer ${__privateGet(this, _apiKey)}` }
372
331
  });
373
332
  if (!confirmRes.ok) throw new SendError(`Failed to confirm upload for ${fieldName}`, confirmRes.status, await confirmRes.text());
374
333
  const confirmed = await confirmRes.json();
@@ -376,9 +335,10 @@ var HostWebhook = class {
376
335
  _file: { id: confirmed.id, key, originalName: confirmed.originalName, mimeType: confirmed.mimeType, size: confirmed.size }
377
336
  };
378
337
  }
379
- return this.send(enrichedPayload, options);
338
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, enrichedPayload, options);
380
339
  }
381
340
  };
341
+ _apiKey = new WeakMap();
382
342
  _token = new WeakMap();
383
343
  _baseUrl = new WeakMap();
384
344
  _signingSecret = new WeakMap();
@@ -386,6 +346,88 @@ _signature = new WeakMap();
386
346
  _defaultHeaders = new WeakMap();
387
347
  _timeoutMs = new WeakMap();
388
348
  _HostWebhook_instances = new WeakSet();
349
+ doSend_fn = async function(token, payload, options) {
350
+ const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
351
+ let tz = "";
352
+ try {
353
+ tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
354
+ } catch {
355
+ }
356
+ const headers = {
357
+ "Content-Type": options?.contentType ?? "application/json",
358
+ "User-Agent": `HostWebhook-SDK/${"1.4.0"}`,
359
+ "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
360
+ ...tz ? { "X-HostWebhook-Timezone": tz } : {},
361
+ ...__privateGet(this, _defaultHeaders),
362
+ ...options?.headers
363
+ };
364
+ if (__privateGet(this, _apiKey)) {
365
+ headers["Authorization"] = `Bearer ${__privateGet(this, _apiKey)}`;
366
+ }
367
+ if (!__privateGet(this, _apiKey)) {
368
+ if (__privateGet(this, _signature)?.type === "static") {
369
+ const { secret, header } = __privateGet(this, _signature);
370
+ if (header) {
371
+ headers[header] = secret;
372
+ } else {
373
+ headers["Authorization"] = `Bearer ${secret}`;
374
+ }
375
+ } else if (__privateGet(this, _signingSecret)) {
376
+ const timestamp = Math.floor(Date.now() / 1e3);
377
+ const sig = (0, import_crypto2.createHmac)("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
378
+ headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
379
+ }
380
+ }
381
+ const url = `${__privateGet(this, _baseUrl)}/api/in/${token}`;
382
+ const controller = new AbortController();
383
+ const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
384
+ let ingressResult;
385
+ try {
386
+ const res = await fetch(url, {
387
+ method: "POST",
388
+ headers,
389
+ body,
390
+ signal: controller.signal
391
+ });
392
+ const text = await res.text();
393
+ if (!res.ok) {
394
+ throw new SendError(
395
+ `HostWebhook ingress error (${res.status}): ${text}`,
396
+ res.status,
397
+ text
398
+ );
399
+ }
400
+ try {
401
+ ingressResult = JSON.parse(text);
402
+ } catch {
403
+ ingressResult = { eventId: text };
404
+ }
405
+ } catch (err) {
406
+ if (err instanceof SendError) throw err;
407
+ if (err instanceof DOMException && err.name === "AbortError") {
408
+ throw new SendError(
409
+ `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
410
+ 0,
411
+ ""
412
+ );
413
+ }
414
+ throw err;
415
+ } finally {
416
+ clearTimeout(timer);
417
+ }
418
+ if (ingressResult.syncStatus !== void 0) {
419
+ if (options?.onResult) options.onResult(ingressResult);
420
+ return ingressResult;
421
+ }
422
+ if (options?.waitForResult && ingressResult.streamToken) {
423
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
424
+ }
425
+ if (options?.onResult && ingressResult.streamToken) {
426
+ __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
427
+ });
428
+ }
429
+ return ingressResult;
430
+ };
389
431
  waitForStream_fn = async function(ingressResult, onStep) {
390
432
  const { eventId, streamToken } = ingressResult;
391
433
  const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
package/dist/index.mjs CHANGED
@@ -174,24 +174,26 @@ var SendError = class extends Error {
174
174
  // src/client.ts
175
175
  var DEFAULT_BASE_URL = "https://api.hostwebhook.com";
176
176
  var DEFAULT_TIMEOUT = 1e4;
177
- var _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, waitForStream_fn;
177
+ var _apiKey, _token, _baseUrl, _signingSecret, _signature, _defaultHeaders, _timeoutMs, _HostWebhook_instances, doSend_fn, waitForStream_fn;
178
178
  var HostWebhook = class {
179
179
  constructor(config) {
180
180
  __privateAdd(this, _HostWebhook_instances);
181
+ __privateAdd(this, _apiKey);
181
182
  __privateAdd(this, _token);
182
183
  __privateAdd(this, _baseUrl);
183
184
  __privateAdd(this, _signingSecret);
184
185
  __privateAdd(this, _signature);
185
186
  __privateAdd(this, _defaultHeaders);
186
187
  __privateAdd(this, _timeoutMs);
187
- if (!config.token) {
188
- throw new Error("HostWebhook: token is required");
188
+ if (!config.apiKey && !config.token) {
189
+ throw new Error("HostWebhook: apiKey or token is required");
189
190
  }
190
191
  if (config.signingSecret && config.signature) {
191
192
  throw new Error(
192
193
  "HostWebhook: use either signingSecret or signature, not both"
193
194
  );
194
195
  }
196
+ __privateSet(this, _apiKey, config.apiKey);
195
197
  __privateSet(this, _token, config.token);
196
198
  __privateSet(this, _baseUrl, (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""));
197
199
  __privateSet(this, _signingSecret, config.signingSecret);
@@ -202,91 +204,29 @@ var HostWebhook = class {
202
204
  /**
203
205
  * Send a webhook payload through HostWebhook.
204
206
  *
205
- * The payload is JSON-stringified and POST'd to the ingress endpoint.
206
- * HostWebhook then handles delivery to the target URL with retries,
207
- * monitoring, transforms, and signing.
207
+ * @overload send(payload, options?) — uses default token from constructor
208
+ * @overload send(token, payload, options?) — specify token per call
208
209
  *
209
- * @param payload - The webhook payload (will be JSON.stringify'd)
210
- * @param options - Optional per-request configuration
211
- * @returns The event ID assigned by HostWebhook
212
210
  * @throws {SendError} If the API returns a non-2xx status
211
+ * @throws {Error} If no token is available
213
212
  */
214
- async send(payload, options) {
215
- const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
216
- let tz = "";
217
- try {
218
- tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
219
- } catch {
220
- }
221
- const headers = {
222
- "Content-Type": options?.contentType ?? "application/json",
223
- "User-Agent": `HostWebhook-SDK/${"1.3.0"}`,
224
- "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
225
- ...tz ? { "X-HostWebhook-Timezone": tz } : {},
226
- ...__privateGet(this, _defaultHeaders),
227
- ...options?.headers
228
- };
229
- if (__privateGet(this, _signature)?.type === "static") {
230
- const { secret, header } = __privateGet(this, _signature);
231
- if (header) {
232
- headers[header] = secret;
233
- } else {
234
- headers["Authorization"] = `Bearer ${secret}`;
213
+ async send(tokenOrPayload, payloadOrOptions, maybeOptions) {
214
+ let token;
215
+ let payload;
216
+ let options;
217
+ if (typeof tokenOrPayload === "string") {
218
+ token = tokenOrPayload;
219
+ payload = payloadOrOptions ?? {};
220
+ options = maybeOptions;
221
+ } else {
222
+ if (!__privateGet(this, _token)) {
223
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument: send(token, payload)");
235
224
  }
236
- } else if (__privateGet(this, _signingSecret)) {
237
- const timestamp = Math.floor(Date.now() / 1e3);
238
- const sig = createHmac2("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
239
- headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
225
+ token = __privateGet(this, _token);
226
+ payload = tokenOrPayload;
227
+ options = payloadOrOptions;
240
228
  }
241
- const url = `${__privateGet(this, _baseUrl)}/api/in/${__privateGet(this, _token)}`;
242
- const controller = new AbortController();
243
- const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
244
- let ingressResult;
245
- try {
246
- const res = await fetch(url, {
247
- method: "POST",
248
- headers,
249
- body,
250
- signal: controller.signal
251
- });
252
- const text = await res.text();
253
- if (!res.ok) {
254
- throw new SendError(
255
- `HostWebhook ingress error (${res.status}): ${text}`,
256
- res.status,
257
- text
258
- );
259
- }
260
- try {
261
- ingressResult = JSON.parse(text);
262
- } catch {
263
- ingressResult = { eventId: text };
264
- }
265
- } catch (err) {
266
- if (err instanceof SendError) throw err;
267
- if (err instanceof DOMException && err.name === "AbortError") {
268
- throw new SendError(
269
- `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
270
- 0,
271
- ""
272
- );
273
- }
274
- throw err;
275
- } finally {
276
- clearTimeout(timer);
277
- }
278
- if (ingressResult.syncStatus !== void 0) {
279
- if (options?.onResult) options.onResult(ingressResult);
280
- return ingressResult;
281
- }
282
- if (options?.waitForResult && ingressResult.streamToken) {
283
- return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
284
- }
285
- if (options?.onResult && ingressResult.streamToken) {
286
- __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
287
- });
288
- }
289
- return ingressResult;
229
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, payload, options);
290
230
  }
291
231
  /**
292
232
  * Convenience: send multiple payloads in parallel.
@@ -301,41 +241,60 @@ var HostWebhook = class {
301
241
  /**
302
242
  * Send a webhook with file attachments.
303
243
  *
304
- * Files are uploaded directly to storage via presigned URLs (zero memory on API),
305
- * then file references are injected into the payload before sending.
244
+ * Requires `apiKey` with `canvas:write` scope for file operations.
245
+ * Files are uploaded via presigned URLs, then references are injected into the payload.
306
246
  *
307
- * @param payload - The webhook payload
308
- * @param files - Map of field name → file (Buffer or Blob with metadata)
309
- * @param options - Optional per-request configuration
247
+ * @overload sendWithFiles(payload, files, options?) — uses default token
248
+ * @overload sendWithFiles(token, payload, files, options?) — specify token per call
310
249
  *
311
250
  * @example
312
251
  * ```ts
313
- * const hw = new HostWebhook({ token: 'your_token' });
252
+ * const hw = new HostWebhook({ apiKey: 'hwk_...', token: 'ingress_token' });
314
253
  * await hw.sendWithFiles(
315
- * { orderId: '123', type: 'invoice' },
254
+ * { orderId: '123' },
316
255
  * { invoice: { data: pdfBuffer, fileName: 'inv.pdf', mimeType: 'application/pdf' } },
317
256
  * );
318
257
  * ```
319
258
  */
320
- async sendWithFiles(payload, files, options) {
259
+ async sendWithFiles(tokenOrPayload, payloadOrFiles, filesOrOptions, maybeOptions) {
260
+ let token;
261
+ let payload;
262
+ let files;
263
+ let options;
264
+ if (typeof tokenOrPayload === "string") {
265
+ token = tokenOrPayload;
266
+ payload = payloadOrFiles;
267
+ files = filesOrOptions ?? {};
268
+ options = maybeOptions;
269
+ } else {
270
+ if (!__privateGet(this, _token)) {
271
+ throw new Error("HostWebhook: no default token set \u2014 pass token as first argument");
272
+ }
273
+ token = __privateGet(this, _token);
274
+ payload = tokenOrPayload;
275
+ files = payloadOrFiles ?? {};
276
+ options = filesOrOptions;
277
+ }
278
+ if (!__privateGet(this, _apiKey)) {
279
+ throw new Error("HostWebhook: apiKey is required for sendWithFiles (needs canvas:write scope)");
280
+ }
321
281
  const enrichedPayload = { ...payload };
322
282
  for (const [fieldName, file] of Object.entries(files)) {
323
283
  const uploadRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/upload-url`, {
324
284
  method: "POST",
325
285
  headers: {
326
286
  "Content-Type": "application/json",
327
- "Authorization": `Bearer ${__privateGet(this, _token)}`
287
+ "Authorization": `Bearer ${__privateGet(this, _apiKey)}`
328
288
  },
329
289
  body: JSON.stringify({ fileName: file.fileName, mimeType: file.mimeType })
330
290
  });
331
291
  if (!uploadRes.ok) throw new SendError(`Failed to get upload URL for ${fieldName}`, uploadRes.status, await uploadRes.text());
332
292
  const { fileId, uploadUrl, key } = await uploadRes.json();
333
- const uploadBody = file.data instanceof Blob ? file.data : new Blob([new Uint8Array(file.data)], { type: file.mimeType });
334
- const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: uploadBody });
293
+ const putRes = await fetch(uploadUrl, { method: "PUT", headers: { "Content-Type": file.mimeType }, body: file.data });
335
294
  if (!putRes.ok) throw new SendError(`Failed to upload ${fieldName} to storage`, putRes.status, await putRes.text());
336
295
  const confirmRes = await fetch(`${__privateGet(this, _baseUrl)}/api/files/${fileId}/confirm`, {
337
296
  method: "POST",
338
- headers: { "Authorization": `Bearer ${__privateGet(this, _token)}` }
297
+ headers: { "Authorization": `Bearer ${__privateGet(this, _apiKey)}` }
339
298
  });
340
299
  if (!confirmRes.ok) throw new SendError(`Failed to confirm upload for ${fieldName}`, confirmRes.status, await confirmRes.text());
341
300
  const confirmed = await confirmRes.json();
@@ -343,9 +302,10 @@ var HostWebhook = class {
343
302
  _file: { id: confirmed.id, key, originalName: confirmed.originalName, mimeType: confirmed.mimeType, size: confirmed.size }
344
303
  };
345
304
  }
346
- return this.send(enrichedPayload, options);
305
+ return __privateMethod(this, _HostWebhook_instances, doSend_fn).call(this, token, enrichedPayload, options);
347
306
  }
348
307
  };
308
+ _apiKey = new WeakMap();
349
309
  _token = new WeakMap();
350
310
  _baseUrl = new WeakMap();
351
311
  _signingSecret = new WeakMap();
@@ -353,6 +313,88 @@ _signature = new WeakMap();
353
313
  _defaultHeaders = new WeakMap();
354
314
  _timeoutMs = new WeakMap();
355
315
  _HostWebhook_instances = new WeakSet();
316
+ doSend_fn = async function(token, payload, options) {
317
+ const body = options?.rawBody !== void 0 ? typeof options.rawBody === "string" ? options.rawBody : options.rawBody.toString("utf8") : JSON.stringify(payload);
318
+ let tz = "";
319
+ try {
320
+ tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
321
+ } catch {
322
+ }
323
+ const headers = {
324
+ "Content-Type": options?.contentType ?? "application/json",
325
+ "User-Agent": `HostWebhook-SDK/${"1.4.0"}`,
326
+ "X-HostWebhook-Sent-At": (/* @__PURE__ */ new Date()).toISOString(),
327
+ ...tz ? { "X-HostWebhook-Timezone": tz } : {},
328
+ ...__privateGet(this, _defaultHeaders),
329
+ ...options?.headers
330
+ };
331
+ if (__privateGet(this, _apiKey)) {
332
+ headers["Authorization"] = `Bearer ${__privateGet(this, _apiKey)}`;
333
+ }
334
+ if (!__privateGet(this, _apiKey)) {
335
+ if (__privateGet(this, _signature)?.type === "static") {
336
+ const { secret, header } = __privateGet(this, _signature);
337
+ if (header) {
338
+ headers[header] = secret;
339
+ } else {
340
+ headers["Authorization"] = `Bearer ${secret}`;
341
+ }
342
+ } else if (__privateGet(this, _signingSecret)) {
343
+ const timestamp = Math.floor(Date.now() / 1e3);
344
+ const sig = createHmac2("sha256", __privateGet(this, _signingSecret)).update(`${timestamp}.${body}`).digest("hex");
345
+ headers["X-HostWebhook-Signature"] = `t=${timestamp},v1=${sig}`;
346
+ }
347
+ }
348
+ const url = `${__privateGet(this, _baseUrl)}/api/in/${token}`;
349
+ const controller = new AbortController();
350
+ const timer = setTimeout(() => controller.abort(), __privateGet(this, _timeoutMs));
351
+ let ingressResult;
352
+ try {
353
+ const res = await fetch(url, {
354
+ method: "POST",
355
+ headers,
356
+ body,
357
+ signal: controller.signal
358
+ });
359
+ const text = await res.text();
360
+ if (!res.ok) {
361
+ throw new SendError(
362
+ `HostWebhook ingress error (${res.status}): ${text}`,
363
+ res.status,
364
+ text
365
+ );
366
+ }
367
+ try {
368
+ ingressResult = JSON.parse(text);
369
+ } catch {
370
+ ingressResult = { eventId: text };
371
+ }
372
+ } catch (err) {
373
+ if (err instanceof SendError) throw err;
374
+ if (err instanceof DOMException && err.name === "AbortError") {
375
+ throw new SendError(
376
+ `HostWebhook request timed out after ${__privateGet(this, _timeoutMs)}ms`,
377
+ 0,
378
+ ""
379
+ );
380
+ }
381
+ throw err;
382
+ } finally {
383
+ clearTimeout(timer);
384
+ }
385
+ if (ingressResult.syncStatus !== void 0) {
386
+ if (options?.onResult) options.onResult(ingressResult);
387
+ return ingressResult;
388
+ }
389
+ if (options?.waitForResult && ingressResult.streamToken) {
390
+ return __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep);
391
+ }
392
+ if (options?.onResult && ingressResult.streamToken) {
393
+ __privateMethod(this, _HostWebhook_instances, waitForStream_fn).call(this, ingressResult, options?.onStep).then((result) => options.onResult(result)).catch(() => {
394
+ });
395
+ }
396
+ return ingressResult;
397
+ };
356
398
  waitForStream_fn = async function(ingressResult, onStep) {
357
399
  const { eventId, streamToken } = ingressResult;
358
400
  const streamUrl = `${__privateGet(this, _baseUrl)}/api/events/${eventId}/stream?token=${streamToken}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hostwebhook/node",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Official HostWebhook SDK — verify signatures, parse headers, and send webhooks",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",