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