waba-toolkit 0.1.3 → 0.3.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 CHANGED
@@ -1,9 +1,20 @@
1
1
  # waba-toolkit
2
2
 
3
- A minimal, type-safe toolkit for WhatsApp Business API webhook processing and media handling.
3
+ Type-safe WhatsApp Business API toolkit with CLI for webhooks, media downloads, signature verification, and message sending.
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
 
7
+ ## Features
8
+
9
+ - **CLI Tool** - Send messages, manage phone numbers, and configure settings from the command line
10
+ - **Type-Safe** - Full TypeScript support with discriminated unions for webhook and message types
11
+ - **Media Downloads** - Stream or buffer media with automatic URL refresh handling
12
+ - **Webhook Processing** - Classify and verify webhooks with HMAC-SHA256 signature verification
13
+ - **Message Sending** - Send text, template, and custom messages programmatically
14
+ - **Phone Management** - Register, deregister, and list phone numbers
15
+ - **Zero Dependencies** - Core library has no runtime dependencies (CLI uses Commander + Inquirer)
16
+ - **Dual Format** - ESM and CommonJS support
17
+
7
18
  ---
8
19
 
9
20
  ## Table of Contents
@@ -11,8 +22,15 @@ A minimal, type-safe toolkit for WhatsApp Business API webhook processing and me
11
22
  - [Installation](#installation)
12
23
  - [Requirements](#requirements)
13
24
  - [Quick Start](#quick-start)
25
+ - [CLI Usage](#cli-usage)
26
+ - [Library Usage](#library-usage)
27
+ - [CLI Reference](#cli-reference)
28
+ - [Configuration](#configuration)
29
+ - [Phone Management](#phone-management)
30
+ - [Sending Messages](#sending-messages)
14
31
  - [API Reference](#api-reference)
15
32
  - [WABAClient](#wabaclient)
33
+ - [WABAApiClient](#wabaapiclient)
16
34
  - [Webhook Functions](#webhook-functions)
17
35
  - [Helper Functions](#helper-functions)
18
36
  - [Error Classes](#error-classes)
@@ -38,11 +56,35 @@ npm install waba-toolkit
38
56
 
39
57
  ## Quick Start
40
58
 
59
+ ### CLI Usage
60
+
61
+ ```bash
62
+ # Configure once (interactive)
63
+ waba-toolkit configure
64
+
65
+ # List phone numbers
66
+ waba-toolkit list-phones --waba-id YOUR_WABA_ID
67
+
68
+ # Send a text message
69
+ waba-toolkit send text --to 1234567890 --message "Hello World"
70
+
71
+ # Register a phone number
72
+ waba-toolkit register --pin 123456
73
+
74
+ # View all commands
75
+ waba-toolkit --help
76
+ ```
77
+
78
+ ### Library Usage
79
+
41
80
  ```typescript
42
81
  import {
43
82
  WABAClient,
83
+ WABAApiClient,
44
84
  classifyWebhook,
45
85
  classifyMessage,
86
+ getContactInfo,
87
+ getMessageId,
46
88
  isMediaMessage,
47
89
  extractMediaId,
48
90
  } from 'waba-toolkit';
@@ -69,22 +111,36 @@ app.post('/webhook', async (req, res) => {
69
111
  const webhook = classifyWebhook(req.body);
70
112
 
71
113
  if (webhook.type === 'message') {
114
+ // 3. Extract contact info
115
+ const contact = getContactInfo(req.body);
116
+ if (contact) {
117
+ console.log('From:', contact.waId, contact.profileName);
118
+ }
119
+
120
+ // 4. Get message ID for marking as read
121
+ const messageId = getMessageId(req.body);
122
+
72
123
  const message = webhook.payload.messages?.[0];
73
124
  if (!message) return res.sendStatus(200);
74
125
 
75
- // 3. Classify message type
126
+ // 5. Classify message type
76
127
  const classified = classifyMessage(message);
77
128
 
78
129
  if (classified.type === 'text') {
79
130
  console.log('Text:', classified.message.text.body);
80
131
  }
81
132
 
82
- // 4. Handle media messages
133
+ // 6. Handle media messages
83
134
  if (isMediaMessage(message)) {
84
135
  const mediaId = extractMediaId(message);
85
136
  const { stream, mimeType } = await client.getMedia(mediaId);
86
137
  // Process stream...
87
138
  }
139
+
140
+ // 7. Mark as read
141
+ if (messageId) {
142
+ await markAsRead(messageId);
143
+ }
88
144
  }
89
145
 
90
146
  res.sendStatus(200);
@@ -93,6 +149,211 @@ app.post('/webhook', async (req, res) => {
93
149
 
94
150
  ---
95
151
 
152
+ ## CLI Reference
153
+
154
+ The CLI provides a convenient way to interact with the WhatsApp Business API without writing code.
155
+
156
+ ### Configuration
157
+
158
+ Config is stored at `~/.waba-toolkit` (encrypted, machine-locked).
159
+
160
+ **Priority:** CLI flags > environment variables > config file
161
+
162
+ #### Interactive Setup
163
+
164
+ ```bash
165
+ waba-toolkit configure
166
+ ```
167
+
168
+ Prompts for:
169
+ - Access token (required)
170
+ - Default phone number ID (optional)
171
+ - API version (optional, default: v22.0)
172
+ - WABA ID (optional)
173
+ - Business ID (optional)
174
+
175
+ #### View Configuration
176
+
177
+ ```bash
178
+ waba-toolkit config show
179
+ ```
180
+
181
+ Shows current configuration with sensitive values masked (e.g., `EAA...****...abc`).
182
+
183
+ #### Update Configuration
184
+
185
+ ```bash
186
+ # Set default phone number ID
187
+ waba-toolkit config set-default-phone 1234567890
188
+
189
+ # Update specific fields
190
+ waba-toolkit config set access-token EAABsbCS...
191
+ waba-toolkit config set waba-id 1234567890
192
+ waba-toolkit config set api-version v22.0
193
+ ```
194
+
195
+ Valid fields: `access-token`, `default-phone-number-id`, `api-version`, `waba-id`, `business-id`
196
+
197
+ #### Environment Variables
198
+
199
+ ```bash
200
+ export WABA_TOOLKIT_ACCESS_TOKEN="your-token"
201
+ export WABA_TOOLKIT_PHONE_NUMBER_ID="your-phone-number-id"
202
+ export WABA_TOOLKIT_WABA_ID="your-waba-id"
203
+ export WABA_TOOLKIT_API_VERSION="v22.0"
204
+ ```
205
+
206
+ ### Phone Management
207
+
208
+ #### List Phone Numbers
209
+
210
+ ```bash
211
+ # List all phone numbers for a WABA
212
+ waba-toolkit list-phones --waba-id 1234567890
213
+
214
+ # Using environment variable
215
+ WABA_TOOLKIT_WABA_ID=1234567890 waba-toolkit list-phones
216
+ ```
217
+
218
+ Returns JSON with phone numbers, quality ratings, and verified names:
219
+
220
+ ```json
221
+ {
222
+ "data": [
223
+ {
224
+ "verified_name": "Your Business",
225
+ "display_phone_number": "+1 555-0100",
226
+ "id": "1234567890",
227
+ "quality_rating": "GREEN"
228
+ }
229
+ ]
230
+ }
231
+ ```
232
+
233
+ #### Register Phone Number
234
+
235
+ ```bash
236
+ # Register with default phone number ID from config
237
+ waba-toolkit register --pin 123456
238
+
239
+ # Register specific phone number
240
+ waba-toolkit register --bpid 1234567890 --pin 123456
241
+ ```
242
+
243
+ #### Deregister Phone Number
244
+
245
+ ```bash
246
+ # Deregister default phone number
247
+ waba-toolkit deregister
248
+
249
+ # Deregister specific phone number
250
+ waba-toolkit deregister --bpid 1234567890
251
+ ```
252
+
253
+ ### Sending Messages
254
+
255
+ #### Text Message
256
+
257
+ ```bash
258
+ # Basic text message
259
+ waba-toolkit send text --to 1234567890 --message "Hello World"
260
+
261
+ # With URL preview
262
+ waba-toolkit send text --to 1234567890 --message "Check out https://example.com" --preview-url
263
+
264
+ # Reply to message
265
+ waba-toolkit send text --to 1234567890 --message "Got it!" --reply-to wamid.abc123
266
+ ```
267
+
268
+ #### Template Message
269
+
270
+ ```bash
271
+ waba-toolkit send template --to 1234567890 --file template.json
272
+ ```
273
+
274
+ Template JSON format:
275
+
276
+ ```json
277
+ {
278
+ "name": "hello_world",
279
+ "language": {
280
+ "code": "en_US"
281
+ },
282
+ "components": [
283
+ {
284
+ "type": "body",
285
+ "parameters": [
286
+ { "type": "text", "text": "John" }
287
+ ]
288
+ }
289
+ ]
290
+ }
291
+ ```
292
+
293
+ #### Custom Message (from JSON)
294
+
295
+ ```bash
296
+ waba-toolkit send file --payload message.json
297
+ ```
298
+
299
+ Example payloads:
300
+
301
+ **Text:**
302
+ ```json
303
+ {
304
+ "messaging_product": "whatsapp",
305
+ "to": "1234567890",
306
+ "type": "text",
307
+ "text": {
308
+ "body": "Hello World"
309
+ }
310
+ }
311
+ ```
312
+
313
+ **Image:**
314
+ ```json
315
+ {
316
+ "messaging_product": "whatsapp",
317
+ "to": "1234567890",
318
+ "type": "image",
319
+ "image": {
320
+ "link": "https://example.com/image.jpg"
321
+ }
322
+ }
323
+ ```
324
+
325
+ **Interactive (List):**
326
+ ```json
327
+ {
328
+ "messaging_product": "whatsapp",
329
+ "to": "1234567890",
330
+ "type": "interactive",
331
+ "interactive": {
332
+ "type": "list",
333
+ "body": {
334
+ "text": "Choose an option"
335
+ },
336
+ "action": {
337
+ "button": "View Menu",
338
+ "sections": [
339
+ {
340
+ "title": "Options",
341
+ "rows": [
342
+ {
343
+ "id": "1",
344
+ "title": "Option 1",
345
+ "description": "First option"
346
+ }
347
+ ]
348
+ }
349
+ ]
350
+ }
351
+ }
352
+ }
353
+ ```
354
+
355
+ ---
356
+
96
357
  ## API Reference
97
358
 
98
359
  ### WABAClient
@@ -136,6 +397,89 @@ const { buffer, mimeType, sha256, fileSize, url } = await client.getMedia(mediaI
136
397
 
137
398
  ---
138
399
 
400
+ ### WABAApiClient
401
+
402
+ Client for outbound WhatsApp Business API operations (sending messages, phone registration).
403
+
404
+ #### Constructor
405
+
406
+ ```typescript
407
+ const apiClient = new WABAApiClient({
408
+ accessToken: string, // Required: Meta access token
409
+ phoneNumberId: string, // Required: Your business phone number ID
410
+ apiVersion?: string, // Optional: API version (default: 'v22.0')
411
+ baseUrl?: string, // Optional: Base URL (default: 'https://graph.facebook.com')
412
+ });
413
+ ```
414
+
415
+ #### Methods
416
+
417
+ | Method | Description |
418
+ |--------|-------------|
419
+ | `registerPhone(pin)` | Register phone number with 6-digit PIN |
420
+ | `deregisterPhone()` | Deregister phone number |
421
+ | `listPhoneNumbers(wabaId)` | List all phone numbers for a WABA |
422
+ | `sendTextMessage(to, text, options)` | Send text message |
423
+ | `sendTemplateMessage(to, template)` | Send approved template message |
424
+ | `sendMessage(payload)` | Send any message type with custom payload |
425
+
426
+ #### Examples
427
+
428
+ ```typescript
429
+ import { WABAApiClient } from 'waba-toolkit';
430
+
431
+ const apiClient = new WABAApiClient({
432
+ accessToken: process.env.META_ACCESS_TOKEN,
433
+ phoneNumberId: '1234567890',
434
+ });
435
+
436
+ // List phone numbers
437
+ const phones = await apiClient.listPhoneNumbers('YOUR_WABA_ID');
438
+ console.log(phones.data);
439
+
440
+ // Register phone
441
+ await apiClient.registerPhone('123456');
442
+
443
+ // Send text message
444
+ const response = await apiClient.sendTextMessage(
445
+ '1234567890',
446
+ 'Hello from waba-toolkit!',
447
+ { previewUrl: true }
448
+ );
449
+ console.log('Message ID:', response.messages[0].id);
450
+
451
+ // Reply to a message
452
+ await apiClient.sendTextMessage(
453
+ '1234567890',
454
+ 'This is a reply',
455
+ { context: { message_id: 'wamid.abc123' } }
456
+ );
457
+
458
+ // Send template message
459
+ await apiClient.sendTemplateMessage('1234567890', {
460
+ name: 'hello_world',
461
+ language: { code: 'en_US' },
462
+ components: [
463
+ {
464
+ type: 'body',
465
+ parameters: [{ type: 'text', text: 'John' }],
466
+ },
467
+ ],
468
+ });
469
+
470
+ // Send custom message (image)
471
+ await apiClient.sendMessage({
472
+ messaging_product: 'whatsapp',
473
+ to: '1234567890',
474
+ type: 'image',
475
+ image: {
476
+ link: 'https://example.com/image.jpg',
477
+ },
478
+ });
479
+ ```
480
+
481
+ ---
482
+
139
483
  ### Webhook Functions
140
484
 
141
485
  | Function | Description |
@@ -226,35 +570,73 @@ switch (result.type) {
226
570
 
227
571
  ### Helper Functions
228
572
 
229
- | Function | Description |
230
- |----------|-------------|
231
- | `isMediaMessage(message)` | Type guard: returns `true` if message has downloadable media |
232
- | `extractMediaId(message)` | Extracts media ID from image/audio/video/document/sticker messages |
233
- | `getContactInfo(webhook)` | Extracts sender's `waId`, `profileName`, and `phoneNumberId` |
234
- | `getMessageTimestamp(message)` | Parses timestamp string to `Date` object |
573
+ #### Webhook-level Helpers
574
+
575
+ These helpers accept `WebhookPayload` and extract data from the top-level webhook structure:
576
+
577
+ | Function | Description | Returns |
578
+ |----------|-------------|---------|
579
+ | `getContactInfo(webhook)` | Extracts sender's `waId`, `profileName`, and `phoneNumberId` | `ContactInfo \| null` |
580
+ | `getMessageId(webhook)` | Extracts message ID from message or status webhooks | `string \| null` |
581
+ | `getCallId(webhook)` | Extracts call ID from call webhooks | `string \| null` |
582
+
583
+ ```typescript
584
+ import { getContactInfo, getMessageId, getCallId } from 'waba-toolkit';
585
+
586
+ // Get sender info from message/call webhooks
587
+ const contact = getContactInfo(webhookPayload);
588
+ if (contact) {
589
+ console.log(contact.waId); // e.g., '14155551234'
590
+ console.log(contact.profileName); // e.g., 'John Doe' (may be undefined)
591
+ console.log(contact.phoneNumberId); // Your business phone number ID
592
+ }
593
+
594
+ // Get message ID from message webhooks
595
+ const messageId = getMessageId(webhookPayload);
596
+ if (messageId) {
597
+ await markAsRead(messageId);
598
+ }
599
+
600
+ // Get message ID from status webhooks
601
+ const statusMessageId = getMessageId(statusWebhook);
602
+ if (statusMessageId) {
603
+ console.log('Status update for message:', statusMessageId);
604
+ }
605
+
606
+ // Get call ID from call webhooks
607
+ const callId = getCallId(webhookPayload);
608
+ if (callId) {
609
+ await logCall(callId);
610
+ }
611
+ ```
612
+
613
+ #### Message-level Helpers
614
+
615
+ These helpers operate on individual message objects:
616
+
617
+ | Function | Description | Returns |
618
+ |----------|-------------|---------|
619
+ | `isMediaMessage(message)` | Type guard: returns `true` if message has downloadable media | `boolean` |
620
+ | `extractMediaId(message)` | Extracts media ID from image/audio/video/document/sticker messages | `string \| undefined` |
621
+ | `getMessageTimestamp(message)` | Parses timestamp string to `Date` object | `Date` |
235
622
 
236
623
  ```typescript
237
624
  import {
238
625
  isMediaMessage,
239
626
  extractMediaId,
240
- getContactInfo,
241
627
  getMessageTimestamp,
242
628
  } from 'waba-toolkit';
243
629
 
630
+ // Extract message from webhook first
631
+ const message = webhookPayload.entry[0].changes[0].value.messages?.[0];
632
+ if (!message) return;
633
+
244
634
  // Check if message has media
245
635
  if (isMediaMessage(message)) {
246
636
  const mediaId = extractMediaId(message); // guaranteed non-undefined
247
637
  const media = await client.getMedia(mediaId);
248
638
  }
249
639
 
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
640
  // Parse timestamp
259
641
  const sentAt = getMessageTimestamp(message);
260
642
  console.log(sentAt.toISOString());
@@ -270,10 +652,19 @@ console.log(sentAt.toISOString());
270
652
  | `WABAMediaError` | Media download failures (includes `mediaId` and `code`) |
271
653
  | `WABANetworkError` | Network/connection failures (includes `cause`) |
272
654
  | `WABASignatureError` | Invalid webhook signature |
655
+ | `WABAConfigError` | Configuration issues (includes `field`) |
656
+ | `WABAAuthError` | Authentication failures (includes `statusCode`) |
657
+ | `WABASendError` | Message sending failures (includes `statusCode` and `errorPayload`) |
273
658
 
274
659
  ```typescript
275
- import { WABAMediaError, WABANetworkError } from 'waba-toolkit';
660
+ import {
661
+ WABAMediaError,
662
+ WABANetworkError,
663
+ WABASendError,
664
+ WABAAuthError,
665
+ } from 'waba-toolkit';
276
666
 
667
+ // Media download errors
277
668
  try {
278
669
  const media = await client.getMedia(mediaId);
279
670
  } catch (error) {
@@ -283,6 +674,18 @@ try {
283
674
  console.error('Network error:', error.cause);
284
675
  }
285
676
  }
677
+
678
+ // Message sending errors
679
+ try {
680
+ await apiClient.sendTextMessage('1234567890', 'Hello');
681
+ } catch (error) {
682
+ if (error instanceof WABAAuthError) {
683
+ console.error(`Auth failed: ${error.statusCode}`);
684
+ } else if (error instanceof WABASendError) {
685
+ console.error(`Send failed: ${error.statusCode}`);
686
+ console.error('Details:', error.errorPayload);
687
+ }
688
+ }
286
689
  ```
287
690
 
288
691
  ---
@@ -293,8 +696,9 @@ All types are exported for use in your application:
293
696
 
294
697
  ```typescript
295
698
  import type {
296
- // Client
699
+ // Clients
297
700
  WABAClientOptions,
701
+ WABAApiClientOptions,
298
702
  GetMediaOptions,
299
703
 
300
704
  // Media
@@ -302,7 +706,19 @@ import type {
302
706
  MediaStreamResult,
303
707
  MediaBufferResult,
304
708
 
305
- // Webhooks
709
+ // API (Outbound)
710
+ SendTextMessageRequest,
711
+ SendTemplateMessageRequest,
712
+ SendMessageResponse,
713
+ MessagePayload,
714
+ RegisterPhoneRequest,
715
+ DeregisterPhoneRequest,
716
+ TemplateParameter,
717
+ TemplateComponent,
718
+ PhoneNumber,
719
+ ListPhoneNumbersResponse,
720
+
721
+ // Webhooks (Inbound)
306
722
  WebhookPayload,
307
723
  WebhookClassification,
308
724
  MessageWebhookValue,