@skoolite/notify-hub 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/README.md +452 -0
- package/dist/index.d.mts +1080 -0
- package/dist/index.d.ts +1080 -0
- package/dist/index.js +1692 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1646 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1692 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var libphonenumberJs = require('libphonenumber-js');
|
|
4
|
+
var crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
function _interopNamespace(e) {
|
|
7
|
+
if (e && e.__esModule) return e;
|
|
8
|
+
var n = Object.create(null);
|
|
9
|
+
if (e) {
|
|
10
|
+
Object.keys(e).forEach(function (k) {
|
|
11
|
+
if (k !== 'default') {
|
|
12
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
13
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
14
|
+
enumerable: true,
|
|
15
|
+
get: function () { return e[k]; }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
n.default = e;
|
|
21
|
+
return Object.freeze(n);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
25
|
+
|
|
26
|
+
// src/utils/retry.ts
|
|
27
|
+
var DEFAULT_RETRY_CONFIG = {
|
|
28
|
+
maxAttempts: 3,
|
|
29
|
+
backoffMs: 1e3,
|
|
30
|
+
backoffMultiplier: 2,
|
|
31
|
+
maxBackoffMs: 3e4,
|
|
32
|
+
jitter: true
|
|
33
|
+
};
|
|
34
|
+
var RETRIABLE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
35
|
+
"ETIMEDOUT",
|
|
36
|
+
"ECONNRESET",
|
|
37
|
+
"ECONNREFUSED",
|
|
38
|
+
"ENOTFOUND",
|
|
39
|
+
"EPIPE",
|
|
40
|
+
"RATE_LIMIT",
|
|
41
|
+
"SERVICE_UNAVAILABLE",
|
|
42
|
+
"GATEWAY_TIMEOUT",
|
|
43
|
+
"429",
|
|
44
|
+
// Too Many Requests
|
|
45
|
+
"500",
|
|
46
|
+
// Internal Server Error
|
|
47
|
+
"502",
|
|
48
|
+
// Bad Gateway
|
|
49
|
+
"503",
|
|
50
|
+
// Service Unavailable
|
|
51
|
+
"504"
|
|
52
|
+
// Gateway Timeout
|
|
53
|
+
]);
|
|
54
|
+
function isRetriableError(error) {
|
|
55
|
+
if (!error) return false;
|
|
56
|
+
if (typeof error === "object" && "retriable" in error) {
|
|
57
|
+
return error.retriable;
|
|
58
|
+
}
|
|
59
|
+
const errorCode = getErrorCode(error);
|
|
60
|
+
if (errorCode && RETRIABLE_ERROR_CODES.has(errorCode)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
const statusCode = getStatusCode(error);
|
|
64
|
+
if (statusCode && RETRIABLE_ERROR_CODES.has(String(statusCode))) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
function getErrorCode(error) {
|
|
70
|
+
if (typeof error === "object" && error !== null) {
|
|
71
|
+
const err = error;
|
|
72
|
+
if (typeof err["code"] === "string") {
|
|
73
|
+
return err["code"];
|
|
74
|
+
}
|
|
75
|
+
if (typeof err["errorCode"] === "string") {
|
|
76
|
+
return err["errorCode"];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return void 0;
|
|
80
|
+
}
|
|
81
|
+
function getStatusCode(error) {
|
|
82
|
+
if (typeof error === "object" && error !== null) {
|
|
83
|
+
const err = error;
|
|
84
|
+
if (typeof err["status"] === "number") {
|
|
85
|
+
return err["status"];
|
|
86
|
+
}
|
|
87
|
+
if (typeof err["statusCode"] === "number") {
|
|
88
|
+
return err["statusCode"];
|
|
89
|
+
}
|
|
90
|
+
if (typeof err["response"] === "object" && err["response"] !== null && typeof err["response"]["status"] === "number") {
|
|
91
|
+
return err["response"]["status"];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return void 0;
|
|
95
|
+
}
|
|
96
|
+
function calculateBackoff(attempt, config) {
|
|
97
|
+
const exponentialDelay = config.backoffMs * Math.pow(config.backoffMultiplier, attempt - 1);
|
|
98
|
+
const maxDelay = config.maxBackoffMs ?? 3e4;
|
|
99
|
+
const baseDelay = Math.min(exponentialDelay, maxDelay);
|
|
100
|
+
if (config.jitter) {
|
|
101
|
+
const jitter = Math.random() * baseDelay * 0.5;
|
|
102
|
+
return Math.floor(baseDelay + jitter);
|
|
103
|
+
}
|
|
104
|
+
return Math.floor(baseDelay);
|
|
105
|
+
}
|
|
106
|
+
function sleep(ms) {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
|
109
|
+
async function withRetry(fn, config = DEFAULT_RETRY_CONFIG, shouldRetry = isRetriableError) {
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
let lastError;
|
|
112
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const value = await fn();
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
value,
|
|
118
|
+
attempts: attempt,
|
|
119
|
+
totalDurationMs: Date.now() - startTime
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
123
|
+
const isLastAttempt = attempt >= config.maxAttempts;
|
|
124
|
+
const canRetry = !isLastAttempt && shouldRetry(error);
|
|
125
|
+
if (!canRetry) {
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: lastError,
|
|
129
|
+
attempts: attempt,
|
|
130
|
+
totalDurationMs: Date.now() - startTime
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const backoffMs = calculateBackoff(attempt, config);
|
|
134
|
+
await sleep(backoffMs);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: lastError,
|
|
140
|
+
attempts: config.maxAttempts,
|
|
141
|
+
totalDurationMs: Date.now() - startTime
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function createNotifyError(error, defaultCode = "UNKNOWN_ERROR") {
|
|
145
|
+
if (typeof error === "object" && error !== null) {
|
|
146
|
+
const err = error;
|
|
147
|
+
return {
|
|
148
|
+
code: typeof err["code"] === "string" ? err["code"] : defaultCode,
|
|
149
|
+
message: err["message"] ?? String(error),
|
|
150
|
+
retriable: isRetriableError(error),
|
|
151
|
+
originalError: error
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
code: defaultCode,
|
|
156
|
+
message: String(error),
|
|
157
|
+
retriable: false,
|
|
158
|
+
originalError: error
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function validatePhoneNumber(phone, defaultCountry = "PK") {
|
|
162
|
+
try {
|
|
163
|
+
const cleaned = cleanPhoneNumber(phone);
|
|
164
|
+
if (!libphonenumberJs.isValidPhoneNumber(cleaned, defaultCountry)) {
|
|
165
|
+
return {
|
|
166
|
+
isValid: false,
|
|
167
|
+
error: "Invalid phone number format"
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
const parsed = libphonenumberJs.parsePhoneNumber(cleaned, defaultCountry);
|
|
171
|
+
return {
|
|
172
|
+
isValid: true,
|
|
173
|
+
e164: parsed.format("E.164"),
|
|
174
|
+
countryCode: parsed.country,
|
|
175
|
+
nationalNumber: parsed.nationalNumber
|
|
176
|
+
};
|
|
177
|
+
} catch (error) {
|
|
178
|
+
return {
|
|
179
|
+
isValid: false,
|
|
180
|
+
error: error instanceof Error ? error.message : "Failed to parse phone number"
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function cleanPhoneNumber(phone) {
|
|
185
|
+
let cleaned = phone.trim();
|
|
186
|
+
const hasPlus = cleaned.startsWith("+");
|
|
187
|
+
cleaned = cleaned.replace(/[\s\-().]/g, "");
|
|
188
|
+
if (hasPlus && !cleaned.startsWith("+")) {
|
|
189
|
+
cleaned = "+" + cleaned;
|
|
190
|
+
}
|
|
191
|
+
return cleaned;
|
|
192
|
+
}
|
|
193
|
+
function toE164(phone, defaultCountry = "PK") {
|
|
194
|
+
const result = validatePhoneNumber(phone, defaultCountry);
|
|
195
|
+
return result.e164 ?? null;
|
|
196
|
+
}
|
|
197
|
+
function isCountry(phone, countryCode) {
|
|
198
|
+
try {
|
|
199
|
+
const parsed = libphonenumberJs.parsePhoneNumber(phone);
|
|
200
|
+
return parsed.country === countryCode;
|
|
201
|
+
} catch {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function getCountryCode(phone) {
|
|
206
|
+
try {
|
|
207
|
+
const parsed = libphonenumberJs.parsePhoneNumber(phone);
|
|
208
|
+
return parsed.country ?? null;
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function getCallingCode(phone) {
|
|
214
|
+
try {
|
|
215
|
+
const parsed = libphonenumberJs.parsePhoneNumber(phone);
|
|
216
|
+
return parsed.countryCallingCode ? `+${parsed.countryCallingCode}` : null;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function isPakistanMobile(phone) {
|
|
222
|
+
try {
|
|
223
|
+
const parsed = libphonenumberJs.parsePhoneNumber(phone, "PK");
|
|
224
|
+
if (parsed.country !== "PK") return false;
|
|
225
|
+
const national = parsed.nationalNumber;
|
|
226
|
+
return national.startsWith("3");
|
|
227
|
+
} catch {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function getPakistanNetwork(phone) {
|
|
232
|
+
try {
|
|
233
|
+
const parsed = libphonenumberJs.parsePhoneNumber(phone, "PK");
|
|
234
|
+
if (parsed.country !== "PK") return null;
|
|
235
|
+
const national = parsed.nationalNumber;
|
|
236
|
+
if (!national.startsWith("3")) return null;
|
|
237
|
+
const prefix = national.substring(0, 3);
|
|
238
|
+
if (["300", "301", "302", "303", "304", "305", "306", "307", "308", "309"].includes(prefix) || ["310", "311", "312", "313", "314", "315", "316", "317", "318", "319"].includes(prefix) || ["320", "321", "322", "323", "324", "325", "326", "327", "328", "329"].includes(prefix) || ["330", "331", "332", "333", "334", "335", "336", "337", "338", "339"].includes(prefix)) {
|
|
239
|
+
return "jazz";
|
|
240
|
+
}
|
|
241
|
+
if (["340", "341", "342", "343", "344", "345", "346", "347", "348", "349"].includes(prefix) || ["350", "351", "352", "353", "354", "355", "356", "357", "358", "359"].includes(prefix)) {
|
|
242
|
+
return "telenor";
|
|
243
|
+
}
|
|
244
|
+
if (["310", "311", "312", "313", "314", "315", "316", "317", "318", "319"].includes(prefix)) {
|
|
245
|
+
return "zong";
|
|
246
|
+
}
|
|
247
|
+
if (["330", "331", "332", "333"].includes(prefix)) {
|
|
248
|
+
return "ufone";
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
} catch {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function routeByPrefix(phone, routing) {
|
|
256
|
+
const prefixes = Object.keys(routing).filter((k) => k !== "*").sort((a, b) => b.length - a.length);
|
|
257
|
+
for (const prefix of prefixes) {
|
|
258
|
+
if (phone.startsWith(prefix)) {
|
|
259
|
+
return routing[prefix];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return routing["*"] ?? "primary";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// src/providers/sms/twilio.provider.ts
|
|
266
|
+
function mapTwilioStatus(twilioStatus) {
|
|
267
|
+
const statusMap = {
|
|
268
|
+
queued: "queued",
|
|
269
|
+
sending: "queued",
|
|
270
|
+
sent: "sent",
|
|
271
|
+
delivered: "delivered",
|
|
272
|
+
undelivered: "undelivered",
|
|
273
|
+
failed: "failed",
|
|
274
|
+
read: "read",
|
|
275
|
+
accepted: "queued"
|
|
276
|
+
};
|
|
277
|
+
return statusMap[twilioStatus] ?? "unknown";
|
|
278
|
+
}
|
|
279
|
+
var TwilioSmsProvider = class {
|
|
280
|
+
name = "twilio";
|
|
281
|
+
client = null;
|
|
282
|
+
config;
|
|
283
|
+
logger;
|
|
284
|
+
retryConfig;
|
|
285
|
+
constructor(config, options) {
|
|
286
|
+
this.config = config;
|
|
287
|
+
this.logger = options?.logger;
|
|
288
|
+
this.retryConfig = options?.retryConfig ?? DEFAULT_RETRY_CONFIG;
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Initialize the Twilio client
|
|
292
|
+
*/
|
|
293
|
+
async initialize() {
|
|
294
|
+
if (this.client) return;
|
|
295
|
+
const twilio = await import('twilio');
|
|
296
|
+
this.client = twilio.default(this.config.accountSid, this.config.authToken);
|
|
297
|
+
this.logger?.debug("TwilioSmsProvider initialized");
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Ensure client is initialized
|
|
301
|
+
*/
|
|
302
|
+
async ensureClient() {
|
|
303
|
+
if (!this.client) {
|
|
304
|
+
await this.initialize();
|
|
305
|
+
}
|
|
306
|
+
if (!this.client) {
|
|
307
|
+
throw new Error("Twilio client not initialized");
|
|
308
|
+
}
|
|
309
|
+
return this.client;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Send an SMS message
|
|
313
|
+
*/
|
|
314
|
+
async send(to, message, options) {
|
|
315
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
316
|
+
const from = options?.from ?? this.config.fromNumber;
|
|
317
|
+
const e164To = toE164(to);
|
|
318
|
+
if (!e164To) {
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
channel: "sms",
|
|
322
|
+
status: "failed",
|
|
323
|
+
provider: this.name,
|
|
324
|
+
error: {
|
|
325
|
+
code: "INVALID_PHONE_NUMBER",
|
|
326
|
+
message: `Invalid phone number: ${to}`,
|
|
327
|
+
retriable: false
|
|
328
|
+
},
|
|
329
|
+
timestamp,
|
|
330
|
+
metadata: options?.metadata
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
this.logger?.debug(`Sending SMS to ${e164To}`);
|
|
334
|
+
const result = await withRetry(
|
|
335
|
+
async () => {
|
|
336
|
+
const client = await this.ensureClient();
|
|
337
|
+
const messageParams = {
|
|
338
|
+
to: e164To,
|
|
339
|
+
body: message
|
|
340
|
+
};
|
|
341
|
+
if (this.config.messagingServiceSid) {
|
|
342
|
+
messageParams.messagingServiceSid = this.config.messagingServiceSid;
|
|
343
|
+
} else {
|
|
344
|
+
messageParams.from = from;
|
|
345
|
+
}
|
|
346
|
+
return client.messages.create(messageParams);
|
|
347
|
+
},
|
|
348
|
+
this.retryConfig
|
|
349
|
+
);
|
|
350
|
+
if (result.success && result.value) {
|
|
351
|
+
const twilioMessage = result.value;
|
|
352
|
+
this.logger?.info(`SMS sent successfully: ${twilioMessage.sid}`);
|
|
353
|
+
return {
|
|
354
|
+
success: true,
|
|
355
|
+
messageId: twilioMessage.sid,
|
|
356
|
+
channel: "sms",
|
|
357
|
+
status: mapTwilioStatus(twilioMessage.status),
|
|
358
|
+
provider: this.name,
|
|
359
|
+
cost: twilioMessage.price ? {
|
|
360
|
+
amount: Math.abs(parseFloat(twilioMessage.price)),
|
|
361
|
+
currency: twilioMessage.priceUnit ?? "USD"
|
|
362
|
+
} : void 0,
|
|
363
|
+
timestamp,
|
|
364
|
+
metadata: options?.metadata
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
this.logger?.error(`SMS send failed: ${result.error?.message}`);
|
|
368
|
+
return {
|
|
369
|
+
success: false,
|
|
370
|
+
channel: "sms",
|
|
371
|
+
status: "failed",
|
|
372
|
+
provider: this.name,
|
|
373
|
+
error: createNotifyError(result.error, "TWILIO_ERROR"),
|
|
374
|
+
timestamp,
|
|
375
|
+
metadata: options?.metadata
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Get delivery status of a message
|
|
380
|
+
*/
|
|
381
|
+
async getStatus(messageId) {
|
|
382
|
+
try {
|
|
383
|
+
const client = await this.ensureClient();
|
|
384
|
+
const message = await client.messages(messageId).fetch();
|
|
385
|
+
return mapTwilioStatus(message.status);
|
|
386
|
+
} catch (error) {
|
|
387
|
+
this.logger?.error(`Failed to get status for ${messageId}: ${error}`);
|
|
388
|
+
return "unknown";
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Send bulk SMS messages
|
|
393
|
+
*/
|
|
394
|
+
async sendBulk(messages, options) {
|
|
395
|
+
const startTime = Date.now();
|
|
396
|
+
const results = [];
|
|
397
|
+
let sent = 0;
|
|
398
|
+
let failed = 0;
|
|
399
|
+
let totalCost = 0;
|
|
400
|
+
const concurrency = options?.concurrency ?? 10;
|
|
401
|
+
const messagesPerSecond = options?.rateLimit?.messagesPerSecond ?? 10;
|
|
402
|
+
const delayBetweenBatches = Math.ceil(1e3 / messagesPerSecond) * concurrency;
|
|
403
|
+
for (let i = 0; i < messages.length; i += concurrency) {
|
|
404
|
+
const batch = messages.slice(i, i + concurrency);
|
|
405
|
+
const batchResults = await Promise.all(
|
|
406
|
+
batch.map(
|
|
407
|
+
(msg) => this.send(msg.to, msg.message, { metadata: msg.metadata })
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
for (const result of batchResults) {
|
|
411
|
+
results.push(result);
|
|
412
|
+
if (result.success) {
|
|
413
|
+
sent++;
|
|
414
|
+
if (result.cost) {
|
|
415
|
+
totalCost += result.cost.amount;
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
failed++;
|
|
419
|
+
if (options?.stopOnError) {
|
|
420
|
+
return {
|
|
421
|
+
results,
|
|
422
|
+
summary: {
|
|
423
|
+
total: messages.length,
|
|
424
|
+
sent,
|
|
425
|
+
failed,
|
|
426
|
+
cost: { amount: totalCost, currency: "USD" },
|
|
427
|
+
durationMs: Date.now() - startTime
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
options?.onProgress?.(sent + failed, messages.length);
|
|
434
|
+
if (i + concurrency < messages.length) {
|
|
435
|
+
await new Promise((resolve) => setTimeout(resolve, delayBetweenBatches));
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
results,
|
|
440
|
+
summary: {
|
|
441
|
+
total: messages.length,
|
|
442
|
+
sent,
|
|
443
|
+
failed,
|
|
444
|
+
cost: { amount: totalCost, currency: "USD" },
|
|
445
|
+
durationMs: Date.now() - startTime
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Cleanup resources
|
|
451
|
+
*/
|
|
452
|
+
async destroy() {
|
|
453
|
+
this.client = null;
|
|
454
|
+
this.logger?.debug("TwilioSmsProvider destroyed");
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
// src/providers/sms/local/base.provider.ts
|
|
459
|
+
var BaseLocalSmsProvider = class {
|
|
460
|
+
config;
|
|
461
|
+
logger;
|
|
462
|
+
retryConfig;
|
|
463
|
+
constructor(config, options) {
|
|
464
|
+
this.config = config;
|
|
465
|
+
this.logger = options?.logger;
|
|
466
|
+
this.retryConfig = options?.retryConfig ?? DEFAULT_RETRY_CONFIG;
|
|
467
|
+
}
|
|
468
|
+
/**
|
|
469
|
+
* Initialize the provider
|
|
470
|
+
*/
|
|
471
|
+
async initialize() {
|
|
472
|
+
if (!this.config.apiKey) {
|
|
473
|
+
throw new Error(`${this.name}: apiKey is required`);
|
|
474
|
+
}
|
|
475
|
+
if (!this.config.senderId) {
|
|
476
|
+
throw new Error(`${this.name}: senderId is required`);
|
|
477
|
+
}
|
|
478
|
+
this.logger?.debug(`${this.name} provider initialized`);
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Format phone number for local provider
|
|
482
|
+
* Pakistan providers typically want: 923001234567 (no + prefix)
|
|
483
|
+
*/
|
|
484
|
+
formatPhoneNumber(phone) {
|
|
485
|
+
const e164 = toE164(phone, "PK");
|
|
486
|
+
if (!e164) return null;
|
|
487
|
+
return e164.replace("+", "");
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Send an SMS message
|
|
491
|
+
*/
|
|
492
|
+
async send(to, message, options) {
|
|
493
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
494
|
+
const formattedTo = this.formatPhoneNumber(to);
|
|
495
|
+
if (!formattedTo) {
|
|
496
|
+
return {
|
|
497
|
+
success: false,
|
|
498
|
+
channel: "sms",
|
|
499
|
+
status: "failed",
|
|
500
|
+
provider: this.name,
|
|
501
|
+
error: {
|
|
502
|
+
code: "INVALID_PHONE_NUMBER",
|
|
503
|
+
message: `Invalid phone number: ${to}`,
|
|
504
|
+
retriable: false
|
|
505
|
+
},
|
|
506
|
+
timestamp,
|
|
507
|
+
metadata: options?.metadata
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
if (!formattedTo.startsWith("92")) {
|
|
511
|
+
return {
|
|
512
|
+
success: false,
|
|
513
|
+
channel: "sms",
|
|
514
|
+
status: "failed",
|
|
515
|
+
provider: this.name,
|
|
516
|
+
error: {
|
|
517
|
+
code: "UNSUPPORTED_DESTINATION",
|
|
518
|
+
message: "Local SMS providers only support Pakistan numbers",
|
|
519
|
+
retriable: false
|
|
520
|
+
},
|
|
521
|
+
timestamp,
|
|
522
|
+
metadata: options?.metadata
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
this.logger?.debug(`Sending SMS via ${this.name} to ${formattedTo}`);
|
|
526
|
+
const result = await withRetry(
|
|
527
|
+
async () => this.makeRequest(formattedTo, message),
|
|
528
|
+
this.retryConfig
|
|
529
|
+
);
|
|
530
|
+
if (result.success && result.value?.success) {
|
|
531
|
+
this.logger?.info(`SMS sent via ${this.name}: ${result.value.messageId}`);
|
|
532
|
+
return {
|
|
533
|
+
success: true,
|
|
534
|
+
messageId: result.value.messageId,
|
|
535
|
+
channel: "sms",
|
|
536
|
+
status: "sent",
|
|
537
|
+
provider: this.name,
|
|
538
|
+
timestamp,
|
|
539
|
+
metadata: options?.metadata
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
this.logger?.error(`SMS send via ${this.name} failed: ${result.error?.message ?? result.value?.error}`);
|
|
543
|
+
return {
|
|
544
|
+
success: false,
|
|
545
|
+
channel: "sms",
|
|
546
|
+
status: "failed",
|
|
547
|
+
provider: this.name,
|
|
548
|
+
error: createNotifyError(result.error ?? new Error(result.value?.error ?? "Unknown error"), `${this.name.toUpperCase()}_ERROR`),
|
|
549
|
+
timestamp,
|
|
550
|
+
metadata: options?.metadata
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Get delivery status (most local providers don't support this)
|
|
555
|
+
*/
|
|
556
|
+
async getStatus(_messageId) {
|
|
557
|
+
this.logger?.warn(`${this.name} does not support status queries`);
|
|
558
|
+
return "unknown";
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Send bulk SMS messages
|
|
562
|
+
*/
|
|
563
|
+
async sendBulk(messages, options) {
|
|
564
|
+
const startTime = Date.now();
|
|
565
|
+
const results = [];
|
|
566
|
+
let sent = 0;
|
|
567
|
+
let failed = 0;
|
|
568
|
+
for (let i = 0; i < messages.length; i++) {
|
|
569
|
+
const msg = messages[i];
|
|
570
|
+
const result = await this.send(msg.to, msg.message, { metadata: msg.metadata });
|
|
571
|
+
results.push(result);
|
|
572
|
+
if (result.success) {
|
|
573
|
+
sent++;
|
|
574
|
+
} else {
|
|
575
|
+
failed++;
|
|
576
|
+
if (options?.stopOnError) {
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
options?.onProgress?.(i + 1, messages.length);
|
|
581
|
+
if (options?.rateLimit?.messagesPerSecond && i < messages.length - 1) {
|
|
582
|
+
const delay = 1e3 / options.rateLimit.messagesPerSecond;
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
results,
|
|
588
|
+
summary: {
|
|
589
|
+
total: messages.length,
|
|
590
|
+
sent,
|
|
591
|
+
failed,
|
|
592
|
+
durationMs: Date.now() - startTime
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Cleanup resources
|
|
598
|
+
*/
|
|
599
|
+
async destroy() {
|
|
600
|
+
this.logger?.debug(`${this.name} provider destroyed`);
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// src/providers/sms/local/telenor.provider.ts
|
|
605
|
+
var TelenorSmsProvider = class extends BaseLocalSmsProvider {
|
|
606
|
+
name = "telenor";
|
|
607
|
+
baseUrl;
|
|
608
|
+
constructor(config, options) {
|
|
609
|
+
super(config, options);
|
|
610
|
+
this.baseUrl = config.baseUrl ?? "https://api.telenor.com.pk/sms/v2/send";
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Make API request to Telenor
|
|
614
|
+
*/
|
|
615
|
+
async makeRequest(to, message) {
|
|
616
|
+
try {
|
|
617
|
+
const response = await fetch(this.baseUrl, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: {
|
|
620
|
+
"Content-Type": "application/json",
|
|
621
|
+
"Authorization": `Bearer ${this.config.apiKey}`
|
|
622
|
+
},
|
|
623
|
+
body: JSON.stringify({
|
|
624
|
+
to,
|
|
625
|
+
message,
|
|
626
|
+
sender_id: this.config.senderId,
|
|
627
|
+
...this.config.apiSecret && { api_secret: this.config.apiSecret }
|
|
628
|
+
})
|
|
629
|
+
});
|
|
630
|
+
const data = await response.json();
|
|
631
|
+
if (!response.ok || data.error) {
|
|
632
|
+
return {
|
|
633
|
+
success: false,
|
|
634
|
+
error: data.error ?? data.response?.error ?? `HTTP ${response.status}`
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
success: true,
|
|
639
|
+
messageId: data.response?.message_id ?? `tel-${Date.now()}`
|
|
640
|
+
};
|
|
641
|
+
} catch (error) {
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// src/providers/sms/local/jazz.provider.ts
|
|
651
|
+
var JazzSmsProvider = class extends BaseLocalSmsProvider {
|
|
652
|
+
name = "jazz";
|
|
653
|
+
baseUrl;
|
|
654
|
+
constructor(config, options) {
|
|
655
|
+
super(config, options);
|
|
656
|
+
this.baseUrl = config.baseUrl ?? "https://api.jazz.com.pk/sms/v1/send";
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Make API request to Jazz
|
|
660
|
+
*/
|
|
661
|
+
async makeRequest(to, message) {
|
|
662
|
+
try {
|
|
663
|
+
const params = new URLSearchParams({
|
|
664
|
+
username: this.config.apiKey,
|
|
665
|
+
...this.config.apiSecret && { password: this.config.apiSecret },
|
|
666
|
+
to,
|
|
667
|
+
text: message,
|
|
668
|
+
from: this.config.senderId
|
|
669
|
+
});
|
|
670
|
+
const response = await fetch(this.baseUrl, {
|
|
671
|
+
method: "POST",
|
|
672
|
+
headers: {
|
|
673
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
674
|
+
},
|
|
675
|
+
body: params.toString()
|
|
676
|
+
});
|
|
677
|
+
const data = await response.json();
|
|
678
|
+
if (!response.ok || data.errorCode) {
|
|
679
|
+
return {
|
|
680
|
+
success: false,
|
|
681
|
+
error: data.errorMessage ?? data.errorCode ?? `HTTP ${response.status}`
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
messageId: data.messageId ?? `jazz-${Date.now()}`
|
|
687
|
+
};
|
|
688
|
+
} catch (error) {
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
// src/providers/sms/local/zong.provider.ts
|
|
698
|
+
var ZongSmsProvider = class extends BaseLocalSmsProvider {
|
|
699
|
+
name = "zong";
|
|
700
|
+
baseUrl;
|
|
701
|
+
constructor(config, options) {
|
|
702
|
+
super(config, options);
|
|
703
|
+
this.baseUrl = config.baseUrl ?? "https://api.zong.com.pk/sms/send";
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Make API request to Zong
|
|
707
|
+
*/
|
|
708
|
+
async makeRequest(to, message) {
|
|
709
|
+
try {
|
|
710
|
+
const response = await fetch(this.baseUrl, {
|
|
711
|
+
method: "POST",
|
|
712
|
+
headers: {
|
|
713
|
+
"Content-Type": "application/json",
|
|
714
|
+
"X-API-Key": this.config.apiKey
|
|
715
|
+
},
|
|
716
|
+
body: JSON.stringify({
|
|
717
|
+
recipient: to,
|
|
718
|
+
message,
|
|
719
|
+
senderId: this.config.senderId,
|
|
720
|
+
...this.config.apiSecret && { apiSecret: this.config.apiSecret }
|
|
721
|
+
})
|
|
722
|
+
});
|
|
723
|
+
const data = await response.json();
|
|
724
|
+
if (!response.ok || data.responseCode && data.responseCode !== "00" && data.responseCode !== "0") {
|
|
725
|
+
return {
|
|
726
|
+
success: false,
|
|
727
|
+
error: data.responseMessage ?? `Error code: ${data.responseCode}`
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
return {
|
|
731
|
+
success: true,
|
|
732
|
+
messageId: data.messageId ?? `zong-${Date.now()}`
|
|
733
|
+
};
|
|
734
|
+
} catch (error) {
|
|
735
|
+
return {
|
|
736
|
+
success: false,
|
|
737
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
// src/providers/whatsapp/meta.provider.ts
|
|
744
|
+
var MetaWhatsAppProvider = class {
|
|
745
|
+
name = "meta";
|
|
746
|
+
config;
|
|
747
|
+
logger;
|
|
748
|
+
retryConfig;
|
|
749
|
+
apiVersion;
|
|
750
|
+
baseUrl;
|
|
751
|
+
constructor(config, options) {
|
|
752
|
+
this.config = config;
|
|
753
|
+
this.logger = options?.logger;
|
|
754
|
+
this.retryConfig = options?.retryConfig ?? DEFAULT_RETRY_CONFIG;
|
|
755
|
+
this.apiVersion = config.apiVersion ?? "v18.0";
|
|
756
|
+
this.baseUrl = `https://graph.facebook.com/${this.apiVersion}/${config.phoneNumberId}/messages`;
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Initialize the provider (no-op for Meta, just validates config)
|
|
760
|
+
*/
|
|
761
|
+
async initialize() {
|
|
762
|
+
if (!this.config.accessToken) {
|
|
763
|
+
throw new Error("Meta WhatsApp: accessToken is required");
|
|
764
|
+
}
|
|
765
|
+
if (!this.config.phoneNumberId) {
|
|
766
|
+
throw new Error("Meta WhatsApp: phoneNumberId is required");
|
|
767
|
+
}
|
|
768
|
+
this.logger?.debug("MetaWhatsAppProvider initialized");
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Make an API request to Meta WhatsApp Cloud API
|
|
772
|
+
*/
|
|
773
|
+
async makeRequest(body) {
|
|
774
|
+
const response = await fetch(this.baseUrl, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
headers: {
|
|
777
|
+
"Authorization": `Bearer ${this.config.accessToken}`,
|
|
778
|
+
"Content-Type": "application/json"
|
|
779
|
+
},
|
|
780
|
+
body: JSON.stringify(body)
|
|
781
|
+
});
|
|
782
|
+
const data = await response.json();
|
|
783
|
+
if (!response.ok || "error" in data) {
|
|
784
|
+
const error = data.error;
|
|
785
|
+
const err = new Error(error?.message ?? "Unknown Meta API error");
|
|
786
|
+
err.code = String(error?.code ?? "UNKNOWN");
|
|
787
|
+
err.statusCode = response.status;
|
|
788
|
+
throw err;
|
|
789
|
+
}
|
|
790
|
+
return data;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Send a text message (within 24hr conversation window)
|
|
794
|
+
*/
|
|
795
|
+
async sendText(to, message, options) {
|
|
796
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
797
|
+
const e164 = toE164(to);
|
|
798
|
+
if (!e164) {
|
|
799
|
+
return {
|
|
800
|
+
success: false,
|
|
801
|
+
channel: "whatsapp",
|
|
802
|
+
status: "failed",
|
|
803
|
+
provider: this.name,
|
|
804
|
+
error: {
|
|
805
|
+
code: "INVALID_PHONE_NUMBER",
|
|
806
|
+
message: `Invalid phone number: ${to}`,
|
|
807
|
+
retriable: false
|
|
808
|
+
},
|
|
809
|
+
timestamp,
|
|
810
|
+
metadata: options?.metadata
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
const whatsappNumber = e164.replace("+", "");
|
|
814
|
+
this.logger?.debug(`Sending WhatsApp text to ${whatsappNumber}`);
|
|
815
|
+
const result = await withRetry(
|
|
816
|
+
async () => {
|
|
817
|
+
return this.makeRequest({
|
|
818
|
+
messaging_product: "whatsapp",
|
|
819
|
+
recipient_type: "individual",
|
|
820
|
+
to: whatsappNumber,
|
|
821
|
+
type: "text",
|
|
822
|
+
text: {
|
|
823
|
+
preview_url: false,
|
|
824
|
+
body: message
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
this.retryConfig
|
|
829
|
+
);
|
|
830
|
+
if (result.success && result.value) {
|
|
831
|
+
const messageId = result.value.messages[0]?.id;
|
|
832
|
+
this.logger?.info(`WhatsApp text sent successfully: ${messageId}`);
|
|
833
|
+
return {
|
|
834
|
+
success: true,
|
|
835
|
+
messageId,
|
|
836
|
+
channel: "whatsapp",
|
|
837
|
+
status: "sent",
|
|
838
|
+
provider: this.name,
|
|
839
|
+
timestamp,
|
|
840
|
+
metadata: options?.metadata
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
this.logger?.error(`WhatsApp text send failed: ${result.error?.message}`);
|
|
844
|
+
return {
|
|
845
|
+
success: false,
|
|
846
|
+
channel: "whatsapp",
|
|
847
|
+
status: "failed",
|
|
848
|
+
provider: this.name,
|
|
849
|
+
error: createNotifyError(result.error, "META_API_ERROR"),
|
|
850
|
+
timestamp,
|
|
851
|
+
metadata: options?.metadata
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Send a template message (works outside 24hr window)
|
|
856
|
+
*/
|
|
857
|
+
async sendTemplate(to, template, options) {
|
|
858
|
+
const timestamp = /* @__PURE__ */ new Date();
|
|
859
|
+
const e164 = toE164(to);
|
|
860
|
+
if (!e164) {
|
|
861
|
+
return {
|
|
862
|
+
success: false,
|
|
863
|
+
channel: "whatsapp",
|
|
864
|
+
status: "failed",
|
|
865
|
+
provider: this.name,
|
|
866
|
+
error: {
|
|
867
|
+
code: "INVALID_PHONE_NUMBER",
|
|
868
|
+
message: `Invalid phone number: ${to}`,
|
|
869
|
+
retriable: false
|
|
870
|
+
},
|
|
871
|
+
timestamp,
|
|
872
|
+
metadata: options?.metadata
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
const whatsappNumber = e164.replace("+", "");
|
|
876
|
+
this.logger?.debug(`Sending WhatsApp template "${template.name}" to ${whatsappNumber}`);
|
|
877
|
+
const components = [];
|
|
878
|
+
if (template.header) {
|
|
879
|
+
const headerComponent = {
|
|
880
|
+
type: "header",
|
|
881
|
+
parameters: []
|
|
882
|
+
};
|
|
883
|
+
if (template.header.type === "text" && template.header.text) {
|
|
884
|
+
headerComponent.parameters = [{ type: "text", text: template.header.text }];
|
|
885
|
+
} else if (template.header.type === "image" && template.header.url) {
|
|
886
|
+
headerComponent.parameters = [{ type: "image", image: { link: template.header.url } }];
|
|
887
|
+
} else if (template.header.type === "document" && template.header.url) {
|
|
888
|
+
headerComponent.parameters = [{ type: "document", document: { link: template.header.url } }];
|
|
889
|
+
} else if (template.header.type === "video" && template.header.url) {
|
|
890
|
+
headerComponent.parameters = [{ type: "video", video: { link: template.header.url } }];
|
|
891
|
+
}
|
|
892
|
+
if (headerComponent.parameters && headerComponent.parameters.length > 0) {
|
|
893
|
+
components.push(headerComponent);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (template.variables) {
|
|
897
|
+
const variables = Array.isArray(template.variables) ? template.variables : Object.values(template.variables);
|
|
898
|
+
if (variables.length > 0) {
|
|
899
|
+
components.push({
|
|
900
|
+
type: "body",
|
|
901
|
+
parameters: variables.map((text) => ({
|
|
902
|
+
type: "text",
|
|
903
|
+
text: String(text)
|
|
904
|
+
}))
|
|
905
|
+
});
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (template.buttons && template.buttons.length > 0) {
|
|
909
|
+
for (const button of template.buttons) {
|
|
910
|
+
if (button.payload) {
|
|
911
|
+
components.push({
|
|
912
|
+
type: "button",
|
|
913
|
+
sub_type: button.type === "url" ? "url" : "quick_reply",
|
|
914
|
+
index: button.index,
|
|
915
|
+
parameters: [{ type: "text", text: button.payload }]
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const result = await withRetry(
|
|
921
|
+
async () => {
|
|
922
|
+
return this.makeRequest({
|
|
923
|
+
messaging_product: "whatsapp",
|
|
924
|
+
recipient_type: "individual",
|
|
925
|
+
to: whatsappNumber,
|
|
926
|
+
type: "template",
|
|
927
|
+
template: {
|
|
928
|
+
name: template.name,
|
|
929
|
+
language: {
|
|
930
|
+
code: template.language
|
|
931
|
+
},
|
|
932
|
+
components: components.length > 0 ? components : void 0
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
},
|
|
936
|
+
this.retryConfig
|
|
937
|
+
);
|
|
938
|
+
if (result.success && result.value) {
|
|
939
|
+
const messageId = result.value.messages[0]?.id;
|
|
940
|
+
this.logger?.info(`WhatsApp template sent successfully: ${messageId}`);
|
|
941
|
+
return {
|
|
942
|
+
success: true,
|
|
943
|
+
messageId,
|
|
944
|
+
channel: "whatsapp",
|
|
945
|
+
status: "sent",
|
|
946
|
+
provider: this.name,
|
|
947
|
+
timestamp,
|
|
948
|
+
metadata: options?.metadata
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
this.logger?.error(`WhatsApp template send failed: ${result.error?.message}`);
|
|
952
|
+
return {
|
|
953
|
+
success: false,
|
|
954
|
+
channel: "whatsapp",
|
|
955
|
+
status: "failed",
|
|
956
|
+
provider: this.name,
|
|
957
|
+
error: createNotifyError(result.error, "META_API_ERROR"),
|
|
958
|
+
timestamp,
|
|
959
|
+
metadata: options?.metadata
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Get delivery status of a message
|
|
964
|
+
* Note: Meta doesn't have a direct status lookup API - statuses come via webhooks
|
|
965
|
+
*/
|
|
966
|
+
async getStatus(_messageId) {
|
|
967
|
+
this.logger?.warn("Meta WhatsApp does not support direct status queries. Use webhooks instead.");
|
|
968
|
+
return "unknown";
|
|
969
|
+
}
|
|
970
|
+
/**
|
|
971
|
+
* Cleanup resources (no-op for Meta)
|
|
972
|
+
*/
|
|
973
|
+
async destroy() {
|
|
974
|
+
this.logger?.debug("MetaWhatsAppProvider destroyed");
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// src/notify-hub.ts
|
|
979
|
+
var noopLogger = {
|
|
980
|
+
debug: () => void 0,
|
|
981
|
+
info: () => void 0,
|
|
982
|
+
warn: () => void 0,
|
|
983
|
+
error: () => void 0
|
|
984
|
+
};
|
|
985
|
+
var NotifyHub = class {
|
|
986
|
+
config;
|
|
987
|
+
logger;
|
|
988
|
+
retryConfig;
|
|
989
|
+
// Providers
|
|
990
|
+
primarySmsProvider;
|
|
991
|
+
fallbackSmsProvider;
|
|
992
|
+
whatsappProvider;
|
|
993
|
+
emailProvider;
|
|
994
|
+
// Initialization state
|
|
995
|
+
initialized = false;
|
|
996
|
+
constructor(config) {
|
|
997
|
+
this.config = config;
|
|
998
|
+
this.logger = config.logger ?? noopLogger;
|
|
999
|
+
this.retryConfig = config.retry ?? DEFAULT_RETRY_CONFIG;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Initialize all configured providers
|
|
1003
|
+
*/
|
|
1004
|
+
async initialize() {
|
|
1005
|
+
if (this.initialized) return;
|
|
1006
|
+
this.logger.debug("Initializing NotifyHub");
|
|
1007
|
+
if (this.config.sms) {
|
|
1008
|
+
this.primarySmsProvider = await this.createSmsProvider(
|
|
1009
|
+
this.config.sms.primary
|
|
1010
|
+
);
|
|
1011
|
+
if (this.config.sms.fallback) {
|
|
1012
|
+
this.fallbackSmsProvider = await this.createSmsProvider(
|
|
1013
|
+
this.config.sms.fallback
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
if (this.config.whatsapp) {
|
|
1018
|
+
this.whatsappProvider = await this.createWhatsAppProvider(
|
|
1019
|
+
this.config.whatsapp
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
if (this.config.email) {
|
|
1023
|
+
this.emailProvider = await this.createEmailProvider(this.config.email);
|
|
1024
|
+
}
|
|
1025
|
+
this.initialized = true;
|
|
1026
|
+
this.logger.info("NotifyHub initialized");
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Create an SMS provider from configuration
|
|
1030
|
+
*/
|
|
1031
|
+
async createSmsProvider(config) {
|
|
1032
|
+
if (config.provider === "custom" && config.instance) {
|
|
1033
|
+
return config.instance;
|
|
1034
|
+
}
|
|
1035
|
+
if (config.provider === "twilio") {
|
|
1036
|
+
const twilioConfig = config.config;
|
|
1037
|
+
const provider = new TwilioSmsProvider(twilioConfig, {
|
|
1038
|
+
logger: this.logger,
|
|
1039
|
+
retryConfig: this.retryConfig
|
|
1040
|
+
});
|
|
1041
|
+
await provider.initialize();
|
|
1042
|
+
return provider;
|
|
1043
|
+
}
|
|
1044
|
+
if (config.provider === "local") {
|
|
1045
|
+
const localConfig = config.config;
|
|
1046
|
+
let provider;
|
|
1047
|
+
switch (localConfig.provider) {
|
|
1048
|
+
case "telenor":
|
|
1049
|
+
provider = new TelenorSmsProvider(localConfig, {
|
|
1050
|
+
logger: this.logger,
|
|
1051
|
+
retryConfig: this.retryConfig
|
|
1052
|
+
});
|
|
1053
|
+
break;
|
|
1054
|
+
case "jazz":
|
|
1055
|
+
provider = new JazzSmsProvider(localConfig, {
|
|
1056
|
+
logger: this.logger,
|
|
1057
|
+
retryConfig: this.retryConfig
|
|
1058
|
+
});
|
|
1059
|
+
break;
|
|
1060
|
+
case "zong":
|
|
1061
|
+
provider = new ZongSmsProvider(localConfig, {
|
|
1062
|
+
logger: this.logger,
|
|
1063
|
+
retryConfig: this.retryConfig
|
|
1064
|
+
});
|
|
1065
|
+
break;
|
|
1066
|
+
default:
|
|
1067
|
+
throw new Error(`Unknown local SMS provider: ${localConfig.provider}`);
|
|
1068
|
+
}
|
|
1069
|
+
await provider.initialize();
|
|
1070
|
+
return provider;
|
|
1071
|
+
}
|
|
1072
|
+
throw new Error(`Unknown SMS provider: ${config.provider}`);
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Create a WhatsApp provider from configuration
|
|
1076
|
+
*/
|
|
1077
|
+
async createWhatsAppProvider(config) {
|
|
1078
|
+
if (config.provider === "custom" && config.instance) {
|
|
1079
|
+
return config.instance;
|
|
1080
|
+
}
|
|
1081
|
+
if (config.provider === "meta") {
|
|
1082
|
+
const metaConfig = config.config;
|
|
1083
|
+
const provider = new MetaWhatsAppProvider(metaConfig, {
|
|
1084
|
+
logger: this.logger,
|
|
1085
|
+
retryConfig: this.retryConfig
|
|
1086
|
+
});
|
|
1087
|
+
await provider.initialize();
|
|
1088
|
+
return provider;
|
|
1089
|
+
}
|
|
1090
|
+
throw new Error(`Unknown WhatsApp provider: ${config.provider}`);
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Create an Email provider from configuration
|
|
1094
|
+
*/
|
|
1095
|
+
async createEmailProvider(config) {
|
|
1096
|
+
if (config.provider === "custom" && config.instance) {
|
|
1097
|
+
return config.instance;
|
|
1098
|
+
}
|
|
1099
|
+
if (config.provider === "ses") {
|
|
1100
|
+
throw new Error("SES email provider not yet implemented");
|
|
1101
|
+
}
|
|
1102
|
+
throw new Error(`Unknown email provider: ${config.provider}`);
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Ensure NotifyHub is initialized
|
|
1106
|
+
*/
|
|
1107
|
+
async ensureInitialized() {
|
|
1108
|
+
if (!this.initialized) {
|
|
1109
|
+
await this.initialize();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
/**
|
|
1113
|
+
* Get the appropriate SMS provider based on phone number
|
|
1114
|
+
*/
|
|
1115
|
+
getSmsProvider(to) {
|
|
1116
|
+
if (!this.primarySmsProvider) {
|
|
1117
|
+
throw new Error("SMS provider not configured");
|
|
1118
|
+
}
|
|
1119
|
+
if (!this.config.sms?.routing || !this.fallbackSmsProvider) {
|
|
1120
|
+
return this.primarySmsProvider;
|
|
1121
|
+
}
|
|
1122
|
+
const e164 = toE164(to);
|
|
1123
|
+
if (!e164) {
|
|
1124
|
+
return this.primarySmsProvider;
|
|
1125
|
+
}
|
|
1126
|
+
const route = routeByPrefix(e164, this.config.sms.routing);
|
|
1127
|
+
return route === "fallback" && this.fallbackSmsProvider ? this.fallbackSmsProvider : this.primarySmsProvider;
|
|
1128
|
+
}
|
|
1129
|
+
// ==================== SMS METHODS ====================
|
|
1130
|
+
/**
|
|
1131
|
+
* Send an SMS message
|
|
1132
|
+
* @param to Phone number in any format (will be converted to E.164)
|
|
1133
|
+
* @param message Message content
|
|
1134
|
+
* @param options Additional options
|
|
1135
|
+
*/
|
|
1136
|
+
async sms(to, message, options) {
|
|
1137
|
+
await this.ensureInitialized();
|
|
1138
|
+
const provider = this.getSmsProvider(to);
|
|
1139
|
+
this.logger.debug(`Sending SMS via ${provider.name} to ${to}`);
|
|
1140
|
+
try {
|
|
1141
|
+
const result = await provider.send(to, message, options);
|
|
1142
|
+
if (!result.success && result.error?.retriable && this.fallbackSmsProvider && provider !== this.fallbackSmsProvider) {
|
|
1143
|
+
this.logger.warn(
|
|
1144
|
+
`Primary SMS provider failed, trying fallback: ${result.error.message}`
|
|
1145
|
+
);
|
|
1146
|
+
return this.fallbackSmsProvider.send(to, message, options);
|
|
1147
|
+
}
|
|
1148
|
+
return result;
|
|
1149
|
+
} catch (error) {
|
|
1150
|
+
this.logger.error(`SMS send error: ${error}`);
|
|
1151
|
+
if (this.fallbackSmsProvider && provider !== this.fallbackSmsProvider) {
|
|
1152
|
+
this.logger.warn("Primary SMS provider threw, trying fallback");
|
|
1153
|
+
return this.fallbackSmsProvider.send(to, message, options);
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
success: false,
|
|
1157
|
+
channel: "sms",
|
|
1158
|
+
status: "failed",
|
|
1159
|
+
provider: provider.name,
|
|
1160
|
+
error: {
|
|
1161
|
+
code: "SEND_ERROR",
|
|
1162
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1163
|
+
retriable: false
|
|
1164
|
+
},
|
|
1165
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1166
|
+
metadata: options?.metadata
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Send bulk SMS messages
|
|
1172
|
+
*/
|
|
1173
|
+
async bulkSms(messages, options) {
|
|
1174
|
+
await this.ensureInitialized();
|
|
1175
|
+
if (!this.primarySmsProvider) {
|
|
1176
|
+
throw new Error("SMS provider not configured");
|
|
1177
|
+
}
|
|
1178
|
+
if (this.primarySmsProvider.sendBulk) {
|
|
1179
|
+
return this.primarySmsProvider.sendBulk(messages, options);
|
|
1180
|
+
}
|
|
1181
|
+
const startTime = Date.now();
|
|
1182
|
+
const results = [];
|
|
1183
|
+
let sent = 0;
|
|
1184
|
+
let failed = 0;
|
|
1185
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1186
|
+
const msg = messages[i];
|
|
1187
|
+
const result = await this.sms(msg.to, msg.message, { metadata: msg.metadata });
|
|
1188
|
+
results.push(result);
|
|
1189
|
+
if (result.success) {
|
|
1190
|
+
sent++;
|
|
1191
|
+
} else {
|
|
1192
|
+
failed++;
|
|
1193
|
+
if (options?.stopOnError) {
|
|
1194
|
+
break;
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
options?.onProgress?.(i + 1, messages.length);
|
|
1198
|
+
if (options?.rateLimit?.messagesPerSecond && i < messages.length - 1) {
|
|
1199
|
+
const delay = 1e3 / options.rateLimit.messagesPerSecond;
|
|
1200
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return {
|
|
1204
|
+
results,
|
|
1205
|
+
summary: {
|
|
1206
|
+
total: messages.length,
|
|
1207
|
+
sent,
|
|
1208
|
+
failed,
|
|
1209
|
+
durationMs: Date.now() - startTime
|
|
1210
|
+
}
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Get SMS delivery status
|
|
1215
|
+
*/
|
|
1216
|
+
async getSmsStatus(messageId) {
|
|
1217
|
+
await this.ensureInitialized();
|
|
1218
|
+
if (!this.primarySmsProvider?.getStatus) {
|
|
1219
|
+
throw new Error("SMS provider does not support status checks");
|
|
1220
|
+
}
|
|
1221
|
+
return this.primarySmsProvider.getStatus(messageId);
|
|
1222
|
+
}
|
|
1223
|
+
// ==================== WHATSAPP METHODS ====================
|
|
1224
|
+
/**
|
|
1225
|
+
* Send a WhatsApp text message (within 24hr window)
|
|
1226
|
+
*/
|
|
1227
|
+
async whatsapp(to, message, options) {
|
|
1228
|
+
await this.ensureInitialized();
|
|
1229
|
+
if (!this.whatsappProvider) {
|
|
1230
|
+
throw new Error("WhatsApp provider not configured");
|
|
1231
|
+
}
|
|
1232
|
+
return this.whatsappProvider.sendText(to, message, options);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Send a WhatsApp template message
|
|
1236
|
+
*/
|
|
1237
|
+
async whatsappTemplate(to, template, options) {
|
|
1238
|
+
await this.ensureInitialized();
|
|
1239
|
+
if (!this.whatsappProvider) {
|
|
1240
|
+
throw new Error("WhatsApp provider not configured");
|
|
1241
|
+
}
|
|
1242
|
+
return this.whatsappProvider.sendTemplate(to, template, options);
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Send bulk WhatsApp messages
|
|
1246
|
+
*/
|
|
1247
|
+
async bulkWhatsApp(messages, options) {
|
|
1248
|
+
await this.ensureInitialized();
|
|
1249
|
+
if (!this.whatsappProvider) {
|
|
1250
|
+
throw new Error("WhatsApp provider not configured");
|
|
1251
|
+
}
|
|
1252
|
+
const startTime = Date.now();
|
|
1253
|
+
const results = [];
|
|
1254
|
+
let sent = 0;
|
|
1255
|
+
let failed = 0;
|
|
1256
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1257
|
+
const msg = messages[i];
|
|
1258
|
+
let result;
|
|
1259
|
+
if (msg.template) {
|
|
1260
|
+
result = await this.whatsappTemplate(msg.to, msg.template, {
|
|
1261
|
+
metadata: msg.metadata
|
|
1262
|
+
});
|
|
1263
|
+
} else if (msg.message) {
|
|
1264
|
+
result = await this.whatsapp(msg.to, msg.message, { metadata: msg.metadata });
|
|
1265
|
+
} else {
|
|
1266
|
+
result = {
|
|
1267
|
+
success: false,
|
|
1268
|
+
channel: "whatsapp",
|
|
1269
|
+
status: "failed",
|
|
1270
|
+
provider: this.whatsappProvider.name,
|
|
1271
|
+
error: {
|
|
1272
|
+
code: "INVALID_MESSAGE",
|
|
1273
|
+
message: "Message must have either message or template",
|
|
1274
|
+
retriable: false
|
|
1275
|
+
},
|
|
1276
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
results.push(result);
|
|
1280
|
+
if (result.success) {
|
|
1281
|
+
sent++;
|
|
1282
|
+
} else {
|
|
1283
|
+
failed++;
|
|
1284
|
+
if (options?.stopOnError) {
|
|
1285
|
+
break;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
options?.onProgress?.(i + 1, messages.length);
|
|
1289
|
+
if (options?.rateLimit?.messagesPerSecond && i < messages.length - 1) {
|
|
1290
|
+
const delay = 1e3 / options.rateLimit.messagesPerSecond;
|
|
1291
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return {
|
|
1295
|
+
results,
|
|
1296
|
+
summary: {
|
|
1297
|
+
total: messages.length,
|
|
1298
|
+
sent,
|
|
1299
|
+
failed,
|
|
1300
|
+
durationMs: Date.now() - startTime
|
|
1301
|
+
}
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
// ==================== EMAIL METHODS ====================
|
|
1305
|
+
/**
|
|
1306
|
+
* Send an email
|
|
1307
|
+
*/
|
|
1308
|
+
async email(to, email, options) {
|
|
1309
|
+
await this.ensureInitialized();
|
|
1310
|
+
if (!this.emailProvider) {
|
|
1311
|
+
throw new Error("Email provider not configured");
|
|
1312
|
+
}
|
|
1313
|
+
return this.emailProvider.send(to, email, options);
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Send bulk emails
|
|
1317
|
+
*/
|
|
1318
|
+
async bulkEmail(messages, options) {
|
|
1319
|
+
await this.ensureInitialized();
|
|
1320
|
+
if (!this.emailProvider) {
|
|
1321
|
+
throw new Error("Email provider not configured");
|
|
1322
|
+
}
|
|
1323
|
+
if (this.emailProvider.sendBulk) {
|
|
1324
|
+
return this.emailProvider.sendBulk(messages, options);
|
|
1325
|
+
}
|
|
1326
|
+
const startTime = Date.now();
|
|
1327
|
+
const results = [];
|
|
1328
|
+
let sent = 0;
|
|
1329
|
+
let failed = 0;
|
|
1330
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1331
|
+
const msg = messages[i];
|
|
1332
|
+
const result = await this.email(msg.to, msg, { metadata: msg.metadata });
|
|
1333
|
+
results.push(result);
|
|
1334
|
+
if (result.success) {
|
|
1335
|
+
sent++;
|
|
1336
|
+
} else {
|
|
1337
|
+
failed++;
|
|
1338
|
+
if (options?.stopOnError) {
|
|
1339
|
+
break;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
options?.onProgress?.(i + 1, messages.length);
|
|
1343
|
+
}
|
|
1344
|
+
return {
|
|
1345
|
+
results,
|
|
1346
|
+
summary: {
|
|
1347
|
+
total: messages.length,
|
|
1348
|
+
sent,
|
|
1349
|
+
failed,
|
|
1350
|
+
durationMs: Date.now() - startTime
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
// ==================== GENERIC SEND ====================
|
|
1355
|
+
/**
|
|
1356
|
+
* Send a notification using the specified channel
|
|
1357
|
+
*/
|
|
1358
|
+
async send(notification) {
|
|
1359
|
+
switch (notification.channel) {
|
|
1360
|
+
case "sms":
|
|
1361
|
+
if (!notification.message) {
|
|
1362
|
+
return {
|
|
1363
|
+
success: false,
|
|
1364
|
+
channel: "sms",
|
|
1365
|
+
status: "failed",
|
|
1366
|
+
provider: "unknown",
|
|
1367
|
+
error: {
|
|
1368
|
+
code: "INVALID_NOTIFICATION",
|
|
1369
|
+
message: "SMS notification requires message",
|
|
1370
|
+
retriable: false
|
|
1371
|
+
},
|
|
1372
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
return this.sms(notification.to, notification.message, {
|
|
1376
|
+
metadata: notification.metadata
|
|
1377
|
+
});
|
|
1378
|
+
case "whatsapp":
|
|
1379
|
+
if (notification.template) {
|
|
1380
|
+
return this.whatsappTemplate(notification.to, notification.template, {
|
|
1381
|
+
metadata: notification.metadata
|
|
1382
|
+
});
|
|
1383
|
+
}
|
|
1384
|
+
if (notification.message) {
|
|
1385
|
+
return this.whatsapp(notification.to, notification.message, {
|
|
1386
|
+
metadata: notification.metadata
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
return {
|
|
1390
|
+
success: false,
|
|
1391
|
+
channel: "whatsapp",
|
|
1392
|
+
status: "failed",
|
|
1393
|
+
provider: "unknown",
|
|
1394
|
+
error: {
|
|
1395
|
+
code: "INVALID_NOTIFICATION",
|
|
1396
|
+
message: "WhatsApp notification requires message or template",
|
|
1397
|
+
retriable: false
|
|
1398
|
+
},
|
|
1399
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1400
|
+
};
|
|
1401
|
+
case "email":
|
|
1402
|
+
if (!notification.email) {
|
|
1403
|
+
return {
|
|
1404
|
+
success: false,
|
|
1405
|
+
channel: "email",
|
|
1406
|
+
status: "failed",
|
|
1407
|
+
provider: "unknown",
|
|
1408
|
+
error: {
|
|
1409
|
+
code: "INVALID_NOTIFICATION",
|
|
1410
|
+
message: "Email notification requires email object",
|
|
1411
|
+
retriable: false
|
|
1412
|
+
},
|
|
1413
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
return this.email(notification.to, notification.email, {
|
|
1417
|
+
metadata: notification.metadata
|
|
1418
|
+
});
|
|
1419
|
+
default:
|
|
1420
|
+
return {
|
|
1421
|
+
success: false,
|
|
1422
|
+
channel: notification.channel,
|
|
1423
|
+
status: "failed",
|
|
1424
|
+
provider: "unknown",
|
|
1425
|
+
error: {
|
|
1426
|
+
code: "INVALID_CHANNEL",
|
|
1427
|
+
message: `Unknown channel: ${notification.channel}`,
|
|
1428
|
+
retriable: false
|
|
1429
|
+
},
|
|
1430
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
// ==================== LIFECYCLE ====================
|
|
1435
|
+
/**
|
|
1436
|
+
* Cleanup and destroy all providers
|
|
1437
|
+
*/
|
|
1438
|
+
async destroy() {
|
|
1439
|
+
this.logger.debug("Destroying NotifyHub");
|
|
1440
|
+
await Promise.all([
|
|
1441
|
+
this.primarySmsProvider?.destroy?.(),
|
|
1442
|
+
this.fallbackSmsProvider?.destroy?.(),
|
|
1443
|
+
this.whatsappProvider?.destroy?.(),
|
|
1444
|
+
this.emailProvider?.destroy?.()
|
|
1445
|
+
]);
|
|
1446
|
+
this.primarySmsProvider = void 0;
|
|
1447
|
+
this.fallbackSmsProvider = void 0;
|
|
1448
|
+
this.whatsappProvider = void 0;
|
|
1449
|
+
this.emailProvider = void 0;
|
|
1450
|
+
this.initialized = false;
|
|
1451
|
+
this.logger.info("NotifyHub destroyed");
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
function mapTwilioStatus2(status) {
|
|
1455
|
+
const statusMap = {
|
|
1456
|
+
queued: "queued",
|
|
1457
|
+
sending: "queued",
|
|
1458
|
+
sent: "sent",
|
|
1459
|
+
delivered: "delivered",
|
|
1460
|
+
undelivered: "undelivered",
|
|
1461
|
+
failed: "failed",
|
|
1462
|
+
read: "read",
|
|
1463
|
+
accepted: "queued"
|
|
1464
|
+
};
|
|
1465
|
+
return statusMap[status.toLowerCase()] ?? "unknown";
|
|
1466
|
+
}
|
|
1467
|
+
var TwilioWebhookHandler = class {
|
|
1468
|
+
authToken;
|
|
1469
|
+
constructor(authToken) {
|
|
1470
|
+
this.authToken = authToken;
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Verify Twilio webhook signature
|
|
1474
|
+
* @see https://www.twilio.com/docs/usage/webhooks/webhooks-security
|
|
1475
|
+
*/
|
|
1476
|
+
verify(body, headers) {
|
|
1477
|
+
const signature = headers["x-twilio-signature"] ?? headers["X-Twilio-Signature"];
|
|
1478
|
+
const url = headers["x-original-url"] ?? headers["X-Original-Url"] ?? "";
|
|
1479
|
+
if (!signature || !url) {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
const params = body;
|
|
1483
|
+
let validationString = url;
|
|
1484
|
+
if (params && typeof params === "object") {
|
|
1485
|
+
const sortedKeys = Object.keys(params).sort();
|
|
1486
|
+
for (const key of sortedKeys) {
|
|
1487
|
+
validationString += key + params[key];
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
const expectedSignature = crypto__namespace.createHmac("sha1", this.authToken).update(validationString, "utf-8").digest("base64");
|
|
1491
|
+
try {
|
|
1492
|
+
return crypto__namespace.timingSafeEqual(
|
|
1493
|
+
Buffer.from(signature),
|
|
1494
|
+
Buffer.from(expectedSignature)
|
|
1495
|
+
);
|
|
1496
|
+
} catch {
|
|
1497
|
+
return false;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* Parse Twilio webhook payload
|
|
1502
|
+
*/
|
|
1503
|
+
parse(body) {
|
|
1504
|
+
const events = [];
|
|
1505
|
+
if ("MessageStatus" in body) {
|
|
1506
|
+
const statusEvent = {
|
|
1507
|
+
type: "status",
|
|
1508
|
+
messageId: body.MessageSid,
|
|
1509
|
+
channel: "sms",
|
|
1510
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1511
|
+
provider: "twilio",
|
|
1512
|
+
status: mapTwilioStatus2(body.MessageStatus),
|
|
1513
|
+
to: body.To,
|
|
1514
|
+
error: body.ErrorCode ? {
|
|
1515
|
+
code: body.ErrorCode,
|
|
1516
|
+
message: body.ErrorMessage ?? "Unknown error",
|
|
1517
|
+
retriable: false
|
|
1518
|
+
} : void 0
|
|
1519
|
+
};
|
|
1520
|
+
events.push(statusEvent);
|
|
1521
|
+
} else if ("Body" in body) {
|
|
1522
|
+
const messageEvent = {
|
|
1523
|
+
type: "message",
|
|
1524
|
+
messageId: body.MessageSid,
|
|
1525
|
+
channel: "sms",
|
|
1526
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1527
|
+
provider: "twilio",
|
|
1528
|
+
from: body.From,
|
|
1529
|
+
message: body.Body,
|
|
1530
|
+
mediaUrl: body.MediaUrl0,
|
|
1531
|
+
mediaType: body.MediaContentType0
|
|
1532
|
+
};
|
|
1533
|
+
events.push(messageEvent);
|
|
1534
|
+
}
|
|
1535
|
+
return events;
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
function createTwilioWebhookHandler(authToken) {
|
|
1539
|
+
return new TwilioWebhookHandler(authToken);
|
|
1540
|
+
}
|
|
1541
|
+
function mapMetaStatus(status) {
|
|
1542
|
+
const statusMap = {
|
|
1543
|
+
sent: "sent",
|
|
1544
|
+
delivered: "delivered",
|
|
1545
|
+
read: "read",
|
|
1546
|
+
failed: "failed"
|
|
1547
|
+
};
|
|
1548
|
+
return statusMap[status.toLowerCase()] ?? "unknown";
|
|
1549
|
+
}
|
|
1550
|
+
var WhatsAppWebhookHandler = class {
|
|
1551
|
+
appSecret;
|
|
1552
|
+
verifyToken;
|
|
1553
|
+
constructor(appSecret, verifyToken) {
|
|
1554
|
+
this.appSecret = appSecret;
|
|
1555
|
+
this.verifyToken = verifyToken;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Verify WhatsApp webhook challenge (for initial setup)
|
|
1559
|
+
*/
|
|
1560
|
+
verifyChallenge(verifyToken, challenge) {
|
|
1561
|
+
if (verifyToken === this.verifyToken) {
|
|
1562
|
+
return challenge;
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Verify Meta webhook signature
|
|
1568
|
+
* @see https://developers.facebook.com/docs/graph-api/webhooks/getting-started#verification-requests
|
|
1569
|
+
*/
|
|
1570
|
+
verify(body, headers) {
|
|
1571
|
+
const signature = headers["x-hub-signature-256"] ?? headers["X-Hub-Signature-256"];
|
|
1572
|
+
if (!signature || !this.appSecret) {
|
|
1573
|
+
return false;
|
|
1574
|
+
}
|
|
1575
|
+
const rawBody = typeof body === "string" ? body : JSON.stringify(body);
|
|
1576
|
+
const expectedSignature = "sha256=" + crypto__namespace.createHmac("sha256", this.appSecret).update(rawBody, "utf-8").digest("hex");
|
|
1577
|
+
try {
|
|
1578
|
+
return crypto__namespace.timingSafeEqual(
|
|
1579
|
+
Buffer.from(signature),
|
|
1580
|
+
Buffer.from(expectedSignature)
|
|
1581
|
+
);
|
|
1582
|
+
} catch {
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
/**
|
|
1587
|
+
* Parse Meta WhatsApp webhook payload
|
|
1588
|
+
*/
|
|
1589
|
+
parse(body) {
|
|
1590
|
+
const events = [];
|
|
1591
|
+
if (body.object !== "whatsapp_business_account") {
|
|
1592
|
+
return events;
|
|
1593
|
+
}
|
|
1594
|
+
for (const entry of body.entry) {
|
|
1595
|
+
for (const change of entry.changes) {
|
|
1596
|
+
if (change.field !== "messages") continue;
|
|
1597
|
+
const value = change.value;
|
|
1598
|
+
if (value.statuses) {
|
|
1599
|
+
for (const status of value.statuses) {
|
|
1600
|
+
const statusEvent = {
|
|
1601
|
+
type: "status",
|
|
1602
|
+
messageId: status.id,
|
|
1603
|
+
channel: "whatsapp",
|
|
1604
|
+
timestamp: new Date(parseInt(status.timestamp) * 1e3),
|
|
1605
|
+
provider: "meta",
|
|
1606
|
+
status: mapMetaStatus(status.status),
|
|
1607
|
+
to: status.recipient_id,
|
|
1608
|
+
error: status.errors && status.errors.length > 0 ? {
|
|
1609
|
+
code: String(status.errors[0].code),
|
|
1610
|
+
message: status.errors[0].message ?? status.errors[0].title,
|
|
1611
|
+
retriable: false
|
|
1612
|
+
} : void 0
|
|
1613
|
+
};
|
|
1614
|
+
events.push(statusEvent);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (value.messages) {
|
|
1618
|
+
for (const message of value.messages) {
|
|
1619
|
+
let messageText;
|
|
1620
|
+
let mediaType;
|
|
1621
|
+
switch (message.type) {
|
|
1622
|
+
case "text":
|
|
1623
|
+
messageText = message.text?.body;
|
|
1624
|
+
break;
|
|
1625
|
+
case "button":
|
|
1626
|
+
messageText = message.button?.text;
|
|
1627
|
+
break;
|
|
1628
|
+
case "interactive":
|
|
1629
|
+
messageText = message.interactive?.button_reply?.title ?? message.interactive?.list_reply?.title;
|
|
1630
|
+
break;
|
|
1631
|
+
case "image":
|
|
1632
|
+
mediaType = message.image?.mime_type;
|
|
1633
|
+
break;
|
|
1634
|
+
case "document":
|
|
1635
|
+
mediaType = message.document?.mime_type;
|
|
1636
|
+
break;
|
|
1637
|
+
case "audio":
|
|
1638
|
+
mediaType = message.audio?.mime_type;
|
|
1639
|
+
break;
|
|
1640
|
+
case "video":
|
|
1641
|
+
mediaType = message.video?.mime_type;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
const messageEvent = {
|
|
1645
|
+
type: "message",
|
|
1646
|
+
messageId: message.id,
|
|
1647
|
+
channel: "whatsapp",
|
|
1648
|
+
timestamp: new Date(parseInt(message.timestamp) * 1e3),
|
|
1649
|
+
provider: "meta",
|
|
1650
|
+
from: message.from,
|
|
1651
|
+
message: messageText,
|
|
1652
|
+
mediaType
|
|
1653
|
+
};
|
|
1654
|
+
events.push(messageEvent);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
return events;
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
function createWhatsAppWebhookHandler(appSecret, verifyToken) {
|
|
1663
|
+
return new WhatsAppWebhookHandler(appSecret, verifyToken);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
exports.BaseLocalSmsProvider = BaseLocalSmsProvider;
|
|
1667
|
+
exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
|
|
1668
|
+
exports.JazzSmsProvider = JazzSmsProvider;
|
|
1669
|
+
exports.MetaWhatsAppProvider = MetaWhatsAppProvider;
|
|
1670
|
+
exports.NotifyHub = NotifyHub;
|
|
1671
|
+
exports.TelenorSmsProvider = TelenorSmsProvider;
|
|
1672
|
+
exports.TwilioSmsProvider = TwilioSmsProvider;
|
|
1673
|
+
exports.TwilioWebhookHandler = TwilioWebhookHandler;
|
|
1674
|
+
exports.WhatsAppWebhookHandler = WhatsAppWebhookHandler;
|
|
1675
|
+
exports.ZongSmsProvider = ZongSmsProvider;
|
|
1676
|
+
exports.calculateBackoff = calculateBackoff;
|
|
1677
|
+
exports.cleanPhoneNumber = cleanPhoneNumber;
|
|
1678
|
+
exports.createNotifyError = createNotifyError;
|
|
1679
|
+
exports.createTwilioWebhookHandler = createTwilioWebhookHandler;
|
|
1680
|
+
exports.createWhatsAppWebhookHandler = createWhatsAppWebhookHandler;
|
|
1681
|
+
exports.getCallingCode = getCallingCode;
|
|
1682
|
+
exports.getCountryCode = getCountryCode;
|
|
1683
|
+
exports.getPakistanNetwork = getPakistanNetwork;
|
|
1684
|
+
exports.isCountry = isCountry;
|
|
1685
|
+
exports.isPakistanMobile = isPakistanMobile;
|
|
1686
|
+
exports.isRetriableError = isRetriableError;
|
|
1687
|
+
exports.routeByPrefix = routeByPrefix;
|
|
1688
|
+
exports.toE164 = toE164;
|
|
1689
|
+
exports.validatePhoneNumber = validatePhoneNumber;
|
|
1690
|
+
exports.withRetry = withRetry;
|
|
1691
|
+
//# sourceMappingURL=index.js.map
|
|
1692
|
+
//# sourceMappingURL=index.js.map
|