@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 CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/%40mikkelscheike%2Femail-provider-links)](https://www.npmjs.com/package/@mikkelscheike/email-provider-links)
4
4
 
5
- > **Generate direct login links for any email address across 129+ providers (Gmail, Outlook, Yahoo, etc.) to streamline user authentication flows.**
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 **129 email providers** (217 domains) with **concurrent DNS resolution**, **optimized performance**, **comprehensive email validation**, and advanced security features for login and password reset flows.
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
- - 📧 **129 Email Providers**: Gmail, Outlook, Yahoo, ProtonMail, iCloud, and many more
30
- - 🌐 **217 Domains Supported**: Comprehensive international coverage
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: 129 providers supporting 217 domains**
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.
@@ -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
- if (!isValidEmail(email)) {
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
@@ -176,7 +176,7 @@ export declare function getEmailProviderFast(email: string, options?: {
176
176
  total: number;
177
177
  };
178
178
  confidence?: number;
179
- debug?: any;
179
+ debug?: unknown;
180
180
  }>;
181
181
  /**
182
182
  * Configuration constants
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
- // Input validation
50
- if (!email || typeof email !== 'string') {
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?.includes('Rate limit exceeded')) {
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?.includes('timeout')) {
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 || 'Unknown network error'
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
- // Input validation
193
- if (!email || typeof email !== 'string') {
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 = new Map();
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 || 'Invalid email address'
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 = new Map();
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
- // Input validation
418
- if (!email || typeof email !== 'string') {
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(email);
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 || 'DNS detection failed'
484
+ message: error instanceof Error ? error.message : 'DNS detection failed'
509
485
  }
510
486
  };
511
487
  }
@@ -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?: any[];
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?: any;
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
@@ -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).unref();
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
- const queryEntry = { promise: wrappedPromise, reject: rejectFn };
430
+ queryEntry = { promise: wrappedPromise, reject: rejectFn };
424
431
  this.activeQueries.add(queryEntry);
425
432
  }
426
433
  return wrappedPromise;
@@ -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: any, expectedHash?: string): HashVerificationResult;
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: any[]): {
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
@@ -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': 'eb87dda83960aa1c2c3bc7a6e4bb4800ff5b8a53d7cc319181662a4b02ba5287',
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 jsonString = JSON.stringify(providersData, Object.keys(providersData).sort(), 2);
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 2.7.0
15
+ * @version See package.json
16
16
  */
17
- export { getEmailProvider, getEmailProviderSync, getEmailProviderFast, normalizeEmail, emailsMatch, Config } from './api';
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 } from './loader';
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 = 130;
173
- export declare const DOMAIN_COUNT = 218;
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: number;
207
- DOMAIN_COUNT: number;
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 2.7.0
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
- // ===== PRIMARY API =====
28
- // Core functions that 95% of users need
29
- var api_1 = require("./api");
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
- // ===== ADVANCED FEATURES =====
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
- var idn_1 = require("./idn");
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, idn_2.validateInternationalEmail)(trimmedEmail);
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, loader_2.loadProviders)();
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, api_2.getEmailProviderSync)(email);
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: '2.7.0',
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: '2.7.0',
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, api_2.normalizeEmail)(validation.normalizedEmail);
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, api_2.getEmailProviderSync)(validation.normalizedEmail);
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: api_2.getEmailProvider,
347
- getEmailProviderSync: api_2.getEmailProviderSync,
348
- getEmailProviderFast: api_2.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: api_2.normalizeEmail,
353
- emailsMatch: api_2.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: loader_2.loadProviders,
362
- detectProviderConcurrent: concurrent_dns_2.detectProviderConcurrent,
363
- validateInternationalEmail: idn_2.validateInternationalEmail,
353
+ loadProviders: loader_1.loadProviders,
354
+ detectProviderConcurrent: concurrent_dns_1.detectProviderConcurrent,
355
+ validateInternationalEmail: idn_1.validateInternationalEmail,
364
356
  // Constants
365
- Config: api_2.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
- exports.VERSION = '2.7.0';
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 schema_1 = require("./schema");
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 content = (0, fs_1.readFileSync)(dataPath, 'utf8');
77
- const data = JSON.parse(content);
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
- const domainMap = new Map();
124
- for (const provider of providers) {
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 './index';
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: any, res: any, next: any) => any;
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;
@@ -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 schema_1 = require("./schema");
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 fileContent = (0, fs_1.readFileSync)(filePath, 'utf8');
87
- const data = JSON.parse(fileContent);
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
- return res.status(500).json({
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
@@ -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: any[]): Array<{
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: any[]): {
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
@@ -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 loader_1 = require("./loader");
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 { providers } = (0, loader_1.loadProviders)();
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": "4.0.11",
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",