@bernierllc/email-domain-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 (36) hide show
  1. package/.eslintrc.cjs +29 -0
  2. package/README.md +397 -0
  3. package/__mocks__/@bernierllc/crypto-utils.ts +15 -0
  4. package/__mocks__/@bernierllc/logger.ts +42 -0
  5. package/__mocks__/@bernierllc/neverhub-adapter.ts +17 -0
  6. package/__tests__/EmailDomainVerificationService.test.ts +582 -0
  7. package/coverage/clover.xml +263 -0
  8. package/coverage/coverage-final.json +3 -0
  9. package/coverage/lcov-report/EmailDomainVerificationService.ts.html +3061 -0
  10. package/coverage/lcov-report/base.css +224 -0
  11. package/coverage/lcov-report/block-navigation.js +87 -0
  12. package/coverage/lcov-report/errors.ts.html +322 -0
  13. package/coverage/lcov-report/favicon.png +0 -0
  14. package/coverage/lcov-report/index.html +131 -0
  15. package/coverage/lcov-report/prettify.css +1 -0
  16. package/coverage/lcov-report/prettify.js +2 -0
  17. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  18. package/coverage/lcov-report/sorter.js +210 -0
  19. package/coverage/lcov.info +485 -0
  20. package/dist/EmailDomainVerificationService.d.ts +179 -0
  21. package/dist/EmailDomainVerificationService.js +822 -0
  22. package/dist/errors.d.ts +39 -0
  23. package/dist/errors.js +66 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +28 -0
  26. package/dist/types.d.ts +209 -0
  27. package/dist/types.js +9 -0
  28. package/jest.config.cjs +44 -0
  29. package/jest.setup.js +19 -0
  30. package/package.json +72 -0
  31. package/src/EmailDomainVerificationService.ts +992 -0
  32. package/src/dns2.d.ts +29 -0
  33. package/src/errors.ts +79 -0
  34. package/src/index.ts +11 -0
  35. package/src/types.ts +281 -0
  36. package/tsconfig.json +30 -0
@@ -0,0 +1,822 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.EmailDomainVerificationService = void 0;
14
+ const logger_1 = require("@bernierllc/logger");
15
+ const crypto_utils_1 = require("@bernierllc/crypto-utils");
16
+ const neverhub_adapter_1 = require("@bernierllc/neverhub-adapter");
17
+ const dns2_1 = require("dns2");
18
+ const axios_1 = __importDefault(require("axios"));
19
+ const cron_1 = require("cron");
20
+ const crypto_1 = require("crypto");
21
+ const errors_1 = require("./errors");
22
+ /**
23
+ * Email Domain Verification Service
24
+ *
25
+ * Manages domain verification for email service providers including
26
+ * DNS record generation, propagation monitoring, and health checks.
27
+ */
28
+ class EmailDomainVerificationService {
29
+ constructor(config) {
30
+ this.logger = config?.logger || new logger_1.Logger({
31
+ level: logger_1.LogLevel.INFO,
32
+ transports: [new logger_1.ConsoleTransport({ level: logger_1.LogLevel.INFO })]
33
+ });
34
+ this.neverhub = config?.neverhub;
35
+ this.httpClient = axios_1.default.create({
36
+ timeout: 30000,
37
+ headers: { 'User-Agent': '@bernierllc/email-domain-verification/1.0.0' }
38
+ });
39
+ this.healthCheckJobs = new Map();
40
+ this.verificationHistory = new Map();
41
+ }
42
+ /**
43
+ * Initialize service with NeverHub registration
44
+ */
45
+ async initialize() {
46
+ if (await neverhub_adapter_1.NeverHubAdapter.detect()) {
47
+ this.neverhub = new neverhub_adapter_1.NeverHubAdapter();
48
+ await this.registerWithNeverHub();
49
+ }
50
+ this.logger.info('Email Domain Verification Service initialized', {
51
+ neverhubEnabled: !!this.neverhub
52
+ });
53
+ }
54
+ /**
55
+ * Setup domain verification for a provider
56
+ *
57
+ * @param config - Domain configuration
58
+ * @returns Verification result with DNS records to configure
59
+ */
60
+ async setupDomain(config) {
61
+ this.logger.info('Setting up domain verification', {
62
+ domain: config.domain,
63
+ provider: config.provider
64
+ });
65
+ try {
66
+ // Validate domain
67
+ this.validateDomain(config.domain);
68
+ // Generate DNS records based on provider and configuration
69
+ const dnsRecords = await this.generateDNSRecords(config);
70
+ // Generate DKIM keypair if enabled
71
+ let dkimConfig;
72
+ if (config.dkim.enabled) {
73
+ dkimConfig = await this.generateDKIMConfig(config);
74
+ dnsRecords.push(dkimConfig.dnsRecord);
75
+ }
76
+ // Register domain with provider
77
+ const providerData = await this.registerWithProvider(config, dkimConfig);
78
+ // Record verification initiation
79
+ this.addHistoryEntry(config.domain, {
80
+ eventType: 'initiated',
81
+ details: `Domain verification initiated for ${config.provider}`,
82
+ newStatus: 'pending'
83
+ });
84
+ // Publish event
85
+ await this.publishEvent('domain.verification.initiated', {
86
+ domain: config.domain,
87
+ provider: config.provider,
88
+ recordCount: dnsRecords.length
89
+ });
90
+ return {
91
+ success: true,
92
+ domain: config.domain,
93
+ status: 'pending',
94
+ dnsRecords,
95
+ dkimConfig,
96
+ providerData,
97
+ estimatedPropagationTime: 3600, // 1 hour typical
98
+ nextSteps: [
99
+ 'Add the DNS records to your domain registrar',
100
+ 'Wait for DNS propagation (typically 1-24 hours)',
101
+ 'Use checkPropagation() to monitor DNS propagation status',
102
+ 'Use verifyDomain() to complete verification with provider'
103
+ ]
104
+ };
105
+ }
106
+ catch (error) {
107
+ const errorMessage = error instanceof Error ? error.message : String(error);
108
+ this.logger.error(`Domain setup failed for ${config.domain}: ${errorMessage}`);
109
+ return {
110
+ success: false,
111
+ domain: config.domain,
112
+ status: 'failed',
113
+ dnsRecords: [],
114
+ errors: [error instanceof Error ? error.message : 'Unknown error']
115
+ };
116
+ }
117
+ }
118
+ /**
119
+ * Check DNS propagation status for domain records
120
+ *
121
+ * @param domain - Domain name
122
+ * @param records - DNS records to check
123
+ * @returns Propagation results for each record
124
+ */
125
+ async checkPropagation(domain, records) {
126
+ this.logger.info('Checking DNS propagation', {
127
+ domain,
128
+ recordCount: records.length
129
+ });
130
+ const results = [];
131
+ for (const record of records) {
132
+ try {
133
+ const result = await this.checkRecordPropagation(domain, record);
134
+ results.push(result);
135
+ if (result.propagated && result.valuesMatch) {
136
+ this.logger.info('DNS record propagated successfully', {
137
+ domain,
138
+ type: record.type,
139
+ host: record.host
140
+ });
141
+ }
142
+ else {
143
+ this.logger.warn('DNS record not yet propagated', {
144
+ domain,
145
+ type: record.type,
146
+ host: record.host,
147
+ propagationPercentage: result.propagationPercentage
148
+ });
149
+ }
150
+ }
151
+ catch (error) {
152
+ const errorMessage = error instanceof Error ? error.message : String(error);
153
+ this.logger.error(`Propagation check failed for ${domain} ${record.type}: ${errorMessage}`);
154
+ results.push({
155
+ domain,
156
+ record,
157
+ propagated: false,
158
+ nameservers: [],
159
+ propagationPercentage: 0,
160
+ expectedValue: record.value,
161
+ valuesMatch: false,
162
+ errors: [error instanceof Error ? error.message : 'Unknown error']
163
+ });
164
+ }
165
+ }
166
+ // Publish propagation status event
167
+ const overallPropagation = results.reduce((acc, r) => acc + r.propagationPercentage, 0) / results.length;
168
+ await this.publishEvent('domain.propagation.checked', {
169
+ domain,
170
+ propagationPercentage: overallPropagation,
171
+ recordCount: records.length,
172
+ propagatedCount: results.filter(r => r.propagated).length
173
+ });
174
+ return results;
175
+ }
176
+ /**
177
+ * Verify domain with email provider
178
+ *
179
+ * @param domain - Domain name
180
+ * @param provider - Email provider
181
+ * @param providerCredentials - Provider API credentials
182
+ * @returns Verification result
183
+ */
184
+ async verifyDomain(domain, provider, providerCredentials) {
185
+ this.logger.info('Verifying domain with provider', { domain, provider });
186
+ try {
187
+ const providerData = await this.verifyWithProvider(domain, provider, providerCredentials);
188
+ const status = providerData.verified ? 'verified' : 'failed';
189
+ const detailsMessage = typeof providerData.message === 'string' ? providerData.message : 'Verification completed';
190
+ this.addHistoryEntry(domain, {
191
+ eventType: status === 'verified' ? 'verified' : 'failed',
192
+ details: detailsMessage,
193
+ newStatus: status,
194
+ previousStatus: 'propagating'
195
+ });
196
+ await this.publishEvent('domain.verification.completed', {
197
+ domain,
198
+ provider,
199
+ status,
200
+ success: status === 'verified'
201
+ });
202
+ // Start health monitoring if verified
203
+ if (status === 'verified') {
204
+ this.startHealthMonitoring(domain, provider, providerCredentials);
205
+ }
206
+ const errorMessage = typeof providerData.message === 'string' ? providerData.message : 'Verification failed';
207
+ return {
208
+ success: status === 'verified',
209
+ domain,
210
+ status,
211
+ dnsRecords: [],
212
+ providerData,
213
+ errors: status === 'failed' ? [errorMessage] : undefined
214
+ };
215
+ }
216
+ catch (error) {
217
+ const errMsg = error instanceof Error ? error.message : String(error);
218
+ this.logger.error(`Domain verification failed for ${domain} with ${provider}: ${errMsg}`);
219
+ return {
220
+ success: false,
221
+ domain,
222
+ status: 'failed',
223
+ dnsRecords: [],
224
+ errors: [error instanceof Error ? error.message : 'Unknown error']
225
+ };
226
+ }
227
+ }
228
+ /**
229
+ * Get domain health status
230
+ *
231
+ * @param domain - Domain name
232
+ * @returns Health status
233
+ */
234
+ async getDomainHealth(domain) {
235
+ this.logger.info('Checking domain health', { domain });
236
+ // Implementation would check DNS records, provider status, etc.
237
+ // Returning basic structure for type safety
238
+ return {
239
+ domain,
240
+ healthy: true,
241
+ verificationStatus: 'verified',
242
+ lastChecked: new Date(),
243
+ records: []
244
+ };
245
+ }
246
+ /**
247
+ * Get verification history for domain
248
+ *
249
+ * @param domain - Domain name
250
+ * @param limit - Maximum number of entries to return
251
+ * @returns History entries
252
+ */
253
+ getVerificationHistory(domain, limit = 50) {
254
+ const history = this.verificationHistory.get(domain) || [];
255
+ return history.slice(-limit);
256
+ }
257
+ /**
258
+ * Start automated health monitoring for domain
259
+ *
260
+ * @param domain - Domain name
261
+ * @param provider - Email provider
262
+ * @param credentials - Provider credentials
263
+ */
264
+ startHealthMonitoring(domain, provider, credentials) {
265
+ // Stop existing job if any
266
+ this.stopHealthMonitoring(domain);
267
+ // Create new cron job (runs every 6 hours)
268
+ const job = new cron_1.CronJob('0 */6 * * *', async () => {
269
+ try {
270
+ const health = await this.performHealthCheck(domain, provider, credentials);
271
+ if (!health.healthy) {
272
+ this.logger.warn('Domain health check failed', { domain, warnings: health.warnings });
273
+ await this.publishEvent('domain.health.unhealthy', {
274
+ domain,
275
+ provider,
276
+ warnings: health.warnings
277
+ });
278
+ }
279
+ }
280
+ catch (error) {
281
+ const errMsg = error instanceof Error ? error.message : String(error);
282
+ this.logger.error(`Health check failed for ${domain}: ${errMsg}`);
283
+ }
284
+ });
285
+ job.start();
286
+ this.healthCheckJobs.set(domain, job);
287
+ this.logger.info('Health monitoring started', { domain });
288
+ }
289
+ /**
290
+ * Stop health monitoring for domain
291
+ *
292
+ * @param domain - Domain name
293
+ */
294
+ stopHealthMonitoring(domain) {
295
+ const job = this.healthCheckJobs.get(domain);
296
+ if (job) {
297
+ job.stop();
298
+ this.healthCheckJobs.delete(domain);
299
+ this.logger.info('Health monitoring stopped', { domain });
300
+ }
301
+ }
302
+ /**
303
+ * Perform health check for domain
304
+ *
305
+ * @param domain - Domain name
306
+ * @param provider - Email provider
307
+ * @param credentials - Provider credentials
308
+ * @returns Health status
309
+ */
310
+ async performHealthCheck(domain, _provider, _credentials) {
311
+ // Implementation would check DNS records, provider API status, etc.
312
+ // Returning basic structure for type safety
313
+ return {
314
+ domain,
315
+ healthy: true,
316
+ verificationStatus: 'verified',
317
+ lastChecked: new Date(),
318
+ records: []
319
+ };
320
+ }
321
+ /**
322
+ * Generate DNS records for domain configuration
323
+ */
324
+ async generateDNSRecords(config) {
325
+ const records = [];
326
+ // Generate SPF record
327
+ if (config.spf.enabled) {
328
+ records.push(this.generateSPFRecord(config));
329
+ }
330
+ // Generate DMARC record
331
+ if (config.dmarc.enabled) {
332
+ records.push(this.generateDMARCRecord(config));
333
+ }
334
+ // Generate provider-specific records
335
+ const providerRecords = await this.generateProviderRecords(config);
336
+ records.push(...providerRecords);
337
+ return records;
338
+ }
339
+ /**
340
+ * Generate SPF DNS record
341
+ */
342
+ generateSPFRecord(config) {
343
+ const includes = config.spf.includes || [];
344
+ const ipv4s = config.spf.ipv4Addresses || [];
345
+ const ipv6s = config.spf.ipv6Addresses || [];
346
+ const strict = config.spf.strict !== false;
347
+ let spfValue = 'v=spf1';
348
+ // Add provider-specific includes
349
+ if (config.provider === 'sendgrid') {
350
+ includes.push('include:sendgrid.net');
351
+ }
352
+ else if (config.provider === 'mailgun') {
353
+ includes.push('include:mailgun.org');
354
+ }
355
+ else if (config.provider === 'ses') {
356
+ includes.push('include:amazonses.com');
357
+ }
358
+ includes.forEach(inc => {
359
+ spfValue += ` ${inc}`;
360
+ });
361
+ ipv4s.forEach(ip => {
362
+ spfValue += ` ip4:${ip}`;
363
+ });
364
+ ipv6s.forEach(ip => {
365
+ spfValue += ` ip6:${ip}`;
366
+ });
367
+ spfValue += strict ? ' -all' : ' ~all';
368
+ return {
369
+ type: 'SPF',
370
+ host: config.domain,
371
+ value: spfValue,
372
+ ttl: 3600,
373
+ provider: config.provider,
374
+ required: true,
375
+ instructions: `Add this TXT record to your domain:\n\nHost: ${config.domain}\nValue: ${spfValue}\nTTL: 3600`
376
+ };
377
+ }
378
+ /**
379
+ * Generate DMARC DNS record
380
+ */
381
+ generateDMARCRecord(config) {
382
+ const policy = config.dmarc.policy || 'none';
383
+ const percentage = config.dmarc.percentage || 100;
384
+ const reportEmail = config.dmarc.reportEmail;
385
+ const aggregateEmail = config.dmarc.aggregateEmail;
386
+ const subdomain = config.dmarc.subdomain || 'none';
387
+ let dmarcValue = `v=DMARC1; p=${policy}; pct=${percentage}; sp=${subdomain};`;
388
+ if (reportEmail) {
389
+ dmarcValue += ` rua=mailto:${reportEmail};`;
390
+ }
391
+ if (aggregateEmail) {
392
+ dmarcValue += ` ruf=mailto:${aggregateEmail};`;
393
+ }
394
+ return {
395
+ type: 'DMARC',
396
+ host: `_dmarc.${config.domain}`,
397
+ value: dmarcValue,
398
+ ttl: 3600,
399
+ provider: config.provider,
400
+ required: true,
401
+ instructions: `Add this TXT record to your domain:\n\nHost: _dmarc.${config.domain}\nValue: ${dmarcValue}\nTTL: 3600`
402
+ };
403
+ }
404
+ /**
405
+ * Generate provider-specific DNS records
406
+ */
407
+ async generateProviderRecords(config) {
408
+ switch (config.provider) {
409
+ case 'sendgrid':
410
+ return this.generateSendGridRecords(config);
411
+ case 'mailgun':
412
+ return this.generateMailgunRecords(config);
413
+ case 'ses':
414
+ return this.generateSESRecords(config);
415
+ default:
416
+ return [];
417
+ }
418
+ }
419
+ /**
420
+ * Generate SendGrid-specific DNS records
421
+ */
422
+ generateSendGridRecords(config) {
423
+ const records = [];
424
+ // SendGrid domain authentication requires CNAME records
425
+ // These would normally come from SendGrid API
426
+ records.push({
427
+ type: 'CNAME',
428
+ host: `em123.${config.domain}`,
429
+ value: 'u123.wl.sendgrid.net',
430
+ ttl: 3600,
431
+ provider: 'sendgrid',
432
+ required: true,
433
+ instructions: 'Add this CNAME record (actual values will come from SendGrid API)'
434
+ });
435
+ return records;
436
+ }
437
+ /**
438
+ * Generate Mailgun-specific DNS records
439
+ */
440
+ generateMailgunRecords(config) {
441
+ const records = [];
442
+ // Mailgun requires specific TXT records
443
+ records.push({
444
+ type: 'TXT',
445
+ host: config.domain,
446
+ value: 'v=spf1 include:mailgun.org ~all',
447
+ ttl: 3600,
448
+ provider: 'mailgun',
449
+ required: true,
450
+ instructions: 'Add this TXT record for Mailgun SPF'
451
+ });
452
+ return records;
453
+ }
454
+ /**
455
+ * Generate AWS SES-specific DNS records
456
+ */
457
+ generateSESRecords(config) {
458
+ const records = [];
459
+ // SES requires TXT record for verification
460
+ records.push({
461
+ type: 'TXT',
462
+ host: `_amazonses.${config.domain}`,
463
+ value: 'verification-token-from-ses',
464
+ ttl: 3600,
465
+ provider: 'ses',
466
+ required: true,
467
+ instructions: 'Add this TXT record (actual token will come from AWS SES)'
468
+ });
469
+ return records;
470
+ }
471
+ /**
472
+ * Generate DKIM configuration with keypair
473
+ */
474
+ async generateDKIMConfig(config) {
475
+ const selector = config.dkim.selector || 'default';
476
+ const keySize = config.dkim.keySize || 2048;
477
+ // Generate DKIM keypair using Node.js crypto
478
+ const { publicKey, privateKey } = (0, crypto_1.generateKeyPairSync)('rsa', {
479
+ modulusLength: keySize,
480
+ publicKeyEncoding: {
481
+ type: 'spki',
482
+ format: 'pem'
483
+ },
484
+ privateKeyEncoding: {
485
+ type: 'pkcs8',
486
+ format: 'pem'
487
+ }
488
+ });
489
+ // Format public key for DNS (remove headers/newlines, base64)
490
+ const publicKeyForDNS = publicKey
491
+ .replace(/-----BEGIN PUBLIC KEY-----/, '')
492
+ .replace(/-----END PUBLIC KEY-----/, '')
493
+ .replace(/\n/g, '')
494
+ .replace(/\r/g, '');
495
+ const dkimValue = `v=DKIM1; k=rsa; p=${publicKeyForDNS}`;
496
+ const dnsRecord = {
497
+ type: 'DKIM',
498
+ host: `${selector}._domainkey.${config.domain}`,
499
+ value: dkimValue,
500
+ ttl: 3600,
501
+ provider: config.provider,
502
+ required: true,
503
+ instructions: `Add this TXT record to your domain:\n\nHost: ${selector}._domainkey.${config.domain}\nValue: ${dkimValue}\nTTL: 3600`
504
+ };
505
+ return {
506
+ selector,
507
+ privateKey,
508
+ publicKey,
509
+ keySize,
510
+ dnsRecord
511
+ };
512
+ }
513
+ /**
514
+ * Register domain with email provider
515
+ */
516
+ async registerWithProvider(config, _dkimConfig) {
517
+ switch (config.provider) {
518
+ case 'sendgrid':
519
+ return this.registerWithSendGrid(config);
520
+ case 'mailgun':
521
+ return this.registerWithMailgun(config);
522
+ case 'ses':
523
+ return this.registerWithSES(config);
524
+ default:
525
+ return { registered: false, message: 'Provider not supported' };
526
+ }
527
+ }
528
+ /**
529
+ * Register domain with SendGrid
530
+ */
531
+ async registerWithSendGrid(config) {
532
+ try {
533
+ const response = await this.httpClient.post('https://api.sendgrid.com/v3/whitelabel/domains', {
534
+ domain: config.domain,
535
+ subdomain: 'em',
536
+ username: 'sendgrid',
537
+ ips: [],
538
+ custom_spf: false,
539
+ default: true,
540
+ automatic_security: true
541
+ }, {
542
+ headers: {
543
+ Authorization: `Bearer ${config.providerCredentials.apiKey}`
544
+ }
545
+ });
546
+ return {
547
+ registered: true,
548
+ domainId: response.data.id,
549
+ dnsRecords: response.data.dns,
550
+ message: 'Domain registered with SendGrid'
551
+ };
552
+ }
553
+ catch (error) {
554
+ const errMsg = error instanceof Error ? error.message : String(error);
555
+ this.logger.error(`SendGrid registration failed for ${config.domain}: ${errMsg}`);
556
+ throw error;
557
+ }
558
+ }
559
+ /**
560
+ * Register domain with Mailgun
561
+ */
562
+ async registerWithMailgun(config) {
563
+ try {
564
+ const response = await this.httpClient.post('https://api.mailgun.net/v3/domains', new URLSearchParams({
565
+ name: config.domain,
566
+ smtp_password: config.providerCredentials.smtpPassword || '',
567
+ spam_action: 'disabled',
568
+ wildcard: 'false'
569
+ }), {
570
+ auth: {
571
+ username: 'api',
572
+ password: config.providerCredentials.apiKey
573
+ }
574
+ });
575
+ return {
576
+ registered: true,
577
+ domain: response.data.domain,
578
+ dnsRecords: response.data.receiving_dns_records.concat(response.data.sending_dns_records),
579
+ message: 'Domain registered with Mailgun'
580
+ };
581
+ }
582
+ catch (error) {
583
+ const errMsg = error instanceof Error ? error.message : String(error);
584
+ this.logger.error(`Mailgun registration failed for ${config.domain}: ${errMsg}`);
585
+ throw error;
586
+ }
587
+ }
588
+ /**
589
+ * Register domain with AWS SES
590
+ */
591
+ async registerWithSES(config) {
592
+ // AWS SES registration would use AWS SDK
593
+ // This is a placeholder implementation
594
+ this.logger.info('AWS SES registration not yet implemented', {
595
+ domain: config.domain
596
+ });
597
+ return {
598
+ registered: false,
599
+ message: 'AWS SES registration requires AWS SDK integration'
600
+ };
601
+ }
602
+ /**
603
+ * Verify domain with email provider
604
+ */
605
+ async verifyWithProvider(domain, provider, credentials) {
606
+ switch (provider) {
607
+ case 'sendgrid':
608
+ return this.verifyWithSendGrid(domain, credentials);
609
+ case 'mailgun':
610
+ return this.verifyWithMailgun(domain, credentials);
611
+ case 'ses':
612
+ return this.verifyWithSES(domain, credentials);
613
+ default:
614
+ return { verified: false, message: 'Provider not supported' };
615
+ }
616
+ }
617
+ /**
618
+ * Verify domain with SendGrid
619
+ */
620
+ async verifyWithSendGrid(domain, credentials) {
621
+ try {
622
+ // Get domain ID from SendGrid
623
+ const domainsResponse = await this.httpClient.get('https://api.sendgrid.com/v3/whitelabel/domains', {
624
+ headers: {
625
+ Authorization: `Bearer ${credentials.apiKey}`
626
+ }
627
+ });
628
+ const domainData = domainsResponse.data.find((d) => d.domain === domain);
629
+ if (!domainData) {
630
+ return { verified: false, message: 'Domain not found in SendGrid' };
631
+ }
632
+ // Trigger verification
633
+ const verifyResponse = await this.httpClient.post(`https://api.sendgrid.com/v3/whitelabel/domains/${domainData.id}/validate`, {}, {
634
+ headers: {
635
+ Authorization: `Bearer ${credentials.apiKey}`
636
+ }
637
+ });
638
+ return {
639
+ verified: verifyResponse.data.valid,
640
+ validationResults: verifyResponse.data.validation_results,
641
+ message: verifyResponse.data.valid ? 'Domain verified' : 'Verification failed'
642
+ };
643
+ }
644
+ catch (error) {
645
+ const errMsg = error instanceof Error ? error.message : String(error);
646
+ this.logger.error(`SendGrid verification failed for ${domain}: ${errMsg}`);
647
+ throw error;
648
+ }
649
+ }
650
+ /**
651
+ * Verify domain with Mailgun
652
+ */
653
+ async verifyWithMailgun(domain, credentials) {
654
+ try {
655
+ const response = await this.httpClient.get(`https://api.mailgun.net/v3/domains/${domain}`, {
656
+ auth: {
657
+ username: 'api',
658
+ password: credentials.apiKey
659
+ }
660
+ });
661
+ return {
662
+ verified: response.data.domain.state === 'active',
663
+ state: response.data.domain.state,
664
+ message: response.data.domain.state === 'active' ? 'Domain verified' : 'Verification pending'
665
+ };
666
+ }
667
+ catch (error) {
668
+ const errMsg = error instanceof Error ? error.message : String(error);
669
+ this.logger.error(`Mailgun verification failed for ${domain}: ${errMsg}`);
670
+ throw error;
671
+ }
672
+ }
673
+ /**
674
+ * Verify domain with AWS SES
675
+ */
676
+ async verifyWithSES(domain, _credentials) {
677
+ // AWS SES verification would use AWS SDK
678
+ this.logger.info('AWS SES verification not yet implemented', { domain });
679
+ return {
680
+ verified: false,
681
+ message: 'AWS SES verification requires AWS SDK integration'
682
+ };
683
+ }
684
+ /**
685
+ * Check propagation for single DNS record
686
+ */
687
+ async checkRecordPropagation(domain, record) {
688
+ const nameservers = await this.getNameservers(domain);
689
+ const results = [];
690
+ for (const ns of nameservers) {
691
+ try {
692
+ const resolver = new dns2_1.Resolver({ nameServers: [ns] });
693
+ const response = await resolver.resolve(record.host, 'TXT');
694
+ const value = response.answers
695
+ .filter((a) => a.type === 'TXT')
696
+ .map((a) => a.data)
697
+ .join('');
698
+ results.push({ ns, value });
699
+ }
700
+ catch (error) {
701
+ results.push({
702
+ ns,
703
+ error: error instanceof Error ? error.message : 'Unknown error'
704
+ });
705
+ }
706
+ }
707
+ const propagatedCount = results.filter(r => r.value && r.value.includes(record.value.substring(0, 20))).length;
708
+ const propagationPercentage = (propagatedCount / nameservers.length) * 100;
709
+ const propagated = propagationPercentage >= 80; // 80% threshold
710
+ const actualValue = results.find(r => r.value)?.value;
711
+ const valuesMatch = actualValue?.includes(record.value.substring(0, 20)) || false;
712
+ return {
713
+ domain,
714
+ record,
715
+ propagated,
716
+ nameservers,
717
+ propagationPercentage,
718
+ actualValue,
719
+ expectedValue: record.value,
720
+ valuesMatch,
721
+ errors: results.filter(r => r.error).map(r => r.error)
722
+ };
723
+ }
724
+ /**
725
+ * Get nameservers for domain
726
+ */
727
+ async getNameservers(domain) {
728
+ try {
729
+ const resolver = new dns2_1.Resolver();
730
+ const response = await resolver.resolve(domain, 'NS');
731
+ return response.answers
732
+ .filter((a) => a.ns !== undefined)
733
+ .map((a) => a.ns);
734
+ }
735
+ catch (error) {
736
+ const errMsg = error instanceof Error ? error.message : String(error);
737
+ this.logger.warn(`Failed to get nameservers for ${domain}, using defaults: ${errMsg}`);
738
+ // Return common public DNS servers as fallback
739
+ return [
740
+ '8.8.8.8', // Google
741
+ '1.1.1.1', // Cloudflare
742
+ '208.67.222.222' // OpenDNS
743
+ ];
744
+ }
745
+ }
746
+ /**
747
+ * Validate domain name format
748
+ */
749
+ validateDomain(domain) {
750
+ const domainRegex = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i;
751
+ if (!domainRegex.test(domain)) {
752
+ throw new errors_1.InvalidDomainError(`Invalid domain name: ${domain}`);
753
+ }
754
+ }
755
+ /**
756
+ * Add entry to verification history
757
+ */
758
+ async addHistoryEntry(domain, entry) {
759
+ const history = this.verificationHistory.get(domain) || [];
760
+ const result = await (0, crypto_utils_1.generateUUIDKey)();
761
+ history.push({
762
+ id: result.key,
763
+ domain,
764
+ timestamp: new Date(),
765
+ ...entry
766
+ });
767
+ this.verificationHistory.set(domain, history);
768
+ }
769
+ /**
770
+ * Register with NeverHub
771
+ */
772
+ async registerWithNeverHub() {
773
+ if (!this.neverhub)
774
+ return;
775
+ await this.neverhub.register({
776
+ type: 'email-domain-verification',
777
+ name: '@bernierllc/email-domain-verification',
778
+ version: '1.0.0',
779
+ capabilities: [
780
+ { type: 'email', name: 'domain-verification', version: '1.0.0' },
781
+ { type: 'dns', name: 'record-management', version: '1.0.0' },
782
+ { type: 'monitoring', name: 'health-checks', version: '1.0.0' }
783
+ ],
784
+ dependencies: ['logging', 'crypto']
785
+ });
786
+ this.logger.info('Registered with NeverHub');
787
+ }
788
+ /**
789
+ * Publish event to NeverHub
790
+ */
791
+ async publishEvent(eventType, data) {
792
+ if (!this.neverhub)
793
+ return;
794
+ try {
795
+ await this.neverhub.publishEvent({
796
+ type: eventType,
797
+ data,
798
+ source: '@bernierllc/email-domain-verification',
799
+ timestamp: new Date().toISOString()
800
+ });
801
+ }
802
+ catch (error) {
803
+ this.logger.warn('Failed to publish event to NeverHub', {
804
+ eventType,
805
+ error: error instanceof Error ? error.message : String(error)
806
+ });
807
+ }
808
+ }
809
+ /**
810
+ * Cleanup resources
811
+ */
812
+ async shutdown() {
813
+ // Stop all health monitoring jobs
814
+ for (const [domain, job] of this.healthCheckJobs.entries()) {
815
+ job.stop();
816
+ this.logger.info('Stopped health monitoring', { domain });
817
+ }
818
+ this.healthCheckJobs.clear();
819
+ this.logger.info('Email Domain Verification Service shut down');
820
+ }
821
+ }
822
+ exports.EmailDomainVerificationService = EmailDomainVerificationService;