@mikkelscheike/email-provider-links 4.0.11 → 5.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.
- package/README.md +23 -5
- package/dist/alias-detection.js +4 -3
- package/dist/api.d.ts +1 -1
- package/dist/api.js +96 -120
- package/dist/concurrent-dns.d.ts +7 -2
- package/dist/concurrent-dns.js +10 -3
- package/dist/hash-verifier.d.ts +7 -2
- package/dist/hash-verifier.js +12 -3
- package/dist/index.d.ts +12 -17
- package/dist/index.js +34 -33
- package/dist/loader.js +5 -47
- package/dist/provider-loader.d.ts +12 -2
- package/dist/provider-loader.js +6 -40
- package/dist/provider-store.d.ts +9 -0
- package/dist/provider-store.js +49 -0
- package/dist/url-validator.d.ts +7 -2
- package/dist/url-validator.js +19 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@mikkelscheike/email-provider-links)
|
|
4
4
|
|
|
5
|
-
> **Generate direct login links for any email address across
|
|
5
|
+
> **Generate direct login links for any email address across 130+ providers (Gmail, Outlook, Yahoo, etc.) to streamline user authentication flows.**
|
|
6
6
|
|
|
7
|
-
A robust TypeScript library providing direct links to **
|
|
7
|
+
A robust TypeScript library providing direct links to **130 email providers** (218 domains) with **concurrent DNS resolution**, **optimized performance**, **comprehensive email validation**, and advanced security features for login and password reset flows.
|
|
8
8
|
|
|
9
9
|
## 🚀 Try it out
|
|
10
10
|
|
|
@@ -26,8 +26,8 @@ A robust TypeScript library providing direct links to **129 email providers** (2
|
|
|
26
26
|
## ✨ Core Features
|
|
27
27
|
|
|
28
28
|
- 🚀 **Fast & Lightweight**: Zero dependencies, ultra-low memory (0.10MB initial, 0.00004MB per 1000 ops), small footprint (~39.5KB compressed)
|
|
29
|
-
- 📧 **
|
|
30
|
-
- 🌐 **
|
|
29
|
+
- 📧 **130 Email Providers**: Gmail, Outlook, Yahoo, ProtonMail, iCloud, and many more
|
|
30
|
+
- 🌐 **218 Domains Supported**: Comprehensive international coverage
|
|
31
31
|
- 🌍 **Full IDN Support**: International domain names with RFC compliance and Punycode
|
|
32
32
|
- ✅ **Advanced Email Validation**: International email validation with detailed error reporting
|
|
33
33
|
- 🏢 **Business Domain Detection**: DNS-based detection for custom domains (Google Workspace, Microsoft 365, etc.)
|
|
@@ -66,7 +66,7 @@ Fully compatible with the latest Node.js 24.x and 25.x! The library is tested on
|
|
|
66
66
|
|
|
67
67
|
## Supported Providers
|
|
68
68
|
|
|
69
|
-
**📊 Current Coverage:
|
|
69
|
+
**📊 Current Coverage: 130 providers supporting 218 domains**
|
|
70
70
|
|
|
71
71
|
**Consumer Email Providers:**
|
|
72
72
|
- **Gmail** (2 domains): gmail.com, googlemail.com
|
|
@@ -102,6 +102,10 @@ Fully compatible with the latest Node.js 24.x and 25.x! The library is tested on
|
|
|
102
102
|
#### `getEmailProvider(email, timeout?)`
|
|
103
103
|
**Recommended** - Complete provider detection with business domain support.
|
|
104
104
|
|
|
105
|
+
Error notes:
|
|
106
|
+
- `INVALID_EMAIL` is returned for common malformed inputs (e.g. missing `@`, missing TLD).
|
|
107
|
+
- `IDN_VALIDATION_ERROR` is reserved for true encoding issues.
|
|
108
|
+
|
|
105
109
|
```typescript
|
|
106
110
|
// Known providers (instant response)
|
|
107
111
|
const result1 = await getEmailProvider('user@gmail.com');
|
|
@@ -286,6 +290,20 @@ npm run benchmark:dns
|
|
|
286
290
|
# and can be modified for custom performance testing
|
|
287
291
|
```
|
|
288
292
|
|
|
293
|
+
### Live DNS verification (optional)
|
|
294
|
+
|
|
295
|
+
There is an optional test suite that performs real DNS lookups for all domains in `providers/emailproviders.json`:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
RUN_LIVE_DNS=1 npm test -- __tests__/provider-live-dns.test.ts
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Optional strict mode (also validates configured MX/TXT patterns):
|
|
302
|
+
|
|
303
|
+
```bash
|
|
304
|
+
RUN_LIVE_DNS=1 RUN_LIVE_DNS_STRICT=1 npm test -- __tests__/provider-live-dns.test.ts
|
|
305
|
+
```
|
|
306
|
+
|
|
289
307
|
## Contributing
|
|
290
308
|
|
|
291
309
|
We welcome contributions! See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for guidelines on adding new email providers.
|
package/dist/alias-detection.js
CHANGED
|
@@ -47,6 +47,7 @@ exports.detectEmailAlias = detectEmailAlias;
|
|
|
47
47
|
exports.normalizeEmail = normalizeEmail;
|
|
48
48
|
exports.emailsMatch = emailsMatch;
|
|
49
49
|
const loader_1 = require("./loader");
|
|
50
|
+
const idn_1 = require("./idn");
|
|
50
51
|
/**
|
|
51
52
|
* Validates email format
|
|
52
53
|
*/
|
|
@@ -79,14 +80,14 @@ function isValidEmail(email) {
|
|
|
79
80
|
* ```
|
|
80
81
|
*/
|
|
81
82
|
function detectEmailAlias(email) {
|
|
82
|
-
|
|
83
|
+
const originalEmail = email.trim();
|
|
84
|
+
if (!isValidEmail(originalEmail)) {
|
|
83
85
|
throw new Error('Invalid email format');
|
|
84
86
|
}
|
|
85
|
-
const originalEmail = email.trim();
|
|
86
87
|
// Split normally, lowering case both for username and domain by default
|
|
87
88
|
const emailParts = originalEmail.toLowerCase().split('@');
|
|
88
89
|
const username = emailParts[0];
|
|
89
|
-
const domain = emailParts[1]; // domain is always case-insensitive per RFC 5321
|
|
90
|
+
const domain = (0, idn_1.domainToPunycode)(emailParts[1] || ''); // domain is always case-insensitive per RFC 5321
|
|
90
91
|
if (!username || !domain) {
|
|
91
92
|
throw new Error('Invalid email format - missing username or domain');
|
|
92
93
|
}
|
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -14,6 +14,74 @@ exports.emailsMatch = emailsMatch;
|
|
|
14
14
|
exports.getEmailProviderFast = getEmailProviderFast;
|
|
15
15
|
const concurrent_dns_1 = require("./concurrent-dns");
|
|
16
16
|
const provider_loader_1 = require("./provider-loader");
|
|
17
|
+
const idn_1 = require("./idn");
|
|
18
|
+
let cachedProvidersRef = null;
|
|
19
|
+
let cachedDomainMap = null;
|
|
20
|
+
function getDomainMapFromProviders(providers) {
|
|
21
|
+
if (cachedProvidersRef === providers && cachedDomainMap) {
|
|
22
|
+
return cachedDomainMap;
|
|
23
|
+
}
|
|
24
|
+
const domainMap = new Map();
|
|
25
|
+
for (const loadedProvider of providers) {
|
|
26
|
+
for (const domain of loadedProvider.domains) {
|
|
27
|
+
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
cachedProvidersRef = providers;
|
|
31
|
+
cachedDomainMap = domainMap;
|
|
32
|
+
return domainMap;
|
|
33
|
+
}
|
|
34
|
+
function validateAndParseEmailForLookup(email) {
|
|
35
|
+
if (!email || typeof email !== 'string') {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
email: email || '',
|
|
39
|
+
error: {
|
|
40
|
+
type: 'INVALID_EMAIL',
|
|
41
|
+
message: 'Email address is required and must be a string'
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const trimmedEmail = email.trim();
|
|
46
|
+
// Strict validation: treat any IDN validation failure as invalid input.
|
|
47
|
+
// Only surface IDN_VALIDATION_ERROR for true encoding issues.
|
|
48
|
+
const idnError = (0, idn_1.validateInternationalEmail)(trimmedEmail);
|
|
49
|
+
if (idnError) {
|
|
50
|
+
if (idnError.code === idn_1.IDNValidationError.INVALID_ENCODING) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
email: trimmedEmail,
|
|
54
|
+
error: {
|
|
55
|
+
type: 'IDN_VALIDATION_ERROR',
|
|
56
|
+
message: idnError.message,
|
|
57
|
+
idnError: idnError.code
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
email: trimmedEmail,
|
|
64
|
+
error: {
|
|
65
|
+
type: 'INVALID_EMAIL',
|
|
66
|
+
message: 'Invalid email format'
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const atIndex = trimmedEmail.lastIndexOf('@');
|
|
71
|
+
if (atIndex === -1) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
email: trimmedEmail,
|
|
75
|
+
error: {
|
|
76
|
+
type: 'INVALID_EMAIL',
|
|
77
|
+
message: 'Invalid email format'
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const domainRaw = trimmedEmail.slice(atIndex + 1).toLowerCase();
|
|
82
|
+
const domain = (0, idn_1.domainToPunycode)(domainRaw);
|
|
83
|
+
return { ok: true, trimmedEmail, domain };
|
|
84
|
+
}
|
|
17
85
|
/**
|
|
18
86
|
* Get email provider information for any email address.
|
|
19
87
|
*
|
|
@@ -46,43 +114,16 @@ const provider_loader_1 = require("./provider-loader");
|
|
|
46
114
|
*/
|
|
47
115
|
async function getEmailProvider(email, timeout) {
|
|
48
116
|
try {
|
|
49
|
-
|
|
50
|
-
if (!
|
|
117
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
118
|
+
if (!parsed.ok) {
|
|
51
119
|
return {
|
|
52
120
|
provider: null,
|
|
53
|
-
email: email
|
|
121
|
+
email: parsed.email,
|
|
54
122
|
loginUrl: null,
|
|
55
|
-
error:
|
|
56
|
-
type: 'INVALID_EMAIL',
|
|
57
|
-
message: 'Email address is required and must be a string'
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
// Basic email format validation
|
|
62
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
63
|
-
if (!emailRegex.test(email)) {
|
|
64
|
-
return {
|
|
65
|
-
provider: null,
|
|
66
|
-
email,
|
|
67
|
-
loginUrl: null,
|
|
68
|
-
error: {
|
|
69
|
-
type: 'INVALID_EMAIL',
|
|
70
|
-
message: 'Invalid email format'
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
75
|
-
if (!domain) {
|
|
76
|
-
return {
|
|
77
|
-
provider: null,
|
|
78
|
-
email,
|
|
79
|
-
loginUrl: null,
|
|
80
|
-
error: {
|
|
81
|
-
type: 'INVALID_EMAIL',
|
|
82
|
-
message: 'Invalid email format - missing domain'
|
|
83
|
-
}
|
|
123
|
+
error: parsed.error
|
|
84
124
|
};
|
|
85
125
|
}
|
|
126
|
+
const domain = parsed.domain;
|
|
86
127
|
// First try synchronous domain matching
|
|
87
128
|
const syncResult = getEmailProviderSync(email);
|
|
88
129
|
if (syncResult.provider) {
|
|
@@ -130,9 +171,9 @@ async function getEmailProvider(email, timeout) {
|
|
|
130
171
|
}
|
|
131
172
|
catch (error) {
|
|
132
173
|
// Enhanced error handling
|
|
133
|
-
if (error.message
|
|
174
|
+
if (error instanceof Error && error.message.includes('Rate limit exceeded')) {
|
|
134
175
|
const retryMatch = error.message.match(/Try again in (\d+) seconds/);
|
|
135
|
-
const retryAfter = retryMatch ? parseInt(retryMatch[1], 10) : undefined;
|
|
176
|
+
const retryAfter = retryMatch?.[1] ? parseInt(retryMatch[1], 10) : undefined;
|
|
136
177
|
return {
|
|
137
178
|
provider: null,
|
|
138
179
|
email,
|
|
@@ -144,7 +185,7 @@ async function getEmailProvider(email, timeout) {
|
|
|
144
185
|
}
|
|
145
186
|
};
|
|
146
187
|
}
|
|
147
|
-
if (error.message
|
|
188
|
+
if (error instanceof Error && error.message.includes('timeout')) {
|
|
148
189
|
return {
|
|
149
190
|
provider: null,
|
|
150
191
|
email,
|
|
@@ -161,7 +202,7 @@ async function getEmailProvider(email, timeout) {
|
|
|
161
202
|
loginUrl: null,
|
|
162
203
|
error: {
|
|
163
204
|
type: 'NETWORK_ERROR',
|
|
164
|
-
message: error.message
|
|
205
|
+
message: error instanceof Error ? error.message : 'Unknown network error'
|
|
165
206
|
}
|
|
166
207
|
};
|
|
167
208
|
}
|
|
@@ -189,44 +230,16 @@ async function getEmailProvider(email, timeout) {
|
|
|
189
230
|
*/
|
|
190
231
|
function getEmailProviderSync(email) {
|
|
191
232
|
try {
|
|
192
|
-
|
|
193
|
-
if (!
|
|
194
|
-
return {
|
|
195
|
-
provider: null,
|
|
196
|
-
email: email || '',
|
|
197
|
-
loginUrl: null,
|
|
198
|
-
error: {
|
|
199
|
-
type: 'INVALID_EMAIL',
|
|
200
|
-
message: 'Email address is required and must be a string'
|
|
201
|
-
}
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
// Basic email format validation
|
|
205
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
206
|
-
if (!emailRegex.test(email)) {
|
|
233
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
234
|
+
if (!parsed.ok) {
|
|
207
235
|
return {
|
|
208
236
|
provider: null,
|
|
209
|
-
email,
|
|
237
|
+
email: parsed.email,
|
|
210
238
|
loginUrl: null,
|
|
211
|
-
error:
|
|
212
|
-
type: 'INVALID_EMAIL',
|
|
213
|
-
message: 'Invalid email format'
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
// Pure synchronous domain matching
|
|
218
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
219
|
-
if (!domain) {
|
|
220
|
-
return {
|
|
221
|
-
provider: null,
|
|
222
|
-
email,
|
|
223
|
-
loginUrl: null,
|
|
224
|
-
error: {
|
|
225
|
-
type: 'INVALID_EMAIL',
|
|
226
|
-
message: 'Invalid email format - missing domain'
|
|
227
|
-
}
|
|
239
|
+
error: parsed.error
|
|
228
240
|
};
|
|
229
241
|
}
|
|
242
|
+
const domain = parsed.domain;
|
|
230
243
|
// Load providers with verification
|
|
231
244
|
let provider = null;
|
|
232
245
|
try {
|
|
@@ -246,13 +259,7 @@ function getEmailProviderSync(email) {
|
|
|
246
259
|
}
|
|
247
260
|
};
|
|
248
261
|
}
|
|
249
|
-
const domainMap =
|
|
250
|
-
// Build domain map from loaded providers
|
|
251
|
-
for (const loadedProvider of result.providers) {
|
|
252
|
-
for (const domain of loadedProvider.domains) {
|
|
253
|
-
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
262
|
+
const domainMap = getDomainMapFromProviders(result.providers);
|
|
256
263
|
provider = domainMap.get(domain) || null;
|
|
257
264
|
}
|
|
258
265
|
catch (error) {
|
|
@@ -291,7 +298,7 @@ function getEmailProviderSync(email) {
|
|
|
291
298
|
loginUrl: null,
|
|
292
299
|
error: {
|
|
293
300
|
type: 'INVALID_EMAIL',
|
|
294
|
-
message: error.message
|
|
301
|
+
message: error instanceof Error ? error.message : 'Invalid email address'
|
|
295
302
|
}
|
|
296
303
|
};
|
|
297
304
|
}
|
|
@@ -327,7 +334,7 @@ function normalizeEmail(email) {
|
|
|
327
334
|
return lowercaseEmail;
|
|
328
335
|
}
|
|
329
336
|
let localPart = lowercaseEmail.slice(0, atIndex);
|
|
330
|
-
const domainPart = lowercaseEmail.slice(atIndex + 1);
|
|
337
|
+
const domainPart = (0, idn_1.domainToPunycode)(lowercaseEmail.slice(atIndex + 1));
|
|
331
338
|
// Use providers for domain lookup
|
|
332
339
|
let provider = null;
|
|
333
340
|
try {
|
|
@@ -335,12 +342,7 @@ function normalizeEmail(email) {
|
|
|
335
342
|
if (!result.success) {
|
|
336
343
|
return lowercaseEmail; // Return as-is if providers can't be loaded
|
|
337
344
|
}
|
|
338
|
-
const domainMap =
|
|
339
|
-
for (const loadedProvider of result.providers) {
|
|
340
|
-
for (const domain of loadedProvider.domains) {
|
|
341
|
-
domainMap.set(domain.toLowerCase(), loadedProvider);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
345
|
+
const domainMap = getDomainMapFromProviders(result.providers);
|
|
344
346
|
provider = domainMap.get(domainPart) || null;
|
|
345
347
|
}
|
|
346
348
|
catch (error) {
|
|
@@ -414,45 +416,19 @@ function emailsMatch(email1, email2) {
|
|
|
414
416
|
async function getEmailProviderFast(email, options = {}) {
|
|
415
417
|
const { timeout = 5000, enableParallel = true, collectDebugInfo = false } = options;
|
|
416
418
|
try {
|
|
417
|
-
|
|
418
|
-
if (!
|
|
419
|
-
return {
|
|
420
|
-
provider: null,
|
|
421
|
-
email: email || '',
|
|
422
|
-
loginUrl: null,
|
|
423
|
-
error: {
|
|
424
|
-
type: 'INVALID_EMAIL',
|
|
425
|
-
message: 'Email address is required and must be a string'
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
|
-
// Basic email format validation
|
|
430
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
431
|
-
if (!emailRegex.test(email)) {
|
|
432
|
-
return {
|
|
433
|
-
provider: null,
|
|
434
|
-
email,
|
|
435
|
-
loginUrl: null,
|
|
436
|
-
error: {
|
|
437
|
-
type: 'INVALID_EMAIL',
|
|
438
|
-
message: 'Invalid email format'
|
|
439
|
-
}
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
const domain = email.split('@')[1]?.toLowerCase();
|
|
443
|
-
if (!domain) {
|
|
419
|
+
const parsed = validateAndParseEmailForLookup(email);
|
|
420
|
+
if (!parsed.ok) {
|
|
444
421
|
return {
|
|
445
422
|
provider: null,
|
|
446
|
-
email,
|
|
423
|
+
email: parsed.email,
|
|
447
424
|
loginUrl: null,
|
|
448
|
-
error:
|
|
449
|
-
type: 'INVALID_EMAIL',
|
|
450
|
-
message: 'Invalid email format - missing domain'
|
|
451
|
-
}
|
|
425
|
+
error: parsed.error
|
|
452
426
|
};
|
|
453
427
|
}
|
|
428
|
+
const domain = parsed.domain;
|
|
429
|
+
const trimmedEmail = parsed.trimmedEmail;
|
|
454
430
|
// First try standard domain matching (fast path)
|
|
455
|
-
const syncResult = getEmailProviderSync(
|
|
431
|
+
const syncResult = getEmailProviderSync(trimmedEmail);
|
|
456
432
|
if (syncResult.provider) {
|
|
457
433
|
return {
|
|
458
434
|
...syncResult,
|
|
@@ -466,7 +442,7 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
466
442
|
if (!result.success) {
|
|
467
443
|
return {
|
|
468
444
|
provider: null,
|
|
469
|
-
email,
|
|
445
|
+
email: trimmedEmail,
|
|
470
446
|
loginUrl: null,
|
|
471
447
|
error: {
|
|
472
448
|
type: 'NETWORK_ERROR',
|
|
@@ -482,7 +458,7 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
482
458
|
});
|
|
483
459
|
const fastResult = {
|
|
484
460
|
provider: concurrentResult.provider,
|
|
485
|
-
email,
|
|
461
|
+
email: trimmedEmail,
|
|
486
462
|
loginUrl: concurrentResult.provider?.loginUrl || null,
|
|
487
463
|
detectionMethod: concurrentResult.detectionMethod || 'mx_record',
|
|
488
464
|
timing: concurrentResult.timing,
|
|
@@ -505,7 +481,7 @@ async function getEmailProviderFast(email, options = {}) {
|
|
|
505
481
|
loginUrl: null,
|
|
506
482
|
error: {
|
|
507
483
|
type: 'NETWORK_ERROR',
|
|
508
|
-
message: error.message
|
|
484
|
+
message: error instanceof Error ? error.message : 'DNS detection failed'
|
|
509
485
|
}
|
|
510
486
|
};
|
|
511
487
|
}
|
package/dist/concurrent-dns.d.ts
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* Uses Promise.allSettled for fault tolerance and intelligent result merging.
|
|
6
6
|
*/
|
|
7
7
|
import { EmailProvider } from './api';
|
|
8
|
+
type MXRecordLike = {
|
|
9
|
+
exchange?: string;
|
|
10
|
+
};
|
|
11
|
+
type TXTRecordLike = string;
|
|
8
12
|
/**
|
|
9
13
|
* Configuration for concurrent DNS detection
|
|
10
14
|
*/
|
|
@@ -29,13 +33,13 @@ export interface DNSQueryResult {
|
|
|
29
33
|
/** Whether the query succeeded */
|
|
30
34
|
success: boolean;
|
|
31
35
|
/** DNS records returned (if successful) */
|
|
32
|
-
records?:
|
|
36
|
+
records?: MXRecordLike[] | TXTRecordLike[];
|
|
33
37
|
/** Error information (if failed) */
|
|
34
38
|
error?: Error;
|
|
35
39
|
/** Query execution time in milliseconds */
|
|
36
40
|
timing: number;
|
|
37
41
|
/** Raw DNS response for debugging */
|
|
38
|
-
rawResponse?:
|
|
42
|
+
rawResponse?: unknown;
|
|
39
43
|
}
|
|
40
44
|
/**
|
|
41
45
|
* Result from concurrent DNS detection
|
|
@@ -130,4 +134,5 @@ export declare function createConcurrentDNSDetector(providers: EmailProvider[],
|
|
|
130
134
|
* Utility function for quick concurrent DNS detection
|
|
131
135
|
*/
|
|
132
136
|
export declare function detectProviderConcurrent(domain: string, providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>): Promise<ConcurrentDNSResult>;
|
|
137
|
+
export {};
|
|
133
138
|
//# sourceMappingURL=concurrent-dns.d.ts.map
|
package/dist/concurrent-dns.js
CHANGED
|
@@ -295,6 +295,8 @@ class ConcurrentDNSDetector {
|
|
|
295
295
|
let confidence = 0;
|
|
296
296
|
if (query.type === 'mx' && detection.mxPatterns) {
|
|
297
297
|
for (const record of query.records) {
|
|
298
|
+
if (typeof record !== 'object' || record === null)
|
|
299
|
+
continue;
|
|
298
300
|
const exchange = record.exchange?.toLowerCase() || '';
|
|
299
301
|
for (const pattern of detection.mxPatterns) {
|
|
300
302
|
if (exchange.includes(pattern.toLowerCase())) {
|
|
@@ -306,6 +308,8 @@ class ConcurrentDNSDetector {
|
|
|
306
308
|
}
|
|
307
309
|
else if (query.type === 'txt' && detection.txtPatterns) {
|
|
308
310
|
for (const record of query.records) {
|
|
311
|
+
if (typeof record !== 'string')
|
|
312
|
+
continue;
|
|
309
313
|
const txtRecord = record.toLowerCase();
|
|
310
314
|
for (const pattern of detection.txtPatterns) {
|
|
311
315
|
if (txtRecord.includes(pattern.toLowerCase())) {
|
|
@@ -368,6 +372,8 @@ class ConcurrentDNSDetector {
|
|
|
368
372
|
if (!mxQuery?.records)
|
|
369
373
|
return null;
|
|
370
374
|
for (const record of mxQuery.records) {
|
|
375
|
+
if (typeof record !== 'object' || record === null)
|
|
376
|
+
continue;
|
|
371
377
|
const exchange = record.exchange?.toLowerCase() || '';
|
|
372
378
|
for (const provider of this.providers) {
|
|
373
379
|
if (provider.type === 'proxy_service' && provider.customDomainDetection?.mxPatterns) {
|
|
@@ -402,9 +408,11 @@ class ConcurrentDNSDetector {
|
|
|
402
408
|
return Promise.reject(new Error(`DNS query timeout after ${ms}ms`));
|
|
403
409
|
}
|
|
404
410
|
let rejectFn;
|
|
411
|
+
let queryEntry;
|
|
405
412
|
const wrappedPromise = new Promise((resolve, reject) => {
|
|
406
413
|
rejectFn = reject;
|
|
407
|
-
const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms)
|
|
414
|
+
const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms);
|
|
415
|
+
timeout.unref?.();
|
|
408
416
|
// Start the underlying operation only after setting up the timeout
|
|
409
417
|
fn()
|
|
410
418
|
.then(resolve)
|
|
@@ -412,7 +420,6 @@ class ConcurrentDNSDetector {
|
|
|
412
420
|
.finally(() => {
|
|
413
421
|
clearTimeout(timeout);
|
|
414
422
|
// Clean up active query
|
|
415
|
-
const queryEntry = Array.from(this.activeQueries).find(entry => entry.promise === wrappedPromise);
|
|
416
423
|
if (queryEntry) {
|
|
417
424
|
this.activeQueries.delete(queryEntry);
|
|
418
425
|
}
|
|
@@ -420,7 +427,7 @@ class ConcurrentDNSDetector {
|
|
|
420
427
|
});
|
|
421
428
|
// Track active query for potential cleanup in tests
|
|
422
429
|
if (rejectFn) {
|
|
423
|
-
|
|
430
|
+
queryEntry = { promise: wrappedPromise, reject: rejectFn };
|
|
424
431
|
this.activeQueries.add(queryEntry);
|
|
425
432
|
}
|
|
426
433
|
return wrappedPromise;
|
package/dist/hash-verifier.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Provides cryptographic integrity verification for the email providers database
|
|
5
5
|
* to detect tampering or unauthorized modifications.
|
|
6
6
|
*/
|
|
7
|
+
type ProviderLike = {
|
|
8
|
+
companyProvider?: string;
|
|
9
|
+
loginUrl?: string | null;
|
|
10
|
+
};
|
|
7
11
|
export interface HashVerificationResult {
|
|
8
12
|
isValid: boolean;
|
|
9
13
|
expectedHash?: string;
|
|
@@ -40,7 +44,7 @@ export declare function verifyProvidersIntegrity(filePath: string, expectedHash?
|
|
|
40
44
|
* @param expectedHash - Expected hash of the JSON string
|
|
41
45
|
* @returns Verification result
|
|
42
46
|
*/
|
|
43
|
-
export declare function verifyProvidersDataIntegrity(providersData:
|
|
47
|
+
export declare function verifyProvidersDataIntegrity(providersData: unknown, expectedHash?: string): HashVerificationResult;
|
|
44
48
|
/**
|
|
45
49
|
* Generates security hashes for critical files - use this during development
|
|
46
50
|
*
|
|
@@ -85,10 +89,11 @@ export declare function performSecurityAudit(providersFilePath?: string): {
|
|
|
85
89
|
* @param providers - Array of email providers
|
|
86
90
|
* @returns Signed manifest with URL hashes
|
|
87
91
|
*/
|
|
88
|
-
export declare function createProviderManifest(providers:
|
|
92
|
+
export declare function createProviderManifest(providers: ProviderLike[]): {
|
|
89
93
|
timestamp: string;
|
|
90
94
|
providerCount: number;
|
|
91
95
|
urlHashes: Record<string, string>;
|
|
92
96
|
manifestHash: string;
|
|
93
97
|
};
|
|
98
|
+
export {};
|
|
94
99
|
//# sourceMappingURL=hash-verifier.d.ts.map
|
package/dist/hash-verifier.js
CHANGED
|
@@ -29,7 +29,7 @@ const KNOWN_GOOD_HASHES = {
|
|
|
29
29
|
// SHA-256 hash of the legitimate emailproviders.json
|
|
30
30
|
'emailproviders.json': 'a4fe056edad44ae5479cc100d5cc67cb5f6df86e19c4209db6c5f715f5bf070e',
|
|
31
31
|
// You can add hashes for other critical files
|
|
32
|
-
'package.json': '
|
|
32
|
+
'package.json': '4133771103c0b1600b7d19c2290822eaeaaa9b1b84ef42965de4066545e1e960',
|
|
33
33
|
};
|
|
34
34
|
/**
|
|
35
35
|
* Calculates SHA-256 hash of a file or string content
|
|
@@ -96,8 +96,17 @@ function verifyProvidersIntegrity(filePath, expectedHash) {
|
|
|
96
96
|
*/
|
|
97
97
|
function verifyProvidersDataIntegrity(providersData, expectedHash) {
|
|
98
98
|
try {
|
|
99
|
+
if (providersData === null || typeof providersData !== 'object') {
|
|
100
|
+
return {
|
|
101
|
+
isValid: false,
|
|
102
|
+
actualHash: '',
|
|
103
|
+
reason: 'Invalid providers data format',
|
|
104
|
+
file: 'providersData'
|
|
105
|
+
};
|
|
106
|
+
}
|
|
99
107
|
// Create deterministic JSON string (sorted keys)
|
|
100
|
-
const
|
|
108
|
+
const providersObject = providersData;
|
|
109
|
+
const jsonString = JSON.stringify(providersObject, Object.keys(providersObject).sort(), 2);
|
|
101
110
|
const actualHash = calculateHash(jsonString);
|
|
102
111
|
const expectedHashToUse = expectedHash || KNOWN_GOOD_HASHES['emailproviders.json'];
|
|
103
112
|
if (expectedHashToUse === 'TO_BE_CALCULATED') {
|
|
@@ -266,7 +275,7 @@ function createProviderManifest(providers) {
|
|
|
266
275
|
const urlHashes = {};
|
|
267
276
|
for (const provider of providers) {
|
|
268
277
|
if (provider.loginUrl) {
|
|
269
|
-
const key = `${provider.companyProvider}::${provider.loginUrl}`;
|
|
278
|
+
const key = `${provider.companyProvider ?? 'Unknown'}::${provider.loginUrl}`;
|
|
270
279
|
urlHashes[key] = calculateHash(provider.loginUrl);
|
|
271
280
|
}
|
|
272
281
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
*
|
|
13
13
|
* @author Email Provider Links Team
|
|
14
14
|
* @license MIT
|
|
15
|
-
* @version
|
|
15
|
+
* @version See package.json
|
|
16
16
|
*/
|
|
17
|
-
|
|
17
|
+
import { loadProviders } from './loader';
|
|
18
|
+
import { getEmailProvider, getEmailProviderSync, getEmailProviderFast, normalizeEmail, emailsMatch, Config } from './api';
|
|
19
|
+
import { detectProviderConcurrent } from './concurrent-dns';
|
|
20
|
+
import { validateInternationalEmail, emailToPunycode, domainToPunycode } from './idn';
|
|
21
|
+
export { getEmailProvider, getEmailProviderSync, getEmailProviderFast, normalizeEmail, emailsMatch, Config };
|
|
18
22
|
export type { EmailProvider, EmailProviderResult } from './api';
|
|
19
|
-
export { loadProviders
|
|
20
|
-
export { detectProviderConcurrent } from './concurrent-dns';
|
|
21
|
-
export { validateInternationalEmail, emailToPunycode, domainToPunycode } from './idn';
|
|
23
|
+
export { loadProviders, detectProviderConcurrent, validateInternationalEmail, emailToPunycode, domainToPunycode };
|
|
22
24
|
export type { ConcurrentDNSConfig, ConcurrentDNSResult } from './concurrent-dns';
|
|
23
25
|
/**
|
|
24
26
|
* Enhanced email validation with comprehensive error reporting
|
|
@@ -45,10 +47,6 @@ export declare function validateEmailAddress(email: string): {
|
|
|
45
47
|
message: string;
|
|
46
48
|
};
|
|
47
49
|
};
|
|
48
|
-
import { loadProviders } from './loader';
|
|
49
|
-
import { getEmailProvider, getEmailProviderSync, getEmailProviderFast, normalizeEmail, emailsMatch } from './api';
|
|
50
|
-
import { detectProviderConcurrent } from './concurrent-dns';
|
|
51
|
-
import { validateInternationalEmail } from './idn';
|
|
52
50
|
/**
|
|
53
51
|
* Get comprehensive list of all supported email providers
|
|
54
52
|
*
|
|
@@ -169,8 +167,8 @@ export declare const isValidEmailAddress: typeof isValidEmail;
|
|
|
169
167
|
/**
|
|
170
168
|
* Library metadata (legacy constants)
|
|
171
169
|
*/
|
|
172
|
-
export declare const PROVIDER_COUNT
|
|
173
|
-
export declare const DOMAIN_COUNT
|
|
170
|
+
export declare const PROVIDER_COUNT: 130;
|
|
171
|
+
export declare const DOMAIN_COUNT: 218;
|
|
174
172
|
/**
|
|
175
173
|
* Default export for convenience
|
|
176
174
|
*
|
|
@@ -203,12 +201,9 @@ declare const _default: {
|
|
|
203
201
|
readonly SUPPORTED_PROVIDERS_COUNT: 130;
|
|
204
202
|
readonly SUPPORTED_DOMAINS_COUNT: 218;
|
|
205
203
|
};
|
|
206
|
-
PROVIDER_COUNT:
|
|
207
|
-
DOMAIN_COUNT:
|
|
204
|
+
PROVIDER_COUNT: 130;
|
|
205
|
+
DOMAIN_COUNT: 218;
|
|
208
206
|
};
|
|
209
207
|
export default _default;
|
|
210
|
-
|
|
211
|
-
* Version information
|
|
212
|
-
*/
|
|
213
|
-
export declare const VERSION = "2.7.0";
|
|
208
|
+
export declare const VERSION: string;
|
|
214
209
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
* @author Email Provider Links Team
|
|
15
15
|
* @license MIT
|
|
16
|
-
* @version
|
|
16
|
+
* @version See package.json
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
19
|
exports.VERSION = exports.DOMAIN_COUNT = exports.PROVIDER_COUNT = exports.isValidEmailAddress = exports.domainToPunycode = exports.emailToPunycode = exports.validateInternationalEmail = exports.detectProviderConcurrent = exports.loadProviders = exports.Config = exports.emailsMatch = exports.normalizeEmail = exports.getEmailProviderFast = exports.getEmailProviderSync = exports.getEmailProvider = void 0;
|
|
@@ -24,22 +24,18 @@ exports.extractDomain = extractDomain;
|
|
|
24
24
|
exports.isValidEmail = isValidEmail;
|
|
25
25
|
exports.getLibraryStats = getLibraryStats;
|
|
26
26
|
exports.batchProcessEmails = batchProcessEmails;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
const loader_1 = require("./loader");
|
|
28
|
+
Object.defineProperty(exports, "loadProviders", { enumerable: true, get: function () { return loader_1.loadProviders; } });
|
|
29
|
+
const api_1 = require("./api");
|
|
30
30
|
Object.defineProperty(exports, "getEmailProvider", { enumerable: true, get: function () { return api_1.getEmailProvider; } });
|
|
31
31
|
Object.defineProperty(exports, "getEmailProviderSync", { enumerable: true, get: function () { return api_1.getEmailProviderSync; } });
|
|
32
32
|
Object.defineProperty(exports, "getEmailProviderFast", { enumerable: true, get: function () { return api_1.getEmailProviderFast; } });
|
|
33
33
|
Object.defineProperty(exports, "normalizeEmail", { enumerable: true, get: function () { return api_1.normalizeEmail; } });
|
|
34
34
|
Object.defineProperty(exports, "emailsMatch", { enumerable: true, get: function () { return api_1.emailsMatch; } });
|
|
35
35
|
Object.defineProperty(exports, "Config", { enumerable: true, get: function () { return api_1.Config; } });
|
|
36
|
-
|
|
37
|
-
// For power users and custom implementations
|
|
38
|
-
var loader_1 = require("./loader");
|
|
39
|
-
Object.defineProperty(exports, "loadProviders", { enumerable: true, get: function () { return loader_1.loadProviders; } });
|
|
40
|
-
var concurrent_dns_1 = require("./concurrent-dns");
|
|
36
|
+
const concurrent_dns_1 = require("./concurrent-dns");
|
|
41
37
|
Object.defineProperty(exports, "detectProviderConcurrent", { enumerable: true, get: function () { return concurrent_dns_1.detectProviderConcurrent; } });
|
|
42
|
-
|
|
38
|
+
const idn_1 = require("./idn");
|
|
43
39
|
Object.defineProperty(exports, "validateInternationalEmail", { enumerable: true, get: function () { return idn_1.validateInternationalEmail; } });
|
|
44
40
|
Object.defineProperty(exports, "emailToPunycode", { enumerable: true, get: function () { return idn_1.emailToPunycode; } });
|
|
45
41
|
Object.defineProperty(exports, "domainToPunycode", { enumerable: true, get: function () { return idn_1.domainToPunycode; } });
|
|
@@ -86,7 +82,7 @@ function validateEmailAddress(email) {
|
|
|
86
82
|
};
|
|
87
83
|
}
|
|
88
84
|
// Use international validation
|
|
89
|
-
const idnError = (0,
|
|
85
|
+
const idnError = (0, idn_1.validateInternationalEmail)(trimmedEmail);
|
|
90
86
|
if (idnError) {
|
|
91
87
|
return {
|
|
92
88
|
isValid: false,
|
|
@@ -104,10 +100,6 @@ function validateEmailAddress(email) {
|
|
|
104
100
|
}
|
|
105
101
|
// ===== UTILITY FUNCTIONS =====
|
|
106
102
|
// Helper functions for common tasks
|
|
107
|
-
const loader_2 = require("./loader");
|
|
108
|
-
const api_2 = require("./api");
|
|
109
|
-
const concurrent_dns_2 = require("./concurrent-dns");
|
|
110
|
-
const idn_2 = require("./idn");
|
|
111
103
|
/**
|
|
112
104
|
* Get comprehensive list of all supported email providers
|
|
113
105
|
*
|
|
@@ -124,7 +116,7 @@ const idn_2 = require("./idn");
|
|
|
124
116
|
*/
|
|
125
117
|
function getSupportedProviders() {
|
|
126
118
|
try {
|
|
127
|
-
const { providers } = (0,
|
|
119
|
+
const { providers } = (0, loader_1.loadProviders)();
|
|
128
120
|
return [...providers]; // Return defensive copy to prevent external mutations
|
|
129
121
|
}
|
|
130
122
|
catch (error) {
|
|
@@ -150,7 +142,7 @@ function isEmailProviderSupported(email) {
|
|
|
150
142
|
if (!email || typeof email !== 'string') {
|
|
151
143
|
return false;
|
|
152
144
|
}
|
|
153
|
-
const result = (0,
|
|
145
|
+
const result = (0, api_1.getEmailProviderSync)(email);
|
|
154
146
|
return result.provider !== null;
|
|
155
147
|
}
|
|
156
148
|
catch {
|
|
@@ -227,7 +219,7 @@ function getLibraryStats() {
|
|
|
227
219
|
return {
|
|
228
220
|
providerCount: providers.length,
|
|
229
221
|
domainCount,
|
|
230
|
-
version:
|
|
222
|
+
version: exports.VERSION,
|
|
231
223
|
supportsAsync: true,
|
|
232
224
|
supportsIDN: true,
|
|
233
225
|
supportsAliasDetection: true,
|
|
@@ -238,7 +230,7 @@ function getLibraryStats() {
|
|
|
238
230
|
return {
|
|
239
231
|
providerCount: 0,
|
|
240
232
|
domainCount: 0,
|
|
241
|
-
version:
|
|
233
|
+
version: exports.VERSION,
|
|
242
234
|
supportsAsync: true,
|
|
243
235
|
supportsIDN: true,
|
|
244
236
|
supportsAliasDetection: true,
|
|
@@ -282,7 +274,7 @@ function batchProcessEmails(emails, options = {}) {
|
|
|
282
274
|
// Add normalized email if requested
|
|
283
275
|
if (normalizeEmails && validation.normalizedEmail) {
|
|
284
276
|
try {
|
|
285
|
-
result.normalized = (0,
|
|
277
|
+
result.normalized = (0, api_1.normalizeEmail)(validation.normalizedEmail);
|
|
286
278
|
}
|
|
287
279
|
catch {
|
|
288
280
|
result.normalized = validation.normalizedEmail;
|
|
@@ -300,7 +292,7 @@ function batchProcessEmails(emails, options = {}) {
|
|
|
300
292
|
// Add provider info if requested
|
|
301
293
|
if (includeProviderInfo && validation.normalizedEmail) {
|
|
302
294
|
try {
|
|
303
|
-
const providerResult = (0,
|
|
295
|
+
const providerResult = (0, api_1.getEmailProviderSync)(validation.normalizedEmail);
|
|
304
296
|
result.provider = providerResult.provider?.companyProvider || null;
|
|
305
297
|
result.loginUrl = providerResult.loginUrl;
|
|
306
298
|
}
|
|
@@ -329,8 +321,8 @@ exports.isValidEmailAddress = isValidEmail;
|
|
|
329
321
|
/**
|
|
330
322
|
* Library metadata (legacy constants)
|
|
331
323
|
*/
|
|
332
|
-
exports.PROVIDER_COUNT = 130;
|
|
333
|
-
exports.DOMAIN_COUNT = 218;
|
|
324
|
+
exports.PROVIDER_COUNT = api_1.Config?.SUPPORTED_PROVIDERS_COUNT ?? 130;
|
|
325
|
+
exports.DOMAIN_COUNT = api_1.Config?.SUPPORTED_DOMAINS_COUNT ?? 218;
|
|
334
326
|
/**
|
|
335
327
|
* Default export for convenience
|
|
336
328
|
*
|
|
@@ -343,14 +335,14 @@ exports.DOMAIN_COUNT = 218;
|
|
|
343
335
|
*/
|
|
344
336
|
exports.default = {
|
|
345
337
|
// Core functions
|
|
346
|
-
getEmailProvider:
|
|
347
|
-
getEmailProviderSync:
|
|
348
|
-
getEmailProviderFast:
|
|
338
|
+
getEmailProvider: api_1.getEmailProvider,
|
|
339
|
+
getEmailProviderSync: api_1.getEmailProviderSync,
|
|
340
|
+
getEmailProviderFast: api_1.getEmailProviderFast,
|
|
349
341
|
// Validation
|
|
350
342
|
validateEmailAddress,
|
|
351
343
|
isValidEmail,
|
|
352
|
-
normalizeEmail:
|
|
353
|
-
emailsMatch:
|
|
344
|
+
normalizeEmail: api_1.normalizeEmail,
|
|
345
|
+
emailsMatch: api_1.emailsMatch,
|
|
354
346
|
// Utilities
|
|
355
347
|
getSupportedProviders,
|
|
356
348
|
isEmailProviderSupported,
|
|
@@ -358,16 +350,25 @@ exports.default = {
|
|
|
358
350
|
getLibraryStats,
|
|
359
351
|
batchProcessEmails,
|
|
360
352
|
// Advanced
|
|
361
|
-
loadProviders:
|
|
362
|
-
detectProviderConcurrent:
|
|
363
|
-
validateInternationalEmail:
|
|
353
|
+
loadProviders: loader_1.loadProviders,
|
|
354
|
+
detectProviderConcurrent: concurrent_dns_1.detectProviderConcurrent,
|
|
355
|
+
validateInternationalEmail: idn_1.validateInternationalEmail,
|
|
364
356
|
// Constants
|
|
365
|
-
Config:
|
|
357
|
+
Config: api_1.Config,
|
|
366
358
|
PROVIDER_COUNT: exports.PROVIDER_COUNT,
|
|
367
359
|
DOMAIN_COUNT: exports.DOMAIN_COUNT
|
|
368
360
|
};
|
|
369
361
|
/**
|
|
370
362
|
* Version information
|
|
371
363
|
*/
|
|
372
|
-
|
|
364
|
+
function readPackageVersion() {
|
|
365
|
+
try {
|
|
366
|
+
const pkg = require('../package.json');
|
|
367
|
+
return pkg.version || 'unknown';
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return 'unknown';
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
exports.VERSION = readPackageVersion();
|
|
373
374
|
//# sourceMappingURL=index.js.map
|
package/dist/loader.js
CHANGED
|
@@ -10,9 +10,8 @@ exports.clearCache = clearCache;
|
|
|
10
10
|
exports.getLoadingStats = getLoadingStats;
|
|
11
11
|
exports.loadProviders = loadProviders;
|
|
12
12
|
exports.loadProvidersDebug = loadProvidersDebug;
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
13
|
const path_1 = require("path");
|
|
15
|
-
const
|
|
14
|
+
const provider_store_1 = require("./provider-store");
|
|
16
15
|
/**
|
|
17
16
|
* Internal cached data
|
|
18
17
|
*/
|
|
@@ -25,35 +24,6 @@ let loadingStats = null;
|
|
|
25
24
|
const DEFAULT_CONFIG = {
|
|
26
25
|
debug: false
|
|
27
26
|
};
|
|
28
|
-
/**
|
|
29
|
-
* Convert compressed provider to EmailProvider format
|
|
30
|
-
*/
|
|
31
|
-
function convertProviderToEmailProvider(compressedProvider) {
|
|
32
|
-
if (!compressedProvider.type) {
|
|
33
|
-
console.warn(`Missing type for provider ${compressedProvider.id}`);
|
|
34
|
-
}
|
|
35
|
-
const provider = {
|
|
36
|
-
companyProvider: compressedProvider.companyProvider,
|
|
37
|
-
loginUrl: compressedProvider.loginUrl || null,
|
|
38
|
-
domains: compressedProvider.domains || [],
|
|
39
|
-
type: compressedProvider.type,
|
|
40
|
-
alias: compressedProvider.alias
|
|
41
|
-
};
|
|
42
|
-
// Include DNS detection patterns for business email services and proxy services
|
|
43
|
-
const needsCustomDomainDetection = compressedProvider.type === 'custom_provider' ||
|
|
44
|
-
compressedProvider.type === 'proxy_service';
|
|
45
|
-
if (needsCustomDomainDetection && (compressedProvider.mx?.length || compressedProvider.txt?.length)) {
|
|
46
|
-
provider.customDomainDetection = {};
|
|
47
|
-
if (compressedProvider.mx?.length) {
|
|
48
|
-
provider.customDomainDetection.mxPatterns = compressedProvider.mx;
|
|
49
|
-
}
|
|
50
|
-
if (compressedProvider.txt?.length) {
|
|
51
|
-
// Decompress TXT patterns
|
|
52
|
-
provider.customDomainDetection.txtPatterns = compressedProvider.txt.map(schema_1.decompressTxtPattern);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return provider;
|
|
56
|
-
}
|
|
57
27
|
/**
|
|
58
28
|
* Internal provider data loader with configuration
|
|
59
29
|
*/
|
|
@@ -73,14 +43,8 @@ function loadProvidersInternal(config = {}) {
|
|
|
73
43
|
const dataPath = mergedConfig.path || (0, path_1.join)(basePath, 'emailproviders.json');
|
|
74
44
|
if (mergedConfig.debug)
|
|
75
45
|
console.log('🔄 Loading provider data...');
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
// Validate format
|
|
79
|
-
if (!data.version || !data.providers || !Array.isArray(data.providers)) {
|
|
80
|
-
throw new Error('Invalid provider data format');
|
|
81
|
-
}
|
|
82
|
-
const providers = data.providers.map(convertProviderToEmailProvider);
|
|
83
|
-
const fileSize = content.length;
|
|
46
|
+
const { data, fileSize } = (0, provider_store_1.readProvidersDataFile)(dataPath);
|
|
47
|
+
const providers = data.providers.map(provider_store_1.convertProviderToEmailProviderShared);
|
|
84
48
|
if (mergedConfig.debug) {
|
|
85
49
|
console.log(`✅ Loaded ${providers.length} providers`);
|
|
86
50
|
console.log(`📊 File size: ${(fileSize / 1024).toFixed(1)} KB`);
|
|
@@ -120,14 +84,8 @@ function buildDomainMap(providers) {
|
|
|
120
84
|
if (cachedDomainMap) {
|
|
121
85
|
return cachedDomainMap;
|
|
122
86
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
for (const domain of provider.domains) {
|
|
126
|
-
domainMap.set(domain.toLowerCase(), provider);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
cachedDomainMap = domainMap;
|
|
130
|
-
return domainMap;
|
|
87
|
+
cachedDomainMap = (0, provider_store_1.buildDomainMapShared)(providers);
|
|
88
|
+
return cachedDomainMap;
|
|
131
89
|
}
|
|
132
90
|
/**
|
|
133
91
|
* Clear all caches (useful for testing or hot reloading)
|
|
@@ -4,7 +4,17 @@
|
|
|
4
4
|
* Integrates URL validation and hash verification to load and validate
|
|
5
5
|
* email provider data with security checks.
|
|
6
6
|
*/
|
|
7
|
-
import type { EmailProvider } from './
|
|
7
|
+
import type { EmailProvider } from './api';
|
|
8
|
+
type MiddlewareRequestLike = Record<string, unknown> & {
|
|
9
|
+
secureProviders?: EmailProvider[];
|
|
10
|
+
securityReport?: LoadResult['securityReport'];
|
|
11
|
+
};
|
|
12
|
+
type MiddlewareResponseLike = {
|
|
13
|
+
status: (code: number) => {
|
|
14
|
+
json: (body: unknown) => unknown;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
type MiddlewareNextLike = () => void;
|
|
8
18
|
export interface LoadResult {
|
|
9
19
|
success: boolean;
|
|
10
20
|
providers: EmailProvider[];
|
|
@@ -43,7 +53,7 @@ interface SecurityMiddlewareOptions {
|
|
|
43
53
|
onSecurityIssue?: (report: LoadResult['securityReport']) => void;
|
|
44
54
|
getProviders?: () => LoadResult;
|
|
45
55
|
}
|
|
46
|
-
export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): (req:
|
|
56
|
+
export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): (req: MiddlewareRequestLike, res: MiddlewareResponseLike, next: MiddlewareNextLike) => void;
|
|
47
57
|
declare const _default: {
|
|
48
58
|
loadProviders: typeof loadProviders;
|
|
49
59
|
initializeSecurity: typeof initializeSecurity;
|
package/dist/provider-loader.js
CHANGED
|
@@ -10,11 +10,10 @@ exports.clearCache = clearCache;
|
|
|
10
10
|
exports.loadProviders = loadProviders;
|
|
11
11
|
exports.initializeSecurity = initializeSecurity;
|
|
12
12
|
exports.createSecurityMiddleware = createSecurityMiddleware;
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
13
|
const path_1 = require("path");
|
|
15
14
|
const url_validator_1 = require("./url-validator");
|
|
16
15
|
const hash_verifier_1 = require("./hash-verifier");
|
|
17
|
-
const
|
|
16
|
+
const provider_store_1 = require("./provider-store");
|
|
18
17
|
// Cache for load results
|
|
19
18
|
let cachedLoadResult = null;
|
|
20
19
|
/**
|
|
@@ -23,35 +22,6 @@ let cachedLoadResult = null;
|
|
|
23
22
|
function clearCache() {
|
|
24
23
|
cachedLoadResult = null;
|
|
25
24
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Convert compressed provider to EmailProvider format
|
|
28
|
-
*/
|
|
29
|
-
function convertProviderToEmailProvider(compressedProvider) {
|
|
30
|
-
if (!compressedProvider.type) {
|
|
31
|
-
console.warn(`Missing type for provider ${compressedProvider.id}`);
|
|
32
|
-
}
|
|
33
|
-
const provider = {
|
|
34
|
-
companyProvider: compressedProvider.companyProvider,
|
|
35
|
-
loginUrl: compressedProvider.loginUrl || null,
|
|
36
|
-
domains: compressedProvider.domains || [],
|
|
37
|
-
type: compressedProvider.type,
|
|
38
|
-
alias: compressedProvider.alias
|
|
39
|
-
};
|
|
40
|
-
// Include DNS detection patterns for business email services and proxy services
|
|
41
|
-
const needsCustomDomainDetection = compressedProvider.type === 'custom_provider' ||
|
|
42
|
-
compressedProvider.type === 'proxy_service';
|
|
43
|
-
if (needsCustomDomainDetection && (compressedProvider.mx?.length || compressedProvider.txt?.length)) {
|
|
44
|
-
provider.customDomainDetection = {};
|
|
45
|
-
if (compressedProvider.mx?.length) {
|
|
46
|
-
provider.customDomainDetection.mxPatterns = compressedProvider.mx;
|
|
47
|
-
}
|
|
48
|
-
if (compressedProvider.txt?.length) {
|
|
49
|
-
// Decompress TXT patterns
|
|
50
|
-
provider.customDomainDetection.txtPatterns = compressedProvider.txt.map(schema_1.decompressTxtPattern);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return provider;
|
|
54
|
-
}
|
|
55
25
|
/**
|
|
56
26
|
* Loads and validates email provider data with security checks
|
|
57
27
|
*
|
|
@@ -83,14 +53,8 @@ function loadProviders(providersPath, expectedHash) {
|
|
|
83
53
|
}
|
|
84
54
|
// Step 2: Load and parse JSON
|
|
85
55
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
// Validate format
|
|
89
|
-
if (!data.version || !data.providers || !Array.isArray(data.providers)) {
|
|
90
|
-
throw new Error('Invalid provider data format');
|
|
91
|
-
}
|
|
92
|
-
// Convert to EmailProvider format
|
|
93
|
-
providers = data.providers.map(convertProviderToEmailProvider);
|
|
56
|
+
const { data } = (0, provider_store_1.readProvidersDataFile)(filePath);
|
|
57
|
+
providers = data.providers.map(provider_store_1.convertProviderToEmailProviderShared);
|
|
94
58
|
}
|
|
95
59
|
catch (error) {
|
|
96
60
|
issues.push(`Failed to load providers file: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
@@ -178,15 +142,17 @@ function createSecurityMiddleware(options = {}) {
|
|
|
178
142
|
if (options.onSecurityIssue) {
|
|
179
143
|
options.onSecurityIssue(result.securityReport);
|
|
180
144
|
}
|
|
181
|
-
|
|
145
|
+
res.status(500).json({
|
|
182
146
|
error: 'Security validation failed',
|
|
183
147
|
details: result.securityReport
|
|
184
148
|
});
|
|
149
|
+
return;
|
|
185
150
|
}
|
|
186
151
|
// Attach secure providers to request
|
|
187
152
|
req.secureProviders = result.providers;
|
|
188
153
|
req.securityReport = result.securityReport;
|
|
189
154
|
next();
|
|
155
|
+
return;
|
|
190
156
|
};
|
|
191
157
|
}
|
|
192
158
|
exports.default = {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EmailProvider } from './api';
|
|
2
|
+
import { Provider, ProvidersData } from './schema';
|
|
3
|
+
export declare function convertProviderToEmailProviderShared(compressedProvider: Provider): EmailProvider;
|
|
4
|
+
export declare function readProvidersDataFile(filePath: string): {
|
|
5
|
+
data: ProvidersData;
|
|
6
|
+
fileSize: number;
|
|
7
|
+
};
|
|
8
|
+
export declare function buildDomainMapShared(providers: EmailProvider[]): Map<string, EmailProvider>;
|
|
9
|
+
//# sourceMappingURL=provider-store.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.convertProviderToEmailProviderShared = convertProviderToEmailProviderShared;
|
|
4
|
+
exports.readProvidersDataFile = readProvidersDataFile;
|
|
5
|
+
exports.buildDomainMapShared = buildDomainMapShared;
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const schema_1 = require("./schema");
|
|
8
|
+
function convertProviderToEmailProviderShared(compressedProvider) {
|
|
9
|
+
if (!compressedProvider.type) {
|
|
10
|
+
console.warn(`Missing type for provider ${compressedProvider.id}`);
|
|
11
|
+
}
|
|
12
|
+
const provider = {
|
|
13
|
+
companyProvider: compressedProvider.companyProvider,
|
|
14
|
+
loginUrl: compressedProvider.loginUrl || null,
|
|
15
|
+
domains: compressedProvider.domains || [],
|
|
16
|
+
type: compressedProvider.type,
|
|
17
|
+
alias: compressedProvider.alias
|
|
18
|
+
};
|
|
19
|
+
const needsCustomDomainDetection = compressedProvider.type === 'custom_provider' ||
|
|
20
|
+
compressedProvider.type === 'proxy_service';
|
|
21
|
+
if (needsCustomDomainDetection && (compressedProvider.mx?.length || compressedProvider.txt?.length)) {
|
|
22
|
+
provider.customDomainDetection = {};
|
|
23
|
+
if (compressedProvider.mx?.length) {
|
|
24
|
+
provider.customDomainDetection.mxPatterns = compressedProvider.mx;
|
|
25
|
+
}
|
|
26
|
+
if (compressedProvider.txt?.length) {
|
|
27
|
+
provider.customDomainDetection.txtPatterns = compressedProvider.txt.map(schema_1.decompressTxtPattern);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return provider;
|
|
31
|
+
}
|
|
32
|
+
function readProvidersDataFile(filePath) {
|
|
33
|
+
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
34
|
+
const data = JSON.parse(content);
|
|
35
|
+
if (!data.version || !data.providers || !Array.isArray(data.providers)) {
|
|
36
|
+
throw new Error('Invalid provider data format');
|
|
37
|
+
}
|
|
38
|
+
return { data, fileSize: content.length };
|
|
39
|
+
}
|
|
40
|
+
function buildDomainMapShared(providers) {
|
|
41
|
+
const domainMap = new Map();
|
|
42
|
+
for (const provider of providers) {
|
|
43
|
+
for (const domain of provider.domains) {
|
|
44
|
+
domainMap.set(domain.toLowerCase(), provider);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return domainMap;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=provider-store.js.map
|
package/dist/url-validator.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Provides validation and allowlisting for email provider URLs to prevent
|
|
5
5
|
* malicious redirects and supply chain attacks.
|
|
6
6
|
*/
|
|
7
|
+
type ProviderUrlLike = {
|
|
8
|
+
companyProvider?: string;
|
|
9
|
+
loginUrl?: string | null;
|
|
10
|
+
};
|
|
7
11
|
export interface URLValidationResult {
|
|
8
12
|
isValid: boolean;
|
|
9
13
|
reason?: string;
|
|
@@ -23,7 +27,7 @@ export declare function validateEmailProviderUrl(url: string): URLValidationResu
|
|
|
23
27
|
* @param providers - Array of email providers to validate
|
|
24
28
|
* @returns Array of validation results
|
|
25
29
|
*/
|
|
26
|
-
export declare function validateAllProviderUrls(providers:
|
|
30
|
+
export declare function validateAllProviderUrls(providers: ProviderUrlLike[]): Array<{
|
|
27
31
|
provider: string;
|
|
28
32
|
url: string;
|
|
29
33
|
validation: URLValidationResult;
|
|
@@ -34,7 +38,7 @@ export declare function validateAllProviderUrls(providers: any[]): Array<{
|
|
|
34
38
|
* @param providers - Array of email providers to audit
|
|
35
39
|
* @returns Security audit report
|
|
36
40
|
*/
|
|
37
|
-
export declare function auditProviderSecurity(providers:
|
|
41
|
+
export declare function auditProviderSecurity(providers: ProviderUrlLike[]): {
|
|
38
42
|
total: number;
|
|
39
43
|
valid: number;
|
|
40
44
|
invalid: number;
|
|
@@ -45,4 +49,5 @@ export declare function auditProviderSecurity(providers: any[]): {
|
|
|
45
49
|
}[];
|
|
46
50
|
report: string;
|
|
47
51
|
};
|
|
52
|
+
export {};
|
|
48
53
|
//# sourceMappingURL=url-validator.d.ts.map
|
package/dist/url-validator.js
CHANGED
|
@@ -9,19 +9,31 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.validateEmailProviderUrl = validateEmailProviderUrl;
|
|
10
10
|
exports.validateAllProviderUrls = validateAllProviderUrls;
|
|
11
11
|
exports.auditProviderSecurity = auditProviderSecurity;
|
|
12
|
-
const
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const hash_verifier_1 = require("./hash-verifier");
|
|
13
15
|
/**
|
|
14
16
|
* Get allowlisted domains from provider data
|
|
15
17
|
* Only URLs from these domains will be considered safe.
|
|
16
18
|
*/
|
|
17
19
|
function getAllowedDomains() {
|
|
18
|
-
const
|
|
20
|
+
const filePath = (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
|
|
21
|
+
const integrity = (0, hash_verifier_1.verifyProvidersIntegrity)(filePath);
|
|
22
|
+
if (!integrity.isValid) {
|
|
23
|
+
return new Set();
|
|
24
|
+
}
|
|
25
|
+
if (cachedAllowedDomains && cachedAllowlistHash === integrity.actualHash) {
|
|
26
|
+
return cachedAllowedDomains;
|
|
27
|
+
}
|
|
28
|
+
const fileContent = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
29
|
+
const data = JSON.parse(fileContent);
|
|
30
|
+
const providers = data.providers;
|
|
19
31
|
const allowedDomains = new Set();
|
|
20
32
|
for (const provider of providers) {
|
|
21
33
|
if (provider.loginUrl) {
|
|
22
34
|
try {
|
|
23
35
|
const url = new URL(provider.loginUrl);
|
|
24
|
-
allowedDomains.add(url.hostname);
|
|
36
|
+
allowedDomains.add((0, idn_1.domainToPunycode)(url.hostname.toLowerCase()));
|
|
25
37
|
}
|
|
26
38
|
catch {
|
|
27
39
|
// Skip invalid URLs
|
|
@@ -29,8 +41,12 @@ function getAllowedDomains() {
|
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
}
|
|
44
|
+
cachedAllowedDomains = allowedDomains;
|
|
45
|
+
cachedAllowlistHash = integrity.actualHash;
|
|
32
46
|
return allowedDomains;
|
|
33
47
|
}
|
|
48
|
+
let cachedAllowedDomains = null;
|
|
49
|
+
let cachedAllowlistHash = null;
|
|
34
50
|
/**
|
|
35
51
|
* Suspicious URL patterns that should always be rejected
|
|
36
52
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikkelscheike/email-provider-links",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "TypeScript library for email provider detection with 130 providers (218 domains), concurrent DNS resolution, optimized performance, 94.65% test coverage, and enterprise security for login and password reset flows",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|