@mikkelscheike/email-provider-links
Version:
TypeScript library for email provider detection with 93 providers (207 domains), concurrent DNS resolution, optimized performance, 94.65% test coverage, and enterprise security for login and password reset flows
198 lines ⢠8 kB
JavaScript
;
/**
* Email Provider Loader
*
* Integrates URL validation and hash verification to load and validate
* email provider data with security checks.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.clearCache = clearCache;
exports.loadProviders = loadProviders;
exports.initializeSecurity = initializeSecurity;
exports.createSecurityMiddleware = createSecurityMiddleware;
const fs_1 = require("fs");
const path_1 = require("path");
const url_validator_1 = require("./url-validator");
const hash_verifier_1 = require("./hash-verifier");
const schema_1 = require("./schema");
// Cache for load results
let cachedLoadResult = null;
/**
* Clear the cache (useful for testing or when providers file changes)
*/
function clearCache() {
cachedLoadResult = null;
}
/**
* Convert compressed provider to EmailProvider format
*/
function convertProviderToEmailProvider(compressedProvider) {
if (!compressedProvider.type) {
console.warn(`Missing type for provider ${compressedProvider.id}`);
}
const provider = {
companyProvider: compressedProvider.companyProvider,
loginUrl: compressedProvider.loginUrl || null,
domains: compressedProvider.domains || [],
type: compressedProvider.type,
alias: compressedProvider.alias
};
// Include DNS detection patterns for business email services and proxy services
const needsCustomDomainDetection = compressedProvider.type === 'custom_provider' ||
compressedProvider.type === 'proxy_service';
if (needsCustomDomainDetection && (compressedProvider.mx?.length || compressedProvider.txt?.length)) {
provider.customDomainDetection = {};
if (compressedProvider.mx?.length) {
provider.customDomainDetection.mxPatterns = compressedProvider.mx;
}
if (compressedProvider.txt?.length) {
// Decompress TXT patterns
provider.customDomainDetection.txtPatterns = compressedProvider.txt.map(schema_1.decompressTxtPattern);
}
}
return provider;
}
/**
* Loads and validates email provider data with security checks
*
* @param providersPath - Path to the providers JSON file
* @param expectedHash - Optional expected hash for verification
* @returns Load result with validation details
*/
function loadProviders(providersPath, expectedHash) {
// Return cached result if available (both success and failure)
if (cachedLoadResult) {
return cachedLoadResult;
}
const filePath = providersPath || (0, path_1.join)(__dirname, '..', 'providers', 'emailproviders.json');
const issues = [];
let providers = [];
// Step 1: Hash verification
const hashResult = (0, hash_verifier_1.verifyProvidersIntegrity)(filePath, expectedHash);
if (!hashResult.isValid) {
issues.push(`Hash verification failed: ${hashResult.reason}`);
// In production, you might want to abort here
// Suppress logging during tests to avoid console noise
if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) {
console.error('đ¨ SECURITY WARNING: Hash verification failed!');
console.error('File:', hashResult.file);
console.error('Reason:', hashResult.reason);
console.error('Expected:', hashResult.expectedHash);
console.error('Actual:', hashResult.actualHash);
}
}
// Step 2: Load and parse JSON
try {
const fileContent = (0, fs_1.readFileSync)(filePath, 'utf8');
const data = JSON.parse(fileContent);
// Validate format
if (!data.version || !data.providers || !Array.isArray(data.providers)) {
throw new Error('Invalid provider data format');
}
// Convert to EmailProvider format
providers = data.providers.map(convertProviderToEmailProvider);
}
catch (error) {
issues.push(`Failed to load providers file: ${error instanceof Error ? error.message : 'Unknown error'}`);
return {
success: false,
providers: [],
securityReport: {
hashVerification: false,
urlValidation: false,
totalProviders: 0,
validUrls: 0,
invalidUrls: 0,
securityLevel: 'CRITICAL',
issues
}
};
}
// Step 3: URL validation audit
const urlAudit = (0, url_validator_1.auditProviderSecurity)(providers);
if (urlAudit.invalid > 0) {
issues.push(`${urlAudit.invalid} providers have invalid URLs`);
// Suppress logging during tests to avoid console noise
if (process.env.NODE_ENV !== 'test' && !process.env.JEST_WORKER_ID) {
console.warn('â ď¸ URL validation issues found:');
for (const invalid of urlAudit.invalidProviders) {
console.warn(`- ${invalid.provider}: ${invalid.validation.reason}`);
}
}
}
// Step 4: Filter out invalid providers in production
const secureProviders = providers.filter(provider => {
if (!provider.loginUrl)
return true; // Allow providers without login URLs
const validation = (0, url_validator_1.validateEmailProviderUrl)(provider.loginUrl);
return validation.isValid;
});
if (secureProviders.length < providers.length) {
const filtered = providers.length - secureProviders.length;
issues.push(`Filtered out ${filtered} providers with invalid URLs`);
}
// Step 5: Determine security level
let securityLevel = 'SECURE';
if (!hashResult.isValid) {
securityLevel = 'CRITICAL';
}
else if (urlAudit.invalid > 0 || issues.length > 0) {
securityLevel = 'WARNING';
}
const loadResult = {
success: securityLevel !== 'CRITICAL',
providers: secureProviders,
securityReport: {
hashVerification: hashResult.isValid,
urlValidation: urlAudit.invalid === 0,
totalProviders: providers.length,
validUrls: urlAudit.valid,
invalidUrls: urlAudit.invalid,
securityLevel,
issues
}
};
// Cache the result for future calls
cachedLoadResult = loadResult;
return loadResult;
}
/**
* Development utility to generate and display current hashes
*/
function initializeSecurity() {
console.log('đ Generating security hashes for email providers...');
const hashes = (0, hash_verifier_1.generateSecurityHashes)();
console.log('\nđ Security Setup Instructions:');
console.log('1. Store these hashes securely (environment variables, CI/CD secrets)');
console.log('2. Update KNOWN_GOOD_HASHES in hash-verifier.ts');
console.log('3. Enable hash verification in production');
console.log('\nâ ď¸ Remember to update hashes when making legitimate changes to provider data!');
return hashes;
}
function createSecurityMiddleware(options = {}) {
return (req, res, next) => {
// If a custom providers getter is provided, use that instead of loading from file
const result = options.getProviders ? options.getProviders() : loadProviders(undefined, options.expectedHash);
// Handle security level
if (result.securityReport.securityLevel === 'CRITICAL' && !options.allowInvalidUrls) {
if (options.onSecurityIssue) {
options.onSecurityIssue(result.securityReport);
}
return res.status(500).json({
error: 'Security validation failed',
details: result.securityReport
});
}
// Attach secure providers to request
req.secureProviders = result.providers;
req.securityReport = result.securityReport;
next();
};
}
exports.default = {
loadProviders,
initializeSecurity,
createSecurityMiddleware,
clearCache
};
//# sourceMappingURL=provider-loader.js.map