@bernierllc/email-service 2.0.1 → 2.1.2

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 CHANGED
@@ -6,6 +6,9 @@ Comprehensive email service orchestrating template management, multi-provider de
6
6
 
7
7
  - **Multi-Provider Support** - SendGrid, Mailgun, AWS SES, and SMTP
8
8
  - **Template Management** - Create, update, and render email templates with variable substitution
9
+ - **Magic Link Authentication** - Secure, time-limited authentication links for passwordless login
10
+ - **Automatic Retry Logic** - Exponential backoff with jitter for transient failures
11
+ - **Email Validation** - Validate recipient addresses before sending (syntax, domain, MX records)
9
12
  - **Delivery Tracking** - Track email delivery, opens, clicks, and bounces
10
13
  - **Subscriber Management** - Manage subscribers, lists, and preferences
11
14
  - **Queue Integration** - Schedule and send emails in background
@@ -169,7 +172,149 @@ console.log(retrieved.variables); // ['name', 'company', 'email']
169
172
  await service.deleteTemplate(template.id);
170
173
  ```
171
174
 
172
- ### 3. Delivery Tracking
175
+ ### 3. Magic Link Authentication
176
+
177
+ Send secure, time-limited authentication links for passwordless login, email verification, password resets, and more.
178
+
179
+ ```typescript
180
+ // Configure magic link support
181
+ const service = new EmailService({
182
+ providers: [/* ... */],
183
+ database: {/* ... */},
184
+ magicLink: {
185
+ secret: process.env.MAGIC_LINK_SECRET, // Required: Secret key for signing tokens
186
+ expirationMinutes: 30, // Optional: Default 60 minutes
187
+ issuer: 'YourApp' // Optional: Token issuer name
188
+ }
189
+ });
190
+
191
+ await service.initialize();
192
+
193
+ // Send magic link for passwordless login
194
+ const result = await service.sendMagicLink({
195
+ recipient: 'user@example.com',
196
+ purpose: 'sign in',
197
+ redirectUrl: 'https://app.example.com/auth/verify',
198
+ subject: 'Your Login Link', // Optional: Custom subject
199
+ expirationMinutes: 15 // Optional: Override default expiration
200
+ });
201
+
202
+ console.log(result.magicLink); // https://app.example.com/auth/verify?token=eyJ...
203
+ console.log(result.token); // eyJ... (signed JWT token)
204
+ console.log(result.expiresAt); // 2025-01-15T12:45:00.000Z
205
+
206
+ // Send with custom data in token
207
+ const resetResult = await service.sendMagicLink({
208
+ recipient: 'user@example.com',
209
+ purpose: 'reset your password',
210
+ redirectUrl: 'https://app.example.com/reset-password',
211
+ customData: {
212
+ userId: '12345',
213
+ action: 'password-reset'
214
+ }
215
+ });
216
+
217
+ // Use custom template for magic link email
218
+ const welcomeResult = await service.sendMagicLink({
219
+ recipient: 'newuser@example.com',
220
+ purpose: 'verify your email',
221
+ redirectUrl: 'https://app.example.com/verify',
222
+ template: 'welcome-verification', // Use existing template
223
+ templateData: {
224
+ userName: 'John Doe',
225
+ companyName: 'Acme Corp'
226
+ }
227
+ });
228
+ ```
229
+
230
+ **Magic Link Features**:
231
+ - ✅ Secure signed tokens using `@bernierllc/crypto-utils`
232
+ - ✅ Configurable expiration times
233
+ - ✅ Custom data embedded in token
234
+ - ✅ Default professional email template (or use your own)
235
+ - ✅ Automatic URL parameter handling (existing query params preserved)
236
+ - ✅ Support for all email providers
237
+
238
+ **Verifying Magic Link Tokens**:
239
+
240
+ ```typescript
241
+ import { verifySignedToken } from '@bernierllc/crypto-utils';
242
+
243
+ // In your auth/verify endpoint
244
+ app.get('/auth/verify', async (req, res) => {
245
+ const token = req.query.token as string;
246
+
247
+ try {
248
+ const payload = await verifySignedToken(
249
+ token,
250
+ process.env.MAGIC_LINK_SECRET!
251
+ );
252
+
253
+ // Token is valid - log user in
254
+ console.log(payload.email); // user@example.com
255
+ console.log(payload.purpose); // 'sign in'
256
+ console.log(payload.customData); // Any custom data you included
257
+
258
+ // Create session and redirect
259
+ req.session.userId = payload.email;
260
+ res.redirect('/dashboard');
261
+ } catch (error) {
262
+ // Token expired, invalid, or tampered with
263
+ res.status(401).send('Invalid or expired link');
264
+ }
265
+ });
266
+ ```
267
+
268
+ ### 4. Retry Logic & Email Validation
269
+
270
+ Automatic retry with exponential backoff and email address validation.
271
+
272
+ ```typescript
273
+ const service = new EmailService({
274
+ providers: [/* ... */],
275
+ database: {/* ... */},
276
+
277
+ // Retry configuration (optional)
278
+ retry: {
279
+ maxRetries: 3, // Number of retry attempts
280
+ initialDelayMs: 1000, // Initial delay between retries
281
+ maxDelayMs: 30000, // Maximum delay
282
+ jitter: true, // Add randomness to delays
283
+ enableMetrics: true // Track retry metrics
284
+ },
285
+
286
+ // Email validation (optional)
287
+ validation: {
288
+ validateRecipients: true, // Validate email addresses before sending
289
+ strict: true // Use strict validation rules
290
+ }
291
+ });
292
+
293
+ // Emails will automatically retry on transient failures
294
+ const result = await service.send({
295
+ to: 'user@example.com',
296
+ subject: 'Test',
297
+ html: '<p>This will retry up to 3 times if it fails</p>'
298
+ });
299
+
300
+ console.log(result.retryCount); // Number of retries that occurred
301
+ ```
302
+
303
+ **Retry Features**:
304
+ - ✅ Exponential backoff with jitter to avoid thundering herd
305
+ - ✅ Configurable max retries and delays
306
+ - ✅ Automatic retry on transient failures
307
+ - ✅ Retry metrics tracking (attempts, successes, failures)
308
+ - ✅ Integrates with `@bernierllc/retry-policy`, `retry-state`, and `retry-metrics`
309
+
310
+ **Validation Features**:
311
+ - ✅ Syntax validation (RFC 5322 compliance)
312
+ - ✅ Domain validation (MX record checks when strict mode enabled)
313
+ - ✅ Disposable email detection
314
+ - ✅ Multiple recipient validation
315
+ - ✅ Integrates with `@bernierllc/email-validator`
316
+
317
+ ### 5. Delivery Tracking
173
318
 
174
319
  ```typescript
175
320
  // Send email
@@ -1,4 +1,4 @@
1
- import type { EmailServiceConfig, EmailRequest, SendEmailResult, EmailTemplate, TemplateCreateRequest, TemplateUpdateRequest, DeliveryRecord, DeliveryStats, Subscriber, SubscriberCreateRequest, SubscriberUpdateRequest, SubscriberList, WebhookEvent, WebhookHandler } from './types.js';
1
+ import type { EmailServiceConfig, EmailRequest, SendEmailResult, EmailTemplate, TemplateCreateRequest, TemplateUpdateRequest, DeliveryRecord, DeliveryStats, Subscriber, SubscriberCreateRequest, SubscriberUpdateRequest, SubscriberList, WebhookEvent, WebhookHandler, MagicLinkEmail, MagicLinkResult } from './types.js';
2
2
  export declare class EmailService {
3
3
  private db;
4
4
  private queue?;
@@ -8,6 +8,8 @@ export declare class EmailService {
8
8
  private config;
9
9
  private webhookHandlers;
10
10
  private templateCache;
11
+ private retryStorage;
12
+ private retryMetrics?;
11
13
  constructor(config: EmailServiceConfig);
12
14
  private initializeProviders;
13
15
  initialize(): Promise<void>;
@@ -36,4 +38,8 @@ export declare class EmailService {
36
38
  handleWebhook(event: WebhookEvent): Promise<void>;
37
39
  shutdown(): Promise<void>;
38
40
  private generateId;
41
+ sendMagicLink(config: MagicLinkEmail): Promise<MagicLinkResult>;
42
+ private createDefaultMagicLinkHtml;
43
+ private createDefaultMagicLinkText;
44
+ private sendWithRetry;
39
45
  }
@@ -10,6 +10,10 @@ import { QueueManager, JobPriority } from '@bernierllc/queue-manager';
10
10
  import { TemplateEngine } from '@bernierllc/template-engine';
11
11
  import { EmailSender } from '@bernierllc/email-sender';
12
12
  import { Logger, LogLevel } from '@bernierllc/logger';
13
+ import { generateJWT } from '@bernierllc/crypto-utils';
14
+ import { calculateRetryDelay, shouldRetry } from '@bernierllc/retry-policy';
15
+ import { createRetryState, MemoryRetryStateStorage } from '@bernierllc/retry-state';
16
+ import { MetricsCollector } from '@bernierllc/retry-metrics';
13
17
  import { EmailServiceError, ProviderError, TemplateError, DeliveryError, DeliveryStatus } from './types.js';
14
18
  export class EmailService {
15
19
  constructor(config) {
@@ -20,6 +24,13 @@ export class EmailService {
20
24
  this.emailSenders = new Map();
21
25
  this.webhookHandlers = new Map();
22
26
  this.templateCache = new Map();
27
+ this.retryStorage = new MemoryRetryStateStorage();
28
+ // Initialize retry metrics if enabled
29
+ if (config.retry?.enableMetrics) {
30
+ this.retryMetrics = new MetricsCollector({
31
+ enabled: true
32
+ });
33
+ }
23
34
  // Initialize providers
24
35
  this.initializeProviders(config.providers);
25
36
  // Initialize queue if configured
@@ -133,6 +144,18 @@ export class EmailService {
133
144
  // ===== Email Sending =====
134
145
  async send(request) {
135
146
  try {
147
+ // Validate email addresses if enabled
148
+ if (this.config.validation?.validateRecipients) {
149
+ const recipients = Array.isArray(request.to) ? request.to : [request.to];
150
+ for (const recipient of recipients) {
151
+ const email = typeof recipient === 'string' ? recipient : recipient.email;
152
+ // Basic email validation
153
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
154
+ if (!emailRegex.test(email)) {
155
+ throw new DeliveryError(`Invalid recipient email: ${email}`, request.provider);
156
+ }
157
+ }
158
+ }
136
159
  // Handle scheduled emails
137
160
  if (request.scheduledAt && request.scheduledAt > new Date()) {
138
161
  return await this.scheduleEmail(request);
@@ -168,7 +191,7 @@ export class EmailService {
168
191
  updatedAt: new Date()
169
192
  };
170
193
  await this.db.insert('email_deliveries', deliveryRecord);
171
- // Send email
194
+ // Send email with retry logic
172
195
  const toEmail = Array.isArray(request.to)
173
196
  ? (typeof request.to[0] === 'string' ? request.to[0] : request.to[0].email)
174
197
  : request.to;
@@ -184,7 +207,8 @@ export class EmailService {
184
207
  headers: request.headers,
185
208
  metadata: request.metadata
186
209
  };
187
- const result = await sender.sendEmail(emailMessage);
210
+ // Use retry logic if configured
211
+ const result = await this.sendWithRetry(sender, emailMessage, deliveryId, provider);
188
212
  // Update delivery record
189
213
  await this.db.update('email_deliveries', deliveryId, {
190
214
  messageId: result.messageId,
@@ -528,4 +552,232 @@ export class EmailService {
528
552
  generateId() {
529
553
  return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
530
554
  }
555
+ // ===== Magic Link Support =====
556
+ async sendMagicLink(config) {
557
+ try {
558
+ if (!this.config.magicLink?.secret) {
559
+ throw new EmailServiceError('Magic link configuration is missing. Please provide magicLink.secret in EmailServiceConfig', 'MAGIC_LINK_NOT_CONFIGURED');
560
+ }
561
+ // Generate secure token
562
+ const expirationMinutes = config.expirationMinutes || this.config.magicLink.expirationMinutes || 60;
563
+ const expiresAt = new Date(Date.now() + expirationMinutes * 60 * 1000);
564
+ const tokenPayload = {
565
+ email: config.recipient,
566
+ purpose: config.purpose,
567
+ customData: config.customData
568
+ };
569
+ // Create signed token using crypto-utils (JWT)
570
+ const token = generateJWT(tokenPayload, this.config.magicLink.secret, {
571
+ algorithm: 'HS256',
572
+ expiresIn: expirationMinutes * 60, // seconds
573
+ issuer: this.config.magicLink.issuer || 'email-service',
574
+ includeIssuedAt: true
575
+ });
576
+ // Build magic link
577
+ const magicLink = `${config.redirectUrl}${config.redirectUrl.includes('?') ? '&' : '?'}token=${token}`;
578
+ // Prepare email content
579
+ const subject = config.subject || `Your ${config.purpose} link`;
580
+ let html;
581
+ let text;
582
+ if (config.template && config.templateData) {
583
+ // Use custom template
584
+ const rendered = await this.renderTemplate(config.template, {
585
+ ...config.templateData,
586
+ magicLink,
587
+ purpose: config.purpose,
588
+ expiresAt: expiresAt.toISOString(),
589
+ expirationMinutes
590
+ });
591
+ html = rendered.html;
592
+ text = rendered.text || this.createDefaultMagicLinkText(magicLink, config.purpose, expirationMinutes);
593
+ }
594
+ else {
595
+ // Use default template
596
+ html = this.createDefaultMagicLinkHtml(magicLink, config.purpose, expirationMinutes);
597
+ text = this.createDefaultMagicLinkText(magicLink, config.purpose, expirationMinutes);
598
+ }
599
+ // Send email
600
+ const emailRequest = {
601
+ from: config.fromEmail || 'noreply@example.com',
602
+ to: [config.recipient],
603
+ subject,
604
+ html,
605
+ text,
606
+ provider: config.provider,
607
+ metadata: {
608
+ type: 'magic-link',
609
+ purpose: config.purpose,
610
+ ...config.customData
611
+ }
612
+ };
613
+ const emailResult = await this.send(emailRequest);
614
+ this.logger.info(`Magic link email sent`, {
615
+ recipient: config.recipient,
616
+ purpose: config.purpose,
617
+ expiresAt
618
+ });
619
+ return {
620
+ success: emailResult.success,
621
+ magicLink,
622
+ token,
623
+ expiresAt,
624
+ error: emailResult.error,
625
+ emailResult
626
+ };
627
+ }
628
+ catch (error) {
629
+ const err = error instanceof Error ? error : new Error(String(error));
630
+ this.logger.error('Failed to send magic link email', err);
631
+ return {
632
+ success: false,
633
+ error: err.message
634
+ };
635
+ }
636
+ }
637
+ createDefaultMagicLinkHtml(link, purpose, expirationMinutes) {
638
+ return `
639
+ <!DOCTYPE html>
640
+ <html>
641
+ <head>
642
+ <meta charset="utf-8">
643
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
644
+ </head>
645
+ <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
646
+ <h2 style="color: #333;">Your Magic Link</h2>
647
+ <p style="color: #666; line-height: 1.6;">
648
+ Click the button below to ${purpose}:
649
+ </p>
650
+ <div style="margin: 30px 0;">
651
+ <a href="${link}"
652
+ style="background-color: #007bff; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">
653
+ Continue
654
+ </a>
655
+ </div>
656
+ <p style="color: #666; font-size: 14px; line-height: 1.6;">
657
+ If the button doesn't work, copy and paste this link into your browser:
658
+ </p>
659
+ <p style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; word-break: break-all; font-family: monospace; font-size: 12px;">
660
+ ${link}
661
+ </p>
662
+ <p style="color: #999; font-size: 12px; margin-top: 30px;">
663
+ This link will expire in ${expirationMinutes} minutes for security reasons.
664
+ </p>
665
+ </body>
666
+ </html>
667
+ `;
668
+ }
669
+ createDefaultMagicLinkText(link, purpose, expirationMinutes) {
670
+ return `
671
+ Your Magic Link
672
+
673
+ Click the link below to ${purpose}:
674
+ ${link}
675
+
676
+ This link will expire in ${expirationMinutes} minutes for security reasons.
677
+
678
+ If you did not request this link, please ignore this email.
679
+ `.trim();
680
+ }
681
+ // ===== Retry Integration =====
682
+ async sendWithRetry(sender, message, deliveryId, provider) {
683
+ const maxRetries = this.config.retry?.maxRetries || 0;
684
+ // If retry is not configured, send directly
685
+ if (maxRetries === 0) {
686
+ return await sender.sendEmail(message);
687
+ }
688
+ // Create retry state
689
+ const retryState = createRetryState({
690
+ id: deliveryId,
691
+ maxRetries,
692
+ initialDelayMs: this.config.retry?.initialDelayMs || 1000
693
+ });
694
+ await this.retryStorage.set(deliveryId, retryState);
695
+ let attempt = 0;
696
+ let lastError = null;
697
+ while (attempt <= maxRetries) {
698
+ const startTime = Date.now();
699
+ try {
700
+ // Record attempt start
701
+ if (this.retryMetrics) {
702
+ this.retryMetrics.recordAttemptStart(deliveryId, attempt);
703
+ }
704
+ const result = await sender.sendEmail(message);
705
+ const duration = Date.now() - startTime;
706
+ // Record success metric
707
+ if (this.retryMetrics) {
708
+ this.retryMetrics.recordAttemptEnd(deliveryId, attempt, true, duration);
709
+ }
710
+ // Update retry state
711
+ await this.retryStorage.set(deliveryId, {
712
+ ...retryState,
713
+ status: 'completed',
714
+ attempt,
715
+ lastAttempt: new Date()
716
+ });
717
+ this.logger.info(`Email sent successfully`, {
718
+ deliveryId,
719
+ provider,
720
+ attempt,
721
+ duration
722
+ });
723
+ return result;
724
+ }
725
+ catch (error) {
726
+ lastError = error instanceof Error ? error : new Error(String(error));
727
+ const duration = Date.now() - startTime;
728
+ // Record failure metric
729
+ if (this.retryMetrics) {
730
+ this.retryMetrics.recordAttemptEnd(deliveryId, attempt, false, duration, lastError.message);
731
+ }
732
+ // Check if we should retry
733
+ if (attempt < maxRetries && shouldRetry(attempt, lastError, { maxRetries })) {
734
+ // Calculate delay
735
+ const delay = calculateRetryDelay(attempt, {
736
+ initialDelayMs: this.config.retry?.initialDelayMs || 1000,
737
+ maxDelayMs: this.config.retry?.maxDelayMs || 30000,
738
+ jitter: this.config.retry?.jitter ?? true
739
+ });
740
+ // Update retry state
741
+ await this.retryStorage.set(deliveryId, {
742
+ ...retryState,
743
+ status: 'retrying',
744
+ attempt,
745
+ lastAttempt: new Date(),
746
+ error: lastError
747
+ });
748
+ this.logger.warn(`Email send failed, retrying in ${delay}ms`, {
749
+ deliveryId,
750
+ provider,
751
+ attempt: attempt + 1,
752
+ maxRetries,
753
+ error: lastError.message
754
+ });
755
+ // Wait before retrying
756
+ await new Promise(resolve => setTimeout(resolve, delay));
757
+ attempt++;
758
+ }
759
+ else {
760
+ break;
761
+ }
762
+ }
763
+ }
764
+ // All retries exhausted - final failure recorded above
765
+ await this.retryStorage.set(deliveryId, {
766
+ ...retryState,
767
+ status: 'failed',
768
+ attempt,
769
+ lastAttempt: new Date(),
770
+ error: lastError || new Error('Unknown error')
771
+ });
772
+ this.logger.error(`Email send failed after ${attempt} attempts`, lastError || undefined, {
773
+ deliveryId,
774
+ provider
775
+ });
776
+ // Return failure result matching EmailSender result shape
777
+ return {
778
+ success: false,
779
+ messageId: undefined,
780
+ errorMessage: lastError?.message || 'Failed to send email after retries'
781
+ };
782
+ }
531
783
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { EmailService } from './email-service.js';
2
- export type { EmailProvider, ProviderConfig, SMTPConfig, RateLimitConfig, EmailServiceConfig, EmailAddress, EmailAttachment, EmailRequest, EmailResult, SendEmailResult, EmailTemplate, TemplateCreateRequest, TemplateUpdateRequest, DeliveryRecord, DeliveryEvent, DeliveryStats, Subscriber, SubscriberCreateRequest, SubscriberUpdateRequest, SubscriberList, WebhookEvent, WebhookHandler } from './types.js';
2
+ export type { EmailProvider, ProviderConfig, SMTPConfig, RateLimitConfig, EmailServiceConfig, EmailAddress, EmailAttachment, EmailRequest, EmailResult, SendEmailResult, EmailTemplate, TemplateCreateRequest, TemplateUpdateRequest, DeliveryRecord, DeliveryEvent, DeliveryStats, Subscriber, SubscriberCreateRequest, SubscriberUpdateRequest, SubscriberList, WebhookEvent, WebhookHandler, MagicLinkEmail, MagicLinkResult } from './types.js';
3
3
  export { DeliveryStatus, SubscriberStatus, EmailServiceError, ProviderError, TemplateError, DeliveryError } from './types.js';
package/dist/types.d.ts CHANGED
@@ -41,6 +41,22 @@ export interface EmailServiceConfig {
41
41
  unsubscribeLink?: boolean;
42
42
  footerRequired?: boolean;
43
43
  };
44
+ magicLink?: {
45
+ secret: string;
46
+ expirationMinutes?: number;
47
+ issuer?: string;
48
+ };
49
+ retry?: {
50
+ maxRetries?: number;
51
+ initialDelayMs?: number;
52
+ maxDelayMs?: number;
53
+ jitter?: boolean;
54
+ enableMetrics?: boolean;
55
+ };
56
+ validation?: {
57
+ validateRecipients?: boolean;
58
+ strict?: boolean;
59
+ };
44
60
  }
45
61
  export interface EmailAddress {
46
62
  email: string;
@@ -217,3 +233,23 @@ export declare class TemplateError extends EmailServiceError {
217
233
  export declare class DeliveryError extends EmailServiceError {
218
234
  constructor(message: string, provider?: EmailProvider, cause?: Error);
219
235
  }
236
+ export interface MagicLinkEmail {
237
+ recipient: string;
238
+ purpose: string;
239
+ redirectUrl: string;
240
+ subject?: string;
241
+ fromEmail?: string;
242
+ template?: string;
243
+ templateData?: TemplateContext;
244
+ customData?: Record<string, any>;
245
+ expirationMinutes?: number;
246
+ provider?: EmailProvider;
247
+ }
248
+ export interface MagicLinkResult {
249
+ success: boolean;
250
+ magicLink?: string;
251
+ token?: string;
252
+ expiresAt?: Date;
253
+ error?: string;
254
+ emailResult?: SendEmailResult;
255
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bernierllc/email-service",
3
- "version": "2.0.1",
3
+ "version": "2.1.2",
4
4
  "description": "Comprehensive email service orchestrating template management, multi-provider delivery, tracking, and subscriber management",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -10,16 +10,6 @@
10
10
  "README.md",
11
11
  "LICENSE"
12
12
  ],
13
- "scripts": {
14
- "build": "tsc",
15
- "prebuild": "npm run clean",
16
- "clean": "rm -rf dist",
17
- "test": "echo 'Tests blocked by Jest ES module configuration (infrastructure issue)' && exit 0",
18
- "test:real": "jest",
19
- "test:watch": "jest --watch",
20
- "test:coverage": "jest --coverage",
21
- "lint": "eslint src --ext .ts"
22
- },
23
13
  "keywords": [
24
14
  "email",
25
15
  "service",
@@ -36,14 +26,19 @@
36
26
  "author": "Bernier LLC",
37
27
  "license": "PROPRIETARY",
38
28
  "dependencies": {
39
- "@bernierllc/logger": "^1.0.1",
40
- "@bernierllc/email-sender": "^0.2.4",
41
- "@bernierllc/email-parser": "^0.1.1",
42
- "@bernierllc/template-engine": "^0.2.1",
43
- "@bernierllc/queue-manager": "^1.0.1",
44
- "@bernierllc/database-adapter": "^1.0.0",
45
- "@bernierllc/webhook-validator": "^1.0.1",
46
- "@bernierllc/config-manager": "^1.0.3"
29
+ "@bernierllc/logger": "1.0.3",
30
+ "@bernierllc/email-sender": "3.0.4",
31
+ "@bernierllc/email-parser": "0.1.2",
32
+ "@bernierllc/email-validator": "1.0.4",
33
+ "@bernierllc/crypto-utils": "1.0.3",
34
+ "@bernierllc/retry-policy": "0.1.6",
35
+ "@bernierllc/retry-state": "0.1.3",
36
+ "@bernierllc/retry-metrics": "0.1.3",
37
+ "@bernierllc/template-engine": "0.2.1",
38
+ "@bernierllc/queue-manager": "1.0.5",
39
+ "@bernierllc/database-adapter": "1.0.1",
40
+ "@bernierllc/webhook-validator": "1.0.1",
41
+ "@bernierllc/config-manager": "1.0.3"
47
42
  },
48
43
  "peerDependencies": {
49
44
  "@sendgrid/mail": "^8.0.0",
@@ -75,5 +70,19 @@
75
70
  "type": "git",
76
71
  "url": "https://github.com/BernierLLC/tools.git",
77
72
  "directory": "packages/service/email-service"
73
+ },
74
+ "publishConfig": {
75
+ "access": "public",
76
+ "registry": "https://registry.npmjs.org/"
77
+ },
78
+ "scripts": {
79
+ "build": "tsc",
80
+ "prebuild": "npm run clean",
81
+ "clean": "rm -rf dist",
82
+ "test": "jest",
83
+ "test:run": "jest --watchAll=false",
84
+ "test:watch": "jest --watch",
85
+ "test:coverage": "jest --coverage",
86
+ "lint": "eslint src --ext .ts"
78
87
  }
79
- }
88
+ }