waba-toolkit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,7 +41,6 @@ npm install waba-toolkit
41
41
  ```typescript
42
42
  import {
43
43
  WABAClient,
44
- verifyWebhookSignature,
45
44
  classifyWebhook,
46
45
  classifyMessage,
47
46
  isMediaMessage,
@@ -51,16 +50,16 @@ import {
51
50
  // Initialize client
52
51
  const client = new WABAClient({
53
52
  accessToken: process.env.META_ACCESS_TOKEN,
53
+ appSecret: process.env.META_APP_SECRET, // Required for webhook verification
54
54
  });
55
55
 
56
56
  // In your webhook handler
57
57
  app.post('/webhook', async (req, res) => {
58
58
  // 1. Verify signature
59
- const isValid = verifyWebhookSignature({
60
- signature: req.headers['x-hub-signature-256'],
61
- rawBody: req.rawBody,
62
- appSecret: process.env.META_APP_SECRET,
63
- });
59
+ const isValid = client.verifyWebhook(
60
+ req.headers['x-hub-signature-256'],
61
+ req.rawBody
62
+ );
64
63
 
65
64
  if (!isValid) {
66
65
  return res.status(401).send('Invalid signature');
@@ -105,6 +104,7 @@ Client for downloading media from WhatsApp Business API.
105
104
  ```typescript
106
105
  const client = new WABAClient({
107
106
  accessToken: string, // Required: Meta access token
107
+ appSecret?: string, // Optional: Meta App Secret (required for verifyWebhook)
108
108
  apiVersion?: string, // Optional: API version (default: 'v22.0')
109
109
  baseUrl?: string, // Optional: Base URL (default: 'https://graph.facebook.com')
110
110
  });
@@ -114,10 +114,17 @@ const client = new WABAClient({
114
114
 
115
115
  | Method | Description |
116
116
  |--------|-------------|
117
+ | `verifyWebhook(signature, rawBody)` | Verifies webhook signature (requires `appSecret`) |
117
118
  | `getMedia(mediaId)` | Downloads media, returns `ReadableStream` |
118
119
  | `getMedia(mediaId, { asBuffer: true })` | Downloads media, returns `ArrayBuffer` |
119
120
 
120
121
  ```typescript
122
+ // Verify webhook signature
123
+ const isValid = client.verifyWebhook(
124
+ req.headers['x-hub-signature-256'],
125
+ req.rawBody
126
+ );
127
+
121
128
  // Stream (default)
122
129
  const { stream, mimeType, sha256, fileSize, url } = await client.getMedia(mediaId);
123
130
 
@@ -139,11 +146,26 @@ const { buffer, mimeType, sha256, fileSize, url } = await client.getMedia(mediaI
139
146
 
140
147
  #### verifyWebhookSignature
141
148
 
149
+ Two options for verifying webhook signatures:
150
+
142
151
  ```typescript
152
+ // Option 1: Via client method (recommended)
153
+ const client = new WABAClient({
154
+ accessToken: '...',
155
+ appSecret: process.env.META_APP_SECRET,
156
+ });
157
+ const isValid = client.verifyWebhook(
158
+ req.headers['x-hub-signature-256'],
159
+ req.rawBody
160
+ );
161
+
162
+ // Option 2: Standalone function
163
+ import { verifyWebhookSignature } from 'waba-toolkit';
164
+
143
165
  const isValid = verifyWebhookSignature({
144
166
  signature: req.headers['x-hub-signature-256'], // X-Hub-Signature-256 header
145
167
  rawBody: req.rawBody, // Raw body as Buffer or string
146
- appSecret: process.env.META_APP_SECRET, // Meta App Secret
168
+ appSecret: process.env.META_APP_SECRET, // Meta App Secret (required)
147
169
  });
148
170
  ```
149
171
 
@@ -196,7 +218,7 @@ switch (result.type) {
196
218
  case 'reaction':
197
219
  console.log(result.message.reaction.emoji);
198
220
  break;
199
- // ... button, order, system, referral, unsupported
221
+ // Additional cases: button, order, system, referral, ...
200
222
  }
201
223
  ```
202
224
 
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  interface WABAClientOptions {
2
2
  /** Meta access token with whatsapp_business_messaging permission */
3
3
  accessToken: string;
4
+ /** Meta App Secret for webhook signature verification */
5
+ appSecret?: string;
4
6
  /** API version (default: 'v22.0') */
5
7
  apiVersion?: string;
6
8
  /** Base URL (default: 'https://graph.facebook.com') */
@@ -37,9 +39,18 @@ interface MediaBufferResult extends MediaMetadata {
37
39
  */
38
40
  declare class WABAClient {
39
41
  private readonly accessToken;
42
+ private readonly appSecret?;
40
43
  private readonly apiVersion;
41
44
  private readonly baseUrl;
42
45
  constructor(options: WABAClientOptions);
46
+ /**
47
+ * Verifies webhook signature using HMAC-SHA256.
48
+ * Requires appSecret to be set in constructor options.
49
+ *
50
+ * @throws {WABASignatureError} If appSecret was not provided in constructor
51
+ * @returns true if signature is valid, false otherwise
52
+ */
53
+ verifyWebhook(signature: string | undefined, rawBody: Buffer | string): boolean;
43
54
  /**
44
55
  * Fetches media by ID from WhatsApp Business API.
45
56
  *
@@ -494,12 +505,13 @@ interface VerifyWebhookSignatureOptions {
494
505
  /** Raw request body (NOT parsed JSON) */
495
506
  rawBody: Buffer | string;
496
507
  /** Meta App Secret */
497
- appSecret: string;
508
+ appSecret: string | undefined;
498
509
  }
499
510
  /**
500
511
  * Verifies webhook signature using HMAC-SHA256.
501
512
  * Uses timing-safe comparison to prevent timing attacks.
502
513
  *
514
+ * @throws {WABASignatureError} If appSecret is not provided
503
515
  * @returns true if signature is valid, false otherwise
504
516
  */
505
517
  declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): boolean;
package/dist/index.js CHANGED
@@ -33,18 +33,57 @@ var WABASignatureError = class extends WABAError {
33
33
  }
34
34
  };
35
35
 
36
+ // src/verify.ts
37
+ import { createHmac, timingSafeEqual } from "crypto";
38
+ function verifyWebhookSignature(options) {
39
+ const { signature, rawBody, appSecret } = options;
40
+ if (!appSecret) {
41
+ throw new WABASignatureError("appSecret is required for webhook verification");
42
+ }
43
+ if (!signature) {
44
+ return false;
45
+ }
46
+ const signatureHash = signature.startsWith("sha256=") ? signature.slice(7) : signature;
47
+ const hmac = createHmac("sha256", appSecret);
48
+ const bodyBuffer = typeof rawBody === "string" ? Buffer.from(rawBody, "utf-8") : rawBody;
49
+ const expectedHash = hmac.update(bodyBuffer).digest("hex");
50
+ const signatureBuffer = Buffer.from(signatureHash, "utf-8");
51
+ const expectedBuffer = Buffer.from(expectedHash, "utf-8");
52
+ if (signatureBuffer.length !== expectedBuffer.length) {
53
+ return false;
54
+ }
55
+ return timingSafeEqual(signatureBuffer, expectedBuffer);
56
+ }
57
+
36
58
  // src/client.ts
37
59
  var DEFAULT_API_VERSION = "v22.0";
38
60
  var DEFAULT_BASE_URL = "https://graph.facebook.com";
39
61
  var WABAClient = class {
40
62
  accessToken;
63
+ appSecret;
41
64
  apiVersion;
42
65
  baseUrl;
43
66
  constructor(options) {
44
67
  this.accessToken = options.accessToken;
68
+ this.appSecret = options.appSecret;
45
69
  this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
46
70
  this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
47
71
  }
72
+ /**
73
+ * Verifies webhook signature using HMAC-SHA256.
74
+ * Requires appSecret to be set in constructor options.
75
+ *
76
+ * @throws {WABASignatureError} If appSecret was not provided in constructor
77
+ * @returns true if signature is valid, false otherwise
78
+ */
79
+ verifyWebhook(signature, rawBody) {
80
+ if (!this.appSecret) {
81
+ throw new WABASignatureError(
82
+ "appSecret is required for webhook verification. Pass it in WABAClientOptions."
83
+ );
84
+ }
85
+ return verifyWebhookSignature({ signature, rawBody, appSecret: this.appSecret });
86
+ }
48
87
  async getMedia(mediaId, options) {
49
88
  const metadata = await this.fetchMediaMetadata(mediaId);
50
89
  const response = await this.downloadMedia(metadata.url, mediaId);
@@ -181,25 +220,6 @@ function classifyMessage(message) {
181
220
  }
182
221
  }
183
222
 
184
- // src/verify.ts
185
- import { createHmac, timingSafeEqual } from "crypto";
186
- function verifyWebhookSignature(options) {
187
- const { signature, rawBody, appSecret } = options;
188
- if (!signature) {
189
- return false;
190
- }
191
- const signatureHash = signature.startsWith("sha256=") ? signature.slice(7) : signature;
192
- const hmac = createHmac("sha256", appSecret);
193
- const bodyBuffer = typeof rawBody === "string" ? Buffer.from(rawBody, "utf-8") : rawBody;
194
- const expectedHash = hmac.update(bodyBuffer).digest("hex");
195
- const signatureBuffer = Buffer.from(signatureHash, "utf-8");
196
- const expectedBuffer = Buffer.from(expectedHash, "utf-8");
197
- if (signatureBuffer.length !== expectedBuffer.length) {
198
- return false;
199
- }
200
- return timingSafeEqual(signatureBuffer, expectedBuffer);
201
- }
202
-
203
223
  // src/helpers.ts
204
224
  var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
205
225
  function isMediaMessage(message) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/webhooks/classify.ts","../src/webhooks/messages.ts","../src/verify.ts","../src/helpers.ts"],"sourcesContent":["/** Base error class for all WABA errors */\nexport class WABAError extends Error {\n constructor(\n message: string,\n public readonly code?: number,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'WABAError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for media-related failures (404, access denied) */\nexport class WABAMediaError extends WABAError {\n constructor(\n message: string,\n public readonly mediaId: string,\n code?: number\n ) {\n super(message, code);\n this.name = 'WABAMediaError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for network/connection failures */\nexport class WABANetworkError extends WABAError {\n override readonly cause?: Error;\n\n constructor(message: string, cause?: Error) {\n super(message);\n this.name = 'WABANetworkError';\n this.cause = cause;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for invalid webhook signatures */\nexport class WABASignatureError extends WABAError {\n constructor(message: string = 'Invalid webhook signature') {\n super(message);\n this.name = 'WABASignatureError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import type { WABAClientOptions, GetMediaOptions } from './types/client.js';\nimport type {\n MediaMetadata,\n MediaStreamResult,\n MediaBufferResult,\n RawMediaResponse,\n} from './types/media.js';\nimport { WABAMediaError, WABANetworkError } from './errors.js';\n\nconst DEFAULT_API_VERSION = 'v22.0';\nconst DEFAULT_BASE_URL = 'https://graph.facebook.com';\n\n/**\n * Client for WhatsApp Business API media operations.\n */\nexport class WABAClient {\n private readonly accessToken: string;\n private readonly apiVersion: string;\n private readonly baseUrl: string;\n\n constructor(options: WABAClientOptions) {\n this.accessToken = options.accessToken;\n this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;\n this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n }\n\n /**\n * Fetches media by ID from WhatsApp Business API.\n *\n * Step 1: GET /{apiVersion}/{mediaId} → retrieves temporary URL + metadata\n * Step 2: GET {temporaryUrl} → downloads binary content\n *\n * @throws {WABAMediaError} - Media not found (404) or access denied\n * @throws {WABANetworkError} - Network/connection failures\n */\n async getMedia(mediaId: string): Promise<MediaStreamResult>;\n async getMedia(\n mediaId: string,\n options: { asBuffer: true }\n ): Promise<MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult> {\n // Step 1: Get media metadata and temporary URL\n const metadata = await this.fetchMediaMetadata(mediaId);\n\n // Step 2: Download the actual media\n const response = await this.downloadMedia(metadata.url, mediaId);\n\n if (options?.asBuffer) {\n const buffer = await response.arrayBuffer();\n return {\n ...metadata,\n buffer,\n };\n }\n\n if (!response.body) {\n throw new WABAMediaError('Response body is null', mediaId);\n }\n\n return {\n ...metadata,\n stream: response.body,\n };\n }\n\n private async fetchMediaMetadata(mediaId: string): Promise<MediaMetadata> {\n const url = `${this.baseUrl}/${this.apiVersion}/${mediaId}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to fetch media metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => 'Unknown error');\n throw new WABAMediaError(\n `Failed to fetch media metadata: ${response.status} ${errorBody}`,\n mediaId,\n response.status\n );\n }\n\n const data = (await response.json()) as RawMediaResponse;\n\n // Normalize snake_case to camelCase\n return {\n id: data.id,\n mimeType: data.mime_type,\n sha256: data.sha256,\n fileSize: parseInt(data.file_size, 10),\n url: data.url,\n };\n }\n\n private async downloadMedia(url: string, mediaId: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to download media: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n throw new WABAMediaError(\n `Failed to download media: ${response.status}`,\n mediaId,\n response.status\n );\n }\n\n return response;\n }\n}\n","import type {\n WebhookPayload,\n WebhookClassification,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} from '../types/webhooks.js';\n\n/**\n * Classifies a webhook payload into its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyWebhook(payload: WebhookPayload): WebhookClassification {\n const entry = payload.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return { type: 'unknown', payload };\n }\n\n // Check for calls array (call webhooks)\n if ('calls' in value && Array.isArray(value.calls)) {\n return { type: 'call', payload: value as CallWebhookValue };\n }\n\n // Check for statuses array (status webhooks)\n if ('statuses' in value && Array.isArray(value.statuses)) {\n return { type: 'status', payload: value as StatusWebhookValue };\n }\n\n // Check for messages array (message webhooks)\n if ('messages' in value && Array.isArray(value.messages)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n // Check for errors without messages (error webhook)\n if ('errors' in value && Array.isArray(value.errors)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n return { type: 'unknown', payload };\n}\n","import type {\n IncomingMessage,\n MessageClassification,\n TextMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n LocationMessage,\n ContactsMessage,\n InteractiveMessage,\n ReactionMessage,\n ButtonMessage,\n OrderMessage,\n SystemMessage,\n ReferralMessage,\n UnsupportedMessage,\n} from '../types/messages.js';\n\n/**\n * Classifies an incoming message by its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyMessage(message: IncomingMessage): MessageClassification {\n switch (message.type) {\n case 'text':\n return { type: 'text', message: message as TextMessage };\n case 'image':\n return { type: 'image', message: message as ImageMessage };\n case 'audio':\n return { type: 'audio', message: message as AudioMessage };\n case 'video':\n return { type: 'video', message: message as VideoMessage };\n case 'document':\n return { type: 'document', message: message as DocumentMessage };\n case 'sticker':\n return { type: 'sticker', message: message as StickerMessage };\n case 'location':\n return { type: 'location', message: message as LocationMessage };\n case 'contacts':\n return { type: 'contacts', message: message as ContactsMessage };\n case 'interactive':\n return { type: 'interactive', message: message as InteractiveMessage };\n case 'reaction':\n return { type: 'reaction', message: message as ReactionMessage };\n case 'button':\n return { type: 'button', message: message as ButtonMessage };\n case 'order':\n return { type: 'order', message: message as OrderMessage };\n case 'system':\n return { type: 'system', message: message as SystemMessage };\n case 'referral':\n return { type: 'referral', message: message as ReferralMessage };\n default:\n return { type: 'unsupported', message: message as UnsupportedMessage };\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nexport interface VerifyWebhookSignatureOptions {\n /** X-Hub-Signature-256 header value */\n signature: string | undefined;\n /** Raw request body (NOT parsed JSON) */\n rawBody: Buffer | string;\n /** Meta App Secret */\n appSecret: string;\n}\n\n/**\n * Verifies webhook signature using HMAC-SHA256.\n * Uses timing-safe comparison to prevent timing attacks.\n *\n * @returns true if signature is valid, false otherwise\n */\nexport function verifyWebhookSignature(\n options: VerifyWebhookSignatureOptions\n): boolean {\n const { signature, rawBody, appSecret } = options;\n\n if (!signature) {\n return false;\n }\n\n // Remove 'sha256=' prefix if present\n const signatureHash = signature.startsWith('sha256=')\n ? signature.slice(7)\n : signature;\n\n // Compute expected signature\n const hmac = createHmac('sha256', appSecret);\n const bodyBuffer =\n typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf-8') : rawBody;\n const expectedHash = hmac.update(bodyBuffer).digest('hex');\n\n // Convert to buffers for timing-safe comparison\n const signatureBuffer = Buffer.from(signatureHash, 'utf-8');\n const expectedBuffer = Buffer.from(expectedHash, 'utf-8');\n\n // Ensure same length before comparison\n if (signatureBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(signatureBuffer, expectedBuffer);\n}\n","import type {\n IncomingMessage,\n MediaMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n} from './types/messages.js';\nimport type { WebhookPayload } from './types/webhooks.js';\n\nconst MEDIA_TYPES = new Set(['image', 'audio', 'video', 'document', 'sticker']);\n\n/**\n * Type guard: returns true if message contains downloadable media.\n * Narrows type to messages with image/audio/video/document/sticker.\n */\nexport function isMediaMessage(message: IncomingMessage): message is MediaMessage {\n return MEDIA_TYPES.has(message.type);\n}\n\n/**\n * Extracts media ID from any media message type.\n * Returns undefined if message has no media.\n */\nexport function extractMediaId(message: IncomingMessage): string | undefined {\n if (!isMediaMessage(message)) {\n return undefined;\n }\n\n switch (message.type) {\n case 'image':\n return (message as ImageMessage).image.id;\n case 'audio':\n return (message as AudioMessage).audio.id;\n case 'video':\n return (message as VideoMessage).video.id;\n case 'document':\n return (message as DocumentMessage).document.id;\n case 'sticker':\n return (message as StickerMessage).sticker.id;\n default:\n return undefined;\n }\n}\n\nexport interface ContactInfo {\n waId: string;\n profileName: string | undefined;\n phoneNumberId: string;\n}\n\n/**\n * Extracts sender info from webhook payload.\n * Returns undefined if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | undefined {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value || !('contacts' in value) || !value.contacts?.length) {\n return undefined;\n }\n\n const contact = value.contacts[0];\n const metadata = value.metadata;\n\n return {\n waId: contact.wa_id,\n profileName: contact.profile?.name,\n phoneNumberId: metadata.phone_number_id,\n };\n}\n\n/**\n * Parses message timestamp to Date object.\n * WhatsApp timestamps are Unix epoch seconds as strings.\n */\nexport function getMessageTimestamp(message: IncomingMessage): Date {\n const epochSeconds = parseInt(message.timestamp, 10);\n return new Date(epochSeconds * 1000);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/errors.ts","../src/verify.ts","../src/client.ts","../src/webhooks/classify.ts","../src/webhooks/messages.ts","../src/helpers.ts"],"sourcesContent":["/** Base error class for all WABA errors */\nexport class WABAError extends Error {\n constructor(\n message: string,\n public readonly code?: number,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'WABAError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for media-related failures (404, access denied) */\nexport class WABAMediaError extends WABAError {\n constructor(\n message: string,\n public readonly mediaId: string,\n code?: number\n ) {\n super(message, code);\n this.name = 'WABAMediaError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for network/connection failures */\nexport class WABANetworkError extends WABAError {\n override readonly cause?: Error;\n\n constructor(message: string, cause?: Error) {\n super(message);\n this.name = 'WABANetworkError';\n this.cause = cause;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for invalid webhook signatures */\nexport class WABASignatureError extends WABAError {\n constructor(message: string = 'Invalid webhook signature') {\n super(message);\n this.name = 'WABASignatureError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\nimport { WABASignatureError } from './errors.js';\n\nexport interface VerifyWebhookSignatureOptions {\n /** X-Hub-Signature-256 header value */\n signature: string | undefined;\n /** Raw request body (NOT parsed JSON) */\n rawBody: Buffer | string;\n /** Meta App Secret */\n appSecret: string | undefined;\n}\n\n/**\n * Verifies webhook signature using HMAC-SHA256.\n * Uses timing-safe comparison to prevent timing attacks.\n *\n * @throws {WABASignatureError} If appSecret is not provided\n * @returns true if signature is valid, false otherwise\n */\nexport function verifyWebhookSignature(\n options: VerifyWebhookSignatureOptions\n): boolean {\n const { signature, rawBody, appSecret } = options;\n\n if (!appSecret) {\n throw new WABASignatureError('appSecret is required for webhook verification');\n }\n\n if (!signature) {\n return false;\n }\n\n // Remove 'sha256=' prefix if present\n const signatureHash = signature.startsWith('sha256=')\n ? signature.slice(7)\n : signature;\n\n // Compute expected signature\n const hmac = createHmac('sha256', appSecret);\n const bodyBuffer =\n typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf-8') : rawBody;\n const expectedHash = hmac.update(bodyBuffer).digest('hex');\n\n // Convert to buffers for timing-safe comparison\n const signatureBuffer = Buffer.from(signatureHash, 'utf-8');\n const expectedBuffer = Buffer.from(expectedHash, 'utf-8');\n\n // Ensure same length before comparison\n if (signatureBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(signatureBuffer, expectedBuffer);\n}\n","import type { WABAClientOptions, GetMediaOptions } from './types/client.js';\nimport type {\n MediaMetadata,\n MediaStreamResult,\n MediaBufferResult,\n RawMediaResponse,\n} from './types/media.js';\nimport { WABAMediaError, WABANetworkError, WABASignatureError } from './errors.js';\nimport { verifyWebhookSignature } from './verify.js';\n\nconst DEFAULT_API_VERSION = 'v22.0';\nconst DEFAULT_BASE_URL = 'https://graph.facebook.com';\n\n/**\n * Client for WhatsApp Business API media operations.\n */\nexport class WABAClient {\n private readonly accessToken: string;\n private readonly appSecret?: string;\n private readonly apiVersion: string;\n private readonly baseUrl: string;\n\n constructor(options: WABAClientOptions) {\n this.accessToken = options.accessToken;\n this.appSecret = options.appSecret;\n this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;\n this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n }\n\n /**\n * Verifies webhook signature using HMAC-SHA256.\n * Requires appSecret to be set in constructor options.\n *\n * @throws {WABASignatureError} If appSecret was not provided in constructor\n * @returns true if signature is valid, false otherwise\n */\n verifyWebhook(signature: string | undefined, rawBody: Buffer | string): boolean {\n if (!this.appSecret) {\n throw new WABASignatureError(\n 'appSecret is required for webhook verification. Pass it in WABAClientOptions.'\n );\n }\n return verifyWebhookSignature({ signature, rawBody, appSecret: this.appSecret });\n }\n\n /**\n * Fetches media by ID from WhatsApp Business API.\n *\n * Step 1: GET /{apiVersion}/{mediaId} → retrieves temporary URL + metadata\n * Step 2: GET {temporaryUrl} → downloads binary content\n *\n * @throws {WABAMediaError} - Media not found (404) or access denied\n * @throws {WABANetworkError} - Network/connection failures\n */\n async getMedia(mediaId: string): Promise<MediaStreamResult>;\n async getMedia(\n mediaId: string,\n options: { asBuffer: true }\n ): Promise<MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult> {\n // Step 1: Get media metadata and temporary URL\n const metadata = await this.fetchMediaMetadata(mediaId);\n\n // Step 2: Download the actual media\n const response = await this.downloadMedia(metadata.url, mediaId);\n\n if (options?.asBuffer) {\n const buffer = await response.arrayBuffer();\n return {\n ...metadata,\n buffer,\n };\n }\n\n if (!response.body) {\n throw new WABAMediaError('Response body is null', mediaId);\n }\n\n return {\n ...metadata,\n stream: response.body,\n };\n }\n\n private async fetchMediaMetadata(mediaId: string): Promise<MediaMetadata> {\n const url = `${this.baseUrl}/${this.apiVersion}/${mediaId}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to fetch media metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => 'Unknown error');\n throw new WABAMediaError(\n `Failed to fetch media metadata: ${response.status} ${errorBody}`,\n mediaId,\n response.status\n );\n }\n\n const data = (await response.json()) as RawMediaResponse;\n\n // Normalize snake_case to camelCase\n return {\n id: data.id,\n mimeType: data.mime_type,\n sha256: data.sha256,\n fileSize: parseInt(data.file_size, 10),\n url: data.url,\n };\n }\n\n private async downloadMedia(url: string, mediaId: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to download media: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n throw new WABAMediaError(\n `Failed to download media: ${response.status}`,\n mediaId,\n response.status\n );\n }\n\n return response;\n }\n}\n","import type {\n WebhookPayload,\n WebhookClassification,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} from '../types/webhooks.js';\n\n/**\n * Classifies a webhook payload into its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyWebhook(payload: WebhookPayload): WebhookClassification {\n const entry = payload.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return { type: 'unknown', payload };\n }\n\n // Check for calls array (call webhooks)\n if ('calls' in value && Array.isArray(value.calls)) {\n return { type: 'call', payload: value as CallWebhookValue };\n }\n\n // Check for statuses array (status webhooks)\n if ('statuses' in value && Array.isArray(value.statuses)) {\n return { type: 'status', payload: value as StatusWebhookValue };\n }\n\n // Check for messages array (message webhooks)\n if ('messages' in value && Array.isArray(value.messages)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n // Check for errors without messages (error webhook)\n if ('errors' in value && Array.isArray(value.errors)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n return { type: 'unknown', payload };\n}\n","import type {\n IncomingMessage,\n MessageClassification,\n TextMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n LocationMessage,\n ContactsMessage,\n InteractiveMessage,\n ReactionMessage,\n ButtonMessage,\n OrderMessage,\n SystemMessage,\n ReferralMessage,\n UnsupportedMessage,\n} from '../types/messages.js';\n\n/**\n * Classifies an incoming message by its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyMessage(message: IncomingMessage): MessageClassification {\n switch (message.type) {\n case 'text':\n return { type: 'text', message: message as TextMessage };\n case 'image':\n return { type: 'image', message: message as ImageMessage };\n case 'audio':\n return { type: 'audio', message: message as AudioMessage };\n case 'video':\n return { type: 'video', message: message as VideoMessage };\n case 'document':\n return { type: 'document', message: message as DocumentMessage };\n case 'sticker':\n return { type: 'sticker', message: message as StickerMessage };\n case 'location':\n return { type: 'location', message: message as LocationMessage };\n case 'contacts':\n return { type: 'contacts', message: message as ContactsMessage };\n case 'interactive':\n return { type: 'interactive', message: message as InteractiveMessage };\n case 'reaction':\n return { type: 'reaction', message: message as ReactionMessage };\n case 'button':\n return { type: 'button', message: message as ButtonMessage };\n case 'order':\n return { type: 'order', message: message as OrderMessage };\n case 'system':\n return { type: 'system', message: message as SystemMessage };\n case 'referral':\n return { type: 'referral', message: message as ReferralMessage };\n default:\n return { type: 'unsupported', message: message as UnsupportedMessage };\n }\n}\n","import type {\n IncomingMessage,\n MediaMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n} from './types/messages.js';\nimport type { WebhookPayload } from './types/webhooks.js';\n\nconst MEDIA_TYPES = new Set(['image', 'audio', 'video', 'document', 'sticker']);\n\n/**\n * Type guard: returns true if message contains downloadable media.\n * Narrows type to messages with image/audio/video/document/sticker.\n */\nexport function isMediaMessage(message: IncomingMessage): message is MediaMessage {\n return MEDIA_TYPES.has(message.type);\n}\n\n/**\n * Extracts media ID from any media message type.\n * Returns undefined if message has no media.\n */\nexport function extractMediaId(message: IncomingMessage): string | undefined {\n if (!isMediaMessage(message)) {\n return undefined;\n }\n\n switch (message.type) {\n case 'image':\n return (message as ImageMessage).image.id;\n case 'audio':\n return (message as AudioMessage).audio.id;\n case 'video':\n return (message as VideoMessage).video.id;\n case 'document':\n return (message as DocumentMessage).document.id;\n case 'sticker':\n return (message as StickerMessage).sticker.id;\n default:\n return undefined;\n }\n}\n\nexport interface ContactInfo {\n waId: string;\n profileName: string | undefined;\n phoneNumberId: string;\n}\n\n/**\n * Extracts sender info from webhook payload.\n * Returns undefined if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | undefined {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value || !('contacts' in value) || !value.contacts?.length) {\n return undefined;\n }\n\n const contact = value.contacts[0];\n const metadata = value.metadata;\n\n return {\n waId: contact.wa_id,\n profileName: contact.profile?.name,\n phoneNumberId: metadata.phone_number_id,\n };\n}\n\n/**\n * Parses message timestamp to Date object.\n * WhatsApp timestamps are Unix epoch seconds as strings.\n */\nexport function getMessageTimestamp(message: IncomingMessage): Date {\n const epochSeconds = parseInt(message.timestamp, 10);\n return new Date(epochSeconds * 1000);\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waba-toolkit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Minimal, type-safe toolkit for WhatsApp Business API webhook processing and media handling",
5
5
  "type": "module",
6
6
  "engines": {
@@ -18,6 +18,8 @@
18
18
  "scripts": {
19
19
  "build": "tsup",
20
20
  "typecheck": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
21
23
  "prepublishOnly": "npm run build"
22
24
  },
23
25
  "keywords": [
@@ -31,6 +33,7 @@
31
33
  "devDependencies": {
32
34
  "@types/node": "^25.0.3",
33
35
  "tsup": "^8.0.0",
34
- "typescript": "^5.0.0"
36
+ "typescript": "^5.0.0",
37
+ "vitest": "^4.0.16"
35
38
  }
36
39
  }