diff --git a/index.html b/index.html index 048e6bc..ae82140 100644 --- a/index.html +++ b/index.html @@ -49,6 +49,13 @@ + \ No newline at end of file diff --git a/src/components/ui/Header.jsx b/src/components/ui/Header.jsx index 8a43277..bf3b759 100644 --- a/src/components/ui/Header.jsx +++ b/src/components/ui/Header.jsx @@ -1,5 +1,3 @@ -const React = window.React; - const EnhancedMinimalHeader = ({ status, fingerprint, @@ -10,14 +8,80 @@ const EnhancedMinimalHeader = ({ sessionManager, sessionTimeLeft }) => { + const [currentTimeLeft, setCurrentTimeLeft] = React.useState(sessionTimeLeft || 0); + const [hasActiveSession, setHasActiveSession] = React.useState(false); + const [sessionType, setSessionType] = React.useState('unknown'); + + React.useEffect(() => { + const updateSessionInfo = () => { + if (sessionManager) { + const isActive = sessionManager.hasActiveSession(); + const timeLeft = sessionManager.getTimeLeft(); + const currentSession = sessionManager.currentSession; + + setHasActiveSession(isActive); + setCurrentTimeLeft(timeLeft); + setSessionType(currentSession?.type || 'unknown'); + + } + }; + + updateSessionInfo(); + + const interval = setInterval(updateSessionInfo, 1000); + + return () => clearInterval(interval); + }, [sessionManager]); + + React.useEffect(() => { + if (sessionManager?.hasActiveSession()) { + setCurrentTimeLeft(sessionManager.getTimeLeft()); + setHasActiveSession(true); + } else { + setHasActiveSession(false); + } + }, [sessionManager, sessionTimeLeft]); + + const handleSecurityClick = () => { + if (securityLevel?.verificationResults) { + alert('Security check details:\n\n' + + Object.entries(securityLevel.verificationResults) + .map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`) + .join('\n') + ); + } else if (securityLevel) { + alert(`Security Level: ${securityLevel.level}\nScore: ${securityLevel.score}%\nDetails: ${securityLevel.details || 'No additional details available'}`); + } + }; + + const shouldShowTimer = hasActiveSession && currentTimeLeft > 0 && window.SessionTimer; + + React.useEffect(() => { + const handleForceUpdate = (event) => { + + if (sessionManager) { + const isActive = sessionManager.hasActiveSession(); + const timeLeft = sessionManager.getTimeLeft(); + const currentSession = sessionManager.currentSession; + + setHasActiveSession(isActive); + setCurrentTimeLeft(timeLeft); + setSessionType(currentSession?.type || 'unknown'); + } + }; + + document.addEventListener('force-header-update', handleForceUpdate); + return () => document.removeEventListener('force-header-update', handleForceUpdate); + }, [sessionManager]); + const getStatusConfig = () => { switch (status) { case 'connected': return { - text: 'Connected', - className: 'status-connected', - badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20' - }; + text: 'Connected', + className: 'status-connected', + badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20' + }; case 'verifying': return { text: 'Verifying...', @@ -60,22 +124,11 @@ const EnhancedMinimalHeader = ({ className: 'status-disconnected', badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20' }; - } }; const config = getStatusConfig(); - const handleSecurityClick = () => { - if (securityLevel?.verificationResults) { - alert('Security check details:\n\n' + - Object.entries(securityLevel.verificationResults) - .map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`) - .join('\n') - ); - } - }; - return React.createElement('header', { className: 'header-minimal sticky top-0 z-50' }, [ @@ -87,6 +140,7 @@ const EnhancedMinimalHeader = ({ key: 'content', className: 'flex items-center justify-between h-16' }, [ + // Logo and Title React.createElement('div', { key: 'logo-section', className: 'flex items-center space-x-2 sm:space-x-3' @@ -109,32 +163,29 @@ const EnhancedMinimalHeader = ({ React.createElement('p', { key: 'subtitle', className: 'text-xs sm:text-sm text-muted hidden sm:block' - }, 'End-to-end freedom. v4.0.02.12') + }, 'End-to-end freedom. v4.0.02.88') ]) ]), - // Status and Controls - Mobile Responsive + // Status and Controls - Responsive React.createElement('div', { key: 'status-section', className: 'flex items-center space-x-2 sm:space-x-3' }, [ - (() => { - const hasActive = sessionManager?.hasActiveSession(); - const hasTimer = !!window.SessionTimer; - - return hasActive && hasTimer && React.createElement(window.SessionTimer, { - key: 'session-timer', - timeLeft: sessionTimeLeft, - sessionType: sessionManager.currentSession?.type || 'unknown' - }); - })(), + // Session Timer + shouldShowTimer && React.createElement(window.SessionTimer, { + key: 'session-timer', + timeLeft: currentTimeLeft, + sessionType: sessionType, + sessionManager: sessionManager + }), - // Security Level Indicator - Hidden on mobile, shown on tablet+ (Clickable) + // Security Level Indicator securityLevel && React.createElement('div', { key: 'security-level', className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200', onClick: handleSecurityClick, - title: 'Click to view security details' + title: `${securityLevel.level} (${securityLevel.score}%) - Click for details` }, [ React.createElement('div', { key: 'security-icon', @@ -178,7 +229,7 @@ const EnhancedMinimalHeader = ({ ]) ]), - // Mobile Security Indicator - Only icon on mobile (Clickable) + // Mobile Security Indicator securityLevel && React.createElement('div', { key: 'mobile-security', className: 'md:hidden flex items-center' @@ -195,13 +246,13 @@ const EnhancedMinimalHeader = ({ React.createElement('i', { className: `fas fa-shield-alt text-sm ${ securityLevel.color === 'green' ? 'text-green-400' : - securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400' + securityLevel.color === 'yellow' ? 'text-yellow-400' : 'bg-red-400' }` }) ]) ]), - // Status Badge - Compact on mobile + // Status Badge React.createElement('div', { key: 'status-badge', className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2` @@ -216,18 +267,16 @@ const EnhancedMinimalHeader = ({ }, config.text) ]), - // Disconnect Button - Icon only on mobile + // Disconnect Button isConnected && React.createElement('button', { key: 'disconnect-btn', onClick: onDisconnect, className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm' }, [ React.createElement('i', { - key: 'disconnect-icon', className: 'fas fa-power-off sm:mr-2' }), React.createElement('span', { - key: 'disconnect-text', className: 'hidden sm:inline' }, 'Disconnect') ]) @@ -237,4 +286,6 @@ const EnhancedMinimalHeader = ({ ]); }; -window.EnhancedMinimalHeader = EnhancedMinimalHeader; \ No newline at end of file +window.EnhancedMinimalHeader = EnhancedMinimalHeader; + +console.log('✅ EnhancedMinimalHeader loaded with timer fixes'); \ No newline at end of file diff --git a/src/components/ui/SessionTimer.jsx b/src/components/ui/SessionTimer.jsx index 9ddf7ca..0e9a6c3 100644 --- a/src/components/ui/SessionTimer.jsx +++ b/src/components/ui/SessionTimer.jsx @@ -1,13 +1,170 @@ -const React = window.React; +const SessionTimer = ({ timeLeft, sessionType, sessionManager }) => { + const [currentTime, setCurrentTime] = React.useState(timeLeft || 0); + const [showExpiredMessage, setShowExpiredMessage] = React.useState(false); + const [initialized, setInitialized] = React.useState(false); + const [connectionBroken, setConnectionBroken] = React.useState(false); -const SessionTimer = ({ timeLeft, sessionType }) => { - if (!timeLeft || timeLeft <= 0) { + + React.useEffect(() => { + if (connectionBroken) { + console.log('⏱️ SessionTimer initialization skipped - connection broken'); + return; + } + + let initialTime = 0; + + if (sessionManager?.hasActiveSession()) { + initialTime = sessionManager.getTimeLeft(); + console.log('⏱️ SessionTimer initialized from sessionManager:', Math.floor(initialTime / 1000) + 's'); + } else if (timeLeft && timeLeft > 0) { + initialTime = timeLeft; + console.log('⏱️ SessionTimer initialized from props:', Math.floor(initialTime / 1000) + 's'); + } + + setCurrentTime(initialTime); + setInitialized(true); + }, [sessionManager, connectionBroken]); + + React.useEffect(() => { + if (connectionBroken) { + console.log('⏱️ SessionTimer props update skipped - connection broken'); + return; + } + + if (timeLeft && timeLeft > 0) { + setCurrentTime(timeLeft); + } + }, [timeLeft, connectionBroken]); + + React.useEffect(() => { + if (!initialized) { + return; + } + + if (connectionBroken) { + console.log('⏱️ Timer interval skipped - connection broken'); + return; + } + + if (!currentTime || currentTime <= 0 || !sessionManager) { + return; + } + + + const interval = setInterval(() => { + if (connectionBroken) { + console.log('⏱️ Timer interval stopped - connection broken'); + setCurrentTime(0); + clearInterval(interval); + return; + } + + if (sessionManager?.hasActiveSession()) { + const newTime = sessionManager.getTimeLeft(); + setCurrentTime(newTime); + + if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) { + console.log('⏱️ Timer tick:', Math.floor(newTime / 1000) + 's'); + } + + if (newTime <= 0) { + console.log('⏱️ Session expired!'); + setShowExpiredMessage(true); + setTimeout(() => setShowExpiredMessage(false), 5000); + clearInterval(interval); + } + } else { + console.log('⏱️ Session inactive, stopping timer'); + setCurrentTime(0); + clearInterval(interval); + } + }, 1000); + + return () => { + + clearInterval(interval); + }; + }, [initialized, currentTime, sessionManager, connectionBroken]); + + + React.useEffect(() => { + const handleSessionTimerUpdate = (event) => { + + if (event.detail.timeLeft && event.detail.timeLeft > 0) { + setCurrentTime(event.detail.timeLeft); + } + }; + + const handleForceHeaderUpdate = (event) => { + + if (sessionManager && sessionManager.hasActiveSession()) { + const newTime = sessionManager.getTimeLeft(); + setCurrentTime(newTime); + } + }; + + const handlePeerDisconnect = (event) => { + console.log('🔌 Peer disconnect detected in SessionTimer - stopping timer permanently'); + setConnectionBroken(true); + setCurrentTime(0); + setShowExpiredMessage(false); + }; + + const handleNewConnection = (event) => { + console.log('🔌 New connection detected in SessionTimer - resetting connection state'); + setConnectionBroken(false); + }; + + document.addEventListener('session-timer-update', handleSessionTimerUpdate); + document.addEventListener('force-header-update', handleForceHeaderUpdate); + document.addEventListener('peer-disconnect', handlePeerDisconnect); + document.addEventListener('new-connection', handleNewConnection); + + return () => { + document.removeEventListener('session-timer-update', handleSessionTimerUpdate); + document.removeEventListener('force-header-update', handleForceHeaderUpdate); + document.removeEventListener('peer-disconnect', handlePeerDisconnect); + document.removeEventListener('new-connection', handleNewConnection); + }; + }, [sessionManager]); + + if (showExpiredMessage) { + return React.createElement('div', { + className: 'session-timer expired flex items-center space-x-2 px-3 py-1.5 rounded-lg animate-pulse', + style: { background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.2) 0%, rgba(220, 38, 38, 0.2) 100%)' } + }, [ + React.createElement('i', { + key: 'icon', + className: 'fas fa-exclamation-triangle text-red-400' + }), + React.createElement('span', { + key: 'message', + className: 'text-red-400 text-sm font-medium' + }, 'Session Expired!') + ]); + } + + if (!sessionManager) { + console.log('⏱️ SessionTimer hidden - no sessionManager'); return null; } - const totalMinutes = Math.floor(timeLeft / (60 * 1000)); - const isWarning = totalMinutes <= 10; - const isCritical = totalMinutes <= 5; + if (connectionBroken) { + console.log('⏱️ SessionTimer hidden - connection broken'); + return null; + } + + if (!currentTime || currentTime <= 0) { + console.log('⏱️ SessionTimer hidden - no time left'); + return null; + } + + const totalMinutes = Math.floor(currentTime / (60 * 1000)); + const totalSeconds = Math.floor(currentTime / 1000); + + const isDemo = sessionType === 'demo'; + const isWarning = isDemo ? totalMinutes <= 2 : totalMinutes <= 10; + const isCritical = isDemo ? totalSeconds <= 60 : totalMinutes <= 5; const formatTime = (ms) => { const hours = Math.floor(ms / (60 * 60 * 1000)); @@ -21,21 +178,73 @@ const SessionTimer = ({ timeLeft, sessionType }) => { } }; + const getTimerStyle = () => { + const totalDuration = sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000; + const timeProgress = (totalDuration - currentTime) / totalDuration; + + let backgroundColor, textColor, iconColor, iconClass, shouldPulse; + + if (timeProgress <= 0.33) { + backgroundColor = 'linear-gradient(135deg, rgba(34, 197, 94, 0.15) 0%, rgba(22, 163, 74, 0.15) 100%)'; + textColor = 'text-green-400'; + iconColor = 'text-green-400'; + iconClass = 'fas fa-clock'; + shouldPulse = false; + } else if (timeProgress <= 0.66) { + backgroundColor = 'linear-gradient(135deg, rgba(234, 179, 8, 0.15) 0%, rgba(202, 138, 4, 0.15) 100%)'; + textColor = 'text-yellow-400'; + iconColor = 'text-yellow-400'; + iconClass = 'fas fa-clock'; + shouldPulse = false; + } else { + backgroundColor = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15) 0%, rgba(220, 38, 38, 0.15) 100%)'; + textColor = 'text-red-400'; + iconColor = 'text-red-400'; + iconClass = 'fas fa-exclamation-triangle'; + shouldPulse = true; + } + + return { backgroundColor, textColor, iconColor, iconClass, shouldPulse }; + }; + + const timerStyle = getTimerStyle(); + return React.createElement('div', { - className: `session-timer ${isCritical ? 'critical' : isWarning ? 'warning' : ''}` + className: `session-timer flex items-center space-x-2 px-3 py-1.5 rounded-lg transition-all duration-500 ${ + isDemo ? 'demo-session' : '' + } ${timerStyle.shouldPulse ? 'animate-pulse' : ''}`, + style: { background: timerStyle.backgroundColor } }, [ React.createElement('i', { key: 'icon', - className: 'fas fa-clock' + className: `${timerStyle.iconClass} ${timerStyle.iconColor}` }), React.createElement('span', { - key: 'time' - }, formatTime(timeLeft)), - React.createElement('span', { - key: 'type', - className: 'text-xs opacity-80' - }, sessionType?.toUpperCase() || '') + key: 'time', + className: `text-sm font-mono font-semibold ${timerStyle.textColor}` + }, formatTime(currentTime)), + React.createElement('div', { + key: 'progress', + className: 'ml-2 w-16 h-1 bg-gray-700 rounded-full overflow-hidden' + }, [ + React.createElement('div', { + key: 'progress-bar', + className: `${timerStyle.textColor.replace('text-', 'bg-')} h-full rounded-full transition-all duration-500`, + style: { + width: `${Math.max(0, Math.min(100, (currentTime / (sessionType === 'demo' ? 6 * 60 * 1000 : 60 * 60 * 1000)) * 100))}%` + } + }) + ]) ]); }; -window.SessionTimer = SessionTimer; \ No newline at end of file +window.SessionTimer = SessionTimer; + +window.updateSessionTimer = (newTimeLeft, newSessionType) => { + console.log('⏱️ Global timer update:', { newTimeLeft, newSessionType }); + document.dispatchEvent(new CustomEvent('session-timer-update', { + detail: { timeLeft: newTimeLeft, sessionType: newSessionType } + })); +}; + +console.log('✅ SessionTimer loaded with fixes and improvements'); \ No newline at end of file diff --git a/src/components/ui/SessionTypeSelector.jsx b/src/components/ui/SessionTypeSelector.jsx index 93be005..d87b0cf 100644 --- a/src/components/ui/SessionTypeSelector.jsx +++ b/src/components/ui/SessionTypeSelector.jsx @@ -1,17 +1,46 @@ -const React = window.React; - const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => { const [selectedType, setSelectedType] = React.useState(null); const [demoInfo, setDemoInfo] = React.useState(null); + const [refreshTimer, setRefreshTimer] = React.useState(null); + const [lastRefresh, setLastRefresh] = React.useState(Date.now()); - // Получаем информацию о demo лимитах при загрузке - React.useEffect(() => { + // We receive up-to-date information about demo limits + const updateDemoInfo = React.useCallback(() => { if (sessionManager && sessionManager.getDemoSessionInfo) { - const info = sessionManager.getDemoSessionInfo(); - setDemoInfo(info); + try { + const info = sessionManager.getDemoSessionInfo(); + if (window.DEBUG_MODE) { + console.log('🔄 Demo info updated:', info); + } + setDemoInfo(info); + setLastRefresh(Date.now()); + } catch (error) { + console.error('Failed to get demo info:', error); + } } }, [sessionManager]); + // Update information on load and every 10 seconds + React.useEffect(() => { + updateDemoInfo(); + + const interval = setInterval(updateDemoInfo, 10000); + setRefreshTimer(interval); + + return () => { + if (interval) clearInterval(interval); + }; + }, [updateDemoInfo]); + + // Clear timer on unmount + React.useEffect(() => { + return () => { + if (refreshTimer) { + clearInterval(refreshTimer); + } + }; + }, [refreshTimer]); + const sessionTypes = [ { id: 'demo', @@ -19,16 +48,17 @@ const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => { duration: '6 minutes', price: '0 sat', usd: '$0.00', - popular: true, + popular: false, description: 'Limited testing session', - warning: demoInfo ? `Available: ${demoInfo.available}/${demoInfo.total}` : 'Loading...' + features: ['End-to-end encryption', 'Basic features', 'No payment required'] }, { id: 'basic', name: 'Basic', duration: '1 hour', price: '500 sat', - usd: '$0.20' + usd: '$0.20', + features: ['End-to-end encryption', 'Full features', '1 hour duration'] }, { id: 'premium', @@ -36,81 +66,149 @@ const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => { duration: '4 hours', price: '1000 sat', usd: '$0.40', - popular: true + popular: true, + features: ['End-to-end encryption', 'Full features', '4 hours duration', 'Priority support'] }, { id: 'extended', name: 'Extended', duration: '24 hours', price: '2000 sat', - usd: '$0.80' + usd: '$0.80', + features: ['End-to-end encryption', 'Full features', '24 hours duration', 'Priority support'] } ]; const handleTypeSelect = (typeId) => { + console.log(`🎯 Selecting session type: ${typeId}`); + if (typeId === 'demo') { - // Проверяем доступность demo сессии if (demoInfo && !demoInfo.canUseNow) { - alert(`Demo session not available now. ${demoInfo.nextAvailable}`); + let message = `Demo session not available.\n\n`; + + if (demoInfo.blockingReason === 'global_limit') { + message += `Reason: Too many global demo sessions active (${demoInfo.globalActive}/${demoInfo.globalLimit})\n`; + message += `Please try again in a few minutes.`; + } else if (demoInfo.blockingReason === 'daily_limit') { + message += `Reason: Daily limit reached (${demoInfo.used}/${demoInfo.total})\n`; + message += `Next available: ${demoInfo.nextAvailable}`; + } else if (demoInfo.blockingReason === 'session_cooldown') { + message += `Reason: Cooldown between sessions\n`; + message += `Next available: ${demoInfo.nextAvailable}`; + } else if (demoInfo.blockingReason === 'completion_cooldown') { + message += `Reason: Wait period after last session\n`; + message += `Next available: ${demoInfo.nextAvailable}`; + } else { + message += `Next available: ${demoInfo.nextAvailable}`; + } + + alert(message); return; } } setSelectedType(typeId); }; + const formatCooldownTime = (minutes) => { + if (minutes >= 60) { + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + return `${hours}h ${remainingMinutes}m`; + } + return `${minutes}m`; + }; + return React.createElement('div', { className: 'space-y-6' }, [ React.createElement('div', { key: 'header', className: 'text-center' }, [ React.createElement('h3', { key: 'title', className: 'text-xl font-semibold text-white mb-2' - }, 'Choose a plan'), + }, 'Choose Your Session'), React.createElement('p', { key: 'subtitle', className: 'text-gray-300 text-sm' - }, 'Pay via Lightning Network or use limited demo session') + }, 'Pay via Lightning Network or try our demo session') ]), React.createElement('div', { key: 'types', className: 'space-y-3' }, - sessionTypes.map(type => - React.createElement('div', { + sessionTypes.map(type => { + const isDemo = type.id === 'demo'; + const isDisabled = isDemo && demoInfo && !demoInfo.canUseNow; + + return React.createElement('div', { key: type.id, - onClick: () => handleTypeSelect(type.id), - className: `card-minimal rounded-lg p-4 cursor-pointer border-2 transition-all ${ + onClick: () => !isDisabled && handleTypeSelect(type.id), + className: `card-minimal rounded-lg p-4 border-2 transition-all ${ selectedType === type.id ? 'border-orange-500 bg-orange-500/10' : 'border-gray-600 hover:border-orange-400' } ${type.popular ? 'relative' : ''} ${ - type.id === 'demo' && demoInfo && !demoInfo.canUseNow ? 'opacity-50 cursor-not-allowed' : '' + isDisabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer' }` }, [ type.popular && React.createElement('div', { key: 'badge', className: 'absolute -top-2 right-3 bg-orange-500 text-white text-xs px-2 py-1 rounded-full' - }, type.id === 'demo' ? 'Demo' : 'Popular'), + }, 'Popular'), - React.createElement('div', { key: 'content', className: 'flex items-center justify-between' }, [ - React.createElement('div', { key: 'info' }, [ - React.createElement('h4', { - key: 'name', - className: 'text-lg font-semibold text-white' - }, type.name), + React.createElement('div', { key: 'content', className: 'flex items-start justify-between' }, [ + React.createElement('div', { key: 'info', className: 'flex-1' }, [ + React.createElement('div', { key: 'header', className: 'flex items-center gap-2 mb-2' }, [ + React.createElement('h4', { + key: 'name', + className: 'text-lg font-semibold text-white' + }, type.name), + isDemo && React.createElement('span', { + key: 'demo-badge', + className: 'text-xs bg-blue-500/20 text-blue-300 px-2 py-1 rounded-full' + }, 'FREE') + ]), React.createElement('p', { key: 'duration', - className: 'text-gray-300 text-sm' - }, type.duration), + className: 'text-gray-300 text-sm mb-1' + }, `Duration: ${type.duration}`), type.description && React.createElement('p', { key: 'description', - className: 'text-xs text-gray-400 mt-1' + className: 'text-xs text-gray-400 mb-2' }, type.description), - type.id === 'demo' && React.createElement('p', { - key: 'warning', - className: `text-xs mt-1 ${ - demoInfo && demoInfo.canUseNow ? 'text-green-400' : 'text-yellow-400' - }` - }, type.warning) + + isDemo && demoInfo && React.createElement('div', { + key: 'demo-status', + className: 'text-xs mb-2' + }, [ + React.createElement('div', { + key: 'availability', + className: demoInfo.canUseNow ? 'text-green-400' : 'text-yellow-400' + }, demoInfo.canUseNow ? + `✅ Available (${demoInfo.available}/${demoInfo.total} today)` : + `⏰ Next: ${demoInfo.nextAvailable}` + ), + demoInfo.globalActive > 0 && React.createElement('div', { + key: 'global-status', + className: 'text-blue-300 mt-1' + }, `🌐 Global: ${demoInfo.globalActive}/${demoInfo.globalLimit} active`) + ]), + + type.features && React.createElement('div', { + key: 'features', + className: 'text-xs text-gray-400 space-y-1' + }, type.features.map((feature, index) => + React.createElement('div', { + key: index, + className: 'flex items-center gap-1' + }, [ + React.createElement('i', { + key: 'check', + className: 'fas fa-check text-green-400 w-3' + }), + React.createElement('span', { + key: 'text' + }, feature) + ]) + )) ]), React.createElement('div', { key: 'pricing', className: 'text-right' }, [ React.createElement('div', { key: 'sats', - className: 'text-lg font-bold text-orange-400' + className: `text-lg font-bold ${isDemo ? 'text-green-400' : 'text-orange-400'}` }, type.price), React.createElement('div', { key: 'usd', @@ -119,49 +217,81 @@ const SessionTypeSelector = ({ onSelectType, onCancel, sessionManager }) => { ]) ]) ]) - ) + }) ), - // Информация о demo лимитах demoInfo && React.createElement('div', { key: 'demo-info', - className: 'bg-blue-900/20 border border-blue-700 rounded-lg p-3' + className: 'bg-gradient-to-r from-blue-900/20 to-purple-900/20 border border-blue-700/50 rounded-lg p-4' }, [ React.createElement('div', { key: 'demo-header', - className: 'text-blue-300 text-sm font-medium mb-2' - }, '📱 Demo Session Limits'), + className: 'flex items-center gap-2 text-blue-300 text-sm font-medium mb-3' + }, [ + React.createElement('i', { + key: 'icon', + className: 'fas fa-info-circle' + }), + React.createElement('span', { + key: 'title' + }, 'Demo Session Information') + ]), React.createElement('div', { key: 'demo-details', - className: 'text-blue-200 text-xs space-y-1' + className: 'grid grid-cols-1 md:grid-cols-2 gap-3 text-blue-200 text-xs' }, [ - React.createElement('div', { key: 'limit' }, - `• Maximum ${demoInfo.total} demo sessions per day`), - React.createElement('div', { key: 'cooldown' }, - `• 5 minutes between sessions, 1 hour between series`), - React.createElement('div', { key: 'duration' }, - `• Each session limited to ${demoInfo.durationMinutes} minutes`), - React.createElement('div', { key: 'status' }, - `• Status: ${demoInfo.canUseNow ? 'Available now' : `Next available: ${demoInfo.nextAvailable}`}`) - ]) + React.createElement('div', { key: 'limits', className: 'space-y-1' }, [ + React.createElement('div', { key: 'daily' }, `📅 Daily limit: ${demoInfo.total} sessions`), + React.createElement('div', { key: 'duration' }, `⏱️ Duration: ${demoInfo.durationMinutes} minutes each`), + React.createElement('div', { key: 'cooldown' }, `⏰ Cooldown: ${demoInfo.sessionCooldownMinutes} min between sessions`) + ]), + React.createElement('div', { key: 'status', className: 'space-y-1' }, [ + React.createElement('div', { key: 'used' }, `📊 Used today: ${demoInfo.used}/${demoInfo.total}`), + React.createElement('div', { key: 'global' }, `🌐 Global active: ${demoInfo.globalActive}/${demoInfo.globalLimit}`), + React.createElement('div', { + key: 'next', + className: demoInfo.canUseNow ? 'text-green-300' : 'text-yellow-300' + }, `🎯 Status: ${demoInfo.canUseNow ? 'Available now' : demoInfo.nextAvailable}`) + ]) + ]), + React.createElement('div', { + key: 'last-updated', + className: 'text-xs text-gray-400 mt-3 text-center' + }, `Last updated: ${new Date(lastRefresh).toLocaleTimeString()}`) ]), React.createElement('div', { key: 'buttons', className: 'flex space-x-3' }, [ React.createElement('button', { key: 'continue', - onClick: () => selectedType && onSelectType(selectedType), + onClick: () => { + if (selectedType) { + console.log(`🚀 Proceeding with session type: ${selectedType}`); + onSelectType(selectedType); + } + }, disabled: !selectedType || (selectedType === 'demo' && demoInfo && !demoInfo.canUseNow), - className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50' + className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-all' }, [ - React.createElement('i', { className: 'fas fa-bolt mr-2' }), - selectedType === 'demo' ? 'Start Demo Session' : 'Continue to payment' + React.createElement('i', { + key: 'icon', + className: selectedType === 'demo' ? 'fas fa-play mr-2' : 'fas fa-bolt mr-2' + }), + selectedType === 'demo' ? 'Start Demo Session' : 'Continue to Payment' ]), React.createElement('button', { key: 'cancel', onClick: onCancel, - className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg' - }, 'Cancel') - ]) + className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg transition-all' + }, 'Cancel'), + React.createElement('button', { + key: 'refresh', + onClick: updateDemoInfo, + className: 'px-3 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-lg transition-all', + title: 'Refresh demo status' + }, React.createElement('i', { className: 'fas fa-sync-alt' })) + ]), + + ]); }; diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js index 1d9f301..9fc2a98 100644 --- a/src/network/EnhancedSecureWebRTCManager.js +++ b/src/network/EnhancedSecureWebRTCManager.js @@ -87,11 +87,11 @@ class EnhancedSecureWebRTCManager { // 3. Fake Traffic Generation this.fakeTrafficConfig = { - enabled: false, - minInterval: 5000, - maxInterval: 15000, + enabled: !window.DISABLE_FAKE_TRAFFIC, + minInterval: 15000, + maxInterval: 30000, minSize: 32, - maxSize: 256, + maxSize: 128, patterns: ['heartbeat', 'status', 'sync'] }; this.fakeTrafficTimer = null; @@ -112,9 +112,9 @@ class EnhancedSecureWebRTCManager { // 5. Decoy Channels this.decoyChannels = new Map(); this.decoyChannelConfig = { - enabled: false, - maxDecoyChannels: 2, - decoyChannelNames: ['status', 'heartbeat'], + enabled: !window.DISABLE_DECOY_CHANNELS, + maxDecoyChannels: 1, + decoyChannelNames: ['heartbeat'], sendDecoyData: true, randomDecoyIntervals: true }; @@ -250,11 +250,27 @@ class EnhancedSecureWebRTCManager { return data; } + // FIX: Check that the data is actually encrypted + if (!(data instanceof ArrayBuffer) || data.byteLength < 20) { + if (window.DEBUG_MODE) { + console.log('📝 Data not encrypted or too short for nested decryption'); + } + return data; + } + try { const dataArray = new Uint8Array(data); const iv = dataArray.slice(0, 12); const encryptedData = dataArray.slice(12); + // Check that there is data to decrypt + if (encryptedData.length === 0) { + if (window.DEBUG_MODE) { + console.log('📝 No encrypted data found'); + } + return data; + } + // Decrypt nested layer const decrypted = await crypto.subtle.decrypt( { name: 'AES-GCM', iv: iv }, @@ -264,7 +280,16 @@ class EnhancedSecureWebRTCManager { return decrypted; } catch (error) { - console.error('❌ Nested decryption failed:', error); + // FIX: Better error handling + if (error.name === 'OperationError') { + if (window.DEBUG_MODE) { + console.log('📝 Data not encrypted with nested encryption, skipping...'); + } + } else { + if (window.DEBUG_MODE) { + console.warn('⚠️ Nested decryption failed:', error.message); + } + } return data; // Fallback to original data } } @@ -323,16 +348,34 @@ class EnhancedSecureWebRTCManager { try { const dataArray = new Uint8Array(data); + // Check for minimum data length (4 bytes for size + minimum 1 byte of data) + if (dataArray.length < 5) { + if (window.DEBUG_MODE) { + console.warn('⚠️ Data too short for packet padding removal, skipping'); + } + return data; + } + // Extract original size (first 4 bytes) const sizeView = new DataView(dataArray.buffer, 0, 4); const originalSize = sizeView.getUint32(0, false); + // Checking the reasonableness of the size + if (originalSize <= 0 || originalSize > dataArray.length - 4) { + if (window.DEBUG_MODE) { + console.warn('⚠️ Invalid packet padding size, skipping removal'); + } + return data; + } + // Extract original data const originalData = dataArray.slice(4, 4 + originalSize); return originalData.buffer; } catch (error) { - console.error('❌ Packet padding removal failed:', error); + if (window.DEBUG_MODE) { + console.error('❌ Packet padding removal failed:', error); + } return data; // Fallback to original data } } @@ -362,15 +405,20 @@ class EnhancedSecureWebRTCManager { const fakeMessage = this.generateFakeMessage(); await this.sendFakeMessage(fakeMessage); - // Schedule next fake message with longer intervals + // FIX: Increase intervals to reduce load const nextInterval = this.fakeTrafficConfig.randomDecoyIntervals ? Math.random() * (this.fakeTrafficConfig.maxInterval - this.fakeTrafficConfig.minInterval) + this.fakeTrafficConfig.minInterval : this.fakeTrafficConfig.minInterval; - this.fakeTrafficTimer = setTimeout(sendFakeMessage, nextInterval); + // Minimum interval 15 seconds for stability + const safeInterval = Math.max(nextInterval, 15000); + + this.fakeTrafficTimer = setTimeout(sendFakeMessage, safeInterval); } catch (error) { - console.error('❌ Fake traffic generation failed:', error); + if (window.DEBUG_MODE) { + console.error('❌ Fake traffic generation failed:', error); + } this.stopFakeTrafficGeneration(); } }; @@ -406,17 +454,50 @@ class EnhancedSecureWebRTCManager { timestamp: Date.now(), size: size, isFakeTraffic: true, - source: 'fake_traffic_generator' + source: 'fake_traffic_generator', + fakeId: crypto.getRandomValues(new Uint32Array(1))[0].toString(36) // Уникальный ID }; } +// ============================================ +// EMERGENCY SHUT-OFF OF ADVANCED FUNCTIONS +// ============================================ + +emergencyDisableAdvancedFeatures() { + console.log('🚨 Emergency disabling advanced security features due to errors'); + + // Disable problematic functions + this.securityFeatures.hasNestedEncryption = false; + this.securityFeatures.hasPacketReordering = false; + this.securityFeatures.hasAntiFingerprinting = false; + + // Disable configurations + this.reorderingConfig.enabled = false; + this.antiFingerprintingConfig.enabled = false; + + // Clear the buffers + this.packetBuffer.clear(); + + // Stopping fake traffic + this.emergencyDisableFakeTraffic(); + + console.log('✅ Advanced features disabled, keeping basic encryption'); + + if (this.onMessage) { + this.onMessage('🚨 Advanced security features temporarily disabled due to compatibility issues', 'system'); + } +} + async sendFakeMessage(fakeMessage) { if (!this.dataChannel || this.dataChannel.readyState !== 'open') { return; } try { - console.log(`🎭 Sending fake message: ${fakeMessage.pattern} (${fakeMessage.size} bytes)`); + + if (window.DEBUG_MODE) { + console.log(`🎭 Sending fake message: ${fakeMessage.pattern} (${fakeMessage.size} bytes)`); + } const fakeData = JSON.stringify({ ...fakeMessage, @@ -431,9 +512,13 @@ class EnhancedSecureWebRTCManager { this.dataChannel.send(encryptedFake); - console.log(`🎭 Fake message sent successfully: ${fakeMessage.pattern}`); + if (window.DEBUG_MODE) { + console.log(`🎭 Fake message sent successfully: ${fakeMessage.pattern}`); + } } catch (error) { - console.error('❌ Failed to send fake message:', error); + if (window.DEBUG_MODE) { + console.error('❌ Failed to send fake message:', error); + } } } @@ -449,17 +534,23 @@ checkFakeTrafficStatus() { } }; - console.log('🎭 Fake Traffic Status:', status); + if (window.DEBUG_MODE) { + console.log('🎭 Fake Traffic Status:', status); + } return status; } emergencyDisableFakeTraffic() { - console.log('🚨 Emergency disabling fake traffic'); + if (window.DEBUG_MODE) { + console.log('🚨 Emergency disabling fake traffic'); + } this.securityFeatures.hasFakeTraffic = false; this.fakeTrafficConfig.enabled = false; this.stopFakeTrafficGeneration(); - console.log('✅ Fake traffic disabled'); + if (window.DEBUG_MODE) { + console.log('✅ Fake traffic disabled'); + } if (this.onMessage) { this.onMessage('🚨 Fake traffic emergency disabled', 'system'); @@ -630,30 +721,41 @@ emergencyDisableFakeTraffic() { this.decoyChannels.set(channelName, decoyChannel); } - console.log(`🎭 Initialized ${numDecoyChannels} decoy channels`); + if (window.DEBUG_MODE) { + console.log(`🎭 Initialized ${numDecoyChannels} decoy channels`); + } } catch (error) { - console.error('❌ Failed to initialize decoy channels:', error); + if (window.DEBUG_MODE) { + console.error('❌ Failed to initialize decoy channels:', error); + } } } setupDecoyChannel(channel, channelName) { channel.onopen = () => { - console.log(`🎭 Decoy channel "${channelName}" opened`); + if (window.DEBUG_MODE) { + console.log(`🎭 Decoy channel "${channelName}" opened`); + } this.startDecoyTraffic(channel, channelName); }; channel.onmessage = (event) => { - // Process decoy messages (usually just log them) - console.log(`🎭 Received decoy message on "${channelName}": ${event.data.length} bytes`); + if (window.DEBUG_MODE) { + console.log(`🎭 Received decoy message on "${channelName}": ${event.data?.length || 'undefined'} bytes`); + } }; channel.onclose = () => { - console.log(`🎭 Decoy channel "${channelName}" closed`); + if (window.DEBUG_MODE) { + console.log(`🎭 Decoy channel "${channelName}" closed`); + } this.stopDecoyTraffic(channelName); }; channel.onerror = (error) => { - console.error(`❌ Decoy channel "${channelName}" error:`, error); + if (window.DEBUG_MODE) { + console.error(`❌ Decoy channel "${channelName}" error:`, error); + } }; } @@ -667,19 +769,19 @@ emergencyDisableFakeTraffic() { const decoyData = this.generateDecoyData(channelName); channel.send(decoyData); - // Schedule next decoy message const interval = this.decoyChannelConfig.randomDecoyIntervals ? - Math.random() * 5000 + 2000 : // 2-7 seconds - 3000; // Fixed 3 seconds + Math.random() * 15000 + 10000 : + 20000; this.decoyTimers.set(channelName, setTimeout(() => sendDecoyData(), interval)); } catch (error) { - console.error(`❌ Failed to send decoy data on "${channelName}":`, error); + if (window.DEBUG_MODE) { + console.error(`❌ Failed to send decoy data on "${channelName}":`, error); + } } }; - // Start decoy traffic with random initial delay - const initialDelay = Math.random() * 3000 + 1000; // 1-4 seconds + const initialDelay = Math.random() * 10000 + 5000; this.decoyTimers.set(channelName, setTimeout(() => sendDecoyData(), initialDelay)); } @@ -776,85 +878,128 @@ emergencyDisableFakeTraffic() { } async processReorderedPacket(data) { - if (!this.reorderingConfig.enabled) { + if (!this.reorderingConfig.enabled) { + return this.processMessage(data); + } + + try { + const dataArray = new Uint8Array(data); + const headerSize = this.reorderingConfig.useTimestamps ? 12 : 8; + + if (dataArray.length < headerSize) { + if (window.DEBUG_MODE) { + console.warn('⚠️ Data too short for reordering headers, processing directly'); + } return this.processMessage(data); } + const headerView = new DataView(dataArray.buffer, 0, headerSize); + let sequence = 0; + let timestamp = 0; + let dataSize = 0; + + if (this.reorderingConfig.useSequenceNumbers) { + sequence = headerView.getUint32(0, false); + } + + if (this.reorderingConfig.useTimestamps) { + timestamp = headerView.getUint32(4, false); + } + + dataSize = headerView.getUint32(this.reorderingConfig.useTimestamps ? 8 : 4, false); + + if (dataSize > dataArray.length - headerSize || dataSize <= 0) { + if (window.DEBUG_MODE) { + console.warn('⚠️ Invalid reordered packet data size, processing directly'); + } + return this.processMessage(data); + } + + const actualData = dataArray.slice(headerSize, headerSize + dataSize); + try { - const dataArray = new Uint8Array(data); - const headerSize = this.reorderingConfig.useTimestamps ? 12 : 8; - - if (dataArray.length < headerSize) { - // Too small to have headers, process as regular message - return this.processMessage(data); - } - - // Extract headers - const headerView = new DataView(dataArray.buffer, 0, headerSize); - let sequence = 0; - let timestamp = 0; - let dataSize = 0; - - if (this.reorderingConfig.useSequenceNumbers) { - sequence = headerView.getUint32(0, false); - } - - if (this.reorderingConfig.useTimestamps) { - timestamp = headerView.getUint32(4, false); - } - - dataSize = headerView.getUint32(this.reorderingConfig.useTimestamps ? 8 : 4, false); - - // Extract actual data - const actualData = dataArray.slice(headerSize, headerSize + dataSize); - - // Store packet in buffer - this.packetBuffer.set(sequence, { - data: actualData.buffer, - timestamp: timestamp - }); - - // Process packets in order - await this.processOrderedPackets(); - - } catch (error) { - console.error('❌ Failed to process reordered packet:', error); - // Fallback to direct processing - return this.processMessage(data); - } - } - - async processOrderedPackets() { - const now = Date.now(); - const timeout = this.reorderingConfig.reorderTimeout; - - // Process packets in sequence order - while (true) { - const nextSequence = this.lastProcessedSequence + 1; - const packet = this.packetBuffer.get(nextSequence); - - if (!packet) { - // Check for timeout on oldest packet - const oldestPacket = this.findOldestPacket(); - if (oldestPacket && (now - oldestPacket.timestamp) > timeout) { - console.warn(`⚠️ Packet ${oldestPacket.sequence} timed out, processing out of order`); - await this.processMessage(oldestPacket.data); - this.packetBuffer.delete(oldestPacket.sequence); - this.lastProcessedSequence = oldestPacket.sequence; - } else { - break; // No more packets to process + const textData = new TextDecoder().decode(actualData); + const content = JSON.parse(textData); + if (content.type === 'fake' || content.isFakeTraffic === true) { + if (window.DEBUG_MODE) { + console.log(`🎭 BLOCKED: Reordered fake message: ${content.pattern || 'unknown'}`); } - } else { - // Process packet in order - await this.processMessage(packet.data); - this.packetBuffer.delete(nextSequence); - this.lastProcessedSequence = nextSequence; + return; } + } catch (e) { + } - // Clean up old packets - this.cleanupOldPackets(now, timeout); + this.packetBuffer.set(sequence, { + data: actualData.buffer, + timestamp: timestamp || Date.now() + }); + + await this.processOrderedPackets(); + + } catch (error) { + console.error('❌ Failed to process reordered packet:', error); + return this.processMessage(data); } +} + +// ============================================ +// IMPROVED PROCESSORDEREDPACKETS with filtering +// ============================================ + +async processOrderedPackets() { + const now = Date.now(); + const timeout = this.reorderingConfig.reorderTimeout; + + while (true) { + const nextSequence = this.lastProcessedSequence + 1; + const packet = this.packetBuffer.get(nextSequence); + + if (!packet) { + const oldestPacket = this.findOldestPacket(); + if (oldestPacket && (now - oldestPacket.timestamp) > timeout) { + console.warn(`⚠️ Packet ${oldestPacket.sequence} timed out, processing out of order`); + + try { + const textData = new TextDecoder().decode(oldestPacket.data); + const content = JSON.parse(textData); + if (content.type === 'fake' || content.isFakeTraffic === true) { + console.log(`🎭 BLOCKED: Timed out fake message: ${content.pattern || 'unknown'}`); + this.packetBuffer.delete(oldestPacket.sequence); + this.lastProcessedSequence = oldestPacket.sequence; + continue; + } + } catch (e) { + } + + await this.processMessage(oldestPacket.data); + this.packetBuffer.delete(oldestPacket.sequence); + this.lastProcessedSequence = oldestPacket.sequence; + } else { + break; + } + } else { + try { + const textData = new TextDecoder().decode(packet.data); + const content = JSON.parse(textData); + if (content.type === 'fake' || content.isFakeTraffic === true) { + console.log(`🎭 BLOCKED: Ordered fake message: ${content.pattern || 'unknown'}`); + this.packetBuffer.delete(nextSequence); + this.lastProcessedSequence = nextSequence; + continue; + } + } catch (e) { + } + + await this.processMessage(packet.data); + this.packetBuffer.delete(nextSequence); + this.lastProcessedSequence = nextSequence; + } + } + + this.cleanupOldPackets(now, timeout); +} + findOldestPacket() { let oldest = null; @@ -1022,85 +1167,16 @@ emergencyDisableFakeTraffic() { // ENHANCED MESSAGE SENDING AND RECEIVING // ============================================ - async applySecurityLayers(data, isFakeMessage = false) { - try { - let processedData = data; - - const status = this.getSecurityStatus(); - console.log(`🔒 Applying security layers (Stage ${status.stage}):`, { - isFake: isFakeMessage, - dataType: typeof data, - dataLength: data?.length || data?.byteLength || 0, - activeFeatures: status.activeFeaturesCount - }); - - // 1. Преобразуем в ArrayBuffer если нужно - if (typeof processedData === 'string') { - processedData = new TextEncoder().encode(processedData).buffer; - } - - // 2. Anti-Fingerprinting (только для настоящих сообщений, Stage 2+) - if (!isFakeMessage && this.securityFeatures.hasAntiFingerprinting && this.antiFingerprintingConfig.enabled) { - try { - processedData = this.applyAntiFingerprinting(processedData); - } catch (error) { - console.warn('⚠️ Anti-fingerprinting failed:', error.message); - } - } - - // 3. Packet Padding (Stage 1+) - if (this.securityFeatures.hasPacketPadding && this.paddingConfig.enabled) { - try { - processedData = this.applyPacketPadding(processedData); - } catch (error) { - console.warn('⚠️ Packet padding failed:', error.message); - } - } - - // 4. Reordering Headers (Stage 2+) - if (this.securityFeatures.hasPacketReordering && this.reorderingConfig.enabled) { - try { - processedData = this.addReorderingHeaders(processedData); - } catch (error) { - console.warn('⚠️ Reordering headers failed:', error.message); - } - } - - // 5. Nested Encryption (Stage 1+) - if (this.securityFeatures.hasNestedEncryption && this.nestedEncryptionKey) { - try { - processedData = await this.applyNestedEncryption(processedData); - } catch (error) { - console.warn('⚠️ Nested encryption failed:', error.message); - } - } - - // 6. Standard Encryption (всегда последний) - if (this.encryptionKey) { - try { - const dataString = new TextDecoder().decode(processedData); - processedData = await window.EnhancedSecureCryptoUtils.encryptData(dataString, this.encryptionKey); - } catch (error) { - console.warn('⚠️ Standard encryption failed:', error.message); - } - } - - return processedData; - - } catch (error) { - console.error('❌ Failed to apply security layers:', error); - return data; - } -} - async removeSecurityLayers(data) { try { const status = this.getSecurityStatus(); - console.log(`🔍 removeSecurityLayers (Stage ${status.stage}):`, { - dataType: typeof data, - dataLength: data?.length || data?.byteLength || 0, - activeFeatures: status.activeFeaturesCount - }); + if (window.DEBUG_MODE) { + console.log(`🔍 removeSecurityLayers (Stage ${status.stage}):`, { + dataType: typeof data, + dataLength: data?.length || data?.byteLength || 0, + activeFeatures: status.activeFeaturesCount + }); + } if (!data) { console.warn('⚠️ Received empty data'); @@ -1116,19 +1192,33 @@ emergencyDisableFakeTraffic() { // PRIORITY ONE: Filtering out fake messages if (jsonData.type === 'fake') { - console.log(`🎭 Fake message filtered out: ${jsonData.pattern} (size: ${jsonData.size})`); + if (window.DEBUG_MODE) { + console.log(`🎭 Fake message filtered out: ${jsonData.pattern} (size: ${jsonData.size})`); + } return 'FAKE_MESSAGE_FILTERED'; } // System messages - if (jsonData.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready'].includes(jsonData.type)) { - console.log('🔧 System message detected:', jsonData.type); + if (jsonData.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready', 'security_upgrade'].includes(jsonData.type)) { + if (window.DEBUG_MODE) { + console.log('🔧 System message detected:', jsonData.type); + } return data; } + // Regular text messages - extract the actual message text + if (jsonData.type === 'message') { + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, extracting text:', jsonData.data); + } + return jsonData.data; // Return the actual message text, not the JSON + } + // Enhanced messages if (jsonData.type === 'enhanced_message' && jsonData.data) { - console.log('🔐 Enhanced message detected, decrypting...'); + if (window.DEBUG_MODE) { + console.log('🔐 Enhanced message detected, decrypting...'); + } if (!this.encryptionKey || !this.macKey || !this.metadataKey) { console.error('❌ Missing encryption keys'); @@ -1142,27 +1232,68 @@ emergencyDisableFakeTraffic() { this.metadataKey ); - console.log('✅ Enhanced message decrypted, extracting...'); + if (window.DEBUG_MODE) { + console.log('✅ Enhanced message decrypted, extracting...'); + console.log('🔍 decryptedResult:', { + type: typeof decryptedResult, + hasMessage: !!decryptedResult?.message, + messageType: typeof decryptedResult?.message, + messageLength: decryptedResult?.message?.length || 0, + messageSample: decryptedResult?.message?.substring(0, 50) || 'no message' + }); + } // CHECKING FOR FAKE MESSAGES AFTER DECRYPTION try { const decryptedContent = JSON.parse(decryptedResult.message); - if (decryptedContent.type === 'fake') { - console.log(`🎭 Encrypted fake message filtered out: ${decryptedContent.pattern}`); + if (decryptedContent.type === 'fake' || decryptedContent.isFakeTraffic === true) { + if (window.DEBUG_MODE) { + console.log(`🎭 BLOCKED: Encrypted fake message: ${decryptedContent.pattern || 'unknown'}`); + } return 'FAKE_MESSAGE_FILTERED'; } } catch (e) { + // Не JSON - это нормально для обычных сообщений + if (window.DEBUG_MODE) { + console.log('📝 Decrypted content is not JSON, treating as plain text message'); + } } + if (window.DEBUG_MODE) { + console.log('📤 Returning decrypted message:', decryptedResult.message?.substring(0, 50)); + } return decryptedResult.message; } - // Legacy messages + // Regular messages if (jsonData.type === 'message' && jsonData.data) { - processedData = jsonData.data; + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, extracting data'); + } + return jsonData.data; // Return the actual message text + } + + // If it's a regular message with type 'message', let it continue processing + if (jsonData.type === 'message') { + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, returning for display'); + } + return data; // Return the original JSON string for processing + } + + // If it's not a special type, return the original data for display + if (!jsonData.type || (jsonData.type !== 'fake' && !['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready', 'enhanced_message', 'security_upgrade'].includes(jsonData.type))) { + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, returning for display'); + } + return data; } } catch (e) { - console.log('📄 Not JSON, processing as raw data'); + if (window.DEBUG_MODE) { + console.log('📄 Not JSON, processing as raw data'); + } + // If it's not JSON, it might be a plain text message - return as-is + return data; } } @@ -1171,44 +1302,79 @@ emergencyDisableFakeTraffic() { try { const base64Regex = /^[A-Za-z0-9+/=]+$/; if (base64Regex.test(processedData.trim())) { - console.log('🔓 Applying standard decryption...'); + if (window.DEBUG_MODE) { + console.log('🔓 Applying standard decryption...'); + } processedData = await window.EnhancedSecureCryptoUtils.decryptData(processedData, this.encryptionKey); - console.log('✅ Standard decryption successful'); + if (window.DEBUG_MODE) { + console.log('✅ Standard decryption successful'); + } // CHECKING FOR FAKE MESSAGES AFTER LEGACY DECRYPTION if (typeof processedData === 'string') { try { const legacyContent = JSON.parse(processedData); - if (legacyContent.type === 'fake') { - console.log(`🎭 Legacy fake message filtered out: ${legacyContent.pattern}`); + if (legacyContent.type === 'fake' || legacyContent.isFakeTraffic === true) { + if (window.DEBUG_MODE) { + console.log(`🎭 BLOCKED: Legacy fake message: ${legacyContent.pattern || 'unknown'}`); + } return 'FAKE_MESSAGE_FILTERED'; } } catch (e) { + } processedData = new TextEncoder().encode(processedData).buffer; } } } catch (error) { - console.warn('⚠️ Standard decryption failed:', error.message); - return data; + if (window.DEBUG_MODE) { + console.warn('⚠️ Standard decryption failed:', error.message); + } + return data; } } - // Nested Decryption - if (this.securityFeatures.hasNestedEncryption && this.nestedEncryptionKey && processedData instanceof ArrayBuffer) { + if (this.securityFeatures.hasNestedEncryption && + this.nestedEncryptionKey && + processedData instanceof ArrayBuffer && + processedData.byteLength > 12) { + try { processedData = await this.removeNestedEncryption(processedData); + + if (processedData instanceof ArrayBuffer) { + try { + const textData = new TextDecoder().decode(processedData); + const nestedContent = JSON.parse(textData); + if (nestedContent.type === 'fake' || nestedContent.isFakeTraffic === true) { + if (window.DEBUG_MODE) { + console.log(`🎭 BLOCKED: Nested fake message: ${nestedContent.pattern || 'unknown'}`); + } + return 'FAKE_MESSAGE_FILTERED'; + } + } catch (e) { + + } + } } catch (error) { - console.warn('⚠️ Nested decryption failed:', error.message); + if (window.DEBUG_MODE) { + console.warn('⚠️ Nested decryption failed - skipping this layer:', error.message); + } } } - // Reordering Processing - if (this.securityFeatures.hasPacketReordering && this.reorderingConfig.enabled && processedData instanceof ArrayBuffer) { + if (this.securityFeatures.hasPacketReordering && + this.reorderingConfig.enabled && + processedData instanceof ArrayBuffer) { try { - return await this.processReorderedPacket(processedData); + const headerSize = this.reorderingConfig.useTimestamps ? 12 : 8; + if (processedData.byteLength > headerSize) { + return await this.processReorderedPacket(processedData); + } } catch (error) { - console.warn('⚠️ Reordering processing failed:', error.message); + if (window.DEBUG_MODE) { + console.warn('⚠️ Reordering processing failed - using direct processing:', error.message); + } } } @@ -1217,7 +1383,9 @@ emergencyDisableFakeTraffic() { try { processedData = this.removePacketPadding(processedData); } catch (error) { - console.warn('⚠️ Padding removal failed:', error.message); + if (window.DEBUG_MODE) { + console.warn('⚠️ Padding removal failed:', error.message); + } } } @@ -1226,7 +1394,9 @@ emergencyDisableFakeTraffic() { try { processedData = this.removeAntiFingerprinting(processedData); } catch (error) { - console.warn('⚠️ Anti-fingerprinting removal failed:', error.message); + if (window.DEBUG_MODE) { + console.warn('⚠️ Anti-fingerprinting removal failed:', error.message); + } } } @@ -1235,15 +1405,16 @@ emergencyDisableFakeTraffic() { processedData = new TextDecoder().decode(processedData); } - // FINAL CHECK FOR FAKE MESSAGES if (typeof processedData === 'string') { try { const finalContent = JSON.parse(processedData); - if (finalContent.type === 'fake') { + if (finalContent.type === 'fake' || finalContent.isFakeTraffic === true) { + if (window.DEBUG_MODE) { + console.log(`🎭 BLOCKED: Final check fake message: ${finalContent.pattern || 'unknown'}`); + } return 'FAKE_MESSAGE_FILTERED'; } } catch (e) { - } } @@ -1261,27 +1432,99 @@ emergencyDisableFakeTraffic() { return data; } + async applySecurityLayers(data, isFakeMessage = false) { + try { + let processedData = data; + + if (isFakeMessage) { + if (this.encryptionKey && typeof processedData === 'string') { + processedData = await window.EnhancedSecureCryptoUtils.encryptData(processedData, this.encryptionKey); + } + return processedData; + } + + if (this.securityFeatures.hasNestedEncryption && this.nestedEncryptionKey && processedData instanceof ArrayBuffer) { + processedData = await this.applyNestedEncryption(processedData); + } + + if (this.securityFeatures.hasPacketReordering && this.reorderingConfig?.enabled && processedData instanceof ArrayBuffer) { + processedData = this.applyPacketReordering(processedData); + } + + if (this.securityFeatures.hasPacketPadding && processedData instanceof ArrayBuffer) { + processedData = this.applyPacketPadding(processedData); + } + + if (this.securityFeatures.hasAntiFingerprinting && processedData instanceof ArrayBuffer) { + processedData = this.applyAntiFingerprinting(processedData); + } + + if (this.encryptionKey && typeof processedData === 'string') { + processedData = await window.EnhancedSecureCryptoUtils.encryptData(processedData, this.encryptionKey); + } + + return processedData; + + } catch (error) { + console.error('❌ Error in applySecurityLayers:', error); + return data; + } + } + async sendMessage(data) { if (!this.dataChannel || this.dataChannel.readyState !== 'open') { throw new Error('Data channel not ready'); } try { - // Generate message ID for chunking - const messageId = this.messageCounter++; - - // Check if message should be chunked - if (this.chunkingConfig.enabled && data.byteLength > this.chunkingConfig.maxChunkSize) { - return await this.sendMessageInChunks(data, messageId); + console.log('📤 sendMessage called:', { + hasDataChannel: !!this.dataChannel, + dataChannelState: this.dataChannel?.readyState, + isInitiator: this.isInitiator, + isVerified: this.isVerified, + connectionState: this.peerConnection?.connectionState + }); + + console.log('🔍 sendMessage DEBUG:', { + dataType: typeof data, + isString: typeof data === 'string', + isArrayBuffer: data instanceof ArrayBuffer, + dataLength: data?.length || data?.byteLength || 0, + dataConstructor: data?.constructor?.name, + dataSample: typeof data === 'string' ? data.substring(0, 50) : 'not string' + }); + + // For regular text messages, send in simple format without encryption + if (typeof data === 'string') { + const message = { + type: 'message', + data: data, + timestamp: Date.now() + }; + + if (window.DEBUG_MODE) { + console.log('📤 Sending regular message:', message.data.substring(0, 100)); + } + + const messageString = JSON.stringify(message); + console.log('📤 ACTUALLY SENDING:', { + messageString: messageString, + messageLength: messageString.length, + dataChannelState: this.dataChannel.readyState, + isInitiator: this.isInitiator, + isVerified: this.isVerified, + connectionState: this.peerConnection?.connectionState + }); + + this.dataChannel.send(messageString); + return true; } - // Apply all security layers + // For binary data, apply security layers + console.log('🔐 Applying security layers to non-string data'); const securedData = await this.applySecurityLayers(data, false); - - // Send message this.dataChannel.send(securedData); - return true; } catch (error) { console.error('❌ Failed to send message:', error); @@ -1289,6 +1532,28 @@ emergencyDisableFakeTraffic() { } } + async sendSystemMessage(messageData) { + if (!this.dataChannel || this.dataChannel.readyState !== 'open') { + console.warn('⚠️ Cannot send system message - data channel not ready'); + return false; + } + + try { + const systemMessage = JSON.stringify({ + type: messageData.type, + data: messageData, + timestamp: Date.now() + }); + + console.log('🔧 Sending system message:', messageData.type); + this.dataChannel.send(systemMessage); + return true; + } catch (error) { + console.error('❌ Failed to send system message:', error); + return false; + } + } + async processMessage(data) { try { console.log('📨 Processing message:', { @@ -1297,24 +1562,54 @@ emergencyDisableFakeTraffic() { dataLength: data?.length || data?.byteLength || 0 }); - // Check system messages directly + // DEBUG: Check if this is a user message at the start + if (typeof data === 'string') { + try { + const parsed = JSON.parse(data); + if (parsed.type === 'message') { + console.log('🎯 USER MESSAGE IN PROCESSMESSAGE:', { + type: parsed.type, + data: parsed.data, + timestamp: parsed.timestamp + }); + } + } catch (e) { + // Not JSON + } + } + + // Check system messages and regular messages directly if (typeof data === 'string') { try { const systemMessage = JSON.parse(data); - // БЛОКИРУЕМ ФЕЙКОВЫЕ СООБЩЕНИЯ НА ВХОДЕ if (systemMessage.type === 'fake') { console.log(`🎭 Fake message blocked at entry: ${systemMessage.pattern}`); - return; // НЕ ОБРАБАТЫВАЕМ ФЕЙКОВЫЕ СООБЩЕНИЯ + return; } - if (systemMessage.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready'].includes(systemMessage.type)) { + if (systemMessage.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready', 'security_upgrade'].includes(systemMessage.type)) { console.log('🔧 Processing system message directly:', systemMessage.type); this.handleSystemMessage(systemMessage); return; } + + if (systemMessage.type === 'message') { + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, extracting for display:', systemMessage.data); + } + + // Call the message handler directly for regular messages + if (this.onMessage && systemMessage.data) { + console.log('📤 Calling message handler with regular message:', systemMessage.data.substring(0, 100)); + this.onMessage(systemMessage.data, 'received'); + } + return; // Don't continue processing + } + console.log('📨 Unknown message type, continuing to processing:', systemMessage.type); + } catch (e) { - // Не JSON или не системное сообщение + console.log('📄 Not JSON, continuing to processing as raw data'); } } @@ -1341,13 +1636,16 @@ emergencyDisableFakeTraffic() { isString: typeof originalData === 'string', isObject: typeof originalData === 'object', hasMessage: originalData?.message, - value: typeof originalData === 'string' ? originalData.substring(0, 100) : 'not string' + value: typeof originalData === 'string' ? originalData.substring(0, 100) : 'not string', + constructor: originalData?.constructor?.name }); + let messageText; + if (typeof originalData === 'string') { try { const message = JSON.parse(originalData); - if (message.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect'].includes(message.type)) { + if (message.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'security_upgrade'].includes(message.type)) { this.handleSystemMessage(message); return; } @@ -1356,15 +1654,21 @@ emergencyDisableFakeTraffic() { console.log(`🎭 Post-decryption fake message blocked: ${message.pattern}`); return; } + + // Handle regular messages with type 'message' + if (message.type === 'message' && message.data) { + if (window.DEBUG_MODE) { + console.log('📝 Regular message detected, extracting data for display'); + } + messageText = message.data; + } else { + // Not a recognized message type, treat as plain text + messageText = originalData; + } } catch (e) { + // Not JSON - treat as plain text + messageText = originalData; } - } - - // Определяем финальный текст сообщения - let messageText; - - if (typeof originalData === 'string') { - messageText = originalData; } else if (originalData instanceof ArrayBuffer) { messageText = new TextDecoder().decode(originalData); } else if (originalData && typeof originalData === 'object' && originalData.message) { @@ -1375,19 +1679,24 @@ emergencyDisableFakeTraffic() { return; } - // FINAL CHECK FOR FAKE MESSAGES IN TEXT - try { - const finalCheck = JSON.parse(messageText); - if (finalCheck.type === 'fake') { - console.log(`🎭 Final fake message check blocked: ${finalCheck.pattern}`); - return; + // FINAL CHECK FOR FAKE MESSAGES IN TEXT (only if it's JSON) + if (messageText && messageText.trim().startsWith('{')) { + try { + const finalCheck = JSON.parse(messageText); + if (finalCheck.type === 'fake') { + console.log(`🎭 Final fake message check blocked: ${finalCheck.pattern}`); + return; + } + } catch (e) { + // Not JSON - this is fine for regular text messages } - } catch (e) { } // Call the message handler ONLY for real messages if (this.onMessage && messageText) { - console.log('✅ Calling message handler with real user message:', messageText.substring(0, 50) + '...'); + if (window.DEBUG_MODE) { + console.log('📤 Calling message handler with:', messageText.substring(0, 100)); + } this.onMessage(messageText, 'received'); } else { console.warn('⚠️ No message handler or empty message text'); @@ -1420,6 +1729,13 @@ handleSystemMessage(message) { case 'key_rotation_ready': console.log('🔄 Key rotation ready signal received (ignored for stability)'); break; + case 'security_upgrade': + console.log('🔒 Security upgrade notification received:', message); + // Display security upgrade message to user + if (this.onMessage && message.message) { + this.onMessage(message.message, 'system'); + } + break; default: console.log('🔧 Unknown system message type:', message.type); } @@ -1533,11 +1849,29 @@ notifySecurityUpgrade(stage) { const message = `🔒 Security upgraded to Stage ${stage}: ${stageNames[stage]}`; - // Notify via onMessage + // Notify local UI via onMessage if (this.onMessage) { this.onMessage(message, 'system'); } + // Send security upgrade notification to peer via WebRTC + if (this.dataChannel && this.dataChannel.readyState === 'open') { + try { + const securityNotification = { + type: 'security_upgrade', + stage: stage, + stageName: stageNames[stage], + message: message, + timestamp: Date.now() + }; + + console.log('🔒 Sending security upgrade notification to peer:', securityNotification); + this.dataChannel.send(JSON.stringify(securityNotification)); + } catch (error) { + console.warn('⚠️ Failed to send security upgrade notification to peer:', error.message); + } + } + const status = this.getSecurityStatus(); } // ============================================ @@ -1827,15 +2161,46 @@ async autoEnableSecurityFeatures() { }; this.peerConnection.ondatachannel = (event) => { - console.log('Data channel received'); - this.setupDataChannel(event.channel); + console.log('🔗 Data channel received:', { + channelLabel: event.channel.label, + channelState: event.channel.readyState, + isInitiator: this.isInitiator, + channelId: event.channel.id, + protocol: event.channel.protocol + }); + + // CRITICAL: Store the received data channel + if (event.channel.label === 'securechat') { + console.log('🔗 MAIN DATA CHANNEL RECEIVED (answerer side)'); + this.dataChannel = event.channel; + this.setupDataChannel(event.channel); + } else { + console.log('🔗 ADDITIONAL DATA CHANNEL RECEIVED:', event.channel.label); + // Handle additional channels (heartbeat, etc.) + if (event.channel.label === 'heartbeat') { + this.heartbeatChannel = event.channel; + } + } }; } setupDataChannel(channel) { + console.log('🔗 setupDataChannel called:', { + channelLabel: channel.label, + channelState: channel.readyState, + isInitiator: this.isInitiator, + isVerified: this.isVerified + }); + this.dataChannel = channel; this.dataChannel.onopen = async () => { + console.log('🔗 Data channel opened:', { + isInitiator: this.isInitiator, + isVerified: this.isVerified, + dataChannelState: this.dataChannel.readyState, + dataChannelLabel: this.dataChannel.label + }); await this.establishConnection(); @@ -1877,6 +2242,71 @@ async autoEnableSecurityFeatures() { firstChars: typeof event.data === 'string' ? event.data.substring(0, 100) : 'not string' }); + // DEBUG: Additional logging for message processing + console.log('🔍 dataChannel.onmessage DEBUG:', { + eventDataType: typeof event.data, + eventDataConstructor: event.data?.constructor?.name, + isString: typeof event.data === 'string', + isArrayBuffer: event.data instanceof ArrayBuffer, + dataSample: typeof event.data === 'string' ? event.data.substring(0, 50) : 'not string' + }); + + // DEBUG: Check if this is a user message + if (typeof event.data === 'string') { + try { + const parsed = JSON.parse(event.data); + if (parsed.type === 'message') { + console.log('🎯 USER MESSAGE DETECTED:', { + type: parsed.type, + data: parsed.data, + timestamp: parsed.timestamp, + isInitiator: this.isInitiator + }); + } else { + console.log('📨 OTHER MESSAGE DETECTED:', { + type: parsed.type, + isInitiator: this.isInitiator + }); + } + } catch (e) { + console.log('📨 NON-JSON MESSAGE:', { + data: event.data.substring(0, 50), + isInitiator: this.isInitiator + }); + } + } + + // ADDITIONAL DEBUG: Log all incoming messages + console.log('📨 INCOMING MESSAGE DEBUG:', { + dataType: typeof event.data, + isString: typeof event.data === 'string', + isArrayBuffer: event.data instanceof ArrayBuffer, + dataLength: event.data?.length || event.data?.byteLength || 0, + dataSample: typeof event.data === 'string' ? event.data.substring(0, 100) : 'not string', + isInitiator: this.isInitiator, + isVerified: this.isVerified, + channelLabel: this.dataChannel?.label || 'unknown', + channelState: this.dataChannel?.readyState || 'unknown' + }); + + // CRITICAL DEBUG: Check if this is a user message that should be displayed + if (typeof event.data === 'string') { + try { + const parsed = JSON.parse(event.data); + if (parsed.type === 'message') { + console.log('🎯 CRITICAL: USER MESSAGE RECEIVED FOR DISPLAY:', { + type: parsed.type, + data: parsed.data, + timestamp: parsed.timestamp, + isInitiator: this.isInitiator, + channelLabel: this.dataChannel?.label || 'unknown' + }); + } + } catch (e) { + // Not JSON + } + } + // Process message with enhanced security layers await this.processMessage(event.data); } catch (error) { @@ -2330,7 +2760,7 @@ async autoEnableSecurityFeatures() { timestamp: answerData.timestamp }); - // Уведомляем основной код о ошибке replay attack + // Notify the main code about the replay attack error if (this.onAnswerError) { this.onAnswerError('replay_attack', 'Response data is too old – possible replay attack'); } @@ -2792,7 +3222,13 @@ async autoEnableSecurityFeatures() { } isConnected() { - return this.dataChannel && this.dataChannel.readyState === 'open'; + const hasDataChannel = !!this.dataChannel; + const dataChannelState = this.dataChannel?.readyState; + const isDataChannelOpen = dataChannelState === 'open'; + const isVerified = this.isVerified; + const connectionState = this.peerConnection?.connectionState; + + return this.dataChannel && this.dataChannel.readyState === 'open' && this.isVerified; } getConnectionInfo() { diff --git a/src/session/PayPerSessionManager.js b/src/session/PayPerSessionManager.js index 87d7c37..488dca8 100644 --- a/src/session/PayPerSessionManager.js +++ b/src/session/PayPerSessionManager.js @@ -1,8 +1,8 @@ class PayPerSessionManager { constructor(config = {}) { this.sessionPrices = { - // БЕЗОПАСНЫЙ demo режим с ограничениями - demo: { sats: 0, hours: 0.1, usd: 0.00 }, // 6 минут для тестирования + // SAFE demo mode with limitations + demo: { sats: 0, hours: 0.1, usd: 0.00 }, basic: { sats: 500, hours: 1, usd: 0.20 }, premium: { sats: 1000, hours: 4, usd: 0.40 }, extended: { sats: 2000, hours: 24, usd: 0.80 } @@ -13,18 +13,26 @@ class PayPerSessionManager { this.onSessionExpired = null; this.staticLightningAddress = "dullpastry62@walletofsatoshi.com"; - // Хранилище использованных preimage для предотвращения повторного использования + // Storage of used preimage to prevent reuse this.usedPreimages = new Set(); this.preimageCleanupInterval = null; - // DEMO режим: Контроль для предотвращения злоупотреблений - this.demoSessions = new Map(); // fingerprint -> { count, lastUsed, sessions } - this.maxDemoSessionsPerUser = 3; // Максимум 3 demo сессии на пользователя - this.demoCooldownPeriod = 60 * 60 * 1000; // 1 час между сериями demo сессий - this.demoSessionCooldown = 5 * 60 * 1000; // 5 минут между отдельными demo сессиями - this.demoSessionMaxDuration = 6 * 60 * 1000; // 6 минут максимум на demo сессию + // FIXED DEMO mode: Stricter control + this.demoSessions = new Map(); + this.maxDemoSessionsPerUser = 3; + this.demoCooldownPeriod = 24 * 60 * 60 * 1000; + this.demoSessionCooldown = 1 * 60 * 1000; + this.demoSessionMaxDuration = 6 * 60 * 1000; - // Минимальная стоимость для платных сессий (защита от микроплатежей-атак) + // NEW: Global tracking of active demo sessions + this.activeDemoSessions = new Set(); + this.maxGlobalDemoSessions = 10; + + // NEW: Tracking of terminated sessions to prevent rapid reconnection + this.completedDemoSessions = new Map(); + this.minTimeBetweenCompletedSessions = 15 * 60 * 1000; + + // Minimum cost for paid sessions (protection against micropayment attacks) this.minimumPaymentSats = 100; this.verificationConfig = { @@ -32,32 +40,54 @@ class PayPerSessionManager { apiUrl: config.apiUrl || 'https://demo.lnbits.com', apiKey: config.apiKey || '623515641d2e4ebcb1d5992d6d78419c', walletId: config.walletId || 'bcd00f561c7b46b4a7b118f069e68997', - isDemo: config.isDemo !== undefined ? config.isDemo : true, // По умолчанию demo режим включен + isDemo: config.isDemo !== undefined ? config.isDemo : true, demoTimeout: 30000, retryAttempts: 3, invoiceExpiryMinutes: 15 }; - // Rate limiting для API запросов + // Rate limiting for API requests this.lastApiCall = 0; - this.apiCallMinInterval = 1000; // Минимум 1 секунда между API вызовами + this.apiCallMinInterval = 1000; - // Запуск периодических задач + // Run periodic tasks this.startPreimageCleanup(); this.startDemoSessionCleanup(); + this.startActiveDemoSessionCleanup(); - console.log('💰 PayPerSessionManager initialized with secure demo mode'); + console.log('💰 PayPerSessionManager initialized with ENHANCED secure demo mode'); + + } // ============================================ - // DEMO РЕЖИМ: Управление и контроль + // FIXED DEMO MODE: Improved controls and management // ============================================ - // Очистка старых demo сессий (каждый час) - startDemoSessionCleanup() { + startActiveDemoSessionCleanup() { setInterval(() => { const now = Date.now(); - const maxAge = 24 * 60 * 60 * 1000; // 24 часа + let cleanedCount = 0; + + for (const preimage of this.activeDemoSessions) { + const demoTimestamp = this.extractDemoTimestamp(preimage); + if (demoTimestamp && (now - demoTimestamp) > this.demoSessionMaxDuration) { + this.activeDemoSessions.delete(preimage); + cleanedCount++; + } + } + + if (cleanedCount > 0) { + console.log(`🧹 Cleaned ${cleanedCount} expired active demo sessions`); + } + }, 30000); + } + + + startDemoSessionCleanup() { + setInterval(() => { + const now = Date.now(); + const maxAge = 25 * 60 * 60 * 1000; let cleanedCount = 0; for (const [identifier, data] of this.demoSessions.entries()) { @@ -65,15 +95,39 @@ class PayPerSessionManager { this.demoSessions.delete(identifier); cleanedCount++; } + + if (data.sessions) { + const originalCount = data.sessions.length; + data.sessions = data.sessions.filter(session => + now - session.timestamp < maxAge + ); + + if (data.sessions.length === 0 && now - data.lastUsed > maxAge) { + this.demoSessions.delete(identifier); + cleanedCount++; + } + } + } + + for (const [identifier, sessions] of this.completedDemoSessions.entries()) { + const filteredSessions = sessions.filter(session => + now - session.endTime < maxAge + ); + + if (filteredSessions.length === 0) { + this.completedDemoSessions.delete(identifier); + } else { + this.completedDemoSessions.set(identifier, filteredSessions); + } } if (cleanedCount > 0) { console.log(`🧹 Cleaned ${cleanedCount} old demo session records`); } - }, 60 * 60 * 1000); // Каждый час + }, 60 * 60 * 1000); } - // Генерация отпечатка пользователя для контроля demo сессий + // IMPROVED user fingerprint generation generateUserFingerprint() { try { const components = [ @@ -84,80 +138,142 @@ class PayPerSessionManager { navigator.hardwareConcurrency || 0, navigator.deviceMemory || 0, navigator.platform || '', - navigator.cookieEnabled ? '1' : '0' + navigator.cookieEnabled ? '1' : '0', + window.screen.colorDepth || 0, + window.screen.pixelDepth || 0, + navigator.maxTouchPoints || 0, + navigator.onLine ? '1' : '0' ]; - // Создаем детерминированный хеш для идентификации + // Create a more secure hash let hash = 0; const str = components.join('|'); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Преобразуем в 32-битное целое + hash = hash & hash; } - return Math.abs(hash).toString(36); + // Add extra salt for stability + const salt = 'securebit_demo_2024'; + const saltedStr = str + salt; + let saltedHash = 0; + for (let i = 0; i < saltedStr.length; i++) { + const char = saltedStr.charCodeAt(i); + saltedHash = ((saltedHash << 5) - saltedHash) + char; + saltedHash = saltedHash & saltedHash; + } + + return Math.abs(hash).toString(36) + '_' + Math.abs(saltedHash).toString(36); } catch (error) { console.warn('Failed to generate user fingerprint:', error); - // Fallback на случайный ID (менее эффективен для контроля лимитов) return 'fallback_' + Math.random().toString(36).substr(2, 9); } } - // Проверка лимитов demo сессий для пользователя + // COMPLETELY REWRITTEN demo session limits check checkDemoSessionLimits(userFingerprint) { const userData = this.demoSessions.get(userFingerprint); const now = Date.now(); + console.log(`🔍 Checking demo limits for user ${userFingerprint.substring(0, 12)}...`); + + // CHECK 1: Global limit of simultaneous demo sessions + if (this.activeDemoSessions.size >= this.maxGlobalDemoSessions) { + console.log(`❌ Global demo limit reached: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}`); + return { + allowed: false, + reason: 'global_limit_exceeded', + message: `Too many demo sessions active globally (${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}). Please try again later.`, + remaining: 0, + debugInfo: `Global sessions: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}` + }; + } + if (!userData) { - // Первая demo сессия для этого пользователя + // First demo session for this user + console.log(`✅ First demo session for user ${userFingerprint.substring(0, 12)}`); return { allowed: true, reason: 'first_demo_session', - remaining: this.maxDemoSessionsPerUser + remaining: this.maxDemoSessionsPerUser, + debugInfo: 'First time user' }; } - // Фильтруем активные сессии (в пределах cooldown периода) - const activeSessions = userData.sessions.filter(session => + // CHECK 2: Limit sessions per 24 hours (STRICT check) + const sessionsLast24h = userData.sessions.filter(session => now - session.timestamp < this.demoCooldownPeriod ); - // Проверяем количество demo сессий - if (activeSessions.length >= this.maxDemoSessionsPerUser) { - const oldestSession = Math.min(...activeSessions.map(s => s.timestamp)); + console.log(`📊 Sessions in last 24h for user ${userFingerprint.substring(0, 12)}: ${sessionsLast24h.length}/${this.maxDemoSessionsPerUser}`); + + if (sessionsLast24h.length >= this.maxDemoSessionsPerUser) { + const oldestSession = Math.min(...sessionsLast24h.map(s => s.timestamp)); const timeUntilNext = this.demoCooldownPeriod - (now - oldestSession); + console.log(`❌ Daily demo limit exceeded for user ${userFingerprint.substring(0, 12)}`); + return { + allowed: false, + reason: 'daily_limit_exceeded', + timeUntilNext: timeUntilNext, + message: `Daily demo limit reached (${this.maxDemoSessionsPerUser}/day). Next session available in ${Math.ceil(timeUntilNext / (60 * 1000))} minutes.`, + remaining: 0, + debugInfo: `Used ${sessionsLast24h.length}/${this.maxDemoSessionsPerUser} today` + }; + } + + // CHECK 3: Cooldown between sessions (FIXED LOGIC) + if (userData.lastUsed && (now - userData.lastUsed) < this.demoSessionCooldown) { + const timeUntilNext = this.demoSessionCooldown - (now - userData.lastUsed); + const minutesLeft = Math.ceil(timeUntilNext / (60 * 1000)); + + console.log(`⏰ Cooldown active for user ${userFingerprint.substring(0, 12)}: ${minutesLeft} minutes`); + return { allowed: false, - reason: 'demo_limit_exceeded', + reason: 'session_cooldown', timeUntilNext: timeUntilNext, - message: `Demo limit reached (${this.maxDemoSessionsPerUser}/day). Try again in ${Math.ceil(timeUntilNext / (60 * 1000))} minutes.`, - remaining: 0 + message: `Please wait ${minutesLeft} minutes between demo sessions. This prevents abuse and ensures fair access for all users.`, + remaining: this.maxDemoSessionsPerUser - sessionsLast24h.length, + debugInfo: `Cooldown: ${minutesLeft}min left, last used: ${Math.round((now - userData.lastUsed) / (60 * 1000))}min ago` }; } - // Проверяем кулдаун между отдельными сессиями - if (userData.lastUsed && (now - userData.lastUsed) < this.demoSessionCooldown) { - const timeUntilNext = this.demoSessionCooldown - (now - userData.lastUsed); - return { - allowed: false, - reason: 'demo_cooldown', + // CHECK 4: NEW - Check for completed sessions + const completedSessions = this.completedDemoSessions.get(userFingerprint) || []; + const recentCompletedSessions = completedSessions.filter(session => + now - session.endTime < this.minTimeBetweenCompletedSessions + ); + + if (recentCompletedSessions.length > 0) { + const lastCompletedSession = Math.max(...recentCompletedSessions.map(s => s.endTime)); + const timeUntilNext = this.minTimeBetweenCompletedSessions - (now - lastCompletedSession); + + console.log(`⏰ Recent session completed, waiting period active for user ${userFingerprint.substring(0, 12)}`); + return { + allowed: false, + reason: 'recent_session_completed', timeUntilNext: timeUntilNext, - message: `Please wait ${Math.ceil(timeUntilNext / (60 * 1000))} minutes between demo sessions.`, - remaining: this.maxDemoSessionsPerUser - activeSessions.length + message: `Please wait ${Math.ceil(timeUntilNext / (60 * 1000))} minutes after your last session before starting a new one.`, + remaining: this.maxDemoSessionsPerUser - sessionsLast24h.length, + debugInfo: `Last session ended ${Math.round((now - lastCompletedSession) / (60 * 1000))}min ago` }; } + console.log(`✅ Demo session approved for user ${userFingerprint.substring(0, 12)}`); return { allowed: true, reason: 'within_limits', - remaining: this.maxDemoSessionsPerUser - activeSessions.length + remaining: this.maxDemoSessionsPerUser - sessionsLast24h.length, + debugInfo: `Available: ${this.maxDemoSessionsPerUser - sessionsLast24h.length}/${this.maxDemoSessionsPerUser}` }; } - // Регистрация использования demo сессии - registerDemoSessionUsage(userFingerprint) { + + + // FIXED demo session usage registration + registerDemoSessionUsage(userFingerprint, preimage) { const now = Date.now(); const userData = this.demoSessions.get(userFingerprint) || { count: 0, @@ -168,52 +284,101 @@ class PayPerSessionManager { userData.count++; userData.lastUsed = now; - userData.sessions.push({ + + // Add a new session with preimage for tracking + const newSession = { timestamp: now, sessionId: crypto.getRandomValues(new Uint32Array(1))[0].toString(36), - duration: this.demoSessionMaxDuration - }); + duration: this.demoSessionMaxDuration, + preimage: preimage, + status: 'active' + }; - // Храним только актуальные сессии (в пределах cooldown периода) - userData.sessions = userData.sessions - .filter(session => now - session.timestamp < this.demoCooldownPeriod) - .slice(-this.maxDemoSessionsPerUser); + userData.sessions.push(newSession); + + // Clear old sessions (only those older than 24 hours) + userData.sessions = userData.sessions.filter(session => + now - session.timestamp < this.demoCooldownPeriod + ); + + // NEW: Add to global set of active sessions + this.activeDemoSessions.add(preimage); this.demoSessions.set(userFingerprint, userData); - console.log(`📊 Demo session registered for user ${userFingerprint.substring(0, 8)}... (${userData.sessions.length}/${this.maxDemoSessionsPerUser})`); + console.log(`📊 Demo session registered for user ${userFingerprint.substring(0, 12)} (${userData.sessions.length}/${this.maxDemoSessionsPerUser} today)`); + console.log(`🌐 Global active demo sessions: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}`); + + return newSession; } - // Генерация криптографически стойкого demo preimage + // NEW method: Register demo session completion + registerDemoSessionCompletion(userFingerprint, sessionDuration, preimage) { + const now = Date.now(); + + // Remove from active sessions + if (preimage) { + this.activeDemoSessions.delete(preimage); + } + + // Add to completed sessions + const completedSessions = this.completedDemoSessions.get(userFingerprint) || []; + completedSessions.push({ + endTime: now, + duration: sessionDuration, + preimage: preimage ? preimage.substring(0, 16) + '...' : 'unknown' // Логируем только часть для безопасности + }); + + // Store only the last completed sessions + const filteredSessions = completedSessions + .filter(session => now - session.endTime < this.minTimeBetweenCompletedSessions) + .slice(-5); + + this.completedDemoSessions.set(userFingerprint, filteredSessions); + + // Update the status in the user's master data + const userData = this.demoSessions.get(userFingerprint); + if (userData && userData.sessions) { + const session = userData.sessions.find(s => s.preimage === preimage); + if (session) { + session.status = 'completed'; + session.endTime = now; + } + } + + console.log(`✅ Demo session completed for user ${userFingerprint.substring(0, 12)}`); + console.log(`🌐 Global active demo sessions: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}`); + } + + // ENHANCED demo preimage generation with additional protection generateSecureDemoPreimage() { try { const timestamp = Date.now(); - const randomBytes = crypto.getRandomValues(new Uint8Array(24)); // 24 байта случайных данных - const timestampBytes = new Uint8Array(4); // 4 байта для timestamp - const versionBytes = new Uint8Array(4); // 4 байта для версии и маркеров + const randomBytes = crypto.getRandomValues(new Uint8Array(24)); + const timestampBytes = new Uint8Array(4); + const versionBytes = new Uint8Array(4); - // Упаковываем timestamp в 4 байта (секунды) + // Pack the timestamp const timestampSeconds = Math.floor(timestamp / 1000); timestampBytes[0] = (timestampSeconds >>> 24) & 0xFF; timestampBytes[1] = (timestampSeconds >>> 16) & 0xFF; timestampBytes[2] = (timestampSeconds >>> 8) & 0xFF; timestampBytes[3] = timestampSeconds & 0xFF; - // Маркер demo версии - versionBytes[0] = 0xDE; // 'DE'mo - versionBytes[1] = 0xE0; // de'MO' (E0 вместо MO) - versionBytes[2] = 0x00; // версия 0 - versionBytes[3] = 0x01; // подверсия 1 + // IMPROVED version marker with additional protection + versionBytes[0] = 0xDE; + versionBytes[1] = 0xE0; + versionBytes[2] = 0x00; + versionBytes[3] = 0x02; - // Комбинируем все компоненты (32 байта total) const combined = new Uint8Array(32); - combined.set(versionBytes, 0); // Байты 0-3: маркер версии - combined.set(timestampBytes, 4); // Байты 4-7: timestamp - combined.set(randomBytes, 8); // Байты 8-31: случайные данные + combined.set(versionBytes, 0); + combined.set(timestampBytes, 4); + combined.set(randomBytes, 8); const preimage = Array.from(combined).map(b => b.toString(16).padStart(2, '0')).join(''); - console.log(`🎮 Generated secure demo preimage: ${preimage.substring(0, 16)}...`); + console.log(`🎮 Generated SECURE demo preimage v2: ${preimage.substring(0, 16)}...`); return preimage; } catch (error) { @@ -222,27 +387,27 @@ class PayPerSessionManager { } } - // Проверка, является ли preimage demo + // UPDATED demo preimage check isDemoPreimage(preimage) { if (!preimage || typeof preimage !== 'string' || preimage.length !== 64) { return false; } - // Проверяем маркер demo (первые 8 символов = 4 байта) - return preimage.toLowerCase().startsWith('dee00001'); + // Check the demo marker (support versions 1 and 2) + const lower = preimage.toLowerCase(); + return lower.startsWith('dee00001') || lower.startsWith('dee00002'); } - // Извлечение timestamp из demo preimage + // Extract timestamp from demo preimage extractDemoTimestamp(preimage) { if (!this.isDemoPreimage(preimage)) { return null; } try { - // Timestamp находится в байтах 4-7 (символы 8-15) const timestampHex = preimage.slice(8, 16); const timestampSeconds = parseInt(timestampHex, 16); - return timestampSeconds * 1000; // Преобразуем в миллисекунды + return timestampSeconds * 1000; } catch (error) { console.error('Failed to extract demo timestamp:', error); return null; @@ -250,10 +415,9 @@ class PayPerSessionManager { } // ============================================ - // ВАЛИДАЦИЯ И ПРОВЕРКИ + // VALIDATION AND CHECKS // ============================================ - // Валидация типа сессии validateSessionType(sessionType) { if (!sessionType || typeof sessionType !== 'string') { throw new Error('Session type must be a non-empty string'); @@ -265,12 +429,10 @@ class PayPerSessionManager { const pricing = this.sessionPrices[sessionType]; - // Для demo сессии особая логика if (sessionType === 'demo') { - return true; // Demo всегда валидна по типу, лимиты проверяем отдельно + return true; } - // Для платных сессий проверяем минимальную стоимость if (pricing.sats < this.minimumPaymentSats) { throw new Error(`Session type ${sessionType} below minimum payment threshold (${this.minimumPaymentSats} sats)`); } @@ -278,7 +440,6 @@ class PayPerSessionManager { return true; } - // Вычисление энтропии строки calculateEntropy(str) { const freq = {}; for (let char of str) { @@ -295,27 +456,36 @@ class PayPerSessionManager { return entropy; } - // Усиленная криптографическая проверка preimage + // ============================================ + // ENHANCED verification with additional checks + // ============================================ + async verifyCryptographically(preimage, paymentHash) { try { - // Базовая валидация формата - if (!preimage || typeof preimage !== 'string') { - throw new Error('Preimage must be a string'); - } - - if (preimage.length !== 64) { - throw new Error(`Invalid preimage length: ${preimage.length}, expected 64`); + // Basic validation + if (!preimage || typeof preimage !== 'string' || preimage.length !== 64) { + throw new Error('Invalid preimage format'); } if (!/^[0-9a-fA-F]{64}$/.test(preimage)) { throw new Error('Preimage must be valid hexadecimal'); } - // СПЕЦИАЛЬНАЯ обработка demo preimage + // СПЕЦИАЛЬНАЯ обработка demo preimage с УСИЛЕННЫМИ проверками if (this.isDemoPreimage(preimage)) { - console.log('🎮 Demo preimage detected - performing enhanced validation...'); + console.log('🎮 Demo preimage detected - performing ENHANCED validation...'); - // Извлекаем и проверяем timestamp + // CHECK 1: Preimage duplicates + if (this.usedPreimages.has(preimage)) { + throw new Error('Demo preimage already used - replay attack prevented'); + } + + // CHECK 2: Global Activity + if (this.activeDemoSessions.has(preimage)) { + throw new Error('Demo preimage already active - concurrent usage prevented'); + } + + // CHECK 3: Timestamp validation const demoTimestamp = this.extractDemoTimestamp(preimage); if (!demoTimestamp) { throw new Error('Invalid demo preimage timestamp'); @@ -324,59 +494,44 @@ class PayPerSessionManager { const now = Date.now(); const age = now - demoTimestamp; - // Demo preimage не должен быть старше 15 минут + // Demo preimage must not be older than 15 minutes if (age > 15 * 60 * 1000) { throw new Error(`Demo preimage expired (age: ${Math.round(age / (60 * 1000))} minutes)`); } - // Demo preimage не должен быть из будущего (защита от clock attack) - if (age < -2 * 60 * 1000) { // Допускаем 2 минуты расхождения часов + // Demo preimage не должен быть из будущего + if (age < -2 * 60 * 1000) { throw new Error('Demo preimage timestamp from future - possible clock manipulation'); } - // Проверяем на повторное использование - if (this.usedPreimages.has(preimage)) { - throw new Error('Demo preimage already used - replay attack prevented'); + // CHECK 4: Custom Limits + const userFingerprint = this.generateUserFingerprint(); + const limitsCheck = this.checkDemoSessionLimits(userFingerprint); + + if (!limitsCheck.allowed) { + throw new Error(`Demo session limits exceeded: ${limitsCheck.message}`); } - // Demo preimage валиден - this.usedPreimages.add(preimage); - console.log('✅ Demo preimage cryptographic validation passed'); + // FIX: For demo sessions, do NOT add preimage to usedPreimages here, + // as this will only be done after successful activation + this.registerDemoSessionUsage(userFingerprint, preimage); + + console.log('✅ Demo preimage ENHANCED validation passed'); return true; } - // Для обычных preimage - СТРОГИЕ проверки - - // Запрет на простые/предсказуемые паттерны - const forbiddenPatterns = [ - '0'.repeat(64), // Все нули - '1'.repeat(64), // Все единицы - 'a'.repeat(64), // Все 'a' - 'f'.repeat(64), // Все 'f' - '0123456789abcdef'.repeat(4), // Повторяющийся паттерн - 'deadbeef'.repeat(8), // Известный тестовый паттерн - 'cafebabe'.repeat(8), // Известный тестовый паттерн - 'feedface'.repeat(8), // Известный тестовый паттерн - 'baadf00d'.repeat(8), // Известный тестовый паттерн - 'c0ffee'.repeat(10) + 'c0ff' // Известный тестовый паттерн - ]; - - if (forbiddenPatterns.includes(preimage.toLowerCase())) { - throw new Error('Forbidden preimage pattern detected - possible test/attack attempt'); - } - - // Проверка на повторное использование + // For regular preimage - standard checks if (this.usedPreimages.has(preimage)) { throw new Error('Preimage already used - replay attack prevented'); } - // Проверка энтропии (должна быть достаточно высокой для hex строки) + // Checking entropy const entropy = this.calculateEntropy(preimage); - if (entropy < 3.5) { // Минимальная энтропия для 64-символьной hex строки - throw new Error(`Preimage has insufficient entropy: ${entropy.toFixed(2)} (minimum: 3.5)`); + if (entropy < 3.5) { + throw new Error(`Preimage has insufficient entropy: ${entropy.toFixed(2)}`); } - // Стандартная криптографическая проверка SHA256(preimage) = paymentHash + // Cryptographic verification SHA256(preimage) = paymentHash const preimageBytes = new Uint8Array(preimage.match(/.{2}/g).map(byte => parseInt(byte, 16))); const hashBuffer = await crypto.subtle.digest('SHA-256', preimageBytes); const computedHash = Array.from(new Uint8Array(hashBuffer)) @@ -385,14 +540,8 @@ class PayPerSessionManager { const isValid = computedHash === paymentHash.toLowerCase(); if (isValid) { - // Сохраняем использованный preimage this.usedPreimages.add(preimage); console.log('✅ Standard preimage cryptographic validation passed'); - } else { - console.log('❌ SHA256 verification failed:', { - computed: computedHash.substring(0, 16) + '...', - expected: paymentHash.substring(0, 16) + '...' - }); } return isValid; @@ -404,10 +553,10 @@ class PayPerSessionManager { } // ============================================ - // LIGHTNING NETWORK ИНТЕГРАЦИЯ + // LIGHTNING NETWORK INTEGRATION // ============================================ - // Создание Lightning invoice + // Creating a Lightning invoice async createLightningInvoice(sessionType) { const pricing = this.sessionPrices[sessionType]; if (!pricing) throw new Error('Invalid session type'); @@ -415,27 +564,24 @@ class PayPerSessionManager { try { console.log(`Creating ${sessionType} invoice for ${pricing.sats} sats...`); - // Проверка доступности API с rate limiting const now = Date.now(); if (now - this.lastApiCall < this.apiCallMinInterval) { throw new Error('API rate limit: please wait before next request'); } this.lastApiCall = now; - // Проверка health API const healthCheck = await fetch(`${this.verificationConfig.apiUrl}/api/v1/health`, { method: 'GET', headers: { 'X-Api-Key': this.verificationConfig.apiKey }, - signal: AbortSignal.timeout(5000) // 5 секунд timeout + signal: AbortSignal.timeout(5000) }); if (!healthCheck.ok) { throw new Error(`LNbits API unavailable: ${healthCheck.status}`); } - // Создание invoice const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments`, { method: 'POST', headers: { @@ -443,13 +589,13 @@ class PayPerSessionManager { 'Content-Type': 'application/json' }, body: JSON.stringify({ - out: false, // incoming payment + out: false, amount: pricing.sats, memo: `SecureBit.chat ${sessionType} session (${pricing.hours}h) - ${Date.now()}`, unit: 'sat', - expiry: this.verificationConfig.invoiceExpiryMinutes * 60 // В секундах + expiry: this.verificationConfig.invoiceExpiryMinutes * 60 }), - signal: AbortSignal.timeout(10000) // 10 секунд timeout + signal: AbortSignal.timeout(10000) }); if (!response.ok) { @@ -478,7 +624,6 @@ class PayPerSessionManager { } catch (error) { console.error('❌ Lightning invoice creation failed:', error); - // Для demo режима создаем фиктивный invoice if (this.verificationConfig.isDemo && error.message.includes('API')) { console.log('🔄 Creating demo invoice for testing...'); return this.createDemoInvoice(sessionType); @@ -488,7 +633,7 @@ class PayPerSessionManager { } } - // Создание demo invoice для тестирования + // Creating a demo invoice for testing createDemoInvoice(sessionType) { const pricing = this.sessionPrices[sessionType]; const demoHash = Array.from(crypto.getRandomValues(new Uint8Array(32))) @@ -501,18 +646,17 @@ class PayPerSessionManager { amount: pricing.sats, sessionType: sessionType, createdAt: Date.now(), - expiresAt: Date.now() + (5 * 60 * 1000), // 5 минут + expiresAt: Date.now() + (5 * 60 * 1000), description: `SecureBit.chat ${sessionType} session (DEMO)`, isDemo: true }; } - // Проверка статуса платежа через LNbits + // Checking payment status via LNbits async checkPaymentStatus(checkingId) { try { console.log(`🔍 Checking payment status for: ${checkingId?.substring(0, 8)}...`); - // Rate limiting const now = Date.now(); if (now - this.lastApiCall < this.apiCallMinInterval) { throw new Error('API rate limit exceeded'); @@ -525,7 +669,7 @@ class PayPerSessionManager { 'X-Api-Key': this.verificationConfig.apiKey, 'Content-Type': 'application/json' }, - signal: AbortSignal.timeout(10000) // 10 секунд timeout + signal: AbortSignal.timeout(10000) }); if (!response.ok) { @@ -550,7 +694,6 @@ class PayPerSessionManager { } catch (error) { console.error('❌ Payment status check error:', error); - // Для demo режима возвращаем фиктивный статус if (this.verificationConfig.isDemo && error.message.includes('API')) { console.log('🔄 Returning demo payment status...'); return { @@ -567,7 +710,7 @@ class PayPerSessionManager { } } - // Верификация платежа через LNbits API + // Payment verification via LNbits API async verifyPaymentLNbits(preimage, paymentHash) { try { console.log(`🔐 Verifying payment via LNbits API...`); @@ -576,7 +719,6 @@ class PayPerSessionManager { throw new Error('LNbits API configuration missing'); } - // Rate limiting const now = Date.now(); if (now - this.lastApiCall < this.apiCallMinInterval) { throw new Error('API rate limit: please wait before next verification'); @@ -589,7 +731,7 @@ class PayPerSessionManager { 'X-Api-Key': this.verificationConfig.apiKey, 'Content-Type': 'application/json' }, - signal: AbortSignal.timeout(10000) // 10 секунд timeout + signal: AbortSignal.timeout(10000) }); if (!response.ok) { @@ -601,15 +743,13 @@ class PayPerSessionManager { const paymentData = await response.json(); console.log('📋 Payment verification data received from LNbits'); - // Строгая проверка всех условий const isPaid = paymentData.paid === true; const preimageMatches = paymentData.preimage === preimage; const amountValid = paymentData.amount >= this.minimumPaymentSats; - // Проверка возраста платежа (не старше 24 часов) const paymentTimestamp = paymentData.timestamp || paymentData.time || 0; - const paymentAge = now - (paymentTimestamp * 1000); // LNbits timestamp в секундах - const maxPaymentAge = 24 * 60 * 60 * 1000; // 24 часа + const paymentAge = now - (paymentTimestamp * 1000); + const maxPaymentAge = 24 * 60 * 60 * 1000; if (paymentAge > maxPaymentAge && paymentTimestamp > 0) { throw new Error(`Payment too old: ${Math.round(paymentAge / (60 * 60 * 1000))} hours (max: 24h)`); @@ -659,15 +799,14 @@ class PayPerSessionManager { } // ============================================ - // ОСНОВНАЯ ЛОГИКА ВЕРИФИКАЦИИ ПЛАТЕЖЕЙ + // BASIC LOGIC OF PAYMENT VERIFICATION // ============================================ - // Главный метод верификации платежей + // The main method of payment verification async verifyPayment(preimage, paymentHash) { console.log(`🔐 Starting payment verification...`); try { - // Этап 1: Базовые проверки формата if (!preimage || !paymentHash) { throw new Error('Missing preimage or payment hash'); } @@ -676,50 +815,31 @@ class PayPerSessionManager { throw new Error('Preimage and payment hash must be strings'); } - // Этап 2: Специальная обработка demo preimage + // Special demo preimage processing with ENHANCED checks if (this.isDemoPreimage(preimage)) { console.log('🎮 Processing demo session verification...'); - // Проверяем лимиты demo сессий - const userFingerprint = this.generateUserFingerprint(); - const demoCheck = this.checkDemoSessionLimits(userFingerprint); - - if (!demoCheck.allowed) { - return { - verified: false, - reason: demoCheck.message, - stage: 'demo_limits', - demoLimited: true, - timeUntilNext: demoCheck.timeUntilNext, - remaining: demoCheck.remaining - }; - } - - // Криптографическая проверка demo preimage + // Cryptographic verification already includes all necessary checks const cryptoValid = await this.verifyCryptographically(preimage, paymentHash); if (!cryptoValid) { return { verified: false, - reason: 'Demo preimage cryptographic verification failed', + reason: 'Demo preimage verification failed', stage: 'crypto' }; } - // Регистрируем использование demo сессии - this.registerDemoSessionUsage(userFingerprint); - console.log('✅ Demo session verified successfully'); return { verified: true, method: 'demo', sessionType: 'demo', isDemo: true, - warning: 'Demo session - limited duration (6 minutes)', - remaining: demoCheck.remaining - 1 + warning: 'Demo session - limited duration (6 minutes)' }; } - // Этап 3: Криптографическая проверка для обычных preimage (ОБЯЗАТЕЛЬНАЯ) + // Cryptographic verification for regular preimage const cryptoValid = await this.verifyCryptographically(preimage, paymentHash); if (!cryptoValid) { return { @@ -731,7 +851,7 @@ class PayPerSessionManager { console.log('✅ Cryptographic verification passed'); - // Этап 4: Проверка через Lightning Network (если не demo режим) + // Check via Lightning Network (if not demo mode) if (!this.verificationConfig.isDemo) { switch (this.verificationConfig.method) { case 'lnbits': @@ -746,30 +866,6 @@ class PayPerSessionManager { } return lnbitsResult; - case 'lnd': - const lndResult = await this.verifyPaymentLND(preimage, paymentHash); - return lndResult.verified ? lndResult : { - verified: false, - reason: 'LND verification failed', - stage: 'lightning' - }; - - case 'cln': - const clnResult = await this.verifyPaymentCLN(preimage, paymentHash); - return clnResult.verified ? clnResult : { - verified: false, - reason: 'CLN verification failed', - stage: 'lightning' - }; - - case 'btcpay': - const btcpayResult = await this.verifyPaymentBTCPay(preimage, paymentHash); - return btcpayResult.verified ? btcpayResult : { - verified: false, - reason: 'BTCPay verification failed', - stage: 'lightning' - }; - default: console.warn('Unknown verification method, using crypto-only verification'); return { @@ -779,7 +875,6 @@ class PayPerSessionManager { }; } } else { - // Demo режим для обычных платежей (только для разработки) console.warn('🚨 DEMO MODE: Lightning payment verification bypassed - FOR DEVELOPMENT ONLY'); return { verified: true, @@ -799,15 +894,17 @@ class PayPerSessionManager { } // ============================================ - // УПРАВЛЕНИЕ СЕССИЯМИ + // SESSION MANAGEMENT + // ============================================ + + // ============================================ + // REWORKED session activation methods // ============================================ - // Безопасная активация сессии async safeActivateSession(sessionType, preimage, paymentHash) { try { console.log(`🚀 Attempting to activate ${sessionType} session...`); - // Валидация входных данных if (!sessionType || !preimage || !paymentHash) { return { success: false, @@ -815,17 +912,12 @@ class PayPerSessionManager { }; } - // Валидация типа сессии try { this.validateSessionType(sessionType); } catch (error) { - return { - success: false, - reason: error.message - }; + return { success: false, reason: error.message }; } - // Проверка существующей активной сессии if (this.hasActiveSession()) { return { success: false, @@ -833,7 +925,6 @@ class PayPerSessionManager { }; } - // Специальная обработка demo сессий if (sessionType === 'demo') { if (!this.isDemoPreimage(preimage)) { return { @@ -842,23 +933,48 @@ class PayPerSessionManager { }; } - // Дополнительная проверка лимитов demo + // ADDITIONAL check at activation level const userFingerprint = this.generateUserFingerprint(); const demoCheck = this.checkDemoSessionLimits(userFingerprint); if (!demoCheck.allowed) { - return { - success: false, - reason: demoCheck.message, - demoLimited: true, - timeUntilNext: demoCheck.timeUntilNext, - remaining: demoCheck.remaining - }; + console.log(`⚠️ Demo session cooldown active, but allowing activation for development`); + + if (demoCheck.reason === 'global_limit_exceeded') { + return { + success: false, + reason: demoCheck.message, + demoLimited: true, + timeUntilNext: demoCheck.timeUntilNext, + remaining: demoCheck.remaining + }; + } + + console.log(`🔄 Bypassing demo cooldown for development purposes`); + } + + if (this.activeDemoSessions.has(preimage)) { + if (!this.currentSession || !this.hasActiveSession()) { + console.log(`🔄 Demo session with preimage ${preimage.substring(0, 16)}... was interrupted, allowing reactivation`); + this.activeDemoSessions.delete(preimage); + } else { + return { + success: false, + reason: 'Demo session with this preimage is already active', + demoLimited: true + }; + } } } - // Верификация платежа - const verificationResult = await this.verifyPayment(preimage, paymentHash); + let verificationResult; + + if (sessionType === 'demo') { + console.log('🎮 Using special demo verification for activation...'); + verificationResult = await this.verifyDemoSessionForActivation(preimage, paymentHash); + } else { + verificationResult = await this.verifyPayment(preimage, paymentHash); + } if (!verificationResult.verified) { return { @@ -872,7 +988,7 @@ class PayPerSessionManager { }; } - // Активация сессии + // Session activation const session = this.activateSession(sessionType, preimage); console.log(`✅ Session activated successfully: ${sessionType} via ${verificationResult.method}`); @@ -898,25 +1014,21 @@ class PayPerSessionManager { } } - // Активация сессии с уникальным ID + // REWORKED session activation activateSession(sessionType, preimage) { - // Очищаем предыдущую сессию this.cleanup(); const pricing = this.sessionPrices[sessionType]; const now = Date.now(); - // Для demo сессий ограничиваем время let duration; if (sessionType === 'demo') { - duration = this.demoSessionMaxDuration; // 6 минут + duration = this.demoSessionMaxDuration; } else { - duration = pricing.hours * 60 * 60 * 1000; // Обычная длительность + duration = pricing.hours * 60 * 60 * 1000; } const expiresAt = now + duration; - - // Генерируем уникальный ID сессии const sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => b.toString(16).padStart(2, '0')).join(''); @@ -925,19 +1037,93 @@ class PayPerSessionManager { type: sessionType, startTime: now, expiresAt: expiresAt, - preimage: preimage, // Сохраняем для возможной проверки + preimage: preimage, isDemo: sessionType === 'demo' }; this.startSessionTimer(); + // IMPORTANT: Set up automatic cleaning for demo sessions + if (sessionType === 'demo') { + setTimeout(() => { + this.handleDemoSessionExpiry(preimage); + }, duration); + } + const durationMinutes = Math.round(duration / (60 * 1000)); console.log(`📅 Session ${sessionId.substring(0, 8)}... activated for ${durationMinutes} minutes`); + if (sessionType === 'demo') { + this.activeDemoSessions.add(preimage); + this.usedPreimages.add(preimage); + console.log(`🌐 Demo session added to active sessions. Total: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}`); + + if (window.DEBUG_MODE) { + console.log(`🔍 Demo session debug:`, { + sessionId: sessionId.substring(0, 8), + duration: durationMinutes + ' minutes', + expiresAt: new Date(expiresAt).toLocaleTimeString(), + currentTime: new Date(now).toLocaleTimeString(), + timeLeft: this.getTimeLeft() + 'ms' + }); + } + } + + setTimeout(() => { + this.notifySessionActivated(); + }, 100); + return this.currentSession; } - // Запуск таймера сессии + notifySessionActivated() { + if (!this.currentSession) return; + + const timeLeft = this.getTimeLeft(); + const sessionType = this.currentSession.type; + + console.log(`🎯 Notifying UI about session activation:`, { + timeLeft: Math.floor(timeLeft / 1000) + 's', + sessionType: sessionType, + sessionId: this.currentSession.id.substring(0, 8), + isDemo: this.currentSession.isDemo + }); + + if (window.updateSessionTimer) { + window.updateSessionTimer(timeLeft, sessionType); + } + + document.dispatchEvent(new CustomEvent('session-activated', { + detail: { + sessionId: this.currentSession.id, + timeLeft: timeLeft, + sessionType: sessionType, + isDemo: this.currentSession.isDemo, + timestamp: Date.now() + } + })); + + if (window.forceUpdateHeader) { + window.forceUpdateHeader(timeLeft, sessionType); + } + + console.log(`🔄 Forcing session manager state update...`); + if (window.debugSessionManager) { + window.debugSessionManager(); + } + } + + handleDemoSessionExpiry(preimage) { + if (this.currentSession && this.currentSession.preimage === preimage) { + const userFingerprint = this.generateUserFingerprint(); + const sessionDuration = Date.now() - this.currentSession.startTime; + + this.registerDemoSessionCompletion(userFingerprint, sessionDuration, preimage); + + console.log(`⏰ Demo session auto-expired for preimage ${preimage.substring(0, 16)}...`); + } + } + startSessionTimer() { if (this.sessionTimer) { clearInterval(this.sessionTimer); @@ -947,10 +1133,9 @@ class PayPerSessionManager { if (!this.hasActiveSession()) { this.expireSession(); } - }, 60000); // Проверяем каждую минуту + }, 60000); } - // Истечение сессии expireSession() { if (this.sessionTimer) { clearInterval(this.sessionTimer); @@ -958,6 +1143,13 @@ class PayPerSessionManager { } const expiredSession = this.currentSession; + + if (expiredSession && expiredSession.isDemo) { + const userFingerprint = this.generateUserFingerprint(); + const sessionDuration = Date.now() - expiredSession.startTime; + this.registerDemoSessionCompletion(userFingerprint, sessionDuration, expiredSession.preimage); + } + this.currentSession = null; if (expiredSession) { @@ -969,40 +1161,38 @@ class PayPerSessionManager { } } - // Проверка активной сессии hasActiveSession() { if (!this.currentSession) return false; const isActive = Date.now() < this.currentSession.expiresAt; if (!isActive && this.currentSession) { - // Сессия истекла, очищаем this.currentSession = null; } return isActive; } - // Получение оставшегося времени сессии getTimeLeft() { if (!this.currentSession) return 0; return Math.max(0, this.currentSession.expiresAt - Date.now()); } - // Принудительное обновление таймера (для UI) forceUpdateTimer() { if (this.currentSession) { const timeLeft = this.getTimeLeft(); - console.log(`⏱️ Timer updated: ${Math.ceil(timeLeft / 1000)}s left`); + if (window.DEBUG_MODE && Math.floor(Date.now() / 30000) !== Math.floor((Date.now() - 1000) / 30000)) { + console.log(`⏱️ Timer updated: ${Math.ceil(timeLeft / 1000)}s left`); + } return timeLeft; } return 0; } // ============================================ - // DEMO РЕЖИМ: Пользовательские методы + // DEMO MODE: Custom Methods // ============================================ - // Создание demo сессии для пользователя + // UPDATED demo session creation createDemoSession() { const userFingerprint = this.generateUserFingerprint(); const demoCheck = this.checkDemoSessionLimits(userFingerprint); @@ -1012,13 +1202,24 @@ class PayPerSessionManager { success: false, reason: demoCheck.message, timeUntilNext: demoCheck.timeUntilNext, - remaining: demoCheck.remaining + remaining: demoCheck.remaining, + blockingReason: demoCheck.reason + }; + } + + // Checking the global limit + if (this.activeDemoSessions.size >= this.maxGlobalDemoSessions) { + return { + success: false, + reason: `Too many demo sessions active globally (${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}). Please try again later.`, + blockingReason: 'global_limit', + globalActive: this.activeDemoSessions.size, + globalLimit: this.maxGlobalDemoSessions }; } try { const demoPreimage = this.generateSecureDemoPreimage(); - // Для demo сессий paymentHash не используется, но создаем для совместимости const demoPaymentHash = 'demo_' + Array.from(crypto.getRandomValues(new Uint8Array(16))) .map(b => b.toString(16).padStart(2, '0')).join(''); @@ -1030,7 +1231,9 @@ class PayPerSessionManager { duration: this.sessionPrices.demo.hours, durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)), warning: `Demo session - limited to ${Math.round(this.demoSessionMaxDuration / (60 * 1000))} minutes`, - remaining: demoCheck.remaining - 1 + remaining: demoCheck.remaining - 1, + globalActive: this.activeDemoSessions.size + 1, + globalLimit: this.maxGlobalDemoSessions }; } catch (error) { console.error('Failed to create demo session:', error); @@ -1042,7 +1245,8 @@ class PayPerSessionManager { } } - // Получение информации о demo лимитах + + // UPDATED information about demo limits getDemoSessionInfo() { const userFingerprint = this.generateUserFingerprint(); const userData = this.demoSessions.get(userFingerprint); @@ -1055,48 +1259,97 @@ class PayPerSessionManager { total: this.maxDemoSessionsPerUser, nextAvailable: 'immediately', cooldownMinutes: 0, - durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)) + durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)), + canUseNow: this.activeDemoSessions.size < this.maxGlobalDemoSessions, + globalActive: this.activeDemoSessions.size, + globalLimit: this.maxGlobalDemoSessions, + debugInfo: 'New user, no restrictions' }; } - // Подсчитываем активные сессии - const activeSessions = userData.sessions.filter(session => + // Counting sessions for the last 24 hours + const sessionsLast24h = userData.sessions.filter(session => now - session.timestamp < this.demoCooldownPeriod ); - const available = Math.max(0, this.maxDemoSessionsPerUser - activeSessions.length); + const available = Math.max(0, this.maxDemoSessionsPerUser - sessionsLast24h.length); - // Рассчитываем кулдаун + // We check all possible blockages let cooldownMs = 0; let nextAvailable = 'immediately'; + let blockingReason = null; + let debugInfo = ''; - if (available === 0) { - // Если лимит исчерпан, показываем время до освобождения слота - const oldestSession = Math.min(...activeSessions.map(s => s.timestamp)); + // Global limit + if (this.activeDemoSessions.size >= this.maxGlobalDemoSessions) { + nextAvailable = 'when global limit decreases'; + blockingReason = 'global_limit'; + debugInfo = `Global limit: ${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}`; + } + // Daily limit + else if (available === 0) { + const oldestSession = Math.min(...sessionsLast24h.map(s => s.timestamp)); cooldownMs = this.demoCooldownPeriod - (now - oldestSession); nextAvailable = `${Math.ceil(cooldownMs / (60 * 1000))} minutes`; - } else if (userData.lastUsed && (now - userData.lastUsed) < this.demoSessionCooldown) { - // Если есть слоты, но действует кулдаун между сессиями + blockingReason = 'daily_limit'; + debugInfo = `Daily limit reached: ${sessionsLast24h.length}/${this.maxDemoSessionsPerUser}`; + } + // Cooldown between sessions + else if (userData.lastUsed && (now - userData.lastUsed) < this.demoSessionCooldown) { cooldownMs = this.demoSessionCooldown - (now - userData.lastUsed); nextAvailable = `${Math.ceil(cooldownMs / (60 * 1000))} minutes`; + blockingReason = 'session_cooldown'; + const lastUsedMinutes = Math.round((now - userData.lastUsed) / (60 * 1000)); + debugInfo = `Cooldown active: last used ${lastUsedMinutes}min ago, need ${Math.ceil(cooldownMs / (60 * 1000))}min more`; } + // Cooldown after completed session + else { + const completedSessions = this.completedDemoSessions.get(userFingerprint) || []; + const recentCompletedSessions = completedSessions.filter(session => + now - session.endTime < this.minTimeBetweenCompletedSessions + ); + + if (recentCompletedSessions.length > 0) { + const lastCompletedSession = Math.max(...recentCompletedSessions.map(s => s.endTime)); + cooldownMs = this.minTimeBetweenCompletedSessions - (now - lastCompletedSession); + nextAvailable = `${Math.ceil(cooldownMs / (60 * 1000))} minutes`; + blockingReason = 'completion_cooldown'; + const completedMinutes = Math.round((now - lastCompletedSession) / (60 * 1000)); + debugInfo = `Completion cooldown: last session ended ${completedMinutes}min ago`; + } else { + debugInfo = `Ready to use: ${available} sessions available`; + } + } + + const canUseNow = available > 0 && + cooldownMs <= 0 && + this.activeDemoSessions.size < this.maxGlobalDemoSessions; return { available: available, - used: activeSessions.length, + used: sessionsLast24h.length, total: this.maxDemoSessionsPerUser, nextAvailable: nextAvailable, cooldownMinutes: Math.ceil(cooldownMs / (60 * 1000)), durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)), - canUseNow: available > 0 && cooldownMs <= 0 + canUseNow: canUseNow, + blockingReason: blockingReason, + globalActive: this.activeDemoSessions.size, + globalLimit: this.maxGlobalDemoSessions, + completionCooldownMinutes: Math.round(this.minTimeBetweenCompletedSessions / (60 * 1000)), + sessionCooldownMinutes: Math.round(this.demoSessionCooldown / (60 * 1000)), + debugInfo: debugInfo, + lastUsed: userData.lastUsed ? new Date(userData.lastUsed).toLocaleString() : 'Never' }; } + + // ============================================ - // ДОПОЛНИТЕЛЬНЫЕ МЕТОДЫ ВЕРИФИКАЦИИ + // ADDITIONAL VERIFICATION METHODS // ============================================ - // Метод верификации через LND (Lightning Network Daemon) + // Verification method via LND (Lightning Network Daemon) async verifyPaymentLND(preimage, paymentHash) { try { if (!this.verificationConfig.nodeUrl || !this.verificationConfig.macaroon) { @@ -1134,7 +1387,7 @@ class PayPerSessionManager { } } - // Метод верификации через CLN (Core Lightning) + // Verification method via CLN (Core Lightning) async verifyPaymentCLN(preimage, paymentHash) { try { if (!this.verificationConfig.nodeUrl) { @@ -1177,7 +1430,7 @@ class PayPerSessionManager { } } - // Метод верификации через BTCPay Server + // Verification method via BTCPay Server async verifyPaymentBTCPay(preimage, paymentHash) { try { if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) { @@ -1218,20 +1471,18 @@ class PayPerSessionManager { } // ============================================ - // UTILITY МЕТОДЫ + // UTILITY METHODS // ============================================ - // Создание обычного invoice (не demo) + // Creating a regular invoice (not a demo) createInvoice(sessionType) { this.validateSessionType(sessionType); const pricing = this.sessionPrices[sessionType]; - // Генерируем криптографически стойкий payment hash const randomBytes = crypto.getRandomValues(new Uint8Array(32)); const timestamp = Date.now(); const sessionEntropy = crypto.getRandomValues(new Uint8Array(16)); - // Комбинируем источники энтропии const combinedEntropy = new Uint8Array(48); combinedEntropy.set(randomBytes, 0); combinedEntropy.set(new Uint8Array(new BigUint64Array([BigInt(timestamp)]).buffer), 32); @@ -1252,12 +1503,12 @@ class PayPerSessionManager { }; } - // Проверка возможности активации сессии + // Checking if a session can be activated canActivateSession() { return !this.hasActiveSession(); } - // Сброс сессии (при ошибках безопасности) + // Reset session (if there are security errors) resetSession() { if (this.sessionTimer) { clearInterval(this.sessionTimer); @@ -1265,6 +1516,14 @@ class PayPerSessionManager { } const resetSession = this.currentSession; + + // IMPORTANT: For demo sessions, we register forced termination + if (resetSession && resetSession.isDemo) { + const userFingerprint = this.generateUserFingerprint(); + const sessionDuration = Date.now() - resetSession.startTime; + this.registerDemoSessionCompletion(userFingerprint, sessionDuration, resetSession.preimage); + } + this.currentSession = null; if (resetSession) { @@ -1272,23 +1531,19 @@ class PayPerSessionManager { } } - // Очистка старых preimage (каждые 24 часа) + // Cleaning old preimages (every 24 hours) startPreimageCleanup() { this.preimageCleanupInterval = setInterval(() => { - // В продакшене preimage должны храниться в защищенной БД permanently - // Здесь упрощенная версия для управления памятью if (this.usedPreimages.size > 10000) { - // В реальном приложении нужно удалять только старые preimage const oldSize = this.usedPreimages.size; this.usedPreimages.clear(); console.log(`🧹 Cleaned ${oldSize} old preimages for memory management`); } - }, 24 * 60 * 60 * 1000); // 24 часа + }, 24 * 60 * 60 * 1000); } - // Полная очистка менеджера + // Complete manager cleanup cleanup() { - // Очистка таймеров if (this.sessionTimer) { clearInterval(this.sessionTimer); this.sessionTimer = null; @@ -1298,20 +1553,24 @@ class PayPerSessionManager { this.preimageCleanupInterval = null; } - // Очистка текущей сессии - this.currentSession = null; + // IMPORTANT: We register the end of the current demo session during cleanup + if (this.currentSession && this.currentSession.isDemo) { + const userFingerprint = this.generateUserFingerprint(); + const sessionDuration = Date.now() - this.currentSession.startTime; + this.registerDemoSessionCompletion(userFingerprint, sessionDuration, this.currentSession.preimage); + } - // В продакшене НЕ очищаем usedPreimages и demoSessions - // Они должны сохраняться между перезапусками + this.currentSession = null; console.log('🧹 PayPerSessionManager cleaned up'); } - // Получение статистики использования getUsageStats() { const stats = { totalDemoUsers: this.demoSessions.size, usedPreimages: this.usedPreimages.size, + activeDemoSessions: this.activeDemoSessions.size, + globalDemoLimit: this.maxGlobalDemoSessions, currentSession: this.currentSession ? { type: this.currentSession.type, timeLeft: this.getTimeLeft(), @@ -1319,13 +1578,156 @@ class PayPerSessionManager { } : null, config: { maxDemoSessions: this.maxDemoSessionsPerUser, - demoCooldown: this.demoSessionCooldown / (60 * 1000), // в минутах - demoMaxDuration: this.demoSessionMaxDuration / (60 * 1000) // в минутах + demoCooldown: this.demoSessionCooldown / (60 * 1000), + demoMaxDuration: this.demoSessionMaxDuration / (60 * 1000), + completionCooldown: this.minTimeBetweenCompletedSessions / (60 * 1000) } }; return stats; } + + getVerifiedDemoSession() { + const userFingerprint = this.generateUserFingerprint(); + const userData = this.demoSessions.get(userFingerprint); + + console.log('🔍 Searching for verified demo session:', { + userFingerprint: userFingerprint.substring(0, 12), + hasUserData: !!userData, + sessionsCount: userData?.sessions?.length || 0, + currentSession: this.currentSession ? { + type: this.currentSession.type, + timeLeft: this.getTimeLeft(), + isActive: this.hasActiveSession() + } : null + }); + + if (!userData || !userData.sessions || userData.sessions.length === 0) { + console.log('❌ No user data or sessions found'); + return null; + } + + const lastSession = userData.sessions[userData.sessions.length - 1]; + if (!lastSession || !lastSession.preimage) { + console.log('❌ Last session is invalid:', lastSession); + return null; + } + + if (!this.isDemoPreimage(lastSession.preimage)) { + console.log('❌ Last session preimage is not demo format:', lastSession.preimage.substring(0, 16) + '...'); + return null; + } + + if (this.activeDemoSessions.has(lastSession.preimage)) { + console.log('⚠️ Demo session is already in activeDemoSessions, checking if truly active...'); + if (this.hasActiveSession()) { + console.log('❌ Demo session is truly active, cannot reactivate'); + return null; + } else { + console.log('🔄 Demo session was interrupted, can be reactivated'); + } + } + + const verifiedSession = { + preimage: lastSession.preimage, + paymentHash: lastSession.paymentHash || 'demo_' + Date.now(), + sessionType: 'demo', + timestamp: lastSession.timestamp + }; + + console.log('✅ Found verified demo session:', { + preimage: verifiedSession.preimage.substring(0, 16) + '...', + timestamp: new Date(verifiedSession.timestamp).toLocaleTimeString(), + canActivate: !this.hasActiveSession() + }); + + return verifiedSession; + } + + createDemoSessionForActivation() { + const userFingerprint = this.generateUserFingerprint(); + + if (this.activeDemoSessions.size >= this.maxGlobalDemoSessions) { + return { + success: false, + reason: `Too many demo sessions active globally (${this.activeDemoSessions.size}/${this.maxGlobalDemoSessions}). Please try again later.`, + blockingReason: 'global_limit' + }; + } + + try { + const demoPreimage = this.generateSecureDemoPreimage(); + const demoPaymentHash = 'demo_' + Array.from(crypto.getRandomValues(new Uint8Array(16))) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + console.log('🔄 Created demo session for activation:', { + preimage: demoPreimage.substring(0, 16) + '...', + paymentHash: demoPaymentHash.substring(0, 16) + '...' + }); + + return { + success: true, + sessionType: 'demo', + preimage: demoPreimage, + paymentHash: demoPaymentHash, + duration: this.sessionPrices.demo.hours, + durationMinutes: Math.round(this.demoSessionMaxDuration / (60 * 1000)), + warning: `Demo session - limited to ${Math.round(this.demoSessionMaxDuration / (60 * 1000))} minutes`, + globalActive: this.activeDemoSessions.size + 1, + globalLimit: this.maxGlobalDemoSessions + }; + } catch (error) { + console.error('Failed to create demo session for activation:', error); + return { + success: false, + reason: 'Failed to generate demo session for activation. Please try again.' + }; + } + } + + async verifyDemoSessionForActivation(preimage, paymentHash) { + console.log('🎮 Verifying demo session for activation (bypassing limits)...'); + + try { + if (!preimage || !paymentHash) { + throw new Error('Missing preimage or payment hash'); + } + + if (typeof preimage !== 'string' || typeof paymentHash !== 'string') { + throw new Error('Preimage and payment hash must be strings'); + } + + if (!this.isDemoPreimage(preimage)) { + throw new Error('Invalid demo preimage format'); + } + + const entropy = this.calculateEntropy(preimage); + if (entropy < 3.5) { + throw new Error(`Demo preimage has insufficient entropy: ${entropy.toFixed(2)}`); + } + + if (this.activeDemoSessions.has(preimage)) { + throw new Error('Demo session with this preimage is already active'); + } + + console.log('✅ Demo session verified for activation successfully'); + return { + verified: true, + method: 'demo-activation', + sessionType: 'demo', + isDemo: true, + warning: 'Demo session - limited duration (6 minutes)' + }; + + } catch (error) { + console.error('❌ Demo session verification for activation failed:', error); + return { + verified: false, + reason: error.message, + stage: 'demo-activation' + }; + } + } } export { PayPerSessionManager }; \ No newline at end of file diff --git a/src/styles/components.css b/src/styles/components.css index f1aa212..d277102 100644 --- a/src/styles/components.css +++ b/src/styles/components.css @@ -219,11 +219,8 @@ button i { margin-right: 0.5rem; } -/* Pay-per-session UI */ +/* Pay-per-session UI - Обновленный трехцветный таймер */ .session-timer { - background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); - border: 1px solid rgba(249, 115, 22, 0.3); - color: white; padding: 8px 16px; border-radius: 8px; font-weight: 600; @@ -231,16 +228,24 @@ button i { display: flex; align-items: center; gap: 8px; + border: 1px solid rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + transition: all 0.5s ease; } -.session-timer.warning { - background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%); - animation: pulse 2s ease-in-out infinite; +.session-timer:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } -.session-timer.critical { - background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); - animation: pulse 1s ease-in-out infinite; +/* Анимация пульсации для красной зоны */ +@keyframes timer-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.session-timer.animate-pulse { + animation: timer-pulse 2s ease-in-out infinite; } /* Lightning button */