waba-toolkit 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,863 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/index.ts
27
+ var import_commander = require("commander");
28
+ var import_node_fs3 = require("fs");
29
+ var import_node_path2 = require("path");
30
+
31
+ // src/cli/commands/configure.ts
32
+ var import_inquirer = __toESM(require("inquirer"));
33
+
34
+ // src/cli/config-manager.ts
35
+ var import_node_crypto = require("crypto");
36
+ var import_node_fs = require("fs");
37
+ var import_node_os = require("os");
38
+ var import_node_path = require("path");
39
+
40
+ // src/errors.ts
41
+ var WABAError = class extends Error {
42
+ constructor(message, code, details) {
43
+ super(message);
44
+ this.code = code;
45
+ this.details = details;
46
+ this.name = "WABAError";
47
+ Object.setPrototypeOf(this, new.target.prototype);
48
+ }
49
+ };
50
+ var WABANetworkError = class extends WABAError {
51
+ cause;
52
+ constructor(message, cause) {
53
+ super(message);
54
+ this.name = "WABANetworkError";
55
+ this.cause = cause;
56
+ Object.setPrototypeOf(this, new.target.prototype);
57
+ }
58
+ };
59
+ var WABAConfigError = class extends WABAError {
60
+ constructor(message, field) {
61
+ super(message);
62
+ this.field = field;
63
+ this.name = "WABAConfigError";
64
+ Object.setPrototypeOf(this, new.target.prototype);
65
+ }
66
+ };
67
+ var WABAAuthError = class extends WABAError {
68
+ constructor(message, statusCode) {
69
+ super(message, statusCode);
70
+ this.statusCode = statusCode;
71
+ this.name = "WABAAuthError";
72
+ Object.setPrototypeOf(this, new.target.prototype);
73
+ }
74
+ };
75
+ var WABASendError = class extends WABAError {
76
+ constructor(message, statusCode, errorPayload) {
77
+ super(message, statusCode, errorPayload);
78
+ this.statusCode = statusCode;
79
+ this.errorPayload = errorPayload;
80
+ this.name = "WABASendError";
81
+ Object.setPrototypeOf(this, new.target.prototype);
82
+ }
83
+ };
84
+
85
+ // src/cli/config-manager.ts
86
+ var CONFIG_FILE = (0, import_node_path.join)((0, import_node_os.homedir)(), ".waba-toolkit");
87
+ var ALGORITHM = "aes-256-gcm";
88
+ var IV_LENGTH = 16;
89
+ function getMachineKey() {
90
+ const host = (0, import_node_os.hostname)();
91
+ let macAddress = "";
92
+ const interfaces = (0, import_node_os.networkInterfaces)();
93
+ for (const name of Object.keys(interfaces)) {
94
+ const iface = interfaces[name];
95
+ if (!iface) continue;
96
+ for (const addr of iface) {
97
+ if (!addr.internal && addr.mac && addr.mac !== "00:00:00:00:00:00") {
98
+ macAddress = addr.mac;
99
+ break;
100
+ }
101
+ }
102
+ if (macAddress) break;
103
+ }
104
+ const keySource = macAddress ? `${host}:${macAddress}` : host;
105
+ return (0, import_node_crypto.createHash)("sha256").update(keySource).digest();
106
+ }
107
+ function encryptConfig(config) {
108
+ const key = getMachineKey();
109
+ const iv = (0, import_node_crypto.randomBytes)(IV_LENGTH);
110
+ const cipher = (0, import_node_crypto.createCipheriv)(ALGORITHM, key, iv);
111
+ const plaintext = JSON.stringify(config);
112
+ let encrypted = cipher.update(plaintext, "utf8", "hex");
113
+ encrypted += cipher.final("hex");
114
+ const authTag = cipher.getAuthTag();
115
+ const result = {
116
+ iv: iv.toString("hex"),
117
+ authTag: authTag.toString("hex"),
118
+ data: encrypted
119
+ };
120
+ return JSON.stringify(result);
121
+ }
122
+ function decryptConfig(encrypted) {
123
+ try {
124
+ const { iv, authTag, data } = JSON.parse(encrypted);
125
+ const key = getMachineKey();
126
+ const decipher = (0, import_node_crypto.createDecipheriv)(
127
+ ALGORITHM,
128
+ key,
129
+ Buffer.from(iv, "hex")
130
+ );
131
+ decipher.setAuthTag(Buffer.from(authTag, "hex"));
132
+ let decrypted = decipher.update(data, "hex", "utf8");
133
+ decrypted += decipher.final("utf8");
134
+ return JSON.parse(decrypted);
135
+ } catch (error) {
136
+ throw new WABAConfigError(
137
+ "Failed to decrypt config file. This may happen if:\n - Config was created on a different machine\n - System hostname or network configuration changed\n - Config file is corrupted"
138
+ );
139
+ }
140
+ }
141
+ function backupConfig() {
142
+ if (!(0, import_node_fs.existsSync)(CONFIG_FILE)) return;
143
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-").replace(/\..+/, "");
144
+ const backupPath = `${CONFIG_FILE}.backup.${timestamp}`;
145
+ try {
146
+ (0, import_node_fs.renameSync)(CONFIG_FILE, backupPath);
147
+ console.log(`\u2713 Old config backed up to: ${backupPath}`);
148
+ } catch (error) {
149
+ console.error(`\u2717 Failed to backup config: ${error}`);
150
+ }
151
+ }
152
+ function loadConfig() {
153
+ if (!(0, import_node_fs.existsSync)(CONFIG_FILE)) {
154
+ return null;
155
+ }
156
+ try {
157
+ const encrypted = (0, import_node_fs.readFileSync)(CONFIG_FILE, "utf8");
158
+ return decryptConfig(encrypted);
159
+ } catch (error) {
160
+ if (error instanceof WABAConfigError) {
161
+ console.error(`\u2717 ${error.message}
162
+ `);
163
+ backupConfig();
164
+ return null;
165
+ }
166
+ throw error;
167
+ }
168
+ }
169
+ function saveConfig(config) {
170
+ try {
171
+ const encrypted = encryptConfig(config);
172
+ (0, import_node_fs.writeFileSync)(CONFIG_FILE, encrypted, { mode: 384 });
173
+ } catch (error) {
174
+ throw new WABAConfigError(
175
+ `Failed to save config: ${error instanceof Error ? error.message : "Unknown error"}`
176
+ );
177
+ }
178
+ }
179
+ function updateConfigField(field, value) {
180
+ const config = loadConfig() || { accessToken: "" };
181
+ config[field] = value;
182
+ saveConfig(config);
183
+ }
184
+ function getConfig() {
185
+ const fileConfig = loadConfig() || {};
186
+ return {
187
+ accessToken: process.env.WABA_TOOLKIT_ACCESS_TOKEN || fileConfig.accessToken,
188
+ defaultPhoneNumberId: process.env.WABA_TOOLKIT_PHONE_NUMBER_ID || fileConfig.defaultPhoneNumberId,
189
+ apiVersion: process.env.WABA_TOOLKIT_API_VERSION || fileConfig.apiVersion || "v22.0",
190
+ wabaId: process.env.WABA_TOOLKIT_WABA_ID || fileConfig.wabaId,
191
+ businessId: process.env.WABA_TOOLKIT_BUSINESS_ID || fileConfig.businessId
192
+ };
193
+ }
194
+ function resolvePhoneNumberId(flagValue) {
195
+ const phoneNumberId = flagValue || process.env.WABA_TOOLKIT_PHONE_NUMBER_ID || loadConfig()?.defaultPhoneNumberId;
196
+ if (!phoneNumberId) {
197
+ throw new WABAConfigError(
198
+ "No phone number ID specified\n\nYou must either:\n 1. Set a default: waba-toolkit config set-default-phone <phone-number-id>\n 2. Use environment variable: WABA_TOOLKIT_PHONE_NUMBER_ID=<id>\n 3. Specify with flag: --bpid <phone-number-id>",
199
+ "phoneNumberId"
200
+ );
201
+ }
202
+ return phoneNumberId;
203
+ }
204
+ function resolveWabaId(flagValue) {
205
+ const wabaId = flagValue || process.env.WABA_TOOLKIT_WABA_ID || loadConfig()?.wabaId;
206
+ if (!wabaId) {
207
+ throw new WABAConfigError(
208
+ "No WABA ID specified\n\nYou must either:\n 1. Set in config: waba-toolkit config set waba-id <id>\n 2. Use environment variable: WABA_TOOLKIT_WABA_ID=<id>\n 3. Specify with flag: --waba-id <id>",
209
+ "wabaId"
210
+ );
211
+ }
212
+ return wabaId;
213
+ }
214
+ function resolveAccessToken() {
215
+ const accessToken = process.env.WABA_TOOLKIT_ACCESS_TOKEN || loadConfig()?.accessToken;
216
+ if (!accessToken) {
217
+ throw new WABAConfigError(
218
+ "Access token not found\n\nYou must either:\n 1. Run: waba-toolkit configure\n 2. Use environment variable: WABA_TOOLKIT_ACCESS_TOKEN=<token>\n 3. Update config: waba-toolkit config set access-token <token>",
219
+ "accessToken"
220
+ );
221
+ }
222
+ return accessToken;
223
+ }
224
+
225
+ // src/cli/utils.ts
226
+ function maskValue(value) {
227
+ if (value.length <= 6) {
228
+ return "***";
229
+ }
230
+ const first = value.slice(0, 3);
231
+ const last = value.slice(-3);
232
+ return `${first}...****...${last}`;
233
+ }
234
+ function formatError(message, payload) {
235
+ let output = `\u2717 ${message}`;
236
+ if (payload) {
237
+ output += "\n\n" + JSON.stringify(payload, null, 2);
238
+ }
239
+ return output;
240
+ }
241
+ function formatSuccess(message) {
242
+ return `\u2713 ${message}`;
243
+ }
244
+ function printJson(data) {
245
+ console.log(JSON.stringify(data, null, 2));
246
+ }
247
+ function handleError(error) {
248
+ if (error instanceof Error) {
249
+ console.error(formatError(error.message));
250
+ } else {
251
+ console.error(formatError("Unknown error occurred"));
252
+ }
253
+ process.exit(1);
254
+ }
255
+
256
+ // src/cli/commands/configure.ts
257
+ async function configure() {
258
+ try {
259
+ console.log("WhatsApp Business API Toolkit - Configuration\n");
260
+ const answers = await import_inquirer.default.prompt([
261
+ {
262
+ type: "input",
263
+ name: "accessToken",
264
+ message: "Access token (required):",
265
+ validate: (input) => {
266
+ if (!input || input.trim().length === 0) {
267
+ return "Access token is required";
268
+ }
269
+ return true;
270
+ }
271
+ },
272
+ {
273
+ type: "input",
274
+ name: "defaultPhoneNumberId",
275
+ message: "Default phone number ID (optional):"
276
+ },
277
+ {
278
+ type: "input",
279
+ name: "apiVersion",
280
+ message: "API version (optional):",
281
+ default: "v22.0"
282
+ },
283
+ {
284
+ type: "input",
285
+ name: "wabaId",
286
+ message: "WhatsApp Business Account ID (optional):"
287
+ },
288
+ {
289
+ type: "input",
290
+ name: "businessId",
291
+ message: "Business Portfolio ID (optional):"
292
+ }
293
+ ]);
294
+ const config = {
295
+ accessToken: answers.accessToken
296
+ };
297
+ if (answers.defaultPhoneNumberId?.trim()) {
298
+ config.defaultPhoneNumberId = answers.defaultPhoneNumberId.trim();
299
+ }
300
+ if (answers.apiVersion?.trim()) {
301
+ config.apiVersion = answers.apiVersion.trim();
302
+ }
303
+ if (answers.wabaId?.trim()) {
304
+ config.wabaId = answers.wabaId.trim();
305
+ }
306
+ if (answers.businessId?.trim()) {
307
+ config.businessId = answers.businessId.trim();
308
+ }
309
+ saveConfig(config);
310
+ console.log("\n" + formatSuccess("Configuration saved successfully"));
311
+ console.log("\nConfig is encrypted and stored at: ~/.waba-toolkit");
312
+ console.log("The config is machine-locked and cannot be moved between systems.");
313
+ } catch (error) {
314
+ handleError(error);
315
+ }
316
+ }
317
+
318
+ // src/cli/commands/config.ts
319
+ function showConfig() {
320
+ try {
321
+ const config = loadConfig();
322
+ if (!config) {
323
+ console.log("No configuration found. Run: waba-toolkit configure");
324
+ return;
325
+ }
326
+ console.log("Current Configuration:\n");
327
+ console.log(`Access Token: ${maskValue(config.accessToken)}`);
328
+ console.log(`Default Phone Number ID: ${config.defaultPhoneNumberId || "(not set)"}`);
329
+ console.log(`API Version: ${config.apiVersion || "v22.0"}`);
330
+ console.log(`WABA ID: ${config.wabaId || "(not set)"}`);
331
+ console.log(`Business ID: ${config.businessId || "(not set)"}`);
332
+ console.log("\nConfig file: ~/.waba-toolkit");
333
+ } catch (error) {
334
+ handleError(error);
335
+ }
336
+ }
337
+ function setDefaultPhone(phoneNumberId) {
338
+ try {
339
+ if (!phoneNumberId || phoneNumberId.trim().length === 0) {
340
+ throw new Error("Phone number ID is required");
341
+ }
342
+ updateConfigField("defaultPhoneNumberId", phoneNumberId.trim());
343
+ console.log(formatSuccess(`Default phone number ID set to: ${phoneNumberId.trim()}`));
344
+ } catch (error) {
345
+ handleError(error);
346
+ }
347
+ }
348
+ function setConfigField(field, value) {
349
+ try {
350
+ if (!value || value.trim().length === 0) {
351
+ throw new Error(`Value for ${field} is required`);
352
+ }
353
+ const validFields = [
354
+ "accessToken",
355
+ "defaultPhoneNumberId",
356
+ "apiVersion",
357
+ "wabaId",
358
+ "businessId"
359
+ ];
360
+ const camelField = field.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
361
+ if (!validFields.includes(camelField)) {
362
+ throw new Error(
363
+ `Invalid field: ${field}
364
+
365
+ Valid fields: ${validFields.join(", ")}`
366
+ );
367
+ }
368
+ updateConfigField(camelField, value.trim());
369
+ console.log(formatSuccess(`${field} updated successfully`));
370
+ } catch (error) {
371
+ handleError(error);
372
+ }
373
+ }
374
+
375
+ // src/api/client.ts
376
+ var DEFAULT_API_VERSION = "v22.0";
377
+ var DEFAULT_BASE_URL = "https://graph.facebook.com";
378
+ var WABAApiClient = class {
379
+ accessToken;
380
+ phoneNumberId;
381
+ apiVersion;
382
+ baseUrl;
383
+ constructor(options) {
384
+ this.accessToken = options.accessToken;
385
+ this.phoneNumberId = options.phoneNumberId;
386
+ this.apiVersion = options.apiVersion ?? DEFAULT_API_VERSION;
387
+ this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
388
+ }
389
+ /**
390
+ * Register phone number with WhatsApp Business API.
391
+ *
392
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
393
+ * @throws {WABASendError} - Registration failure
394
+ * @throws {WABANetworkError} - Network/connection failures
395
+ */
396
+ async registerPhone(pin) {
397
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.phoneNumberId}/register`;
398
+ const payload = {
399
+ messaging_product: "whatsapp",
400
+ pin
401
+ };
402
+ return this.makeRequest(url, "POST", payload);
403
+ }
404
+ /**
405
+ * Deregister phone number from WhatsApp Business API.
406
+ *
407
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
408
+ * @throws {WABASendError} - Deregistration failure
409
+ * @throws {WABANetworkError} - Network/connection failures
410
+ */
411
+ async deregisterPhone() {
412
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.phoneNumberId}/deregister`;
413
+ const payload = {
414
+ messaging_product: "whatsapp"
415
+ };
416
+ return this.makeRequest(url, "POST", payload);
417
+ }
418
+ /**
419
+ * List all phone numbers in a WhatsApp Business Account.
420
+ *
421
+ * @param wabaId - WhatsApp Business Account ID
422
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
423
+ * @throws {WABASendError} - Request failure
424
+ * @throws {WABANetworkError} - Network/connection failures
425
+ */
426
+ async listPhoneNumbers(wabaId) {
427
+ const url = `${this.baseUrl}/${this.apiVersion}/${wabaId}/phone_numbers`;
428
+ let response;
429
+ try {
430
+ response = await fetch(url, {
431
+ method: "GET",
432
+ headers: {
433
+ Authorization: `Bearer ${this.accessToken}`
434
+ }
435
+ });
436
+ } catch (error) {
437
+ throw new WABANetworkError(
438
+ `Failed to make request: ${error instanceof Error ? error.message : "Unknown error"}`,
439
+ error instanceof Error ? error : void 0
440
+ );
441
+ }
442
+ if (response.ok) {
443
+ return await response.json();
444
+ }
445
+ const errorBody = await response.json().catch(() => null);
446
+ if (response.status === 401 || response.status === 403) {
447
+ throw new WABAAuthError(
448
+ `Authentication failed: ${response.status} ${response.statusText}`,
449
+ response.status
450
+ );
451
+ }
452
+ throw new WABASendError(
453
+ `Request failed: ${response.status} ${response.statusText}`,
454
+ response.status,
455
+ errorBody
456
+ );
457
+ }
458
+ /**
459
+ * Send text message.
460
+ *
461
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
462
+ * @throws {WABASendError} - Message sending failure
463
+ * @throws {WABANetworkError} - Network/connection failures
464
+ */
465
+ async sendTextMessage(to, text, options) {
466
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.phoneNumberId}/messages`;
467
+ const payload = {
468
+ messaging_product: "whatsapp",
469
+ to,
470
+ type: "text",
471
+ text: {
472
+ body: text,
473
+ preview_url: options?.previewUrl ?? false
474
+ }
475
+ };
476
+ if (options?.context) {
477
+ payload.context = options.context;
478
+ }
479
+ return this.makeRequest(url, "POST", payload);
480
+ }
481
+ /**
482
+ * Send template message.
483
+ *
484
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
485
+ * @throws {WABASendError} - Message sending failure
486
+ * @throws {WABANetworkError} - Network/connection failures
487
+ */
488
+ async sendTemplateMessage(to, template) {
489
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.phoneNumberId}/messages`;
490
+ const payload = {
491
+ messaging_product: "whatsapp",
492
+ to,
493
+ type: "template",
494
+ template
495
+ };
496
+ return this.makeRequest(url, "POST", payload);
497
+ }
498
+ /**
499
+ * Send generic message (accepts full message payload).
500
+ * Useful for sending any message type (image, video, document, interactive, etc.).
501
+ *
502
+ * @throws {WABAAuthError} - Authentication failure (invalid token)
503
+ * @throws {WABASendError} - Message sending failure
504
+ * @throws {WABANetworkError} - Network/connection failures
505
+ */
506
+ async sendMessage(payload) {
507
+ const url = `${this.baseUrl}/${this.apiVersion}/${this.phoneNumberId}/messages`;
508
+ return this.makeRequest(url, "POST", payload);
509
+ }
510
+ async makeRequest(url, method, body) {
511
+ let response;
512
+ try {
513
+ response = await fetch(url, {
514
+ method,
515
+ headers: {
516
+ Authorization: `Bearer ${this.accessToken}`,
517
+ "Content-Type": "application/json"
518
+ },
519
+ body: JSON.stringify(body)
520
+ });
521
+ } catch (error) {
522
+ throw new WABANetworkError(
523
+ `Failed to make request: ${error instanceof Error ? error.message : "Unknown error"}`,
524
+ error instanceof Error ? error : void 0
525
+ );
526
+ }
527
+ if (response.ok) {
528
+ return await response.json();
529
+ }
530
+ const errorBody = await response.json().catch(() => null);
531
+ if (response.status === 401 || response.status === 403) {
532
+ throw new WABAAuthError(
533
+ `Authentication failed: ${response.status} ${response.statusText}`,
534
+ response.status
535
+ );
536
+ }
537
+ throw new WABASendError(
538
+ `Request failed: ${response.status} ${response.statusText}`,
539
+ response.status,
540
+ errorBody
541
+ );
542
+ }
543
+ };
544
+
545
+ // src/cli/commands/register.ts
546
+ async function registerPhone(options) {
547
+ try {
548
+ const accessToken = resolveAccessToken();
549
+ const phoneNumberId = resolvePhoneNumberId(options.bpid || options.phoneNumberId);
550
+ const config = getConfig();
551
+ const client = new WABAApiClient({
552
+ accessToken,
553
+ phoneNumberId,
554
+ apiVersion: config.apiVersion
555
+ });
556
+ const response = await client.registerPhone(options.pin);
557
+ printJson(response);
558
+ } catch (error) {
559
+ if (error instanceof WABASendError) {
560
+ console.error(formatError("Phone registration failed", error.errorPayload));
561
+ } else {
562
+ handleError(error);
563
+ }
564
+ process.exit(1);
565
+ }
566
+ }
567
+ async function deregisterPhone(options) {
568
+ try {
569
+ const accessToken = resolveAccessToken();
570
+ const phoneNumberId = resolvePhoneNumberId(options.bpid || options.phoneNumberId);
571
+ const config = getConfig();
572
+ const client = new WABAApiClient({
573
+ accessToken,
574
+ phoneNumberId,
575
+ apiVersion: config.apiVersion
576
+ });
577
+ const response = await client.deregisterPhone();
578
+ printJson(response);
579
+ } catch (error) {
580
+ if (error instanceof WABASendError) {
581
+ console.error(formatError("Phone deregistration failed", error.errorPayload));
582
+ } else {
583
+ handleError(error);
584
+ }
585
+ process.exit(1);
586
+ }
587
+ }
588
+
589
+ // src/cli/commands/send.ts
590
+ var import_node_fs2 = require("fs");
591
+ async function sendText(options) {
592
+ try {
593
+ const accessToken = resolveAccessToken();
594
+ const phoneNumberId = resolvePhoneNumberId(options.bpid || options.phoneNumberId);
595
+ const config = getConfig();
596
+ const client = new WABAApiClient({
597
+ accessToken,
598
+ phoneNumberId,
599
+ apiVersion: config.apiVersion
600
+ });
601
+ const context = options.replyTo ? { message_id: options.replyTo } : void 0;
602
+ const response = await client.sendTextMessage(options.to, options.message, {
603
+ previewUrl: options.previewUrl,
604
+ context
605
+ });
606
+ printJson(response);
607
+ } catch (error) {
608
+ if (error instanceof WABASendError) {
609
+ console.error(formatError("Message failed to send", error.errorPayload));
610
+ } else {
611
+ handleError(error);
612
+ }
613
+ process.exit(1);
614
+ }
615
+ }
616
+ async function sendTemplate(options) {
617
+ try {
618
+ const accessToken = resolveAccessToken();
619
+ const phoneNumberId = resolvePhoneNumberId(options.bpid || options.phoneNumberId);
620
+ const config = getConfig();
621
+ let templateData;
622
+ try {
623
+ const fileContent = (0, import_node_fs2.readFileSync)(options.file, "utf8");
624
+ templateData = JSON.parse(fileContent);
625
+ } catch (error) {
626
+ throw new Error(
627
+ `Failed to read template file: ${error instanceof Error ? error.message : "Unknown error"}`
628
+ );
629
+ }
630
+ const client = new WABAApiClient({
631
+ accessToken,
632
+ phoneNumberId,
633
+ apiVersion: config.apiVersion
634
+ });
635
+ const response = await client.sendTemplateMessage(options.to, templateData);
636
+ printJson(response);
637
+ } catch (error) {
638
+ if (error instanceof WABASendError) {
639
+ console.error(formatError("Template message failed to send", error.errorPayload));
640
+ } else {
641
+ handleError(error);
642
+ }
643
+ process.exit(1);
644
+ }
645
+ }
646
+ async function sendFile(options) {
647
+ try {
648
+ const accessToken = resolveAccessToken();
649
+ const phoneNumberId = resolvePhoneNumberId(options.bpid || options.phoneNumberId);
650
+ const config = getConfig();
651
+ let payload;
652
+ try {
653
+ const fileContent = (0, import_node_fs2.readFileSync)(options.payload, "utf8");
654
+ payload = JSON.parse(fileContent);
655
+ } catch (error) {
656
+ throw new Error(
657
+ `Failed to read payload file: ${error instanceof Error ? error.message : "Unknown error"}`
658
+ );
659
+ }
660
+ const client = new WABAApiClient({
661
+ accessToken,
662
+ phoneNumberId,
663
+ apiVersion: config.apiVersion
664
+ });
665
+ const response = await client.sendMessage(payload);
666
+ printJson(response);
667
+ } catch (error) {
668
+ if (error instanceof WABASendError) {
669
+ console.error(formatError("Message failed to send", error.errorPayload));
670
+ } else {
671
+ handleError(error);
672
+ }
673
+ process.exit(1);
674
+ }
675
+ }
676
+
677
+ // src/cli/commands/list-phones.ts
678
+ async function listPhones(options) {
679
+ try {
680
+ const accessToken = resolveAccessToken();
681
+ const wabaId = resolveWabaId(options.wabaId);
682
+ const config = getConfig();
683
+ const client = new WABAApiClient({
684
+ accessToken,
685
+ phoneNumberId: "",
686
+ // Not needed for listPhoneNumbers
687
+ apiVersion: config.apiVersion
688
+ });
689
+ const response = await client.listPhoneNumbers(wabaId);
690
+ printJson(response);
691
+ } catch (error) {
692
+ if (error instanceof WABASendError) {
693
+ console.error(formatError("Failed to list phone numbers", error.errorPayload));
694
+ } else {
695
+ handleError(error);
696
+ }
697
+ process.exit(1);
698
+ }
699
+ }
700
+
701
+ // src/cli/index.ts
702
+ var packageJson = JSON.parse(
703
+ (0, import_node_fs3.readFileSync)((0, import_node_path2.join)(__dirname, "../package.json"), "utf8")
704
+ );
705
+ var program = new import_commander.Command();
706
+ program.name("waba-toolkit").description("WhatsApp Business API Toolkit - CLI for webhooks, media, and messaging").version(packageJson.version).addHelpText("after", `
707
+ Examples:
708
+ $ waba-toolkit configure # Set up configuration interactively
709
+ $ waba-toolkit config show # View current configuration
710
+ $ waba-toolkit list-phones --waba-id 123 # List phone numbers
711
+ $ waba-toolkit send text --to 1234567890 --message "Hello"
712
+ $ waba-toolkit register --pin 123456
713
+
714
+ Environment Variables:
715
+ WABA_TOOLKIT_ACCESS_TOKEN Access token for WhatsApp API
716
+ WABA_TOOLKIT_PHONE_NUMBER_ID Default phone number ID
717
+ WABA_TOOLKIT_WABA_ID WhatsApp Business Account ID
718
+ WABA_TOOLKIT_API_VERSION API version (default: v22.0)
719
+
720
+ Configuration:
721
+ Config is stored at ~/.waba-toolkit (encrypted, machine-locked)
722
+ Priority: CLI flags > environment variables > config file
723
+ `);
724
+ program.command("configure").description("Interactive configuration wizard").addHelpText("after", `
725
+ Examples:
726
+ $ waba-toolkit configure
727
+
728
+ This will prompt you for:
729
+ - Access token (required)
730
+ - Default phone number ID (optional)
731
+ - API version (optional, default: v22.0)
732
+ - WABA ID (optional)
733
+ - Business ID (optional)
734
+ `).action(configure);
735
+ var configCommand = program.command("config").description("Manage configuration");
736
+ configCommand.command("show").description("Show current configuration (sensitive values masked)").addHelpText("after", `
737
+ Examples:
738
+ $ waba-toolkit config show
739
+ `).action(showConfig);
740
+ configCommand.command("set-default-phone <phone-number-id>").description("Set default phone number ID").addHelpText("after", `
741
+ Examples:
742
+ $ waba-toolkit config set-default-phone 1234567890
743
+ `).action(setDefaultPhone);
744
+ configCommand.command("set <field> <value>").description("Update specific configuration value").addHelpText("after", `
745
+ Examples:
746
+ $ waba-toolkit config set access-token EAABsbCS...
747
+ $ waba-toolkit config set waba-id 1234567890
748
+ $ waba-toolkit config set api-version v22.0
749
+
750
+ Valid fields:
751
+ - access-token
752
+ - default-phone-number-id
753
+ - api-version
754
+ - waba-id
755
+ - business-id
756
+ `).action(setConfigField);
757
+ program.command("register").description("Register phone number with WhatsApp Business API").option("--bpid <phone-number-id>", "Phone number ID (overrides default)").option("--phone-number-id <id>", "Phone number ID (alias for --bpid)").requiredOption("--pin <pin>", "Six-digit PIN for registration").addHelpText("after", `
758
+ Examples:
759
+ $ waba-toolkit register --pin 123456
760
+ $ waba-toolkit register --bpid 1234567890 --pin 123456
761
+ `).action(async (options) => {
762
+ await registerPhone({
763
+ bpid: options.bpid,
764
+ phoneNumberId: options.phoneNumberId,
765
+ pin: options.pin
766
+ });
767
+ });
768
+ program.command("deregister").description("Deregister phone number from WhatsApp Business API").option("--bpid <phone-number-id>", "Phone number ID (overrides default)").option("--phone-number-id <id>", "Phone number ID (alias for --bpid)").addHelpText("after", `
769
+ Examples:
770
+ $ waba-toolkit deregister
771
+ $ waba-toolkit deregister --bpid 1234567890
772
+ `).action(async (options) => {
773
+ await deregisterPhone({
774
+ bpid: options.bpid,
775
+ phoneNumberId: options.phoneNumberId
776
+ });
777
+ });
778
+ program.command("list-phones").description("List all phone numbers for a WhatsApp Business Account").option("--waba-id <id>", "WhatsApp Business Account ID (overrides default)").addHelpText("after", `
779
+ Examples:
780
+ $ waba-toolkit list-phones --waba-id 1234567890
781
+ $ WABA_TOOLKIT_WABA_ID=1234567890 waba-toolkit list-phones
782
+
783
+ Output:
784
+ Returns JSON with phone numbers, quality ratings, and verified names
785
+ `).action(async (options) => {
786
+ await listPhones({
787
+ wabaId: options.wabaId
788
+ });
789
+ });
790
+ var sendCommand = program.command("send").description("Send messages via WhatsApp Business API");
791
+ sendCommand.command("text").description("Send text message").option("--bpid <phone-number-id>", "Phone number ID (overrides default)").option("--phone-number-id <id>", "Phone number ID (alias for --bpid)").requiredOption("--to <recipient>", "Recipient phone number").requiredOption("--message <text>", "Message text").option("--preview-url", "Enable URL preview").option("--reply-to <message-id>", "Reply to specific message ID").addHelpText("after", `
792
+ Examples:
793
+ $ waba-toolkit send text --to 1234567890 --message "Hello World"
794
+ $ waba-toolkit send text --to 1234567890 --message "Check this out: https://example.com" --preview-url
795
+ $ waba-toolkit send text --to 1234567890 --message "Reply" --reply-to wamid.abc123
796
+ `).action(async (options) => {
797
+ await sendText({
798
+ bpid: options.bpid,
799
+ phoneNumberId: options.phoneNumberId,
800
+ to: options.to,
801
+ message: options.message,
802
+ previewUrl: options.previewUrl,
803
+ replyTo: options.replyTo
804
+ });
805
+ });
806
+ sendCommand.command("template").description("Send template message from JSON file").option("--bpid <phone-number-id>", "Phone number ID (overrides default)").option("--phone-number-id <id>", "Phone number ID (alias for --bpid)").requiredOption("--to <recipient>", "Recipient phone number").requiredOption("--file <path>", "Path to template JSON file").addHelpText("after", `
807
+ Examples:
808
+ $ waba-toolkit send template --to 1234567890 --file template.json
809
+
810
+ Template JSON format:
811
+ {
812
+ "name": "hello_world",
813
+ "language": {
814
+ "code": "en_US"
815
+ },
816
+ "components": [
817
+ {
818
+ "type": "body",
819
+ "parameters": [
820
+ { "type": "text", "text": "John" }
821
+ ]
822
+ }
823
+ ]
824
+ }
825
+ `).action(async (options) => {
826
+ await sendTemplate({
827
+ bpid: options.bpid,
828
+ phoneNumberId: options.phoneNumberId,
829
+ to: options.to,
830
+ file: options.file
831
+ });
832
+ });
833
+ sendCommand.command("file").description("Send message from JSON payload file").option("--bpid <phone-number-id>", "Phone number ID (overrides default)").option("--phone-number-id <id>", "Phone number ID (alias for --bpid)").requiredOption("--payload <path>", "Path to message payload JSON file").addHelpText("after", `
834
+ Examples:
835
+ $ waba-toolkit send file --payload message.json
836
+
837
+ Payload JSON format (text message):
838
+ {
839
+ "messaging_product": "whatsapp",
840
+ "to": "1234567890",
841
+ "type": "text",
842
+ "text": {
843
+ "body": "Hello World"
844
+ }
845
+ }
846
+
847
+ Payload JSON format (image message):
848
+ {
849
+ "messaging_product": "whatsapp",
850
+ "to": "1234567890",
851
+ "type": "image",
852
+ "image": {
853
+ "link": "https://example.com/image.jpg"
854
+ }
855
+ }
856
+ `).action(async (options) => {
857
+ await sendFile({
858
+ bpid: options.bpid,
859
+ phoneNumberId: options.phoneNumberId,
860
+ payload: options.payload
861
+ });
862
+ });
863
+ program.parse();