@mikkelscheike/email-provider-links 2.5.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,17 +19,24 @@ 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
+
29
36
  ## Requirements
30
37
 
31
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)
32
40
  - **TypeScript**: `>=4.0.0` (optional)
33
41
  - **Zero runtime dependencies** - No external packages required
34
42
 
@@ -314,7 +322,7 @@ if (result.securityReport.securityLevel === 'CRITICAL') {
314
322
 
315
323
  We welcome contributions! See [CONTRIBUTING.md](docs/CONTRIBUTING.md) for guidelines on adding new email providers.
316
324
 
317
- **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.
318
326
  **Security Note**: All new providers undergo security validation and must pass our allowlist verification.
319
327
 
320
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': '6facf7082675511a8938fc7ae69f03cf9610103c410febbf514381373a0946be'
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.5.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",
@@ -60,7 +60,6 @@
60
60
  "publishConfig": {
61
61
  "access": "public"
62
62
  },
63
- "packageManager": "pnpm@10.11.1",
64
63
  "devDependencies": {
65
64
  "@semantic-release/commit-analyzer": "^13.0.1",
66
65
  "@semantic-release/exec": "^6.0.3",
@@ -68,10 +67,11 @@
68
67
  "@semantic-release/github": "^11.0.3",
69
68
  "@semantic-release/npm": "^12.0.1",
70
69
  "@semantic-release/release-notes-generator": "^14.0.3",
70
+ "@jest/globals": "^30.0.2",
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",