waba-toolkit 0.1.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/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/index.d.ts +554 -0
- package/dist/index.js +260 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Robin Biju Thomas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# waba-toolkit
|
|
2
|
+
|
|
3
|
+
A minimal, type-safe toolkit for WhatsApp Business API webhook processing and media handling.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Installation](#installation)
|
|
12
|
+
- [Requirements](#requirements)
|
|
13
|
+
- [Quick Start](#quick-start)
|
|
14
|
+
- [API Reference](#api-reference)
|
|
15
|
+
- [WABAClient](#wabaclient)
|
|
16
|
+
- [Webhook Functions](#webhook-functions)
|
|
17
|
+
- [Helper Functions](#helper-functions)
|
|
18
|
+
- [Error Classes](#error-classes)
|
|
19
|
+
- [Types](#types)
|
|
20
|
+
- [License](#license)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install waba-toolkit
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- Node.js 20+
|
|
35
|
+
- A valid Meta access token with `whatsapp_business_messaging` permission
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import {
|
|
43
|
+
WABAClient,
|
|
44
|
+
verifyWebhookSignature,
|
|
45
|
+
classifyWebhook,
|
|
46
|
+
classifyMessage,
|
|
47
|
+
isMediaMessage,
|
|
48
|
+
extractMediaId,
|
|
49
|
+
} from 'waba-toolkit';
|
|
50
|
+
|
|
51
|
+
// Initialize client
|
|
52
|
+
const client = new WABAClient({
|
|
53
|
+
accessToken: process.env.META_ACCESS_TOKEN,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// In your webhook handler
|
|
57
|
+
app.post('/webhook', async (req, res) => {
|
|
58
|
+
// 1. Verify signature
|
|
59
|
+
const isValid = verifyWebhookSignature({
|
|
60
|
+
signature: req.headers['x-hub-signature-256'],
|
|
61
|
+
rawBody: req.rawBody,
|
|
62
|
+
appSecret: process.env.META_APP_SECRET,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (!isValid) {
|
|
66
|
+
return res.status(401).send('Invalid signature');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 2. Classify webhook type
|
|
70
|
+
const webhook = classifyWebhook(req.body);
|
|
71
|
+
|
|
72
|
+
if (webhook.type === 'message') {
|
|
73
|
+
const message = webhook.payload.messages?.[0];
|
|
74
|
+
if (!message) return res.sendStatus(200);
|
|
75
|
+
|
|
76
|
+
// 3. Classify message type
|
|
77
|
+
const classified = classifyMessage(message);
|
|
78
|
+
|
|
79
|
+
if (classified.type === 'text') {
|
|
80
|
+
console.log('Text:', classified.message.text.body);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 4. Handle media messages
|
|
84
|
+
if (isMediaMessage(message)) {
|
|
85
|
+
const mediaId = extractMediaId(message);
|
|
86
|
+
const { stream, mimeType } = await client.getMedia(mediaId);
|
|
87
|
+
// Process stream...
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
res.sendStatus(200);
|
|
92
|
+
});
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## API Reference
|
|
98
|
+
|
|
99
|
+
### WABAClient
|
|
100
|
+
|
|
101
|
+
Client for downloading media from WhatsApp Business API.
|
|
102
|
+
|
|
103
|
+
#### Constructor
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const client = new WABAClient({
|
|
107
|
+
accessToken: string, // Required: Meta access token
|
|
108
|
+
apiVersion?: string, // Optional: API version (default: 'v22.0')
|
|
109
|
+
baseUrl?: string, // Optional: Base URL (default: 'https://graph.facebook.com')
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
#### Methods
|
|
114
|
+
|
|
115
|
+
| Method | Description |
|
|
116
|
+
|--------|-------------|
|
|
117
|
+
| `getMedia(mediaId)` | Downloads media, returns `ReadableStream` |
|
|
118
|
+
| `getMedia(mediaId, { asBuffer: true })` | Downloads media, returns `ArrayBuffer` |
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// Stream (default)
|
|
122
|
+
const { stream, mimeType, sha256, fileSize, url } = await client.getMedia(mediaId);
|
|
123
|
+
|
|
124
|
+
// Buffer
|
|
125
|
+
const { buffer, mimeType, sha256, fileSize, url } = await client.getMedia(mediaId, {
|
|
126
|
+
asBuffer: true,
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
### Webhook Functions
|
|
133
|
+
|
|
134
|
+
| Function | Description |
|
|
135
|
+
|----------|-------------|
|
|
136
|
+
| `verifyWebhookSignature(options)` | Verifies `X-Hub-Signature-256` header using HMAC-SHA256 |
|
|
137
|
+
| `classifyWebhook(payload)` | Returns discriminated union: `message` \| `status` \| `call` \| `unknown` |
|
|
138
|
+
| `classifyMessage(message)` | Returns discriminated union for 15 message types |
|
|
139
|
+
|
|
140
|
+
#### verifyWebhookSignature
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const isValid = verifyWebhookSignature({
|
|
144
|
+
signature: req.headers['x-hub-signature-256'], // X-Hub-Signature-256 header
|
|
145
|
+
rawBody: req.rawBody, // Raw body as Buffer or string
|
|
146
|
+
appSecret: process.env.META_APP_SECRET, // Meta App Secret
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### classifyWebhook
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const result = classifyWebhook(webhookPayload);
|
|
154
|
+
|
|
155
|
+
switch (result.type) {
|
|
156
|
+
case 'message':
|
|
157
|
+
// result.payload: MessageWebhookValue
|
|
158
|
+
break;
|
|
159
|
+
case 'status':
|
|
160
|
+
// result.payload: StatusWebhookValue
|
|
161
|
+
break;
|
|
162
|
+
case 'call':
|
|
163
|
+
// result.payload: CallWebhookValue
|
|
164
|
+
break;
|
|
165
|
+
case 'unknown':
|
|
166
|
+
// Unrecognized webhook
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### classifyMessage
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
const result = classifyMessage(message);
|
|
175
|
+
|
|
176
|
+
switch (result.type) {
|
|
177
|
+
case 'text':
|
|
178
|
+
console.log(result.message.text.body);
|
|
179
|
+
break;
|
|
180
|
+
case 'image':
|
|
181
|
+
case 'video':
|
|
182
|
+
case 'audio':
|
|
183
|
+
case 'document':
|
|
184
|
+
case 'sticker':
|
|
185
|
+
console.log(result.message[result.type].id);
|
|
186
|
+
break;
|
|
187
|
+
case 'location':
|
|
188
|
+
console.log(result.message.location.latitude);
|
|
189
|
+
break;
|
|
190
|
+
case 'contacts':
|
|
191
|
+
console.log(result.message.contacts[0].name);
|
|
192
|
+
break;
|
|
193
|
+
case 'interactive':
|
|
194
|
+
console.log(result.message.interactive.type);
|
|
195
|
+
break;
|
|
196
|
+
case 'reaction':
|
|
197
|
+
console.log(result.message.reaction.emoji);
|
|
198
|
+
break;
|
|
199
|
+
// ... button, order, system, referral, unsupported
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
### Helper Functions
|
|
206
|
+
|
|
207
|
+
| Function | Description |
|
|
208
|
+
|----------|-------------|
|
|
209
|
+
| `isMediaMessage(message)` | Type guard: returns `true` if message has downloadable media |
|
|
210
|
+
| `extractMediaId(message)` | Extracts media ID from image/audio/video/document/sticker messages |
|
|
211
|
+
| `getContactInfo(webhook)` | Extracts sender's `waId`, `profileName`, and `phoneNumberId` |
|
|
212
|
+
| `getMessageTimestamp(message)` | Parses timestamp string to `Date` object |
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import {
|
|
216
|
+
isMediaMessage,
|
|
217
|
+
extractMediaId,
|
|
218
|
+
getContactInfo,
|
|
219
|
+
getMessageTimestamp,
|
|
220
|
+
} from 'waba-toolkit';
|
|
221
|
+
|
|
222
|
+
// Check if message has media
|
|
223
|
+
if (isMediaMessage(message)) {
|
|
224
|
+
const mediaId = extractMediaId(message); // guaranteed non-undefined
|
|
225
|
+
const media = await client.getMedia(mediaId);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Get sender info
|
|
229
|
+
const contact = getContactInfo(webhookPayload);
|
|
230
|
+
if (contact) {
|
|
231
|
+
console.log(contact.waId); // e.g., '14155551234'
|
|
232
|
+
console.log(contact.profileName); // e.g., 'John Doe' (may be undefined)
|
|
233
|
+
console.log(contact.phoneNumberId); // Your business phone number ID
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Parse timestamp
|
|
237
|
+
const sentAt = getMessageTimestamp(message);
|
|
238
|
+
console.log(sentAt.toISOString());
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### Error Classes
|
|
244
|
+
|
|
245
|
+
| Class | Description |
|
|
246
|
+
|-------|-------------|
|
|
247
|
+
| `WABAError` | Base error class |
|
|
248
|
+
| `WABAMediaError` | Media download failures (includes `mediaId` and `code`) |
|
|
249
|
+
| `WABANetworkError` | Network/connection failures (includes `cause`) |
|
|
250
|
+
| `WABASignatureError` | Invalid webhook signature |
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import { WABAMediaError, WABANetworkError } from 'waba-toolkit';
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const media = await client.getMedia(mediaId);
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (error instanceof WABAMediaError) {
|
|
259
|
+
console.error(`Media error for ${error.mediaId}: ${error.code}`);
|
|
260
|
+
} else if (error instanceof WABANetworkError) {
|
|
261
|
+
console.error('Network error:', error.cause);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Types
|
|
269
|
+
|
|
270
|
+
All types are exported for use in your application:
|
|
271
|
+
|
|
272
|
+
```typescript
|
|
273
|
+
import type {
|
|
274
|
+
// Client
|
|
275
|
+
WABAClientOptions,
|
|
276
|
+
GetMediaOptions,
|
|
277
|
+
|
|
278
|
+
// Media
|
|
279
|
+
MediaMetadata,
|
|
280
|
+
MediaStreamResult,
|
|
281
|
+
MediaBufferResult,
|
|
282
|
+
|
|
283
|
+
// Webhooks
|
|
284
|
+
WebhookPayload,
|
|
285
|
+
WebhookClassification,
|
|
286
|
+
MessageWebhookValue,
|
|
287
|
+
StatusWebhookValue,
|
|
288
|
+
CallWebhookValue,
|
|
289
|
+
|
|
290
|
+
// Messages
|
|
291
|
+
IncomingMessage,
|
|
292
|
+
MessageClassification,
|
|
293
|
+
TextMessage,
|
|
294
|
+
ImageMessage,
|
|
295
|
+
AudioMessage,
|
|
296
|
+
VideoMessage,
|
|
297
|
+
DocumentMessage,
|
|
298
|
+
StickerMessage,
|
|
299
|
+
LocationMessage,
|
|
300
|
+
ContactsMessage,
|
|
301
|
+
InteractiveMessage,
|
|
302
|
+
ReactionMessage,
|
|
303
|
+
MediaMessage,
|
|
304
|
+
// ... and more
|
|
305
|
+
} from 'waba-toolkit';
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## License
|
|
311
|
+
|
|
312
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
interface WABAClientOptions {
|
|
2
|
+
/** Meta access token with whatsapp_business_messaging permission */
|
|
3
|
+
accessToken: string;
|
|
4
|
+
/** API version (default: 'v22.0') */
|
|
5
|
+
apiVersion?: string;
|
|
6
|
+
/** Base URL (default: 'https://graph.facebook.com') */
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
}
|
|
9
|
+
interface GetMediaOptions {
|
|
10
|
+
/** Return ArrayBuffer instead of ReadableStream (default: false) */
|
|
11
|
+
asBuffer?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Media metadata returned from WABA API.
|
|
16
|
+
* Field names are normalized from snake_case to camelCase.
|
|
17
|
+
*/
|
|
18
|
+
interface MediaMetadata {
|
|
19
|
+
id: string;
|
|
20
|
+
/** Normalized from mime_type */
|
|
21
|
+
mimeType: string;
|
|
22
|
+
sha256: string;
|
|
23
|
+
/** Normalized from file_size (string → number) */
|
|
24
|
+
fileSize: number;
|
|
25
|
+
/** Temporary URL (expires in 5 minutes) */
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
interface MediaStreamResult extends MediaMetadata {
|
|
29
|
+
stream: ReadableStream<Uint8Array>;
|
|
30
|
+
}
|
|
31
|
+
interface MediaBufferResult extends MediaMetadata {
|
|
32
|
+
buffer: ArrayBuffer;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Client for WhatsApp Business API media operations.
|
|
37
|
+
*/
|
|
38
|
+
declare class WABAClient {
|
|
39
|
+
private readonly accessToken;
|
|
40
|
+
private readonly apiVersion;
|
|
41
|
+
private readonly baseUrl;
|
|
42
|
+
constructor(options: WABAClientOptions);
|
|
43
|
+
/**
|
|
44
|
+
* Fetches media by ID from WhatsApp Business API.
|
|
45
|
+
*
|
|
46
|
+
* Step 1: GET /{apiVersion}/{mediaId} → retrieves temporary URL + metadata
|
|
47
|
+
* Step 2: GET {temporaryUrl} → downloads binary content
|
|
48
|
+
*
|
|
49
|
+
* @throws {WABAMediaError} - Media not found (404) or access denied
|
|
50
|
+
* @throws {WABANetworkError} - Network/connection failures
|
|
51
|
+
*/
|
|
52
|
+
getMedia(mediaId: string): Promise<MediaStreamResult>;
|
|
53
|
+
getMedia(mediaId: string, options: {
|
|
54
|
+
asBuffer: true;
|
|
55
|
+
}): Promise<MediaBufferResult>;
|
|
56
|
+
getMedia(mediaId: string, options?: GetMediaOptions): Promise<MediaStreamResult | MediaBufferResult>;
|
|
57
|
+
private fetchMediaMetadata;
|
|
58
|
+
private downloadMedia;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Context object for replies and forwarded messages */
|
|
62
|
+
interface MessageContext {
|
|
63
|
+
/** Original sender (for replies) */
|
|
64
|
+
from?: string;
|
|
65
|
+
/** Original message ID */
|
|
66
|
+
id?: string;
|
|
67
|
+
forwarded?: boolean;
|
|
68
|
+
frequently_forwarded?: boolean;
|
|
69
|
+
/** Present for product enquiry messages */
|
|
70
|
+
referred_product?: {
|
|
71
|
+
catalog_id: string;
|
|
72
|
+
product_retailer_id: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Identity object for security notifications */
|
|
76
|
+
interface MessageIdentity {
|
|
77
|
+
/** State of acknowledgment for latest user_identity_changed system notification */
|
|
78
|
+
acknowledged: boolean;
|
|
79
|
+
/** Timestamp when the WhatsApp Business API detected the user potentially changed */
|
|
80
|
+
created_timestamp: string;
|
|
81
|
+
/** Identifier for the latest user_identity_changed system notification */
|
|
82
|
+
hash: string;
|
|
83
|
+
}
|
|
84
|
+
/** Base fields present on all incoming messages */
|
|
85
|
+
interface IncomingMessageBase {
|
|
86
|
+
/** Sender's phone number */
|
|
87
|
+
from: string;
|
|
88
|
+
/** Message ID (use for mark-as-read) */
|
|
89
|
+
id: string;
|
|
90
|
+
/** Unix epoch seconds as string */
|
|
91
|
+
timestamp: string;
|
|
92
|
+
/** Message type */
|
|
93
|
+
type: string;
|
|
94
|
+
/** Present if reply or forwarded */
|
|
95
|
+
context?: MessageContext;
|
|
96
|
+
/** Present if show_security_notifications is enabled in application settings */
|
|
97
|
+
identity?: MessageIdentity;
|
|
98
|
+
}
|
|
99
|
+
/** Text message */
|
|
100
|
+
interface TextMessage extends IncomingMessageBase {
|
|
101
|
+
type: 'text';
|
|
102
|
+
text: {
|
|
103
|
+
body: string;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** Media object base (shared by image, audio, video, document, sticker) */
|
|
107
|
+
interface MediaObject {
|
|
108
|
+
id: string;
|
|
109
|
+
mime_type: string;
|
|
110
|
+
sha256: string;
|
|
111
|
+
caption?: string;
|
|
112
|
+
}
|
|
113
|
+
/** Image message */
|
|
114
|
+
interface ImageMessage extends IncomingMessageBase {
|
|
115
|
+
type: 'image';
|
|
116
|
+
image: MediaObject;
|
|
117
|
+
}
|
|
118
|
+
/** Audio message */
|
|
119
|
+
interface AudioMessage extends IncomingMessageBase {
|
|
120
|
+
type: 'audio';
|
|
121
|
+
audio: MediaObject & {
|
|
122
|
+
/** True if voice note */
|
|
123
|
+
voice?: boolean;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Video message */
|
|
127
|
+
interface VideoMessage extends IncomingMessageBase {
|
|
128
|
+
type: 'video';
|
|
129
|
+
video: MediaObject;
|
|
130
|
+
}
|
|
131
|
+
/** Document message */
|
|
132
|
+
interface DocumentMessage extends IncomingMessageBase {
|
|
133
|
+
type: 'document';
|
|
134
|
+
document: MediaObject & {
|
|
135
|
+
filename?: string;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Sticker message */
|
|
139
|
+
interface StickerMessage extends IncomingMessageBase {
|
|
140
|
+
type: 'sticker';
|
|
141
|
+
sticker: MediaObject & {
|
|
142
|
+
animated?: boolean;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** Location message */
|
|
146
|
+
interface LocationMessage extends IncomingMessageBase {
|
|
147
|
+
type: 'location';
|
|
148
|
+
location: {
|
|
149
|
+
latitude: number;
|
|
150
|
+
longitude: number;
|
|
151
|
+
name?: string;
|
|
152
|
+
address?: string;
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/** Contact in contacts message */
|
|
156
|
+
interface ContactCard {
|
|
157
|
+
name: {
|
|
158
|
+
formatted_name: string;
|
|
159
|
+
first_name?: string;
|
|
160
|
+
last_name?: string;
|
|
161
|
+
middle_name?: string;
|
|
162
|
+
suffix?: string;
|
|
163
|
+
prefix?: string;
|
|
164
|
+
};
|
|
165
|
+
phones?: Array<{
|
|
166
|
+
phone?: string;
|
|
167
|
+
type?: string;
|
|
168
|
+
wa_id?: string;
|
|
169
|
+
}>;
|
|
170
|
+
emails?: Array<{
|
|
171
|
+
email?: string;
|
|
172
|
+
type?: string;
|
|
173
|
+
}>;
|
|
174
|
+
addresses?: Array<{
|
|
175
|
+
street?: string;
|
|
176
|
+
city?: string;
|
|
177
|
+
state?: string;
|
|
178
|
+
zip?: string;
|
|
179
|
+
country?: string;
|
|
180
|
+
country_code?: string;
|
|
181
|
+
type?: string;
|
|
182
|
+
}>;
|
|
183
|
+
org?: {
|
|
184
|
+
company?: string;
|
|
185
|
+
department?: string;
|
|
186
|
+
title?: string;
|
|
187
|
+
};
|
|
188
|
+
urls?: Array<{
|
|
189
|
+
url?: string;
|
|
190
|
+
type?: string;
|
|
191
|
+
}>;
|
|
192
|
+
birthday?: string;
|
|
193
|
+
}
|
|
194
|
+
/** Contacts message */
|
|
195
|
+
interface ContactsMessage extends IncomingMessageBase {
|
|
196
|
+
type: 'contacts';
|
|
197
|
+
contacts: ContactCard[];
|
|
198
|
+
}
|
|
199
|
+
/** Button reply object */
|
|
200
|
+
interface ButtonReply {
|
|
201
|
+
id: string;
|
|
202
|
+
title: string;
|
|
203
|
+
}
|
|
204
|
+
/** List reply object */
|
|
205
|
+
interface ListReply {
|
|
206
|
+
id: string;
|
|
207
|
+
title: string;
|
|
208
|
+
description?: string;
|
|
209
|
+
}
|
|
210
|
+
/** Flow (NFM) reply object */
|
|
211
|
+
interface NfmReply {
|
|
212
|
+
/** JSON string containing the flow response data */
|
|
213
|
+
response_json: string;
|
|
214
|
+
/** Optional body text */
|
|
215
|
+
body?: string;
|
|
216
|
+
/** Flow name */
|
|
217
|
+
name?: string;
|
|
218
|
+
/** Flow token for tracking/correlation */
|
|
219
|
+
flow_token?: string;
|
|
220
|
+
}
|
|
221
|
+
/** Interactive message (button/list replies, flow responses) */
|
|
222
|
+
interface InteractiveMessage extends IncomingMessageBase {
|
|
223
|
+
type: 'interactive';
|
|
224
|
+
interactive: {
|
|
225
|
+
type: 'button_reply' | 'list_reply' | 'nfm_reply';
|
|
226
|
+
button_reply?: ButtonReply;
|
|
227
|
+
list_reply?: ListReply;
|
|
228
|
+
nfm_reply?: NfmReply;
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/** Reaction message */
|
|
232
|
+
interface ReactionMessage extends IncomingMessageBase {
|
|
233
|
+
type: 'reaction';
|
|
234
|
+
reaction: {
|
|
235
|
+
message_id: string;
|
|
236
|
+
emoji: string;
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
/** Button message (quick reply button click) */
|
|
240
|
+
interface ButtonMessage extends IncomingMessageBase {
|
|
241
|
+
type: 'button';
|
|
242
|
+
button: {
|
|
243
|
+
text: string;
|
|
244
|
+
payload: string;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/** Order message */
|
|
248
|
+
interface OrderMessage extends IncomingMessageBase {
|
|
249
|
+
type: 'order';
|
|
250
|
+
order: {
|
|
251
|
+
catalog_id: string;
|
|
252
|
+
product_items: Array<{
|
|
253
|
+
product_retailer_id: string;
|
|
254
|
+
quantity: number;
|
|
255
|
+
item_price: number;
|
|
256
|
+
currency: string;
|
|
257
|
+
}>;
|
|
258
|
+
text?: string;
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
/** System message (number change, identity change) */
|
|
262
|
+
interface SystemMessage extends IncomingMessageBase {
|
|
263
|
+
type: 'system';
|
|
264
|
+
system: {
|
|
265
|
+
body: string;
|
|
266
|
+
type: 'user_changed_number' | 'user_identity_changed';
|
|
267
|
+
new_wa_id?: string;
|
|
268
|
+
identity?: string;
|
|
269
|
+
user?: string;
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/** Referral message (click-to-WhatsApp ads) */
|
|
273
|
+
interface ReferralMessage extends IncomingMessageBase {
|
|
274
|
+
type: 'referral';
|
|
275
|
+
referral: {
|
|
276
|
+
source_url: string;
|
|
277
|
+
source_type: string;
|
|
278
|
+
source_id: string;
|
|
279
|
+
headline?: string;
|
|
280
|
+
body?: string;
|
|
281
|
+
media_type?: string;
|
|
282
|
+
image_url?: string;
|
|
283
|
+
video_url?: string;
|
|
284
|
+
thumbnail_url?: string;
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
/** Unsupported/unknown message type */
|
|
288
|
+
interface UnsupportedMessage extends IncomingMessageBase {
|
|
289
|
+
type: 'unsupported' | 'unknown';
|
|
290
|
+
errors?: Array<{
|
|
291
|
+
code: number;
|
|
292
|
+
title: string;
|
|
293
|
+
details?: string;
|
|
294
|
+
}>;
|
|
295
|
+
}
|
|
296
|
+
/** Union of all incoming message types */
|
|
297
|
+
type IncomingMessage = TextMessage | ImageMessage | AudioMessage | VideoMessage | DocumentMessage | StickerMessage | LocationMessage | ContactsMessage | InteractiveMessage | ReactionMessage | ButtonMessage | OrderMessage | SystemMessage | ReferralMessage | UnsupportedMessage;
|
|
298
|
+
/** Union of media message types */
|
|
299
|
+
type MediaMessage = ImageMessage | AudioMessage | VideoMessage | DocumentMessage | StickerMessage;
|
|
300
|
+
/** Discriminated union for message classification results */
|
|
301
|
+
type MessageClassification = {
|
|
302
|
+
type: 'text';
|
|
303
|
+
message: TextMessage;
|
|
304
|
+
} | {
|
|
305
|
+
type: 'image';
|
|
306
|
+
message: ImageMessage;
|
|
307
|
+
} | {
|
|
308
|
+
type: 'video';
|
|
309
|
+
message: VideoMessage;
|
|
310
|
+
} | {
|
|
311
|
+
type: 'audio';
|
|
312
|
+
message: AudioMessage;
|
|
313
|
+
} | {
|
|
314
|
+
type: 'document';
|
|
315
|
+
message: DocumentMessage;
|
|
316
|
+
} | {
|
|
317
|
+
type: 'sticker';
|
|
318
|
+
message: StickerMessage;
|
|
319
|
+
} | {
|
|
320
|
+
type: 'location';
|
|
321
|
+
message: LocationMessage;
|
|
322
|
+
} | {
|
|
323
|
+
type: 'contacts';
|
|
324
|
+
message: ContactsMessage;
|
|
325
|
+
} | {
|
|
326
|
+
type: 'interactive';
|
|
327
|
+
message: InteractiveMessage;
|
|
328
|
+
} | {
|
|
329
|
+
type: 'reaction';
|
|
330
|
+
message: ReactionMessage;
|
|
331
|
+
} | {
|
|
332
|
+
type: 'button';
|
|
333
|
+
message: ButtonMessage;
|
|
334
|
+
} | {
|
|
335
|
+
type: 'order';
|
|
336
|
+
message: OrderMessage;
|
|
337
|
+
} | {
|
|
338
|
+
type: 'system';
|
|
339
|
+
message: SystemMessage;
|
|
340
|
+
} | {
|
|
341
|
+
type: 'referral';
|
|
342
|
+
message: ReferralMessage;
|
|
343
|
+
} | {
|
|
344
|
+
type: 'unsupported';
|
|
345
|
+
message: UnsupportedMessage;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
/** Top-level webhook payload from Meta */
|
|
349
|
+
interface WebhookPayload {
|
|
350
|
+
object: 'whatsapp_business_account';
|
|
351
|
+
entry: WebhookEntry[];
|
|
352
|
+
}
|
|
353
|
+
interface WebhookEntry {
|
|
354
|
+
/** WABA ID */
|
|
355
|
+
id: string;
|
|
356
|
+
changes: WebhookChange[];
|
|
357
|
+
}
|
|
358
|
+
interface WebhookChange {
|
|
359
|
+
value: WebhookValue;
|
|
360
|
+
field: string;
|
|
361
|
+
}
|
|
362
|
+
/** Union of possible webhook value types */
|
|
363
|
+
type WebhookValue = MessageWebhookValue | StatusWebhookValue | CallWebhookValue;
|
|
364
|
+
/** Contact info included in webhooks */
|
|
365
|
+
interface WebhookContact {
|
|
366
|
+
profile: {
|
|
367
|
+
/** Sender's profile name (optional per WABA docs) */
|
|
368
|
+
name?: string;
|
|
369
|
+
};
|
|
370
|
+
wa_id: string;
|
|
371
|
+
}
|
|
372
|
+
/** Metadata included in all webhook values */
|
|
373
|
+
interface WebhookMetadata {
|
|
374
|
+
display_phone_number: string;
|
|
375
|
+
phone_number_id: string;
|
|
376
|
+
}
|
|
377
|
+
/** Error object in webhooks */
|
|
378
|
+
interface WebhookError {
|
|
379
|
+
code: number;
|
|
380
|
+
title: string;
|
|
381
|
+
message?: string;
|
|
382
|
+
error_data?: {
|
|
383
|
+
details: string;
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
/** Webhook value for incoming messages */
|
|
387
|
+
interface MessageWebhookValue {
|
|
388
|
+
messaging_product: 'whatsapp';
|
|
389
|
+
metadata: WebhookMetadata;
|
|
390
|
+
contacts?: WebhookContact[];
|
|
391
|
+
messages?: IncomingMessage[];
|
|
392
|
+
errors?: WebhookError[];
|
|
393
|
+
}
|
|
394
|
+
/** Status update types */
|
|
395
|
+
type MessageStatus = 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';
|
|
396
|
+
/** Conversation origin types */
|
|
397
|
+
type ConversationOriginType = 'business_initiated' | 'user_initiated' | 'referral_conversion';
|
|
398
|
+
/** Conversation object in status webhooks */
|
|
399
|
+
interface ConversationObject {
|
|
400
|
+
id: string;
|
|
401
|
+
origin: {
|
|
402
|
+
type: ConversationOriginType;
|
|
403
|
+
};
|
|
404
|
+
expiration_timestamp?: string;
|
|
405
|
+
}
|
|
406
|
+
/** Pricing model types */
|
|
407
|
+
type PricingModel = 'CBP' | 'NBP';
|
|
408
|
+
/** Pricing category types */
|
|
409
|
+
type PricingCategory = 'business_initiated' | 'user_initiated' | 'referral_conversion' | 'authentication' | 'marketing' | 'utility' | 'service';
|
|
410
|
+
/** Pricing object in status webhooks */
|
|
411
|
+
interface PricingObject {
|
|
412
|
+
billable: boolean;
|
|
413
|
+
pricing_model: PricingModel;
|
|
414
|
+
category: PricingCategory;
|
|
415
|
+
}
|
|
416
|
+
/** Individual status entry */
|
|
417
|
+
interface StatusEntry {
|
|
418
|
+
id: string;
|
|
419
|
+
recipient_id: string;
|
|
420
|
+
status: MessageStatus;
|
|
421
|
+
timestamp: string;
|
|
422
|
+
conversation?: ConversationObject;
|
|
423
|
+
pricing?: PricingObject;
|
|
424
|
+
errors?: WebhookError[];
|
|
425
|
+
}
|
|
426
|
+
/** Webhook value for message status updates */
|
|
427
|
+
interface StatusWebhookValue {
|
|
428
|
+
messaging_product: 'whatsapp';
|
|
429
|
+
metadata: WebhookMetadata;
|
|
430
|
+
statuses: StatusEntry[];
|
|
431
|
+
}
|
|
432
|
+
/** Call entry in call webhooks */
|
|
433
|
+
interface CallEntry {
|
|
434
|
+
id: string;
|
|
435
|
+
from: string;
|
|
436
|
+
to: string;
|
|
437
|
+
/** Present on connect webhooks */
|
|
438
|
+
event?: 'connect';
|
|
439
|
+
direction: 'USER_INITIATED' | 'BUSINESS_INITIATED';
|
|
440
|
+
timestamp: string;
|
|
441
|
+
session?: {
|
|
442
|
+
sdp_type: string;
|
|
443
|
+
sdp: string;
|
|
444
|
+
};
|
|
445
|
+
/** e.g. ['COMPLETED'] or ['FAILED'] on terminate */
|
|
446
|
+
status?: string[];
|
|
447
|
+
/** Present on terminate if connected */
|
|
448
|
+
start_time?: string;
|
|
449
|
+
end_time?: string;
|
|
450
|
+
/** Seconds, present on terminate if connected */
|
|
451
|
+
duration?: number;
|
|
452
|
+
errors?: {
|
|
453
|
+
code: number;
|
|
454
|
+
message: string;
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/** Webhook value for call events */
|
|
458
|
+
interface CallWebhookValue {
|
|
459
|
+
messaging_product: 'whatsapp';
|
|
460
|
+
metadata: WebhookMetadata;
|
|
461
|
+
contacts?: WebhookContact[];
|
|
462
|
+
calls: CallEntry[];
|
|
463
|
+
}
|
|
464
|
+
/** Discriminated union for webhook classification results */
|
|
465
|
+
type WebhookClassification = {
|
|
466
|
+
type: 'message';
|
|
467
|
+
payload: MessageWebhookValue;
|
|
468
|
+
} | {
|
|
469
|
+
type: 'status';
|
|
470
|
+
payload: StatusWebhookValue;
|
|
471
|
+
} | {
|
|
472
|
+
type: 'call';
|
|
473
|
+
payload: CallWebhookValue;
|
|
474
|
+
} | {
|
|
475
|
+
type: 'unknown';
|
|
476
|
+
payload: unknown;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Classifies a webhook payload into its type.
|
|
481
|
+
* Returns a discriminated union for type-safe handling.
|
|
482
|
+
*/
|
|
483
|
+
declare function classifyWebhook(payload: WebhookPayload): WebhookClassification;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Classifies an incoming message by its type.
|
|
487
|
+
* Returns a discriminated union for type-safe handling.
|
|
488
|
+
*/
|
|
489
|
+
declare function classifyMessage(message: IncomingMessage): MessageClassification;
|
|
490
|
+
|
|
491
|
+
interface VerifyWebhookSignatureOptions {
|
|
492
|
+
/** X-Hub-Signature-256 header value */
|
|
493
|
+
signature: string | undefined;
|
|
494
|
+
/** Raw request body (NOT parsed JSON) */
|
|
495
|
+
rawBody: Buffer | string;
|
|
496
|
+
/** Meta App Secret */
|
|
497
|
+
appSecret: string;
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Verifies webhook signature using HMAC-SHA256.
|
|
501
|
+
* Uses timing-safe comparison to prevent timing attacks.
|
|
502
|
+
*
|
|
503
|
+
* @returns true if signature is valid, false otherwise
|
|
504
|
+
*/
|
|
505
|
+
declare function verifyWebhookSignature(options: VerifyWebhookSignatureOptions): boolean;
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Type guard: returns true if message contains downloadable media.
|
|
509
|
+
* Narrows type to messages with image/audio/video/document/sticker.
|
|
510
|
+
*/
|
|
511
|
+
declare function isMediaMessage(message: IncomingMessage): message is MediaMessage;
|
|
512
|
+
/**
|
|
513
|
+
* Extracts media ID from any media message type.
|
|
514
|
+
* Returns undefined if message has no media.
|
|
515
|
+
*/
|
|
516
|
+
declare function extractMediaId(message: IncomingMessage): string | undefined;
|
|
517
|
+
interface ContactInfo {
|
|
518
|
+
waId: string;
|
|
519
|
+
profileName: string | undefined;
|
|
520
|
+
phoneNumberId: string;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Extracts sender info from webhook payload.
|
|
524
|
+
* Returns undefined if the webhook doesn't contain message contact info.
|
|
525
|
+
*/
|
|
526
|
+
declare function getContactInfo(webhook: WebhookPayload): ContactInfo | undefined;
|
|
527
|
+
/**
|
|
528
|
+
* Parses message timestamp to Date object.
|
|
529
|
+
* WhatsApp timestamps are Unix epoch seconds as strings.
|
|
530
|
+
*/
|
|
531
|
+
declare function getMessageTimestamp(message: IncomingMessage): Date;
|
|
532
|
+
|
|
533
|
+
/** Base error class for all WABA errors */
|
|
534
|
+
declare class WABAError extends Error {
|
|
535
|
+
readonly code?: number | undefined;
|
|
536
|
+
readonly details?: unknown | undefined;
|
|
537
|
+
constructor(message: string, code?: number | undefined, details?: unknown | undefined);
|
|
538
|
+
}
|
|
539
|
+
/** Error for media-related failures (404, access denied) */
|
|
540
|
+
declare class WABAMediaError extends WABAError {
|
|
541
|
+
readonly mediaId: string;
|
|
542
|
+
constructor(message: string, mediaId: string, code?: number);
|
|
543
|
+
}
|
|
544
|
+
/** Error for network/connection failures */
|
|
545
|
+
declare class WABANetworkError extends WABAError {
|
|
546
|
+
readonly cause?: Error;
|
|
547
|
+
constructor(message: string, cause?: Error);
|
|
548
|
+
}
|
|
549
|
+
/** Error for invalid webhook signatures */
|
|
550
|
+
declare class WABASignatureError extends WABAError {
|
|
551
|
+
constructor(message?: string);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
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, getContactInfo, getMessageTimestamp, isMediaMessage, verifyWebhookSignature };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
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/client.ts
|
|
37
|
+
var DEFAULT_API_VERSION = "v22.0";
|
|
38
|
+
var DEFAULT_BASE_URL = "https://graph.facebook.com";
|
|
39
|
+
var WABAClient = class {
|
|
40
|
+
accessToken;
|
|
41
|
+
apiVersion;
|
|
42
|
+
baseUrl;
|
|
43
|
+
constructor(options) {
|
|
44
|
+
this.accessToken = options.accessToken;
|
|
45
|
+
this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
|
|
46
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
47
|
+
}
|
|
48
|
+
async getMedia(mediaId, options) {
|
|
49
|
+
const metadata = await this.fetchMediaMetadata(mediaId);
|
|
50
|
+
const response = await this.downloadMedia(metadata.url, mediaId);
|
|
51
|
+
if (options?.asBuffer) {
|
|
52
|
+
const buffer = await response.arrayBuffer();
|
|
53
|
+
return {
|
|
54
|
+
...metadata,
|
|
55
|
+
buffer
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (!response.body) {
|
|
59
|
+
throw new WABAMediaError("Response body is null", mediaId);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...metadata,
|
|
63
|
+
stream: response.body
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
async fetchMediaMetadata(mediaId) {
|
|
67
|
+
const url = `${this.baseUrl}/${this.apiVersion}/${mediaId}`;
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(url, {
|
|
71
|
+
method: "GET",
|
|
72
|
+
headers: {
|
|
73
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new WABANetworkError(
|
|
78
|
+
`Failed to fetch media metadata: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
79
|
+
error instanceof Error ? error : void 0
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
84
|
+
throw new WABAMediaError(
|
|
85
|
+
`Failed to fetch media metadata: ${response.status} ${errorBody}`,
|
|
86
|
+
mediaId,
|
|
87
|
+
response.status
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const data = await response.json();
|
|
91
|
+
return {
|
|
92
|
+
id: data.id,
|
|
93
|
+
mimeType: data.mime_type,
|
|
94
|
+
sha256: data.sha256,
|
|
95
|
+
fileSize: parseInt(data.file_size, 10),
|
|
96
|
+
url: data.url
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async downloadMedia(url, mediaId) {
|
|
100
|
+
let response;
|
|
101
|
+
try {
|
|
102
|
+
response = await fetch(url, {
|
|
103
|
+
method: "GET",
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new WABANetworkError(
|
|
110
|
+
`Failed to download media: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
111
|
+
error instanceof Error ? error : void 0
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
throw new WABAMediaError(
|
|
116
|
+
`Failed to download media: ${response.status}`,
|
|
117
|
+
mediaId,
|
|
118
|
+
response.status
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return response;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// src/webhooks/classify.ts
|
|
126
|
+
function classifyWebhook(payload) {
|
|
127
|
+
const entry = payload.entry?.[0];
|
|
128
|
+
const change = entry?.changes?.[0];
|
|
129
|
+
const value = change?.value;
|
|
130
|
+
if (!value) {
|
|
131
|
+
return { type: "unknown", payload };
|
|
132
|
+
}
|
|
133
|
+
if ("calls" in value && Array.isArray(value.calls)) {
|
|
134
|
+
return { type: "call", payload: value };
|
|
135
|
+
}
|
|
136
|
+
if ("statuses" in value && Array.isArray(value.statuses)) {
|
|
137
|
+
return { type: "status", payload: value };
|
|
138
|
+
}
|
|
139
|
+
if ("messages" in value && Array.isArray(value.messages)) {
|
|
140
|
+
return { type: "message", payload: value };
|
|
141
|
+
}
|
|
142
|
+
if ("errors" in value && Array.isArray(value.errors)) {
|
|
143
|
+
return { type: "message", payload: value };
|
|
144
|
+
}
|
|
145
|
+
return { type: "unknown", payload };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/webhooks/messages.ts
|
|
149
|
+
function classifyMessage(message) {
|
|
150
|
+
switch (message.type) {
|
|
151
|
+
case "text":
|
|
152
|
+
return { type: "text", message };
|
|
153
|
+
case "image":
|
|
154
|
+
return { type: "image", message };
|
|
155
|
+
case "audio":
|
|
156
|
+
return { type: "audio", message };
|
|
157
|
+
case "video":
|
|
158
|
+
return { type: "video", message };
|
|
159
|
+
case "document":
|
|
160
|
+
return { type: "document", message };
|
|
161
|
+
case "sticker":
|
|
162
|
+
return { type: "sticker", message };
|
|
163
|
+
case "location":
|
|
164
|
+
return { type: "location", message };
|
|
165
|
+
case "contacts":
|
|
166
|
+
return { type: "contacts", message };
|
|
167
|
+
case "interactive":
|
|
168
|
+
return { type: "interactive", message };
|
|
169
|
+
case "reaction":
|
|
170
|
+
return { type: "reaction", message };
|
|
171
|
+
case "button":
|
|
172
|
+
return { type: "button", message };
|
|
173
|
+
case "order":
|
|
174
|
+
return { type: "order", message };
|
|
175
|
+
case "system":
|
|
176
|
+
return { type: "system", message };
|
|
177
|
+
case "referral":
|
|
178
|
+
return { type: "referral", message };
|
|
179
|
+
default:
|
|
180
|
+
return { type: "unsupported", message };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// src/verify.ts
|
|
185
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
186
|
+
function verifyWebhookSignature(options) {
|
|
187
|
+
const { signature, rawBody, appSecret } = options;
|
|
188
|
+
if (!signature) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
const signatureHash = signature.startsWith("sha256=") ? signature.slice(7) : signature;
|
|
192
|
+
const hmac = createHmac("sha256", appSecret);
|
|
193
|
+
const bodyBuffer = typeof rawBody === "string" ? Buffer.from(rawBody, "utf-8") : rawBody;
|
|
194
|
+
const expectedHash = hmac.update(bodyBuffer).digest("hex");
|
|
195
|
+
const signatureBuffer = Buffer.from(signatureHash, "utf-8");
|
|
196
|
+
const expectedBuffer = Buffer.from(expectedHash, "utf-8");
|
|
197
|
+
if (signatureBuffer.length !== expectedBuffer.length) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return timingSafeEqual(signatureBuffer, expectedBuffer);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/helpers.ts
|
|
204
|
+
var MEDIA_TYPES = /* @__PURE__ */ new Set(["image", "audio", "video", "document", "sticker"]);
|
|
205
|
+
function isMediaMessage(message) {
|
|
206
|
+
return MEDIA_TYPES.has(message.type);
|
|
207
|
+
}
|
|
208
|
+
function extractMediaId(message) {
|
|
209
|
+
if (!isMediaMessage(message)) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
switch (message.type) {
|
|
213
|
+
case "image":
|
|
214
|
+
return message.image.id;
|
|
215
|
+
case "audio":
|
|
216
|
+
return message.audio.id;
|
|
217
|
+
case "video":
|
|
218
|
+
return message.video.id;
|
|
219
|
+
case "document":
|
|
220
|
+
return message.document.id;
|
|
221
|
+
case "sticker":
|
|
222
|
+
return message.sticker.id;
|
|
223
|
+
default:
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function getContactInfo(webhook) {
|
|
228
|
+
const entry = webhook.entry?.[0];
|
|
229
|
+
const change = entry?.changes?.[0];
|
|
230
|
+
const value = change?.value;
|
|
231
|
+
if (!value || !("contacts" in value) || !value.contacts?.length) {
|
|
232
|
+
return void 0;
|
|
233
|
+
}
|
|
234
|
+
const contact = value.contacts[0];
|
|
235
|
+
const metadata = value.metadata;
|
|
236
|
+
return {
|
|
237
|
+
waId: contact.wa_id,
|
|
238
|
+
profileName: contact.profile?.name,
|
|
239
|
+
phoneNumberId: metadata.phone_number_id
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function getMessageTimestamp(message) {
|
|
243
|
+
const epochSeconds = parseInt(message.timestamp, 10);
|
|
244
|
+
return new Date(epochSeconds * 1e3);
|
|
245
|
+
}
|
|
246
|
+
export {
|
|
247
|
+
WABAClient,
|
|
248
|
+
WABAError,
|
|
249
|
+
WABAMediaError,
|
|
250
|
+
WABANetworkError,
|
|
251
|
+
WABASignatureError,
|
|
252
|
+
classifyMessage,
|
|
253
|
+
classifyWebhook,
|
|
254
|
+
extractMediaId,
|
|
255
|
+
getContactInfo,
|
|
256
|
+
getMessageTimestamp,
|
|
257
|
+
isMediaMessage,
|
|
258
|
+
verifyWebhookSignature
|
|
259
|
+
};
|
|
260
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/client.ts","../src/webhooks/classify.ts","../src/webhooks/messages.ts","../src/verify.ts","../src/helpers.ts"],"sourcesContent":["/** Base error class for all WABA errors */\nexport class WABAError extends Error {\n constructor(\n message: string,\n public readonly code?: number,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'WABAError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for media-related failures (404, access denied) */\nexport class WABAMediaError extends WABAError {\n constructor(\n message: string,\n public readonly mediaId: string,\n code?: number\n ) {\n super(message, code);\n this.name = 'WABAMediaError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for network/connection failures */\nexport class WABANetworkError extends WABAError {\n override readonly cause?: Error;\n\n constructor(message: string, cause?: Error) {\n super(message);\n this.name = 'WABANetworkError';\n this.cause = cause;\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n/** Error for invalid webhook signatures */\nexport class WABASignatureError extends WABAError {\n constructor(message: string = 'Invalid webhook signature') {\n super(message);\n this.name = 'WABASignatureError';\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n","import type { WABAClientOptions, GetMediaOptions } from './types/client.js';\nimport type {\n MediaMetadata,\n MediaStreamResult,\n MediaBufferResult,\n RawMediaResponse,\n} from './types/media.js';\nimport { WABAMediaError, WABANetworkError } from './errors.js';\n\nconst DEFAULT_API_VERSION = 'v22.0';\nconst DEFAULT_BASE_URL = 'https://graph.facebook.com';\n\n/**\n * Client for WhatsApp Business API media operations.\n */\nexport class WABAClient {\n private readonly accessToken: string;\n private readonly apiVersion: string;\n private readonly baseUrl: string;\n\n constructor(options: WABAClientOptions) {\n this.accessToken = options.accessToken;\n this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;\n this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;\n }\n\n /**\n * Fetches media by ID from WhatsApp Business API.\n *\n * Step 1: GET /{apiVersion}/{mediaId} → retrieves temporary URL + metadata\n * Step 2: GET {temporaryUrl} → downloads binary content\n *\n * @throws {WABAMediaError} - Media not found (404) or access denied\n * @throws {WABANetworkError} - Network/connection failures\n */\n async getMedia(mediaId: string): Promise<MediaStreamResult>;\n async getMedia(\n mediaId: string,\n options: { asBuffer: true }\n ): Promise<MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult>;\n async getMedia(\n mediaId: string,\n options?: GetMediaOptions\n ): Promise<MediaStreamResult | MediaBufferResult> {\n // Step 1: Get media metadata and temporary URL\n const metadata = await this.fetchMediaMetadata(mediaId);\n\n // Step 2: Download the actual media\n const response = await this.downloadMedia(metadata.url, mediaId);\n\n if (options?.asBuffer) {\n const buffer = await response.arrayBuffer();\n return {\n ...metadata,\n buffer,\n };\n }\n\n if (!response.body) {\n throw new WABAMediaError('Response body is null', mediaId);\n }\n\n return {\n ...metadata,\n stream: response.body,\n };\n }\n\n private async fetchMediaMetadata(mediaId: string): Promise<MediaMetadata> {\n const url = `${this.baseUrl}/${this.apiVersion}/${mediaId}`;\n\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to fetch media metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n const errorBody = await response.text().catch(() => 'Unknown error');\n throw new WABAMediaError(\n `Failed to fetch media metadata: ${response.status} ${errorBody}`,\n mediaId,\n response.status\n );\n }\n\n const data = (await response.json()) as RawMediaResponse;\n\n // Normalize snake_case to camelCase\n return {\n id: data.id,\n mimeType: data.mime_type,\n sha256: data.sha256,\n fileSize: parseInt(data.file_size, 10),\n url: data.url,\n };\n }\n\n private async downloadMedia(url: string, mediaId: string): Promise<Response> {\n let response: Response;\n try {\n response = await fetch(url, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${this.accessToken}`,\n },\n });\n } catch (error) {\n throw new WABANetworkError(\n `Failed to download media: ${error instanceof Error ? error.message : 'Unknown error'}`,\n error instanceof Error ? error : undefined\n );\n }\n\n if (!response.ok) {\n throw new WABAMediaError(\n `Failed to download media: ${response.status}`,\n mediaId,\n response.status\n );\n }\n\n return response;\n }\n}\n","import type {\n WebhookPayload,\n WebhookClassification,\n MessageWebhookValue,\n StatusWebhookValue,\n CallWebhookValue,\n} from '../types/webhooks.js';\n\n/**\n * Classifies a webhook payload into its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyWebhook(payload: WebhookPayload): WebhookClassification {\n const entry = payload.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value) {\n return { type: 'unknown', payload };\n }\n\n // Check for calls array (call webhooks)\n if ('calls' in value && Array.isArray(value.calls)) {\n return { type: 'call', payload: value as CallWebhookValue };\n }\n\n // Check for statuses array (status webhooks)\n if ('statuses' in value && Array.isArray(value.statuses)) {\n return { type: 'status', payload: value as StatusWebhookValue };\n }\n\n // Check for messages array (message webhooks)\n if ('messages' in value && Array.isArray(value.messages)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n // Check for errors without messages (error webhook)\n if ('errors' in value && Array.isArray(value.errors)) {\n return { type: 'message', payload: value as MessageWebhookValue };\n }\n\n return { type: 'unknown', payload };\n}\n","import type {\n IncomingMessage,\n MessageClassification,\n TextMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n LocationMessage,\n ContactsMessage,\n InteractiveMessage,\n ReactionMessage,\n ButtonMessage,\n OrderMessage,\n SystemMessage,\n ReferralMessage,\n UnsupportedMessage,\n} from '../types/messages.js';\n\n/**\n * Classifies an incoming message by its type.\n * Returns a discriminated union for type-safe handling.\n */\nexport function classifyMessage(message: IncomingMessage): MessageClassification {\n switch (message.type) {\n case 'text':\n return { type: 'text', message: message as TextMessage };\n case 'image':\n return { type: 'image', message: message as ImageMessage };\n case 'audio':\n return { type: 'audio', message: message as AudioMessage };\n case 'video':\n return { type: 'video', message: message as VideoMessage };\n case 'document':\n return { type: 'document', message: message as DocumentMessage };\n case 'sticker':\n return { type: 'sticker', message: message as StickerMessage };\n case 'location':\n return { type: 'location', message: message as LocationMessage };\n case 'contacts':\n return { type: 'contacts', message: message as ContactsMessage };\n case 'interactive':\n return { type: 'interactive', message: message as InteractiveMessage };\n case 'reaction':\n return { type: 'reaction', message: message as ReactionMessage };\n case 'button':\n return { type: 'button', message: message as ButtonMessage };\n case 'order':\n return { type: 'order', message: message as OrderMessage };\n case 'system':\n return { type: 'system', message: message as SystemMessage };\n case 'referral':\n return { type: 'referral', message: message as ReferralMessage };\n default:\n return { type: 'unsupported', message: message as UnsupportedMessage };\n }\n}\n","import { createHmac, timingSafeEqual } from 'node:crypto';\n\nexport interface VerifyWebhookSignatureOptions {\n /** X-Hub-Signature-256 header value */\n signature: string | undefined;\n /** Raw request body (NOT parsed JSON) */\n rawBody: Buffer | string;\n /** Meta App Secret */\n appSecret: string;\n}\n\n/**\n * Verifies webhook signature using HMAC-SHA256.\n * Uses timing-safe comparison to prevent timing attacks.\n *\n * @returns true if signature is valid, false otherwise\n */\nexport function verifyWebhookSignature(\n options: VerifyWebhookSignatureOptions\n): boolean {\n const { signature, rawBody, appSecret } = options;\n\n if (!signature) {\n return false;\n }\n\n // Remove 'sha256=' prefix if present\n const signatureHash = signature.startsWith('sha256=')\n ? signature.slice(7)\n : signature;\n\n // Compute expected signature\n const hmac = createHmac('sha256', appSecret);\n const bodyBuffer =\n typeof rawBody === 'string' ? Buffer.from(rawBody, 'utf-8') : rawBody;\n const expectedHash = hmac.update(bodyBuffer).digest('hex');\n\n // Convert to buffers for timing-safe comparison\n const signatureBuffer = Buffer.from(signatureHash, 'utf-8');\n const expectedBuffer = Buffer.from(expectedHash, 'utf-8');\n\n // Ensure same length before comparison\n if (signatureBuffer.length !== expectedBuffer.length) {\n return false;\n }\n\n return timingSafeEqual(signatureBuffer, expectedBuffer);\n}\n","import type {\n IncomingMessage,\n MediaMessage,\n ImageMessage,\n AudioMessage,\n VideoMessage,\n DocumentMessage,\n StickerMessage,\n} from './types/messages.js';\nimport type { WebhookPayload } from './types/webhooks.js';\n\nconst MEDIA_TYPES = new Set(['image', 'audio', 'video', 'document', 'sticker']);\n\n/**\n * Type guard: returns true if message contains downloadable media.\n * Narrows type to messages with image/audio/video/document/sticker.\n */\nexport function isMediaMessage(message: IncomingMessage): message is MediaMessage {\n return MEDIA_TYPES.has(message.type);\n}\n\n/**\n * Extracts media ID from any media message type.\n * Returns undefined if message has no media.\n */\nexport function extractMediaId(message: IncomingMessage): string | undefined {\n if (!isMediaMessage(message)) {\n return undefined;\n }\n\n switch (message.type) {\n case 'image':\n return (message as ImageMessage).image.id;\n case 'audio':\n return (message as AudioMessage).audio.id;\n case 'video':\n return (message as VideoMessage).video.id;\n case 'document':\n return (message as DocumentMessage).document.id;\n case 'sticker':\n return (message as StickerMessage).sticker.id;\n default:\n return undefined;\n }\n}\n\nexport interface ContactInfo {\n waId: string;\n profileName: string | undefined;\n phoneNumberId: string;\n}\n\n/**\n * Extracts sender info from webhook payload.\n * Returns undefined if the webhook doesn't contain message contact info.\n */\nexport function getContactInfo(webhook: WebhookPayload): ContactInfo | undefined {\n const entry = webhook.entry?.[0];\n const change = entry?.changes?.[0];\n const value = change?.value;\n\n if (!value || !('contacts' in value) || !value.contacts?.length) {\n return undefined;\n }\n\n const contact = value.contacts[0];\n const metadata = value.metadata;\n\n return {\n waId: contact.wa_id,\n profileName: contact.profile?.name,\n phoneNumberId: metadata.phone_number_id,\n };\n}\n\n/**\n * Parses message timestamp to Date object.\n * WhatsApp timestamps are Unix epoch seconds as strings.\n */\nexport function getMessageTimestamp(message: IncomingMessage): Date {\n const epochSeconds = parseInt(message.timestamp, 10);\n return new Date(epochSeconds * 1000);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "waba-toolkit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Minimal, type-safe toolkit for WhatsApp Business API webhook processing and media handling",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.0.0"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"whatsapp",
|
|
25
|
+
"waba",
|
|
26
|
+
"whatsapp-business-api",
|
|
27
|
+
"webhook",
|
|
28
|
+
"meta"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@types/node": "^25.0.3",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|