pathao-merchant-sdk 2.1.0 → 2.2.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.
@@ -5,35 +5,15 @@ import { EventEmitter } from 'events';
5
5
  *
6
6
  * Handles incoming webhook events from Pathao.
7
7
  *
8
- * IMPORTANT: The official Pathao API documentation does not describe a webhook
9
- * specification. This implementation is based on observed webhook behaviour and
10
- * community research. Treat it as best-effort until Pathao publishes official
11
- * webhook docs.
12
- *
13
- * Signature mechanism: Pathao sends the raw shared secret in the
14
- * `X-PATHAO-Signature` header. There is no HMAC — the header value IS the
15
- * secret. A constant-time comparison is used to prevent timing attacks.
16
- *
17
- * @example — framework-agnostic
18
- * ```typescript
19
- * import {
20
- * PathaoWebhookHandler,
21
- * PathaoWebhookEvent,
22
- * } from 'pathao-merchant-sdk/webhooks';
23
- *
24
- * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);
25
- *
26
- * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {
27
- * console.log(payload.consignment_id, payload.collected_amount);
28
- * });
29
- *
30
- * // In your HTTP server — raw body as Buffer/string required:
31
- * const event = handler.process(rawBody, req.headers);
32
- * res.status(200).json({ received: true });
33
- * ```
8
+ * IMPORTANT INTEGRATION DETAILS:
9
+ * - Pathao does NOT sign incoming requests.
10
+ * - Instead, Pathao requires you to prove ownership by echoing your webhook secret
11
+ * in the \`X-Pathao-Merchant-Webhook-Integration-Secret\` header of EVERY response.
12
+ * - This SDK automatically handles the \`webhook_integration\` handshake event, which
13
+ * expects a 202 status code and the secret header.
34
14
  *
35
15
  * @example — Express
36
- * ```typescript
16
+ * \`\`\`typescript
37
17
  * import express from 'express';
38
18
  * import {
39
19
  * PathaoWebhookHandler,
@@ -45,13 +25,18 @@ import { EventEmitter } from 'events';
45
25
  *
46
26
  * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => { ... });
47
27
  *
48
- * // Mount express.raw() BEFORE the webhook middleware
28
+ * // Mount express.json() BEFORE the webhook middleware
49
29
  * app.post(
50
30
  * '/webhooks/pathao',
51
- * express.raw({ type: 'application/json' }),
31
+ * express.json(),
52
32
  * handler.expressMiddleware(),
33
+ * (req, res) => {
34
+ * // The middleware already sets the required secret header.
35
+ * // You just need to return a 200 OK for standard events.
36
+ * res.status(200).send('OK');
37
+ * }
53
38
  * );
54
- * ```
39
+ * \`\`\`
55
40
  */
56
41
 
57
42
  declare class PathaoWebhookError extends Error {
@@ -59,9 +44,10 @@ declare class PathaoWebhookError extends Error {
59
44
  }
60
45
  /**
61
46
  * All webhook event types emitted by Pathao.
62
- * Values are the exact strings that appear in the `event` field of each payload.
47
+ * Values are the exact strings that appear in the \`event\` field of each payload.
63
48
  */
64
49
  declare enum PathaoWebhookEvent {
50
+ WEBHOOK_INTEGRATION = "webhook_integration",
65
51
  ORDER_CREATED = "order.created",
66
52
  ORDER_UPDATED = "order.updated",
67
53
  ORDER_PICKUP_REQUESTED = "order.pickup-requested",
@@ -84,13 +70,15 @@ declare enum PathaoWebhookEvent {
84
70
  STORE_CREATED = "store.created",
85
71
  STORE_UPDATED = "store.updated"
86
72
  }
87
- /** Fields present on every webhook payload */
73
+ interface WebhookIntegrationPayload {
74
+ event: PathaoWebhookEvent.WEBHOOK_INTEGRATION;
75
+ }
76
+ /** Fields present on every normal webhook payload */
88
77
  interface BaseWebhookPayload {
89
- /** Dot-notation event type — matches a `PathaoWebhookEvent` value */
90
78
  event: string;
91
- /** ISO timestamp of when the state change occurred */
79
+ /** Format: MySQL datetime YYYY-MM-DD HH:MM:SS (no timezone indicator) */
92
80
  updated_at: string;
93
- /** ISO timestamp of when this webhook was dispatched */
81
+ /** Format: ISO 8601 timestamp */
94
82
  timestamp: string;
95
83
  }
96
84
  /** Fields shared by all order-related events */
@@ -185,9 +173,10 @@ interface StoreUpdatedPayload extends StoreWebhookPayload {
185
173
  event: PathaoWebhookEvent.STORE_UPDATED;
186
174
  }
187
175
  /** Union of all possible webhook payloads */
188
- type PathaoWebhookPayload = OrderCreatedPayload | OrderUpdatedPayload | OrderPickupRequestedPayload | OrderAssignedForPickupPayload | OrderPickedPayload | OrderPickupFailedPayload | OrderPickupCancelledPayload | OrderAtSortingHubPayload | OrderInTransitPayload | OrderAtLastMileHubPayload | OrderAssignedForDeliveryPayload | OrderDeliveredPayload | OrderPartialDeliveryPayload | OrderReturnedPayload | OrderDeliveryFailedPayload | OrderOnHoldPayload | OrderPaidPayload | OrderPaidReturnPayload | OrderExchangedPayload | StoreCreatedPayload | StoreUpdatedPayload;
189
- /** Maps each `PathaoWebhookEvent` to its specific payload type */
176
+ type PathaoWebhookPayload = WebhookIntegrationPayload | OrderCreatedPayload | OrderUpdatedPayload | OrderPickupRequestedPayload | OrderAssignedForPickupPayload | OrderPickedPayload | OrderPickupFailedPayload | OrderPickupCancelledPayload | OrderAtSortingHubPayload | OrderInTransitPayload | OrderAtLastMileHubPayload | OrderAssignedForDeliveryPayload | OrderDeliveredPayload | OrderPartialDeliveryPayload | OrderReturnedPayload | OrderDeliveryFailedPayload | OrderOnHoldPayload | OrderPaidPayload | OrderPaidReturnPayload | OrderExchangedPayload | StoreCreatedPayload | StoreUpdatedPayload;
177
+ /** Maps each \`PathaoWebhookEvent\` to its specific payload type */
190
178
  interface WebhookEventPayloadMap {
179
+ [PathaoWebhookEvent.WEBHOOK_INTEGRATION]: WebhookIntegrationPayload;
191
180
  [PathaoWebhookEvent.ORDER_CREATED]: OrderCreatedPayload;
192
181
  [PathaoWebhookEvent.ORDER_UPDATED]: OrderUpdatedPayload;
193
182
  [PathaoWebhookEvent.ORDER_PICKUP_REQUESTED]: OrderPickupRequestedPayload;
@@ -210,108 +199,114 @@ interface WebhookEventPayloadMap {
210
199
  [PathaoWebhookEvent.STORE_CREATED]: StoreCreatedPayload;
211
200
  [PathaoWebhookEvent.STORE_UPDATED]: StoreUpdatedPayload;
212
201
  }
213
- /** Signature header name sent by Pathao on every webhook request */
214
- declare const PATHAO_SIGNATURE_HEADER = "x-pathao-signature";
215
- /**
216
- * Verify the `X-PATHAO-Signature` header against the configured webhook secret.
217
- *
218
- * Uses a constant-time comparison to prevent timing attacks. The header value
219
- * is the raw shared secret — Pathao does not hash the signature.
220
- *
221
- * @returns `true` if valid, `false` if missing or mismatched.
222
- */
223
- declare function verifySignature(signature: string | undefined, webhookSecret: string): boolean;
202
+ /** Required response header used to authorize your endpoint with Pathao */
203
+ declare const PATHAO_SECRET_HEADER = "x-pathao-merchant-webhook-integration-secret";
224
204
  /**
225
- * Verify the webhook signature and parse the raw body in one step.
205
+ * Parses the raw body into a webhook payload.
206
+ * Pathao does not sign inbound requests, so this just ensures it is valid JSON with an event field.
226
207
  *
227
- * Throws `PathaoWebhookError` on signature failure or malformed JSON.
208
+ * Throws \`PathaoWebhookError\` on malformed JSON or missing event.
228
209
  *
229
- * @param rawBody Raw request body — do NOT pre-parse with `JSON.parse`
230
- * @param headers Request headers object
231
- * @param webhookSecret Your Pathao webhook integration secret
210
+ * @param rawBody Raw request body or parsed object
232
211
  */
233
- declare function constructEvent(rawBody: Buffer | string, headers: Record<string, string | string[] | undefined>, webhookSecret: string): PathaoWebhookPayload;
234
- type GenericHeaders = Record<string, string | string[] | undefined>;
212
+ declare function constructEvent(rawBody: Buffer | string | object): PathaoWebhookPayload;
213
+ type WebhookResponseInstructions = {
214
+ statusCode: number;
215
+ headers: Record<string, string>;
216
+ payload: PathaoWebhookPayload | null;
217
+ error: PathaoWebhookError | null;
218
+ };
235
219
  /**
236
- * Stateful webhook handler that verifies incoming requests, parses payloads,
220
+ * Stateful webhook handler that parses payloads, provides response instructions,
237
221
  * and dispatches events to typed listeners.
238
222
  *
239
223
  * @example
240
- * ```typescript
224
+ * \`\`\`typescript
241
225
  * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);
242
226
  *
243
227
  * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {
244
228
  * // payload is fully typed as OrderDeliveredPayload
245
229
  * console.log(payload.consignment_id, payload.collected_amount);
246
230
  * });
247
- * ```
231
+ * \`\`\`
248
232
  */
249
233
  declare class PathaoWebhookHandler extends EventEmitter {
250
234
  private readonly webhookSecret;
251
235
  constructor(webhookSecret: string);
252
236
  /** Listen for a specific Pathao event with a fully-typed payload callback. */
253
237
  on<E extends PathaoWebhookEvent>(event: E, listener: (payload: WebhookEventPayloadMap[E]) => void): this;
254
- /** Fires for every successfully verified event regardless of type. */
238
+ /** Fires for every successfully parsed event regardless of type. */
255
239
  on(event: 'webhook', listener: (payload: PathaoWebhookPayload) => void): this;
256
- /** Fires when verification or parsing fails. */
240
+ /** Fires when parsing fails. */
257
241
  on(event: 'error', listener: (error: PathaoWebhookError) => void): this;
242
+ /** Listen once for a specific Pathao event. */
243
+ once<E extends PathaoWebhookEvent>(event: E, listener: (payload: WebhookEventPayloadMap[E]) => void): this;
244
+ once(event: 'webhook', listener: (payload: PathaoWebhookPayload) => void): this;
245
+ once(event: 'error', listener: (error: PathaoWebhookError) => void): this;
258
246
  /**
259
- * Verify the signature, parse the body, and dispatch the event.
247
+ * Parse the body and dispatch the event.
260
248
  *
261
- * Throws `PathaoWebhookError` on failure and also emits `'error'` so
249
+ * Throws \`PathaoWebhookError\` on failure and also emits \`'error'\` so
262
250
  * listeners can respond without a try/catch.
263
251
  *
264
- * @param rawBody Raw request body — must NOT be pre-parsed
265
- * @param headers HTTP request headers
252
+ * @param rawBody Raw request body or matched json object
266
253
  */
267
- process(rawBody: Buffer | string, headers: GenericHeaders): PathaoWebhookPayload;
254
+ process(rawBody: Buffer | string | object): PathaoWebhookPayload;
268
255
  /**
269
256
  * Returns an Express-compatible middleware function.
270
257
  *
271
- * **Requires** `express.raw({ type: 'application/json' })` mounted on the
272
- * same route before this middleware so the raw body Buffer is preserved.
258
+ * Automatically sets the required \`X-Pathao-Merchant-Webhook-Integration-Secret\` header.
259
+ * Automatically responds with 202 for the \`webhook_integration\` handshake.
260
+ * For standard events, attaches the payload to \`req.pathaoWebhook\` and calls \`next()\`.
261
+ * On error, calls \`next(err)\`.
273
262
  *
274
- * On success the parsed payload is attached to `req.pathaoWebhook`.
275
- * On failure a `400` response is returned.
276
- *
277
- * ```typescript
263
+ * \`\`\`typescript
278
264
  * app.post(
279
265
  * '/webhooks/pathao',
280
- * express.raw({ type: 'application/json' }),
266
+ * express.json(),
281
267
  * handler.expressMiddleware(),
268
+ * (req, res) => res.sendStatus(200) // You must send 200 for other events
282
269
  * );
283
- * ```
270
+ * \`\`\`
284
271
  */
285
272
  expressMiddleware(): (req: {
286
- body: Buffer | string;
287
- headers: GenericHeaders;
273
+ body: Buffer | string | object;
288
274
  pathaoWebhook?: PathaoWebhookPayload;
289
275
  }, res: {
276
+ setHeader: (name: string, value: string) => void;
290
277
  status: (code: number) => {
291
- json: (body: unknown) => void;
278
+ send: () => void;
292
279
  };
293
280
  }, next: (err?: unknown) => void) => void;
294
281
  /**
295
- * Returns a generic async handler for any framework (Fastify, Hono, plain
296
- * `http.createServer`, etc.).
282
+ * Returns response instructions for any framework (Fastify, Hono, etc.).
297
283
  *
298
- * Never rejects — always resolves with either `{ payload, error: null }` or
299
- * `{ payload: null, error: PathaoWebhookError }`.
284
+ * Never throws — always resolves with a \`WebhookResponseInstructions\` object
285
+ * that tells you which status code and headers to return, along with the payload/error.
300
286
  *
301
- * ```typescript
287
+ * \`\`\`typescript
302
288
  * const handle = handler.middleware();
303
- * const { payload, error } = await handle(rawBody, request.headers);
304
- * if (error) { reply.status(400).send({ error: error.message }); return; }
305
- * reply.send({ received: true });
306
- * ```
289
+ * const instructions = await handle(request.body);
290
+ *
291
+ * // Apply the required headers (the secret header)
292
+ * for (const [key, value] of Object.entries(instructions.headers)) {
293
+ * reply.header(key, value);
294
+ * }
295
+ *
296
+ * if (instructions.error) {
297
+ * return reply.status(instructions.statusCode).send({ error: instructions.error.message });
298
+ * }
299
+ *
300
+ * // If it was the handshake, we should just return 202 as instructed
301
+ * if (instructions.payload?.event === 'webhook_integration') {
302
+ * return reply.status(instructions.statusCode).send();
303
+ * }
304
+ *
305
+ * // Process your real webhook
306
+ * return reply.status(instructions.statusCode).send({ received: true });
307
+ * \`\`\`
307
308
  */
308
- middleware(): (rawBody: Buffer | string, headers: GenericHeaders) => Promise<{
309
- payload: PathaoWebhookPayload;
310
- error: null;
311
- } | {
312
- payload: null;
313
- error: PathaoWebhookError;
314
- }>;
309
+ middleware(): (rawBody: Buffer | string | object) => Promise<WebhookResponseInstructions>;
315
310
  }
316
311
 
317
- export { type BaseWebhookPayload, type OrderAssignedForDeliveryPayload, type OrderAssignedForPickupPayload, type OrderAtLastMileHubPayload, type OrderAtSortingHubPayload, type OrderCreatedPayload, type OrderDeliveredPayload, type OrderDeliveryFailedPayload, type OrderExchangedPayload, type OrderInTransitPayload, type OrderOnHoldPayload, type OrderPaidPayload, type OrderPaidReturnPayload, type OrderPartialDeliveryPayload, type OrderPickedPayload, type OrderPickupCancelledPayload, type OrderPickupFailedPayload, type OrderPickupRequestedPayload, type OrderReturnedPayload, type OrderUpdatedPayload, type OrderWebhookPayload, PATHAO_SIGNATURE_HEADER, PathaoWebhookError, PathaoWebhookEvent, PathaoWebhookHandler, type PathaoWebhookPayload, type StoreCreatedPayload, type StoreUpdatedPayload, type StoreWebhookPayload, type WebhookEventPayloadMap, constructEvent, verifySignature };
312
+ export { type BaseWebhookPayload, type OrderAssignedForDeliveryPayload, type OrderAssignedForPickupPayload, type OrderAtLastMileHubPayload, type OrderAtSortingHubPayload, type OrderCreatedPayload, type OrderDeliveredPayload, type OrderDeliveryFailedPayload, type OrderExchangedPayload, type OrderInTransitPayload, type OrderOnHoldPayload, type OrderPaidPayload, type OrderPaidReturnPayload, type OrderPartialDeliveryPayload, type OrderPickedPayload, type OrderPickupCancelledPayload, type OrderPickupFailedPayload, type OrderPickupRequestedPayload, type OrderReturnedPayload, type OrderUpdatedPayload, type OrderWebhookPayload, PATHAO_SECRET_HEADER, PathaoWebhookError, PathaoWebhookEvent, PathaoWebhookHandler, type PathaoWebhookPayload, type StoreCreatedPayload, type StoreUpdatedPayload, type StoreWebhookPayload, type WebhookEventPayloadMap, type WebhookIntegrationPayload, type WebhookResponseInstructions, constructEvent };
package/dist/webhooks.js CHANGED
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- var crypto = require('crypto');
4
3
  var events = require('events');
5
4
 
6
5
  // src/webhooks.ts
@@ -11,6 +10,7 @@ var PathaoWebhookError = class extends Error {
11
10
  }
12
11
  };
13
12
  var PathaoWebhookEvent = /* @__PURE__ */ ((PathaoWebhookEvent2) => {
13
+ PathaoWebhookEvent2["WEBHOOK_INTEGRATION"] = "webhook_integration";
14
14
  PathaoWebhookEvent2["ORDER_CREATED"] = "order.created";
15
15
  PathaoWebhookEvent2["ORDER_UPDATED"] = "order.updated";
16
16
  PathaoWebhookEvent2["ORDER_PICKUP_REQUESTED"] = "order.pickup-requested";
@@ -34,36 +34,18 @@ var PathaoWebhookEvent = /* @__PURE__ */ ((PathaoWebhookEvent2) => {
34
34
  PathaoWebhookEvent2["STORE_UPDATED"] = "store.updated";
35
35
  return PathaoWebhookEvent2;
36
36
  })(PathaoWebhookEvent || {});
37
- var PATHAO_SIGNATURE_HEADER = "x-pathao-signature";
38
- function verifySignature(signature, webhookSecret) {
39
- if (!signature || !webhookSecret) return false;
40
- try {
41
- const sigBuf = Buffer.from(signature, "utf8");
42
- const secretBuf = Buffer.from(webhookSecret, "utf8");
43
- if (sigBuf.length !== secretBuf.length) return false;
44
- return crypto.timingSafeEqual(sigBuf, secretBuf);
45
- } catch {
46
- return false;
47
- }
48
- }
49
- function constructEvent(rawBody, headers, webhookSecret) {
50
- const signature = extractHeader(headers, PATHAO_SIGNATURE_HEADER);
51
- if (!signature) {
52
- throw new PathaoWebhookError(
53
- `Missing ${PATHAO_SIGNATURE_HEADER} header. Ensure Pathao is sending the signature.`
54
- );
55
- }
56
- if (!verifySignature(signature, webhookSecret)) {
57
- throw new PathaoWebhookError(
58
- "Invalid webhook signature. Check that your webhookSecret matches the Pathao integration secret."
59
- );
60
- }
61
- const bodyStr = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
37
+ var PATHAO_SECRET_HEADER = "x-pathao-merchant-webhook-integration-secret";
38
+ function constructEvent(rawBody) {
62
39
  let parsed;
63
- try {
64
- parsed = JSON.parse(bodyStr);
65
- } catch {
66
- throw new PathaoWebhookError("Webhook payload is not valid JSON.");
40
+ if (typeof rawBody === "object" && !Buffer.isBuffer(rawBody)) {
41
+ parsed = rawBody;
42
+ } else {
43
+ const bodyStr = Buffer.isBuffer(rawBody) ? rawBody.toString("utf8") : rawBody;
44
+ try {
45
+ parsed = JSON.parse(bodyStr);
46
+ } catch {
47
+ throw new PathaoWebhookError("Webhook payload is not valid JSON.");
48
+ }
67
49
  }
68
50
  if (!parsed || typeof parsed !== "object" || !("event" in parsed)) {
69
51
  throw new PathaoWebhookError(
@@ -86,23 +68,28 @@ var PathaoWebhookHandler = class extends events.EventEmitter {
86
68
  on(event, listener) {
87
69
  return super.on(event, listener);
88
70
  }
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ once(event, listener) {
73
+ return super.once(event, listener);
74
+ }
89
75
  // ─────────────────────────────────────────────────────────────────────────
90
76
  /**
91
- * Verify the signature, parse the body, and dispatch the event.
77
+ * Parse the body and dispatch the event.
92
78
  *
93
- * Throws `PathaoWebhookError` on failure and also emits `'error'` so
79
+ * Throws \`PathaoWebhookError\` on failure and also emits \`'error'\` so
94
80
  * listeners can respond without a try/catch.
95
81
  *
96
- * @param rawBody Raw request body — must NOT be pre-parsed
97
- * @param headers HTTP request headers
82
+ * @param rawBody Raw request body or matched json object
98
83
  */
99
- process(rawBody, headers) {
84
+ process(rawBody) {
100
85
  let payload;
101
86
  try {
102
- payload = constructEvent(rawBody, headers, this.webhookSecret);
87
+ payload = constructEvent(rawBody);
103
88
  } catch (err) {
104
89
  const webhookErr = err instanceof PathaoWebhookError ? err : new PathaoWebhookError(err instanceof Error ? err.message : "Unknown error");
105
- this.emit("error", webhookErr);
90
+ if (this.listenerCount("error") > 0) {
91
+ this.emit("error", webhookErr);
92
+ }
106
93
  throw webhookErr;
107
94
  }
108
95
  this.emit(payload.event, payload);
@@ -112,74 +99,95 @@ var PathaoWebhookHandler = class extends events.EventEmitter {
112
99
  /**
113
100
  * Returns an Express-compatible middleware function.
114
101
  *
115
- * **Requires** `express.raw({ type: 'application/json' })` mounted on the
116
- * same route before this middleware so the raw body Buffer is preserved.
117
- *
118
- * On success the parsed payload is attached to `req.pathaoWebhook`.
119
- * On failure a `400` response is returned.
102
+ * Automatically sets the required \`X-Pathao-Merchant-Webhook-Integration-Secret\` header.
103
+ * Automatically responds with 202 for the \`webhook_integration\` handshake.
104
+ * For standard events, attaches the payload to \`req.pathaoWebhook\` and calls \`next()\`.
105
+ * On error, calls \`next(err)\`.
120
106
  *
121
- * ```typescript
107
+ * \`\`\`typescript
122
108
  * app.post(
123
109
  * '/webhooks/pathao',
124
- * express.raw({ type: 'application/json' }),
110
+ * express.json(),
125
111
  * handler.expressMiddleware(),
112
+ * (req, res) => res.sendStatus(200) // You must send 200 for other events
126
113
  * );
127
- * ```
114
+ * \`\`\`
128
115
  */
129
116
  expressMiddleware() {
130
117
  return (req, res, next) => {
131
118
  try {
132
- req.pathaoWebhook = this.process(req.body, req.headers);
119
+ res.setHeader(PATHAO_SECRET_HEADER, this.webhookSecret);
120
+ const payload = this.process(req.body);
121
+ req.pathaoWebhook = payload;
122
+ if (payload.event === "webhook_integration" /* WEBHOOK_INTEGRATION */) {
123
+ res.status(202).send();
124
+ return;
125
+ }
133
126
  next();
134
127
  } catch (err) {
135
- res.status(400).json({
136
- error: err instanceof Error ? err.message : "Webhook processing failed"
137
- });
128
+ next(err);
138
129
  }
139
130
  };
140
131
  }
141
132
  /**
142
- * Returns a generic async handler for any framework (Fastify, Hono, plain
143
- * `http.createServer`, etc.).
133
+ * Returns response instructions for any framework (Fastify, Hono, etc.).
144
134
  *
145
- * Never rejects — always resolves with either `{ payload, error: null }` or
146
- * `{ payload: null, error: PathaoWebhookError }`.
135
+ * Never throws — always resolves with a \`WebhookResponseInstructions\` object
136
+ * that tells you which status code and headers to return, along with the payload/error.
147
137
  *
148
- * ```typescript
138
+ * \`\`\`typescript
149
139
  * const handle = handler.middleware();
150
- * const { payload, error } = await handle(rawBody, request.headers);
151
- * if (error) { reply.status(400).send({ error: error.message }); return; }
152
- * reply.send({ received: true });
153
- * ```
140
+ * const instructions = await handle(request.body);
141
+ *
142
+ * // Apply the required headers (the secret header)
143
+ * for (const [key, value] of Object.entries(instructions.headers)) {
144
+ * reply.header(key, value);
145
+ * }
146
+ *
147
+ * if (instructions.error) {
148
+ * return reply.status(instructions.statusCode).send({ error: instructions.error.message });
149
+ * }
150
+ *
151
+ * // If it was the handshake, we should just return 202 as instructed
152
+ * if (instructions.payload?.event === 'webhook_integration') {
153
+ * return reply.status(instructions.statusCode).send();
154
+ * }
155
+ *
156
+ * // Process your real webhook
157
+ * return reply.status(instructions.statusCode).send({ received: true });
158
+ * \`\`\`
154
159
  */
155
160
  middleware() {
156
- return async (rawBody, headers) => {
161
+ return async (rawBody) => {
162
+ const headers = { [PATHAO_SECRET_HEADER]: this.webhookSecret };
157
163
  try {
158
- const payload = this.process(rawBody, headers);
159
- return { payload, error: null };
164
+ const payload = this.process(rawBody);
165
+ const isHandshake = payload.event === "webhook_integration" /* WEBHOOK_INTEGRATION */;
166
+ return {
167
+ statusCode: isHandshake ? 202 : 200,
168
+ headers,
169
+ payload,
170
+ error: null
171
+ };
160
172
  } catch (err) {
161
173
  const webhookErr = err instanceof PathaoWebhookError ? err : new PathaoWebhookError(
162
174
  err instanceof Error ? err.message : "Unknown error"
163
175
  );
164
- return { payload: null, error: webhookErr };
176
+ return {
177
+ statusCode: 400,
178
+ headers,
179
+ payload: null,
180
+ error: webhookErr
181
+ };
165
182
  }
166
183
  };
167
184
  }
168
185
  };
169
- function extractHeader(headers, name) {
170
- const lower = name.toLowerCase();
171
- const key = Object.keys(headers).find((k) => k.toLowerCase() === lower);
172
- if (!key) return void 0;
173
- const value = headers[key];
174
- if (Array.isArray(value)) return value[0];
175
- return value;
176
- }
177
186
 
178
- exports.PATHAO_SIGNATURE_HEADER = PATHAO_SIGNATURE_HEADER;
187
+ exports.PATHAO_SECRET_HEADER = PATHAO_SECRET_HEADER;
179
188
  exports.PathaoWebhookError = PathaoWebhookError;
180
189
  exports.PathaoWebhookEvent = PathaoWebhookEvent;
181
190
  exports.PathaoWebhookHandler = PathaoWebhookHandler;
182
191
  exports.constructEvent = constructEvent;
183
- exports.verifySignature = verifySignature;
184
192
  //# sourceMappingURL=webhooks.js.map
185
193
  //# sourceMappingURL=webhooks.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/webhooks.ts"],"names":["PathaoWebhookEvent","timingSafeEqual","EventEmitter"],"mappings":"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","file":"webhooks.js","sourcesContent":["/**\n * Pathao Webhook Support\n *\n * Handles incoming webhook events from Pathao.\n *\n * IMPORTANT: The official Pathao API documentation does not describe a webhook\n * specification. This implementation is based on observed webhook behaviour and\n * community research. Treat it as best-effort until Pathao publishes official\n * webhook docs.\n *\n * Signature mechanism: Pathao sends the raw shared secret in the\n * `X-PATHAO-Signature` header. There is no HMAC — the header value IS the\n * secret. A constant-time comparison is used to prevent timing attacks.\n *\n * @example — framework-agnostic\n * ```typescript\n * import {\n * PathaoWebhookHandler,\n * PathaoWebhookEvent,\n * } from 'pathao-merchant-sdk/webhooks';\n *\n * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);\n *\n * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {\n * console.log(payload.consignment_id, payload.collected_amount);\n * });\n *\n * // In your HTTP server — raw body as Buffer/string required:\n * const event = handler.process(rawBody, req.headers);\n * res.status(200).json({ received: true });\n * ```\n *\n * @example — Express\n * ```typescript\n * import express from 'express';\n * import {\n * PathaoWebhookHandler,\n * PathaoWebhookEvent,\n * } from 'pathao-merchant-sdk/webhooks';\n *\n * const app = express();\n * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);\n *\n * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => { ... });\n *\n * // Mount express.raw() BEFORE the webhook middleware\n * app.post(\n * '/webhooks/pathao',\n * express.raw({ type: 'application/json' }),\n * handler.expressMiddleware(),\n * );\n * ```\n */\n\nimport { timingSafeEqual } from 'crypto';\nimport { EventEmitter } from 'events';\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class PathaoWebhookError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PathaoWebhookError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Event types\n// ---------------------------------------------------------------------------\n\n/**\n * All webhook event types emitted by Pathao.\n * Values are the exact strings that appear in the `event` field of each payload.\n */\nexport enum PathaoWebhookEvent {\n ORDER_CREATED = 'order.created',\n ORDER_UPDATED = 'order.updated',\n ORDER_PICKUP_REQUESTED = 'order.pickup-requested',\n ORDER_ASSIGNED_FOR_PICKUP = 'order.assigned-for-pickup',\n ORDER_PICKED = 'order.picked',\n ORDER_PICKUP_FAILED = 'order.pickup-failed',\n ORDER_PICKUP_CANCELLED = 'order.pickup-cancelled',\n ORDER_AT_THE_SORTING_HUB = 'order.at-the-sorting-hub',\n ORDER_IN_TRANSIT = 'order.in-transit',\n ORDER_RECEIVED_AT_LAST_MILE_HUB = 'order.received-at-last-mile-hub',\n ORDER_ASSIGNED_FOR_DELIVERY = 'order.assigned-for-delivery',\n ORDER_DELIVERED = 'order.delivered',\n ORDER_PARTIAL_DELIVERY = 'order.partial-delivery',\n ORDER_RETURNED = 'order.returned',\n ORDER_DELIVERY_FAILED = 'order.delivery-failed',\n ORDER_ON_HOLD = 'order.on-hold',\n ORDER_PAID = 'order.paid',\n ORDER_PAID_RETURN = 'order.paid-return',\n ORDER_EXCHANGED = 'order.exchanged',\n STORE_CREATED = 'store.created',\n STORE_UPDATED = 'store.updated',\n}\n\n// ---------------------------------------------------------------------------\n// Payload types\n// ---------------------------------------------------------------------------\n\n/** Fields present on every webhook payload */\nexport interface BaseWebhookPayload {\n /** Dot-notation event type — matches a `PathaoWebhookEvent` value */\n event: string;\n /** ISO timestamp of when the state change occurred */\n updated_at: string;\n /** ISO timestamp of when this webhook was dispatched */\n timestamp: string;\n}\n\n/** Fields shared by all order-related events */\nexport interface OrderWebhookPayload extends BaseWebhookPayload {\n consignment_id: string;\n merchant_order_id?: string;\n store_id: number;\n delivery_fee?: number;\n}\n\n/** Fields shared by all store-related events */\nexport interface StoreWebhookPayload extends BaseWebhookPayload {\n store_id: number;\n store_name: string;\n store_address: string;\n is_active: 0 | 1;\n}\n\n// Per-event payload types ────────────────────────────────────────────────────\n\nexport interface OrderCreatedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_CREATED;\n delivery_fee: number;\n}\n\nexport interface OrderUpdatedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_UPDATED;\n delivery_fee: number;\n}\n\nexport interface OrderPickupRequestedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_REQUESTED;\n delivery_fee: number;\n}\n\nexport interface OrderAssignedForPickupPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ASSIGNED_FOR_PICKUP;\n}\n\nexport interface OrderPickedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKED;\n}\n\nexport interface OrderPickupFailedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_FAILED;\n}\n\nexport interface OrderPickupCancelledPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_CANCELLED;\n}\n\nexport interface OrderAtSortingHubPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_AT_THE_SORTING_HUB;\n}\n\nexport interface OrderInTransitPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_IN_TRANSIT;\n}\n\nexport interface OrderAtLastMileHubPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_RECEIVED_AT_LAST_MILE_HUB;\n}\n\nexport interface OrderAssignedForDeliveryPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ASSIGNED_FOR_DELIVERY;\n}\n\nexport interface OrderDeliveredPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_DELIVERED;\n collected_amount: number;\n}\n\nexport interface OrderPartialDeliveryPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PARTIAL_DELIVERY;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface OrderReturnedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_RETURNED;\n reason?: string;\n}\n\nexport interface OrderDeliveryFailedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_DELIVERY_FAILED;\n reason?: string;\n}\n\nexport interface OrderOnHoldPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ON_HOLD;\n reason?: string;\n}\n\nexport interface OrderPaidPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PAID;\n invoice_id: string;\n}\n\nexport interface OrderPaidReturnPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PAID_RETURN;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface OrderExchangedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_EXCHANGED;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface StoreCreatedPayload extends StoreWebhookPayload {\n event: PathaoWebhookEvent.STORE_CREATED;\n}\n\nexport interface StoreUpdatedPayload extends StoreWebhookPayload {\n event: PathaoWebhookEvent.STORE_UPDATED;\n}\n\n/** Union of all possible webhook payloads */\nexport type PathaoWebhookPayload =\n | OrderCreatedPayload\n | OrderUpdatedPayload\n | OrderPickupRequestedPayload\n | OrderAssignedForPickupPayload\n | OrderPickedPayload\n | OrderPickupFailedPayload\n | OrderPickupCancelledPayload\n | OrderAtSortingHubPayload\n | OrderInTransitPayload\n | OrderAtLastMileHubPayload\n | OrderAssignedForDeliveryPayload\n | OrderDeliveredPayload\n | OrderPartialDeliveryPayload\n | OrderReturnedPayload\n | OrderDeliveryFailedPayload\n | OrderOnHoldPayload\n | OrderPaidPayload\n | OrderPaidReturnPayload\n | OrderExchangedPayload\n | StoreCreatedPayload\n | StoreUpdatedPayload;\n\n/** Maps each `PathaoWebhookEvent` to its specific payload type */\nexport interface WebhookEventPayloadMap {\n [PathaoWebhookEvent.ORDER_CREATED]: OrderCreatedPayload;\n [PathaoWebhookEvent.ORDER_UPDATED]: OrderUpdatedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_REQUESTED]: OrderPickupRequestedPayload;\n [PathaoWebhookEvent.ORDER_ASSIGNED_FOR_PICKUP]: OrderAssignedForPickupPayload;\n [PathaoWebhookEvent.ORDER_PICKED]: OrderPickedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_FAILED]: OrderPickupFailedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_CANCELLED]: OrderPickupCancelledPayload;\n [PathaoWebhookEvent.ORDER_AT_THE_SORTING_HUB]: OrderAtSortingHubPayload;\n [PathaoWebhookEvent.ORDER_IN_TRANSIT]: OrderInTransitPayload;\n [PathaoWebhookEvent.ORDER_RECEIVED_AT_LAST_MILE_HUB]: OrderAtLastMileHubPayload;\n [PathaoWebhookEvent.ORDER_ASSIGNED_FOR_DELIVERY]: OrderAssignedForDeliveryPayload;\n [PathaoWebhookEvent.ORDER_DELIVERED]: OrderDeliveredPayload;\n [PathaoWebhookEvent.ORDER_PARTIAL_DELIVERY]: OrderPartialDeliveryPayload;\n [PathaoWebhookEvent.ORDER_RETURNED]: OrderReturnedPayload;\n [PathaoWebhookEvent.ORDER_DELIVERY_FAILED]: OrderDeliveryFailedPayload;\n [PathaoWebhookEvent.ORDER_ON_HOLD]: OrderOnHoldPayload;\n [PathaoWebhookEvent.ORDER_PAID]: OrderPaidPayload;\n [PathaoWebhookEvent.ORDER_PAID_RETURN]: OrderPaidReturnPayload;\n [PathaoWebhookEvent.ORDER_EXCHANGED]: OrderExchangedPayload;\n [PathaoWebhookEvent.STORE_CREATED]: StoreCreatedPayload;\n [PathaoWebhookEvent.STORE_UPDATED]: StoreUpdatedPayload;\n}\n\n// ---------------------------------------------------------------------------\n// Core functions\n// ---------------------------------------------------------------------------\n\n/** Signature header name sent by Pathao on every webhook request */\nexport const PATHAO_SIGNATURE_HEADER = 'x-pathao-signature';\n\n/**\n * Verify the `X-PATHAO-Signature` header against the configured webhook secret.\n *\n * Uses a constant-time comparison to prevent timing attacks. The header value\n * is the raw shared secret — Pathao does not hash the signature.\n *\n * @returns `true` if valid, `false` if missing or mismatched.\n */\nexport function verifySignature(\n signature: string | undefined,\n webhookSecret: string,\n): boolean {\n if (!signature || !webhookSecret) return false;\n\n try {\n const sigBuf = Buffer.from(signature, 'utf8');\n const secretBuf = Buffer.from(webhookSecret, 'utf8');\n if (sigBuf.length !== secretBuf.length) return false;\n return timingSafeEqual(sigBuf, secretBuf);\n } catch {\n return false;\n }\n}\n\n/**\n * Verify the webhook signature and parse the raw body in one step.\n *\n * Throws `PathaoWebhookError` on signature failure or malformed JSON.\n *\n * @param rawBody Raw request body — do NOT pre-parse with `JSON.parse`\n * @param headers Request headers object\n * @param webhookSecret Your Pathao webhook integration secret\n */\nexport function constructEvent(\n rawBody: Buffer | string,\n headers: Record<string, string | string[] | undefined>,\n webhookSecret: string,\n): PathaoWebhookPayload {\n const signature = extractHeader(headers, PATHAO_SIGNATURE_HEADER);\n\n if (!signature) {\n throw new PathaoWebhookError(\n `Missing ${PATHAO_SIGNATURE_HEADER} header. ` +\n 'Ensure Pathao is sending the signature.',\n );\n }\n\n if (!verifySignature(signature, webhookSecret)) {\n throw new PathaoWebhookError(\n 'Invalid webhook signature. ' +\n 'Check that your webhookSecret matches the Pathao integration secret.',\n );\n }\n\n const bodyStr = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(bodyStr);\n } catch {\n throw new PathaoWebhookError('Webhook payload is not valid JSON.');\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n !('event' in parsed)\n ) {\n throw new PathaoWebhookError(\n 'Webhook payload is missing the required `event` field.',\n );\n }\n\n return parsed as PathaoWebhookPayload;\n}\n\n// ---------------------------------------------------------------------------\n// PathaoWebhookHandler\n// ---------------------------------------------------------------------------\n\ntype GenericHeaders = Record<string, string | string[] | undefined>;\n\n/**\n * Stateful webhook handler that verifies incoming requests, parses payloads,\n * and dispatches events to typed listeners.\n *\n * @example\n * ```typescript\n * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);\n *\n * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {\n * // payload is fully typed as OrderDeliveredPayload\n * console.log(payload.consignment_id, payload.collected_amount);\n * });\n * ```\n */\nexport class PathaoWebhookHandler extends EventEmitter {\n private readonly webhookSecret: string;\n\n constructor(webhookSecret: string) {\n super();\n if (!webhookSecret) {\n throw new PathaoWebhookError(\n 'webhookSecret is required to create a PathaoWebhookHandler.',\n );\n }\n this.webhookSecret = webhookSecret;\n }\n\n // Typed on() overloads ──────────────────────────────────────────────────\n\n /** Listen for a specific Pathao event with a fully-typed payload callback. */\n on<E extends PathaoWebhookEvent>(\n event: E,\n listener: (payload: WebhookEventPayloadMap[E]) => void,\n ): this;\n /** Fires for every successfully verified event regardless of type. */\n on(event: 'webhook', listener: (payload: PathaoWebhookPayload) => void): this;\n /** Fires when verification or parsing fails. */\n on(event: 'error', listener: (error: PathaoWebhookError) => void): this;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n on(event: string | symbol, listener: (...args: any[]) => void): this {\n return super.on(event, listener);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Verify the signature, parse the body, and dispatch the event.\n *\n * Throws `PathaoWebhookError` on failure and also emits `'error'` so\n * listeners can respond without a try/catch.\n *\n * @param rawBody Raw request body — must NOT be pre-parsed\n * @param headers HTTP request headers\n */\n process(rawBody: Buffer | string, headers: GenericHeaders): PathaoWebhookPayload {\n let payload: PathaoWebhookPayload;\n\n try {\n payload = constructEvent(rawBody, headers, this.webhookSecret);\n } catch (err) {\n const webhookErr = err instanceof PathaoWebhookError\n ? err\n : new PathaoWebhookError(err instanceof Error ? err.message : 'Unknown error');\n this.emit('error', webhookErr);\n throw webhookErr;\n }\n\n this.emit(payload.event, payload);\n this.emit('webhook', payload);\n\n return payload;\n }\n\n /**\n * Returns an Express-compatible middleware function.\n *\n * **Requires** `express.raw({ type: 'application/json' })` mounted on the\n * same route before this middleware so the raw body Buffer is preserved.\n *\n * On success the parsed payload is attached to `req.pathaoWebhook`.\n * On failure a `400` response is returned.\n *\n * ```typescript\n * app.post(\n * '/webhooks/pathao',\n * express.raw({ type: 'application/json' }),\n * handler.expressMiddleware(),\n * );\n * ```\n */\n expressMiddleware() {\n return (\n req: {\n body: Buffer | string;\n headers: GenericHeaders;\n pathaoWebhook?: PathaoWebhookPayload;\n },\n res: { status: (code: number) => { json: (body: unknown) => void } },\n next: (err?: unknown) => void,\n ): void => {\n try {\n req.pathaoWebhook = this.process(req.body, req.headers);\n next();\n } catch (err) {\n res.status(400).json({\n error: err instanceof Error ? err.message : 'Webhook processing failed',\n });\n }\n };\n }\n\n /**\n * Returns a generic async handler for any framework (Fastify, Hono, plain\n * `http.createServer`, etc.).\n *\n * Never rejects — always resolves with either `{ payload, error: null }` or\n * `{ payload: null, error: PathaoWebhookError }`.\n *\n * ```typescript\n * const handle = handler.middleware();\n * const { payload, error } = await handle(rawBody, request.headers);\n * if (error) { reply.status(400).send({ error: error.message }); return; }\n * reply.send({ received: true });\n * ```\n */\n middleware() {\n return async (\n rawBody: Buffer | string,\n headers: GenericHeaders,\n ): Promise<\n | { payload: PathaoWebhookPayload; error: null }\n | { payload: null; error: PathaoWebhookError }\n > => {\n try {\n const payload = this.process(rawBody, headers);\n return { payload, error: null };\n } catch (err) {\n const webhookErr = err instanceof PathaoWebhookError\n ? err\n : new PathaoWebhookError(\n err instanceof Error ? err.message : 'Unknown error',\n );\n return { payload: null, error: webhookErr };\n }\n };\n }\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Extract a single-value header, case-insensitively */\nfunction extractHeader(\n headers: GenericHeaders,\n name: string,\n): string | undefined {\n const lower = name.toLowerCase();\n const key = Object.keys(headers).find(k => k.toLowerCase() === lower);\n if (!key) return undefined;\n const value = headers[key];\n if (Array.isArray(value)) return value[0];\n return value;\n}\n"]}
1
+ {"version":3,"sources":["../src/webhooks.ts"],"names":["PathaoWebhookEvent","EventEmitter"],"mappings":"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","file":"webhooks.js","sourcesContent":["/**\n * Pathao Webhook Support\n *\n * Handles incoming webhook events from Pathao.\n *\n * IMPORTANT INTEGRATION DETAILS:\n * - Pathao does NOT sign incoming requests.\n * - Instead, Pathao requires you to prove ownership by echoing your webhook secret\n * in the \\`X-Pathao-Merchant-Webhook-Integration-Secret\\` header of EVERY response.\n * - This SDK automatically handles the \\`webhook_integration\\` handshake event, which\n * expects a 202 status code and the secret header.\n *\n * @example — Express\n * \\`\\`\\`typescript\n * import express from 'express';\n * import {\n * PathaoWebhookHandler,\n * PathaoWebhookEvent,\n * } from 'pathao-merchant-sdk/webhooks';\n *\n * const app = express();\n * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);\n *\n * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => { ... });\n *\n * // Mount express.json() BEFORE the webhook middleware\n * app.post(\n * '/webhooks/pathao',\n * express.json(),\n * handler.expressMiddleware(),\n * (req, res) => {\n * // The middleware already sets the required secret header.\n * // You just need to return a 200 OK for standard events.\n * res.status(200).send('OK');\n * }\n * );\n * \\`\\`\\`\n */\n\nimport { EventEmitter } from 'events';\n\n// ---------------------------------------------------------------------------\n// Error\n// ---------------------------------------------------------------------------\n\nexport class PathaoWebhookError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'PathaoWebhookError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Event types\n// ---------------------------------------------------------------------------\n\n/**\n * All webhook event types emitted by Pathao.\n * Values are the exact strings that appear in the \\`event\\` field of each payload.\n */\nexport enum PathaoWebhookEvent {\n WEBHOOK_INTEGRATION = 'webhook_integration',\n ORDER_CREATED = 'order.created',\n ORDER_UPDATED = 'order.updated',\n ORDER_PICKUP_REQUESTED = 'order.pickup-requested',\n ORDER_ASSIGNED_FOR_PICKUP = 'order.assigned-for-pickup',\n ORDER_PICKED = 'order.picked',\n ORDER_PICKUP_FAILED = 'order.pickup-failed',\n ORDER_PICKUP_CANCELLED = 'order.pickup-cancelled',\n ORDER_AT_THE_SORTING_HUB = 'order.at-the-sorting-hub',\n ORDER_IN_TRANSIT = 'order.in-transit',\n ORDER_RECEIVED_AT_LAST_MILE_HUB = 'order.received-at-last-mile-hub',\n ORDER_ASSIGNED_FOR_DELIVERY = 'order.assigned-for-delivery',\n ORDER_DELIVERED = 'order.delivered',\n ORDER_PARTIAL_DELIVERY = 'order.partial-delivery',\n ORDER_RETURNED = 'order.returned',\n ORDER_DELIVERY_FAILED = 'order.delivery-failed',\n ORDER_ON_HOLD = 'order.on-hold',\n ORDER_PAID = 'order.paid',\n ORDER_PAID_RETURN = 'order.paid-return',\n ORDER_EXCHANGED = 'order.exchanged',\n STORE_CREATED = 'store.created',\n STORE_UPDATED = 'store.updated',\n}\n\n// ---------------------------------------------------------------------------\n// Payload types\n// ---------------------------------------------------------------------------\n\nexport interface WebhookIntegrationPayload {\n event: PathaoWebhookEvent.WEBHOOK_INTEGRATION;\n}\n\n/** Fields present on every normal webhook payload */\nexport interface BaseWebhookPayload {\n event: string;\n /** Format: MySQL datetime YYYY-MM-DD HH:MM:SS (no timezone indicator) */\n updated_at: string;\n /** Format: ISO 8601 timestamp */\n timestamp: string;\n}\n\n/** Fields shared by all order-related events */\nexport interface OrderWebhookPayload extends BaseWebhookPayload {\n consignment_id: string;\n merchant_order_id?: string;\n store_id: number;\n delivery_fee?: number;\n}\n\n/** Fields shared by all store-related events */\nexport interface StoreWebhookPayload extends BaseWebhookPayload {\n store_id: number;\n store_name: string;\n store_address: string;\n is_active: 0 | 1;\n}\n\n// Per-event payload types ────────────────────────────────────────────────────\n\nexport interface OrderCreatedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_CREATED;\n delivery_fee: number;\n}\n\nexport interface OrderUpdatedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_UPDATED;\n delivery_fee: number;\n}\n\nexport interface OrderPickupRequestedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_REQUESTED;\n delivery_fee: number;\n}\n\nexport interface OrderAssignedForPickupPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ASSIGNED_FOR_PICKUP;\n}\n\nexport interface OrderPickedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKED;\n}\n\nexport interface OrderPickupFailedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_FAILED;\n}\n\nexport interface OrderPickupCancelledPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PICKUP_CANCELLED;\n}\n\nexport interface OrderAtSortingHubPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_AT_THE_SORTING_HUB;\n}\n\nexport interface OrderInTransitPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_IN_TRANSIT;\n}\n\nexport interface OrderAtLastMileHubPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_RECEIVED_AT_LAST_MILE_HUB;\n}\n\nexport interface OrderAssignedForDeliveryPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ASSIGNED_FOR_DELIVERY;\n}\n\nexport interface OrderDeliveredPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_DELIVERED;\n collected_amount: number;\n}\n\nexport interface OrderPartialDeliveryPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PARTIAL_DELIVERY;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface OrderReturnedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_RETURNED;\n reason?: string;\n}\n\nexport interface OrderDeliveryFailedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_DELIVERY_FAILED;\n reason?: string;\n}\n\nexport interface OrderOnHoldPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_ON_HOLD;\n reason?: string;\n}\n\nexport interface OrderPaidPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PAID;\n invoice_id: string;\n}\n\nexport interface OrderPaidReturnPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_PAID_RETURN;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface OrderExchangedPayload extends OrderWebhookPayload {\n event: PathaoWebhookEvent.ORDER_EXCHANGED;\n collected_amount: number;\n reason?: string;\n}\n\nexport interface StoreCreatedPayload extends StoreWebhookPayload {\n event: PathaoWebhookEvent.STORE_CREATED;\n}\n\nexport interface StoreUpdatedPayload extends StoreWebhookPayload {\n event: PathaoWebhookEvent.STORE_UPDATED;\n}\n\n/** Union of all possible webhook payloads */\nexport type PathaoWebhookPayload =\n | WebhookIntegrationPayload\n | OrderCreatedPayload\n | OrderUpdatedPayload\n | OrderPickupRequestedPayload\n | OrderAssignedForPickupPayload\n | OrderPickedPayload\n | OrderPickupFailedPayload\n | OrderPickupCancelledPayload\n | OrderAtSortingHubPayload\n | OrderInTransitPayload\n | OrderAtLastMileHubPayload\n | OrderAssignedForDeliveryPayload\n | OrderDeliveredPayload\n | OrderPartialDeliveryPayload\n | OrderReturnedPayload\n | OrderDeliveryFailedPayload\n | OrderOnHoldPayload\n | OrderPaidPayload\n | OrderPaidReturnPayload\n | OrderExchangedPayload\n | StoreCreatedPayload\n | StoreUpdatedPayload;\n\n/** Maps each \\`PathaoWebhookEvent\\` to its specific payload type */\nexport interface WebhookEventPayloadMap {\n [PathaoWebhookEvent.WEBHOOK_INTEGRATION]: WebhookIntegrationPayload;\n [PathaoWebhookEvent.ORDER_CREATED]: OrderCreatedPayload;\n [PathaoWebhookEvent.ORDER_UPDATED]: OrderUpdatedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_REQUESTED]: OrderPickupRequestedPayload;\n [PathaoWebhookEvent.ORDER_ASSIGNED_FOR_PICKUP]: OrderAssignedForPickupPayload;\n [PathaoWebhookEvent.ORDER_PICKED]: OrderPickedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_FAILED]: OrderPickupFailedPayload;\n [PathaoWebhookEvent.ORDER_PICKUP_CANCELLED]: OrderPickupCancelledPayload;\n [PathaoWebhookEvent.ORDER_AT_THE_SORTING_HUB]: OrderAtSortingHubPayload;\n [PathaoWebhookEvent.ORDER_IN_TRANSIT]: OrderInTransitPayload;\n [PathaoWebhookEvent.ORDER_RECEIVED_AT_LAST_MILE_HUB]: OrderAtLastMileHubPayload;\n [PathaoWebhookEvent.ORDER_ASSIGNED_FOR_DELIVERY]: OrderAssignedForDeliveryPayload;\n [PathaoWebhookEvent.ORDER_DELIVERED]: OrderDeliveredPayload;\n [PathaoWebhookEvent.ORDER_PARTIAL_DELIVERY]: OrderPartialDeliveryPayload;\n [PathaoWebhookEvent.ORDER_RETURNED]: OrderReturnedPayload;\n [PathaoWebhookEvent.ORDER_DELIVERY_FAILED]: OrderDeliveryFailedPayload;\n [PathaoWebhookEvent.ORDER_ON_HOLD]: OrderOnHoldPayload;\n [PathaoWebhookEvent.ORDER_PAID]: OrderPaidPayload;\n [PathaoWebhookEvent.ORDER_PAID_RETURN]: OrderPaidReturnPayload;\n [PathaoWebhookEvent.ORDER_EXCHANGED]: OrderExchangedPayload;\n [PathaoWebhookEvent.STORE_CREATED]: StoreCreatedPayload;\n [PathaoWebhookEvent.STORE_UPDATED]: StoreUpdatedPayload;\n}\n\n// ---------------------------------------------------------------------------\n// Core functions\n// ---------------------------------------------------------------------------\n\n/** Required response header used to authorize your endpoint with Pathao */\nexport const PATHAO_SECRET_HEADER = 'x-pathao-merchant-webhook-integration-secret';\n\n/**\n * Parses the raw body into a webhook payload.\n * Pathao does not sign inbound requests, so this just ensures it is valid JSON with an event field.\n *\n * Throws \\`PathaoWebhookError\\` on malformed JSON or missing event.\n *\n * @param rawBody Raw request body or parsed object\n */\nexport function constructEvent(\n rawBody: Buffer | string | object,\n): PathaoWebhookPayload {\n let parsed: unknown;\n if (typeof rawBody === 'object' && !Buffer.isBuffer(rawBody)) {\n parsed = rawBody;\n } else {\n const bodyStr = Buffer.isBuffer(rawBody) ? rawBody.toString('utf8') : rawBody;\n try {\n parsed = JSON.parse(bodyStr);\n } catch {\n throw new PathaoWebhookError('Webhook payload is not valid JSON.');\n }\n }\n\n if (\n !parsed ||\n typeof parsed !== 'object' ||\n !('event' in parsed)\n ) {\n throw new PathaoWebhookError(\n 'Webhook payload is missing the required \\`event\\` field.',\n );\n }\n\n return parsed as PathaoWebhookPayload;\n}\n\n// ---------------------------------------------------------------------------\n// PathaoWebhookHandler\n// ---------------------------------------------------------------------------\n\nexport type WebhookResponseInstructions = {\n statusCode: number;\n headers: Record<string, string>;\n payload: PathaoWebhookPayload | null;\n error: PathaoWebhookError | null;\n};\n\n/**\n * Stateful webhook handler that parses payloads, provides response instructions,\n * and dispatches events to typed listeners.\n *\n * @example\n * \\`\\`\\`typescript\n * const handler = new PathaoWebhookHandler(process.env.PATHAO_WEBHOOK_SECRET!);\n *\n * handler.on(PathaoWebhookEvent.ORDER_DELIVERED, (payload) => {\n * // payload is fully typed as OrderDeliveredPayload\n * console.log(payload.consignment_id, payload.collected_amount);\n * });\n * \\`\\`\\`\n */\nexport class PathaoWebhookHandler extends EventEmitter {\n private readonly webhookSecret: string;\n\n constructor(webhookSecret: string) {\n super();\n if (!webhookSecret) {\n throw new PathaoWebhookError(\n 'webhookSecret is required to create a PathaoWebhookHandler.',\n );\n }\n this.webhookSecret = webhookSecret;\n }\n\n // Typed on() overloads ──────────────────────────────────────────────────\n\n /** Listen for a specific Pathao event with a fully-typed payload callback. */\n on<E extends PathaoWebhookEvent>(\n event: E,\n listener: (payload: WebhookEventPayloadMap[E]) => void,\n ): this;\n /** Fires for every successfully parsed event regardless of type. */\n on(event: 'webhook', listener: (payload: PathaoWebhookPayload) => void): this;\n /** Fires when parsing fails. */\n on(event: 'error', listener: (error: PathaoWebhookError) => void): this;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n on(event: string | symbol, listener: (...args: any[]) => void): this {\n return super.on(event, listener);\n }\n\n /** Listen once for a specific Pathao event. */\n once<E extends PathaoWebhookEvent>(\n event: E,\n listener: (payload: WebhookEventPayloadMap[E]) => void,\n ): this;\n once(event: 'webhook', listener: (payload: PathaoWebhookPayload) => void): this;\n once(event: 'error', listener: (error: PathaoWebhookError) => void): this;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n once(event: string | symbol, listener: (...args: any[]) => void): this {\n return super.once(event, listener);\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Parse the body and dispatch the event.\n *\n * Throws \\`PathaoWebhookError\\` on failure and also emits \\`'error'\\` so\n * listeners can respond without a try/catch.\n *\n * @param rawBody Raw request body or matched json object\n */\n process(rawBody: Buffer | string | object): PathaoWebhookPayload {\n let payload: PathaoWebhookPayload;\n\n try {\n payload = constructEvent(rawBody);\n } catch (err) {\n const webhookErr = err instanceof PathaoWebhookError\n ? err\n : new PathaoWebhookError(err instanceof Error ? err.message : 'Unknown error');\n if (this.listenerCount('error') > 0) {\n this.emit('error', webhookErr);\n }\n throw webhookErr;\n }\n\n this.emit(payload.event, payload);\n this.emit('webhook', payload);\n\n return payload;\n }\n\n /**\n * Returns an Express-compatible middleware function.\n *\n * Automatically sets the required \\`X-Pathao-Merchant-Webhook-Integration-Secret\\` header.\n * Automatically responds with 202 for the \\`webhook_integration\\` handshake.\n * For standard events, attaches the payload to \\`req.pathaoWebhook\\` and calls \\`next()\\`.\n * On error, calls \\`next(err)\\`.\n *\n * \\`\\`\\`typescript\n * app.post(\n * '/webhooks/pathao',\n * express.json(),\n * handler.expressMiddleware(),\n * (req, res) => res.sendStatus(200) // You must send 200 for other events\n * );\n * \\`\\`\\`\n */\n expressMiddleware() {\n return (\n req: {\n body: Buffer | string | object;\n pathaoWebhook?: PathaoWebhookPayload;\n },\n res: {\n setHeader: (name: string, value: string) => void;\n status: (code: number) => { send: () => void };\n },\n next: (err?: unknown) => void,\n ): void => {\n try {\n res.setHeader(PATHAO_SECRET_HEADER, this.webhookSecret);\n \n const payload = this.process(req.body);\n req.pathaoWebhook = payload;\n \n if (payload.event === PathaoWebhookEvent.WEBHOOK_INTEGRATION) {\n res.status(202).send();\n return;\n }\n\n next();\n } catch (err) {\n next(err);\n }\n };\n }\n\n /**\n * Returns response instructions for any framework (Fastify, Hono, etc.).\n *\n * Never throws — always resolves with a \\`WebhookResponseInstructions\\` object\n * that tells you which status code and headers to return, along with the payload/error.\n *\n * \\`\\`\\`typescript\n * const handle = handler.middleware();\n * const instructions = await handle(request.body);\n * \n * // Apply the required headers (the secret header)\n * for (const [key, value] of Object.entries(instructions.headers)) {\n * reply.header(key, value);\n * }\n * \n * if (instructions.error) {\n * return reply.status(instructions.statusCode).send({ error: instructions.error.message });\n * }\n * \n * // If it was the handshake, we should just return 202 as instructed\n * if (instructions.payload?.event === 'webhook_integration') {\n * return reply.status(instructions.statusCode).send();\n * }\n * \n * // Process your real webhook\n * return reply.status(instructions.statusCode).send({ received: true });\n * \\`\\`\\`\n */\n middleware() {\n return async (\n rawBody: Buffer | string | object,\n ): Promise<WebhookResponseInstructions> => {\n const headers = { [PATHAO_SECRET_HEADER]: this.webhookSecret };\n \n try {\n const payload = this.process(rawBody);\n const isHandshake = payload.event === PathaoWebhookEvent.WEBHOOK_INTEGRATION;\n return {\n statusCode: isHandshake ? 202 : 200,\n headers,\n payload,\n error: null\n };\n } catch (err) {\n const webhookErr = err instanceof PathaoWebhookError\n ? err\n : new PathaoWebhookError(\n err instanceof Error ? err.message : 'Unknown error',\n );\n return {\n statusCode: 400,\n headers,\n payload: null,\n error: webhookErr\n };\n }\n };\n }\n}\n"]}