@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.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