@develit-services/notification 0.0.4 → 0.0.6

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.
@@ -0,0 +1,562 @@
1
+ 'use strict';
2
+
3
+ const backendSdk = require('@develit-io/backend-sdk');
4
+ const z = require('zod');
5
+ const v4 = require('zod/v4');
6
+ const twilio = require('twilio');
7
+ const sqliteCore = require('drizzle-orm/sqlite-core');
8
+ require('drizzle-orm');
9
+ const cloudflare_workers = require('cloudflare:workers');
10
+ const d1 = require('drizzle-orm/d1');
11
+
12
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e.default : e; }
13
+
14
+ const z__default = /*#__PURE__*/_interopDefaultCompat(z);
15
+ const twilio__default = /*#__PURE__*/_interopDefaultCompat(twilio);
16
+
17
+ const iContactSchema = z.z.union([
18
+ z.z.string(),
19
+ z.z.object({
20
+ email: z.z.string(),
21
+ name: z.z.union([z.z.string(), z.z.undefined()])
22
+ })
23
+ ]);
24
+ const iEmailSchema = z.z.object({
25
+ to: z.z.union([iContactSchema, z.z.array(iContactSchema)]),
26
+ replyTo: z.z.union([iContactSchema, z.z.array(iContactSchema)]).optional(),
27
+ cc: z.z.union([iContactSchema, z.z.array(iContactSchema)]).optional(),
28
+ bcc: z.z.union([iContactSchema, z.z.array(iContactSchema)]).optional(),
29
+ from: iContactSchema.optional(),
30
+ subject: z.z.string(),
31
+ text: z.z.string().optional(),
32
+ html: z.z.string().optional(),
33
+ templateId: z.z.number().optional(),
34
+ templateVariables: z.z.record(z.z.string(), z.z.string()).optional()
35
+ });
36
+
37
+ class IEmailConnector {
38
+ static providerName;
39
+ API_KEY;
40
+ SMTP_HOST;
41
+ SENDER;
42
+ constructor({
43
+ API_KEY,
44
+ SMTP_HOST,
45
+ SENDER
46
+ }) {
47
+ this.API_KEY = API_KEY;
48
+ this.SMTP_HOST = SMTP_HOST;
49
+ this.SENDER = SENDER;
50
+ }
51
+ }
52
+
53
+ class EcomailConnector extends IEmailConnector {
54
+ static providerName = "ecomail";
55
+ constructor({
56
+ API_KEY,
57
+ SMTP_HOST,
58
+ SENDER
59
+ }) {
60
+ super({ API_KEY, SMTP_HOST, SENDER });
61
+ }
62
+ async sendEmail(email) {
63
+ if (email.templateVariables) {
64
+ for (const [key, value] of Object.entries(email.templateVariables)) {
65
+ if (typeof value === "string" && String(value).includes("localhost") && key in email.templateVariables) {
66
+ email.templateVariables[key] = String(value).replace("localhost", "origin");
67
+ }
68
+ }
69
+ }
70
+ const emEmail = this.convertEmail(email);
71
+ const uri = emEmail.message.template_id ? "https://api2.ecomailapp.cz/transactional/send-template" : "https://api2.ecomailapp.cz/transactional/send-message";
72
+ const [data, error] = await backendSdk.useResult(
73
+ fetch(uri, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ key: this.API_KEY
78
+ },
79
+ body: JSON.stringify(emEmail)
80
+ })
81
+ );
82
+ if (error) throw error;
83
+ return data;
84
+ }
85
+ convertEmail(email) {
86
+ const toContacts = this.convertContacts(email.to);
87
+ const ccContacts = email.cc ? this.convertContacts(email.cc) : [];
88
+ const bccContacts = email.bcc ? this.convertContacts(email.bcc) : [];
89
+ const replyTo = email.replyTo ? this.convertContacts(email.replyTo)[0] : void 0;
90
+ const from = this.convertContact(email.from || this.SENDER);
91
+ const subject = email.subject;
92
+ const textAttachments = email.text ? [{ name: "plain", type: "text/plain", content: email.text }] : [];
93
+ const htmlAttachments = email.html ? [{ type: "text/html", content: email.html, name: "email_body.html" }] : [];
94
+ const attachments = [...textAttachments, ...htmlAttachments];
95
+ const contacts = toContacts.flatMap((to) => {
96
+ const entries = [];
97
+ if (ccContacts.length > 0) {
98
+ ccContacts.forEach((cc) => {
99
+ entries.push({ email: cc?.email, name: cc?.name, cc: cc?.email });
100
+ });
101
+ }
102
+ if (bccContacts.length > 0) {
103
+ bccContacts.forEach((bcc) => {
104
+ entries.push({ email: bcc?.email, name: bcc?.name, bcc: bcc?.email });
105
+ });
106
+ }
107
+ return entries.length > 0 ? entries : [to];
108
+ });
109
+ return {
110
+ message: {
111
+ from_email: from?.email,
112
+ from_name: from.name || "",
113
+ subject,
114
+ attachments,
115
+ to: contacts,
116
+ text: email.text,
117
+ html: email.html,
118
+ reply_to: replyTo?.email,
119
+ template_id: email.templateId,
120
+ global_merge_vars: email.templateVariables ? Object.keys(email.templateVariables).map((key) => ({
121
+ name: key,
122
+ content: email.templateVariables[key]
123
+ })) : null
124
+ }
125
+ };
126
+ }
127
+ convertContacts(contacts) {
128
+ if (!contacts) {
129
+ return [];
130
+ }
131
+ const contactArray = Array.isArray(contacts) ? contacts : [contacts];
132
+ return contactArray.map(this.convertContact);
133
+ }
134
+ convertContact(contact) {
135
+ if (typeof contact === "string") {
136
+ return { email: contact, name: void 0 };
137
+ }
138
+ return { email: contact?.email, name: contact?.name };
139
+ }
140
+ }
141
+
142
+ const sendEmailInputSchema = z__default.object({
143
+ email: iEmailSchema,
144
+ metadata: z__default.object({
145
+ userAgent: z__default.string().optional(),
146
+ ip: z__default.ipv4().or(z__default.ipv6()).optional(),
147
+ initiator: z__default.object({
148
+ service: z__default.string(),
149
+ userId: z__default.string().optional()
150
+ })
151
+ })
152
+ });
153
+
154
+ const sendSlackInputSchema = v4.z.object({
155
+ slack: v4.z.object({
156
+ message: v4.z.string()
157
+ }),
158
+ metadata: v4.z.object({
159
+ userAgent: v4.z.string().optional(),
160
+ ip: v4.z.ipv4().or(v4.z.ipv6()).optional(),
161
+ initiator: v4.z.object({
162
+ service: v4.z.string(),
163
+ userId: v4.z.string().optional()
164
+ })
165
+ })
166
+ });
167
+
168
+ const sendSmsInputSchema = z.z.object({
169
+ sms: z.z.object({
170
+ message: z.z.string(),
171
+ to: z.z.string()
172
+ }),
173
+ metadata: z.z.object({
174
+ userAgent: z.z.string().optional(),
175
+ ip: z.z.ipv4().or(z.z.ipv6()).optional(),
176
+ initiator: z.z.object({
177
+ service: z.z.string(),
178
+ userId: z.z.string().optional()
179
+ })
180
+ })
181
+ });
182
+
183
+ class ISmsConnector {
184
+ static providerName;
185
+ ACCOUNT_ID;
186
+ AUTH_TOKEN;
187
+ SERVICE_ID;
188
+ constructor({
189
+ ACCOUNT_ID,
190
+ AUTH_TOKEN,
191
+ SERVICE_ID
192
+ }) {
193
+ this.ACCOUNT_ID = ACCOUNT_ID;
194
+ this.AUTH_TOKEN = AUTH_TOKEN;
195
+ this.SERVICE_ID = SERVICE_ID;
196
+ }
197
+ }
198
+
199
+ class TwilioConnector extends ISmsConnector {
200
+ static providerName = "twilio";
201
+ twilioClient;
202
+ constructor({
203
+ ACCOUNT_ID,
204
+ AUTH_TOKEN,
205
+ SERVICE_ID
206
+ }) {
207
+ super({ ACCOUNT_ID, AUTH_TOKEN, SERVICE_ID });
208
+ this.twilioClient = twilio__default(ACCOUNT_ID, AUTH_TOKEN);
209
+ }
210
+ async sendSms(sms) {
211
+ const message = await this.twilioClient.messages.create({
212
+ body: sms.message,
213
+ messagingServiceSid: this.SERVICE_ID,
214
+ to: sms.to
215
+ });
216
+ if (message.errorMessage)
217
+ return backendSdk.createInternalError(null, {
218
+ message: message.errorMessage,
219
+ status: message.errorCode
220
+ });
221
+ }
222
+ }
223
+
224
+ const auditLog = sqliteCore.sqliteTable("audit_log", {
225
+ ...backendSdk.base,
226
+ event: sqliteCore.text("event").$type().notNull(),
227
+ ip: sqliteCore.text("ip"),
228
+ userAgent: sqliteCore.text("user_agent"),
229
+ description: sqliteCore.text("description"),
230
+ initiatorService: sqliteCore.text("initiator_service").notNull(),
231
+ initiatorUserId: sqliteCore.text("initiator_user_id")
232
+ });
233
+
234
+ const schema = {
235
+ __proto__: null,
236
+ auditLog: auditLog
237
+ };
238
+
239
+ const tables = schema;
240
+
241
+ const initiateEmailConnector = async (provider, apiKey, smtpHost, sender) => {
242
+ const connector = [EcomailConnector].find(
243
+ (conn) => conn.providerName === provider
244
+ );
245
+ if (!connector)
246
+ throw backendSdk.createInternalError(null, {
247
+ message: "Unsupported email provider",
248
+ status: 404
249
+ });
250
+ return new connector({ API_KEY: apiKey, SMTP_HOST: smtpHost, SENDER: sender });
251
+ };
252
+
253
+ const createAuditLogCommand = async ({
254
+ db,
255
+ auditLog: {
256
+ event,
257
+ description,
258
+ initiatorService,
259
+ initiatorUserId,
260
+ ip,
261
+ userAgent
262
+ }
263
+ }) => {
264
+ const command = db.insert(tables.auditLog).values({
265
+ id: backendSdk.uuidv4(),
266
+ createdAt: /* @__PURE__ */ new Date(),
267
+ event,
268
+ description,
269
+ ip,
270
+ userAgent,
271
+ initiatorService,
272
+ initiatorUserId
273
+ });
274
+ return {
275
+ command
276
+ };
277
+ };
278
+
279
+ const initiateSmsConnector = async (provider, accountId, authToken, serviceId) => {
280
+ const connector = [TwilioConnector].find(
281
+ (conn) => conn.providerName === provider
282
+ );
283
+ if (!connector)
284
+ throw backendSdk.createInternalError(null, {
285
+ message: "Unsupported sms provider",
286
+ status: 404
287
+ });
288
+ return new connector({
289
+ ACCOUNT_ID: accountId,
290
+ AUTH_TOKEN: authToken,
291
+ SERVICE_ID: serviceId
292
+ });
293
+ };
294
+
295
+ class SlackConnector {
296
+ webhooks;
297
+ constructor(webhooks) {
298
+ this.webhooks = webhooks;
299
+ }
300
+ async sendNotificationToAllSlack(message) {
301
+ const controller = new AbortController();
302
+ const timeoutId = setTimeout(() => controller.abort(), 3e3);
303
+ for (const webhook of this.webhooks) {
304
+ let response = await fetch(webhook, {
305
+ method: "POST",
306
+ body: JSON.stringify({ text: message }),
307
+ headers: { "Content-Type": "application/json" },
308
+ signal: controller.signal
309
+ });
310
+ clearTimeout(timeoutId);
311
+ if (!response.ok) {
312
+ throw new Error("Failed sending Slack notification to " + message);
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ class NotificationServiceBase extends backendSdk.develitWorker(
319
+ cloudflare_workers.WorkerEntrypoint
320
+ ) {
321
+ name;
322
+ slackConnector = new SlackConnector(this.env.SLACK_WEBHOOKS);
323
+ emailConnector;
324
+ smsConnector;
325
+ db;
326
+ constructor(ctx, env) {
327
+ super(ctx, env);
328
+ this.name = "notification-service";
329
+ this.db = d1.drizzle(this.env.NOTIFICATION_D1, { schema: tables });
330
+ }
331
+ @backendSdk.cloudflareQueue({ baseDelay: 60 })
332
+ async queue(batch) {
333
+ for (const message of batch.messages) {
334
+ this.logInput({ message });
335
+ let notificationAction;
336
+ const { type, metadata, payload } = message.body;
337
+ if (type === "email") {
338
+ const [emailConnector, error2] = await backendSdk.useResult(
339
+ initiateEmailConnector(
340
+ this.env.EMAIL_PROVIDER,
341
+ this.env.EMAIL_API_KEY,
342
+ this.env.EMAIL_SMTP_HOST,
343
+ this.env.EMAIL_SENDER
344
+ )
345
+ );
346
+ if (error2) {
347
+ this.logError({ error: error2 });
348
+ message.retry();
349
+ continue;
350
+ }
351
+ this.emailConnector = emailConnector;
352
+ }
353
+ if (type === "sms") {
354
+ const [smsConnector, error2] = await backendSdk.useResult(
355
+ initiateSmsConnector(
356
+ this.env.SMS_PROVIDER,
357
+ this.env.SMS_ACCOUNT_ID,
358
+ this.env.SMS_AUTH_TOKEN,
359
+ this.env.SMS_SERVICE_ID
360
+ )
361
+ );
362
+ if (error2) {
363
+ this.logError({ error: error2 });
364
+ message.retry();
365
+ continue;
366
+ }
367
+ this.smsConnector = smsConnector;
368
+ }
369
+ switch (type) {
370
+ case "email":
371
+ notificationAction = async () => this._sendEmail({
372
+ email: payload.email,
373
+ metadata
374
+ });
375
+ break;
376
+ case "sms":
377
+ notificationAction = async () => this._sendSms({
378
+ sms: payload.sms,
379
+ metadata
380
+ });
381
+ break;
382
+ case "pushNotification":
383
+ notificationAction = async () => this._sendPushNotification();
384
+ break;
385
+ case "slack":
386
+ notificationAction = async () => this.sendSlackNotification({
387
+ slack: message.body.payload.slack,
388
+ metadata
389
+ });
390
+ break;
391
+ default:
392
+ this.logError({ error: `Unknown notification type: ${type}` });
393
+ message.retry();
394
+ continue;
395
+ }
396
+ const { error, message: errorMessage } = await notificationAction();
397
+ if (error) {
398
+ this.logError({ error: errorMessage });
399
+ message.retry();
400
+ continue;
401
+ }
402
+ message.ack();
403
+ }
404
+ }
405
+ @backendSdk.action("private-send-email")
406
+ async _sendEmail(input) {
407
+ return this.handleAction(
408
+ { data: input, schema: sendEmailInputSchema },
409
+ { successMessage: "Email sent." },
410
+ async (params) => {
411
+ const {
412
+ email,
413
+ metadata: {
414
+ ip,
415
+ userAgent,
416
+ initiator: { service, userId }
417
+ }
418
+ } = params;
419
+ if (!this.emailConnector)
420
+ this.emailConnector = await initiateEmailConnector(
421
+ this.env.EMAIL_PROVIDER,
422
+ this.env.EMAIL_API_KEY,
423
+ this.env.EMAIL_SMTP_HOST,
424
+ this.env.EMAIL_SENDER
425
+ );
426
+ const response = await this.emailConnector.sendEmail(email);
427
+ if (!response?.ok) {
428
+ throw backendSdk.createInternalError(null, {
429
+ message: `Could not send email: ${JSON.stringify(await response?.json())}`,
430
+ status: 500
431
+ });
432
+ }
433
+ const { command } = await createAuditLogCommand({
434
+ db: this.db,
435
+ auditLog: {
436
+ id: backendSdk.uuidv4(),
437
+ event: "EMAIL",
438
+ ip,
439
+ initiatorService: service,
440
+ initiatorUserId: userId,
441
+ userAgent,
442
+ description: JSON.stringify(input)
443
+ }
444
+ });
445
+ await this.db.batch([command]);
446
+ return {};
447
+ }
448
+ );
449
+ }
450
+ @backendSdk.action("private-send-sms")
451
+ async _sendSms(input) {
452
+ return this.handleAction(
453
+ { data: input, schema: sendSmsInputSchema },
454
+ { successMessage: "Sms sent." },
455
+ async (params) => {
456
+ const {
457
+ sms: { message, to },
458
+ metadata: {
459
+ ip,
460
+ userAgent,
461
+ initiator: { service, userId }
462
+ }
463
+ } = params;
464
+ await this.smsConnector.sendSms({ message, to });
465
+ const { command } = await createAuditLogCommand({
466
+ db: this.db,
467
+ auditLog: {
468
+ id: backendSdk.uuidv4(),
469
+ event: "SMS",
470
+ ip,
471
+ userAgent,
472
+ initiatorService: service,
473
+ initiatorUserId: userId,
474
+ description: JSON.stringify(input)
475
+ }
476
+ });
477
+ await this.db.batch([command]);
478
+ return {};
479
+ }
480
+ );
481
+ }
482
+ @backendSdk.action("public-send-email")
483
+ async sendEmail(input) {
484
+ return this.handleAction(
485
+ { data: input, schema: sendEmailInputSchema },
486
+ { successMessage: "Email sent." },
487
+ async (params) => {
488
+ const { email, metadata } = params;
489
+ await this.pushToQueue(
490
+ this.env.NOTIFICATIONS_QUEUE,
491
+ {
492
+ type: "email",
493
+ payload: {
494
+ email
495
+ },
496
+ metadata
497
+ }
498
+ );
499
+ return {};
500
+ }
501
+ );
502
+ }
503
+ @backendSdk.action("public-send-sms")
504
+ async sendSms(input) {
505
+ return this.handleAction(
506
+ { data: input, schema: sendSmsInputSchema },
507
+ { successMessage: "SMS sent." },
508
+ async (params) => {
509
+ const {
510
+ sms: { message, to },
511
+ metadata
512
+ } = params;
513
+ await this.pushToQueue(
514
+ this.env.NOTIFICATIONS_QUEUE,
515
+ {
516
+ type: "sms",
517
+ payload: {
518
+ sms: {
519
+ message,
520
+ to
521
+ }
522
+ },
523
+ metadata
524
+ }
525
+ );
526
+ return {};
527
+ }
528
+ );
529
+ }
530
+ @backendSdk.action("send-push-notification")
531
+ async _sendPushNotification() {
532
+ this.logInput({});
533
+ this.logError({ error: "Method not implemented." });
534
+ throw new Error("Method not implemented.");
535
+ }
536
+ @backendSdk.action("send-slack-notification")
537
+ async sendSlackNotification(input) {
538
+ return this.handleAction(
539
+ {
540
+ data: input,
541
+ schema: sendSlackInputSchema
542
+ },
543
+ { successMessage: "Slack sent." },
544
+ async (params) => {
545
+ const { slack } = params;
546
+ await this.slackConnector.sendNotificationToAllSlack(slack.message);
547
+ return {};
548
+ }
549
+ );
550
+ }
551
+ }
552
+ function defineNotificationService() {
553
+ return class NotificationService extends NotificationServiceBase {
554
+ constructor(ctx, env) {
555
+ super(ctx, env);
556
+ }
557
+ };
558
+ }
559
+
560
+ const NotificationService = defineNotificationService();
561
+
562
+ module.exports = NotificationService;
@@ -0,0 +1,5 @@
1
+ import { WorkerEntrypoint } from 'cloudflare:workers';
2
+
3
+ declare const _default: new (ctx: ExecutionContext, env: NotificationEnv) => WorkerEntrypoint<NotificationEnv>;
4
+
5
+ export = _default;
@@ -0,0 +1,5 @@
1
+ import { WorkerEntrypoint } from 'cloudflare:workers';
2
+
3
+ declare const _default: new (ctx: ExecutionContext, env: NotificationEnv) => WorkerEntrypoint<NotificationEnv>;
4
+
5
+ export { _default as default };
@@ -0,0 +1,5 @@
1
+ import { WorkerEntrypoint } from 'cloudflare:workers';
2
+
3
+ declare const _default: new (ctx: ExecutionContext, env: NotificationEnv) => WorkerEntrypoint<NotificationEnv>;
4
+
5
+ export = _default;