@mikkelscheike/email-provider-links 2.5.0 → 2.6.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
@@ -6,9 +6,11 @@ A TypeScript library providing direct links to **93 email providers** (178 domai
6
6
 
7
7
  ## ✨ Features
8
8
 
9
- - 🚀 **Fast & Lightweight**: Zero dependencies, minimal footprint
9
+ - 🚀 **Fast & Lightweight**: Zero dependencies, ultra-low memory (~0.39MB initial, ~0.02MB per 1000 ops)
10
10
  - 📧 **93 Email Providers**: Gmail, Outlook, Yahoo, ProtonMail, iCloud, and many more
11
11
  - 🌐 **178 Domains Supported**: Comprehensive international coverage
12
+ - 🌍 **IDN Support**: Full internationalized domain name (punycode) support with RFC compliance
13
+ - ✅ **Email Validation**: International email validation following RFC 5321, 5322, and 6530 standards
12
14
  - 🏢 **Business Domain Detection**: DNS-based detection for custom domains (Google Workspace, Microsoft 365, etc.)
13
15
  - 🔒 **Enterprise Security**: Multi-layer protection against malicious URLs and supply chain attacks
14
16
  - 🛡️ **URL Validation**: HTTPS-only enforcement with domain allowlisting
@@ -18,10 +20,11 @@ A TypeScript library providing direct links to **93 email providers** (178 domai
18
20
  - 🚦 **Rate Limiting**: Built-in DNS query rate limiting to prevent abuse
19
21
  - 🔄 **Email Alias Detection**: Normalize Gmail dots, plus addressing, and provider-specific aliases
20
22
  - 🛡️ **Fraud Prevention**: Detect duplicate accounts through email alias manipulation
21
- - 🧪 **Thoroughly Tested**: 371 tests with 91.75% code coverage
23
+ - 🧪 **Thoroughly Tested**: 370 tests with 92.89% code coverage
22
24
 
23
25
  ## Installation
24
26
 
27
+ Using npm:
25
28
  ```bash
26
29
  npm install @mikkelscheike/email-provider-links
27
30
  ```
@@ -152,6 +155,39 @@ console.log('Max requests:', Config.MAX_DNS_REQUESTS_PER_MINUTE); // 10
152
155
  console.log('Default timeout:', Config.DEFAULT_DNS_TIMEOUT); // 5000ms
153
156
  ```
154
157
 
158
+ ## Email Validation
159
+
160
+ The library includes comprehensive email validation following international standards:
161
+
162
+ ```typescript
163
+ import { validateInternationalEmail } from '@mikkelscheike/email-provider-links';
164
+
165
+ // Validate any email address
166
+ const result = validateInternationalEmail('user@example.com');
167
+
168
+ // Returns undefined for valid emails
169
+ console.log(result); // undefined
170
+
171
+ // Returns detailed error information for invalid emails
172
+ const invalid = validateInternationalEmail('user@münchen.com');
173
+ if (invalid) {
174
+ console.log(invalid.code); // IDN_VALIDATION_ERROR
175
+ console.log(invalid.message); // Human-readable error message
176
+ }
177
+ ```
178
+
179
+ ### Validation Features
180
+
181
+ - ✅ **RFC Compliance**: Follows RFC 5321, 5322, and 6530 standards
182
+ - 🌍 **International Support**: Full IDN (Punycode) validation
183
+ - 📝 **Detailed Errors**: Clear, translatable error messages
184
+ - 🔍 **Comprehensive Checks**:
185
+ - Local part validation (username)
186
+ - Domain format validation
187
+ - IDN encoding validation
188
+ - Length limits (local part, domain, total)
189
+ - TLD validation
190
+
155
191
  ## Advanced Usage
156
192
 
157
193
  <details>
@@ -254,6 +290,12 @@ interface EmailProviderResult {
254
290
  loginUrl: string | null;
255
291
  detectionMethod?: 'domain_match' | 'mx_record' | 'txt_record' | 'proxy_detected';
256
292
  proxyService?: string;
293
+ error?: {
294
+ type: 'INVALID_EMAIL' | 'DNS_TIMEOUT' | 'RATE_LIMITED' | 'UNKNOWN_DOMAIN' |
295
+ 'NETWORK_ERROR' | 'IDN_VALIDATION_ERROR';
296
+ message: string;
297
+ idnError?: string; // Specific IDN validation error message
298
+ };
257
299
  }
258
300
 
259
301
  interface ConfigConstants {
@@ -310,11 +352,29 @@ if (result.securityReport.securityLevel === 'CRITICAL') {
310
352
  }
311
353
  ```
312
354
 
355
+ ## Performance Benchmarks
356
+
357
+ This package is designed to be extremely memory efficient and fast. We continuously monitor performance metrics through automated benchmarks that run on every PR and release.
358
+
359
+ Latest benchmark results show:
360
+ - Provider loading: ~0.39MB heap usage, <0.5ms
361
+ - Email lookups: ~0.02MB heap usage per 100 operations
362
+ - Concurrent DNS: ~0.03MB heap usage, ~110ms for 10 lookups
363
+ - Large scale (1000 ops): ~0.02MB heap usage, <3ms total
364
+ - Cache effectiveness: ~0.01MB impact on subsequent loads
365
+
366
+ To run benchmarks locally:
367
+ ```bash
368
+ npm run benchmark
369
+ ```
370
+
371
+ Benchmarks are automatically run in CI to catch any performance regressions.
372
+
313
373
  ## Contributing
314
374
 
315
375
  We welcome contributions! See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for guidelines on adding new email providers.
316
376
 
317
- **Quality Assurance**: This project maintains high standards with 371 comprehensive tests achieving 91.75% code coverage.
377
+ **Quality Assurance**: This project maintains high standards with 370 comprehensive tests achieving 92.89% code coverage.
318
378
  **Security Note**: All new providers undergo security validation and must pass our allowlist verification.
319
379
 
320
380
  ## Security
package/dist/api.d.ts CHANGED
@@ -29,9 +29,10 @@ export interface EmailProviderResult {
29
29
  proxyService?: string;
30
30
  /** Error information if detection failed */
31
31
  error?: {
32
- type: 'INVALID_EMAIL' | 'DNS_TIMEOUT' | 'RATE_LIMITED' | 'UNKNOWN_DOMAIN' | 'NETWORK_ERROR';
32
+ type: 'INVALID_EMAIL' | 'DNS_TIMEOUT' | 'RATE_LIMITED' | 'UNKNOWN_DOMAIN' | 'NETWORK_ERROR' | 'IDN_VALIDATION_ERROR';
33
33
  message: string;
34
34
  retryAfter?: number;
35
+ idnError?: string;
35
36
  };
36
37
  }
37
38
  /**
@@ -68,6 +68,8 @@ export interface ConcurrentDNSResult {
68
68
  * Concurrent DNS Detection Engine
69
69
  */
70
70
  export declare class ConcurrentDNSDetector {
71
+ private activeQueries;
72
+ cleanup(): Promise<void>;
71
73
  private config;
72
74
  private providers;
73
75
  constructor(providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>);
@@ -28,7 +28,19 @@ const DEFAULT_CONFIG = {
28
28
  * Concurrent DNS Detection Engine
29
29
  */
30
30
  class ConcurrentDNSDetector {
31
+ // Cleanup method for tests
32
+ cleanup() {
33
+ // Cancel any in-progress timeouts
34
+ const timeoutError = new Error('Operation cancelled by cleanup');
35
+ for (const { reject } of this.activeQueries) {
36
+ reject(timeoutError);
37
+ }
38
+ this.activeQueries.clear();
39
+ return Promise.resolve();
40
+ }
31
41
  constructor(providers, config = {}) {
42
+ // Store active query states
43
+ this.activeQueries = new Set();
32
44
  this.config = { ...DEFAULT_CONFIG, ...config };
33
45
  this.providers = providers.filter(p => p.customDomainDetection &&
34
46
  (p.customDomainDetection.mxPatterns || p.customDomainDetection.txtPatterns));
@@ -38,7 +50,7 @@ class ConcurrentDNSDetector {
38
50
  */
39
51
  async detectProvider(domain) {
40
52
  const startTime = Date.now();
41
- const normalizedDomain = domain.toLowerCase();
53
+ const normalizedDomain = domain.toLowerCase().trim().replace(/\.+$/, '');
42
54
  // Initialize result
43
55
  const result = {
44
56
  provider: null,
@@ -378,13 +390,28 @@ class ConcurrentDNSDetector {
378
390
  * Wrap a promise with a timeout
379
391
  */
380
392
  withTimeout(promise, ms) {
381
- return new Promise((resolve, reject) => {
382
- const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms);
393
+ let rejectFn;
394
+ const timeoutPromise = new Promise((resolve, reject) => {
395
+ rejectFn = reject;
396
+ const timeout = setTimeout(() => reject(new Error(`DNS query timeout after ${ms}ms`)), ms).unref();
383
397
  promise
384
398
  .then(resolve)
385
399
  .catch(reject)
386
- .finally(() => clearTimeout(timeout));
400
+ .finally(() => {
401
+ clearTimeout(timeout);
402
+ // Clean up active query
403
+ const queryEntry = Array.from(this.activeQueries).find(entry => entry.promise === timeoutPromise);
404
+ if (queryEntry) {
405
+ this.activeQueries.delete(queryEntry);
406
+ }
407
+ });
387
408
  });
409
+ // Only add to active queries if we have a reject function
410
+ if (rejectFn) {
411
+ const queryEntry = { promise: timeoutPromise, reject: rejectFn };
412
+ this.activeQueries.add(queryEntry);
413
+ }
414
+ return timeoutPromise;
388
415
  }
389
416
  }
390
417
  exports.ConcurrentDNSDetector = ConcurrentDNSDetector;
@@ -29,7 +29,7 @@ const KNOWN_GOOD_HASHES = {
29
29
  // SHA-256 hash of the legitimate emailproviders.json
30
30
  'emailproviders.json': 'f77814bf0537019c6f38bf2744ae21640f04a2d39cb67c5116f6e03160c9486f',
31
31
  // You can add hashes for other critical files
32
- 'package.json': '6facf7082675511a8938fc7ae69f03cf9610103c410febbf514381373a0946be'
32
+ 'package.json': 'da14e15d9602b7e2cdf3d08f4756ca7ff9af842906e4de1af7664c1d1cce8e9e'
33
33
  };
34
34
  /**
35
35
  * Calculates SHA-256 hash of a file or string content
@@ -140,7 +140,7 @@ function generateSecurityHashes(basePath = __dirname) {
140
140
  const hashes = {};
141
141
  for (const file of files) {
142
142
  try {
143
- const fullPath = (0, path_1.join)(basePath, '..', '..', file);
143
+ const fullPath = (0, path_1.join)(basePath, '..', file);
144
144
  const hash = calculateFileHash(fullPath);
145
145
  hashes[file.split('/').pop() || file] = hash;
146
146
  console.log(`✅ ${file}: ${hash}`);
@@ -231,7 +231,7 @@ function handleHashMismatch(result, options = {}) {
231
231
  * @returns Complete security audit result
232
232
  */
233
233
  function performSecurityAudit(providersFilePath) {
234
- const filePath = providersFilePath || (0, path_1.join)(__dirname, '..', '..', 'providers', 'emailproviders.json');
234
+ const filePath = providersFilePath || (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
235
235
  const hashResult = verifyProvidersIntegrity(filePath);
236
236
  const recommendations = [];
237
237
  let securityLevel = 'HIGH';
package/dist/idn.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * IDN (Internationalized Domain Names) utilities
3
+ * Zero-dependency implementation for domain name handling
4
+ */
5
+ /**
6
+ * Convert domain to Punycode format
7
+ * @param domain Domain name to convert
8
+ * @returns Punycode encoded domain
9
+ */
10
+ export declare function domainToPunycode(domain: string): string;
11
+ /**
12
+ * Convert email address's domain to Punycode
13
+ * @param email Email address to convert
14
+ * @returns Email with Punycode encoded domain
15
+ */
16
+ export declare function emailToPunycode(email: string): string;
17
+ /**
18
+ * Error codes for IDN validation
19
+ * These can be used as keys for translation systems
20
+ */
21
+ export declare enum IDNValidationError {
22
+ MISSING_INPUT = "MISSING_INPUT",
23
+ EMAIL_TOO_LONG = "EMAIL_TOO_LONG",
24
+ MISSING_AT_SYMBOL = "MISSING_AT_SYMBOL",
25
+ LOCAL_PART_EMPTY = "LOCAL_PART_EMPTY",
26
+ LOCAL_PART_TOO_LONG = "LOCAL_PART_TOO_LONG",
27
+ LOCAL_PART_INVALID = "LOCAL_PART_INVALID",
28
+ DOMAIN_EMPTY = "DOMAIN_EMPTY",
29
+ DOMAIN_TOO_LONG = "DOMAIN_TOO_LONG",
30
+ DOMAIN_INVALID_FORMAT = "DOMAIN_INVALID_FORMAT",
31
+ MISSING_TLD = "MISSING_TLD",
32
+ NUMERIC_TLD = "NUMERIC_TLD",
33
+ INVALID_ENCODING = "INVALID_ENCODING"
34
+ }
35
+ /**
36
+ * Validates an email address according to international standards (IDNA)
37
+ * This implementation follows RFC 5321, 5322, and 6530 standards for email addresses
38
+ *
39
+ * @param email The email address to validate
40
+ * @returns Error information if validation fails, undefined if valid
41
+ */
42
+ export declare function validateInternationalEmail(email: string): {
43
+ type: 'IDN_VALIDATION_ERROR';
44
+ code: IDNValidationError;
45
+ message: string;
46
+ } | undefined;
package/dist/idn.js ADDED
@@ -0,0 +1,248 @@
1
+ "use strict";
2
+ /**
3
+ * IDN (Internationalized Domain Names) utilities
4
+ * Zero-dependency implementation for domain name handling
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.IDNValidationError = void 0;
8
+ exports.domainToPunycode = domainToPunycode;
9
+ exports.emailToPunycode = emailToPunycode;
10
+ exports.validateInternationalEmail = validateInternationalEmail;
11
+ const BASE = 36;
12
+ const INITIAL_N = 128;
13
+ const INITIAL_BIAS = 72;
14
+ const DAMP = 700;
15
+ const TMIN = 1;
16
+ const TMAX = 26;
17
+ const SKEW = 38;
18
+ const DELIMITER = '-';
19
+ function adaptBias(delta, numPoints, firstTime) {
20
+ delta = firstTime ? Math.floor(delta / DAMP) : Math.floor(delta / 2);
21
+ delta += Math.floor(delta / numPoints);
22
+ let k = 0;
23
+ while (delta > ((BASE - TMIN) * TMAX) / 2) {
24
+ delta = Math.floor(delta / (BASE - TMIN));
25
+ k += BASE;
26
+ }
27
+ return k + Math.floor(((BASE - TMIN + 1) * delta) / (delta + SKEW));
28
+ }
29
+ function digitToBasic(digit) {
30
+ return digit + 22 + 75 * Number(digit < 26);
31
+ }
32
+ function encode(str) {
33
+ const codePoints = Array.from(str).map(c => c.codePointAt(0));
34
+ let n = INITIAL_N;
35
+ let delta = 0;
36
+ let bias = INITIAL_BIAS;
37
+ let output = '';
38
+ // Copy ASCII chars directly
39
+ const basic = codePoints.filter(c => c < 0x80);
40
+ let h = basic.length;
41
+ let b = h;
42
+ if (b > 0) {
43
+ output = String.fromCodePoint(...basic);
44
+ }
45
+ if (b > 0) {
46
+ output += DELIMITER;
47
+ }
48
+ while (h < codePoints.length) {
49
+ let m = Number.MAX_SAFE_INTEGER;
50
+ for (const c of codePoints) {
51
+ if (c >= n && c < m)
52
+ m = c;
53
+ }
54
+ delta += (m - n) * (h + 1);
55
+ n = m;
56
+ for (const c of codePoints) {
57
+ if (c < n) {
58
+ delta++;
59
+ }
60
+ else if (c === n) {
61
+ let q = delta;
62
+ for (let k = BASE;; k += BASE) {
63
+ const t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias;
64
+ if (q < t)
65
+ break;
66
+ output += String.fromCodePoint(digitToBasic(t + (q - t) % (BASE - t)));
67
+ q = Math.floor((q - t) / (BASE - t));
68
+ }
69
+ output += String.fromCodePoint(digitToBasic(q));
70
+ bias = adaptBias(delta, h + 1, h === b);
71
+ delta = 0;
72
+ h++;
73
+ }
74
+ }
75
+ delta++;
76
+ n++;
77
+ }
78
+ return output;
79
+ }
80
+ /**
81
+ * Convert domain to Punycode format
82
+ * @param domain Domain name to convert
83
+ * @returns Punycode encoded domain
84
+ */
85
+ function domainToPunycode(domain) {
86
+ // Split domain into labels
87
+ return domain.toLowerCase().split('.').map(label => {
88
+ // Check if label needs encoding (contains non-ASCII)
89
+ if (!/[^\x00-\x7F]/.test(label)) {
90
+ return label;
91
+ }
92
+ return 'xn--' + encode(label);
93
+ }).join('.');
94
+ }
95
+ /**
96
+ * Convert email address's domain to Punycode
97
+ * @param email Email address to convert
98
+ * @returns Email with Punycode encoded domain
99
+ */
100
+ function emailToPunycode(email) {
101
+ const [local, domain] = email.split('@');
102
+ if (!domain)
103
+ return email;
104
+ return `${local}@${domainToPunycode(domain)}`;
105
+ }
106
+ /**
107
+ * Error codes for IDN validation
108
+ * These can be used as keys for translation systems
109
+ */
110
+ var IDNValidationError;
111
+ (function (IDNValidationError) {
112
+ IDNValidationError["MISSING_INPUT"] = "MISSING_INPUT";
113
+ IDNValidationError["EMAIL_TOO_LONG"] = "EMAIL_TOO_LONG";
114
+ IDNValidationError["MISSING_AT_SYMBOL"] = "MISSING_AT_SYMBOL";
115
+ IDNValidationError["LOCAL_PART_EMPTY"] = "LOCAL_PART_EMPTY";
116
+ IDNValidationError["LOCAL_PART_TOO_LONG"] = "LOCAL_PART_TOO_LONG";
117
+ IDNValidationError["LOCAL_PART_INVALID"] = "LOCAL_PART_INVALID";
118
+ IDNValidationError["DOMAIN_EMPTY"] = "DOMAIN_EMPTY";
119
+ IDNValidationError["DOMAIN_TOO_LONG"] = "DOMAIN_TOO_LONG";
120
+ IDNValidationError["DOMAIN_INVALID_FORMAT"] = "DOMAIN_INVALID_FORMAT";
121
+ IDNValidationError["MISSING_TLD"] = "MISSING_TLD";
122
+ IDNValidationError["NUMERIC_TLD"] = "NUMERIC_TLD";
123
+ IDNValidationError["INVALID_ENCODING"] = "INVALID_ENCODING";
124
+ })(IDNValidationError || (exports.IDNValidationError = IDNValidationError = {}));
125
+ /**
126
+ * Validates an email address according to international standards (IDNA)
127
+ * This implementation follows RFC 5321, 5322, and 6530 standards for email addresses
128
+ *
129
+ * @param email The email address to validate
130
+ * @returns Error information if validation fails, undefined if valid
131
+ */
132
+ function validateInternationalEmail(email) {
133
+ // Basic checks
134
+ if (!email || typeof email !== 'string') {
135
+ return {
136
+ type: 'IDN_VALIDATION_ERROR',
137
+ code: IDNValidationError.MISSING_INPUT,
138
+ message: 'The email field cannot be empty'
139
+ };
140
+ }
141
+ // Split into local and domain parts
142
+ const atIndex = email.lastIndexOf('@');
143
+ if (atIndex === -1) {
144
+ return {
145
+ type: 'IDN_VALIDATION_ERROR',
146
+ code: IDNValidationError.MISSING_AT_SYMBOL,
147
+ message: 'The email address must contain an @ symbol'
148
+ };
149
+ }
150
+ const local = email.slice(0, atIndex);
151
+ const domain = email.slice(atIndex + 1);
152
+ // Check for max length - RFC 5321
153
+ if (email.length > 254) {
154
+ return {
155
+ type: 'IDN_VALIDATION_ERROR',
156
+ code: IDNValidationError.EMAIL_TOO_LONG,
157
+ message: 'The email address is too long'
158
+ };
159
+ }
160
+ // Validate domain part
161
+ if (domain.length === 0) {
162
+ return {
163
+ type: 'IDN_VALIDATION_ERROR',
164
+ code: IDNValidationError.DOMAIN_EMPTY,
165
+ message: 'The domain part of the email cannot be empty'
166
+ };
167
+ }
168
+ if (domain.length > 255) {
169
+ return {
170
+ type: 'IDN_VALIDATION_ERROR',
171
+ code: IDNValidationError.DOMAIN_TOO_LONG,
172
+ message: 'The domain part of the email is too long'
173
+ };
174
+ }
175
+ // Validate local part
176
+ if (local.length === 0) {
177
+ return {
178
+ type: 'IDN_VALIDATION_ERROR',
179
+ code: IDNValidationError.LOCAL_PART_EMPTY,
180
+ message: 'The username part of the email cannot be empty'
181
+ };
182
+ }
183
+ if (local.length > 64) {
184
+ return {
185
+ type: 'IDN_VALIDATION_ERROR',
186
+ code: IDNValidationError.LOCAL_PART_TOO_LONG,
187
+ message: 'The username part of the email is too long'
188
+ };
189
+ }
190
+ // Check local part characters
191
+ // Allows: letters, numbers, and !#$%&'*+-/=?^_`{|}~.
192
+ // Dot can't be first, last, or consecutive
193
+ if (!/^[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~]([a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~.]*[a-zA-Z0-9!#$%&'*+\-/=?^_`{|}~])?$/.test(local) || local.includes('..')) {
194
+ return {
195
+ type: 'IDN_VALIDATION_ERROR',
196
+ code: IDNValidationError.LOCAL_PART_INVALID,
197
+ message: 'The username contains invalid characters or dots in wrong places'
198
+ };
199
+ }
200
+ // Check domain format (including IDN domains)
201
+ try {
202
+ // Check for lone surrogates and control characters
203
+ if (/[\uD800-\uDFFF]/.test(domain)) {
204
+ return {
205
+ type: 'IDN_VALIDATION_ERROR',
206
+ code: IDNValidationError.INVALID_ENCODING,
207
+ message: 'The domain contains invalid characters or encoding'
208
+ };
209
+ }
210
+ // Convert to punycode to handle IDN
211
+ const punycodeDomain = domainToPunycode(domain);
212
+ // Check basic domain format
213
+ // Allows: letters, numbers, hyphens (not first/last), dots separating labels
214
+ if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/.test(punycodeDomain)) {
215
+ return {
216
+ type: 'IDN_VALIDATION_ERROR',
217
+ code: IDNValidationError.DOMAIN_INVALID_FORMAT,
218
+ message: 'The domain format is invalid'
219
+ };
220
+ }
221
+ // Check if domain has at least one dot (TLD required)
222
+ if (!punycodeDomain.includes('.')) {
223
+ return {
224
+ type: 'IDN_VALIDATION_ERROR',
225
+ code: IDNValidationError.MISSING_TLD,
226
+ message: 'The email domain must include a top-level domain (like .com or .org)'
227
+ };
228
+ }
229
+ // Check TLD is not all-numeric
230
+ const tld = punycodeDomain.split('.').pop();
231
+ if (/^[0-9]+$/.test(tld)) {
232
+ return {
233
+ type: 'IDN_VALIDATION_ERROR',
234
+ code: IDNValidationError.NUMERIC_TLD,
235
+ message: 'The top-level domain cannot be all numbers'
236
+ };
237
+ }
238
+ }
239
+ catch (error) {
240
+ return {
241
+ type: 'IDN_VALIDATION_ERROR',
242
+ code: IDNValidationError.INVALID_ENCODING,
243
+ message: 'The domain contains invalid characters or encoding'
244
+ };
245
+ }
246
+ // If we get here, the email is valid
247
+ return undefined;
248
+ }
@@ -4,7 +4,7 @@
4
4
  * Integrates URL validation and hash verification to create a secure
5
5
  * loading system for email provider data.
6
6
  */
7
- import type { EmailProvider } from '../index';
7
+ import type { EmailProvider } from './index';
8
8
  export interface SecureLoadResult {
9
9
  success: boolean;
10
10
  providers: EmailProvider[];
@@ -33,11 +33,13 @@ export declare function initializeSecurity(): Record<string, string>;
33
33
  /**
34
34
  * Express middleware for secure provider loading (if using in web apps)
35
35
  */
36
- export declare function createSecurityMiddleware(options?: {
36
+ interface SecurityMiddlewareOptions {
37
37
  expectedHash?: string;
38
38
  allowInvalidUrls?: boolean;
39
39
  onSecurityIssue?: (report: SecureLoadResult['securityReport']) => void;
40
- }): (req: any, res: any, next: any) => any;
40
+ getProviders?: () => SecureLoadResult;
41
+ }
42
+ export declare function createSecurityMiddleware(options?: SecurityMiddlewareOptions): (req: any, res: any, next: any) => any;
41
43
  declare const _default: {
42
44
  secureLoadProviders: typeof secureLoadProviders;
43
45
  initializeSecurity: typeof initializeSecurity;
@@ -21,7 +21,7 @@ const hash_verifier_1 = require("./hash-verifier");
21
21
  * @returns Secure load result with validation details
22
22
  */
23
23
  function secureLoadProviders(providersPath, expectedHash) {
24
- const filePath = providersPath || (0, path_1.join)(__dirname, '..', '..', 'providers', 'emailproviders.json');
24
+ const filePath = providersPath || (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
25
25
  const issues = [];
26
26
  let providers = [];
27
27
  // Step 1: Hash verification
@@ -118,12 +118,11 @@ function initializeSecurity() {
118
118
  console.log('\n⚠️ Remember to update hashes when making legitimate changes to provider data!');
119
119
  return hashes;
120
120
  }
121
- /**
122
- * Express middleware for secure provider loading (if using in web apps)
123
- */
124
121
  function createSecurityMiddleware(options = {}) {
125
122
  return (req, res, next) => {
126
- const result = secureLoadProviders(undefined, options.expectedHash);
123
+ // If a custom providers getter is provided, use that instead of loading from file
124
+ const result = options.getProviders ? options.getProviders() : secureLoadProviders(undefined, options.expectedHash);
125
+ // Handle security level
127
126
  if (result.securityReport.securityLevel === 'CRITICAL' && !options.allowInvalidUrls) {
128
127
  if (options.onSecurityIssue) {
129
128
  options.onSecurityIssue(result.securityReport);
@@ -134,6 +134,7 @@ const URL_SHORTENERS = [
134
134
  'is.gd',
135
135
  'buff.ly'
136
136
  ];
137
+ const idn_1 = require("./idn");
137
138
  /**
138
139
  * Validates if a URL is safe for email provider redirects
139
140
  *
@@ -179,7 +180,7 @@ function validateEmailProviderUrl(url) {
179
180
  }
180
181
  // Parse and normalize the URL
181
182
  const urlObj = new URL(url);
182
- const domain = urlObj.hostname.toLowerCase();
183
+ const domain = (0, idn_1.domainToPunycode)(urlObj.hostname.toLowerCase());
183
184
  const normalizedUrl = urlObj.toString();
184
185
  // Must use HTTPS
185
186
  if (urlObj.protocol !== 'https:') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikkelscheike/email-provider-links",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "TypeScript library for email provider detection with 93 providers (178 domains), concurrent DNS resolution, optimized performance, 91.75% test coverage, and enterprise security for login and password reset flows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,8 +8,7 @@
8
8
  "dist/**/*.js",
9
9
  "dist/**/*.d.ts",
10
10
  "providers/emailproviders.json",
11
- "!dist/**/*.map",
12
- "!dist/**/security-demo.*"
11
+ "!dist/**/*.map"
13
12
  ],
14
13
  "scripts": {
15
14
  "build": "tsc",
@@ -24,7 +23,8 @@
24
23
  "update-hashes": "tsx scripts/update-hashes.ts",
25
24
  "release:major": "tsx scripts/prepare-release.ts",
26
25
  "release:minor": "tsx scripts/prepare-release.ts",
27
- "release:patch": "tsx scripts/prepare-release.ts"
26
+ "release:patch": "tsx scripts/prepare-release.ts",
27
+ "benchmark": "tsx --expose-gc benchmark/memory.ts"
28
28
  },
29
29
  "keywords": [
30
30
  "email",
@@ -60,10 +60,10 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "packageManager": "pnpm@10.11.1",
64
63
  "devDependencies": {
64
+ "@jest/globals": "^30.0.2",
65
65
  "@semantic-release/commit-analyzer": "^13.0.1",
66
- "@semantic-release/exec": "^6.0.3",
66
+ "@semantic-release/exec": "^7.1.0",
67
67
  "@semantic-release/git": "^10.0.1",
68
68
  "@semantic-release/github": "^11.0.3",
69
69
  "@semantic-release/npm": "^12.0.1",
@@ -71,7 +71,7 @@
71
71
  "@types/jest": "^30.0.0",
72
72
  "@types/node": "^24.0.3",
73
73
  "conventional-changelog-conventionalcommits": "^9.0.0",
74
- "jest": "^30.0.1",
74
+ "jest": "^30.0.2",
75
75
  "semantic-release": "^24.2.5",
76
76
  "ts-jest": "^29.4.0",
77
77
  "tsx": "^4.20.3",