@mikkelscheike/email-provider-links 4.0.11 → 5.1.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 +62 -20
- package/dist/alias-detection.js +102 -8
- package/dist/api.d.ts +2 -41
- package/dist/api.js +147 -212
- package/dist/concurrent-dns.d.ts +7 -2
- package/dist/concurrent-dns.js +10 -3
- package/dist/constants.d.ts +26 -0
- package/dist/constants.js +29 -0
- package/dist/error-utils.d.ts +56 -0
- package/dist/error-utils.js +89 -0
- package/dist/hash-verifier.d.ts +7 -2
- package/dist/hash-verifier.js +12 -3
- package/dist/idn.js +10 -1
- package/dist/index.d.ts +14 -17
- package/dist/index.js +45 -34
- package/dist/provider-loader.d.ts +61 -2
- package/dist/provider-loader.js +134 -58
- package/dist/provider-store.d.ts +9 -0
- package/dist/provider-store.js +49 -0
- package/dist/url-validator.d.ts +12 -2
- package/dist/url-validator.js +32 -3
- package/package.json +3 -2
- package/dist/loader.d.ts +0 -45
- package/dist/loader.js +0 -163
package/dist/provider-loader.js
CHANGED
|
@@ -10,47 +10,29 @@ exports.clearCache = clearCache;
|
|
|
10
10
|
exports.loadProviders = loadProviders;
|
|
11
11
|
exports.initializeSecurity = initializeSecurity;
|
|
12
12
|
exports.createSecurityMiddleware = createSecurityMiddleware;
|
|
13
|
-
|
|
13
|
+
exports.buildDomainMap = buildDomainMap;
|
|
14
|
+
exports.getLoadingStats = getLoadingStats;
|
|
15
|
+
exports.loadProvidersDebug = loadProvidersDebug;
|
|
14
16
|
const path_1 = require("path");
|
|
17
|
+
const fs_1 = require("fs");
|
|
15
18
|
const url_validator_1 = require("./url-validator");
|
|
16
19
|
const hash_verifier_1 = require("./hash-verifier");
|
|
17
|
-
const
|
|
20
|
+
const error_utils_1 = require("./error-utils");
|
|
21
|
+
const constants_1 = require("./constants");
|
|
22
|
+
const provider_store_1 = require("./provider-store");
|
|
18
23
|
// Cache for load results
|
|
19
24
|
let cachedLoadResult = null;
|
|
25
|
+
// Cache for loading statistics
|
|
26
|
+
let loadingStats = null;
|
|
27
|
+
// Cache for domain maps
|
|
28
|
+
let cachedDomainMap = null;
|
|
20
29
|
/**
|
|
21
30
|
* Clear the cache (useful for testing or when providers file changes)
|
|
22
31
|
*/
|
|
23
32
|
function clearCache() {
|
|
24
33
|
cachedLoadResult = null;
|
|
25
|
-
|
|
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;
|
|
34
|
+
loadingStats = null;
|
|
35
|
+
cachedDomainMap = null;
|
|
54
36
|
}
|
|
55
37
|
/**
|
|
56
38
|
* Loads and validates email provider data with security checks
|
|
@@ -83,39 +65,62 @@ function loadProviders(providersPath, expectedHash) {
|
|
|
83
65
|
}
|
|
84
66
|
// Step 2: Load and parse JSON
|
|
85
67
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
//
|
|
89
|
-
if (
|
|
90
|
-
|
|
68
|
+
const { data } = (0, provider_store_1.readProvidersDataFile)(filePath);
|
|
69
|
+
providers = data.providers.map(provider_store_1.convertProviderToEmailProviderShared);
|
|
70
|
+
// Log memory usage in development mode
|
|
71
|
+
if (process.env.NODE_ENV === 'development' && !process.env.JEST_WORKER_ID) {
|
|
72
|
+
const memUsage = process.memoryUsage();
|
|
73
|
+
const memUsageMB = (memUsage.heapUsed / constants_1.MemoryConstants.BYTES_PER_KB / constants_1.MemoryConstants.KB_PER_MB).toFixed(2);
|
|
74
|
+
console.log(`🚀 Current memory usage: ${memUsageMB} MB`);
|
|
91
75
|
}
|
|
92
|
-
// Convert to EmailProvider format
|
|
93
|
-
providers = data.providers.map(convertProviderToEmailProvider);
|
|
94
76
|
}
|
|
95
77
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
// Use standardized error handling utilities
|
|
79
|
+
const errorMessage = (0, error_utils_1.getErrorMessage)(error);
|
|
80
|
+
const fileNotFound = (0, error_utils_1.isFileNotFoundError)(error);
|
|
81
|
+
const jsonError = (0, error_utils_1.isJsonError)(error);
|
|
82
|
+
// Return error result for JSON parse errors and file not found (ENOENT)
|
|
83
|
+
// This allows security tests to check error handling
|
|
84
|
+
// Note: ENOENT errors are already handled by hash verification, but we still need to handle
|
|
85
|
+
// them here in case hash verification passed but file was deleted between verification and read
|
|
86
|
+
if (jsonError || fileNotFound) {
|
|
87
|
+
if (!jsonError) {
|
|
88
|
+
// For file not found, don't add duplicate issue if hash verification already failed
|
|
89
|
+
if (hashResult.isValid) {
|
|
90
|
+
issues.push(`Failed to load providers file: ${errorMessage}`);
|
|
91
|
+
}
|
|
108
92
|
}
|
|
109
|
-
|
|
93
|
+
else {
|
|
94
|
+
issues.push(`Failed to load providers file: ${errorMessage}`);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
providers: [],
|
|
99
|
+
securityReport: {
|
|
100
|
+
hashVerification: hashResult.isValid,
|
|
101
|
+
urlValidation: false,
|
|
102
|
+
totalProviders: 0,
|
|
103
|
+
validUrls: 0,
|
|
104
|
+
invalidUrls: 0,
|
|
105
|
+
securityLevel: 'CRITICAL',
|
|
106
|
+
issues
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
// For other errors, add to issues and throw (to match loader.test.ts expectations)
|
|
111
|
+
issues.push(`Failed to load providers file: ${errorMessage}`);
|
|
112
|
+
throw new Error(`Failed to load provider data: ${errorMessage}`);
|
|
110
113
|
}
|
|
111
114
|
// Step 3: URL validation audit
|
|
112
115
|
const urlAudit = (0, url_validator_1.auditProviderSecurity)(providers);
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
// Count only providers with invalid URLs (not providers without URLs)
|
|
117
|
+
const providersWithInvalidUrls = urlAudit.invalidProviders.filter(invalid => invalid.url !== '' && invalid.url !== undefined && invalid.url !== null);
|
|
118
|
+
if (providersWithInvalidUrls.length > 0) {
|
|
119
|
+
issues.push(`${providersWithInvalidUrls.length} providers have invalid URLs`);
|
|
115
120
|
// Suppress logging during tests to avoid console noise
|
|
116
121
|
if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) {
|
|
117
122
|
console.warn('⚠️ URL validation issues found:');
|
|
118
|
-
for (const invalid of
|
|
123
|
+
for (const invalid of providersWithInvalidUrls) {
|
|
119
124
|
console.warn(`- ${invalid.provider}: ${invalid.validation.reason}`);
|
|
120
125
|
}
|
|
121
126
|
}
|
|
@@ -132,28 +137,39 @@ function loadProviders(providersPath, expectedHash) {
|
|
|
132
137
|
issues.push(`Filtered out ${filtered} providers with invalid URLs`);
|
|
133
138
|
}
|
|
134
139
|
// Step 5: Determine security level
|
|
140
|
+
// Only providers with invalid URLs affect security level, not providers without URLs
|
|
135
141
|
let securityLevel = 'SECURE';
|
|
136
142
|
if (!hashResult.isValid) {
|
|
137
143
|
securityLevel = 'CRITICAL';
|
|
138
144
|
}
|
|
139
|
-
else if (
|
|
145
|
+
else if (providersWithInvalidUrls.length > 0 || issues.length > 0) {
|
|
140
146
|
securityLevel = 'WARNING';
|
|
141
147
|
}
|
|
142
148
|
const loadResult = {
|
|
143
149
|
success: securityLevel !== 'CRITICAL',
|
|
144
150
|
providers: secureProviders,
|
|
151
|
+
domainMap: buildDomainMap(secureProviders),
|
|
152
|
+
stats: {
|
|
153
|
+
loadTime: 0, // Would need to track this during load
|
|
154
|
+
domainMapTime: 0,
|
|
155
|
+
providerCount: secureProviders.length,
|
|
156
|
+
domainCount: secureProviders.reduce((count, p) => count + (p.domains?.length || 0), 0),
|
|
157
|
+
fileSize: (0, fs_1.readFileSync)(filePath, 'utf8').length // Calculate actual file size in bytes
|
|
158
|
+
},
|
|
145
159
|
securityReport: {
|
|
146
160
|
hashVerification: hashResult.isValid,
|
|
147
|
-
urlValidation:
|
|
161
|
+
urlValidation: providersWithInvalidUrls.length === 0, // Only count providers with invalid URLs, not providers without URLs
|
|
148
162
|
totalProviders: providers.length,
|
|
149
163
|
validUrls: urlAudit.valid,
|
|
150
|
-
invalidUrls:
|
|
164
|
+
invalidUrls: providersWithInvalidUrls.length, // Only count actual invalid URLs
|
|
151
165
|
securityLevel,
|
|
152
166
|
issues
|
|
153
167
|
}
|
|
154
168
|
};
|
|
155
169
|
// Cache the result for future calls
|
|
156
170
|
cachedLoadResult = loadResult;
|
|
171
|
+
// Update loading stats for getLoadingStats()
|
|
172
|
+
loadingStats = loadResult.stats;
|
|
157
173
|
return loadResult;
|
|
158
174
|
}
|
|
159
175
|
/**
|
|
@@ -178,19 +194,79 @@ function createSecurityMiddleware(options = {}) {
|
|
|
178
194
|
if (options.onSecurityIssue) {
|
|
179
195
|
options.onSecurityIssue(result.securityReport);
|
|
180
196
|
}
|
|
181
|
-
|
|
197
|
+
res.status(500).json({
|
|
182
198
|
error: 'Security validation failed',
|
|
183
199
|
details: result.securityReport
|
|
184
200
|
});
|
|
201
|
+
return;
|
|
185
202
|
}
|
|
186
203
|
// Attach secure providers to request
|
|
187
204
|
req.secureProviders = result.providers;
|
|
188
205
|
req.securityReport = result.securityReport;
|
|
189
206
|
next();
|
|
207
|
+
return;
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Build domain map from providers
|
|
212
|
+
*/
|
|
213
|
+
function buildDomainMap(providers) {
|
|
214
|
+
// Return cached domain map if available
|
|
215
|
+
if (cachedDomainMap) {
|
|
216
|
+
return cachedDomainMap;
|
|
217
|
+
}
|
|
218
|
+
// Build and cache the domain map
|
|
219
|
+
cachedDomainMap = (0, provider_store_1.buildDomainMapShared)(providers);
|
|
220
|
+
return cachedDomainMap;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get loading statistics from the last load operation
|
|
224
|
+
*/
|
|
225
|
+
function getLoadingStats() {
|
|
226
|
+
return loadingStats;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Load providers with debug information (always reloads cache)
|
|
230
|
+
*/
|
|
231
|
+
function loadProvidersDebug() {
|
|
232
|
+
const startTime = process.hrtime.bigint();
|
|
233
|
+
// Clear cache for debug mode - ensure we always reload
|
|
234
|
+
cachedLoadResult = null;
|
|
235
|
+
loadingStats = null;
|
|
236
|
+
const result = loadProviders();
|
|
237
|
+
const endTime = process.hrtime.bigint();
|
|
238
|
+
// Build domain map and calculate stats
|
|
239
|
+
const domainMapStart = process.hrtime.bigint();
|
|
240
|
+
const domainMap = buildDomainMap(result.providers);
|
|
241
|
+
const domainMapEnd = process.hrtime.bigint();
|
|
242
|
+
// Store loading stats
|
|
243
|
+
loadingStats = {
|
|
244
|
+
loadTime: Number(endTime - startTime) / 1000000, // Convert to milliseconds
|
|
245
|
+
domainMapTime: Number(domainMapEnd - domainMapStart) / 1000000,
|
|
246
|
+
providerCount: result.providers.length,
|
|
247
|
+
domainCount: domainMap.size,
|
|
248
|
+
fileSize: 0 // Would need to track this during load
|
|
249
|
+
};
|
|
250
|
+
// Debug output
|
|
251
|
+
console.log('=== Provider Loading Debug ===');
|
|
252
|
+
console.log(`Providers loaded: ${result.providers.length}`);
|
|
253
|
+
console.log(`Security level: ${result.securityReport.securityLevel}`);
|
|
254
|
+
console.log(`Load time: ${loadingStats.loadTime.toFixed(2)}ms`);
|
|
255
|
+
console.log(`Domain map time: ${loadingStats.domainMapTime.toFixed(2)}ms`);
|
|
256
|
+
console.log(`Total domains: ${loadingStats.domainCount}`);
|
|
257
|
+
console.log('=============================');
|
|
258
|
+
// Return enhanced result with debug info - ensure new objects each time
|
|
259
|
+
return {
|
|
260
|
+
...result,
|
|
261
|
+
domainMap: new Map(domainMap), // Create new Map instance
|
|
262
|
+
stats: { ...loadingStats } // Create new stats object
|
|
190
263
|
};
|
|
191
264
|
}
|
|
192
265
|
exports.default = {
|
|
193
266
|
loadProviders,
|
|
267
|
+
loadProvidersDebug,
|
|
268
|
+
buildDomainMap,
|
|
269
|
+
getLoadingStats,
|
|
194
270
|
initializeSecurity,
|
|
195
271
|
createSecurityMiddleware,
|
|
196
272
|
clearCache
|
|
@@ -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
|
package/dist/url-validator.d.ts
CHANGED
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
* Provides validation and allowlisting for email provider URLs to prevent
|
|
5
5
|
* malicious redirects and supply chain attacks.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* Get allowlisted domains from provider data
|
|
9
|
+
* Only URLs from these domains will be considered safe.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAllowedDomains(): Set<string>;
|
|
12
|
+
type ProviderUrlLike = {
|
|
13
|
+
companyProvider?: string;
|
|
14
|
+
loginUrl?: string | null;
|
|
15
|
+
};
|
|
7
16
|
export interface URLValidationResult {
|
|
8
17
|
isValid: boolean;
|
|
9
18
|
reason?: string;
|
|
@@ -23,7 +32,7 @@ export declare function validateEmailProviderUrl(url: string): URLValidationResu
|
|
|
23
32
|
* @param providers - Array of email providers to validate
|
|
24
33
|
* @returns Array of validation results
|
|
25
34
|
*/
|
|
26
|
-
export declare function validateAllProviderUrls(providers:
|
|
35
|
+
export declare function validateAllProviderUrls(providers: ProviderUrlLike[]): Array<{
|
|
27
36
|
provider: string;
|
|
28
37
|
url: string;
|
|
29
38
|
validation: URLValidationResult;
|
|
@@ -34,7 +43,7 @@ export declare function validateAllProviderUrls(providers: any[]): Array<{
|
|
|
34
43
|
* @param providers - Array of email providers to audit
|
|
35
44
|
* @returns Security audit report
|
|
36
45
|
*/
|
|
37
|
-
export declare function auditProviderSecurity(providers:
|
|
46
|
+
export declare function auditProviderSecurity(providers: ProviderUrlLike[]): {
|
|
38
47
|
total: number;
|
|
39
48
|
valid: number;
|
|
40
49
|
invalid: number;
|
|
@@ -45,4 +54,5 @@ export declare function auditProviderSecurity(providers: any[]): {
|
|
|
45
54
|
}[];
|
|
46
55
|
report: string;
|
|
47
56
|
};
|
|
57
|
+
export {};
|
|
48
58
|
//# sourceMappingURL=url-validator.d.ts.map
|
package/dist/url-validator.js
CHANGED
|
@@ -6,22 +6,35 @@
|
|
|
6
6
|
* malicious redirects and supply chain attacks.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.getAllowedDomains = getAllowedDomains;
|
|
9
10
|
exports.validateEmailProviderUrl = validateEmailProviderUrl;
|
|
10
11
|
exports.validateAllProviderUrls = validateAllProviderUrls;
|
|
11
12
|
exports.auditProviderSecurity = auditProviderSecurity;
|
|
12
|
-
const
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const hash_verifier_1 = require("./hash-verifier");
|
|
13
16
|
/**
|
|
14
17
|
* Get allowlisted domains from provider data
|
|
15
18
|
* Only URLs from these domains will be considered safe.
|
|
16
19
|
*/
|
|
17
20
|
function getAllowedDomains() {
|
|
18
|
-
const
|
|
21
|
+
const filePath = (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
|
|
22
|
+
const integrity = (0, hash_verifier_1.verifyProvidersIntegrity)(filePath);
|
|
23
|
+
if (!integrity.isValid) {
|
|
24
|
+
return new Set();
|
|
25
|
+
}
|
|
26
|
+
if (cachedAllowedDomains && cachedAllowlistHash === integrity.actualHash) {
|
|
27
|
+
return cachedAllowedDomains;
|
|
28
|
+
}
|
|
29
|
+
const fileContent = (0, fs_1.readFileSync)(filePath, 'utf8');
|
|
30
|
+
const data = JSON.parse(fileContent);
|
|
31
|
+
const providers = data.providers;
|
|
19
32
|
const allowedDomains = new Set();
|
|
20
33
|
for (const provider of providers) {
|
|
21
34
|
if (provider.loginUrl) {
|
|
22
35
|
try {
|
|
23
36
|
const url = new URL(provider.loginUrl);
|
|
24
|
-
allowedDomains.add(url.hostname);
|
|
37
|
+
allowedDomains.add((0, idn_1.domainToPunycode)(url.hostname.toLowerCase()));
|
|
25
38
|
}
|
|
26
39
|
catch {
|
|
27
40
|
// Skip invalid URLs
|
|
@@ -29,8 +42,12 @@ function getAllowedDomains() {
|
|
|
29
42
|
}
|
|
30
43
|
}
|
|
31
44
|
}
|
|
45
|
+
cachedAllowedDomains = allowedDomains;
|
|
46
|
+
cachedAllowlistHash = integrity.actualHash;
|
|
32
47
|
return allowedDomains;
|
|
33
48
|
}
|
|
49
|
+
let cachedAllowedDomains = null;
|
|
50
|
+
let cachedAllowlistHash = null;
|
|
34
51
|
/**
|
|
35
52
|
* Suspicious URL patterns that should always be rejected
|
|
36
53
|
*/
|
|
@@ -188,6 +205,18 @@ function validateAllProviderUrls(providers) {
|
|
|
188
205
|
validation: validateEmailProviderUrl(provider.loginUrl)
|
|
189
206
|
});
|
|
190
207
|
}
|
|
208
|
+
else {
|
|
209
|
+
// Providers without URLs are counted but marked as invalid for audit purposes
|
|
210
|
+
// (they don't affect security level, but are tracked for completeness)
|
|
211
|
+
results.push({
|
|
212
|
+
provider: provider.companyProvider || 'Unknown',
|
|
213
|
+
url: provider.loginUrl || '',
|
|
214
|
+
validation: {
|
|
215
|
+
isValid: false,
|
|
216
|
+
reason: provider.loginUrl === '' ? 'Empty URL provided' : 'No URL provided'
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
191
220
|
}
|
|
192
221
|
return results;
|
|
193
222
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mikkelscheike/email-provider-links",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.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",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"test": "jest",
|
|
18
18
|
"test:watch": "jest --watch",
|
|
19
19
|
"test:coverage": "jest --coverage",
|
|
20
|
+
"test:live-dns": "RUN_LIVE_DNS=1 jest",
|
|
20
21
|
"prepublishOnly": "npm run verify-hashes && npm run build",
|
|
21
22
|
"dev": "tsx src/index.ts",
|
|
22
23
|
"sync-versions": "node scripts/sync-versions.js",
|
|
@@ -74,7 +75,7 @@
|
|
|
74
75
|
"@semantic-release/npm": "^13.1.2",
|
|
75
76
|
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
76
77
|
"@types/jest": "^30.0.0",
|
|
77
|
-
"@types/node": "^
|
|
78
|
+
"@types/node": "^25.0.3",
|
|
78
79
|
"conventional-changelog-conventionalcommits": "^9.1.0",
|
|
79
80
|
"jest": "^30.2.0",
|
|
80
81
|
"semantic-release": "^25.0.2",
|
package/dist/loader.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider Data Loader
|
|
3
|
-
*
|
|
4
|
-
* Handles loading email provider data with performance optimizations.
|
|
5
|
-
*/
|
|
6
|
-
import { EmailProvider } from './api';
|
|
7
|
-
/**
|
|
8
|
-
* Loading statistics for performance monitoring
|
|
9
|
-
*/
|
|
10
|
-
interface LoadingStats {
|
|
11
|
-
fileSize: number;
|
|
12
|
-
loadTime: number;
|
|
13
|
-
providerCount: number;
|
|
14
|
-
domainCount: number;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Build optimized domain-to-provider lookup map
|
|
18
|
-
*/
|
|
19
|
-
export declare function buildDomainMap(providers: EmailProvider[]): Map<string, EmailProvider>;
|
|
20
|
-
/**
|
|
21
|
-
* Clear all caches (useful for testing or hot reloading)
|
|
22
|
-
*/
|
|
23
|
-
export declare function clearCache(): void;
|
|
24
|
-
/**
|
|
25
|
-
* Get loading statistics from the last load operation
|
|
26
|
-
*/
|
|
27
|
-
export declare function getLoadingStats(): LoadingStats | null;
|
|
28
|
-
/**
|
|
29
|
-
* Load all providers with optimized domain map for production
|
|
30
|
-
*/
|
|
31
|
-
export declare function loadProviders(): {
|
|
32
|
-
providers: EmailProvider[];
|
|
33
|
-
domainMap: Map<string, EmailProvider>;
|
|
34
|
-
stats: LoadingStats;
|
|
35
|
-
};
|
|
36
|
-
/**
|
|
37
|
-
* Load providers with debug information
|
|
38
|
-
*/
|
|
39
|
-
export declare function loadProvidersDebug(): {
|
|
40
|
-
providers: EmailProvider[];
|
|
41
|
-
domainMap: Map<string, EmailProvider>;
|
|
42
|
-
stats: LoadingStats;
|
|
43
|
-
};
|
|
44
|
-
export {};
|
|
45
|
-
//# sourceMappingURL=loader.d.ts.map
|
package/dist/loader.js
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Provider Data Loader
|
|
4
|
-
*
|
|
5
|
-
* Handles loading email provider data with performance optimizations.
|
|
6
|
-
*/
|
|
7
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.buildDomainMap = buildDomainMap;
|
|
9
|
-
exports.clearCache = clearCache;
|
|
10
|
-
exports.getLoadingStats = getLoadingStats;
|
|
11
|
-
exports.loadProviders = loadProviders;
|
|
12
|
-
exports.loadProvidersDebug = loadProvidersDebug;
|
|
13
|
-
const fs_1 = require("fs");
|
|
14
|
-
const path_1 = require("path");
|
|
15
|
-
const schema_1 = require("./schema");
|
|
16
|
-
/**
|
|
17
|
-
* Internal cached data
|
|
18
|
-
*/
|
|
19
|
-
let cachedProviders = null;
|
|
20
|
-
let cachedDomainMap = null;
|
|
21
|
-
let loadingStats = null;
|
|
22
|
-
/**
|
|
23
|
-
* Default loader configuration
|
|
24
|
-
*/
|
|
25
|
-
const DEFAULT_CONFIG = {
|
|
26
|
-
debug: false
|
|
27
|
-
};
|
|
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
|
-
/**
|
|
58
|
-
* Internal provider data loader with configuration
|
|
59
|
-
*/
|
|
60
|
-
function loadProvidersInternal(config = {}) {
|
|
61
|
-
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
62
|
-
const startTime = Date.now();
|
|
63
|
-
// Return cached data if available
|
|
64
|
-
if (cachedProviders && !mergedConfig.debug) {
|
|
65
|
-
return {
|
|
66
|
-
providers: cachedProviders,
|
|
67
|
-
stats: loadingStats
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
try {
|
|
71
|
-
// Determine file path
|
|
72
|
-
const basePath = (0, path_1.join)(__dirname, '..', 'providers');
|
|
73
|
-
const dataPath = mergedConfig.path || (0, path_1.join)(basePath, 'emailproviders.json');
|
|
74
|
-
if (mergedConfig.debug)
|
|
75
|
-
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;
|
|
84
|
-
if (mergedConfig.debug) {
|
|
85
|
-
console.log(`✅ Loaded ${providers.length} providers`);
|
|
86
|
-
console.log(`📊 File size: ${(fileSize / 1024).toFixed(1)} KB`);
|
|
87
|
-
}
|
|
88
|
-
const loadTime = Date.now() - startTime;
|
|
89
|
-
const domainCount = providers.reduce((sum, p) => sum + p.domains.length, 0);
|
|
90
|
-
// Cache the results
|
|
91
|
-
cachedProviders = providers;
|
|
92
|
-
loadingStats = {
|
|
93
|
-
fileSize,
|
|
94
|
-
loadTime,
|
|
95
|
-
providerCount: providers.length,
|
|
96
|
-
domainCount
|
|
97
|
-
};
|
|
98
|
-
if (mergedConfig.debug) {
|
|
99
|
-
console.log(`⚡ Loading completed in ${loadTime}ms`);
|
|
100
|
-
console.log(`📊 Stats: ${providers.length} providers, ${domainCount} domains`);
|
|
101
|
-
}
|
|
102
|
-
if (process.env.NODE_ENV === 'development') {
|
|
103
|
-
const memoryUsageInMB = process.memoryUsage().heapUsed / 1024 / 1024;
|
|
104
|
-
console.log(`🚀 Current memory usage: ${memoryUsageInMB.toFixed(2)} MB`);
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
providers,
|
|
108
|
-
stats: loadingStats
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
catch (error) {
|
|
112
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
113
|
-
throw new Error(`Failed to load provider data: ${errorMessage}`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Build optimized domain-to-provider lookup map
|
|
118
|
-
*/
|
|
119
|
-
function buildDomainMap(providers) {
|
|
120
|
-
if (cachedDomainMap) {
|
|
121
|
-
return cachedDomainMap;
|
|
122
|
-
}
|
|
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;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Clear all caches (useful for testing or hot reloading)
|
|
134
|
-
*/
|
|
135
|
-
function clearCache() {
|
|
136
|
-
cachedProviders = null;
|
|
137
|
-
cachedDomainMap = null;
|
|
138
|
-
loadingStats = null;
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Get loading statistics from the last load operation
|
|
142
|
-
*/
|
|
143
|
-
function getLoadingStats() {
|
|
144
|
-
return loadingStats;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Load all providers with optimized domain map for production
|
|
148
|
-
*/
|
|
149
|
-
function loadProviders() {
|
|
150
|
-
const { providers, stats } = loadProvidersInternal({ debug: false });
|
|
151
|
-
const domainMap = buildDomainMap(providers);
|
|
152
|
-
return { providers, domainMap, stats };
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Load providers with debug information
|
|
156
|
-
*/
|
|
157
|
-
function loadProvidersDebug() {
|
|
158
|
-
clearCache(); // Always reload in debug mode
|
|
159
|
-
const { providers, stats } = loadProvidersInternal({ debug: true });
|
|
160
|
-
const domainMap = buildDomainMap(providers);
|
|
161
|
-
return { providers, domainMap, stats };
|
|
162
|
-
}
|
|
163
|
-
//# sourceMappingURL=loader.js.map
|