@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,412 @@
1
+ /*
2
+ Copyright (c) 2025 Bernier LLC
3
+
4
+ This file is licensed to the client under a limited-use license.
5
+ The client may use and modify this code *only within the scope of the project it was delivered for*.
6
+ Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC.
7
+ */
8
+
9
+ import { SenderIdentityVerification } from '../src/SenderIdentityVerification';
10
+ import { SenderIdentityConfig } from '../src/config';
11
+ import { EmailProvider, SenderStatus, IEmailDomainVerification, DomainVerificationStatus } from '../src/types';
12
+
13
+ describe('Service Coverage Tests', () => {
14
+ let service: SenderIdentityVerification;
15
+ let config: SenderIdentityConfig;
16
+
17
+ beforeEach(() => {
18
+ config = {
19
+ emailSenderConfig: {
20
+ provider: 'smtp'
21
+ },
22
+ domainVerificationConfig: {},
23
+ verificationBaseUrl: 'https://example.com',
24
+ verificationFromEmail: 'verify@example.com',
25
+ verificationFromName: 'Verification Service'
26
+ };
27
+
28
+ service = new SenderIdentityVerification(config);
29
+ });
30
+
31
+ describe('Domain Verification Integration', () => {
32
+ it('should block sender creation when domain is not verified', async () => {
33
+ const mockDomainVerification: IEmailDomainVerification = {
34
+ getDomainStatus: async (domain: string): Promise<DomainVerificationStatus> => {
35
+ return {
36
+ isVerified: false,
37
+ domain
38
+ };
39
+ }
40
+ };
41
+
42
+ const configWithDomain: SenderIdentityConfig = {
43
+ ...config,
44
+ domainVerificationConfig: {
45
+ instance: mockDomainVerification
46
+ }
47
+ };
48
+
49
+ const serviceWithDomain = new SenderIdentityVerification(configWithDomain);
50
+
51
+ const result = await serviceWithDomain.createSender({
52
+ email: 'test@unverified-domain.com',
53
+ name: 'Test User',
54
+ provider: EmailProvider.SMTP
55
+ });
56
+
57
+ expect(result.success).toBe(false);
58
+ expect(result.error).toContain('not verified');
59
+ expect(result.errors).toContain('Domain verification required');
60
+ });
61
+
62
+ it('should allow sender creation when domain is verified', async () => {
63
+ const mockDomainVerification: IEmailDomainVerification = {
64
+ getDomainStatus: async (domain: string): Promise<DomainVerificationStatus> => {
65
+ return {
66
+ isVerified: true,
67
+ domain
68
+ };
69
+ }
70
+ };
71
+
72
+ const configWithDomain: SenderIdentityConfig = {
73
+ ...config,
74
+ domainVerificationConfig: {
75
+ instance: mockDomainVerification
76
+ }
77
+ };
78
+
79
+ const serviceWithDomain = new SenderIdentityVerification(configWithDomain);
80
+
81
+ const result = await serviceWithDomain.createSender({
82
+ email: 'test@verified-domain.com',
83
+ name: 'Test User',
84
+ provider: EmailProvider.SMTP
85
+ });
86
+
87
+ expect(result.success).toBe(true);
88
+ expect(result.data).toBeDefined();
89
+ });
90
+ });
91
+
92
+ describe('Duplicate Sender Detection', () => {
93
+ it('should prevent creating duplicate sender with same email', async () => {
94
+ const createResult1 = await service.createSender({
95
+ email: 'duplicate@example.com',
96
+ name: 'User One',
97
+ provider: EmailProvider.SMTP
98
+ });
99
+
100
+ expect(createResult1.success).toBe(true);
101
+
102
+ const createResult2 = await service.createSender({
103
+ email: 'duplicate@example.com',
104
+ name: 'User Two',
105
+ provider: EmailProvider.SMTP
106
+ });
107
+
108
+ expect(createResult2.success).toBe(false);
109
+ expect(createResult2.error).toContain('already exists');
110
+ });
111
+ });
112
+
113
+ describe('Token Expiration Handling', () => {
114
+ it('should handle expired verification tokens', async () => {
115
+ const createResult = await service.createSender({
116
+ email: 'expiry-test@example.com',
117
+ name: 'Test User',
118
+ provider: EmailProvider.SMTP
119
+ });
120
+
121
+ if (!createResult.success || !createResult.data) {
122
+ throw new Error('Setup failed');
123
+ }
124
+
125
+ const sender = createResult.data;
126
+
127
+ // Manually set expiration to past date
128
+ sender.verificationExpiresAt = new Date(Date.now() - 1000); // 1 second ago
129
+ sender.status = SenderStatus.VERIFICATION_SENT;
130
+
131
+ if (sender.verificationToken) {
132
+ const result = await service.verifySender(sender.verificationToken);
133
+
134
+ expect(result.success).toBe(false);
135
+ expect(result.status).toBe(SenderStatus.EXPIRED);
136
+ expect(result.errors).toContain('Token expired');
137
+ }
138
+ });
139
+ });
140
+
141
+ describe('Locked Sender Handling', () => {
142
+ it('should prevent verification of locked senders', async () => {
143
+ const createResult = await service.createSender({
144
+ email: 'locked-test@example.com',
145
+ name: 'Test User',
146
+ provider: EmailProvider.SMTP
147
+ });
148
+
149
+ if (!createResult.success || !createResult.data) {
150
+ throw new Error('Setup failed');
151
+ }
152
+
153
+ const sender = createResult.data;
154
+
155
+ // Manually lock the sender
156
+ sender.status = SenderStatus.LOCKED;
157
+
158
+ if (sender.verificationToken) {
159
+ const result = await service.verifySender(sender.verificationToken);
160
+
161
+ expect(result.success).toBe(false);
162
+ expect(result.status).toBe(SenderStatus.LOCKED);
163
+ expect(result.errors).toContain('Account locked');
164
+ }
165
+ });
166
+ });
167
+
168
+ describe('Soft Delete Functionality', () => {
169
+ it('should soft delete sender and set deletedAt', async () => {
170
+ const createResult = await service.createSender({
171
+ email: 'soft-delete@example.com',
172
+ name: 'Test User',
173
+ provider: EmailProvider.SMTP
174
+ });
175
+
176
+ if (!createResult.success || !createResult.data) {
177
+ throw new Error('Setup failed');
178
+ }
179
+
180
+ const senderId = createResult.data.id;
181
+
182
+ const deleteResult = await service.deleteSender(senderId);
183
+
184
+ expect(deleteResult.success).toBe(true);
185
+
186
+ // Verify sender was soft deleted by checking it's removed from map
187
+ const getResult = await service.getSender(senderId);
188
+ expect(getResult.success).toBe(false);
189
+ });
190
+ });
191
+
192
+ describe('Default Sender Management', () => {
193
+ it('should set sender as default', async () => {
194
+ const createResult = await service.createSender({
195
+ email: 'default-test@example.com',
196
+ name: 'Default User',
197
+ provider: EmailProvider.SMTP,
198
+ isDefault: true,
199
+ skipVerification: true
200
+ });
201
+
202
+ expect(createResult.success).toBe(true);
203
+ expect(createResult.data?.isDefault).toBe(true);
204
+
205
+ // Manually verify the sender (needed for getDefaultSender)
206
+ if (createResult.data) {
207
+ createResult.data.status = SenderStatus.VERIFIED;
208
+ createResult.data.verifiedAt = new Date();
209
+ }
210
+
211
+ const defaultResult = await service.getDefaultSender(EmailProvider.SMTP);
212
+
213
+ expect(defaultResult.success).toBe(true);
214
+ expect(defaultResult.data?.email).toBe('default-test@example.com');
215
+ });
216
+
217
+ it('should unset previous default when setting new default', async () => {
218
+ const oldResult = await service.createSender({
219
+ email: 'old-default@example.com',
220
+ name: 'Old Default',
221
+ provider: EmailProvider.SMTP,
222
+ isDefault: true,
223
+ skipVerification: true
224
+ });
225
+
226
+ if (oldResult.data) {
227
+ oldResult.data.status = SenderStatus.VERIFIED;
228
+ oldResult.data.verifiedAt = new Date();
229
+ }
230
+
231
+ const newResult = await service.createSender({
232
+ email: 'new-default@example.com',
233
+ name: 'New Default',
234
+ provider: EmailProvider.SMTP,
235
+ isDefault: true,
236
+ skipVerification: true
237
+ });
238
+
239
+ if (newResult.data) {
240
+ newResult.data.status = SenderStatus.VERIFIED;
241
+ newResult.data.verifiedAt = new Date();
242
+ }
243
+
244
+ const defaultResult = await service.getDefaultSender(EmailProvider.SMTP);
245
+
246
+ expect(defaultResult.success).toBe(true);
247
+ expect(defaultResult.data?.email).toBe('new-default@example.com');
248
+ });
249
+ });
250
+
251
+ describe('List Filtering', () => {
252
+ beforeEach(async () => {
253
+ await service.createSender({
254
+ email: 'active@example.com',
255
+ name: 'Active User',
256
+ provider: EmailProvider.SMTP
257
+ });
258
+
259
+ await service.createSender({
260
+ email: 'sendgrid@example.com',
261
+ name: 'SendGrid User',
262
+ provider: EmailProvider.SENDGRID
263
+ });
264
+
265
+ const createResult = await service.createSender({
266
+ email: 'verified@example.com',
267
+ name: 'Verified User',
268
+ provider: EmailProvider.SMTP
269
+ });
270
+
271
+ if (createResult.success && createResult.data) {
272
+ const sender = createResult.data;
273
+ sender.status = SenderStatus.VERIFIED;
274
+ sender.verifiedAt = new Date();
275
+ }
276
+ });
277
+
278
+ it('should filter by provider', async () => {
279
+ const result = await service.listSenders({ provider: EmailProvider.SMTP });
280
+
281
+ expect(result.success).toBe(true);
282
+ expect(result.data).toBeDefined();
283
+ expect(result.data!.length).toBeGreaterThan(0);
284
+ expect(result.data!.every(s => s.provider === EmailProvider.SMTP)).toBe(true);
285
+ });
286
+
287
+ it('should filter by status', async () => {
288
+ const result = await service.listSenders({ status: SenderStatus.VERIFIED });
289
+
290
+ expect(result.success).toBe(true);
291
+ expect(result.data).toBeDefined();
292
+ expect(result.data!.every(s => s.status === SenderStatus.VERIFIED)).toBe(true);
293
+ });
294
+
295
+ it('should filter by isActive', async () => {
296
+ const result = await service.listSenders({ isActive: true });
297
+
298
+ expect(result.success).toBe(true);
299
+ expect(result.data).toBeDefined();
300
+ expect(result.data!.every(s => s.isActive === true)).toBe(true);
301
+ });
302
+
303
+ it('should filter by domain', async () => {
304
+ const result = await service.listSenders({ domain: 'example.com' });
305
+
306
+ expect(result.success).toBe(true);
307
+ expect(result.data).toBeDefined();
308
+ expect(result.data!.every(s => s.domain === 'example.com')).toBe(true);
309
+ });
310
+
311
+ it('should apply limit and offset', async () => {
312
+ const result1 = await service.listSenders({ limit: 1 });
313
+ expect(result1.data!.length).toBeLessThanOrEqual(1);
314
+
315
+ const result2 = await service.listSenders({ offset: 1, limit: 10 });
316
+ expect(result2.success).toBe(true);
317
+ });
318
+ });
319
+
320
+ describe('Compliance Checking', () => {
321
+ it('should check compliance for verified sender', async () => {
322
+ const createResult = await service.createSender({
323
+ email: 'compliance@example.com',
324
+ name: 'Compliance User',
325
+ provider: EmailProvider.SMTP
326
+ });
327
+
328
+ if (!createResult.success || !createResult.data) {
329
+ throw new Error('Setup failed');
330
+ }
331
+
332
+ const sender = createResult.data;
333
+ sender.status = SenderStatus.VERIFIED;
334
+ sender.verifiedAt = new Date();
335
+
336
+ const result = await service.checkCompliance(sender.id);
337
+
338
+ expect(result.success).toBe(true);
339
+ expect(result.data).toBeDefined();
340
+ });
341
+ });
342
+
343
+ describe('Update Sender', () => {
344
+ it('should update sender name and reply-to', async () => {
345
+ const createResult = await service.createSender({
346
+ email: 'update-test@example.com',
347
+ name: 'Original Name',
348
+ provider: EmailProvider.SMTP
349
+ });
350
+
351
+ if (!createResult.success || !createResult.data) {
352
+ throw new Error('Setup failed');
353
+ }
354
+
355
+ const senderId = createResult.data.id;
356
+
357
+ const updateResult = await service.updateSender(senderId, {
358
+ name: 'Updated Name',
359
+ replyToEmail: 'reply@example.com',
360
+ replyToName: 'Reply Name'
361
+ });
362
+
363
+ expect(updateResult.success).toBe(true);
364
+ expect(updateResult.data?.name).toBe('Updated Name');
365
+ expect(updateResult.data?.replyToEmail).toBe('reply@example.com');
366
+ expect(updateResult.data?.replyToName).toBe('Reply Name');
367
+ });
368
+
369
+ it('should update isDefault flag', async () => {
370
+ const createResult = await service.createSender({
371
+ email: 'default-update@example.com',
372
+ name: 'Test User',
373
+ provider: EmailProvider.SMTP,
374
+ isDefault: false
375
+ });
376
+
377
+ if (!createResult.success || !createResult.data) {
378
+ throw new Error('Setup failed');
379
+ }
380
+
381
+ const senderId = createResult.data.id;
382
+
383
+ const updateResult = await service.updateSender(senderId, {
384
+ isDefault: true
385
+ });
386
+
387
+ expect(updateResult.success).toBe(true);
388
+ expect(updateResult.data?.isDefault).toBe(true);
389
+ });
390
+
391
+ it('should update isActive flag', async () => {
392
+ const createResult = await service.createSender({
393
+ email: 'active-update@example.com',
394
+ name: 'Test User',
395
+ provider: EmailProvider.SMTP
396
+ });
397
+
398
+ if (!createResult.success || !createResult.data) {
399
+ throw new Error('Setup failed');
400
+ }
401
+
402
+ const senderId = createResult.data.id;
403
+
404
+ const updateResult = await service.updateSender(senderId, {
405
+ isActive: false
406
+ });
407
+
408
+ expect(updateResult.success).toBe(true);
409
+ expect(updateResult.data?.isActive).toBe(false);
410
+ });
411
+ });
412
+ });
@@ -0,0 +1,72 @@
1
+ import { SenderIdentity, EmailProvider, CreateSenderInput, UpdateSenderInput, VerificationResult, ComplianceCheckResult, ListSendersOptions, SenderIdentityResult } from './types.js';
2
+ import { SenderIdentityConfig } from './config.js';
3
+ /**
4
+ * Sender identity verification service
5
+ */
6
+ export declare class SenderIdentityVerification {
7
+ private config;
8
+ private senders;
9
+ private emailSender?;
10
+ private domainVerification?;
11
+ private cryptoUtils?;
12
+ private logger?;
13
+ private emailValidator?;
14
+ private neverhub?;
15
+ constructor(config: SenderIdentityConfig);
16
+ /**
17
+ * Initialize service and register with NeverHub
18
+ */
19
+ initialize(): Promise<void>;
20
+ /**
21
+ * Create a new sender identity
22
+ */
23
+ createSender(input: CreateSenderInput): Promise<SenderIdentityResult<SenderIdentity>>;
24
+ /**
25
+ * Send verification email to sender
26
+ */
27
+ sendVerificationEmail(sender: SenderIdentity): Promise<SenderIdentityResult<void>>;
28
+ /**
29
+ * Verify sender email with token
30
+ */
31
+ verifySender(token: string): Promise<VerificationResult>;
32
+ /**
33
+ * Verify sender with email provider
34
+ */
35
+ private verifyWithProvider;
36
+ /**
37
+ * Get sender by ID
38
+ */
39
+ getSender(senderId: string): Promise<SenderIdentityResult<SenderIdentity>>;
40
+ /**
41
+ * List senders
42
+ */
43
+ listSenders(options?: ListSendersOptions): Promise<SenderIdentityResult<SenderIdentity[]>>;
44
+ /**
45
+ * Update sender
46
+ */
47
+ updateSender(senderId: string, input: UpdateSenderInput): Promise<SenderIdentityResult<SenderIdentity>>;
48
+ /**
49
+ * Delete sender
50
+ */
51
+ deleteSender(senderId: string): Promise<SenderIdentityResult<void>>;
52
+ /**
53
+ * Check provider compliance for sender
54
+ */
55
+ checkCompliance(senderId: string): Promise<SenderIdentityResult<ComplianceCheckResult>>;
56
+ /**
57
+ * Get default sender for provider
58
+ */
59
+ getDefaultSender(provider: EmailProvider): Promise<SenderIdentityResult<SenderIdentity>>;
60
+ /**
61
+ * Resend verification email
62
+ */
63
+ resendVerification(senderId: string): Promise<SenderIdentityResult<void>>;
64
+ private generateSenderId;
65
+ private fallbackGenerateToken;
66
+ private isValidEmail;
67
+ private findSenderByEmail;
68
+ private findSenderByToken;
69
+ private unsetOtherDefaults;
70
+ private loadSendersFromDatabase;
71
+ private saveSenderToDatabase;
72
+ }