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.
- package/ARCHITECTURE.md +783 -0
- package/README.md +238 -242
- package/dist/index.cli.js +863 -0
- package/dist/index.d.mts +180 -1
- package/dist/index.d.ts +180 -1
- package/dist/index.js +203 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +199 -0
- package/dist/index.mjs.map +1 -1
- package/docs/CLI.md +445 -0
- package/docs/MEDIA.md +576 -0
- package/docs/SENDING.md +635 -0
- package/docs/TROUBLESHOOTING.md +622 -0
- package/docs/WEBHOOKS.md +515 -0
- package/package.json +13 -3
|
@@ -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();
|