@bernierllc/sender-identity-verification 1.0.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.
Files changed (51) hide show
  1. package/.env.test +8 -0
  2. package/.eslintrc.js +30 -0
  3. package/README.md +376 -0
  4. package/__tests__/SenderIdentityVerification.test.ts +461 -0
  5. package/__tests__/__mocks__/fetch-mock.ts +156 -0
  6. package/__tests__/additional-coverage.test.ts +129 -0
  7. package/__tests__/additional-error-coverage.test.ts +483 -0
  8. package/__tests__/branch-coverage.test.ts +509 -0
  9. package/__tests__/config.test.ts +119 -0
  10. package/__tests__/error-handling.test.ts +321 -0
  11. package/__tests__/final-branch-coverage.test.ts +372 -0
  12. package/__tests__/integration.real-api.test.ts +295 -0
  13. package/__tests__/providers.test.ts +331 -0
  14. package/__tests__/service-coverage.test.ts +412 -0
  15. package/dist/SenderIdentityVerification.d.ts +72 -0
  16. package/dist/SenderIdentityVerification.js +643 -0
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.js +38 -0
  19. package/dist/errors.d.ts +27 -0
  20. package/dist/errors.js +61 -0
  21. package/dist/index.d.ts +4 -0
  22. package/dist/index.js +21 -0
  23. package/dist/providers/MailgunProvider.d.ts +13 -0
  24. package/dist/providers/MailgunProvider.js +35 -0
  25. package/dist/providers/SESProvider.d.ts +12 -0
  26. package/dist/providers/SESProvider.js +47 -0
  27. package/dist/providers/SMTPProvider.d.ts +12 -0
  28. package/dist/providers/SMTPProvider.js +30 -0
  29. package/dist/providers/SendGridProvider.d.ts +19 -0
  30. package/dist/providers/SendGridProvider.js +98 -0
  31. package/dist/templates/verification-email.d.ts +9 -0
  32. package/dist/templates/verification-email.js +67 -0
  33. package/dist/types.d.ts +139 -0
  34. package/dist/types.js +33 -0
  35. package/dist/utils/domain-extractor.d.ts +4 -0
  36. package/dist/utils/domain-extractor.js +20 -0
  37. package/jest.config.cjs +33 -0
  38. package/package.json +60 -0
  39. package/src/SenderIdentityVerification.ts +796 -0
  40. package/src/config.ts +81 -0
  41. package/src/errors.ts +64 -0
  42. package/src/global.d.ts +24 -0
  43. package/src/index.ts +24 -0
  44. package/src/providers/MailgunProvider.ts +35 -0
  45. package/src/providers/SESProvider.ts +51 -0
  46. package/src/providers/SMTPProvider.ts +29 -0
  47. package/src/providers/SendGridProvider.ts +108 -0
  48. package/src/templates/verification-email.ts +67 -0
  49. package/src/types.ts +163 -0
  50. package/src/utils/domain-extractor.ts +18 -0
  51. package/tsconfig.json +22 -0
@@ -0,0 +1,643 @@
1
+ "use strict";
2
+ /*
3
+ Copyright (c) 2025 Bernier LLC
4
+
5
+ This file is licensed to the client under a limited-use license.
6
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
7
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SenderIdentityVerification = void 0;
11
+ const types_js_1 = require("./types.js");
12
+ const SendGridProvider_js_1 = require("./providers/SendGridProvider.js");
13
+ const MailgunProvider_js_1 = require("./providers/MailgunProvider.js");
14
+ const SESProvider_js_1 = require("./providers/SESProvider.js");
15
+ const SMTPProvider_js_1 = require("./providers/SMTPProvider.js");
16
+ const verification_email_js_1 = require("./templates/verification-email.js");
17
+ const domain_extractor_js_1 = require("./utils/domain-extractor.js");
18
+ /**
19
+ * Sender identity verification service
20
+ */
21
+ class SenderIdentityVerification {
22
+ constructor(config) {
23
+ this.config = config;
24
+ this.senders = new Map();
25
+ // Initialize domain verification from config if provided
26
+ if (config.domainVerificationConfig?.instance) {
27
+ this.domainVerification = config.domainVerificationConfig.instance;
28
+ }
29
+ }
30
+ /**
31
+ * Initialize service and register with NeverHub
32
+ */
33
+ async initialize() {
34
+ // Initialize dependencies (would import from actual packages)
35
+ // For now, we'll use stubs since we're missing dependencies
36
+ await this.loadSendersFromDatabase();
37
+ this.logger?.info('Sender identity verification service initialized');
38
+ }
39
+ /**
40
+ * Create a new sender identity
41
+ */
42
+ async createSender(input) {
43
+ try {
44
+ // 1. Validate email format
45
+ if (!this.isValidEmail(input.email)) {
46
+ return {
47
+ success: false,
48
+ error: 'Invalid email format'
49
+ };
50
+ }
51
+ // 2. Extract domain
52
+ const domain = (0, domain_extractor_js_1.extractDomain)(input.email);
53
+ // 3. Check if domain is verified (stub for missing dependency)
54
+ if (this.domainVerification) {
55
+ const domainStatus = await this.domainVerification.getDomainStatus(domain);
56
+ if (!domainStatus.isVerified) {
57
+ return {
58
+ success: false,
59
+ error: `Domain ${domain} is not verified. Please verify the domain first.`,
60
+ errors: [`Domain verification required`]
61
+ };
62
+ }
63
+ }
64
+ // 4. Check for duplicate sender
65
+ const existing = await this.findSenderByEmail(input.email);
66
+ if (existing) {
67
+ return {
68
+ success: false,
69
+ error: `Sender with email ${input.email} already exists (ID: ${existing.id})`
70
+ };
71
+ }
72
+ // 5. Create sender record
73
+ const sender = {
74
+ id: this.generateSenderId(),
75
+ email: input.email,
76
+ name: input.name,
77
+ replyToEmail: input.replyToEmail || input.email,
78
+ replyToName: input.replyToName || input.name,
79
+ domain,
80
+ provider: input.provider,
81
+ status: input.skipVerification ? types_js_1.SenderStatus.VERIFIED : types_js_1.SenderStatus.PENDING,
82
+ isDefault: input.isDefault || false,
83
+ isActive: true,
84
+ verificationAttempts: 0,
85
+ createdAt: new Date(),
86
+ updatedAt: new Date(),
87
+ };
88
+ // If setting as default, unset other defaults for this provider
89
+ if (input.isDefault) {
90
+ await this.unsetOtherDefaults(input.provider);
91
+ }
92
+ // 6. Persist to database
93
+ await this.saveSenderToDatabase(sender);
94
+ this.senders.set(sender.id, sender);
95
+ // 7. Send verification email (unless skipped)
96
+ if (!input.skipVerification) {
97
+ await this.sendVerificationEmail(sender);
98
+ }
99
+ // 8. Publish event
100
+ if (this.neverhub) {
101
+ await this.neverhub.publishEvent({
102
+ type: 'sender-identity.created',
103
+ data: { senderId: sender.id, email: sender.email, provider: sender.provider }
104
+ });
105
+ }
106
+ this.logger?.info('Sender identity created', { senderId: sender.id, email: sender.email });
107
+ return {
108
+ success: true,
109
+ data: sender
110
+ };
111
+ }
112
+ catch (error) {
113
+ this.logger?.error('Failed to create sender identity', { error, input });
114
+ return {
115
+ success: false,
116
+ error: error instanceof Error ? error.message : 'Unknown error creating sender'
117
+ };
118
+ }
119
+ }
120
+ /**
121
+ * Send verification email to sender
122
+ */
123
+ async sendVerificationEmail(sender) {
124
+ try {
125
+ // 1. Generate verification token
126
+ const token = this.cryptoUtils?.generateSecureToken(32) || this.fallbackGenerateToken(32);
127
+ const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
128
+ // 2. Update sender record
129
+ sender.verificationToken = token;
130
+ sender.verificationSentAt = new Date();
131
+ sender.verificationExpiresAt = expiresAt;
132
+ sender.status = types_js_1.SenderStatus.VERIFICATION_SENT;
133
+ sender.verificationAttempts++;
134
+ sender.updatedAt = new Date();
135
+ await this.saveSenderToDatabase(sender);
136
+ // 3. Build verification URL
137
+ const verificationUrl = `${this.config.verificationBaseUrl}/verify-sender?token=${token}`;
138
+ // 4. Send verification email
139
+ if (this.emailSender) {
140
+ const emailResult = await this.emailSender.sendEmail({
141
+ to: sender.email,
142
+ subject: 'Verify your sender email address',
143
+ html: (0, verification_email_js_1.buildVerificationEmailHtml)(sender, verificationUrl),
144
+ text: (0, verification_email_js_1.buildVerificationEmailText)(sender, verificationUrl),
145
+ from: this.config.verificationFromEmail,
146
+ fromName: this.config.verificationFromName,
147
+ });
148
+ if (!emailResult.success) {
149
+ throw new Error(`Failed to send verification email: ${emailResult.error}`);
150
+ }
151
+ }
152
+ // 5. Publish event
153
+ if (this.neverhub) {
154
+ await this.neverhub.publishEvent({
155
+ type: 'sender-identity.verification-sent',
156
+ data: { senderId: sender.id, email: sender.email, expiresAt }
157
+ });
158
+ }
159
+ this.logger?.info('Verification email sent', { senderId: sender.id, email: sender.email });
160
+ return { success: true };
161
+ }
162
+ catch (error) {
163
+ this.logger?.error('Failed to send verification email', { error, senderId: sender.id });
164
+ return {
165
+ success: false,
166
+ error: error instanceof Error ? error.message : 'Failed to send verification email'
167
+ };
168
+ }
169
+ }
170
+ /**
171
+ * Verify sender email with token
172
+ */
173
+ async verifySender(token) {
174
+ try {
175
+ // 1. Find sender by token
176
+ const sender = await this.findSenderByToken(token);
177
+ if (!sender) {
178
+ return {
179
+ success: false,
180
+ senderId: '',
181
+ status: types_js_1.SenderStatus.FAILED,
182
+ message: 'Invalid verification token',
183
+ errors: ['Token not found or already used']
184
+ };
185
+ }
186
+ // 2. Check if already verified
187
+ if (sender.status === types_js_1.SenderStatus.VERIFIED) {
188
+ return {
189
+ success: true,
190
+ senderId: sender.id,
191
+ status: sender.status,
192
+ message: 'Sender already verified',
193
+ verifiedAt: sender.verifiedAt
194
+ };
195
+ }
196
+ // 3. Check if token expired
197
+ if (sender.verificationExpiresAt && sender.verificationExpiresAt < new Date()) {
198
+ sender.status = types_js_1.SenderStatus.EXPIRED;
199
+ sender.updatedAt = new Date();
200
+ await this.saveSenderToDatabase(sender);
201
+ return {
202
+ success: false,
203
+ senderId: sender.id,
204
+ status: types_js_1.SenderStatus.EXPIRED,
205
+ message: 'Verification token expired. Please request a new verification email.',
206
+ errors: ['Token expired']
207
+ };
208
+ }
209
+ // 4. Check if locked
210
+ if (sender.status === types_js_1.SenderStatus.LOCKED) {
211
+ return {
212
+ success: false,
213
+ senderId: sender.id,
214
+ status: types_js_1.SenderStatus.LOCKED,
215
+ message: 'Sender locked due to too many failed verification attempts',
216
+ errors: ['Account locked']
217
+ };
218
+ }
219
+ // 5. Verify with provider
220
+ const providerResult = await this.verifyWithProvider(sender);
221
+ if (!providerResult.success) {
222
+ sender.status = types_js_1.SenderStatus.FAILED;
223
+ sender.validationErrors = providerResult.errors || [providerResult.error || 'Provider verification failed'];
224
+ sender.updatedAt = new Date();
225
+ await this.saveSenderToDatabase(sender);
226
+ return {
227
+ success: false,
228
+ senderId: sender.id,
229
+ status: types_js_1.SenderStatus.FAILED,
230
+ message: 'Provider verification failed',
231
+ errors: sender.validationErrors
232
+ };
233
+ }
234
+ // 6. Mark as verified
235
+ sender.status = types_js_1.SenderStatus.VERIFIED;
236
+ sender.verifiedAt = new Date();
237
+ sender.verificationToken = undefined;
238
+ sender.providerSenderId = providerResult.data?.providerId;
239
+ sender.providerMetadata = providerResult.data?.metadata;
240
+ sender.lastValidated = new Date();
241
+ sender.validationErrors = undefined;
242
+ sender.updatedAt = new Date();
243
+ await this.saveSenderToDatabase(sender);
244
+ // 7. Publish event
245
+ if (this.neverhub) {
246
+ await this.neverhub.publishEvent({
247
+ type: 'sender-identity.verified',
248
+ data: {
249
+ senderId: sender.id,
250
+ email: sender.email,
251
+ provider: sender.provider,
252
+ verifiedAt: sender.verifiedAt
253
+ }
254
+ });
255
+ }
256
+ this.logger?.info('Sender verified successfully', { senderId: sender.id, email: sender.email });
257
+ return {
258
+ success: true,
259
+ senderId: sender.id,
260
+ status: sender.status,
261
+ message: 'Sender verified successfully',
262
+ verifiedAt: sender.verifiedAt
263
+ };
264
+ }
265
+ catch (error) {
266
+ this.logger?.error('Failed to verify sender', { error, token });
267
+ return {
268
+ success: false,
269
+ senderId: '',
270
+ status: types_js_1.SenderStatus.FAILED,
271
+ message: error instanceof Error ? error.message : 'Verification failed',
272
+ errors: [error instanceof Error ? error.message : 'Unknown error']
273
+ };
274
+ }
275
+ }
276
+ /**
277
+ * Verify sender with email provider
278
+ */
279
+ async verifyWithProvider(sender) {
280
+ switch (sender.provider) {
281
+ case types_js_1.EmailProvider.SENDGRID:
282
+ if (this.config.sendgridApiKey) {
283
+ const provider = new SendGridProvider_js_1.SendGridProvider(this.config.sendgridApiKey);
284
+ return provider.verifySender(sender);
285
+ }
286
+ return { success: false, error: 'SendGrid API key not configured' };
287
+ case types_js_1.EmailProvider.MAILGUN:
288
+ if (this.config.mailgunApiKey) {
289
+ const provider = new MailgunProvider_js_1.MailgunProvider(this.config.mailgunApiKey);
290
+ return provider.verifySender(sender);
291
+ }
292
+ return { success: true, data: {} }; // Mailgun doesn't require verification
293
+ case types_js_1.EmailProvider.SES:
294
+ if (this.config.sesAccessKey && this.config.sesSecretKey && this.config.sesRegion) {
295
+ const provider = new SESProvider_js_1.SESProvider(this.config.sesAccessKey, this.config.sesSecretKey, this.config.sesRegion);
296
+ return provider.verifySender(sender);
297
+ }
298
+ return { success: false, error: 'SES credentials not configured' };
299
+ case types_js_1.EmailProvider.SMTP: {
300
+ const provider = new SMTPProvider_js_1.SMTPProvider();
301
+ return provider.verifySender(sender);
302
+ }
303
+ default:
304
+ return {
305
+ success: false,
306
+ error: `Unsupported provider: ${sender.provider}`
307
+ };
308
+ }
309
+ }
310
+ /**
311
+ * Get sender by ID
312
+ */
313
+ async getSender(senderId) {
314
+ const sender = this.senders.get(senderId);
315
+ if (!sender) {
316
+ return {
317
+ success: false,
318
+ error: `Sender not found: ${senderId}`
319
+ };
320
+ }
321
+ return {
322
+ success: true,
323
+ data: sender
324
+ };
325
+ }
326
+ /**
327
+ * List senders
328
+ */
329
+ async listSenders(options = {}) {
330
+ try {
331
+ let senders = Array.from(this.senders.values());
332
+ // Filter by provider
333
+ if (options.provider) {
334
+ senders = senders.filter(s => s.provider === options.provider);
335
+ }
336
+ // Filter by status
337
+ if (options.status) {
338
+ senders = senders.filter(s => s.status === options.status);
339
+ }
340
+ // Filter by active
341
+ if (options.isActive !== undefined) {
342
+ senders = senders.filter(s => s.isActive === options.isActive);
343
+ }
344
+ // Filter by domain
345
+ if (options.domain) {
346
+ senders = senders.filter(s => s.domain === options.domain);
347
+ }
348
+ // Sort by created date (newest first)
349
+ senders.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
350
+ // Pagination
351
+ const limit = options.limit || 50;
352
+ const offset = options.offset || 0;
353
+ senders = senders.slice(offset, offset + limit);
354
+ return {
355
+ success: true,
356
+ data: senders
357
+ };
358
+ }
359
+ catch (error) {
360
+ return {
361
+ success: false,
362
+ error: error instanceof Error ? error.message : 'Failed to list senders'
363
+ };
364
+ }
365
+ }
366
+ /**
367
+ * Update sender
368
+ */
369
+ async updateSender(senderId, input) {
370
+ try {
371
+ const sender = this.senders.get(senderId);
372
+ if (!sender) {
373
+ return {
374
+ success: false,
375
+ error: `Sender not found: ${senderId}`
376
+ };
377
+ }
378
+ // Update fields
379
+ if (input.name !== undefined)
380
+ sender.name = input.name;
381
+ if (input.replyToEmail !== undefined)
382
+ sender.replyToEmail = input.replyToEmail;
383
+ if (input.replyToName !== undefined)
384
+ sender.replyToName = input.replyToName;
385
+ if (input.isDefault !== undefined) {
386
+ // If setting as default, unset other defaults
387
+ if (input.isDefault) {
388
+ await this.unsetOtherDefaults(sender.provider);
389
+ }
390
+ sender.isDefault = input.isDefault;
391
+ }
392
+ if (input.isActive !== undefined)
393
+ sender.isActive = input.isActive;
394
+ sender.updatedAt = new Date();
395
+ // If changing email-related fields, require re-verification
396
+ if (input.replyToEmail && input.replyToEmail !== sender.email) {
397
+ sender.status = types_js_1.SenderStatus.PENDING;
398
+ sender.verifiedAt = undefined;
399
+ sender.lastValidated = undefined;
400
+ }
401
+ await this.saveSenderToDatabase(sender);
402
+ // Publish event
403
+ if (this.neverhub) {
404
+ await this.neverhub.publishEvent({
405
+ type: 'sender-identity.updated',
406
+ data: { senderId: sender.id, updates: input }
407
+ });
408
+ }
409
+ this.logger?.info('Sender updated', { senderId, updates: input });
410
+ return {
411
+ success: true,
412
+ data: sender
413
+ };
414
+ }
415
+ catch (error) {
416
+ this.logger?.error('Failed to update sender', { error, senderId, input });
417
+ return {
418
+ success: false,
419
+ error: error instanceof Error ? error.message : 'Failed to update sender'
420
+ };
421
+ }
422
+ }
423
+ /**
424
+ * Delete sender
425
+ */
426
+ async deleteSender(senderId) {
427
+ try {
428
+ const sender = this.senders.get(senderId);
429
+ if (!sender) {
430
+ return {
431
+ success: false,
432
+ error: `Sender not found: ${senderId}`
433
+ };
434
+ }
435
+ // Soft delete
436
+ sender.deletedAt = new Date();
437
+ sender.isActive = false;
438
+ sender.updatedAt = new Date();
439
+ await this.saveSenderToDatabase(sender);
440
+ this.senders.delete(senderId);
441
+ // Publish event
442
+ if (this.neverhub) {
443
+ await this.neverhub.publishEvent({
444
+ type: 'sender-identity.deleted',
445
+ data: { senderId, email: sender.email }
446
+ });
447
+ }
448
+ this.logger?.info('Sender deleted', { senderId, email: sender.email });
449
+ return { success: true };
450
+ }
451
+ catch (error) {
452
+ this.logger?.error('Failed to delete sender', { error, senderId });
453
+ return {
454
+ success: false,
455
+ error: error instanceof Error ? error.message : 'Failed to delete sender'
456
+ };
457
+ }
458
+ }
459
+ /**
460
+ * Check provider compliance for sender
461
+ */
462
+ async checkCompliance(senderId) {
463
+ try {
464
+ const sender = this.senders.get(senderId);
465
+ if (!sender) {
466
+ return {
467
+ success: false,
468
+ error: `Sender not found: ${senderId}`
469
+ };
470
+ }
471
+ const errors = [];
472
+ const warnings = [];
473
+ // 1. Check email format
474
+ const emailFormat = this.isValidEmail(sender.email);
475
+ if (!emailFormat) {
476
+ errors.push('Invalid email format');
477
+ }
478
+ // 2. Check domain verification (stub)
479
+ let domainVerified = true;
480
+ let spfValid = true;
481
+ let dkimValid = true;
482
+ if (this.domainVerification) {
483
+ const domainStatus = await this.domainVerification.getDomainStatus(sender.domain);
484
+ domainVerified = domainStatus.isVerified;
485
+ spfValid = domainStatus.dnsRecords?.spf?.isValid || false;
486
+ dkimValid = domainStatus.dnsRecords?.dkim?.isValid || false;
487
+ if (!domainVerified) {
488
+ errors.push(`Domain ${sender.domain} is not verified`);
489
+ }
490
+ if (!spfValid) {
491
+ errors.push('SPF record not configured or invalid');
492
+ }
493
+ if (!dkimValid) {
494
+ errors.push('DKIM record not configured or invalid');
495
+ }
496
+ }
497
+ // 3. Provider-specific checks
498
+ if (sender.provider === types_js_1.EmailProvider.SENDGRID) {
499
+ if (!sender.providerSenderId) {
500
+ warnings.push('SendGrid sender ID not set - may need re-verification');
501
+ }
502
+ }
503
+ const isCompliant = errors.length === 0;
504
+ const result = {
505
+ isCompliant,
506
+ checks: {
507
+ domainVerified,
508
+ spfValid,
509
+ dkimValid,
510
+ emailFormat
511
+ },
512
+ errors,
513
+ warnings
514
+ };
515
+ // Update sender validation
516
+ sender.lastValidated = new Date();
517
+ sender.validationErrors = errors.length > 0 ? errors : undefined;
518
+ await this.saveSenderToDatabase(sender);
519
+ return {
520
+ success: true,
521
+ data: result
522
+ };
523
+ }
524
+ catch (error) {
525
+ this.logger?.error('Failed to check compliance', { error, senderId });
526
+ return {
527
+ success: false,
528
+ error: error instanceof Error ? error.message : 'Failed to check compliance'
529
+ };
530
+ }
531
+ }
532
+ /**
533
+ * Get default sender for provider
534
+ */
535
+ async getDefaultSender(provider) {
536
+ const senders = Array.from(this.senders.values());
537
+ const defaultSender = senders.find(s => s.provider === provider &&
538
+ s.isDefault &&
539
+ s.status === types_js_1.SenderStatus.VERIFIED &&
540
+ s.isActive);
541
+ if (!defaultSender) {
542
+ return {
543
+ success: false,
544
+ error: `No default sender found for provider ${provider}`
545
+ };
546
+ }
547
+ return {
548
+ success: true,
549
+ data: defaultSender
550
+ };
551
+ }
552
+ /**
553
+ * Resend verification email
554
+ */
555
+ async resendVerification(senderId) {
556
+ try {
557
+ const sender = this.senders.get(senderId);
558
+ if (!sender) {
559
+ return {
560
+ success: false,
561
+ error: `Sender not found: ${senderId}`
562
+ };
563
+ }
564
+ if (sender.status === types_js_1.SenderStatus.VERIFIED) {
565
+ return {
566
+ success: false,
567
+ error: 'Sender already verified'
568
+ };
569
+ }
570
+ if (sender.status === types_js_1.SenderStatus.LOCKED) {
571
+ return {
572
+ success: false,
573
+ error: 'Sender locked due to too many attempts'
574
+ };
575
+ }
576
+ // Check rate limiting (max 3 emails per hour)
577
+ if (sender.verificationSentAt) {
578
+ const hourAgo = new Date(Date.now() - 60 * 60 * 1000);
579
+ if (sender.verificationSentAt > hourAgo && sender.verificationAttempts >= 3) {
580
+ return {
581
+ success: false,
582
+ error: 'Too many verification emails sent. Please try again later.'
583
+ };
584
+ }
585
+ }
586
+ return this.sendVerificationEmail(sender);
587
+ }
588
+ catch (error) {
589
+ this.logger?.error('Failed to resend verification', { error, senderId });
590
+ return {
591
+ success: false,
592
+ error: error instanceof Error ? error.message : 'Failed to resend verification'
593
+ };
594
+ }
595
+ }
596
+ // ============================================
597
+ // Private Helper Methods
598
+ // ============================================
599
+ generateSenderId() {
600
+ return this.cryptoUtils?.generateSecureToken(16) || this.fallbackGenerateToken(16);
601
+ }
602
+ fallbackGenerateToken(length) {
603
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
604
+ let result = '';
605
+ for (let i = 0; i < length; i++) {
606
+ result += chars.charAt(Math.floor(Math.random() * chars.length));
607
+ }
608
+ return result;
609
+ }
610
+ isValidEmail(email) {
611
+ if (this.emailValidator) {
612
+ return this.emailValidator.validate(email).isValid;
613
+ }
614
+ // Fallback basic validation
615
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
616
+ return emailRegex.test(email);
617
+ }
618
+ async findSenderByEmail(email) {
619
+ return Array.from(this.senders.values()).find(s => s.email === email && !s.deletedAt);
620
+ }
621
+ async findSenderByToken(token) {
622
+ return Array.from(this.senders.values()).find(s => s.verificationToken === token);
623
+ }
624
+ async unsetOtherDefaults(provider) {
625
+ const senders = Array.from(this.senders.values())
626
+ .filter(s => s.provider === provider && s.isDefault);
627
+ for (const sender of senders) {
628
+ sender.isDefault = false;
629
+ sender.updatedAt = new Date();
630
+ await this.saveSenderToDatabase(sender);
631
+ }
632
+ }
633
+ async loadSendersFromDatabase() {
634
+ // TODO: Implement database loading
635
+ // For now, no-op
636
+ }
637
+ async saveSenderToDatabase(sender) {
638
+ // TODO: Implement database persistence
639
+ // For now, just update in-memory map
640
+ this.senders.set(sender.id, sender);
641
+ }
642
+ }
643
+ exports.SenderIdentityVerification = SenderIdentityVerification;
@@ -0,0 +1,31 @@
1
+ import { IEmailDomainVerification } from './types.js';
2
+ /**
3
+ * Configuration for sender identity verification service
4
+ */
5
+ export interface SenderIdentityConfig {
6
+ emailSenderConfig: {
7
+ provider?: string;
8
+ apiKey?: string;
9
+ };
10
+ domainVerificationConfig: {
11
+ instance?: IEmailDomainVerification;
12
+ };
13
+ retryConfig?: {
14
+ maxRetries?: number;
15
+ initialDelayMs?: number;
16
+ maxDelayMs?: number;
17
+ };
18
+ verificationBaseUrl: string;
19
+ verificationFromEmail: string;
20
+ verificationFromName: string;
21
+ sendgridApiKey?: string;
22
+ mailgunApiKey?: string;
23
+ sesAccessKey?: string;
24
+ sesSecretKey?: string;
25
+ sesRegion?: string;
26
+ databaseUrl?: string;
27
+ }
28
+ /**
29
+ * Load configuration from environment variables
30
+ */
31
+ export declare function loadConfigFromEnv(overrides?: Partial<SenderIdentityConfig>): SenderIdentityConfig;