@mikkelscheike/email-provider-links 2.4.0 โ†’ 2.5.1

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
@@ -9,6 +9,7 @@ A TypeScript library providing direct links to **93 email providers** (178 domai
9
9
  - ๐Ÿš€ **Fast & Lightweight**: Zero dependencies, minimal footprint
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
12
13
  - ๐Ÿข **Business Domain Detection**: DNS-based detection for custom domains (Google Workspace, Microsoft 365, etc.)
13
14
  - ๐Ÿ”’ **Enterprise Security**: Multi-layer protection against malicious URLs and supply chain attacks
14
15
  - ๐Ÿ›ก๏ธ **URL Validation**: HTTPS-only enforcement with domain allowlisting
@@ -18,14 +19,35 @@ A TypeScript library providing direct links to **93 email providers** (178 domai
18
19
  - ๐Ÿšฆ **Rate Limiting**: Built-in DNS query rate limiting to prevent abuse
19
20
  - ๐Ÿ”„ **Email Alias Detection**: Normalize Gmail dots, plus addressing, and provider-specific aliases
20
21
  - ๐Ÿ›ก๏ธ **Fraud Prevention**: Detect duplicate accounts through email alias manipulation
21
- - ๐Ÿงช **Thoroughly Tested**: 371 tests with 91.75% code coverage
22
+ - ๐Ÿงช **Thoroughly Tested**: 370 tests with 92.89% code coverage
22
23
 
23
24
  ## Installation
24
25
 
26
+ Using npm:
25
27
  ```bash
26
28
  npm install @mikkelscheike/email-provider-links
27
29
  ```
28
30
 
31
+ Using pnpm:
32
+ ```bash
33
+ pnpm add @mikkelscheike/email-provider-links
34
+ ```
35
+
36
+ ## Requirements
37
+
38
+ - **Node.js**: `>=18.0.0` (Tested on 18.x, 20.x, 22.x, **24.x**)
39
+ - **Package Managers**: npm and pnpm (Tested on pnpm 18.x through 24.x)
40
+ - **TypeScript**: `>=4.0.0` (optional)
41
+ - **Zero runtime dependencies** - No external packages required
42
+
43
+ ### Node.js 24 Support โœจ
44
+
45
+ Fully compatible with the latest Node.js 24.x! The library is tested on:
46
+ - Node.js 18.x (LTS)
47
+ - Node.js 20.x (LTS)
48
+ - Node.js 22.x (Current)
49
+ - **Node.js 24.x (Latest)** - Full support with latest features
50
+
29
51
  ## Quick Start
30
52
 
31
53
  **One function handles everything** - consumer emails, business domains, and unknown providers:
@@ -84,8 +106,23 @@ console.log(unknown.loginUrl); // null
84
106
  **Recommended** - Detects any email provider including business domains.
85
107
 
86
108
  ```typescript
87
- const result = await getEmailProvider('user@gmail.com', 3000);
88
- // Returns: { provider, loginUrl, detectionMethod, email }
109
+ // ๐Ÿš€ SAME CALL, DIFFERENT SCENARIOS:
110
+
111
+ // โœ… For known providers (Gmail, Yahoo, etc.) - INSTANT response
112
+ const gmail1 = await getEmailProvider('user@gmail.com'); // No timeout needed
113
+ const gmail2 = await getEmailProvider('user@gmail.com', 3000); // Same speed - timeout ignored
114
+ // Both return instantly: { provider: "Gmail", loginUrl: "https://mail.google.com/mail/" }
115
+
116
+ // ๐Ÿ” For business domains - DNS lookup required, timeout matters
117
+ const biz1 = await getEmailProvider('user@mycompany.com'); // 5000ms timeout (default)
118
+ const biz2 = await getEmailProvider('user@mycompany.com', 2000); // 2000ms timeout (faster fail)
119
+ const biz3 = await getEmailProvider('user@mycompany.com', 10000); // 10000ms timeout (slower networks)
120
+ // All may detect: { provider: "Google Workspace", detectionMethod: "mx_record" }
121
+
122
+ // ๐ŸŽฏ WHY USE CUSTOM TIMEOUT?
123
+ // - Faster apps: Use 2000ms to fail fast on unknown domains
124
+ // - Slower networks: Use 10000ms to avoid premature timeouts
125
+ // - Enterprise: Use 1000ms for strict SLA requirements
89
126
  ```
90
127
 
91
128
  ### `getEmailProviderSync(email)`
@@ -285,7 +322,7 @@ if (result.securityReport.securityLevel === 'CRITICAL') {
285
322
 
286
323
  We welcome contributions! See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for guidelines on adding new email providers.
287
324
 
288
- **Quality Assurance**: This project maintains high standards with 371 comprehensive tests achieving 91.75% code coverage.
325
+ **Quality Assurance**: This project maintains high standards with 370 comprehensive tests achieving 92.89% code coverage.
289
326
  **Security Note**: All new providers undergo security validation and must pass our allowlist verification.
290
327
 
291
328
  ## Security
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const globals_1 = require("@jest/globals");
4
+ (0, globals_1.describe)('Basic Test', () => {
5
+ (0, globals_1.test)('true should be true', () => {
6
+ (0, globals_1.expect)(true).toBe(true);
7
+ });
8
+ });
@@ -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(): void;
71
73
  private config;
72
74
  private providers;
73
75
  constructor(providers: EmailProvider[], config?: Partial<ConcurrentDNSConfig>);
@@ -28,7 +28,13 @@ const DEFAULT_CONFIG = {
28
28
  * Concurrent DNS Detection Engine
29
29
  */
30
30
  class ConcurrentDNSDetector {
31
+ // Cleanup method for tests
32
+ cleanup() {
33
+ this.activeQueries.clear();
34
+ }
31
35
  constructor(providers, config = {}) {
36
+ // Store active query states
37
+ this.activeQueries = new Set();
32
38
  this.config = { ...DEFAULT_CONFIG, ...config };
33
39
  this.providers = providers.filter(p => p.customDomainDetection &&
34
40
  (p.customDomainDetection.mxPatterns || p.customDomainDetection.txtPatterns));
@@ -38,7 +44,7 @@ class ConcurrentDNSDetector {
38
44
  */
39
45
  async detectProvider(domain) {
40
46
  const startTime = Date.now();
41
- const normalizedDomain = domain.toLowerCase();
47
+ const normalizedDomain = domain.toLowerCase().trim().replace(/\.+$/, '');
42
48
  // Initialize result
43
49
  const result = {
44
50
  provider: null,
package/dist/idn.d.ts ADDED
@@ -0,0 +1,16 @@
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;
package/dist/idn.js ADDED
@@ -0,0 +1,103 @@
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.domainToPunycode = domainToPunycode;
8
+ exports.emailToPunycode = emailToPunycode;
9
+ const BASE = 36;
10
+ const INITIAL_N = 128;
11
+ const INITIAL_BIAS = 72;
12
+ const DAMP = 700;
13
+ const TMIN = 1;
14
+ const TMAX = 26;
15
+ const SKEW = 38;
16
+ const DELIMITER = '-';
17
+ function adaptBias(delta, numPoints, firstTime) {
18
+ delta = firstTime ? Math.floor(delta / DAMP) : Math.floor(delta / 2);
19
+ delta += Math.floor(delta / numPoints);
20
+ let k = 0;
21
+ while (delta > ((BASE - TMIN) * TMAX) / 2) {
22
+ delta = Math.floor(delta / (BASE - TMIN));
23
+ k += BASE;
24
+ }
25
+ return k + Math.floor(((BASE - TMIN + 1) * delta) / (delta + SKEW));
26
+ }
27
+ function digitToBasic(digit) {
28
+ return digit + 22 + 75 * Number(digit < 26);
29
+ }
30
+ function encode(str) {
31
+ const codePoints = Array.from(str).map(c => c.codePointAt(0));
32
+ let n = INITIAL_N;
33
+ let delta = 0;
34
+ let bias = INITIAL_BIAS;
35
+ let output = '';
36
+ // Copy ASCII chars directly
37
+ const basic = codePoints.filter(c => c < 0x80);
38
+ let h = basic.length;
39
+ let b = h;
40
+ if (b > 0) {
41
+ output = String.fromCodePoint(...basic);
42
+ }
43
+ if (b > 0) {
44
+ output += DELIMITER;
45
+ }
46
+ while (h < codePoints.length) {
47
+ let m = Number.MAX_SAFE_INTEGER;
48
+ for (const c of codePoints) {
49
+ if (c >= n && c < m)
50
+ m = c;
51
+ }
52
+ delta += (m - n) * (h + 1);
53
+ n = m;
54
+ for (const c of codePoints) {
55
+ if (c < n) {
56
+ delta++;
57
+ }
58
+ else if (c === n) {
59
+ let q = delta;
60
+ for (let k = BASE;; k += BASE) {
61
+ const t = k <= bias ? TMIN : k >= bias + TMAX ? TMAX : k - bias;
62
+ if (q < t)
63
+ break;
64
+ output += String.fromCodePoint(digitToBasic(t + (q - t) % (BASE - t)));
65
+ q = Math.floor((q - t) / (BASE - t));
66
+ }
67
+ output += String.fromCodePoint(digitToBasic(q));
68
+ bias = adaptBias(delta, h + 1, h === b);
69
+ delta = 0;
70
+ h++;
71
+ }
72
+ }
73
+ delta++;
74
+ n++;
75
+ }
76
+ return output;
77
+ }
78
+ /**
79
+ * Convert domain to Punycode format
80
+ * @param domain Domain name to convert
81
+ * @returns Punycode encoded domain
82
+ */
83
+ function domainToPunycode(domain) {
84
+ // Split domain into labels
85
+ return domain.toLowerCase().split('.').map(label => {
86
+ // Check if label needs encoding (contains non-ASCII)
87
+ if (!/[^\x00-\x7F]/.test(label)) {
88
+ return label;
89
+ }
90
+ return 'xn--' + encode(label);
91
+ }).join('.');
92
+ }
93
+ /**
94
+ * Convert email address's domain to Punycode
95
+ * @param email Email address to convert
96
+ * @returns Email with Punycode encoded domain
97
+ */
98
+ function emailToPunycode(email) {
99
+ const [local, domain] = email.split('@');
100
+ if (!domain)
101
+ return email;
102
+ return `${local}@${domainToPunycode(domain)}`;
103
+ }
@@ -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': 'b9ef67d46c55b947ea366d49d66dadcf37e729cef7a319fa710a5d251fcac60c'
32
+ 'package.json': '92cc46fb47a1466bf3f448cd2f289d51e65daf7e7a93b389e956a49b6ffc4bbe'
33
33
  };
34
34
  /**
35
35
  * Calculates SHA-256 hash of a file or string content
@@ -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;
@@ -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.4.0",
3
+ "version": "2.5.1",
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",
@@ -53,10 +53,13 @@
53
53
  "bugs": {
54
54
  "url": "https://github.com/mikkelscheike/email-provider-links/issues"
55
55
  },
56
+ "engines": {
57
+ "node": ">=18.0.0",
58
+ "comment": "Supports Node.js 18.x, 20.x, 22.x, and 24.x"
59
+ },
56
60
  "publishConfig": {
57
61
  "access": "public"
58
62
  },
59
- "packageManager": "pnpm@10.11.1",
60
63
  "devDependencies": {
61
64
  "@semantic-release/commit-analyzer": "^13.0.1",
62
65
  "@semantic-release/exec": "^6.0.3",
@@ -64,10 +67,11 @@
64
67
  "@semantic-release/github": "^11.0.3",
65
68
  "@semantic-release/npm": "^12.0.1",
66
69
  "@semantic-release/release-notes-generator": "^14.0.3",
70
+ "@jest/globals": "^30.0.2",
67
71
  "@types/jest": "^30.0.0",
68
72
  "@types/node": "^24.0.3",
69
73
  "conventional-changelog-conventionalcommits": "^9.0.0",
70
- "jest": "^30.0.1",
74
+ "jest": "^30.0.2",
71
75
  "semantic-release": "^24.2.5",
72
76
  "ts-jest": "^29.4.0",
73
77
  "tsx": "^4.20.3",