waba-toolkit 0.1.3 → 0.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.
- package/README.md +72 -18
- package/dist/index.d.mts +580 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +45 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +312 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# waba-toolkit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe, zero-dependency WhatsApp Business API toolkit for webhooks, media downloads, and signature verification.
|
|
4
4
|
|
|
5
5
|
> **Note:** This is not an official Meta/WhatsApp package, nor is it a full API wrapper. It is a utility toolkit derived from patterns across several production projects that interface directly with the WhatsApp Business API (Cloud API).
|
|
6
6
|
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
WABAClient,
|
|
44
44
|
classifyWebhook,
|
|
45
45
|
classifyMessage,
|
|
46
|
+
getContactInfo,
|
|
47
|
+
getMessageId,
|
|
46
48
|
isMediaMessage,
|
|
47
49
|
extractMediaId,
|
|
48
50
|
} from 'waba-toolkit';
|
|
@@ -69,22 +71,36 @@ app.post('/webhook', async (req, res) => {
|
|
|
69
71
|
const webhook = classifyWebhook(req.body);
|
|
70
72
|
|
|
71
73
|
if (webhook.type === 'message') {
|
|
74
|
+
// 3. Extract contact info
|
|
75
|
+
const contact = getContactInfo(req.body);
|
|
76
|
+
if (contact) {
|
|
77
|
+
console.log('From:', contact.waId, contact.profileName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Get message ID for marking as read
|
|
81
|
+
const messageId = getMessageId(req.body);
|
|
82
|
+
|
|
72
83
|
const message = webhook.payload.messages?.[0];
|
|
73
84
|
if (!message) return res.sendStatus(200);
|
|
74
85
|
|
|
75
|
-
//
|
|
86
|
+
// 5. Classify message type
|
|
76
87
|
const classified = classifyMessage(message);
|
|
77
88
|
|
|
78
89
|
if (classified.type === 'text') {
|
|
79
90
|
console.log('Text:', classified.message.text.body);
|
|
80
91
|
}
|
|
81
92
|
|
|
82
|
-
//
|
|
93
|
+
// 6. Handle media messages
|
|
83
94
|
if (isMediaMessage(message)) {
|
|
84
95
|
const mediaId = extractMediaId(message);
|
|
85
96
|
const { stream, mimeType } = await client.getMedia(mediaId);
|
|
86
97
|
// Process stream...
|
|
87
98
|
}
|
|
99
|
+
|
|
100
|
+
// 7. Mark as read
|
|
101
|
+
if (messageId) {
|
|
102
|
+
await markAsRead(messageId);
|
|
103
|
+
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
res.sendStatus(200);
|
|
@@ -226,35 +242,73 @@ switch (result.type) {
|
|
|
226
242
|
|
|
227
243
|
### Helper Functions
|
|
228
244
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
|
234
|
-
|
|
245
|
+
#### Webhook-level Helpers
|
|
246
|
+
|
|
247
|
+
These helpers accept `WebhookPayload` and extract data from the top-level webhook structure:
|
|
248
|
+
|
|
249
|
+
| Function | Description | Returns |
|
|
250
|
+
|----------|-------------|---------|
|
|
251
|
+
| `getContactInfo(webhook)` | Extracts sender's `waId`, `profileName`, and `phoneNumberId` | `ContactInfo \| null` |
|
|
252
|
+
| `getMessageId(webhook)` | Extracts message ID from message or status webhooks | `string \| null` |
|
|
253
|
+
| `getCallId(webhook)` | Extracts call ID from call webhooks | `string \| null` |
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
import { getContactInfo, getMessageId, getCallId } from 'waba-toolkit';
|
|
257
|
+
|
|
258
|
+
// Get sender info from message/call webhooks
|
|
259
|
+
const contact = getContactInfo(webhookPayload);
|
|
260
|
+
if (contact) {
|
|
261
|
+
console.log(contact.waId); // e.g., '14155551234'
|
|
262
|
+
console.log(contact.profileName); // e.g., 'John Doe' (may be undefined)
|
|
263
|
+
console.log(contact.phoneNumberId); // Your business phone number ID
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get message ID from message webhooks
|
|
267
|
+
const messageId = getMessageId(webhookPayload);
|
|
268
|
+
if (messageId) {
|
|
269
|
+
await markAsRead(messageId);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Get message ID from status webhooks
|
|
273
|
+
const statusMessageId = getMessageId(statusWebhook);
|
|
274
|
+
if (statusMessageId) {
|
|
275
|
+
console.log('Status update for message:', statusMessageId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Get call ID from call webhooks
|
|
279
|
+
const callId = getCallId(webhookPayload);
|
|
280
|
+
if (callId) {
|
|
281
|
+
await logCall(callId);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
#### Message-level Helpers
|
|
286
|
+
|
|
287
|
+
These helpers operate on individual message objects:
|
|
288
|
+
|
|
289
|
+
| Function | Description | Returns |
|
|
290
|
+
|----------|-------------|---------|
|
|
291
|
+
| `isMediaMessage(message)` | Type guard: returns `true` if message has downloadable media | `boolean` |
|
|
292
|
+
| `extractMediaId(message)` | Extracts media ID from image/audio/video/document/sticker messages | `string \| undefined` |
|
|
293
|
+
| `getMessageTimestamp(message)` | Parses timestamp string to `Date` object | `Date` |
|
|
235
294
|
|
|
236
295
|
```typescript
|
|
237
296
|
import {
|
|
238
297
|
isMediaMessage,
|
|
239
298
|
extractMediaId,
|
|
240
|
-
getContactInfo,
|
|
241
299
|
getMessageTimestamp,
|
|
242
300
|
} from 'waba-toolkit';
|
|
243
301
|
|
|
302
|
+
// Extract message from webhook first
|
|
303
|
+
const message = webhookPayload.entry[0].changes[0].value.messages?.[0];
|
|
304
|
+
if (!message) return;
|
|
305
|
+
|
|
244
306
|
// Check if message has media
|
|
245
307
|
if (isMediaMessage(message)) {
|
|
246
308
|
const mediaId = extractMediaId(message); // guaranteed non-undefined
|
|
247
309
|
const media = await client.getMedia(mediaId);
|
|
248
310
|
}
|
|
249
311
|
|
|
250
|
-
// Get sender info
|
|
251
|
-
const contact = getContactInfo(webhookPayload);
|
|
252
|
-
if (contact) {
|
|
253
|
-
console.log(contact.waId); // e.g., '14155551234'
|
|
254
|
-
console.log(contact.profileName); // e.g., 'John Doe' (may be undefined)
|
|
255
|
-
console.log(contact.phoneNumberId); // Your business phone number ID
|
|
256
|
-
}
|
|
257
|
-
|
|
258
312
|
// Parse timestamp
|
|
259
313
|
const sentAt = getMessageTimestamp(message);
|
|
260
314
|
console.log(sentAt.toISOString());
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
interface WABAClientOptions {
|
|
2
|
+
/** Meta access token with whatsapp_business_messaging permission */
|
|
3
|
+
accessToken: string;
|
|
4
|
+
/** Meta App Secret for webhook signature verification */
|
|
5
|
+
appSecret?: string;
|
|
6
|
+
/** API version (default: 'v22.0') */
|
|
7
|
+
apiVersion?: string;
|
|
8
|
+
/** Base URL (default: 'https://graph.facebook.com') */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
interface GetMediaOptions {
|
|
12
|
+
/** Return ArrayBuffer instead of ReadableStream (default: false) */
|
|
13
|
+
asBuffer?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Media metadata returned from WABA API.
|
|
18
|
+
* Field names are normalized from snake_case to camelCase.
|
|
19
|
+
*/
|
|
20
|
+
interface MediaMetadata {
|
|
21
|
+
id: string;
|
|
22
|
+
/** Normalized from mime_type */
|
|
23
|
+
mimeType: string;
|
|
24
|
+
sha256: string;
|
|
25
|
+
/** Normalized from file_size (string → number) */
|
|
26
|
+
fileSize: number;
|
|
27
|
+
/** Temporary URL (expires in 5 minutes) */
|
|
28
|
+
url: string;
|
|
29
|
+
}
|
|
30
|
+
interface MediaStreamResult extends MediaMetadata {
|
|
31
|
+
stream: ReadableStream<Uint8Array>;
|
|
32
|
+
}
|
|
33
|
+
interface MediaBufferResult extends MediaMetadata {
|
|
34
|
+
buffer: ArrayBuffer;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Client for WhatsApp Business API media operations.
|
|
39
|
+
*/
|
|
40
|
+
declare class WABAClient {
|
|
41
|
+
private readonly accessToken;
|
|
42
|
+
private readonly appSecret?;
|
|
43
|
+
private readonly apiVersion;
|
|
44
|
+
private readonly baseUrl;
|
|
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;
|
|
54
|
+
/**
|
|
55
|
+
* Fetches media by ID from WhatsApp Business API.
|
|
56
|
+
*
|
|
57
|
+
* Step 1: GET /{apiVersion}/{mediaId} → retrieves temporary URL + metadata
|
|
58
|
+
* Step 2: GET {temporaryUrl} → downloads binary content
|
|
59
|
+
*
|
|
60
|
+
* @throws {WABAMediaError} - Media not found (404) or access denied
|
|
61
|
+
* @throws {WABANetworkError} - Network/connection failures
|
|
62
|
+
*/
|
|
63
|
+
getMedia(mediaId: string): Promise<MediaStreamResult>;
|
|
64
|
+
getMedia(mediaId: string, options: {
|
|
65
|
+
asBuffer: true;
|
|
66
|
+
}): Promise<MediaBufferResult>;
|
|
67
|
+
getMedia(mediaId: string, options?: GetMediaOptions): Promise<MediaStreamResult | MediaBufferResult>;
|
|
68
|
+
private fetchMediaMetadata;
|
|
69
|
+
private downloadMedia;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Context object for replies and forwarded messages */
|
|
73
|
+
interface MessageContext {
|
|
74
|
+
/** Original sender (for replies) */
|
|
75
|
+
from?: string;
|
|
76
|
+
/** Original message ID */
|
|
77
|
+
id?: string;
|
|
78
|
+
forwarded?: boolean;
|
|
79
|
+
frequently_forwarded?: boolean;
|
|
80
|
+
/** Present for product enquiry messages */
|
|
81
|
+
referred_product?: {
|
|
82
|
+
catalog_id: string;
|
|
83
|
+
product_retailer_id: string;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** Identity object for security notifications */
|
|
87
|
+
interface MessageIdentity {
|
|
88
|
+
/** State of acknowledgment for latest user_identity_changed system notification */
|
|
89
|
+
acknowledged: boolean;
|
|
90
|
+
/** Timestamp when the WhatsApp Business API detected the user potentially changed */
|
|
91
|
+
created_timestamp: string;
|
|
92
|
+
/** Identifier for the latest user_identity_changed system notification */
|
|
93
|
+
hash: string;
|
|
94
|
+
}
|
|
95
|
+
/** Base fields present on all incoming messages */
|
|
96
|
+
interface IncomingMessageBase {
|
|
97
|
+
/** Sender's phone number */
|
|
98
|
+
from: string;
|
|
99
|
+
/** Message ID (use for mark-as-read) */
|
|
100
|
+
id: string;
|
|
101
|
+
/** Unix epoch seconds as string */
|
|
102
|
+
timestamp: string;
|
|
103
|
+
/** Message type */
|
|
104
|
+
type: string;
|
|
105
|
+
/** Present if reply or forwarded */
|
|
106
|
+
context?: MessageContext;
|
|
107
|
+
/** Present if show_security_notifications is enabled in application settings */
|
|
108
|
+
identity?: MessageIdentity;
|
|
109
|
+
}
|
|
110
|
+
/** Text message */
|
|
111
|
+
interface TextMessage extends IncomingMessageBase {
|
|
112
|
+
type: 'text';
|
|
113
|
+
text: {
|
|
114
|
+
body: string;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Media object base (shared by image, audio, video, document, sticker) */
|
|
118
|
+
interface MediaObject {
|
|
119
|
+
id: string;
|
|
120
|
+
mime_type: string;
|
|
121
|
+
sha256: string;
|
|
122
|
+
caption?: string;
|
|
123
|
+
}
|
|
124
|
+
/** Image message */
|
|
125
|
+
interface ImageMessage extends IncomingMessageBase {
|
|
126
|
+
type: 'image';
|
|
127
|
+
image: MediaObject;
|
|
128
|
+
}
|
|
129
|
+
/** Audio message */
|
|
130
|
+
interface AudioMessage extends IncomingMessageBase {
|
|
131
|
+
type: 'audio';
|
|
132
|
+
audio: MediaObject & {
|
|
133
|
+
/** True if voice note */
|
|
134
|
+
voice?: boolean;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
/** Video message */
|
|
138
|
+
interface VideoMessage extends IncomingMessageBase {
|
|
139
|
+
type: 'video';
|
|
140
|
+
video: MediaObject;
|
|
141
|
+
}
|
|
142
|
+
/** Document message */
|
|
143
|
+
interface DocumentMessage extends IncomingMessageBase {
|
|
144
|
+
type: 'document';
|
|
145
|
+
document: MediaObject & {
|
|
146
|
+
filename?: string;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** Sticker message */
|
|
150
|
+
interface StickerMessage extends IncomingMessageBase {
|
|
151
|
+
type: 'sticker';
|
|
152
|
+
sticker: MediaObject & {
|
|
153
|
+
animated?: boolean;
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Location message */
|
|
157
|
+
interface LocationMessage extends IncomingMessageBase {
|
|
158
|
+
type: 'location';
|
|
159
|
+
location: {
|
|
160
|
+
latitude: number;
|
|
161
|
+
longitude: number;
|
|
162
|
+
name?: string;
|
|
163
|
+
address?: string;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Contact in contacts message */
|
|
167
|
+
interface ContactCard {
|
|
168
|
+
name: {
|
|
169
|
+
formatted_name: string;
|
|
170
|
+
first_name?: string;
|
|
171
|
+
last_name?: string;
|
|
172
|
+
middle_name?: string;
|
|
173
|
+
suffix?: string;
|
|
174
|
+
prefix?: string;
|
|
175
|
+
};
|
|
176
|
+
phones?: Array<{
|
|
177
|
+
phone?: string;
|
|
178
|
+
type?: string;
|
|
179
|
+
wa_id?: string;
|
|
180
|
+
}>;
|
|
181
|
+
emails?: Array<{
|
|
182
|
+
email?: string;
|
|
183
|
+
type?: string;
|
|
184
|
+
}>;
|
|
185
|
+
addresses?: Array<{
|
|
186
|
+
street?: string;
|
|
187
|
+
city?: string;
|
|
188
|
+
state?: string;
|
|
189
|
+
zip?: string;
|
|
190
|
+
country?: string;
|
|
191
|
+
country_code?: string;
|
|
192
|
+
type?: string;
|
|
193
|
+
}>;
|
|
194
|
+
org?: {
|
|
195
|
+
company?: string;
|
|
196
|
+
department?: string;
|
|
197
|
+
title?: string;
|
|
198
|
+
};
|
|
199
|
+
urls?: Array<{
|
|
200
|
+
url?: string;
|
|
201
|
+
type?: string;
|
|
202
|
+
}>;
|
|
203
|
+
birthday?: string;
|
|
204
|
+
}
|
|
205
|
+
/** Contacts message */
|
|
206
|
+
interface ContactsMessage extends IncomingMessageBase {
|
|
207
|
+
type: 'contacts';
|
|
208
|
+
contacts: ContactCard[];
|
|
209
|
+
}
|
|
210
|
+
/** Button reply object */
|
|
211
|
+
interface ButtonReply {
|
|
212
|
+
id: string;
|
|
213
|
+
title: string;
|
|
214
|
+
}
|
|
215
|
+
/** List reply object */
|
|
216
|
+
interface ListReply {
|
|
217
|
+
id: string;
|
|
218
|
+
title: string;
|
|
219
|
+
description?: string;
|
|
220
|
+
}
|
|
221
|
+
/** Flow (NFM) reply object */
|
|
222
|
+
interface NfmReply {
|
|
223
|
+
/** JSON string containing the flow response data */
|
|
224
|
+
response_json: string;
|
|
225
|
+
/** Optional body text */
|
|
226
|
+
body?: string;
|
|
227
|
+
/** Flow name */
|
|
228
|
+
name?: string;
|
|
229
|
+
/** Flow token for tracking/correlation */
|
|
230
|
+
flow_token?: string;
|
|
231
|
+
}
|
|
232
|
+
/** Interactive message (button/list replies, flow responses) */
|
|
233
|
+
interface InteractiveMessage extends IncomingMessageBase {
|
|
234
|
+
type: 'interactive';
|
|
235
|
+
interactive: {
|
|
236
|
+
type: 'button_reply' | 'list_reply' | 'nfm_reply';
|
|
237
|
+
button_reply?: ButtonReply;
|
|
238
|
+
list_reply?: ListReply;
|
|
239
|
+
nfm_reply?: NfmReply;
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/** Reaction message */
|
|
243
|
+
interface ReactionMessage extends IncomingMessageBase {
|
|
244
|
+
type: 'reaction';
|
|
245
|
+
reaction: {
|
|
246
|
+
message_id: string;
|
|
247
|
+
emoji: string;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/** Button message (quick reply button click) */
|
|
251
|
+
interface ButtonMessage extends IncomingMessageBase {
|
|
252
|
+
type: 'button';
|
|
253
|
+
button: {
|
|
254
|
+
text: string;
|
|
255
|
+
payload: string;
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** Order message */
|
|
259
|
+
interface OrderMessage extends IncomingMessageBase {
|
|
260
|
+
type: 'order';
|
|
261
|
+
order: {
|
|
262
|
+
catalog_id: string;
|
|
263
|
+
product_items: Array<{
|
|
264
|
+
product_retailer_id: string;
|
|
265
|
+
quantity: number;
|
|
266
|
+
item_price: number;
|
|
267
|
+
currency: string;
|
|
268
|
+
}>;
|
|
269
|
+
text?: string;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** System message (number change, identity change) */
|
|
273
|
+
interface SystemMessage extends IncomingMessageBase {
|
|
274
|
+
type: 'system';
|
|
275
|
+
system: {
|
|
276
|
+
body: string;
|
|
277
|
+
type: 'user_changed_number' | 'user_identity_changed';
|
|
278
|
+
new_wa_id?: string;
|
|
279
|
+
identity?: string;
|
|
280
|
+
user?: string;
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** Referral message (click-to-WhatsApp ads) */
|
|
284
|
+
interface ReferralMessage extends IncomingMessageBase {
|
|
285
|
+
type: 'referral';
|
|
286
|
+
referral: {
|
|
287
|
+
source_url: string;
|
|
288
|
+
source_type: string;
|
|
289
|
+
source_id: string;
|
|
290
|
+
headline?: string;
|
|
291
|
+
body?: string;
|
|
292
|
+
media_type?: string;
|
|
293
|
+
image_url?: string;
|
|
294
|
+
video_url?: string;
|
|
295
|
+
thumbnail_url?: string;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
/** Unsupported/unknown message type */
|
|
299
|
+
interface UnsupportedMessage extends IncomingMessageBase {
|
|
300
|
+
type: 'unsupported' | 'unknown';
|
|
301
|
+
errors?: Array<{
|
|
302
|
+
code: number;
|
|
303
|
+
title: string;
|
|
304
|
+
details?: string;
|
|
305
|
+
}>;
|
|
306
|
+
}
|
|
307
|
+
/** Union of all incoming message types */
|
|
308
|
+
type IncomingMessage = TextMessage | ImageMessage | AudioMessage | VideoMessage | DocumentMessage | StickerMessage | LocationMessage | ContactsMessage | InteractiveMessage | ReactionMessage | ButtonMessage | OrderMessage | SystemMessage | ReferralMessage | UnsupportedMessage;
|
|
309
|
+
/** Union of media message types */
|
|
310
|
+
type MediaMessage = ImageMessage | AudioMessage | VideoMessage | DocumentMessage | StickerMessage;
|
|
311
|
+
/** Discriminated union for message classification results */
|
|
312
|
+
type MessageClassification = {
|
|
313
|
+
type: 'text';
|
|
314
|
+
message: TextMessage;
|
|
315
|
+
} | {
|
|
316
|
+
type: 'image';
|
|
317
|
+
message: ImageMessage;
|
|
318
|
+
} | {
|
|
319
|
+
type: 'video';
|
|
320
|
+
message: VideoMessage;
|
|
321
|
+
} | {
|
|
322
|
+
type: 'audio';
|
|
323
|
+
message: AudioMessage;
|
|
324
|
+
} | {
|
|
325
|
+
type: 'document';
|
|
326
|
+
message: DocumentMessage;
|
|
327
|
+
} | {
|
|
328
|
+
type: 'sticker';
|
|
329
|
+
message: StickerMessage;
|
|
330
|
+
} | {
|
|
331
|
+
type: 'location';
|
|
332
|
+
message: LocationMessage;
|
|
333
|
+
} | {
|
|
334
|
+
type: 'contacts';
|
|
335
|
+
message: ContactsMessage;
|
|
336
|
+
} | {
|
|
337
|
+
type: 'interactive';
|
|
338
|
+
message: InteractiveMessage;
|
|
339
|
+
} | {
|
|
340
|
+
type: 'reaction';
|
|
341
|
+
message: ReactionMessage;
|
|
342
|
+
} | {
|
|
343
|
+
type: 'button';
|
|
344
|
+
message: ButtonMessage;
|
|
345
|
+
} | {
|
|
346
|
+
type: 'order';
|
|
347
|
+
message: OrderMessage;
|
|
348
|
+
} | {
|
|
349
|
+
type: 'system';
|
|
350
|
+
message: SystemMessage;
|
|
351
|
+
} | {
|
|
352
|
+
type: 'referral';
|
|
353
|
+
message: ReferralMessage;
|
|
354
|
+
} | {
|
|
355
|
+
type: 'unsupported';
|
|
356
|
+
message: UnsupportedMessage;
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
/** Top-level webhook payload from Meta */
|
|
360
|
+
interface WebhookPayload {
|
|
361
|
+
object: 'whatsapp_business_account';
|
|
362
|
+
entry: WebhookEntry[];
|
|
363
|
+
}
|
|
364
|
+
interface WebhookEntry {
|
|
365
|
+
/** WABA ID */
|
|
366
|
+
id: string;
|
|
367
|
+
changes: WebhookChange[];
|
|
368
|
+
}
|
|
369
|
+
interface WebhookChange {
|
|
370
|
+
value: WebhookValue;
|
|
371
|
+
field: string;
|
|
372
|
+
}
|
|
373
|
+
/** Union of possible webhook value types */
|
|
374
|
+
type WebhookValue = MessageWebhookValue | StatusWebhookValue | CallWebhookValue;
|
|
375
|
+
/** Contact info included in webhooks */
|
|
376
|
+
interface WebhookContact {
|
|
377
|
+
profile: {
|
|
378
|
+
/** Sender's profile name (optional per WABA docs) */
|
|
379
|
+
name?: string;
|
|
380
|
+
};
|
|
381
|
+
wa_id: string;
|
|
382
|
+
}
|
|
383
|
+
/** Metadata included in all webhook values */
|
|
384
|
+
interface WebhookMetadata {
|
|
385
|
+
display_phone_number: string;
|
|
386
|
+
phone_number_id: string;
|
|
387
|
+
}
|
|
388
|
+
/** Error object in webhooks */
|
|
389
|
+
interface WebhookError {
|
|
390
|
+
code: number;
|
|
391
|
+
title: string;
|
|
392
|
+
message?: string;
|
|
393
|
+
error_data?: {
|
|
394
|
+
details: string;
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** Webhook value for incoming messages */
|
|
398
|
+
interface MessageWebhookValue {
|
|
399
|
+
messaging_product: 'whatsapp';
|
|
400
|
+
metadata: WebhookMetadata;
|
|
401
|
+
contacts?: WebhookContact[];
|
|
402
|
+
messages?: IncomingMessage[];
|
|
403
|
+
errors?: WebhookError[];
|
|
404
|
+
}
|
|
405
|
+
/** Status update types */
|
|
406
|
+
type MessageStatus = 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';
|
|
407
|
+
/** Conversation origin types */
|
|
408
|
+
type ConversationOriginType = 'business_initiated' | 'user_initiated' | 'referral_conversion';
|
|
409
|
+
/** Conversation object in status webhooks */
|
|
410
|
+
interface ConversationObject {
|
|
411
|
+
id: string;
|
|
412
|
+
origin: {
|
|
413
|
+
type: ConversationOriginType;
|
|
414
|
+
};
|
|
415
|
+
expiration_timestamp?: string;
|
|
416
|
+
}
|
|
417
|
+
/** Pricing model types */
|
|
418
|
+
type PricingModel = 'CBP' | 'NBP';
|
|
419
|
+
/** Pricing category types */
|
|
420
|
+
type PricingCategory = 'business_initiated' | 'user_initiated' | 'referral_conversion' | 'authentication' | 'marketing' | 'utility' | 'service';
|
|
421
|
+
/** Pricing object in status webhooks */
|
|
422
|
+
interface PricingObject {
|
|
423
|
+
billable: boolean;
|
|
424
|
+
pricing_model: PricingModel;
|
|
425
|
+
category: PricingCategory;
|
|
426
|
+
}
|
|
427
|
+
/** Individual status entry */
|
|
428
|
+
interface StatusEntry {
|
|
429
|
+
id: string;
|
|
430
|
+
recipient_id: string;
|
|
431
|
+
status: MessageStatus;
|
|
432
|
+
timestamp: string;
|
|
433
|
+
conversation?: ConversationObject;
|
|
434
|
+
pricing?: PricingObject;
|
|
435
|
+
errors?: WebhookError[];
|
|
436
|
+
}
|
|
437
|
+
/** Webhook value for message status updates */
|
|
438
|
+
interface StatusWebhookValue {
|
|
439
|
+
messaging_product: 'whatsapp';
|
|
440
|
+
metadata: WebhookMetadata;
|
|
441
|
+
statuses: StatusEntry[];
|
|
442
|
+
}
|
|
443
|
+
/** Call event types */
|
|
444
|
+
type CallEvent = 'connect' | 'terminate' | 'status';
|
|
445
|
+
/** Call entry in call webhooks */
|
|
446
|
+
interface CallEntry {
|
|
447
|
+
id: string;
|
|
448
|
+
from: string;
|
|
449
|
+
to: string;
|
|
450
|
+
/** Present on connect webhooks */
|
|
451
|
+
event?: CallEvent;
|
|
452
|
+
direction: 'USER_INITIATED' | 'BUSINESS_INITIATED';
|
|
453
|
+
timestamp: string;
|
|
454
|
+
session?: {
|
|
455
|
+
sdp_type: string;
|
|
456
|
+
sdp: string;
|
|
457
|
+
};
|
|
458
|
+
/** e.g. ['COMPLETED'] or ['FAILED'] on terminate */
|
|
459
|
+
status?: string[];
|
|
460
|
+
/** Present on terminate if connected */
|
|
461
|
+
start_time?: string;
|
|
462
|
+
end_time?: string;
|
|
463
|
+
/** Seconds, present on terminate if connected */
|
|
464
|
+
duration?: number;
|
|
465
|
+
/** Arbitrary tracking string that allows you to attach custom metadata to calls */
|
|
466
|
+
biz_opaque_callback_data?: string;
|
|
467
|
+
errors?: {
|
|
468
|
+
code: number;
|
|
469
|
+
message: string;
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
/** Webhook value for call events */
|
|
473
|
+
interface CallWebhookValue {
|
|
474
|
+
messaging_product: 'whatsapp';
|
|
475
|
+
metadata: WebhookMetadata;
|
|
476
|
+
contacts?: WebhookContact[];
|
|
477
|
+
calls: CallEntry[];
|
|
478
|
+
}
|
|
479
|
+
/** Discriminated union for webhook classification results */
|
|
480
|
+
type WebhookClassification = {
|
|
481
|
+
type: 'message';
|
|
482
|
+
payload: MessageWebhookValue;
|
|
483
|
+
} | {
|
|
484
|
+
type: 'status';
|
|
485
|
+
payload: StatusWebhookValue;
|
|
486
|
+
} | {
|
|
487
|
+
type: 'call';
|
|
488
|
+
payload: CallWebhookValue;
|
|
489
|
+
} | {
|
|
490
|
+
type: 'unknown';
|
|
491
|
+
payload: unknown;
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Classifies a webhook payload into its type.
|
|
496
|
+
* Returns a discriminated union for type-safe handling.
|
|
497
|
+
*/
|
|
498
|
+
declare function classifyWebhook(payload: WebhookPayload): WebhookClassification;
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Classifies an incoming message by its type.
|
|
502
|
+
* Returns a discriminated union for type-safe handling.
|
|
503
|
+
*/
|
|
504
|
+
declare function classifyMessage(message: IncomingMessage): MessageClassification;
|
|
505
|
+
|
|
506
|
+
interface VerifyWebhookSignatureOptions {
|
|
507
|
+
/** X-Hub-Signature-256 header value */
|
|
508
|
+
signature: string | undefined;
|
|
509
|
+
/** Raw request body (NOT parsed JSON) */
|
|
510
|
+
rawBody: Buffer | string;
|
|
511
|
+
/** Meta App Secret */
|
|
512
|
+
appSecret: string | undefined;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* Verifies webhook signature using HMAC-SHA256.
|
|
516
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
517
|
+
*
|
|
518
|
+
* @throws {WABASignatureError} If appSecret is not provided
|
|
519
|
+
* @returns true if signature is valid, false otherwise
|
|
520
|
+
*/
|
|
521
|
+
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): boolean;
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Type guard: returns true if message contains downloadable media.
|
|
525
|
+
* Narrows type to messages with image/audio/video/document/sticker.
|
|
526
|
+
*/
|
|
527
|
+
declare function isMediaMessage(message: IncomingMessage): message is MediaMessage;
|
|
528
|
+
/**
|
|
529
|
+
* Extracts media ID from any media message type.
|
|
530
|
+
* Returns undefined if message has no media.
|
|
531
|
+
*/
|
|
532
|
+
declare function extractMediaId(message: IncomingMessage): string | undefined;
|
|
533
|
+
interface ContactInfo {
|
|
534
|
+
waId: string;
|
|
535
|
+
profileName: string | undefined;
|
|
536
|
+
phoneNumberId: string;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Extracts sender info from webhook payload.
|
|
540
|
+
* Returns null if the webhook doesn't contain message contact info.
|
|
541
|
+
*/
|
|
542
|
+
declare function getContactInfo(webhook: WebhookPayload): ContactInfo | null;
|
|
543
|
+
/**
|
|
544
|
+
* Parses message timestamp to Date object.
|
|
545
|
+
* WhatsApp timestamps are Unix epoch seconds as strings.
|
|
546
|
+
*/
|
|
547
|
+
declare function getMessageTimestamp(message: IncomingMessage): Date;
|
|
548
|
+
/**
|
|
549
|
+
* Extracts message ID from message or status webhook.
|
|
550
|
+
* Returns null if not a message/status webhook or ID not present.
|
|
551
|
+
*/
|
|
552
|
+
declare function getMessageId(webhook: WebhookPayload): string | null;
|
|
553
|
+
/**
|
|
554
|
+
* Extracts call ID from call webhook.
|
|
555
|
+
* Returns null if not a call webhook or ID not present.
|
|
556
|
+
*/
|
|
557
|
+
declare function getCallId(webhook: WebhookPayload): string | null;
|
|
558
|
+
|
|
559
|
+
/** Base error class for all WABA errors */
|
|
560
|
+
declare class WABAError extends Error {
|
|
561
|
+
readonly code?: number | undefined;
|
|
562
|
+
readonly details?: unknown | undefined;
|
|
563
|
+
constructor(message: string, code?: number | undefined, details?: unknown | undefined);
|
|
564
|
+
}
|
|
565
|
+
/** Error for media-related failures (404, access denied) */
|
|
566
|
+
declare class WABAMediaError extends WABAError {
|
|
567
|
+
readonly mediaId: string;
|
|
568
|
+
constructor(message: string, mediaId: string, code?: number);
|
|
569
|
+
}
|
|
570
|
+
/** Error for network/connection failures */
|
|
571
|
+
declare class WABANetworkError extends WABAError {
|
|
572
|
+
readonly cause?: Error;
|
|
573
|
+
constructor(message: string, cause?: Error);
|
|
574
|
+
}
|
|
575
|
+
/** Error for invalid webhook signatures */
|
|
576
|
+
declare class WABASignatureError extends WABAError {
|
|
577
|
+
constructor(message?: string);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export { type AudioMessage, type ButtonMessage, type ButtonReply, type CallEntry, type CallWebhookValue, type ContactCard, type ContactInfo, type ContactsMessage, type ConversationObject, type DocumentMessage, type GetMediaOptions, type ImageMessage, type IncomingMessage, type IncomingMessageBase, type InteractiveMessage, type ListReply, type LocationMessage, type MediaBufferResult, type MediaMessage, type MediaMetadata, type MediaObject, type MediaStreamResult, type MessageClassification, type MessageContext, type MessageStatus, type MessageWebhookValue, type OrderMessage, type PricingObject, type ReactionMessage, type ReferralMessage, type StatusEntry, type StatusWebhookValue, type StickerMessage, type SystemMessage, type TextMessage, type UnsupportedMessage, type VerifyWebhookSignatureOptions, type VideoMessage, WABAClient, type WABAClientOptions, WABAError, WABAMediaError, WABANetworkError, WABASignatureError, type WebhookChange, type WebhookClassification, type WebhookContact, type WebhookEntry, type WebhookError, type WebhookMetadata, type WebhookPayload, type WebhookValue, classifyMessage, classifyWebhook, extractMediaId, getCallId, getContactInfo, getMessageId, getMessageTimestamp, isMediaMessage, verifyWebhookSignature };
|
package/dist/index.d.ts
CHANGED
|
@@ -440,13 +440,15 @@ interface StatusWebhookValue {
|
|
|
440
440
|
metadata: WebhookMetadata;
|
|
441
441
|
statuses: StatusEntry[];
|
|
442
442
|
}
|
|
443
|
+
/** Call event types */
|
|
444
|
+
type CallEvent = 'connect' | 'terminate' | 'status';
|
|
443
445
|
/** Call entry in call webhooks */
|
|
444
446
|
interface CallEntry {
|
|
445
447
|
id: string;
|
|
446
448
|
from: string;
|
|
447
449
|
to: string;
|
|
448
450
|
/** Present on connect webhooks */
|
|
449
|
-
event?:
|
|
451
|
+
event?: CallEvent;
|
|
450
452
|
direction: 'USER_INITIATED' | 'BUSINESS_INITIATED';
|
|
451
453
|
timestamp: string;
|
|
452
454
|
session?: {
|
|
@@ -460,6 +462,8 @@ interface CallEntry {
|
|
|
460
462
|
end_time?: string;
|
|
461
463
|
/** Seconds, present on terminate if connected */
|
|
462
464
|
duration?: number;
|
|
465
|
+
/** Arbitrary tracking string that allows you to attach custom metadata to calls */
|
|
466
|
+
biz_opaque_callback_data?: string;
|
|
463
467
|
errors?: {
|
|
464
468
|
code: number;
|
|
465
469
|
message: string;
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
WABAClient: () => WABAClient,
|
|
24
|
+
WABAError: () => WABAError,
|
|
25
|
+
WABAMediaError: () => WABAMediaError,
|
|
26
|
+
WABANetworkError: () => WABANetworkError,
|
|
27
|
+
WABASignatureError: () => WABASignatureError,
|
|
28
|
+
classifyMessage: () => classifyMessage,
|
|
29
|
+
classifyWebhook: () => classifyWebhook,
|
|
30
|
+
extractMediaId: () => extractMediaId,
|
|
31
|
+
getCallId: () => getCallId,
|
|
32
|
+
getContactInfo: () => getContactInfo,
|
|
33
|
+
getMessageId: () => getMessageId,
|
|
34
|
+
getMessageTimestamp: () => getMessageTimestamp,
|
|
35
|
+
isMediaMessage: () => isMediaMessage,
|
|
36
|
+
verifyWebhookSignature: () => verifyWebhookSignature
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
1
40
|
// src/errors.ts
|
|
2
41
|
var WABAError = class extends Error {
|
|
3
42
|
constructor(message, code, details) {
|
|
@@ -34,7 +73,7 @@ var WABASignatureError = class extends WABAError {
|
|
|
34
73
|
};
|
|
35
74
|
|
|
36
75
|
// src/verify.ts
|
|
37
|
-
|
|
76
|
+
var import_node_crypto = require("crypto");
|
|
38
77
|
function verifyWebhookSignature(options) {
|
|
39
78
|
const { signature, rawBody, appSecret } = options;
|
|
40
79
|
if (!appSecret) {
|
|
@@ -44,7 +83,7 @@ function verifyWebhookSignature(options) {
|
|
|
44
83
|
return false;
|
|
45
84
|
}
|
|
46
85
|
const signatureHash = signature.startsWith("sha256=") ? signature.slice(7) : signature;
|
|
47
|
-
const hmac = createHmac("sha256", appSecret);
|
|
86
|
+
const hmac = (0, import_node_crypto.createHmac)("sha256", appSecret);
|
|
48
87
|
const bodyBuffer = typeof rawBody === "string" ? Buffer.from(rawBody, "utf-8") : rawBody;
|
|
49
88
|
const expectedHash = hmac.update(bodyBuffer).digest("hex");
|
|
50
89
|
const signatureBuffer = Buffer.from(signatureHash, "utf-8");
|
|
@@ -52,7 +91,7 @@ function verifyWebhookSignature(options) {
|
|
|
52
91
|
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
53
92
|
return false;
|
|
54
93
|
}
|
|
55
|
-
return timingSafeEqual(signatureBuffer, expectedBuffer);
|
|
94
|
+
return (0, import_node_crypto.timingSafeEqual)(signatureBuffer, expectedBuffer);
|
|
56
95
|
}
|
|
57
96
|
|
|
58
97
|
// src/client.ts
|
|
@@ -293,7 +332,8 @@ function getCallId(webhook) {
|
|
|
293
332
|
}
|
|
294
333
|
return null;
|
|
295
334
|
}
|
|
296
|
-
export
|
|
335
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
336
|
+
0 && (module.exports = {
|
|
297
337
|
WABAClient,
|
|
298
338
|
WABAError,
|
|
299
339
|
WABAMediaError,
|
|
@@ -308,5 +348,5 @@ export {
|
|
|
308
348
|
getMessageTimestamp,
|
|
309
349
|
isMediaMessage,
|
|
310
350
|
verifyWebhookSignature
|
|
311
|
-
};
|
|
351
|
+
});
|
|
312
352
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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 {\n WebhookPayload,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} 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 null if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | null {\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 null;\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\n/**\n * Extracts message ID from message or status webhook.\n * Returns null if not a message/status webhook or ID not present.\n */\nexport function getMessageId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Try message webhook\n if ('messages' in value) {\n const messageValue = value as MessageWebhookValue;\n return messageValue.messages?.[0]?.id ?? null;\n }\n\n // Try status webhook\n if ('statuses' in value) {\n const statusValue = value as StatusWebhookValue;\n return statusValue.statuses?.[0]?.id ?? null;\n }\n\n return null;\n}\n\n/**\n * Extracts call ID from call webhook.\n * Returns null if not a call webhook or ID not present.\n */\nexport function getCallId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Check if it's a call webhook\n if ('calls' in value) {\n const callValue = value as CallWebhookValue;\n return callValue.calls?.[0]?.id ?? null;\n }\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/verify.ts","../src/client.ts","../src/webhooks/classify.ts","../src/webhooks/messages.ts","../src/helpers.ts"],"sourcesContent":["// Main client\nexport { WABAClient } from './client.js';\n\n// Webhook classification\nexport { classifyWebhook, classifyMessage } from './webhooks/index.js';\n\n// Signature verification\nexport { verifyWebhookSignature } from './verify.js';\nexport type { VerifyWebhookSignatureOptions } from './verify.js';\n\n// Utility helpers\nexport {\n isMediaMessage,\n extractMediaId,\n getContactInfo,\n getMessageTimestamp,\n getMessageId,\n getCallId,\n} from './helpers.js';\nexport type { ContactInfo } from './helpers.js';\n\n// Error classes\nexport {\n WABAError,\n WABAMediaError,\n WABANetworkError,\n WABASignatureError,\n} from './errors.js';\n\n// Types\nexport type {\n // Client types\n WABAClientOptions,\n GetMediaOptions,\n // Media types\n MediaMetadata,\n MediaStreamResult,\n MediaBufferResult,\n // Webhook types\n WebhookPayload,\n WebhookEntry,\n WebhookChange,\n WebhookValue,\n WebhookContact,\n WebhookMetadata,\n WebhookError,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n MessageStatus,\n ConversationObject,\n PricingObject,\n StatusEntry,\n CallEntry,\n WebhookClassification,\n // Message types\n MessageContext,\n IncomingMessageBase,\n TextMessage,\n MediaObject,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n LocationMessage,\n ContactCard,\n ContactsMessage,\n ButtonReply,\n ListReply,\n InteractiveMessage,\n ReactionMessage,\n ButtonMessage,\n OrderMessage,\n SystemMessage,\n ReferralMessage,\n UnsupportedMessage,\n IncomingMessage,\n MediaMessage,\n MessageClassification,\n} from './types/index.js';\n","/** 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 {\n WebhookPayload,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} 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 null if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | null {\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 null;\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\n/**\n * Extracts message ID from message or status webhook.\n * Returns null if not a message/status webhook or ID not present.\n */\nexport function getMessageId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Try message webhook\n if ('messages' in value) {\n const messageValue = value as MessageWebhookValue;\n return messageValue.messages?.[0]?.id ?? null;\n }\n\n // Try status webhook\n if ('statuses' in value) {\n const statusValue = value as StatusWebhookValue;\n return statusValue.statuses?.[0]?.id ?? null;\n }\n\n return null;\n}\n\n/**\n * Extracts call ID from call webhook.\n * Returns null if not a call webhook or ID not present.\n */\nexport function getCallId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Check if it's a call webhook\n if ('calls' in value) {\n const callValue = value as CallWebhookValue;\n return callValue.calls?.[0]?.id ?? null;\n }\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var WABAError = class extends Error {
|
|
3
|
+
constructor(message, code, details) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.details = details;
|
|
7
|
+
this.name = "WABAError";
|
|
8
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
var WABAMediaError = class extends WABAError {
|
|
12
|
+
constructor(message, mediaId, code) {
|
|
13
|
+
super(message, code);
|
|
14
|
+
this.mediaId = mediaId;
|
|
15
|
+
this.name = "WABAMediaError";
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var WABANetworkError = class extends WABAError {
|
|
20
|
+
cause;
|
|
21
|
+
constructor(message, cause) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "WABANetworkError";
|
|
24
|
+
this.cause = cause;
|
|
25
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var WABASignatureError = class extends WABAError {
|
|
29
|
+
constructor(message = "Invalid webhook signature") {
|
|
30
|
+
super(message);
|
|
31
|
+
this.name = "WABASignatureError";
|
|
32
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
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
|
+
|
|
58
|
+
// src/client.ts
|
|
59
|
+
var DEFAULT_API_VERSION = "v22.0";
|
|
60
|
+
var DEFAULT_BASE_URL = "https://graph.facebook.com";
|
|
61
|
+
var WABAClient = class {
|
|
62
|
+
accessToken;
|
|
63
|
+
appSecret;
|
|
64
|
+
apiVersion;
|
|
65
|
+
baseUrl;
|
|
66
|
+
constructor(options) {
|
|
67
|
+
this.accessToken = options.accessToken;
|
|
68
|
+
this.appSecret = options.appSecret;
|
|
69
|
+
this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
|
|
70
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
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
|
+
}
|
|
87
|
+
async getMedia(mediaId, options) {
|
|
88
|
+
const metadata = await this.fetchMediaMetadata(mediaId);
|
|
89
|
+
const response = await this.downloadMedia(metadata.url, mediaId);
|
|
90
|
+
if (options?.asBuffer) {
|
|
91
|
+
const buffer = await response.arrayBuffer();
|
|
92
|
+
return {
|
|
93
|
+
...metadata,
|
|
94
|
+
buffer
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!response.body) {
|
|
98
|
+
throw new WABAMediaError("Response body is null", mediaId);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
...metadata,
|
|
102
|
+
stream: response.body
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async fetchMediaMetadata(mediaId) {
|
|
106
|
+
const url = `${this.baseUrl}/${this.apiVersion}/${mediaId}`;
|
|
107
|
+
let response;
|
|
108
|
+
try {
|
|
109
|
+
response = await fetch(url, {
|
|
110
|
+
method: "GET",
|
|
111
|
+
headers: {
|
|
112
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
throw new WABANetworkError(
|
|
117
|
+
`Failed to fetch media metadata: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
118
|
+
error instanceof Error ? error : void 0
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
123
|
+
throw new WABAMediaError(
|
|
124
|
+
`Failed to fetch media metadata: ${response.status} ${errorBody}`,
|
|
125
|
+
mediaId,
|
|
126
|
+
response.status
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
return {
|
|
131
|
+
id: data.id,
|
|
132
|
+
mimeType: data.mime_type,
|
|
133
|
+
sha256: data.sha256,
|
|
134
|
+
fileSize: parseInt(data.file_size, 10),
|
|
135
|
+
url: data.url
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
async downloadMedia(url, mediaId) {
|
|
139
|
+
let response;
|
|
140
|
+
try {
|
|
141
|
+
response = await fetch(url, {
|
|
142
|
+
method: "GET",
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
} catch (error) {
|
|
148
|
+
throw new WABANetworkError(
|
|
149
|
+
`Failed to download media: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
150
|
+
error instanceof Error ? error : void 0
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (!response.ok) {
|
|
154
|
+
throw new WABAMediaError(
|
|
155
|
+
`Failed to download media: ${response.status}`,
|
|
156
|
+
mediaId,
|
|
157
|
+
response.status
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return response;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/webhooks/classify.ts
|
|
165
|
+
function classifyWebhook(payload) {
|
|
166
|
+
const entry = payload.entry?.[0];
|
|
167
|
+
const change = entry?.changes?.[0];
|
|
168
|
+
const value = change?.value;
|
|
169
|
+
if (!value) {
|
|
170
|
+
return { type: "unknown", payload };
|
|
171
|
+
}
|
|
172
|
+
if ("calls" in value && Array.isArray(value.calls)) {
|
|
173
|
+
return { type: "call", payload: value };
|
|
174
|
+
}
|
|
175
|
+
if ("statuses" in value && Array.isArray(value.statuses)) {
|
|
176
|
+
return { type: "status", payload: value };
|
|
177
|
+
}
|
|
178
|
+
if ("messages" in value && Array.isArray(value.messages)) {
|
|
179
|
+
return { type: "message", payload: value };
|
|
180
|
+
}
|
|
181
|
+
if ("errors" in value && Array.isArray(value.errors)) {
|
|
182
|
+
return { type: "message", payload: value };
|
|
183
|
+
}
|
|
184
|
+
return { type: "unknown", payload };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// src/webhooks/messages.ts
|
|
188
|
+
function classifyMessage(message) {
|
|
189
|
+
switch (message.type) {
|
|
190
|
+
case "text":
|
|
191
|
+
return { type: "text", message };
|
|
192
|
+
case "image":
|
|
193
|
+
return { type: "image", message };
|
|
194
|
+
case "audio":
|
|
195
|
+
return { type: "audio", message };
|
|
196
|
+
case "video":
|
|
197
|
+
return { type: "video", message };
|
|
198
|
+
case "document":
|
|
199
|
+
return { type: "document", message };
|
|
200
|
+
case "sticker":
|
|
201
|
+
return { type: "sticker", message };
|
|
202
|
+
case "location":
|
|
203
|
+
return { type: "location", message };
|
|
204
|
+
case "contacts":
|
|
205
|
+
return { type: "contacts", message };
|
|
206
|
+
case "interactive":
|
|
207
|
+
return { type: "interactive", message };
|
|
208
|
+
case "reaction":
|
|
209
|
+
return { type: "reaction", message };
|
|
210
|
+
case "button":
|
|
211
|
+
return { type: "button", message };
|
|
212
|
+
case "order":
|
|
213
|
+
return { type: "order", message };
|
|
214
|
+
case "system":
|
|
215
|
+
return { type: "system", message };
|
|
216
|
+
case "referral":
|
|
217
|
+
return { type: "referral", message };
|
|
218
|
+
default:
|
|
219
|
+
return { type: "unsupported", message };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/helpers.ts
|
|
224
|
+
var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
|
|
225
|
+
function isMediaMessage(message) {
|
|
226
|
+
return MEDIA_TYPES.has(message.type);
|
|
227
|
+
}
|
|
228
|
+
function extractMediaId(message) {
|
|
229
|
+
if (!isMediaMessage(message)) {
|
|
230
|
+
return void 0;
|
|
231
|
+
}
|
|
232
|
+
switch (message.type) {
|
|
233
|
+
case "image":
|
|
234
|
+
return message.image.id;
|
|
235
|
+
case "audio":
|
|
236
|
+
return message.audio.id;
|
|
237
|
+
case "video":
|
|
238
|
+
return message.video.id;
|
|
239
|
+
case "document":
|
|
240
|
+
return message.document.id;
|
|
241
|
+
case "sticker":
|
|
242
|
+
return message.sticker.id;
|
|
243
|
+
default:
|
|
244
|
+
return void 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function getContactInfo(webhook) {
|
|
248
|
+
const entry = webhook.entry?.[0];
|
|
249
|
+
const change = entry?.changes?.[0];
|
|
250
|
+
const value = change?.value;
|
|
251
|
+
if (!value || !("contacts" in value) || !value.contacts?.length) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const contact = value.contacts[0];
|
|
255
|
+
const metadata = value.metadata;
|
|
256
|
+
return {
|
|
257
|
+
waId: contact.wa_id,
|
|
258
|
+
profileName: contact.profile?.name,
|
|
259
|
+
phoneNumberId: metadata.phone_number_id
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function getMessageTimestamp(message) {
|
|
263
|
+
const epochSeconds = parseInt(message.timestamp, 10);
|
|
264
|
+
return new Date(epochSeconds * 1e3);
|
|
265
|
+
}
|
|
266
|
+
function getMessageId(webhook) {
|
|
267
|
+
const entry = webhook.entry?.[0];
|
|
268
|
+
const change = entry?.changes?.[0];
|
|
269
|
+
const value = change?.value;
|
|
270
|
+
if (!value) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
if ("messages" in value) {
|
|
274
|
+
const messageValue = value;
|
|
275
|
+
return messageValue.messages?.[0]?.id ?? null;
|
|
276
|
+
}
|
|
277
|
+
if ("statuses" in value) {
|
|
278
|
+
const statusValue = value;
|
|
279
|
+
return statusValue.statuses?.[0]?.id ?? null;
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
function getCallId(webhook) {
|
|
284
|
+
const entry = webhook.entry?.[0];
|
|
285
|
+
const change = entry?.changes?.[0];
|
|
286
|
+
const value = change?.value;
|
|
287
|
+
if (!value) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
if ("calls" in value) {
|
|
291
|
+
const callValue = value;
|
|
292
|
+
return callValue.calls?.[0]?.id ?? null;
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
export {
|
|
297
|
+
WABAClient,
|
|
298
|
+
WABAError,
|
|
299
|
+
WABAMediaError,
|
|
300
|
+
WABANetworkError,
|
|
301
|
+
WABASignatureError,
|
|
302
|
+
classifyMessage,
|
|
303
|
+
classifyWebhook,
|
|
304
|
+
extractMediaId,
|
|
305
|
+
getCallId,
|
|
306
|
+
getContactInfo,
|
|
307
|
+
getMessageId,
|
|
308
|
+
getMessageTimestamp,
|
|
309
|
+
isMediaMessage,
|
|
310
|
+
verifyWebhookSignature
|
|
311
|
+
};
|
|
312
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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 {\n WebhookPayload,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} 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 null if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | null {\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 null;\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\n/**\n * Extracts message ID from message or status webhook.\n * Returns null if not a message/status webhook or ID not present.\n */\nexport function getMessageId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Try message webhook\n if ('messages' in value) {\n const messageValue = value as MessageWebhookValue;\n return messageValue.messages?.[0]?.id ?? null;\n }\n\n // Try status webhook\n if ('statuses' in value) {\n const statusValue = value as StatusWebhookValue;\n return statusValue.statuses?.[0]?.id ?? null;\n }\n\n return null;\n}\n\n/**\n * Extracts call ID from call webhook.\n * Returns null if not a call webhook or ID not present.\n */\nexport function getCallId(webhook: WebhookPayload): string | null {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return null;\n }\n\n // Check if it's a call webhook\n if ('calls' in value) {\n const callValue = value as CallWebhookValue;\n return callValue.calls?.[0]?.id ?? null;\n }\n\n return null;\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waba-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Type-safe, zero-dependency WhatsApp Business API toolkit for webhooks, media downloads, and signature verification",
|
|
5
|
-
"
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
6
8
|
"engines": {
|
|
7
9
|
"node": ">=20.0.0"
|
|
8
10
|
},
|
|
9
11
|
"exports": {
|
|
10
12
|
".": {
|
|
11
13
|
"types": "./dist/index.d.ts",
|
|
12
|
-
"import": "./dist/index.
|
|
14
|
+
"import": "./dist/index.mjs",
|
|
15
|
+
"require": "./dist/index.js"
|
|
13
16
|
}
|
|
14
17
|
},
|
|
15
18
|
"files": [
|