UNPKG

@scrubbe-auth/network-info

Version:
673 lines (664 loc) 22.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); class NetworkUtils { static getConnectionTypeScore(connectionType) { const scores = { 'ethernet': 30, 'wifi': 25, '4g': 20, '3g': 10, '2g': 5, 'cellular': 15, 'bluetooth': 8, 'wimax': 12, 'unknown': 10 }; return scores[connectionType.toLowerCase()] || 10; } static getSpeedScore(speedMbps) { if (speedMbps >= 100) return 40; // 100+ Mbps if (speedMbps >= 50) return 35; // 50-100 Mbps if (speedMbps >= 25) return 30; // 25-50 Mbps if (speedMbps >= 10) return 25; // 10-25 Mbps if (speedMbps >= 5) return 20; // 5-10 Mbps if (speedMbps >= 1) return 15; // 1-5 Mbps return 5; // < 1 Mbps } static getLatencyScore(latencyMs) { if (latencyMs <= 20) return 30; // Excellent if (latencyMs <= 50) return 25; // Good if (latencyMs <= 100) return 20; // Fair if (latencyMs <= 200) return 15; // Poor return 5; // Very poor } static formatSpeed(speedMbps) { if (speedMbps >= 1000) { return `${(speedMbps / 1000).toFixed(1)} Gbps`; } return `${speedMbps.toFixed(1)} Mbps`; } static formatLatency(latencyMs) { return `${Math.round(latencyMs)} ms`; } static getQualityDescription(quality) { const descriptions = { excellent: 'Excellent connection quality for all activities', good: 'Good connection quality for most activities', fair: 'Fair connection quality, may affect streaming/gaming', poor: 'Poor connection quality, basic browsing only' }; return descriptions[quality] || 'Unknown connection quality'; } static isSlowConnection(connectionType, downloadSpeed) { // Based on connection type if (['2g', '3g', 'slow-2g'].includes(connectionType.toLowerCase())) { return true; } // Based on speed if (downloadSpeed !== undefined && downloadSpeed < 1) { return true; } return false; } static isMobileConnection(connectionType) { const mobileTypes = ['cellular', '2g', '3g', '4g', '5g', 'slow-2g']; return mobileTypes.includes(connectionType.toLowerCase()); } } class Loggers { constructor(debug = false, logLevel = 'info', outputs = [new ConsoleOutput()]) { this.debugs = debug; this.logLevel = logLevel; this.outputs = outputs; this.context = {}; } setContext(context) { this.context = { ...this.context, ...context }; } clearContext() { this.context = {}; } debug(message, ...args) { if (this.debugs && this.shouldLog('debug')) { this.log('debug', message, args); } } info(message, ...args) { if (this.shouldLog('info')) { this.log('info', message, args); } } warn(message, ...args) { if (this.shouldLog('warn')) { this.log('warn', message, args); } } error(message, ...args) { if (this.shouldLog('error')) { this.log('error', message, args); } } shouldLog(level) { const levels = { debug: 0, info: 1, warn: 2, error: 3 }; return levels[level] >= levels[this.logLevel]; } log(level, message, args) { const logEntry = { timestamp: new Date().toISOString(), level, message, args, context: this.context }; this.outputs.forEach(output => { try { output.write(logEntry); } catch (error) { console.error('Failed to write log:', error); } }); } // Create child logger with additional context child(context) { const child = new Loggers(this.debugs, this.logLevel, this.outputs); child.setContext({ ...this.context, ...context }); return child; } // Add output addOutput(output) { this.outputs.push(output); } // Remove output removeOutput(output) { const index = this.outputs.indexOf(output); if (index > -1) { this.outputs.splice(index, 1); } } } class ConsoleOutput { write(entry) { const prefix = `[${entry.timestamp}] [${entry.level.toUpperCase()}] [Scrubbe Analytics]`; const contextStr = Object.keys(entry.context).length > 0 ? ` ${JSON.stringify(entry.context)}` : ''; const fullMessage = `${prefix}${contextStr} ${entry.message}`; switch (entry.level) { case 'debug': console.debug(fullMessage, ...entry.args); break; case 'info': console.info(fullMessage, ...entry.args); break; case 'warn': console.warn(fullMessage, ...entry.args); break; case 'error': console.error(fullMessage, ...entry.args); break; } } } class ConnectionCollector { collect() { const info = { connectionType: 'unknown', onLine: this.isOnline() }; if (typeof navigator === 'undefined') { return info; } // Get connection info from Network Information API const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (connection) { info.connectionType = connection.type || connection.effectiveType || 'unknown'; info.effectiveType = connection.effectiveType; info.downlink = connection.downlink; info.rtt = connection.rtt; info.saveData = connection.saveData; } else { // Fallback detection based on user agent and other indicators info.connectionType = this.detectConnectionTypeFallback(); } return info; } isOnline() { if (typeof navigator !== 'undefined') { return navigator.onLine; } return true; } detectConnectionTypeFallback() { if (typeof navigator === 'undefined') { return 'unknown'; } const ua = navigator.userAgent.toLowerCase(); // Mobile detection if (/mobile|android|iphone|ipad|phone/i.test(ua)) { return 'cellular'; } // Default to ethernet for desktop return 'ethernet'; } } class SpeedTestCollector { constructor(config) { this.config = config; } async runSpeedTest() { const startTime = Date.now(); // Run download speed test const downloadSpeed = await this.measureDownloadSpeed(); // Measure latency const latency = await this.measureLatency(); // Optionally measure upload speed (more resource intensive) let uploadSpeed; if (this.config.duration > 3000) { uploadSpeed = await this.measureUploadSpeed(); } return { downloadSpeed, uploadSpeed, latency, timestamp: Date.now(), testDuration: Date.now() - startTime }; } async measureDownloadSpeed() { const testUrl = this.config.testUrl || this.getSpeedTestUrl(); const iterations = 3; const results = []; for (let i = 0; i < iterations; i++) { try { const result = await this.singleDownloadTest(testUrl); results.push(result); } catch (error) { console.warn(`Download test ${i + 1} failed:`, error); } } if (results.length === 0) { throw new Error('All download tests failed'); } // Return median result to avoid outliers results.sort((a, b) => a - b); return results[Math.floor(results.length / 2)]; } async singleDownloadTest(url) { const startTime = performance.now(); const response = await fetch(url, { method: 'GET', cache: 'no-cache', signal: AbortSignal.timeout(this.config.timeout) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const buffer = await response.arrayBuffer(); const endTime = performance.now(); const duration = (endTime - startTime) / 1000; // seconds const bytes = buffer.byteLength; const bitsPerSecond = (bytes * 8) / duration; const mbps = bitsPerSecond / (1024 * 1024); return mbps; } async measureUploadSpeed() { try { // Create test payload (1MB) const testData = new ArrayBuffer(1024 * 1024); const startTime = performance.now(); const response = await fetch('/api/speed-test/upload', { method: 'POST', body: testData, signal: AbortSignal.timeout(this.config.timeout) }); const endTime = performance.now(); if (!response.ok) { throw new Error(`Upload test failed: ${response.status}`); } const duration = (endTime - startTime) / 1000; const bitsPerSecond = (testData.byteLength * 8) / duration; return bitsPerSecond / (1024 * 1024); } catch (error) { console.warn('Upload speed test failed:', error); return 0; } } async measureLatency() { const iterations = 5; const results = []; for (let i = 0; i < iterations; i++) { try { const start = performance.now(); await fetch(this.getPingUrl(), { method: 'HEAD', mode: 'no-cors', cache: 'no-cache' }); const end = performance.now(); results.push(end - start); } catch { // Ignore failed pings } } if (results.length === 0) { return -1; } // Return average latency return results.reduce((a, b) => a + b, 0) / results.length; } getSpeedTestUrl() { // Use different sized test files based on expected connection speed const sizes = ['1MB', '5MB', '10MB']; const selectedSize = sizes[0]; // Start with smallest return `https://speed.cloudflare.com/__down?bytes=${this.getMBInBytes(selectedSize)}`; } getPingUrl() { return 'https://www.google.com/favicon.ico'; } getMBInBytes(size) { const mb = parseInt(size.replace('MB', '')); return mb * 1024 * 1024; } } class IPInfoCollector { constructor(timeout = 5000) { this.timeout = timeout; } async collect() { const services = [ () => this.collectFromIPAPI(), () => this.collectFromIPGeolocation(), () => this.collectFromIPInfo() ]; for (const service of services) { try { const result = await service(); if (result) { return result; } } catch (error) { console.warn('IP info service failed:', error); } } return null; } async collectFromIPAPI() { try { const response = await fetch('https://ipapi.co/json/', { signal: AbortSignal.timeout(this.timeout) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); return { ip: data.ip, isp: data.org, organization: data.org, asn: data.asn, country: data.country_name, region: data.region, city: data.city, // Basic VPN/proxy detection based on known indicators vpn: this.isVPN(data.org), proxy: this.isProxy(data.org), hosting: this.isHosting(data.org) }; } catch (error) { throw new Error(`ipapi.co failed: ${error}`); } } async collectFromIPGeolocation() { try { const response = await fetch('https://api.ipgeolocation.io/ipgeo', { signal: AbortSignal.timeout(this.timeout) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); return { ip: data.ip, isp: data.isp, organization: data.organization, asn: data.asn, country: data.country_name, region: data.state_prov, city: data.city, vpn: this.isVPN(data.isp), proxy: this.isProxy(data.isp), hosting: this.isHosting(data.isp) }; } catch (error) { throw new Error(`ipgeolocation.io failed: ${error}`); } } async collectFromIPInfo() { try { const response = await fetch('https://ipinfo.io/json', { signal: AbortSignal.timeout(this.timeout) }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); return { ip: data.ip, isp: data.org, organization: data.org, country: data.country, region: data.region, city: data.city, vpn: this.isVPN(data.org), proxy: this.isProxy(data.org), hosting: this.isHosting(data.org) }; } catch (error) { throw new Error(`ipinfo.io failed: ${error}`); } } isVPN(org) { if (!org) return false; const vpnIndicators = [ 'vpn', 'proxy', 'tor', 'mullvad', 'nordvpn', 'expressvpn', 'protonvpn', 'surfshark', 'cyberghost', 'privatevpn' ]; return vpnIndicators.some(indicator => org.toLowerCase().includes(indicator)); } isProxy(org) { if (!org) return false; const proxyIndicators = [ 'proxy', 'cloudflare', 'fastly', 'akamai', 'maxcdn' ]; return proxyIndicators.some(indicator => org.toLowerCase().includes(indicator)); } isHosting(org) { if (!org) return false; const hostingIndicators = [ 'amazon', 'aws', 'google cloud', 'microsoft azure', 'digitalocean', 'linode', 'vultr', 'hetzner', 'ovh' ]; return hostingIndicators.some(indicator => org.toLowerCase().includes(indicator)); } } class NetworkInfo { constructor(config = {}) { this.config = { enableSpeedTest: config.enableSpeedTest ?? false, enableConnectionType: config.enableConnectionType ?? true, enableISP: config.enableISP ?? false, enableVPNDetection: config.enableVPNDetection ?? false, enableQualityMetrics: config.enableQualityMetrics ?? true, speedTestDuration: config.speedTestDuration || 5000, speedTestUrl: config.speedTestUrl, cacheTimeout: config.cacheTimeout || 300000, // 5 minutes debug: config.debug ?? false, timeout: config.timeout || 10000, ...config }; this.logger = new Loggers(this.config.debug); this.connectionCollector = new ConnectionCollector(); this.speedTestCollector = new SpeedTestCollector({ duration: this.config.speedTestDuration, testUrl: this.config.speedTestUrl, timeout: this.config.timeout }); this.ipInfoCollector = new IPInfoCollector(this.config.timeout); } async getNetworkInfo() { // Check cache if (this.isCacheValid()) { this.logger.debug('Returning cached network info'); return this.cachedInfo; } try { const networkInfo = await this.collectNetworkInfo(); // Cache the result this.cachedInfo = networkInfo; this.cacheTimestamp = Date.now(); this.logger.debug('Network info collected successfully', networkInfo); return networkInfo; } catch (error) { this.logger.error('Failed to collect network info:', error); return this.getBasicNetworkInfo(); } } async collectNetworkInfo() { const networkInfo = { timestamp: Date.now(), connectionType: 'unknown' }; // Collect connection type and basic info if (this.config.enableConnectionType) { const connectionInfo = this.connectionCollector.collect(); Object.assign(networkInfo, connectionInfo); } // Collect IP and ISP info if (this.config.enableISP || this.config.enableVPNDetection) { try { const ipInfo = await this.ipInfoCollector.collect(); if (ipInfo) { networkInfo.ip = ipInfo.ip; networkInfo.isp = ipInfo.isp; networkInfo.organization = ipInfo.organization; networkInfo.asn = ipInfo.asn; networkInfo.vpn = ipInfo.vpn; networkInfo.proxy = ipInfo.proxy; networkInfo.hosting = ipInfo.hosting; } } catch (error) { this.logger.warn('Failed to collect IP info:', error); } } // Perform speed test if (this.config.enableSpeedTest) { try { const speedResult = await this.speedTestCollector.runSpeedTest(); networkInfo.downloadSpeed = speedResult.downloadSpeed; networkInfo.uploadSpeed = speedResult.uploadSpeed; networkInfo.latency = speedResult.latency; networkInfo.jitter = speedResult.jitter; } catch (error) { this.logger.warn('Speed test failed:', error); } } // Calculate quality metrics if (this.config.enableQualityMetrics) { networkInfo.quality = this.calculateNetworkQuality(networkInfo); } return networkInfo; } getBasicNetworkInfo() { return { timestamp: Date.now(), connectionType: this.connectionCollector.collect().connectionType || 'unknown' }; } calculateNetworkQuality(info) { let score = 0; let maxScore = 0; // Connection type scoring maxScore += 30; const connectionScore = NetworkUtils.getConnectionTypeScore(info.connectionType); score += connectionScore; // Speed scoring (if available) if (info.downloadSpeed !== undefined) { maxScore += 40; const speedScore = NetworkUtils.getSpeedScore(info.downloadSpeed); score += speedScore; } // Latency scoring (if available) if (info.latency !== undefined) { maxScore += 30; const latencyScore = NetworkUtils.getLatencyScore(info.latency); score += latencyScore; } const percentage = maxScore > 0 ? Math.round((score / maxScore) * 100) : 0; let quality; if (percentage >= 80) quality = 'excellent'; else if (percentage >= 60) quality = 'good'; else if (percentage >= 40) quality = 'fair'; else quality = 'poor'; return quality; } isCacheValid() { if (!this.cachedInfo || !this.cacheTimestamp) { return false; } return Date.now() - this.cacheTimestamp < this.config.cacheTimeout; } // Specific test methods async runSpeedTest() { if (!this.config.enableSpeedTest) { this.logger.warn('Speed test is disabled'); return null; } try { return await this.speedTestCollector.runSpeedTest(); } catch (error) { this.logger.error('Speed test failed:', error); return null; } } async getConnectionInfo() { return this.connectionCollector.collect(); } async getIPInfo() { return await this.ipInfoCollector.collect(); } // Utility methods clearCache() { this.cachedInfo = undefined; this.cacheTimestamp = undefined; this.logger.debug('Network info cache cleared'); } updateConfig(config) { Object.assign(this.config, config); this.logger.debug('Configuration updated'); } getConfig() { return { ...this.config }; } // Static utility methods static isOnline() { if (typeof navigator !== 'undefined') { return navigator.onLine; } return true; // Assume online if navigator not available } static getConnectionType() { return new ConnectionCollector().collect().connectionType || 'unknown'; } static async measureLatency(url = 'https://www.google.com/favicon.ico') { try { const start = performance.now(); await fetch(url, { method: 'HEAD', mode: 'no-cors' }); const end = performance.now(); return Math.round(end - start); } catch { return -1; } } } exports.NetworkInfo = NetworkInfo; exports.NetworkUtils = NetworkUtils; exports.default = NetworkInfo; //# sourceMappingURL=index.js.map