@bernierllc/sender-identity-verification 1.3.1 → 1.4.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.
- package/CHANGELOG.md +11 -0
- package/dist/SenderIdentityVerification.d.ts +37 -2
- package/dist/SenderIdentityVerification.js +393 -62
- package/dist/config.d.ts +3 -0
- package/dist/config.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -1
- package/dist/manual-instructions.d.ts +9 -0
- package/dist/manual-instructions.js +54 -0
- package/dist/types.d.ts +5 -0
- package/jest.config.cjs +2 -1
- package/package.json +5 -4
- package/src/SenderIdentityVerification.ts +395 -58
- package/src/config.ts +10 -1
- package/src/index.ts +2 -0
- package/src/manual-instructions.ts +61 -0
- package/src/types.ts +5 -0
|
@@ -6,34 +6,97 @@ This file is licensed to the client under a limited-use license.
|
|
|
6
6
|
The client may use and modify this code *only within the scope of the project it was delivered for*.
|
|
7
7
|
Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
|
|
8
8
|
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
9
42
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
43
|
exports.SenderIdentityVerification = void 0;
|
|
11
44
|
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
45
|
const verification_email_js_1 = require("./templates/verification-email.js");
|
|
17
46
|
const domain_extractor_js_1 = require("./utils/domain-extractor.js");
|
|
47
|
+
const manual_instructions_js_1 = require("./manual-instructions.js");
|
|
48
|
+
const email_sender_manager_1 = require("@bernierllc/email-sender-manager");
|
|
18
49
|
/**
|
|
19
|
-
* Sender identity verification service
|
|
50
|
+
* Sender identity verification service.
|
|
51
|
+
*
|
|
52
|
+
* When configured with an `EmailSenderManager` (via config.senderManager) and
|
|
53
|
+
* provider plugins (via config.providerPlugins or registerProviderPlugin()),
|
|
54
|
+
* all persistence and provider API calls are delegated. Without those, the
|
|
55
|
+
* service falls back to the legacy in-memory store.
|
|
20
56
|
*/
|
|
21
57
|
class SenderIdentityVerification {
|
|
22
58
|
constructor(config) {
|
|
23
59
|
this.config = config;
|
|
60
|
+
// Legacy in-memory store — used only when no senderManager is configured
|
|
24
61
|
this.senders = new Map();
|
|
62
|
+
// Provider plugin registry (keyed by providerId)
|
|
63
|
+
this.providerPlugins = new Map();
|
|
25
64
|
// Initialize domain verification from config if provided
|
|
26
65
|
if (config.domainVerificationConfig?.instance) {
|
|
27
66
|
this.domainVerification = config.domainVerificationConfig.instance;
|
|
28
67
|
}
|
|
68
|
+
// Wire core sender manager if provided
|
|
69
|
+
if (config.senderManager) {
|
|
70
|
+
this.senderManager = config.senderManager;
|
|
71
|
+
}
|
|
72
|
+
// Register initial provider plugins
|
|
73
|
+
if (config.providerPlugins) {
|
|
74
|
+
for (const plugin of config.providerPlugins) {
|
|
75
|
+
this.providerPlugins.set(plugin.providerId, plugin);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Register a provider plugin at runtime.
|
|
81
|
+
*/
|
|
82
|
+
registerProviderPlugin(plugin) {
|
|
83
|
+
this.providerPlugins.set(plugin.providerId, plugin);
|
|
84
|
+
this.logger?.info('Provider plugin registered', { providerId: plugin.providerId, name: plugin.name });
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get a registered provider plugin by providerId.
|
|
88
|
+
*/
|
|
89
|
+
getProviderPlugin(providerId) {
|
|
90
|
+
return this.providerPlugins.get(providerId);
|
|
29
91
|
}
|
|
30
92
|
/**
|
|
31
93
|
* Initialize service and register with NeverHub
|
|
32
94
|
*/
|
|
33
95
|
async initialize() {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
96
|
+
// Load from database if no senderManager (legacy path)
|
|
97
|
+
if (!this.senderManager) {
|
|
98
|
+
await this.loadSendersFromDatabase();
|
|
99
|
+
}
|
|
37
100
|
this.logger?.info('Sender identity verification service initialized');
|
|
38
101
|
}
|
|
39
102
|
/**
|
|
@@ -50,7 +113,7 @@ class SenderIdentityVerification {
|
|
|
50
113
|
}
|
|
51
114
|
// 2. Extract domain
|
|
52
115
|
const domain = (0, domain_extractor_js_1.extractDomain)(input.email);
|
|
53
|
-
// 3. Check if domain is verified
|
|
116
|
+
// 3. Check if domain is verified
|
|
54
117
|
if (this.domainVerification) {
|
|
55
118
|
const domainStatus = await this.domainVerification.getDomainStatus(domain);
|
|
56
119
|
if (!domainStatus.isVerified) {
|
|
@@ -89,14 +152,51 @@ class SenderIdentityVerification {
|
|
|
89
152
|
if (input.isDefault) {
|
|
90
153
|
await this.unsetOtherDefaults(input.provider);
|
|
91
154
|
}
|
|
92
|
-
// 6. Persist to
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
155
|
+
// 6. Persist — delegate to senderManager if available, else legacy
|
|
156
|
+
if (this.senderManager) {
|
|
157
|
+
const coreRequest = this.toCoreSenderRequest(input, sender);
|
|
158
|
+
const created = await this.senderManager.createSender(coreRequest);
|
|
159
|
+
sender.id = created.id;
|
|
160
|
+
sender.providerSenderId = created.providerSenderId;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
await this.saveSenderToDatabase(sender);
|
|
164
|
+
this.senders.set(sender.id, sender);
|
|
165
|
+
}
|
|
166
|
+
// 7. Delegate to provider plugin for API-level creation
|
|
167
|
+
const plugin = this.providerPlugins.get(input.provider);
|
|
168
|
+
if (plugin && (0, email_sender_manager_1.isManagedSenderProvider)(plugin) && !input.skipVerification) {
|
|
169
|
+
const providerResult = await plugin.createSender({
|
|
170
|
+
fromEmail: input.email,
|
|
171
|
+
fromName: input.name,
|
|
172
|
+
replyToEmail: input.replyToEmail,
|
|
173
|
+
replyToName: input.replyToName,
|
|
174
|
+
providerFields: input.providerFields,
|
|
175
|
+
});
|
|
176
|
+
if (providerResult.success) {
|
|
177
|
+
sender.providerSenderId = providerResult.providerSenderId;
|
|
178
|
+
sender.providerMetadata = providerResult.metadata;
|
|
179
|
+
// Persist the provider sender ID
|
|
180
|
+
if (this.senderManager) {
|
|
181
|
+
await this.senderManager.updateSender(sender.id, {
|
|
182
|
+
providerSenderId: providerResult.providerSenderId,
|
|
183
|
+
providerMetadata: providerResult.metadata,
|
|
184
|
+
lastModifiedBy: input.createdBy || 'system',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.logger?.warn('Provider creation failed, sender saved locally', {
|
|
190
|
+
senderId: sender.id,
|
|
191
|
+
error: providerResult.error?.message,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// 8. Send verification email (unless skipped or provider handles it)
|
|
196
|
+
if (!input.skipVerification && !plugin) {
|
|
97
197
|
await this.sendVerificationEmail(sender);
|
|
98
198
|
}
|
|
99
|
-
//
|
|
199
|
+
// 9. Publish event
|
|
100
200
|
if (this.neverhub) {
|
|
101
201
|
await this.neverhub.publishEvent({
|
|
102
202
|
type: 'sender-identity.created',
|
|
@@ -216,7 +316,7 @@ class SenderIdentityVerification {
|
|
|
216
316
|
errors: ['Account locked']
|
|
217
317
|
};
|
|
218
318
|
}
|
|
219
|
-
// 5. Verify with provider
|
|
319
|
+
// 5. Verify with provider — use plugin system first, fall back to legacy
|
|
220
320
|
const providerResult = await this.verifyWithProvider(sender);
|
|
221
321
|
if (!providerResult.success) {
|
|
222
322
|
sender.status = types_js_1.SenderStatus.FAILED;
|
|
@@ -241,6 +341,17 @@ class SenderIdentityVerification {
|
|
|
241
341
|
sender.validationErrors = undefined;
|
|
242
342
|
sender.updatedAt = new Date();
|
|
243
343
|
await this.saveSenderToDatabase(sender);
|
|
344
|
+
// Update core layer if available
|
|
345
|
+
if (this.senderManager) {
|
|
346
|
+
await this.senderManager.updateSender(sender.id, {
|
|
347
|
+
isVerified: true,
|
|
348
|
+
verificationStatus: 'verified',
|
|
349
|
+
lastVerifiedAt: sender.verifiedAt,
|
|
350
|
+
providerSenderId: sender.providerSenderId,
|
|
351
|
+
providerMetadata: sender.providerMetadata,
|
|
352
|
+
lastModifiedBy: 'system',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
244
355
|
// 7. Publish event
|
|
245
356
|
if (this.neverhub) {
|
|
246
357
|
await this.neverhub.publishEvent({
|
|
@@ -274,37 +385,90 @@ class SenderIdentityVerification {
|
|
|
274
385
|
}
|
|
275
386
|
}
|
|
276
387
|
/**
|
|
277
|
-
* Verify sender with email provider
|
|
388
|
+
* Verify sender with email provider.
|
|
389
|
+
*
|
|
390
|
+
* Uses the plugin registry first. If no plugin is registered for the
|
|
391
|
+
* provider, falls back to the legacy provider classes.
|
|
278
392
|
*/
|
|
279
393
|
async verifyWithProvider(sender) {
|
|
394
|
+
// Try plugin-based verification
|
|
395
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
396
|
+
if (plugin) {
|
|
397
|
+
if ((0, email_sender_manager_1.isVerifiableSenderProvider)(plugin) && sender.providerSenderId) {
|
|
398
|
+
const result = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
399
|
+
if (result.status === 'verified') {
|
|
400
|
+
return {
|
|
401
|
+
success: true,
|
|
402
|
+
data: {
|
|
403
|
+
providerId: result.providerSenderId,
|
|
404
|
+
metadata: { verificationType: result.verificationType },
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
success: false,
|
|
410
|
+
error: result.error?.message || `Verification status: ${result.status}`,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
// Plugin exists but is not verifiable — validate only
|
|
414
|
+
const validation = await plugin.validateSender(sender.email);
|
|
415
|
+
if (validation.isVerified) {
|
|
416
|
+
return {
|
|
417
|
+
success: true,
|
|
418
|
+
data: { metadata: validation.metadata },
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
return { success: true, data: {} };
|
|
422
|
+
}
|
|
423
|
+
// Manual verification providers
|
|
424
|
+
if ((0, manual_instructions_js_1.isManualVerificationProvider)(sender.provider)) {
|
|
425
|
+
const instructions = (0, manual_instructions_js_1.getManualInstructions)(sender.provider);
|
|
426
|
+
return {
|
|
427
|
+
success: true,
|
|
428
|
+
data: {
|
|
429
|
+
metadata: instructions ? { manualInstructions: instructions } : {},
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// Legacy fallback — for backward compatibility when no plugins are registered
|
|
434
|
+
return this.legacyVerifyWithProvider(sender);
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Legacy provider verification using inline provider classes.
|
|
438
|
+
* Preserved for backward compatibility when no plugins are configured.
|
|
439
|
+
*/
|
|
440
|
+
async legacyVerifyWithProvider(sender) {
|
|
441
|
+
// Dynamic imports to avoid breaking if legacy providers are removed later
|
|
280
442
|
switch (sender.provider) {
|
|
281
443
|
case types_js_1.EmailProvider.SENDGRID:
|
|
282
444
|
if (this.config.sendgridApiKey) {
|
|
283
|
-
const
|
|
445
|
+
const { SendGridProvider } = await Promise.resolve().then(() => __importStar(require('./providers/SendGridProvider.js')));
|
|
446
|
+
const provider = new SendGridProvider(this.config.sendgridApiKey);
|
|
284
447
|
return provider.verifySender(sender);
|
|
285
448
|
}
|
|
286
449
|
return { success: false, error: 'SendGrid API key not configured' };
|
|
287
450
|
case types_js_1.EmailProvider.MAILGUN:
|
|
288
451
|
if (this.config.mailgunApiKey) {
|
|
289
|
-
const
|
|
452
|
+
const { MailgunProvider } = await Promise.resolve().then(() => __importStar(require('./providers/MailgunProvider.js')));
|
|
453
|
+
const provider = new MailgunProvider(this.config.mailgunApiKey);
|
|
290
454
|
return provider.verifySender(sender);
|
|
291
455
|
}
|
|
292
|
-
return { success: true, data: {} };
|
|
456
|
+
return { success: true, data: {} };
|
|
293
457
|
case types_js_1.EmailProvider.SES:
|
|
294
458
|
if (this.config.sesAccessKey && this.config.sesSecretKey && this.config.sesRegion) {
|
|
295
|
-
const
|
|
459
|
+
const { SESProvider } = await Promise.resolve().then(() => __importStar(require('./providers/SESProvider.js')));
|
|
460
|
+
const provider = new SESProvider(this.config.sesAccessKey, this.config.sesSecretKey, this.config.sesRegion);
|
|
296
461
|
return provider.verifySender(sender);
|
|
297
462
|
}
|
|
298
463
|
return { success: false, error: 'SES credentials not configured' };
|
|
299
464
|
case types_js_1.EmailProvider.SMTP: {
|
|
300
|
-
const
|
|
465
|
+
const { SMTPProvider } = await Promise.resolve().then(() => __importStar(require('./providers/SMTPProvider.js')));
|
|
466
|
+
const provider = new SMTPProvider();
|
|
301
467
|
return provider.verifySender(sender);
|
|
302
468
|
}
|
|
303
469
|
case types_js_1.EmailProvider.POSTMARK:
|
|
304
470
|
case types_js_1.EmailProvider.MANDRILL:
|
|
305
471
|
case types_js_1.EmailProvider.SMTP2GO:
|
|
306
|
-
// These providers handle sender verification through their own dashboards.
|
|
307
|
-
// Return success to allow the sender to be registered locally.
|
|
308
472
|
return { success: true, data: {} };
|
|
309
473
|
default:
|
|
310
474
|
return {
|
|
@@ -317,6 +481,13 @@ class SenderIdentityVerification {
|
|
|
317
481
|
* Get sender by ID
|
|
318
482
|
*/
|
|
319
483
|
async getSender(senderId) {
|
|
484
|
+
if (this.senderManager) {
|
|
485
|
+
const config = await this.senderManager.getSender(senderId);
|
|
486
|
+
if (!config) {
|
|
487
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
488
|
+
}
|
|
489
|
+
return { success: true, data: this.coreConfigToIdentity(config) };
|
|
490
|
+
}
|
|
320
491
|
const sender = this.senders.get(senderId);
|
|
321
492
|
if (!sender) {
|
|
322
493
|
return {
|
|
@@ -334,6 +505,21 @@ class SenderIdentityVerification {
|
|
|
334
505
|
*/
|
|
335
506
|
async listSenders(options = {}) {
|
|
336
507
|
try {
|
|
508
|
+
if (this.senderManager) {
|
|
509
|
+
const result = await this.senderManager.listSenders({
|
|
510
|
+
provider: options.provider,
|
|
511
|
+
isActive: options.isActive,
|
|
512
|
+
domain: options.domain,
|
|
513
|
+
offset: options.offset,
|
|
514
|
+
limit: options.limit,
|
|
515
|
+
});
|
|
516
|
+
const identities = result.items.map((c) => this.coreConfigToIdentity(c));
|
|
517
|
+
// Apply status filter locally (core layer doesn't have SenderStatus enum)
|
|
518
|
+
const filtered = options.status
|
|
519
|
+
? identities.filter((s) => s.status === options.status)
|
|
520
|
+
: identities;
|
|
521
|
+
return { success: true, data: filtered };
|
|
522
|
+
}
|
|
337
523
|
let senders = Array.from(this.senders.values());
|
|
338
524
|
// Filter by provider
|
|
339
525
|
if (options.provider) {
|
|
@@ -374,13 +560,12 @@ class SenderIdentityVerification {
|
|
|
374
560
|
*/
|
|
375
561
|
async updateSender(senderId, input) {
|
|
376
562
|
try {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
error: `Sender not found: ${senderId}`
|
|
382
|
-
};
|
|
563
|
+
// Fetch from appropriate store
|
|
564
|
+
const getResult = await this.getSender(senderId);
|
|
565
|
+
if (!getResult.success || !getResult.data) {
|
|
566
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
383
567
|
}
|
|
568
|
+
const sender = getResult.data;
|
|
384
569
|
// Update fields
|
|
385
570
|
if (input.name !== undefined)
|
|
386
571
|
sender.name = input.name;
|
|
@@ -404,7 +589,32 @@ class SenderIdentityVerification {
|
|
|
404
589
|
sender.verifiedAt = undefined;
|
|
405
590
|
sender.lastValidated = undefined;
|
|
406
591
|
}
|
|
407
|
-
|
|
592
|
+
// Persist
|
|
593
|
+
if (this.senderManager) {
|
|
594
|
+
await this.senderManager.updateSender(senderId, {
|
|
595
|
+
name: input.name,
|
|
596
|
+
fromName: input.name,
|
|
597
|
+
replyToEmail: input.replyToEmail,
|
|
598
|
+
replyToName: input.replyToName,
|
|
599
|
+
isDefault: input.isDefault,
|
|
600
|
+
isActive: input.isActive,
|
|
601
|
+
lastModifiedBy: input.lastModifiedBy || 'system',
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
else {
|
|
605
|
+
await this.saveSenderToDatabase(sender);
|
|
606
|
+
}
|
|
607
|
+
// Delegate to provider plugin if available
|
|
608
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
609
|
+
if (plugin && (0, email_sender_manager_1.isManagedSenderProvider)(plugin) && sender.providerSenderId) {
|
|
610
|
+
await plugin.updateSender({
|
|
611
|
+
providerSenderId: sender.providerSenderId,
|
|
612
|
+
fromName: input.name,
|
|
613
|
+
replyToEmail: input.replyToEmail,
|
|
614
|
+
replyToName: input.replyToName,
|
|
615
|
+
providerFields: input.providerFields,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
408
618
|
// Publish event
|
|
409
619
|
if (this.neverhub) {
|
|
410
620
|
await this.neverhub.publishEvent({
|
|
@@ -431,19 +641,27 @@ class SenderIdentityVerification {
|
|
|
431
641
|
*/
|
|
432
642
|
async deleteSender(senderId) {
|
|
433
643
|
try {
|
|
434
|
-
const
|
|
435
|
-
if (!
|
|
436
|
-
return {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
644
|
+
const getResult = await this.getSender(senderId);
|
|
645
|
+
if (!getResult.success || !getResult.data) {
|
|
646
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
647
|
+
}
|
|
648
|
+
const sender = getResult.data;
|
|
649
|
+
// Delete from provider if plugin supports it
|
|
650
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
651
|
+
if (plugin && (0, email_sender_manager_1.isManagedSenderProvider)(plugin) && sender.providerSenderId) {
|
|
652
|
+
await plugin.deleteSender(sender.providerSenderId);
|
|
653
|
+
}
|
|
654
|
+
// Persist deletion
|
|
655
|
+
if (this.senderManager) {
|
|
656
|
+
await this.senderManager.deleteSender(senderId);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
sender.deletedAt = new Date();
|
|
660
|
+
sender.isActive = false;
|
|
661
|
+
sender.updatedAt = new Date();
|
|
662
|
+
await this.saveSenderToDatabase(sender);
|
|
663
|
+
this.senders.delete(senderId);
|
|
440
664
|
}
|
|
441
|
-
// Soft delete
|
|
442
|
-
sender.deletedAt = new Date();
|
|
443
|
-
sender.isActive = false;
|
|
444
|
-
sender.updatedAt = new Date();
|
|
445
|
-
await this.saveSenderToDatabase(sender);
|
|
446
|
-
this.senders.delete(senderId);
|
|
447
665
|
// Publish event
|
|
448
666
|
if (this.neverhub) {
|
|
449
667
|
await this.neverhub.publishEvent({
|
|
@@ -467,13 +685,11 @@ class SenderIdentityVerification {
|
|
|
467
685
|
*/
|
|
468
686
|
async checkCompliance(senderId) {
|
|
469
687
|
try {
|
|
470
|
-
const
|
|
471
|
-
if (!
|
|
472
|
-
return {
|
|
473
|
-
success: false,
|
|
474
|
-
error: `Sender not found: ${senderId}`
|
|
475
|
-
};
|
|
688
|
+
const getResult = await this.getSender(senderId);
|
|
689
|
+
if (!getResult.success || !getResult.data) {
|
|
690
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
476
691
|
}
|
|
692
|
+
const sender = getResult.data;
|
|
477
693
|
const errors = [];
|
|
478
694
|
const warnings = [];
|
|
479
695
|
// 1. Check email format
|
|
@@ -481,7 +697,7 @@ class SenderIdentityVerification {
|
|
|
481
697
|
if (!emailFormat) {
|
|
482
698
|
errors.push('Invalid email format');
|
|
483
699
|
}
|
|
484
|
-
// 2. Check domain verification
|
|
700
|
+
// 2. Check domain verification
|
|
485
701
|
let domainVerified = true;
|
|
486
702
|
let spfValid = true;
|
|
487
703
|
let dkimValid = true;
|
|
@@ -500,12 +716,17 @@ class SenderIdentityVerification {
|
|
|
500
716
|
errors.push('DKIM record not configured or invalid');
|
|
501
717
|
}
|
|
502
718
|
}
|
|
503
|
-
// 3. Provider-specific checks
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
719
|
+
// 3. Provider-specific checks via plugin
|
|
720
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
721
|
+
if (plugin && (0, email_sender_manager_1.isVerifiableSenderProvider)(plugin) && sender.providerSenderId) {
|
|
722
|
+
const verificationResult = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
723
|
+
if (verificationResult.status !== 'verified') {
|
|
724
|
+
warnings.push(`Provider verification status: ${verificationResult.status}`);
|
|
507
725
|
}
|
|
508
726
|
}
|
|
727
|
+
else if (sender.provider === types_js_1.EmailProvider.SENDGRID && !sender.providerSenderId) {
|
|
728
|
+
warnings.push('SendGrid sender ID not set - may need re-verification');
|
|
729
|
+
}
|
|
509
730
|
const isCompliant = errors.length === 0;
|
|
510
731
|
const result = {
|
|
511
732
|
isCompliant,
|
|
@@ -539,6 +760,18 @@ class SenderIdentityVerification {
|
|
|
539
760
|
* Get default sender for provider
|
|
540
761
|
*/
|
|
541
762
|
async getDefaultSender(provider) {
|
|
763
|
+
if (this.senderManager) {
|
|
764
|
+
const result = await this.senderManager.listSenders({
|
|
765
|
+
provider,
|
|
766
|
+
isActive: true,
|
|
767
|
+
isVerified: true,
|
|
768
|
+
});
|
|
769
|
+
const defaultSender = result.items.find((s) => s.isDefault);
|
|
770
|
+
if (!defaultSender) {
|
|
771
|
+
return { success: false, error: `No default sender found for provider ${provider}` };
|
|
772
|
+
}
|
|
773
|
+
return { success: true, data: this.coreConfigToIdentity(defaultSender) };
|
|
774
|
+
}
|
|
542
775
|
const senders = Array.from(this.senders.values());
|
|
543
776
|
const defaultSender = senders.find(s => s.provider === provider &&
|
|
544
777
|
s.isDefault &&
|
|
@@ -560,13 +793,11 @@ class SenderIdentityVerification {
|
|
|
560
793
|
*/
|
|
561
794
|
async resendVerification(senderId) {
|
|
562
795
|
try {
|
|
563
|
-
const
|
|
564
|
-
if (!
|
|
565
|
-
return {
|
|
566
|
-
success: false,
|
|
567
|
-
error: `Sender not found: ${senderId}`
|
|
568
|
-
};
|
|
796
|
+
const getResult = await this.getSender(senderId);
|
|
797
|
+
if (!getResult.success || !getResult.data) {
|
|
798
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
569
799
|
}
|
|
800
|
+
const sender = getResult.data;
|
|
570
801
|
if (sender.status === types_js_1.SenderStatus.VERIFIED) {
|
|
571
802
|
return {
|
|
572
803
|
success: false,
|
|
@@ -589,6 +820,14 @@ class SenderIdentityVerification {
|
|
|
589
820
|
};
|
|
590
821
|
}
|
|
591
822
|
}
|
|
823
|
+
// If provider supports verification, use it
|
|
824
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
825
|
+
if (plugin && (0, email_sender_manager_1.isVerifiableSenderProvider)(plugin) && sender.providerSenderId) {
|
|
826
|
+
const result = await plugin.initiateVerification(sender.providerSenderId);
|
|
827
|
+
if (result.success) {
|
|
828
|
+
return { success: true };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
592
831
|
return this.sendVerificationEmail(sender);
|
|
593
832
|
}
|
|
594
833
|
catch (error) {
|
|
@@ -599,6 +838,35 @@ class SenderIdentityVerification {
|
|
|
599
838
|
};
|
|
600
839
|
}
|
|
601
840
|
}
|
|
841
|
+
/**
|
|
842
|
+
* Check verification status for a sender using the provider plugin.
|
|
843
|
+
*/
|
|
844
|
+
async checkVerificationStatus(senderId) {
|
|
845
|
+
const getResult = await this.getSender(senderId);
|
|
846
|
+
if (!getResult.success || !getResult.data) {
|
|
847
|
+
return { success: false, error: `Sender not found: ${senderId}` };
|
|
848
|
+
}
|
|
849
|
+
const sender = getResult.data;
|
|
850
|
+
const plugin = this.providerPlugins.get(sender.provider);
|
|
851
|
+
if (!plugin || !(0, email_sender_manager_1.isVerifiableSenderProvider)(plugin) || !sender.providerSenderId) {
|
|
852
|
+
return {
|
|
853
|
+
success: true,
|
|
854
|
+
data: {
|
|
855
|
+
status: sender.status,
|
|
856
|
+
verificationType: 'manual',
|
|
857
|
+
},
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
const result = await plugin.checkVerificationStatus(sender.providerSenderId);
|
|
861
|
+
return {
|
|
862
|
+
success: result.success,
|
|
863
|
+
data: {
|
|
864
|
+
status: result.status,
|
|
865
|
+
verificationType: result.verificationType,
|
|
866
|
+
dnsRecords: result.dnsRecords,
|
|
867
|
+
},
|
|
868
|
+
};
|
|
869
|
+
}
|
|
602
870
|
// ============================================
|
|
603
871
|
// Private Helper Methods
|
|
604
872
|
// ============================================
|
|
@@ -622,12 +890,31 @@ class SenderIdentityVerification {
|
|
|
622
890
|
return emailRegex.test(email);
|
|
623
891
|
}
|
|
624
892
|
async findSenderByEmail(email) {
|
|
893
|
+
if (this.senderManager) {
|
|
894
|
+
const result = await this.senderManager.listSenders({ limit: 1000 });
|
|
895
|
+
return result.items
|
|
896
|
+
.map((c) => this.coreConfigToIdentity(c))
|
|
897
|
+
.find((s) => s.email === email && !s.deletedAt);
|
|
898
|
+
}
|
|
625
899
|
return Array.from(this.senders.values()).find(s => s.email === email && !s.deletedAt);
|
|
626
900
|
}
|
|
627
901
|
async findSenderByToken(token) {
|
|
902
|
+
// Token lookup is always in-memory (tokens live in the service layer)
|
|
628
903
|
return Array.from(this.senders.values()).find(s => s.verificationToken === token);
|
|
629
904
|
}
|
|
630
905
|
async unsetOtherDefaults(provider) {
|
|
906
|
+
if (this.senderManager) {
|
|
907
|
+
const result = await this.senderManager.listSenders({ provider });
|
|
908
|
+
for (const sender of result.items) {
|
|
909
|
+
if (sender.isDefault) {
|
|
910
|
+
await this.senderManager.updateSender(sender.id, {
|
|
911
|
+
isDefault: false,
|
|
912
|
+
lastModifiedBy: 'system',
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
631
918
|
const senders = Array.from(this.senders.values())
|
|
632
919
|
.filter(s => s.provider === provider && s.isDefault);
|
|
633
920
|
for (const sender of senders) {
|
|
@@ -637,13 +924,57 @@ class SenderIdentityVerification {
|
|
|
637
924
|
}
|
|
638
925
|
}
|
|
639
926
|
async loadSendersFromDatabase() {
|
|
640
|
-
// TODO: Implement database loading
|
|
927
|
+
// TODO: Implement database loading for legacy path
|
|
641
928
|
// For now, no-op
|
|
642
929
|
}
|
|
643
930
|
async saveSenderToDatabase(sender) {
|
|
644
|
-
// TODO: Implement database persistence
|
|
931
|
+
// TODO: Implement database persistence for legacy path
|
|
645
932
|
// For now, just update in-memory map
|
|
646
933
|
this.senders.set(sender.id, sender);
|
|
647
934
|
}
|
|
935
|
+
// ============================================
|
|
936
|
+
// Mapping helpers between service and core types
|
|
937
|
+
// ============================================
|
|
938
|
+
toCoreSenderRequest(input, sender) {
|
|
939
|
+
return {
|
|
940
|
+
name: input.name,
|
|
941
|
+
fromEmail: input.email,
|
|
942
|
+
fromName: input.name,
|
|
943
|
+
replyToEmail: input.replyToEmail,
|
|
944
|
+
replyToName: input.replyToName,
|
|
945
|
+
provider: input.provider,
|
|
946
|
+
allowedDomains: [sender.domain],
|
|
947
|
+
createdBy: input.createdBy || 'system',
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
coreConfigToIdentity(config) {
|
|
951
|
+
return {
|
|
952
|
+
id: config.id,
|
|
953
|
+
email: config.fromEmail,
|
|
954
|
+
name: config.fromName,
|
|
955
|
+
replyToEmail: config.replyToEmail,
|
|
956
|
+
replyToName: config.replyToName,
|
|
957
|
+
domain: config.domain,
|
|
958
|
+
provider: config.provider,
|
|
959
|
+
status: this.mapVerificationStatus(config.verificationStatus),
|
|
960
|
+
isDefault: config.isDefault,
|
|
961
|
+
isActive: config.isActive,
|
|
962
|
+
verificationAttempts: 0,
|
|
963
|
+
verifiedAt: config.lastVerifiedAt,
|
|
964
|
+
providerSenderId: config.providerSenderId,
|
|
965
|
+
providerMetadata: config.providerMetadata,
|
|
966
|
+
createdAt: config.createdAt,
|
|
967
|
+
updatedAt: config.updatedAt,
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
mapVerificationStatus(status) {
|
|
971
|
+
switch (status) {
|
|
972
|
+
case 'verified': return types_js_1.SenderStatus.VERIFIED;
|
|
973
|
+
case 'failed': return types_js_1.SenderStatus.FAILED;
|
|
974
|
+
case 'expired': return types_js_1.SenderStatus.EXPIRED;
|
|
975
|
+
case 'pending':
|
|
976
|
+
default: return types_js_1.SenderStatus.PENDING;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
648
979
|
}
|
|
649
980
|
exports.SenderIdentityVerification = SenderIdentityVerification;
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IEmailDomainVerification } from './types.js';
|
|
2
|
+
import type { EmailSenderManager, SenderProviderPlugin } from '@bernierllc/email-sender-manager';
|
|
2
3
|
/**
|
|
3
4
|
* Configuration for sender identity verification service
|
|
4
5
|
*/
|
|
@@ -10,6 +11,8 @@ export interface SenderIdentityConfig {
|
|
|
10
11
|
domainVerificationConfig: {
|
|
11
12
|
instance?: IEmailDomainVerification;
|
|
12
13
|
};
|
|
14
|
+
senderManager?: EmailSenderManager;
|
|
15
|
+
providerPlugins?: SenderProviderPlugin[];
|
|
13
16
|
retryConfig?: {
|
|
14
17
|
maxRetries?: number;
|
|
15
18
|
initialDelayMs?: number;
|
package/dist/config.js
CHANGED
|
@@ -24,6 +24,8 @@ function loadConfigFromEnv(overrides = {}) {
|
|
|
24
24
|
databaseUrl: process.env.DATABASE_URL || overrides.databaseUrl,
|
|
25
25
|
emailSenderConfig: overrides.emailSenderConfig || {},
|
|
26
26
|
domainVerificationConfig: overrides.domainVerificationConfig || {},
|
|
27
|
+
senderManager: overrides.senderManager,
|
|
28
|
+
providerPlugins: overrides.providerPlugins,
|
|
27
29
|
retryConfig: overrides.retryConfig
|
|
28
30
|
};
|
|
29
31
|
// Log configuration (without sensitive data)
|