servcraft 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/.claude/settings.local.json +29 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/README.md +1070 -1
  9. package/dist/cli/index.cjs +2026 -2168
  10. package/dist/cli/index.cjs.map +1 -1
  11. package/dist/cli/index.js +2026 -2168
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/index.cjs +595 -616
  14. package/dist/index.cjs.map +1 -1
  15. package/dist/index.d.cts +114 -52
  16. package/dist/index.d.ts +114 -52
  17. package/dist/index.js +595 -616
  18. package/dist/index.js.map +1 -1
  19. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  20. package/docs/DATABASE_MULTI_ORM.md +399 -0
  21. package/docs/PHASE1_BREAKDOWN.md +346 -0
  22. package/docs/PROGRESS.md +550 -0
  23. package/docs/modules/ANALYTICS.md +226 -0
  24. package/docs/modules/API-VERSIONING.md +252 -0
  25. package/docs/modules/AUDIT.md +192 -0
  26. package/docs/modules/AUTH.md +431 -0
  27. package/docs/modules/CACHE.md +346 -0
  28. package/docs/modules/EMAIL.md +254 -0
  29. package/docs/modules/FEATURE-FLAG.md +291 -0
  30. package/docs/modules/I18N.md +294 -0
  31. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  32. package/docs/modules/MFA.md +266 -0
  33. package/docs/modules/NOTIFICATION.md +311 -0
  34. package/docs/modules/OAUTH.md +237 -0
  35. package/docs/modules/PAYMENT.md +804 -0
  36. package/docs/modules/QUEUE.md +540 -0
  37. package/docs/modules/RATE-LIMIT.md +339 -0
  38. package/docs/modules/SEARCH.md +288 -0
  39. package/docs/modules/SECURITY.md +327 -0
  40. package/docs/modules/SESSION.md +382 -0
  41. package/docs/modules/SWAGGER.md +305 -0
  42. package/docs/modules/UPLOAD.md +296 -0
  43. package/docs/modules/USER.md +505 -0
  44. package/docs/modules/VALIDATION.md +294 -0
  45. package/docs/modules/WEBHOOK.md +270 -0
  46. package/docs/modules/WEBSOCKET.md +691 -0
  47. package/package.json +53 -38
  48. package/prisma/schema.prisma +395 -1
  49. package/src/cli/commands/add-module.ts +520 -87
  50. package/src/cli/commands/db.ts +3 -4
  51. package/src/cli/commands/docs.ts +256 -6
  52. package/src/cli/commands/generate.ts +12 -19
  53. package/src/cli/commands/init.ts +384 -214
  54. package/src/cli/index.ts +0 -4
  55. package/src/cli/templates/repository.ts +6 -1
  56. package/src/cli/templates/routes.ts +6 -21
  57. package/src/cli/utils/docs-generator.ts +6 -7
  58. package/src/cli/utils/env-manager.ts +717 -0
  59. package/src/cli/utils/field-parser.ts +16 -7
  60. package/src/cli/utils/interactive-prompt.ts +223 -0
  61. package/src/cli/utils/template-manager.ts +346 -0
  62. package/src/config/database.config.ts +183 -0
  63. package/src/config/env.ts +0 -10
  64. package/src/config/index.ts +0 -14
  65. package/src/core/server.ts +1 -1
  66. package/src/database/adapters/mongoose.adapter.ts +132 -0
  67. package/src/database/adapters/prisma.adapter.ts +118 -0
  68. package/src/database/connection.ts +190 -0
  69. package/src/database/interfaces/database.interface.ts +85 -0
  70. package/src/database/interfaces/index.ts +7 -0
  71. package/src/database/interfaces/repository.interface.ts +129 -0
  72. package/src/database/models/mongoose/index.ts +7 -0
  73. package/src/database/models/mongoose/payment.schema.ts +347 -0
  74. package/src/database/models/mongoose/user.schema.ts +154 -0
  75. package/src/database/prisma.ts +1 -4
  76. package/src/database/redis.ts +101 -0
  77. package/src/database/repositories/mongoose/index.ts +7 -0
  78. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  79. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  80. package/src/database/seed.ts +6 -1
  81. package/src/index.ts +9 -20
  82. package/src/middleware/security.ts +2 -6
  83. package/src/modules/analytics/analytics.routes.ts +80 -0
  84. package/src/modules/analytics/analytics.service.ts +364 -0
  85. package/src/modules/analytics/index.ts +18 -0
  86. package/src/modules/analytics/types.ts +180 -0
  87. package/src/modules/api-versioning/index.ts +15 -0
  88. package/src/modules/api-versioning/types.ts +86 -0
  89. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  90. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  91. package/src/modules/api-versioning/versioning.service.ts +189 -0
  92. package/src/modules/audit/audit.repository.ts +206 -0
  93. package/src/modules/audit/audit.service.ts +27 -59
  94. package/src/modules/auth/auth.controller.ts +2 -2
  95. package/src/modules/auth/auth.middleware.ts +3 -9
  96. package/src/modules/auth/auth.routes.ts +10 -107
  97. package/src/modules/auth/auth.service.ts +126 -23
  98. package/src/modules/auth/index.ts +3 -4
  99. package/src/modules/cache/cache.service.ts +367 -0
  100. package/src/modules/cache/index.ts +10 -0
  101. package/src/modules/cache/types.ts +44 -0
  102. package/src/modules/email/email.service.ts +3 -10
  103. package/src/modules/email/templates.ts +2 -8
  104. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  105. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  106. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  107. package/src/modules/feature-flag/index.ts +20 -0
  108. package/src/modules/feature-flag/types.ts +192 -0
  109. package/src/modules/i18n/i18n.middleware.ts +186 -0
  110. package/src/modules/i18n/i18n.routes.ts +191 -0
  111. package/src/modules/i18n/i18n.service.ts +456 -0
  112. package/src/modules/i18n/index.ts +18 -0
  113. package/src/modules/i18n/types.ts +118 -0
  114. package/src/modules/media-processing/index.ts +17 -0
  115. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  116. package/src/modules/media-processing/media-processing.service.ts +245 -0
  117. package/src/modules/media-processing/types.ts +156 -0
  118. package/src/modules/mfa/index.ts +20 -0
  119. package/src/modules/mfa/mfa.repository.ts +206 -0
  120. package/src/modules/mfa/mfa.routes.ts +595 -0
  121. package/src/modules/mfa/mfa.service.ts +572 -0
  122. package/src/modules/mfa/totp.ts +150 -0
  123. package/src/modules/mfa/types.ts +57 -0
  124. package/src/modules/notification/index.ts +20 -0
  125. package/src/modules/notification/notification.repository.ts +356 -0
  126. package/src/modules/notification/notification.service.ts +483 -0
  127. package/src/modules/notification/types.ts +119 -0
  128. package/src/modules/oauth/index.ts +20 -0
  129. package/src/modules/oauth/oauth.repository.ts +219 -0
  130. package/src/modules/oauth/oauth.routes.ts +446 -0
  131. package/src/modules/oauth/oauth.service.ts +293 -0
  132. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  133. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  134. package/src/modules/oauth/providers/github.provider.ts +248 -0
  135. package/src/modules/oauth/providers/google.provider.ts +189 -0
  136. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  137. package/src/modules/oauth/types.ts +94 -0
  138. package/src/modules/payment/index.ts +19 -0
  139. package/src/modules/payment/payment.repository.ts +733 -0
  140. package/src/modules/payment/payment.routes.ts +390 -0
  141. package/src/modules/payment/payment.service.ts +354 -0
  142. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  143. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  144. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  145. package/src/modules/payment/types.ts +140 -0
  146. package/src/modules/queue/cron.ts +438 -0
  147. package/src/modules/queue/index.ts +87 -0
  148. package/src/modules/queue/queue.routes.ts +600 -0
  149. package/src/modules/queue/queue.service.ts +842 -0
  150. package/src/modules/queue/types.ts +222 -0
  151. package/src/modules/queue/workers.ts +366 -0
  152. package/src/modules/rate-limit/index.ts +59 -0
  153. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  154. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  155. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  156. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  157. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  158. package/src/modules/rate-limit/types.ts +153 -0
  159. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  160. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  161. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  162. package/src/modules/search/index.ts +21 -0
  163. package/src/modules/search/search.service.ts +234 -0
  164. package/src/modules/search/types.ts +214 -0
  165. package/src/modules/security/index.ts +40 -0
  166. package/src/modules/security/sanitize.ts +223 -0
  167. package/src/modules/security/security-audit.service.ts +388 -0
  168. package/src/modules/security/security.middleware.ts +398 -0
  169. package/src/modules/session/index.ts +3 -0
  170. package/src/modules/session/session.repository.ts +159 -0
  171. package/src/modules/session/session.service.ts +340 -0
  172. package/src/modules/session/types.ts +38 -0
  173. package/src/modules/swagger/index.ts +7 -1
  174. package/src/modules/swagger/schema-builder.ts +16 -4
  175. package/src/modules/swagger/swagger.service.ts +9 -10
  176. package/src/modules/swagger/types.ts +0 -2
  177. package/src/modules/upload/index.ts +14 -0
  178. package/src/modules/upload/types.ts +83 -0
  179. package/src/modules/upload/upload.repository.ts +199 -0
  180. package/src/modules/upload/upload.routes.ts +311 -0
  181. package/src/modules/upload/upload.service.ts +448 -0
  182. package/src/modules/user/index.ts +3 -3
  183. package/src/modules/user/user.controller.ts +15 -9
  184. package/src/modules/user/user.repository.ts +237 -113
  185. package/src/modules/user/user.routes.ts +39 -164
  186. package/src/modules/user/user.service.ts +4 -3
  187. package/src/modules/validation/validator.ts +12 -17
  188. package/src/modules/webhook/index.ts +91 -0
  189. package/src/modules/webhook/retry.ts +196 -0
  190. package/src/modules/webhook/signature.ts +135 -0
  191. package/src/modules/webhook/types.ts +181 -0
  192. package/src/modules/webhook/webhook.repository.ts +358 -0
  193. package/src/modules/webhook/webhook.routes.ts +442 -0
  194. package/src/modules/webhook/webhook.service.ts +457 -0
  195. package/src/modules/websocket/features.ts +504 -0
  196. package/src/modules/websocket/index.ts +106 -0
  197. package/src/modules/websocket/middlewares.ts +298 -0
  198. package/src/modules/websocket/types.ts +181 -0
  199. package/src/modules/websocket/websocket.service.ts +692 -0
  200. package/src/utils/errors.ts +7 -0
  201. package/src/utils/pagination.ts +4 -1
  202. package/tests/helpers/db-check.ts +79 -0
  203. package/tests/integration/auth-redis.test.ts +94 -0
  204. package/tests/integration/cache-redis.test.ts +387 -0
  205. package/tests/integration/mongoose-repositories.test.ts +410 -0
  206. package/tests/integration/payment-prisma.test.ts +637 -0
  207. package/tests/integration/queue-bullmq.test.ts +417 -0
  208. package/tests/integration/user-prisma.test.ts +441 -0
  209. package/tests/integration/websocket-socketio.test.ts +552 -0
  210. package/tests/setup.ts +11 -9
  211. package/vitest.config.ts +3 -8
  212. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  213. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  216. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,483 @@
1
+ import { logger } from '../../core/logger.js';
2
+ import { BadRequestError } from '../../utils/errors.js';
3
+ import { prisma } from '../../database/prisma.js';
4
+ import { NotificationRepository } from './notification.repository.js';
5
+ import type {
6
+ Notification,
7
+ NotificationConfig,
8
+ NotificationChannel,
9
+ EmailMessage,
10
+ SMSMessage,
11
+ PushMessage,
12
+ WebhookMessage,
13
+ NotificationTemplate,
14
+ } from './types.js';
15
+
16
+ /**
17
+ * Notification Service
18
+ * Manages multi-channel notifications with Prisma persistence
19
+ *
20
+ * Features:
21
+ * - Multi-channel support (email, SMS, push, webhook, in-app)
22
+ * - Persistent notification storage in database
23
+ * - Template system with variable substitution
24
+ * - Multiple provider support per channel
25
+ */
26
+ export class NotificationService {
27
+ private config: NotificationConfig;
28
+ private repository: NotificationRepository;
29
+
30
+ constructor(config: NotificationConfig = {}) {
31
+ this.config = config;
32
+ this.repository = new NotificationRepository(prisma);
33
+ }
34
+
35
+ // ==========================================
36
+ // SEND NOTIFICATIONS
37
+ // ==========================================
38
+
39
+ /**
40
+ * Send a notification through a specific channel
41
+ */
42
+ async send(
43
+ userId: string,
44
+ channel: NotificationChannel,
45
+ title: string,
46
+ body: string,
47
+ data?: Record<string, unknown>
48
+ ): Promise<Notification> {
49
+ // Create notification record
50
+ const notification = await this.repository.createNotification({
51
+ userId,
52
+ channel,
53
+ status: 'pending',
54
+ title,
55
+ body,
56
+ data,
57
+ });
58
+
59
+ try {
60
+ switch (channel) {
61
+ case 'email':
62
+ await this.sendEmail({ to: data?.email as string, subject: title, text: body });
63
+ break;
64
+ case 'sms':
65
+ await this.sendSMS({ to: data?.phone as string, body });
66
+ break;
67
+ case 'push':
68
+ await this.sendPush({ tokens: data?.tokens as string[], title, body });
69
+ break;
70
+ case 'webhook':
71
+ await this.sendWebhook({ url: data?.url as string, body: { title, body, data } });
72
+ break;
73
+ case 'in_app':
74
+ // Just stored in database
75
+ break;
76
+ }
77
+
78
+ // Update status to sent
79
+ const updated = await this.repository.updateNotification(notification.id, {
80
+ status: 'sent',
81
+ sentAt: new Date(),
82
+ });
83
+
84
+ return updated || notification;
85
+ } catch (error) {
86
+ // Update status to failed
87
+ await this.repository.updateNotification(notification.id, {
88
+ status: 'failed',
89
+ });
90
+
91
+ logger.error({ error, notificationId: notification.id }, 'Failed to send notification');
92
+ return { ...notification, status: 'failed' };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Send notification to user through multiple channels
98
+ */
99
+ async sendToUser(
100
+ userId: string,
101
+ channels: NotificationChannel[],
102
+ title: string,
103
+ body: string,
104
+ data?: Record<string, unknown>
105
+ ): Promise<Notification[]> {
106
+ const results: Notification[] = [];
107
+ for (const channel of channels) {
108
+ const notification = await this.send(userId, channel, title, body, data);
109
+ results.push(notification);
110
+ }
111
+ return results;
112
+ }
113
+
114
+ // ==========================================
115
+ // EMAIL METHODS
116
+ // ==========================================
117
+
118
+ async sendEmail(message: EmailMessage): Promise<void> {
119
+ if (!this.config.email) {
120
+ throw new BadRequestError('Email not configured');
121
+ }
122
+
123
+ const { provider } = this.config.email;
124
+
125
+ switch (provider) {
126
+ case 'sendgrid':
127
+ await this.sendEmailViaSendGrid(message);
128
+ break;
129
+ case 'resend':
130
+ await this.sendEmailViaResend(message);
131
+ break;
132
+ case 'mailgun':
133
+ await this.sendEmailViaMailgun(message);
134
+ break;
135
+ case 'ses':
136
+ await this.sendEmailViaSES(message);
137
+ break;
138
+ default:
139
+ await this.sendEmailViaSMTP(message);
140
+ }
141
+
142
+ logger.info({ to: message.to, subject: message.subject }, 'Email sent');
143
+ }
144
+
145
+ private async sendEmailViaSendGrid(message: EmailMessage): Promise<void> {
146
+ const config = this.config.email!.sendgrid!;
147
+ const body = await this.renderTemplate(message);
148
+
149
+ await fetch('https://api.sendgrid.com/v3/mail/send', {
150
+ method: 'POST',
151
+ headers: {
152
+ Authorization: `Bearer ${config.apiKey}`,
153
+ 'Content-Type': 'application/json',
154
+ },
155
+ body: JSON.stringify({
156
+ personalizations: [
157
+ {
158
+ to: Array.isArray(message.to)
159
+ ? message.to.map((e) => ({ email: e }))
160
+ : [{ email: message.to }],
161
+ },
162
+ ],
163
+ from: { email: this.config.email!.from },
164
+ subject: message.subject,
165
+ content: [{ type: body.html ? 'text/html' : 'text/plain', value: body.html || body.text }],
166
+ }),
167
+ });
168
+ }
169
+
170
+ private async sendEmailViaResend(message: EmailMessage): Promise<void> {
171
+ const config = this.config.email!.resend!;
172
+ const body = await this.renderTemplate(message);
173
+
174
+ await fetch('https://api.resend.com/emails', {
175
+ method: 'POST',
176
+ headers: {
177
+ Authorization: `Bearer ${config.apiKey}`,
178
+ 'Content-Type': 'application/json',
179
+ },
180
+ body: JSON.stringify({
181
+ from: this.config.email!.from,
182
+ to: Array.isArray(message.to) ? message.to : [message.to],
183
+ subject: message.subject,
184
+ html: body.html,
185
+ text: body.text,
186
+ }),
187
+ });
188
+ }
189
+
190
+ private async sendEmailViaMailgun(message: EmailMessage): Promise<void> {
191
+ const config = this.config.email!.mailgun!;
192
+ const body = await this.renderTemplate(message);
193
+
194
+ const formData = new FormData();
195
+ formData.append('from', this.config.email!.from);
196
+ formData.append('to', Array.isArray(message.to) ? message.to.join(',') : message.to);
197
+ formData.append('subject', message.subject);
198
+ if (body.html) formData.append('html', body.html);
199
+ if (body.text) formData.append('text', body.text);
200
+
201
+ await fetch(`https://api.mailgun.net/v3/${config.domain}/messages`, {
202
+ method: 'POST',
203
+ headers: {
204
+ Authorization: `Basic ${Buffer.from(`api:${config.apiKey}`).toString('base64')}`,
205
+ },
206
+ body: formData,
207
+ });
208
+ }
209
+
210
+ private async sendEmailViaSES(_message: EmailMessage): Promise<void> {
211
+ // AWS SES implementation - use @aws-sdk/client-ses in production
212
+ logger.debug('SES email would be sent');
213
+ }
214
+
215
+ private async sendEmailViaSMTP(_message: EmailMessage): Promise<void> {
216
+ // SMTP implementation - use nodemailer in production
217
+ logger.debug('SMTP email would be sent');
218
+ }
219
+
220
+ // ==========================================
221
+ // SMS METHODS
222
+ // ==========================================
223
+
224
+ async sendSMS(message: SMSMessage): Promise<void> {
225
+ if (!this.config.sms) {
226
+ throw new BadRequestError('SMS not configured');
227
+ }
228
+
229
+ const { provider } = this.config.sms;
230
+
231
+ switch (provider) {
232
+ case 'twilio':
233
+ await this.sendSMSViaTwilio(message);
234
+ break;
235
+ case 'nexmo':
236
+ await this.sendSMSViaNexmo(message);
237
+ break;
238
+ case 'africas_talking':
239
+ await this.sendSMSViaAfricasTalking(message);
240
+ break;
241
+ }
242
+
243
+ logger.info({ to: message.to }, 'SMS sent');
244
+ }
245
+
246
+ private async sendSMSViaTwilio(message: SMSMessage): Promise<void> {
247
+ const config = this.config.sms!.twilio!;
248
+ const recipients = Array.isArray(message.to) ? message.to : [message.to];
249
+
250
+ for (const to of recipients) {
251
+ await fetch(`https://api.twilio.com/2010-04-01/Accounts/${config.accountSid}/Messages.json`, {
252
+ method: 'POST',
253
+ headers: {
254
+ Authorization: `Basic ${Buffer.from(`${config.accountSid}:${config.authToken}`).toString('base64')}`,
255
+ 'Content-Type': 'application/x-www-form-urlencoded',
256
+ },
257
+ body: new URLSearchParams({
258
+ From: this.config.sms!.from,
259
+ To: to,
260
+ Body: message.body,
261
+ }),
262
+ });
263
+ }
264
+ }
265
+
266
+ private async sendSMSViaNexmo(message: SMSMessage): Promise<void> {
267
+ const config = this.config.sms!.nexmo!;
268
+ const recipients = Array.isArray(message.to) ? message.to : [message.to];
269
+
270
+ for (const to of recipients) {
271
+ await fetch('https://rest.nexmo.com/sms/json', {
272
+ method: 'POST',
273
+ headers: { 'Content-Type': 'application/json' },
274
+ body: JSON.stringify({
275
+ api_key: config.apiKey,
276
+ api_secret: config.apiSecret,
277
+ from: this.config.sms!.from,
278
+ to,
279
+ text: message.body,
280
+ }),
281
+ });
282
+ }
283
+ }
284
+
285
+ private async sendSMSViaAfricasTalking(message: SMSMessage): Promise<void> {
286
+ const config = this.config.sms!.africasTalking!;
287
+ const recipients = Array.isArray(message.to) ? message.to.join(',') : message.to;
288
+
289
+ await fetch('https://api.africastalking.com/version1/messaging', {
290
+ method: 'POST',
291
+ headers: {
292
+ Accept: 'application/json',
293
+ 'Content-Type': 'application/x-www-form-urlencoded',
294
+ apiKey: config.apiKey,
295
+ },
296
+ body: new URLSearchParams({
297
+ username: config.username,
298
+ to: recipients,
299
+ message: message.body,
300
+ from: this.config.sms!.from,
301
+ }),
302
+ });
303
+ }
304
+
305
+ // ==========================================
306
+ // PUSH NOTIFICATION METHODS
307
+ // ==========================================
308
+
309
+ async sendPush(message: PushMessage): Promise<void> {
310
+ if (!this.config.push) {
311
+ throw new BadRequestError('Push notifications not configured');
312
+ }
313
+
314
+ const { provider } = this.config.push;
315
+
316
+ switch (provider) {
317
+ case 'firebase':
318
+ await this.sendPushViaFirebase(message);
319
+ break;
320
+ case 'onesignal':
321
+ await this.sendPushViaOneSignal(message);
322
+ break;
323
+ }
324
+
325
+ logger.info({ tokens: message.tokens.length }, 'Push notification sent');
326
+ }
327
+
328
+ private async sendPushViaFirebase(message: PushMessage): Promise<void> {
329
+ // Firebase Cloud Messaging - use firebase-admin in production
330
+ logger.debug({ message }, 'Firebase push would be sent');
331
+ }
332
+
333
+ private async sendPushViaOneSignal(message: PushMessage): Promise<void> {
334
+ const config = this.config.push!.onesignal!;
335
+
336
+ await fetch('https://onesignal.com/api/v1/notifications', {
337
+ method: 'POST',
338
+ headers: {
339
+ Authorization: `Basic ${config.apiKey}`,
340
+ 'Content-Type': 'application/json',
341
+ },
342
+ body: JSON.stringify({
343
+ app_id: config.appId,
344
+ include_player_ids: message.tokens,
345
+ headings: { en: message.title },
346
+ contents: { en: message.body },
347
+ data: message.data,
348
+ ios_badgeType: 'SetTo',
349
+ ios_badgeCount: message.badge,
350
+ }),
351
+ });
352
+ }
353
+
354
+ // ==========================================
355
+ // WEBHOOK METHODS
356
+ // ==========================================
357
+
358
+ async sendWebhook(message: WebhookMessage): Promise<void> {
359
+ const config = this.config.webhook || {};
360
+ const method = message.method || 'POST';
361
+
362
+ const response = await fetch(message.url, {
363
+ method,
364
+ headers: {
365
+ 'Content-Type': 'application/json',
366
+ ...config.defaultHeaders,
367
+ ...message.headers,
368
+ },
369
+ body: JSON.stringify(message.body),
370
+ signal: AbortSignal.timeout(config.timeout || 30000),
371
+ });
372
+
373
+ if (!response.ok) {
374
+ throw new Error(`Webhook failed: ${response.status}`);
375
+ }
376
+
377
+ logger.info({ url: message.url, method }, 'Webhook sent');
378
+ }
379
+
380
+ // ==========================================
381
+ // TEMPLATE METHODS
382
+ // ==========================================
383
+
384
+ /**
385
+ * Register a notification template
386
+ */
387
+ async registerTemplate(
388
+ template: Omit<NotificationTemplate, 'id'>
389
+ ): Promise<NotificationTemplate> {
390
+ return this.repository.createTemplate(template);
391
+ }
392
+
393
+ /**
394
+ * Get template by name
395
+ */
396
+ async getTemplate(name: string): Promise<NotificationTemplate | null> {
397
+ return this.repository.getTemplateByName(name);
398
+ }
399
+
400
+ /**
401
+ * Get all templates
402
+ */
403
+ async getAllTemplates(): Promise<NotificationTemplate[]> {
404
+ return this.repository.getAllTemplates();
405
+ }
406
+
407
+ private async renderTemplate(message: EmailMessage): Promise<{ text?: string; html?: string }> {
408
+ if (!message.template) {
409
+ return { text: message.text, html: message.html };
410
+ }
411
+
412
+ const template = await this.repository.getTemplateByName(message.template);
413
+ if (!template) {
414
+ return { text: message.text, html: message.html };
415
+ }
416
+
417
+ let rendered = template.body;
418
+ for (const [key, value] of Object.entries(message.templateData || {})) {
419
+ rendered = rendered.replace(new RegExp(`{{${key}}}`, 'g'), String(value));
420
+ }
421
+
422
+ return { html: rendered, text: rendered.replace(/<[^>]*>/g, '') };
423
+ }
424
+
425
+ // ==========================================
426
+ // IN-APP NOTIFICATION METHODS
427
+ // ==========================================
428
+
429
+ /**
430
+ * Get user's notifications
431
+ */
432
+ async getUserNotifications(
433
+ userId: string,
434
+ options?: { limit?: number; offset?: number }
435
+ ): Promise<Notification[]> {
436
+ return this.repository.getNotificationsByUserId(userId, options);
437
+ }
438
+
439
+ /**
440
+ * Get unread count for user
441
+ */
442
+ async getUnreadCount(userId: string): Promise<number> {
443
+ return this.repository.getUnreadCount(userId);
444
+ }
445
+
446
+ /**
447
+ * Mark notification as read
448
+ */
449
+ async markAsRead(notificationId: string): Promise<Notification | null> {
450
+ return this.repository.markAsRead(notificationId);
451
+ }
452
+
453
+ /**
454
+ * Mark all notifications as read for user
455
+ */
456
+ async markAllAsRead(userId: string): Promise<number> {
457
+ return this.repository.markAllAsRead(userId);
458
+ }
459
+
460
+ /**
461
+ * Delete old notifications (cleanup)
462
+ */
463
+ async cleanupOldNotifications(olderThanDays: number = 30): Promise<number> {
464
+ const cutoffDate = new Date();
465
+ cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
466
+ return this.repository.deleteOldNotifications(cutoffDate);
467
+ }
468
+ }
469
+
470
+ // Singleton instance
471
+ let notificationService: NotificationService | null = null;
472
+
473
+ export function getNotificationService(): NotificationService {
474
+ if (!notificationService) {
475
+ notificationService = new NotificationService();
476
+ }
477
+ return notificationService;
478
+ }
479
+
480
+ export function createNotificationService(config: NotificationConfig): NotificationService {
481
+ notificationService = new NotificationService(config);
482
+ return notificationService;
483
+ }
@@ -0,0 +1,119 @@
1
+ export type NotificationChannel = 'email' | 'sms' | 'push' | 'in_app' | 'webhook';
2
+ export type NotificationStatus = 'pending' | 'sent' | 'delivered' | 'failed' | 'read';
3
+
4
+ export interface Notification {
5
+ id: string;
6
+ userId: string;
7
+ channel: NotificationChannel;
8
+ status: NotificationStatus;
9
+ title: string;
10
+ body: string;
11
+ data?: Record<string, unknown>;
12
+ sentAt?: Date;
13
+ readAt?: Date;
14
+ createdAt: Date;
15
+ }
16
+
17
+ export interface NotificationConfig {
18
+ email?: EmailConfig;
19
+ sms?: SMSConfig;
20
+ push?: PushConfig;
21
+ webhook?: WebhookConfig;
22
+ }
23
+
24
+ // Email configuration
25
+ export interface EmailConfig {
26
+ provider: 'smtp' | 'sendgrid' | 'ses' | 'mailgun' | 'resend';
27
+ from: string;
28
+ replyTo?: string;
29
+ smtp?: SMTPConfig;
30
+ sendgrid?: { apiKey: string };
31
+ ses?: { region: string; accessKeyId: string; secretAccessKey: string };
32
+ mailgun?: { apiKey: string; domain: string };
33
+ resend?: { apiKey: string };
34
+ }
35
+
36
+ export interface SMTPConfig {
37
+ host: string;
38
+ port: number;
39
+ secure: boolean;
40
+ auth: {
41
+ user: string;
42
+ pass: string;
43
+ };
44
+ }
45
+
46
+ export interface EmailMessage {
47
+ to: string | string[];
48
+ subject: string;
49
+ text?: string;
50
+ html?: string;
51
+ template?: string;
52
+ templateData?: Record<string, unknown>;
53
+ attachments?: EmailAttachment[];
54
+ cc?: string[];
55
+ bcc?: string[];
56
+ }
57
+
58
+ export interface EmailAttachment {
59
+ filename: string;
60
+ content?: Buffer | string;
61
+ path?: string;
62
+ contentType?: string;
63
+ }
64
+
65
+ // SMS configuration
66
+ export interface SMSConfig {
67
+ provider: 'twilio' | 'nexmo' | 'africas_talking';
68
+ from: string;
69
+ twilio?: { accountSid: string; authToken: string };
70
+ nexmo?: { apiKey: string; apiSecret: string };
71
+ africasTalking?: { username: string; apiKey: string };
72
+ }
73
+
74
+ export interface SMSMessage {
75
+ to: string | string[];
76
+ body: string;
77
+ }
78
+
79
+ // Push notification configuration
80
+ export interface PushConfig {
81
+ provider: 'firebase' | 'onesignal' | 'pusher';
82
+ firebase?: { serviceAccount: string | object };
83
+ onesignal?: { appId: string; apiKey: string };
84
+ pusher?: { appId: string; key: string; secret: string; cluster: string };
85
+ }
86
+
87
+ export interface PushMessage {
88
+ tokens: string[];
89
+ title: string;
90
+ body: string;
91
+ data?: Record<string, string>;
92
+ badge?: number;
93
+ sound?: string;
94
+ imageUrl?: string;
95
+ }
96
+
97
+ // Webhook configuration
98
+ export interface WebhookConfig {
99
+ defaultHeaders?: Record<string, string>;
100
+ timeout?: number;
101
+ retries?: number;
102
+ }
103
+
104
+ export interface WebhookMessage {
105
+ url: string;
106
+ method?: 'POST' | 'PUT' | 'PATCH';
107
+ headers?: Record<string, string>;
108
+ body: unknown;
109
+ }
110
+
111
+ // Template support
112
+ export interface NotificationTemplate {
113
+ id: string;
114
+ name: string;
115
+ channel: NotificationChannel;
116
+ subject?: string;
117
+ body: string;
118
+ variables: string[];
119
+ }
@@ -0,0 +1,20 @@
1
+ export { OAuthService, getOAuthService, createOAuthService } from './oauth.service.js';
2
+ export { registerOAuthRoutes } from './oauth.routes.js';
3
+ export { GoogleOAuthProvider } from './providers/google.provider.js';
4
+ export { FacebookOAuthProvider } from './providers/facebook.provider.js';
5
+ export { GitHubOAuthProvider } from './providers/github.provider.js';
6
+ export { TwitterOAuthProvider } from './providers/twitter.provider.js';
7
+ export { AppleOAuthProvider } from './providers/apple.provider.js';
8
+ export type {
9
+ OAuthConfig,
10
+ OAuthProvider,
11
+ OAuthUser,
12
+ OAuthTokens,
13
+ OAuthState,
14
+ LinkedAccount,
15
+ GoogleOAuthConfig,
16
+ FacebookOAuthConfig,
17
+ GitHubOAuthConfig,
18
+ TwitterOAuthConfig,
19
+ AppleOAuthConfig,
20
+ } from './types.js';