@skoolite/notify-hub 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +452 -0
- package/dist/index.d.mts +1080 -0
- package/dist/index.d.ts +1080 -0
- package/dist/index.js +1692 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +1646 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +70 -0
package/README.md
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
# @skoolite/notify-hub
|
|
2
|
+
|
|
3
|
+
> **DISCLAIMER: INTERNAL USE PACKAGE**
|
|
4
|
+
>
|
|
5
|
+
> This package is built for internal use in Skoolyte projects. It is published publicly for convenience but comes with **NO SUPPORT**, **NO WARRANTY**, and **NO GUARANTEES**.
|
|
6
|
+
>
|
|
7
|
+
> - **No Issues/Bug Fixes**: Issues and PRs will not be monitored or addressed
|
|
8
|
+
> - **Breaking Changes**: Expect frequent breaking changes without notice
|
|
9
|
+
> - **No SLA**: This is not production-ready software for general use
|
|
10
|
+
> - **Use at Your Own Risk**: If you choose to use this package, you are responsible for any consequences
|
|
11
|
+
>
|
|
12
|
+
> **If you need a production-ready notification library, consider alternatives like [Novu](https://novu.co), [Knock](https://knock.app), or direct provider SDKs.**
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Multi-channel notification hub for SMS, WhatsApp, and Email. Send notifications through Twilio, Meta WhatsApp Cloud API, local Pakistan SMS providers, and AWS SES with a unified API.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Multi-channel**: SMS, WhatsApp, and Email support
|
|
21
|
+
- **Multiple providers**: Twilio, Meta WhatsApp, local Pakistan SMS (Telenor, Jazz, Zong), AWS SES
|
|
22
|
+
- **Smart routing**: Route messages to different providers based on phone number prefix
|
|
23
|
+
- **Automatic fallback**: Fall back to secondary provider on failure
|
|
24
|
+
- **Retry logic**: Exponential backoff with jitter for transient errors
|
|
25
|
+
- **Bulk sending**: Send to many recipients with rate limiting
|
|
26
|
+
- **Type-safe**: Full TypeScript support with comprehensive types
|
|
27
|
+
- **Provider agnostic**: Switch providers without code changes
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @skoolite/notify-hub
|
|
33
|
+
# or
|
|
34
|
+
pnpm add @skoolite/notify-hub
|
|
35
|
+
# or
|
|
36
|
+
yarn add @skoolite/notify-hub
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { NotifyHub } from '@skoolite/notify-hub';
|
|
43
|
+
|
|
44
|
+
// Initialize with Twilio
|
|
45
|
+
const notifyHub = new NotifyHub({
|
|
46
|
+
sms: {
|
|
47
|
+
primary: {
|
|
48
|
+
provider: 'twilio',
|
|
49
|
+
config: {
|
|
50
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
51
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
52
|
+
fromNumber: '+1234567890',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Send SMS
|
|
59
|
+
const result = await notifyHub.sms('+923001234567', 'Hello from NotifyHub!');
|
|
60
|
+
|
|
61
|
+
if (result.success) {
|
|
62
|
+
console.log('Message sent:', result.messageId);
|
|
63
|
+
} else {
|
|
64
|
+
console.error('Failed:', result.error?.message);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Configuration
|
|
69
|
+
|
|
70
|
+
### Full Configuration Example
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { NotifyHub } from '@skoolite/notify-hub';
|
|
74
|
+
|
|
75
|
+
const notifyHub = new NotifyHub({
|
|
76
|
+
// Default channel for notifications
|
|
77
|
+
defaultChannel: 'sms',
|
|
78
|
+
|
|
79
|
+
// SMS Configuration
|
|
80
|
+
sms: {
|
|
81
|
+
// Primary provider
|
|
82
|
+
primary: {
|
|
83
|
+
provider: 'twilio',
|
|
84
|
+
config: {
|
|
85
|
+
accountSid: process.env.TWILIO_ACCOUNT_SID!,
|
|
86
|
+
authToken: process.env.TWILIO_AUTH_TOKEN!,
|
|
87
|
+
fromNumber: '+1234567890',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
// Fallback provider (optional)
|
|
91
|
+
fallback: {
|
|
92
|
+
provider: 'local',
|
|
93
|
+
config: {
|
|
94
|
+
provider: 'telenor',
|
|
95
|
+
apiKey: process.env.TELENOR_API_KEY!,
|
|
96
|
+
senderId: 'SKOOLYTE',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
// Smart routing based on phone prefix
|
|
100
|
+
routing: {
|
|
101
|
+
'+92': 'fallback', // Pakistan -> local provider
|
|
102
|
+
'*': 'primary', // All others -> Twilio
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// WhatsApp Configuration
|
|
107
|
+
whatsapp: {
|
|
108
|
+
provider: 'meta',
|
|
109
|
+
config: {
|
|
110
|
+
accessToken: process.env.META_ACCESS_TOKEN!,
|
|
111
|
+
phoneNumberId: process.env.WHATSAPP_PHONE_ID!,
|
|
112
|
+
businessAccountId: process.env.WHATSAPP_BUSINESS_ID,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Email Configuration
|
|
117
|
+
email: {
|
|
118
|
+
provider: 'ses',
|
|
119
|
+
config: {
|
|
120
|
+
region: 'us-east-1',
|
|
121
|
+
fromAddress: 'noreply@example.com',
|
|
122
|
+
fromName: 'My App',
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
// Retry Configuration
|
|
127
|
+
retry: {
|
|
128
|
+
maxAttempts: 3,
|
|
129
|
+
backoffMs: 1000,
|
|
130
|
+
backoffMultiplier: 2,
|
|
131
|
+
maxBackoffMs: 30000,
|
|
132
|
+
jitter: true,
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Logger (optional)
|
|
136
|
+
logger: console, // or winston, pino, etc.
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Sending SMS
|
|
141
|
+
|
|
142
|
+
### Simple SMS
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// Send to a single recipient
|
|
146
|
+
const result = await notifyHub.sms('+923001234567', 'Your OTP is 123456');
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### SMS with Options
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
const result = await notifyHub.sms('+923001234567', 'Hello!', {
|
|
153
|
+
from: '+1555000000', // Override from number
|
|
154
|
+
metadata: { userId: '123' }, // Custom metadata
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Bulk SMS
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
const messages = [
|
|
162
|
+
{ to: '+923001234567', message: 'Hello John!' },
|
|
163
|
+
{ to: '+923001234568', message: 'Hello Jane!' },
|
|
164
|
+
{ to: '+923001234569', message: 'Hello Bob!' },
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const result = await notifyHub.bulkSms(messages, {
|
|
168
|
+
rateLimit: { messagesPerSecond: 10 },
|
|
169
|
+
concurrency: 5,
|
|
170
|
+
stopOnError: false,
|
|
171
|
+
onProgress: (sent, total) => {
|
|
172
|
+
console.log(`Progress: ${sent}/${total}`);
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
console.log(`Sent: ${result.summary.sent}, Failed: ${result.summary.failed}`);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Get Delivery Status
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const status = await notifyHub.getSmsStatus('SM1234567890');
|
|
183
|
+
// Returns: 'queued' | 'sent' | 'delivered' | 'failed' | 'undelivered'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Sending WhatsApp Messages
|
|
187
|
+
|
|
188
|
+
### Template Messages
|
|
189
|
+
|
|
190
|
+
Template messages can be sent anytime (no 24-hour window restriction):
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
const result = await notifyHub.whatsappTemplate('+923001234567', {
|
|
194
|
+
name: 'appointment_reminder',
|
|
195
|
+
language: 'en',
|
|
196
|
+
variables: {
|
|
197
|
+
'1': 'John',
|
|
198
|
+
'2': 'Dr. Smith',
|
|
199
|
+
'3': 'Tomorrow at 10 AM',
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Text Messages
|
|
205
|
+
|
|
206
|
+
Text messages require an open conversation (24-hour window):
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
const result = await notifyHub.whatsapp('+923001234567', 'Thank you for your message!');
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Sending Email
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
const result = await notifyHub.email('user@example.com', {
|
|
216
|
+
subject: 'Welcome!',
|
|
217
|
+
body: 'Plain text content',
|
|
218
|
+
html: '<h1>Welcome!</h1><p>HTML content</p>',
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Generic Send
|
|
223
|
+
|
|
224
|
+
Use the `send` method for channel-agnostic sending:
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// SMS
|
|
228
|
+
await notifyHub.send({
|
|
229
|
+
channel: 'sms',
|
|
230
|
+
to: '+923001234567',
|
|
231
|
+
message: 'Hello!',
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// WhatsApp Template
|
|
235
|
+
await notifyHub.send({
|
|
236
|
+
channel: 'whatsapp',
|
|
237
|
+
to: '+923001234567',
|
|
238
|
+
template: {
|
|
239
|
+
name: 'welcome',
|
|
240
|
+
language: 'en',
|
|
241
|
+
variables: ['John'],
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Email
|
|
246
|
+
await notifyHub.send({
|
|
247
|
+
channel: 'email',
|
|
248
|
+
to: 'user@example.com',
|
|
249
|
+
email: {
|
|
250
|
+
subject: 'Hello',
|
|
251
|
+
body: 'Hello!',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Smart Routing
|
|
257
|
+
|
|
258
|
+
Route messages to different providers based on phone number prefix:
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
const notifyHub = new NotifyHub({
|
|
262
|
+
sms: {
|
|
263
|
+
primary: { provider: 'twilio', config: { ... } },
|
|
264
|
+
fallback: { provider: 'local', config: { provider: 'telenor', ... } },
|
|
265
|
+
routing: {
|
|
266
|
+
'+92': 'fallback', // Pakistan numbers -> local provider (cheaper)
|
|
267
|
+
'+1': 'primary', // US numbers -> Twilio
|
|
268
|
+
'*': 'primary', // Default -> Twilio
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// This uses local provider (Pakistan number)
|
|
274
|
+
await notifyHub.sms('+923001234567', 'Hello!');
|
|
275
|
+
|
|
276
|
+
// This uses Twilio (US number)
|
|
277
|
+
await notifyHub.sms('+14155551234', 'Hello!');
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Automatic Fallback
|
|
281
|
+
|
|
282
|
+
If the primary provider fails with a retriable error, NotifyHub automatically tries the fallback:
|
|
283
|
+
|
|
284
|
+
```typescript
|
|
285
|
+
const notifyHub = new NotifyHub({
|
|
286
|
+
sms: {
|
|
287
|
+
primary: { provider: 'local', config: { ... } },
|
|
288
|
+
fallback: { provider: 'twilio', config: { ... } },
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
// If local provider fails, automatically retries with Twilio
|
|
293
|
+
await notifyHub.sms('+923001234567', 'Hello!');
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Custom Providers
|
|
297
|
+
|
|
298
|
+
Implement your own provider:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
import { SmsProvider, SendResult } from '@skoolite/notify-hub';
|
|
302
|
+
|
|
303
|
+
class MySmsProvider implements SmsProvider {
|
|
304
|
+
readonly name = 'my-provider';
|
|
305
|
+
|
|
306
|
+
async send(to: string, message: string): Promise<SendResult> {
|
|
307
|
+
// Your implementation
|
|
308
|
+
return {
|
|
309
|
+
success: true,
|
|
310
|
+
messageId: 'my-msg-id',
|
|
311
|
+
channel: 'sms',
|
|
312
|
+
status: 'sent',
|
|
313
|
+
provider: this.name,
|
|
314
|
+
timestamp: new Date(),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async getStatus(messageId: string): Promise<MessageStatus> {
|
|
319
|
+
// Your implementation
|
|
320
|
+
return 'delivered';
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const notifyHub = new NotifyHub({
|
|
325
|
+
sms: {
|
|
326
|
+
primary: {
|
|
327
|
+
provider: 'custom',
|
|
328
|
+
instance: new MySmsProvider(),
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
## NestJS Integration
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
// notify-hub.module.ts
|
|
338
|
+
import { Module, Global } from '@nestjs/common';
|
|
339
|
+
import { ConfigService } from '@nestjs/config';
|
|
340
|
+
import { NotifyHub } from '@skoolite/notify-hub';
|
|
341
|
+
|
|
342
|
+
@Global()
|
|
343
|
+
@Module({
|
|
344
|
+
providers: [
|
|
345
|
+
{
|
|
346
|
+
provide: NotifyHub,
|
|
347
|
+
useFactory: (config: ConfigService) => {
|
|
348
|
+
return new NotifyHub({
|
|
349
|
+
sms: {
|
|
350
|
+
primary: {
|
|
351
|
+
provider: 'twilio',
|
|
352
|
+
config: {
|
|
353
|
+
accountSid: config.getOrThrow('TWILIO_ACCOUNT_SID'),
|
|
354
|
+
authToken: config.getOrThrow('TWILIO_AUTH_TOKEN'),
|
|
355
|
+
fromNumber: config.getOrThrow('TWILIO_FROM_NUMBER'),
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
inject: [ConfigService],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
exports: [NotifyHub],
|
|
365
|
+
})
|
|
366
|
+
export class NotifyHubModule {}
|
|
367
|
+
|
|
368
|
+
// appointment.service.ts
|
|
369
|
+
import { Injectable } from '@nestjs/common';
|
|
370
|
+
import { NotifyHub } from '@skoolite/notify-hub';
|
|
371
|
+
|
|
372
|
+
@Injectable()
|
|
373
|
+
export class AppointmentService {
|
|
374
|
+
constructor(private readonly notifyHub: NotifyHub) {}
|
|
375
|
+
|
|
376
|
+
async sendReminder(phone: string, message: string) {
|
|
377
|
+
return this.notifyHub.sms(phone, message);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Utility Functions
|
|
383
|
+
|
|
384
|
+
NotifyHub exports useful utility functions:
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import {
|
|
388
|
+
validatePhoneNumber,
|
|
389
|
+
toE164,
|
|
390
|
+
isPakistanMobile,
|
|
391
|
+
getPakistanNetwork,
|
|
392
|
+
withRetry,
|
|
393
|
+
} from '@skoolite/notify-hub';
|
|
394
|
+
|
|
395
|
+
// Validate and parse phone number
|
|
396
|
+
const result = validatePhoneNumber('03001234567', 'PK');
|
|
397
|
+
console.log(result.e164); // '+923001234567'
|
|
398
|
+
|
|
399
|
+
// Convert to E.164 format
|
|
400
|
+
const e164 = toE164('0300-1234567', 'PK');
|
|
401
|
+
console.log(e164); // '+923001234567'
|
|
402
|
+
|
|
403
|
+
// Check if Pakistan mobile
|
|
404
|
+
console.log(isPakistanMobile('+923001234567')); // true
|
|
405
|
+
|
|
406
|
+
// Get Pakistan network
|
|
407
|
+
console.log(getPakistanNetwork('+923451234567')); // 'telenor'
|
|
408
|
+
|
|
409
|
+
// Retry with exponential backoff
|
|
410
|
+
const data = await withRetry(
|
|
411
|
+
() => fetchData(),
|
|
412
|
+
{ maxAttempts: 3, backoffMs: 1000, backoffMultiplier: 2, jitter: true }
|
|
413
|
+
);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
## API Reference
|
|
417
|
+
|
|
418
|
+
### NotifyHub
|
|
419
|
+
|
|
420
|
+
| Method | Description |
|
|
421
|
+
|--------|-------------|
|
|
422
|
+
| `sms(to, message, options?)` | Send SMS |
|
|
423
|
+
| `bulkSms(messages, options?)` | Send bulk SMS |
|
|
424
|
+
| `getSmsStatus(messageId)` | Get SMS delivery status |
|
|
425
|
+
| `whatsapp(to, message, options?)` | Send WhatsApp text message |
|
|
426
|
+
| `whatsappTemplate(to, template, options?)` | Send WhatsApp template |
|
|
427
|
+
| `bulkWhatsApp(messages, options?)` | Send bulk WhatsApp |
|
|
428
|
+
| `email(to, email, options?)` | Send email |
|
|
429
|
+
| `bulkEmail(messages, options?)` | Send bulk email |
|
|
430
|
+
| `send(notification)` | Generic send |
|
|
431
|
+
| `initialize()` | Initialize providers (auto-called) |
|
|
432
|
+
| `destroy()` | Cleanup resources |
|
|
433
|
+
|
|
434
|
+
### SendResult
|
|
435
|
+
|
|
436
|
+
```typescript
|
|
437
|
+
interface SendResult {
|
|
438
|
+
success: boolean;
|
|
439
|
+
messageId?: string;
|
|
440
|
+
channel: 'sms' | 'whatsapp' | 'email';
|
|
441
|
+
status: 'queued' | 'sent' | 'delivered' | 'failed' | 'undelivered';
|
|
442
|
+
provider: string;
|
|
443
|
+
error?: { code: string; message: string; retriable: boolean };
|
|
444
|
+
cost?: { amount: number; currency: string };
|
|
445
|
+
timestamp: Date;
|
|
446
|
+
metadata?: Record<string, unknown>;
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## License
|
|
451
|
+
|
|
452
|
+
MIT
|