@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 +10 -2
- package/dist/__tests__/basic.test.d.ts +1 -0
- package/dist/__tests__/basic.test.js +8 -0
- package/dist/concurrent-dns.d.ts +2 -0
- package/dist/concurrent-dns.js +7 -1
- package/dist/idn.d.ts +16 -0
- package/dist/idn.js +103 -0
- package/dist/security/hash-verifier.js +1 -1
- package/dist/security/secure-loader.d.ts +4 -2
- package/dist/security/secure-loader.js +3 -4
- package/dist/security/url-validator.js +2 -1
- package/package.json +3 -3
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**:
|
|
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
|
|
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
|
+
});
|
package/dist/concurrent-dns.d.ts
CHANGED
|
@@ -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>);
|
package/dist/concurrent-dns.js
CHANGED
|
@@ -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': '
|
|
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
|
-
|
|
36
|
+
interface SecurityMiddlewareOptions {
|
|
37
37
|
expectedHash?: string;
|
|
38
38
|
allowInvalidUrls?: boolean;
|
|
39
39
|
onSecurityIssue?: (report: SecureLoadResult['securityReport']) => void;
|
|
40
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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",
|