diff --git a/README.md b/README.md
index 02bbff6..1f750b0 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# SecureBit.chat v4.5.22
+# SecureBit.chat v4.7.53
@@ -7,63 +7,137 @@
**World's first P2P messenger with ECDH + DTLS + SAS security and military-grade cryptography**
[](https://github.com/SecureBitChat/securebit-chat/releases/latest)
-[](https://securebitchat.github.io/securebit-chat/)
+[](https://securebit.chat/)
+[](https://github.com/SecureBitChat/securebit-desktop)
[](https://opensource.org/licenses/MIT)
-[π Try Now](https://securebitchat.github.io/securebit-chat/) β’ [π Documentation](#-quick-start) β’ [π Security](#-security) β’ [π€ Contribute](#-contributing)
+[Try Web Version](https://securebit.chat/) β’ [π₯οΈ Download Desktop Apps](https://github.com/SecureBitChat/securebit-desktop) β’ [π Documentation](#-quick-start) β’ [π Security](#-security)
---
-## π§ Project Update: Transition to Desktop & Mobile Versions
+## Now Available: Desktop Applications!
-The **web version of SecureBit.chat** will remain available and stable,
-but **no major updates** are planned in the near future.
+**SecureBit Chat native desktop apps are now available for Windows, macOS, and Linux!**
-We are now focusing on developing **next-generation desktop and mobile applications** powered by **Tauri v2**,
-bringing new features that will be **fully backward-compatible** with the current web version:
+[](https://github.com/SecureBitChat/securebit-desktop)
-- **Offline communication** via LoRa & mesh networking
-- **Improved encryption performance** with native crypto modules
-- **Cross-platform synchronization** between devices
-- **Local secure storage & hardware key integration**
+### Get Desktop Apps
+- **Windows 10/11** - NSIS Installer (x64)
+- **macOS 11+** - Universal App (Intel + Apple Silicon)
+- **Linux** - AppImage (Universal, amd64)
-These new builds will enable true **serverless communication** even under restricted or offline conditions.
+**Status:** Public Beta v0.1.0 Available
+**Technology:** Built with Tauri v2 (Rust + Web Technologies)
+**Coming Q1 2026:** Windows Store, Mac App Store, Snap Store
-If you appreciate our mission to build **decentralized, censorship-resistant communication**,
-please **support the project by starring it on GitHub** β your support helps development and visibility!
-
-π [β Star SecureBit.chat on GitHub](https://github.com/SecureBitChat/securebit-chat)
+**[Download Desktop Apps β](https://github.com/SecureBitChat/securebit-desktop/releases/latest)**
---
-## π― Overview
+## Shared Security Core
+
+**All SecureBit applications share the same open-source cryptographic core:**
+
+[](https://github.com/SecureBitChat/securebit-core)
+
+### Platform Architecture
+
+```
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+β User Applications β
+ββββββββββββββββββββ¬βββββββββββββββββββ¬βββββββββββββββββββββββ€
+β Web Version β Desktop Apps β Mobile (Coming) β
+β (This Repo) β (Tauri v2) β (Q1 2026) β
+β Browser PWA β Windows/Mac/ β iOS/Android β
+β v4.7.53 β Linux β Native Apps β
+β β v0.1.0 Beta β β
+ββββββββββ¬ββββββββββ΄βββββββββ¬ββββββββββ΄βββββββββββ¬ββββββββββββ
+ β β β
+ ββββββββββββββββββββΌβββββββββββββββββββββ
+ β
+ βΌ
+ ββββββββββββββββββββββββββββββββββββββββββββ
+ β securebit-core (Open Source) β
+ β β’ All Cryptographic Operations β
+ β β’ P2P Protocol Implementation β
+ β β’ End-to-End Encryption β
+ β β’ Key Exchange & Verification β
+ β β’ ASN.1 Structure Validation β
+ β License: Apache 2.0 β
+ ββββββββββββββββββββββββββββββββββββββββββββ
+```
+
+### Why This Architecture?
+
+- **100% of cryptography is open source** - Audit at [securebit-core](https://github.com/SecureBitChat/securebit-core)
+- **Single source of truth** - Same security across all platforms
+- **Full transparency** - Security-critical code is publicly auditable
+- **Community reviewed** - Bug reports and security feedback welcome
+- **Memory-safe core** - Rust implementation prevents entire classes of vulnerabilities
+- **Cross-platform consistency** - Identical security guarantees on all platforms
+
+**Core Repository:** https://github.com/SecureBitChat/securebit-core
+**License:** Apache License 2.0
+**Language:** Rust (memory-safe, zero-cost abstractions)
+
+---
+
+## Overview
SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your privacy with military-grade encryption. No servers, no registration, no data collection - just pure, secure communication.
+### Platform Availability
+
+| Platform | Status | Version | Link |
+|----------|--------|---------|------|
+| **Web Browser** | Production | v4.7.53 | [Launch Web App](https://securebitchat.github.io/securebit-chat/) |
+| **Windows Desktop** | Beta | v0.1.0 | [Download](https://github.com/SecureBitChat/securebit-desktop/releases/latest) |
+| **macOS Desktop** | Beta | v0.1.0 | [Download](https://github.com/SecureBitChat/securebit-desktop/releases/latest) |
+| **Linux Desktop** | Beta | v0.1.0 | [Download](https://github.com/SecureBitChat/securebit-desktop/releases/latest) |
+| **iOS Mobile** | π In Development | - | Coming Q1 2026 |
+| **Android Mobile** | π In Development | - | Coming Q1 2026 |
+
### Key Features
-- π **19-Layer Military Security** - ECDH + DTLS + SAS verification
-- π **Pure P2P Architecture** - No servers, truly decentralized
-- π± **Progressive Web App** - Install like a native app
-- π **Secure File Transfer** - End-to-end encrypted P2P file sharing
-- π **Smart Notifications** - Browser alerts only when away
-- π **Complete Anonymity** - Zero data collection, no registration
+- **18-Layer Military Security** - ECDH + DTLS + SAS triple-layer verification
+- **Pure P2P Architecture** - No servers, truly decentralized
+- **Progressive Web App** - Install like a native app on any device
+- **Native Desktop Apps** - Windows, macOS, Linux (Tauri v2)
+- **Native Mobile Apps** - iOS (Swift/SwiftUI), Android (Kotlin/Jetpack Compose) - Coming Q1 2026
+- **Secure File Transfer** - End-to-end encrypted P2P file sharing
+- **Smart Notifications** - Browser and desktop alerts
+- **Complete Anonymity** - Zero data collection, no registration
+- **Open Source Security** - Cryptographic core is fully auditable
+- **ASN.1 Validation** - Complete key structure verification
+- **Perfect Forward Secrecy** - Automatic key rotation
---
-## β¨ What's New in v4.5.22
+## β¨ What's New in v4.7.53
-### fix: prevent encryption key loss and IndexedDB connection errors
+### Desktop Edition Release
-- Disable timer-based key rotation for Double Ratchet mode
-- Auto-reinitialize encryption keys when missing but ECDH available
-- Preserve active keys during periodic cleanup in ratchet sessions
-- Fix IndexedDB "database closing" errors with connection checking
-- Add individual transactions per queue item to prevent race conditions
+- **Native Desktop Applications** - Windows, macOS, and Linux support
+- **Tauri v2 Framework** - Lightweight, secure, and performant
+- **System Integration** - Native notifications, system tray, auto-start
+- **Offline Support** - Works without internet connection
+- **Multi-window Support** - Multiple conversation windows
+- **Improved Performance** - Native code execution for crypto operations
+
+### Bug Fixes & Improvements
+
+- **Fix:** Prevent encryption key loss and IndexedDB connection errors
+- **Fix:** Disable timer-based key rotation for Double Ratchet mode
+- **Fix:** Auto-reinitialize encryption keys when missing but ECDH available
+- **Fix:** Preserve active keys during periodic cleanup in ratchet sessions
+- **Fix:** IndexedDB "database closing" errors with connection checking
+- **Improvement:** Individual transactions per queue item to prevent race conditions
+- **Improvement:** Enhanced message text wrapping in chat interface
+
+### Security Enhancements
-### π‘οΈ Security Enhancements
- **ECDH + DTLS + SAS System** - Triple-layer security verification
- **ASN.1 Full Structure Validation** - Complete key structure verification
- **Enhanced MITM Protection** - Multi-layer defense system
@@ -73,36 +147,415 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
---
-## πΊοΈ Roadmap
+## Quick Start
-**Current: v4.5.22** - Browser Notifications & Code Cleanup β
+### Web Version (Browser)
-**Next Releases:**
+1. **Visit** [https://securebit.chat/](https://securebit.chat/)
+2. **Share your link** or enter your peer's link
+3. **Start chatting** - No registration required!
-- **v4.5 (Q2 2025)** - Mobile & Desktop Apps
- - Native mobile applications (iOS/Android)
- - **Tauri v2 desktop clients (Windows/macOS/Linux)**
- - Push notifications and cross-device sync
+**Install as PWA:**
+- Click the install prompt in your browser
+- Or use browser menu: "Install SecureBit.chat"
-- **v5.0 (Q4 2025)** - Quantum-Resistant Edition
- - CRYSTALS-Kyber post-quantum key exchange
- - SPHINCS+ post-quantum signatures
- - Hybrid classical + post-quantum schemes
+### Desktop Version (Native Apps)
+
+1. **Download** installer from [securebit-desktop releases](https://github.com/SecureBitChat/securebit-desktop/releases/latest)
+2. **Install** on Windows, macOS, or Linux
+3. **Launch** and start secure communication
+
+**Platform-specific instructions:**
+- **Windows:** Run `.exe` installer, follow setup wizard
+- **macOS:** Open `.zip`, drag `SecureBit Chat.app` to Applications
+- **Linux:** Make AppImage executable: `chmod +x SecureBit.Chat_*.AppImage`, then run
+
+### Features Comparison
+
+| Feature | Web Version | Desktop Apps |
+|---------|-------------|--------------|
+| **P2P Encryption** | β
| β
|
+| **File Sharing** | β
| β
|
+| **Voice/Video Calls** | β
| β
|
+| **Screen Sharing** | β
| β
|
+| **System Notifications** | β
(Browser) | β
(Native) |
+| **Offline Mode** | β | β
|
+| **Auto-start** | β | β
|
+| **System Tray** | β | β
|
+| **Multi-window** | β | β
|
+| **Background Operation** | β | β
|
+| **Lower Resource Usage** | β | β
(Tauri) |
---
-## π€ Contributing
+## πΊοΈ Roadmap
+
+**Current: v4.7.53** - Desktop Edition Available
+
+### Released Versions
+
+- **v4.5** - Enhanced Security Edition
+ - ECDH + DTLS + SAS triple-layer security
+ - 18-layer military-grade cryptography
+ - Complete ASN.1 validation
+ - Perfect Forward Secrecy
+
+- **v4.7** - Desktop Edition (Current)
+ - Native desktop applications (Windows, macOS, Linux)
+ - Built with Tauri v2
+ - System tray integration and native notifications
+ - Offline support and multi-window
+
+- **v0.1.0** - Desktop Apps Beta
+ - Initial desktop release
+ - Windows, macOS, Linux support
+
+### Upcoming Releases
+
+- **v5.0 (Q1 2026)** - Mobile Edition
+ - Native iOS app (Swift/SwiftUI)
+ - Native Android app (Kotlin/Jetpack Compose)
+ - PWA support for mobile browsers
+ - Real-time push notifications
+ - Battery optimization
+ - Biometric authentication
+
+- **v5.5 (Q2 2026)** - Quantum-Resistant Edition
+ - CRYSTALS-Kyber post-quantum key exchange
+ - SPHINCS+ post-quantum signatures
+ - Hybrid classical + post-quantum schemes
+ - Quantum-safe key exchange
+ - Migration of existing sessions
+
+- **v6.0 (Q4 2026)** - Group Communications
+ - P2P group connections up to 8 participants
+ - Mesh networking for groups
+ - Signal Double Ratchet for groups
+ - Anonymous groups without metadata
+ - Ephemeral groups (disappear after session)
+
+- **v6.5 (2027)** - Decentralized Network
+ - DHT for peer discovery
+ - Built-in onion routing
+ - Tokenomics and node incentives
+ - Governance via DAO
+ - Self-healing network
+
+- **v7.0 (2028+)** - AI Privacy Assistant
+ - Local AI threat analysis
+ - Automatic MITM detection
+ - Adaptive cryptography
+ - Zero-knowledge machine learning
+
+---
+
+## Security
+
+### Open Source Cryptographic Core
+
+**All security-critical code is open source and auditable:**
+
+- **Repository:** [securebit-core](https://github.com/SecureBitChat/securebit-core)
+- **License:** Apache License 2.0
+- **Language:** Rust (memory-safe, prevents entire vulnerability classes)
+- **Auditable:** 100% of cryptographic operations
+- **Standards:** RFC 5869 (HKDF), NIST SP 800-56A (ECDH), RFC 8446 (DTLS)
+
+### Security Features
+
+#### Triple-Layer Verification
+1. **ECDH (Elliptic Curve Diffie-Hellman)** - P-384 curve key exchange
+2. **DTLS (Datagram Transport Layer Security)** - WebRTC transport security with fingerprint verification
+3. **SAS (Short Authentication String)** - Visual MITM detection and verification
+
+#### Cryptographic Primitives
+- **Key Exchange:** ECDH P-384 (NIST curve)
+- **Signatures:** ECDSA P-384
+- **Encryption:** AES-256-GCM
+- **Key Derivation:** HKDF-SHA-256 (RFC 5869)
+- **Authentication:** HMAC-SHA-256
+- **Hashing:** SHA-256, SHA-384
+
+#### Protocol Security
+- Perfect Forward Secrecy (PFS)
+- End-to-End Encryption (E2EE)
+- Zero-Knowledge Architecture
+- Replay Protection
+- Metadata Protection
+- ASN.1 Structure Validation
+- OID and EC Point Verification
+- SPKI Structure Validation
+
+#### Security Architecture
+- **18-Layer Defense System** - Multiple independent security layers
+- **MITM Attack Prevention** - Triple verification prevents man-in-the-middle attacks
+- **Key Isolation** - WeakMap-based secure key storage
+- **Secure Memory Management** - Automatic secure deletion of sensitive data
+- **Production Logging** - Sanitized logs prevent information leakage
+
+### What We DON'T Collect
+- No personal information
+- No phone numbers or emails
+- No contact lists on servers
+- No message content or metadata
+- No telemetry or analytics
+- No usage statistics
+- No IP addresses logged
+- No device fingerprints
+- No location data
+
+### Security Audit
+
+Want to audit our security? Check these repositories:
+
+1. **[securebit-core](https://github.com/SecureBitChat/securebit-core)** - All cryptographic operations (Rust)
+2. **[securebit-chat](https://github.com/SecureBitChat/securebit-chat)** - Web UI implementation (this repo, JavaScript/React)
+
+**Report Security Issues:** SecureBitChat@proton.me
+**PGP Key:** Available on request for encrypted security reports
+
+---
+
+## Development
+
+### Prerequisites
+
+- **Node.js** 18+
+- **npm** or **yarn**
+- **Git**
+
+### Installation
+
+```bash
+# Clone repository
+git clone https://github.com/SecureBitChat/securebit-chat.git
+cd securebit-chat
+
+# Install dependencies
+npm install
+
+# Run development server
+npm run dev
+```
+
+### Building
+
+```bash
+# Build for production
+npm run build
+
+# Build CSS only
+npm run build:css
+
+# Build JavaScript only
+npm run build:js
+
+# Preview production build (requires Python)
+python -m http.server 8000
+```
+
+### Development Scripts
+
+```bash
+# Development server with hot reload
+npm run dev
+
+# Watch CSS changes
+npm run watch
+
+# Build everything
+npm run build
+
+# Serve built files
+npm run serve
+```
+
+### Project Structure
+
+```
+securebit-chat/
+βββ src/
+β βββ components/ # React components
+β β βββ ui/ # UI components (Header, Roadmap, etc.)
+β β βββ QRScanner.jsx # QR code scanner
+β βββ crypto/ # Cryptography utilities
+β β βββ EnhancedSecureCryptoUtils.js
+β βββ network/ # WebRTC P2P logic
+β β βββ EnhancedSecureWebRTCManager.js
+β βββ transfer/ # File transfer
+β β βββ EnhancedSecureFileTransfer.js
+β βββ notifications/ # Notification system
+β βββ pwa/ # PWA functionality
+β βββ scripts/ # Bootstrap and initialization
+β βββ styles/ # CSS stylesheets
+βββ dist/ # Built files (generated)
+βββ assets/ # Static assets
+βββ public/ # Public files
+βββ docs/ # Documentation
+```
+
+### Contributing to Core
+
+Want to improve security? Contribute to the cryptographic core:
+- **Repository:** [securebit-core](https://github.com/SecureBitChat/securebit-core)
+- **Focus:** Cryptography, protocol implementation, security features
+- **Language:** Rust
+
+
+## Related Projects
+
+### Official SecureBit Ecosystem
+
+| Project | Description | Status | License |
+|---------|-------------|--------|---------|
+| **[securebit-core](https://github.com/SecureBitChat/securebit-core)** | Cryptographic kernel (Rust) | β
Production | Apache 2.0 |
+| **[securebit-chat](https://github.com/SecureBitChat/securebit-chat)** | Web application (this repo) | β
Production v4.7.53 | MIT |
+| **[securebit-desktop](https://github.com/SecureBitChat/securebit-desktop)** | Desktop apps (Windows/Mac/Linux) | β
Beta v0.1.0 | Proprietary* |
+| **securebit-mobile** | Mobile apps (iOS/Android) | π Coming Q1 2026 | TBD |
+
+*\* Desktop apps are free for personal and commercial use. Only the UI layer is proprietary - all cryptography is open source in securebit-core.*
+
+### Technology Stack
+
+- **Frontend:** React, Tailwind CSS
+- **Build:** esbuild, Tailwind CLI
+- **P2P:** WebRTC
+- **Crypto Core:** Rust (securebit-core)
+- **Desktop:** Tauri v2
+- **Mobile (Future):** Swift/SwiftUI (iOS), Kotlin/Jetpack Compose (Android)
+
+---
+
+## Contributing
We welcome contributions! Here's how:
-1. Fork the repository
-2. Create feature branch: `git checkout -b feature/amazing-feature`
-3. Commit changes: `git commit -m "Add amazing feature"`
-4. Push to branch: `git push origin feature/amazing-feature`
-5. Open Pull Request
+### Contributing to Web Version (This Repo)
-If you support the mission β **please star the repo!**
-[β Star SecureBit.chat on GitHub](https://github.com/SecureBitChat/securebit-chat)
+1. **Fork** the repository
+2. **Create** feature branch: `git checkout -b feature/amazing-feature`
+3. **Commit** changes: `git commit -m "Add amazing feature"`
+4. **Push** to branch: `git push origin feature/amazing-feature`
+5. **Open** Pull Request
+
+### Contributing to Cryptographic Core
+
+Want to improve security? Contribute to the core:
+- **Repository:** [securebit-core](https://github.com/SecureBitChat/securebit-core)
+- **Focus:** Cryptography, protocol implementation, security features
+- **Language:** Rust
+
+### Contributing to Desktop Apps
+
+- **Repository:** [securebit-desktop](https://github.com/SecureBitChat/securebit-desktop)
+- **Focus:** UI/UX improvements, platform-specific features
+- **Technology:** Tauri v2, Rust, TypeScript
+
+### Other Ways to Help
+
+- **Report bugs** - Open issues on GitHub
+- **Security research** - Email SecureBitChat@proton.me
+- **Improve documentation** - Help others understand the project
+- **Star the repositories** - Support visibility and development
+- **Spread the word** - Share with privacy advocates
+- **Provide feedback** - Help shape the future of SecureBit
+
+**If you support our mission - please star the repos!**
+- [β Star securebit-chat (Web)](https://github.com/SecureBitChat/securebit-chat)
+- [β Star securebit-core (Crypto)](https://github.com/SecureBitChat/securebit-core)
+- [β Star securebit-desktop (Apps)](https://github.com/SecureBitChat/securebit-desktop)
+
+---
+
+## π License
+
+### This Repository (Web Version)
+**License:** MIT License
+
+### Cryptographic Core
+**License:** Apache License 2.0
+**Repository:** [securebit-core](https://github.com/SecureBitChat/securebit-core)
+
+### Desktop Applications
+**License:** Proprietary (Free for personal & commercial use)
+**Repository:** [securebit-desktop](https://github.com/SecureBitChat/securebit-desktop)
+
+*Note: Desktop apps are free to use. Only the UI layer is proprietary - all cryptography is open source.*
+
+---
+
+## Community & Support
+
+### Get Help
+- **Documentation:** Check README and [core docs](https://github.com/SecureBitChat/securebit-core)**
+- **Discussions:** [GitHub Discussions](https://github.com/SecureBitChat/securebit-chat/discussions)
+- **Issues:** [Report bugs on GitHub](https://github.com/SecureBitChat/securebit-chat/issues)
+- **Email:** SecureBitChat@proton.me
+
+### Contact
+- **Security Issues:** SecureBitChat@proton.me (encrypted preferred)
+- **Business Inquiries:** hello@securebit.chat
+- **Twitter/X:** [@SecureBitChat](https://twitter.com/SecureBitChat)
+- **Website:** https://securebit.chat (coming soon)
+
+### Community Guidelines
+
+- Be respectful and constructive
+- Focus on privacy and security
+- Help others learn and contribute
+- Report security issues responsibly
+- Follow the code of conduct
+
+---
+
+## Acknowledgments
+
+### Built With
+- **React** - UI framework
+- **Tailwind CSS** - Styling
+- **esbuild** - Build tool
+- **WebRTC** - P2P communication
+- **IndexedDB** - Local storage
+- **Rust** - Cryptographic core
+- **Tauri v2** - Desktop framework
+
+### Special Thanks
+- **Rust Crypto Team** - Cryptographic primitives and standards
+- **WebRTC Community** - P2P technology and standards
+- **Tauri Team** - Desktop framework development
+- **Security Researchers** - Audits, feedback, and improvements
+- **Contributors** - Code, docs, testing, and support
+- **Privacy Advocates** - Inspiration and mission support
+
+### Standards & Specifications
+- **RFC 5869** - HKDF key derivation
+- **NIST SP 800-56A** - ECDH key agreement
+- **RFC 8446** - DTLS 1.3
+- **RFC 7748** - Elliptic curves for security
+- **X.509** - ASN.1 certificate structure
+
+---
+
+## Project Status
+
+### Active Development
+- **Web Version** - Stable (v4.7.53), receiving bug fixes and improvements
+- **Desktop Apps** - Public beta (v0.1.0), active development
+- **Cryptographic Core** - Stable, production-ready
+- **Mobile Apps** - In development (Q1 2026)
+
+### Community
+- **GitHub Stars** - [Help us grow!](https://github.com/SecureBitChat/securebit-chat)
+- **Contributors** - [See all contributors](https://github.com/SecureBitChat/securebit-chat/graphs/contributors)
+- **Issues** - [Open issues](https://github.com/SecureBitChat/securebit-chat/issues)
+- **Pull Requests** - [Contribute](https://github.com/SecureBitChat/securebit-chat/pulls)
+- **Discussions** - [Join the conversation](https://github.com/SecureBitChat/securebit-chat/discussions)
+
+### Metrics
+- **Downloads** - Desktop apps available for all platforms
+- **Security** - 18-layer military-grade protection
+- **Platforms** - Web, Windows, macOS, Linux (Mobile coming Q1 2026)
+- **License** - Open source core, free desktop apps
---
@@ -112,12 +565,18 @@ If you support the mission β **please star the repo!**
*Committed to protecting your privacy with military-grade security*
-**Report vulnerabilities:** SecureBitChat@proton.me
+---
+
+**Latest Release: v4.7.53** - Desktop Edition Available
+**Desktop Apps: v0.1.0** - Public Beta Available
+**Mobile Apps: Coming Q1 2026**
+
+[π Try Web Version](https://securebit.chat/) β’ [π₯οΈ Download Desktop Apps](https://github.com/SecureBitChat/securebit-desktop) β’ [β Star on GitHub](https://github.com/SecureBitChat/securebit-chat)
---
-**Latest Release: v4.5.22** - Browser Notifications & Code Cleanup
+**Made with π for privacy advocates worldwide**
-[π Try Now](https://securebitchat.github.io/securebit-chat/) β’ [β Star on GitHub](https://github.com/SecureBitChat/securebit-chat)
+Copyright Β© 2025-2026 SecureBit Team. All rights reserved.
diff --git a/assets/tailwind.css b/assets/tailwind.css
index f625a98..59e2fae 100644
--- a/assets/tailwind.css
+++ b/assets/tailwind.css
@@ -1 +1 @@
-*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-28{height:7rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-80{height:20rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-\[80vh\]{max-height:80vh}.min-h-\[72px\]{min-height:72px}.min-h-\[calc\(100vh-104px\)\]{min-height:calc(100vh - 104px)}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-28{width:7rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-\[20rem\]{width:20rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/10{border-color:hsla(220,9%,46%,.1)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-green-500\/30{border-color:rgba(34,197,94,.3)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/20{background-color:rgba(59,130,246,.2)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-600\/20{background-color:rgba(75,85,99,.2)}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-400\/20{background-color:rgba(74,222,128,.2)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/20{background-color:rgba(34,197,94,.2)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-green-600\/20{background-color:rgba(22,163,74,.2)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-orange-500\/15{background-color:rgba(249,115,22,.15)}.bg-orange-500\/20{background-color:rgba(249,115,22,.2)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/90{background-color:rgba(239,68,68,.9)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgba(249,115,22,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-zinc-700{--tw-gradient-to:rgba(63,63,70,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#3f3f46 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-2{padding-right:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.lowercase{text-transform:lowercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-600\/30:hover{background-color:rgba(75,85,99,.3)}.hover\:bg-green-500\/20:hover{background-color:rgba(34,197,94,.2)}.hover\:bg-green-500\/30:hover{background-color:rgba(34,197,94,.3)}.hover\:bg-green-600\/30:hover{background-color:rgba(22,163,74,.3)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/20:hover{background-color:rgba(249,115,22,.2)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-purple-500:hover{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.hover\:bg-purple-500\/20:hover{background-color:rgba(168,85,247,.2)}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:from-orange-600:hover{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-orange-700:hover{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-green-500\/40:focus{border-color:rgba(34,197,94,.4)}.focus\:border-orange-500\/40:focus{border-color:rgba(249,115,22,.4)}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:scale-105,.group:hover .group-hover\:scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mb-3{margin-bottom:.75rem}.sm\:mr-2{margin-right:.5rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-12{height:3rem}.sm\:w-10{width:2.5rem}.sm\:w-12{width:3rem}.sm\:w-\[24rem\]{width:24rem}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:p-4{padding:1rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:h-\[32rem\]{height:32rem}.md\:w-\[28rem\]{width:28rem}.md\:w-auto{width:auto}.md\:w-px{width:1px}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:block{display:block}.lg\:w-\[32rem\]{width:32rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
\ No newline at end of file
+*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media (min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media (min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media (min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media (min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media (min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-2{bottom:.5rem}.bottom-4{bottom:1rem}.bottom-6{bottom:1.5rem}.left-0{left:0}.left-1\/2{left:50%}.left-4{left:1rem}.right-0{right:0}.right-3{right:.75rem}.right-4{right:1rem}.right-6{right:1.5rem}.top-0{top:0}.top-1\/2{top:50%}.top-4{top:1rem}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.mb-1{margin-bottom:.25rem}.mb-12{margin-bottom:3rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-2{margin-left:.5rem}.ml-6{margin-left:1.5rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mr-4{margin-right:1rem}.mr-auto{margin-right:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-12{margin-top:3rem}.mt-16{margin-top:4rem}.mt-2{margin-top:.5rem}.mt-20{margin-top:5rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-28{height:7rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-80{height:20rem}.h-96{height:24rem}.h-\[420px\]{height:420px}.h-auto{height:auto}.h-full{height:100%}.h-px{height:1px}.max-h-\[80vh\]{max-height:80vh}.min-h-\[72px\]{min-height:72px}.min-h-\[calc\(100vh-104px\)\]{min-height:calc(100vh - 104px)}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-28{width:7rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-72{width:18rem}.w-8{width:2rem}.w-\[20rem\]{width:20rem}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-\[240px\]{min-width:240px}.max-w-2xl{max-width:42rem}.max-w-3xl{max-width:48rem}.max-w-4xl{max-width:56rem}.max-w-5xl{max-width:64rem}.max-w-6xl{max-width:72rem}.max-w-7xl{max-width:80rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-none{max-width:none}.max-w-sm{max-width:24rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-full{--tw-translate-x:100%}.translate-x-full,.translate-y-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-full{--tw-translate-y:100%}.rotate-180{--tw-rotate:180deg}.rotate-180,.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.items-stretch{align-items:stretch}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-12{gap:3rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(2rem*var(--tw-space-y-reverse))}.self-center{align-self:center}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.scroll-smooth{scroll-behavior:smooth}.whitespace-normal{white-space:normal}.break-words{overflow-wrap:break-word}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-blue-500\/20{border-color:rgba(59,130,246,.2)}.border-gray-500\/10{border-color:hsla(220,9%,46%,.1)}.border-gray-500\/20{border-color:hsla(220,9%,46%,.2)}.border-gray-600{--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity,1))}.border-gray-600\/30{border-color:rgba(75,85,99,.3)}.border-gray-700\/30{border-color:rgba(55,65,81,.3)}.border-green-500\/20{border-color:rgba(34,197,94,.2)}.border-green-500\/30{border-color:rgba(34,197,94,.3)}.border-orange-500\/20{border-color:rgba(249,115,22,.2)}.border-purple-500\/20{border-color:rgba(168,85,247,.2)}.border-red-500\/20{border-color:rgba(239,68,68,.2)}.border-yellow-500\/20{border-color:rgba(234,179,8,.2)}.bg-\[rgb\(20_20_20_\/50\%\)\]{background-color:hsla(0,0%,8%,.5)}.bg-black\/30{background-color:rgba(0,0,0,.3)}.bg-black\/50{background-color:rgba(0,0,0,.5)}.bg-black\/80{background-color:rgba(0,0,0,.8)}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(96 165 250/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-500\/10{background-color:rgba(59,130,246,.1)}.bg-blue-500\/20{background-color:rgba(59,130,246,.2)}.bg-blue-500\/90{background-color:rgba(59,130,246,.9)}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-emerald-500{--tw-bg-opacity:1;background-color:rgb(16 185 129/var(--tw-bg-opacity,1))}.bg-gray-500\/10{background-color:hsla(220,9%,46%,.1)}.bg-gray-500\/20{background-color:hsla(220,9%,46%,.2)}.bg-gray-600{--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity,1))}.bg-gray-600\/20{background-color:rgba(75,85,99,.2)}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.bg-gray-700\/50{background-color:rgba(55,65,81,.5)}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.bg-gray-800\/50{background-color:rgba(31,41,55,.5)}.bg-gray-800\/95{background-color:rgba(31,41,55,.95)}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.bg-gray-900\/30{background-color:rgba(17,24,39,.3)}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(187 247 208/var(--tw-bg-opacity,1))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(74 222 128/var(--tw-bg-opacity,1))}.bg-green-400\/20{background-color:rgba(74,222,128,.2)}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(34 197 94/var(--tw-bg-opacity,1))}.bg-green-500\/10{background-color:rgba(34,197,94,.1)}.bg-green-500\/20{background-color:rgba(34,197,94,.2)}.bg-green-500\/90{background-color:rgba(34,197,94,.9)}.bg-green-600\/20{background-color:rgba(22,163,74,.2)}.bg-neutral-900{--tw-bg-opacity:1;background-color:rgb(23 23 23/var(--tw-bg-opacity,1))}.bg-orange-400{--tw-bg-opacity:1;background-color:rgb(251 146 60/var(--tw-bg-opacity,1))}.bg-orange-500{--tw-bg-opacity:1;background-color:rgb(249 115 22/var(--tw-bg-opacity,1))}.bg-orange-500\/10{background-color:rgba(249,115,22,.1)}.bg-orange-500\/15{background-color:rgba(249,115,22,.15)}.bg-orange-500\/20{background-color:rgba(249,115,22,.2)}.bg-purple-500\/10{background-color:rgba(168,85,247,.1)}.bg-purple-600{--tw-bg-opacity:1;background-color:rgb(147 51 234/var(--tw-bg-opacity,1))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(254 202 202/var(--tw-bg-opacity,1))}.bg-red-400{--tw-bg-opacity:1;background-color:rgb(248 113 113/var(--tw-bg-opacity,1))}.bg-red-500{--tw-bg-opacity:1;background-color:rgb(239 68 68/var(--tw-bg-opacity,1))}.bg-red-500\/10{background-color:rgba(239,68,68,.1)}.bg-red-500\/20{background-color:rgba(239,68,68,.2)}.bg-red-500\/90{background-color:rgba(239,68,68,.9)}.bg-red-900\/50{background-color:rgba(127,29,29,.5)}.bg-transparent{background-color:transparent}.bg-white\/20{background-color:hsla(0,0%,100%,.2)}.bg-yellow-400{--tw-bg-opacity:1;background-color:rgb(250 204 21/var(--tw-bg-opacity,1))}.bg-yellow-500{--tw-bg-opacity:1;background-color:rgb(234 179 8/var(--tw-bg-opacity,1))}.bg-yellow-500\/10{background-color:rgba(234,179,8,.1)}.bg-yellow-500\/20{background-color:rgba(234,179,8,.2)}.bg-yellow-500\/90{background-color:rgba(234,179,8,.9)}.bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}.bg-gradient-to-r{background-image:linear-gradient(to right,var(--tw-gradient-stops))}.bg-gradient-to-t{background-image:linear-gradient(to top,var(--tw-gradient-stops))}.from-\[\#1f1f1f\]\/90{--tw-gradient-from:rgba(31,31,31,.9) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,31,31,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-gray-800\/20{--tw-gradient-from:rgba(31,41,55,.2) var(--tw-gradient-from-position);--tw-gradient-to:rgba(31,41,55,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-orange-500{--tw-gradient-from:#f97316 var(--tw-gradient-from-position);--tw-gradient-to:rgba(249,115,22,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.from-transparent{--tw-gradient-from:transparent var(--tw-gradient-from-position);--tw-gradient-to:transparent var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.via-zinc-700{--tw-gradient-to:rgba(63,63,70,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),#3f3f46 var(--tw-gradient-via-position),var(--tw-gradient-to)}.to-gray-900\/20{--tw-gradient-to:rgba(17,24,39,.2) var(--tw-gradient-to-position)}.to-orange-600{--tw-gradient-to:#ea580c var(--tw-gradient-to-position)}.to-transparent{--tw-gradient-to:transparent var(--tw-gradient-to-position)}.fill-blue-500{fill:#3b82f6}.fill-white{fill:#fff}.p-1\.5{padding:.375rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-14{padding-top:3.5rem;padding-bottom:3.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pr-2{padding-right:.5rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-\[3rem\]{font-size:3rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.lowercase{text-transform:lowercase}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-200{--tw-text-opacity:1;color:rgb(191 219 254/var(--tw-text-opacity,1))}.text-blue-300{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity,1))}.text-cyan-300{--tw-text-opacity:1;color:rgb(103 232 249/var(--tw-text-opacity,1))}.text-cyan-400{--tw-text-opacity:1;color:rgb(34 211 238/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-200{--tw-text-opacity:1;color:rgb(187 247 208/var(--tw-text-opacity,1))}.text-green-300{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-green-500{--tw-text-opacity:1;color:rgb(34 197 94/var(--tw-text-opacity,1))}.text-orange-200{--tw-text-opacity:1;color:rgb(254 215 170/var(--tw-text-opacity,1))}.text-orange-300{--tw-text-opacity:1;color:rgb(253 186 116/var(--tw-text-opacity,1))}.text-orange-400{--tw-text-opacity:1;color:rgb(251 146 60/var(--tw-text-opacity,1))}.text-orange-500{--tw-text-opacity:1;color:rgb(249 115 22/var(--tw-text-opacity,1))}.text-purple-400{--tw-text-opacity:1;color:rgb(192 132 252/var(--tw-text-opacity,1))}.text-purple-500{--tw-text-opacity:1;color:rgb(168 85 247/var(--tw-text-opacity,1))}.text-red-200{--tw-text-opacity:1;color:rgb(254 202 202/var(--tw-text-opacity,1))}.text-red-300{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-red-500{--tw-text-opacity:1;color:rgb(239 68 68/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-white\/10{color:hsla(0,0%,100%,.1)}.text-yellow-200{--tw-text-opacity:1;color:rgb(254 240 138/var(--tw-text-opacity,1))}.text-yellow-300{--tw-text-opacity:1;color:rgb(253 224 71/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.text-yellow-600{--tw-text-opacity:1;color:rgb(202 138 4/var(--tw-text-opacity,1))}.text-zinc-600{--tw-text-opacity:1;color:rgb(82 82 91/var(--tw-text-opacity,1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity:1;color:rgb(107 114 128/var(--tw-placeholder-opacity,1))}.opacity-0{opacity:0}.opacity-50{opacity:.5}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-90{opacity:.9}.shadow-2xl{--tw-shadow:0 25px 50px -12px rgba(0,0,0,.25);--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-2xl,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px rgba(0,0,0,.1),0 4px 6px -4px rgba(0,0,0,.1);--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.ring-2{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur-md{--tw-backdrop-blur:blur(12px)}.backdrop-blur-md,.backdrop-blur-sm{-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.backdrop-blur-sm{--tw-backdrop-blur:blur(4px)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.\[checksum\:4\]{checksum:4}.\[data\:variable\]{data:variable}.\[name\:4\]{name:4}.\[size\:4\]{size:4}.hover\:bg-\[rgb\(20_20_20_\/30\%\)\]:hover{background-color:hsla(0,0%,8%,.3)}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-emerald-600:hover{--tw-bg-opacity:1;background-color:rgb(5 150 105/var(--tw-bg-opacity,1))}.hover\:bg-gray-500:hover{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity,1))}.hover\:bg-gray-600\/30:hover{background-color:rgba(75,85,99,.3)}.hover\:bg-green-500\/20:hover{background-color:rgba(34,197,94,.2)}.hover\:bg-green-500\/30:hover{background-color:rgba(34,197,94,.3)}.hover\:bg-green-600\/30:hover{background-color:rgba(22,163,74,.3)}.hover\:bg-green-600\/40:hover{background-color:rgba(22,163,74,.4)}.hover\:bg-orange-500\/20:hover{background-color:rgba(249,115,22,.2)}.hover\:bg-orange-500\/40:hover{background-color:rgba(249,115,22,.4)}.hover\:bg-orange-600:hover{--tw-bg-opacity:1;background-color:rgb(234 88 12/var(--tw-bg-opacity,1))}.hover\:bg-purple-500:hover{--tw-bg-opacity:1;background-color:rgb(168 85 247/var(--tw-bg-opacity,1))}.hover\:bg-purple-500\/20:hover{background-color:rgba(168,85,247,.2)}.hover\:bg-red-500\/20:hover{background-color:rgba(239,68,68,.2)}.hover\:bg-red-500\/30:hover{background-color:rgba(239,68,68,.3)}.hover\:bg-red-600:hover{--tw-bg-opacity:1;background-color:rgb(220 38 38/var(--tw-bg-opacity,1))}.hover\:bg-red-600\/40:hover{background-color:rgba(220,38,38,.4)}.hover\:bg-white\/30:hover{background-color:hsla(0,0%,100%,.3)}.hover\:bg-yellow-600\/40:hover{background-color:rgba(202,138,4,.4)}.hover\:from-orange-600:hover{--tw-gradient-from:#ea580c var(--tw-gradient-from-position);--tw-gradient-to:rgba(234,88,12,0) var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.hover\:to-orange-700:hover{--tw-gradient-to:#c2410c var(--tw-gradient-to-position)}.hover\:text-gray-300:hover{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity,1))}.hover\:text-green-300:hover{--tw-text-opacity:1;color:rgb(134 239 172/var(--tw-text-opacity,1))}.hover\:text-red-300:hover{--tw-text-opacity:1;color:rgb(252 165 165/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.hover\:opacity-80:hover{opacity:.8}.focus\:border-green-500\/40:focus{border-color:rgba(34,197,94,.4)}.focus\:border-orange-500\/40:focus{border-color:rgba(249,115,22,.4)}.focus\:border-purple-500\/40:focus{border-color:rgba(168,85,247,.4)}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:scale-105{--tw-scale-x:1.05;--tw-scale-y:1.05}.group:hover .group-hover\:scale-105,.group:hover .group-hover\:scale-110{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:mb-0{margin-bottom:0}.sm\:mb-3{margin-bottom:.75rem}.sm\:mr-2{margin-right:.5rem}.sm\:block{display:block}.sm\:inline{display:inline}.sm\:flex{display:flex}.sm\:h-10{height:2.5rem}.sm\:h-12{height:3rem}.sm\:w-10{width:2.5rem}.sm\:w-12{width:3rem}.sm\:w-\[24rem\]{width:24rem}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}.sm\:gap-4{gap:1rem}.sm\:space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.sm\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.sm\:p-4{padding:1rem}.sm\:px-0{padding-left:0;padding-right:0}.sm\:px-3{padding-left:.75rem;padding-right:.75rem}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.sm\:text-3xl{font-size:1.875rem;line-height:2.25rem}.sm\:text-base{font-size:1rem;line-height:1.5rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-32{height:8rem}.md\:h-\[32rem\]{height:32rem}.md\:w-\[28rem\]{width:28rem}.md\:w-auto{width:auto}.md\:w-px{width:1px}.md\:flex-none{flex:none}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:flex-col{flex-direction:column}.md\:bg-gradient-to-b{background-image:linear-gradient(to bottom,var(--tw-gradient-stops))}}@media (min-width:1024px){.lg\:col-span-2{grid-column:span 2/span 2}.lg\:col-span-3{grid-column:span 3/span 3}.lg\:block{display:block}.lg\:w-\[32rem\]{width:32rem}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}
\ No newline at end of file
diff --git a/dist/app-boot.js b/dist/app-boot.js
index ec616dd..7de930c 100644
--- a/dist/app-boot.js
+++ b/dist/app-boot.js
@@ -449,6 +449,9 @@ var require_SecureNotificationManager = __commonJS({
nameEl.textContent = message.senderName + ": ";
const textEl = document.createElement("span");
textEl.textContent = message.text;
+ textEl.style.wordWrap = "break-word";
+ textEl.style.overflowWrap = "break-word";
+ textEl.style.whiteSpace = "normal";
const timeEl = document.createElement("small");
timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();
messageEl.appendChild(nameEl);
@@ -4533,7 +4536,7 @@ var EnhancedSecureWebRTCManager = class _EnhancedSecureWebRTCManager {
SYSTEM_MESSAGE: "SYSTEM_MESSAGE_FILTERED"
};
// Static debug flag instead of this._debugMode
- static DEBUG_MODE = false;
+ static DEBUG_MODE = true;
// Set to true during development, false in production
constructor(onMessage, onStatusChange, onKeyExchange, onVerificationRequired, onAnswerError = null, onVerificationStateChange = null, config = {}) {
this._isProductionMode = this._detectProductionMode();
@@ -15311,7 +15314,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("p", {
key: "subtitle",
className: "text-xs sm:text-sm text-muted hidden sm:block"
- }, "End-to-end freedom v4.5.22")
+ }, "End-to-end freedom v4.7.53")
])
]),
// Status and Controls - Responsive
@@ -15429,9 +15432,9 @@ window.EnhancedMinimalHeader = EnhancedMinimalHeader;
var DownloadApps = () => {
const apps = [
{ id: "web", name: "Web App", subtitle: "Browser Version", icon: "fas fa-globe", platform: "Web", isActive: true, url: "https://securebit.chat/", color: "green" },
- { id: "windows", name: "Windows", subtitle: "Desktop App", icon: "fab fa-windows", platform: "Desktop", isActive: false, url: "https://securebit.chat/download/windows/SecureBit%20Chat%20Setup%204.1.222.exe", color: "blue" },
- { id: "macos", name: "macOS", subtitle: "Desktop App", icon: "fab fa-safari", platform: "Desktop", isActive: false, url: "#", color: "gray" },
- { id: "linux", name: "Linux", subtitle: "Desktop App", icon: "fab fa-linux", platform: "Desktop", isActive: false, url: "#", color: "orange" },
+ { id: "windows", name: "Windows", subtitle: "Desktop App", icon: "fab fa-windows", platform: "Desktop", isActive: true, url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe", color: "blue" },
+ { id: "macos", name: "macOS", subtitle: "Desktop App", icon: "fab fa-safari", platform: "Desktop", isActive: true, url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBitChat.zip", color: "gray" },
+ { id: "linux", name: "Linux", subtitle: "Desktop App", icon: "fab fa-linux", platform: "Desktop", isActive: true, url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage", color: "orange" },
{ id: "ios", name: "iOS", subtitle: "iPhone & iPad", icon: "fab fa-apple", platform: "Mobile", isActive: false, url: "https://apps.apple.com/app/securebit-chat/", color: "white" },
{ id: "android", name: "Android", subtitle: "Google Play", icon: "fab fa-android", platform: "Mobile", isActive: false, url: "https://play.google.com/store/apps/details?id=com.securebit.chat", color: "green" },
{ id: "chrome", name: "Chrome", subtitle: "Browser Extension", icon: "fab fa-chrome", platform: "Browser", isActive: false, url: "#", color: "yellow" },
@@ -16066,11 +16069,11 @@ function Roadmap() {
},
// current and future phases
{
- version: "v4.5.22",
+ version: "v4.5",
title: "Enhanced Security Edition",
- status: "current",
+ status: "done",
date: "Now",
- description: "Current version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
+ description: "Version with ECDH + DTLS + SAS security, 18-layer military-grade cryptography and complete ASN.1 validation",
features: [
"ECDH + DTLS + SAS triple-layer security",
"ECDH P-384 + AES-GCM 256-bit encryption",
@@ -16087,17 +16090,17 @@ function Roadmap() {
]
},
{
- version: "v4.5",
- title: "Mobile & Desktop Edition",
- status: "development",
- date: "Q2 2025",
- description: "Native apps for all platforms",
+ version: "v4.7",
+ title: "Desktop Edition",
+ status: "current",
+ date: "Now",
+ description: "Native desktop applications for Windows, macOS, and Linux",
features: [
- "PWA app for mobile",
- "Electron app for desktop",
+ "Windows desktop app (Tauri v2)",
+ "macOS desktop app (Tauri v2)",
+ "Linux AppImage support (Tauri v2)",
"Real-time notifications",
"Automatic reconnection",
- "Battery optimization",
"Cross-device synchronization",
"Improved UX/UI",
"Support for files up to 100MB"
@@ -16105,9 +16108,26 @@ function Roadmap() {
},
{
version: "v5.0",
+ title: "Mobile Edition",
+ status: "development",
+ date: "Q1 2026",
+ description: "Native mobile applications for iOS and Android",
+ features: [
+ "iOS native app (Swift/SwiftUI)",
+ "Android native app (Kotlin/Jetpack Compose)",
+ "PWA support for mobile browsers",
+ "Real-time push notifications",
+ "Battery optimization",
+ "Mobile-optimized UX/UI",
+ "Offline message queuing",
+ "Biometric authentication"
+ ]
+ },
+ {
+ version: "v5.5",
title: "Quantum-Resistant Edition",
status: "planned",
- date: "Q4 2025",
+ date: "Q2 2026",
description: "Protection against quantum computers",
features: [
"Post-quantum cryptography CRYSTALS-Kyber",
@@ -16121,10 +16141,10 @@ function Roadmap() {
]
},
{
- version: "v5.5",
+ version: "v6.0",
title: "Group Communications",
status: "planned",
- date: "Q2 2026",
+ date: "Q4 2026",
description: "Group chats with preserved privacy",
features: [
"P2P group connections up to 8 participants",
@@ -16137,7 +16157,7 @@ function Roadmap() {
]
},
{
- version: "v6.0",
+ version: "v6.5",
title: "Decentralized Network",
status: "research",
date: "2027",
diff --git a/dist/app-boot.js.map b/dist/app-boot.js.map
index 72c0f70..c8d17c2 100644
--- a/dist/app-boot.js.map
+++ b/dist/app-boot.js.map
@@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../src/notifications/SecureNotificationManager.js", "../src/notifications/NotificationIntegration.js", "../src/crypto/EnhancedSecureCryptoUtils.js", "../src/transfer/EnhancedSecureFileTransfer.js", "../src/network/EnhancedSecureWebRTCManager.js", "../src/scripts/app-boot.js", "../src/components/ui/Header.jsx", "../src/components/ui/DownloadApps.jsx", "../src/components/ui/UniqueFeatureSlider.jsx", "../src/components/ui/SecurityFeatures.jsx", "../src/components/ui/Testimonials.jsx", "../src/components/ui/ComparisonTable.jsx", "../src/components/ui/Roadmap.jsx", "../src/components/ui/FileTransfer.jsx"],
- "sourcesContent": ["/**\n * Secure and Reliable Notification Manager for P2P WebRTC Chat\n * Follows best practices: OWASP, MDN, Chrome DevRel\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nclass SecureChatNotificationManager {\n constructor(config = {}) {\n // Safely read Notification permission (iOS Safari may not define Notification)\n this.permission = (typeof Notification !== 'undefined' && Notification && typeof Notification.permission === 'string')\n ? Notification.permission\n : 'denied';\n this.isTabActive = this.checkTabActive(); // Initialize with proper check\n this.unreadCount = 0;\n this.originalTitle = document.title;\n this.notificationQueue = [];\n this.maxQueueSize = config.maxQueueSize || 5;\n this.rateLimitMs = config.rateLimitMs || 2000; // Spam protection\n this.lastNotificationTime = 0;\n this.trustedOrigins = config.trustedOrigins || [];\n \n // Secure context flag\n this.isSecureContext = window.isSecureContext;\n \n // Cross-browser compatibility for Page Visibility API\n this.hidden = this.getHiddenProperty();\n this.visibilityChange = this.getVisibilityChangeEvent();\n \n this.initVisibilityTracking();\n this.initSecurityChecks();\n }\n\n /**\n * Initialize security checks and validation\n * @private\n */\n initSecurityChecks() {\n // Security checks are performed silently\n }\n\n /**\n * Get hidden property name for cross-browser compatibility\n * @returns {string} Hidden property name\n * @private\n */\n getHiddenProperty() {\n if (typeof document.hidden !== \"undefined\") {\n return \"hidden\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msHidden\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitHidden\";\n }\n return \"hidden\"; // fallback\n }\n\n /**\n * Get visibility change event name for cross-browser compatibility\n * @returns {string} Visibility change event name\n * @private\n */\n getVisibilityChangeEvent() {\n if (typeof document.hidden !== \"undefined\") {\n return \"visibilitychange\";\n } else if (typeof document.msHidden !== \"undefined\") {\n return \"msvisibilitychange\";\n } else if (typeof document.webkitHidden !== \"undefined\") {\n return \"webkitvisibilitychange\";\n }\n return \"visibilitychange\"; // fallback\n }\n\n /**\n * Check if tab is currently active using multiple methods\n * @returns {boolean} True if tab is active\n * @private\n */\n checkTabActive() {\n // Primary method: Page Visibility API\n if (this.hidden && typeof document[this.hidden] !== \"undefined\") {\n return !document[this.hidden];\n }\n \n // Fallback method: document.hasFocus()\n if (typeof document.hasFocus === \"function\") {\n return document.hasFocus();\n }\n \n // Ultimate fallback: assume active\n return true;\n }\n\n /**\n * Initialize page visibility tracking (Page Visibility API)\n * @private\n */\n initVisibilityTracking() {\n // Primary method: Page Visibility API with cross-browser support\n if (typeof document.addEventListener !== \"undefined\" && typeof document[this.hidden] !== \"undefined\") {\n document.addEventListener(this.visibilityChange, () => {\n this.isTabActive = this.checkTabActive();\n \n if (this.isTabActive) {\n this.resetUnreadCount();\n this.clearNotificationQueue();\n }\n });\n }\n\n // Fallback method: Window focus/blur events\n window.addEventListener('focus', () => {\n this.isTabActive = this.checkTabActive();\n if (this.isTabActive) {\n this.resetUnreadCount();\n }\n });\n\n window.addEventListener('blur', () => {\n this.isTabActive = this.checkTabActive();\n });\n\n // Page unload cleanup\n window.addEventListener('beforeunload', () => {\n this.clearNotificationQueue();\n });\n }\n\n /**\n * Request notification permission (BEST PRACTICE: Only call in response to user action)\n * Never call on page load!\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n // Secure context check\n if (!this.isSecureContext || !('Notification' in window)) {\n return false;\n }\n\n if (this.permission === 'granted') {\n return true;\n }\n\n if (this.permission === 'denied') {\n return false;\n }\n\n try {\n this.permission = await Notification.requestPermission();\n return this.permission === 'granted';\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Update page title with unread count\n * @private\n */\n updateTitle() {\n if (this.unreadCount > 0) {\n document.title = `(${this.unreadCount}) ${this.originalTitle}`;\n } else {\n document.title = this.originalTitle;\n }\n }\n\n /**\n * XSS Protection: Sanitize input text\n * @param {string} text - Text to sanitize\n * @returns {string} Sanitized text\n * @private\n */\n sanitizeText(text) {\n if (typeof text !== 'string') {\n return '';\n }\n \n // Remove HTML tags and potentially dangerous characters\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML\n .replace(//g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n .substring(0, 500); // Length limit\n }\n\n /**\n * Validate icon URL (XSS protection)\n * @param {string} url - URL to validate\n * @returns {string|null} Validated URL or null\n * @private\n */\n validateIconUrl(url) {\n if (!url) return null;\n \n try {\n const parsedUrl = new URL(url, window.location.origin);\n \n // Only allow HTTPS and data URLs\n if (parsedUrl.protocol === 'https:' || parsedUrl.protocol === 'data:') {\n // Check trusted origins if specified\n if (this.trustedOrigins.length > 0) {\n const isTrusted = this.trustedOrigins.some(origin => \n parsedUrl.origin === origin\n );\n return isTrusted ? parsedUrl.href : null;\n }\n return parsedUrl.href;\n }\n \n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Rate limiting for spam protection\n * @returns {boolean} Rate limit check passed\n * @private\n */\n checkRateLimit() {\n const now = Date.now();\n if (now - this.lastNotificationTime < this.rateLimitMs) {\n return false;\n }\n this.lastNotificationTime = now;\n return true;\n }\n\n /**\n * Send secure notification\n * @param {string} senderName - Name of message sender\n * @param {string} message - Message content\n * @param {Object} options - Notification options\n * @returns {Notification|null} Created notification or null\n */\n notify(senderName, message, options = {}) {\n // Abort if Notifications API is not available (e.g., iOS Safari)\n if (typeof Notification === 'undefined') {\n return null;\n }\n // Update tab active state before checking\n this.isTabActive = this.checkTabActive();\n \n // Only show if tab is NOT active (user is on another tab or minimized)\n if (this.isTabActive) {\n return null;\n }\n\n // Permission check\n if (this.permission !== 'granted') {\n return null;\n }\n\n // Rate limiting\n if (!this.checkRateLimit()) {\n return null;\n }\n\n // Data sanitization (XSS Protection)\n const safeSenderName = this.sanitizeText(senderName || 'Unknown');\n const safeMessage = this.sanitizeText(message || '');\n const safeIcon = this.validateIconUrl(options.icon) || '/logo/icon-192x192.png';\n\n // Queue overflow protection\n if (this.notificationQueue.length >= this.maxQueueSize) {\n this.clearNotificationQueue();\n }\n\n try {\n \n const notification = new Notification(\n `${safeSenderName}`,\n {\n body: safeMessage.substring(0, 200), // Length limit\n icon: safeIcon,\n badge: safeIcon,\n tag: `chat-${options.senderId || 'unknown'}`, // Grouping\n requireInteraction: false, // Don't block user\n silent: options.silent || false,\n // Vibrate only for mobile and if supported\n vibrate: navigator.vibrate ? [200, 100, 200] : undefined,\n // Safe metadata\n data: {\n senderId: this.sanitizeText(options.senderId),\n timestamp: Date.now(),\n // Don't include sensitive data!\n }\n }\n );\n\n // Increment counter\n this.unreadCount++;\n this.updateTitle();\n\n // Add to queue for management\n this.notificationQueue.push(notification);\n\n // Safe click handler\n notification.onclick = (event) => {\n event.preventDefault(); // Prevent default behavior\n window.focus();\n notification.close();\n \n // Safe callback\n if (typeof options.onClick === 'function') {\n try {\n options.onClick(options.senderId);\n } catch (error) {\n console.error('[Notifications] Error in onClick handler:', error);\n }\n }\n };\n\n // Error handler\n notification.onerror = (event) => {\n console.error('[Notifications] Error showing notification:', event);\n };\n\n // Auto-close after reasonable time\n const autoCloseTimeout = Math.min(options.autoClose || 5000, 10000);\n setTimeout(() => {\n notification.close();\n this.removeFromQueue(notification);\n }, autoCloseTimeout);\n\n return notification;\n \n } catch (error) {\n console.error('[Notifications] Failed to create notification:', error);\n return null;\n }\n }\n\n /**\n * Remove notification from queue\n * @param {Notification} notification - Notification to remove\n * @private\n */\n removeFromQueue(notification) {\n const index = this.notificationQueue.indexOf(notification);\n if (index > -1) {\n this.notificationQueue.splice(index, 1);\n }\n }\n\n /**\n * Clear all notifications\n */\n clearNotificationQueue() {\n this.notificationQueue.forEach(notification => {\n try {\n notification.close();\n } catch (error) {\n // Ignore errors when closing\n }\n });\n this.notificationQueue = [];\n }\n\n /**\n * Reset unread counter\n */\n resetUnreadCount() {\n this.unreadCount = 0;\n this.updateTitle();\n }\n\n /**\n * Get current status\n * @returns {Object} Current notification status\n */\n getStatus() {\n return {\n permission: this.permission,\n isTabActive: this.isTabActive,\n unreadCount: this.unreadCount,\n isSecureContext: this.isSecureContext,\n queueSize: this.notificationQueue.length\n };\n }\n}\n\n/**\n * Secure integration with WebRTC\n */\nclass SecureP2PChat {\n constructor() {\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 5,\n rateLimitMs: 2000,\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.dataChannel = null;\n this.peerConnection = null;\n this.remotePeerName = 'Peer';\n this.messageHistory = [];\n this.maxHistorySize = 100;\n }\n\n /**\n * Initialize when user connects\n */\n async init() {\n // Initialize notification manager silently\n }\n\n /**\n * Method for manual permission request (called on click)\n * @returns {Promise} Permission granted status\n */\n async enableNotifications() {\n const granted = await this.notificationManager.requestPermission();\n return granted;\n }\n\n /**\n * Setup DataChannel with security checks\n * @param {RTCDataChannel} dataChannel - WebRTC data channel\n */\n setupDataChannel(dataChannel) {\n if (!dataChannel) {\n console.error('[Chat] Invalid DataChannel');\n return;\n }\n\n this.dataChannel = dataChannel;\n \n // Setup handlers\n this.dataChannel.onmessage = (event) => {\n this.handleIncomingMessage(event.data);\n };\n\n this.dataChannel.onerror = (error) => {\n // Handle error silently\n };\n }\n\n /**\n * XSS Protection: Validate incoming messages\n * @param {string|Object} data - Message data\n * @returns {Object|null} Validated message or null\n * @private\n */\n validateMessage(data) {\n try {\n const message = typeof data === 'string' ? JSON.parse(data) : data;\n \n // Check message structure\n if (!message || typeof message !== 'object') {\n throw new Error('Invalid message structure');\n }\n\n // Check required fields\n if (!message.text || typeof message.text !== 'string') {\n throw new Error('Invalid message text');\n }\n\n // Message length limit (DoS protection)\n if (message.text.length > 10000) {\n throw new Error('Message too long');\n }\n\n return {\n text: message.text,\n senderName: message.senderName || 'Unknown',\n senderId: message.senderId || 'unknown',\n timestamp: message.timestamp || Date.now(),\n senderAvatar: message.senderAvatar || null\n };\n \n } catch (error) {\n console.error('[Chat] Message validation failed:', error);\n return null;\n }\n }\n\n /**\n * Secure handling of incoming messages\n * @param {string|Object} data - Message data\n * @private\n */\n handleIncomingMessage(data) {\n const message = this.validateMessage(data);\n \n if (!message) {\n return;\n }\n\n // Save to history (with limit)\n this.messageHistory.push(message);\n if (this.messageHistory.length > this.maxHistorySize) {\n this.messageHistory.shift();\n }\n\n // Display in UI (with sanitization)\n this.displayMessage(message);\n\n // Send notification only if tab is inactive\n this.notificationManager.notify(\n message.senderName,\n message.text,\n {\n icon: message.senderAvatar,\n senderId: message.senderId,\n onClick: (senderId) => {\n this.scrollToLatestMessage();\n }\n }\n );\n\n // Optional: sound (with check)\n if (!this.notificationManager.isTabActive) {\n this.playNotificationSound();\n }\n }\n\n /**\n * XSS Protection: Safe message display\n * @param {Object} message - Message to display\n * @private\n */\n displayMessage(message) {\n const container = document.getElementById('messages');\n if (!container) {\n return;\n }\n\n const messageEl = document.createElement('div');\n messageEl.className = 'message';\n \n // Use textContent to prevent XSS\n const nameEl = document.createElement('strong');\n nameEl.textContent = message.senderName + ': ';\n \n const textEl = document.createElement('span');\n textEl.textContent = message.text;\n \n const timeEl = document.createElement('small');\n timeEl.textContent = new Date(message.timestamp).toLocaleTimeString();\n \n messageEl.appendChild(nameEl);\n messageEl.appendChild(textEl);\n messageEl.appendChild(document.createElement('br'));\n messageEl.appendChild(timeEl);\n \n container.appendChild(messageEl);\n this.scrollToLatestMessage();\n }\n\n /**\n * Safe sound playback\n * @private\n */\n playNotificationSound() {\n try {\n // Use only local audio files\n const audio = new Audio('/assets/audio/notification.mp3');\n audio.volume = 0.3; // Moderate volume\n \n // Error handling\n audio.play().catch(error => {\n // Handle audio error silently\n });\n } catch (error) {\n // Handle audio creation error silently\n }\n }\n\n /**\n * Scroll to latest message\n * @private\n */\n scrollToLatestMessage() {\n const container = document.getElementById('messages');\n if (container) {\n container.scrollTop = container.scrollHeight;\n }\n }\n\n /**\n * Get status\n * @returns {Object} Current chat status\n */\n getStatus() {\n return {\n notifications: this.notificationManager.getStatus(),\n messageCount: this.messageHistory.length,\n connected: this.dataChannel?.readyState === 'open'\n };\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { SecureChatNotificationManager, SecureP2PChat };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.SecureChatNotificationManager = SecureChatNotificationManager;\n window.SecureP2PChat = SecureP2PChat;\n}\n", "/**\n * Notification Integration Module for SecureBit WebRTC Chat\n * Integrates secure notifications with existing WebRTC architecture\n * \n * @version 1.0.0\n * @author SecureBit Team\n * @license MIT\n */\n\nimport { SecureChatNotificationManager } from './SecureNotificationManager.js';\n\nclass NotificationIntegration {\n constructor(webrtcManager) {\n this.webrtcManager = webrtcManager;\n this.notificationManager = new SecureChatNotificationManager({\n maxQueueSize: 10,\n rateLimitMs: 1000, // Reduced from 2000ms to 1000ms\n trustedOrigins: [\n window.location.origin,\n // Add other trusted origins for CDN icons\n ]\n });\n \n this.isInitialized = false;\n this.originalOnMessage = null;\n this.originalOnStatusChange = null;\n this.processedMessages = new Set(); // Track processed messages to avoid duplicates\n }\n\n /**\n * Initialize notification integration\n * @returns {Promise} Initialization success\n */\n async init() {\n try {\n if (this.isInitialized) {\n return true;\n }\n\n // Store original callbacks\n this.originalOnMessage = this.webrtcManager.onMessage;\n this.originalOnStatusChange = this.webrtcManager.onStatusChange;\n\n\n // Wrap the original onMessage callback\n this.webrtcManager.onMessage = (message, type) => {\n this.handleIncomingMessage(message, type);\n \n // Call original callback if it exists\n if (this.originalOnMessage) {\n this.originalOnMessage(message, type);\n }\n };\n\n // Wrap the original onStatusChange callback\n this.webrtcManager.onStatusChange = (status) => {\n this.handleStatusChange(status);\n \n // Call original callback if it exists\n if (this.originalOnStatusChange) {\n this.originalOnStatusChange(status);\n }\n };\n\n // Also hook into the deliverMessageToUI method if it exists\n if (this.webrtcManager.deliverMessageToUI) {\n this.originalDeliverMessageToUI = this.webrtcManager.deliverMessageToUI.bind(this.webrtcManager);\n this.webrtcManager.deliverMessageToUI = (message, type) => {\n this.handleIncomingMessage(message, type);\n this.originalDeliverMessageToUI(message, type);\n };\n }\n\n this.isInitialized = true;\n return true;\n\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Handle incoming messages and trigger notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @private\n */\n handleIncomingMessage(message, type) {\n try {\n // Create a unique key for this message to avoid duplicates\n const messageKey = `${type}:${typeof message === 'string' ? message : JSON.stringify(message)}`;\n \n // Skip if we've already processed this message\n if (this.processedMessages.has(messageKey)) {\n return;\n }\n \n // Mark message as processed\n this.processedMessages.add(messageKey);\n \n // Clean up old processed messages (keep only last 100)\n if (this.processedMessages.size > 100) {\n const messagesArray = Array.from(this.processedMessages);\n this.processedMessages.clear();\n messagesArray.slice(-50).forEach(msg => this.processedMessages.add(msg));\n }\n \n \n // Only process chat messages, not system messages\n if (type === 'system' || type === 'file-transfer' || type === 'heartbeat') {\n return;\n }\n\n // Extract message information\n const messageInfo = this.extractMessageInfo(message, type);\n if (!messageInfo) {\n return;\n }\n\n // Send notification\n const notificationResult = this.notificationManager.notify(\n messageInfo.senderName,\n messageInfo.text,\n {\n icon: messageInfo.senderAvatar,\n senderId: messageInfo.senderId,\n onClick: (senderId) => {\n this.focusChatWindow();\n }\n }\n );\n\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Handle status changes\n * @param {string} status - Connection status\n * @private\n */\n handleStatusChange(status) {\n try {\n // Clear notifications when connection is lost\n if (status === 'disconnected' || status === 'failed') {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Extract message information for notifications\n * @param {*} message - Message content\n * @param {string} type - Message type\n * @returns {Object|null} Extracted message info or null\n * @private\n */\n extractMessageInfo(message, type) {\n try {\n let messageData = message;\n\n // Handle different message formats\n if (typeof message === 'string') {\n try {\n messageData = JSON.parse(message);\n } catch (e) {\n // Plain text message\n return {\n senderName: 'Peer',\n text: message,\n senderId: 'peer',\n senderAvatar: null\n };\n }\n }\n\n // Handle structured message data\n if (typeof messageData === 'object' && messageData !== null) {\n return {\n senderName: messageData.senderName || messageData.name || 'Peer',\n text: messageData.text || messageData.message || messageData.content || '',\n senderId: messageData.senderId || messageData.id || 'peer',\n senderAvatar: messageData.senderAvatar || messageData.avatar || null\n };\n }\n\n return null;\n } catch (error) {\n return null;\n }\n }\n\n /**\n * Focus chat window when notification is clicked\n * @private\n */\n focusChatWindow() {\n try {\n window.focus();\n \n // Scroll to bottom of messages if container exists\n const messagesContainer = document.getElementById('messages');\n if (messagesContainer) {\n messagesContainer.scrollTop = messagesContainer.scrollHeight;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n\n /**\n * Request notification permission\n * @returns {Promise} Permission granted status\n */\n async requestPermission() {\n try {\n return await this.notificationManager.requestPermission();\n } catch (error) {\n return false;\n }\n }\n\n /**\n * Get notification status\n * @returns {Object} Notification status\n */\n getStatus() {\n return this.notificationManager.getStatus();\n }\n\n /**\n * Clear all notifications\n */\n clearNotifications() {\n this.notificationManager.clearNotificationQueue();\n this.notificationManager.resetUnreadCount();\n }\n\n /**\n * Cleanup integration\n */\n cleanup() {\n try {\n if (this.isInitialized) {\n // Restore original callbacks\n if (this.originalOnMessage) {\n this.webrtcManager.onMessage = this.originalOnMessage;\n }\n if (this.originalOnStatusChange) {\n this.webrtcManager.onStatusChange = this.originalOnStatusChange;\n }\n if (this.originalDeliverMessageToUI) {\n this.webrtcManager.deliverMessageToUI = this.originalDeliverMessageToUI;\n }\n\n // Clear notifications\n this.clearNotifications();\n\n this.isInitialized = false;\n }\n } catch (error) {\n // Handle error silently\n }\n }\n}\n\n// Export for use in other modules\nif (typeof module !== 'undefined' && module.exports) {\n module.exports = { NotificationIntegration };\n}\n\n// Global export for browser usage\nif (typeof window !== 'undefined') {\n window.NotificationIntegration = NotificationIntegration;\n}\n", "class EnhancedSecureCryptoUtils {\r\n\r\n static _keyMetadata = new WeakMap();\r\n \r\n // Initialize secure logging system after class definition\r\n\r\n // Utility to sort object keys for deterministic serialization\r\n static sortObjectKeys(obj) {\r\n if (typeof obj !== 'object' || obj === null) {\r\n return obj;\r\n }\r\n\r\n if (Array.isArray(obj)) {\r\n return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys);\r\n }\r\n\r\n const sortedObj = {};\r\n Object.keys(obj).sort().forEach(key => {\r\n sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]);\r\n });\r\n return sortedObj;\r\n }\r\n\r\n // Utility to assert CryptoKey type and properties\r\n static assertCryptoKey(key, expectedName = null, expectedUsages = []) {\r\n if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey');\r\n if (expectedName && key.algorithm?.name !== expectedName) {\r\n throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`);\r\n }\r\n for (const u of expectedUsages) {\r\n if (!key.usages || !key.usages.includes(u)) {\r\n throw new Error(`Missing required key usage: ${u}`);\r\n }\r\n }\r\n }\r\n // Helper function to convert ArrayBuffer to Base64\r\n static arrayBufferToBase64(buffer) {\r\n let binary = '';\r\n const bytes = new Uint8Array(buffer);\r\n const len = bytes.byteLength;\r\n for (let i = 0; i < len; i++) {\r\n binary += String.fromCharCode(bytes[i]);\r\n }\r\n return btoa(binary);\r\n }\r\n\r\n // Helper function to convert Base64 to ArrayBuffer\r\n static base64ToArrayBuffer(base64) {\r\n try {\r\n // Validate input\r\n if (typeof base64 !== 'string' || !base64) {\r\n throw new Error('Invalid base64 input: must be a non-empty string');\r\n }\r\n\r\n // Remove any whitespace and validate base64 format\r\n const cleanBase64 = base64.trim();\r\n if (!/^[A-Za-z0-9+/]*={0,2}$/.test(cleanBase64)) {\r\n throw new Error('Invalid base64 format');\r\n }\r\n\r\n // Handle empty string case\r\n if (cleanBase64 === '') {\r\n return new ArrayBuffer(0);\r\n }\r\n\r\n const binaryString = atob(cleanBase64);\r\n const len = binaryString.length;\r\n const bytes = new Uint8Array(len);\r\n for (let i = 0; i < len; i++) {\r\n bytes[i] = binaryString.charCodeAt(i);\r\n }\r\n return bytes.buffer;\r\n } catch (error) {\r\n console.error('Base64 to ArrayBuffer conversion failed:', error.message);\r\n throw new Error(`Base64 conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n // Helper function to convert hex string to Uint8Array\r\n static hexToUint8Array(hexString) {\r\n try {\r\n if (!hexString || typeof hexString !== 'string') {\r\n throw new Error('Invalid hex string input: must be a non-empty string');\r\n }\r\n\r\n // Remove colons and spaces from hex string (e.g., \"aa:bb:cc\" -> \"aabbcc\")\r\n const cleanHex = hexString.replace(/:/g, '').replace(/\\s/g, '');\r\n \r\n // Validate hex format\r\n if (!/^[0-9a-fA-F]*$/.test(cleanHex)) {\r\n throw new Error('Invalid hex format: contains non-hex characters');\r\n }\r\n \r\n // Ensure even length\r\n if (cleanHex.length % 2 !== 0) {\r\n throw new Error('Invalid hex format: odd length');\r\n }\r\n\r\n // Convert hex string to bytes\r\n const bytes = new Uint8Array(cleanHex.length / 2);\r\n for (let i = 0; i < cleanHex.length; i += 2) {\r\n bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);\r\n }\r\n \r\n return bytes;\r\n } catch (error) {\r\n console.error('Hex to Uint8Array conversion failed:', error.message);\r\n throw new Error(`Hex conversion error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async encryptData(data, password) {\r\n try {\r\n const dataString = typeof data === 'string' ? data : JSON.stringify(data);\r\n const salt = crypto.getRandomValues(new Uint8Array(16));\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n const key = await crypto.subtle.deriveKey(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt,\r\n iterations: 310000,\r\n hash: 'SHA-256',\r\n },\r\n keyMaterial,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['encrypt']\r\n );\r\n\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n const dataBuffer = encoder.encode(dataString);\r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: iv },\r\n key,\r\n dataBuffer\r\n );\r\n\r\n const encryptedPackage = {\r\n version: '1.0',\r\n salt: Array.from(salt),\r\n iv: Array.from(iv),\r\n data: Array.from(new Uint8Array(encrypted)),\r\n timestamp: Date.now(),\r\n };\r\n\r\n const packageString = JSON.stringify(encryptedPackage);\r\n return EnhancedSecureCryptoUtils.arrayBufferToBase64(new TextEncoder().encode(packageString).buffer);\r\n\r\n } catch (error) {\r\n console.error('Encryption failed:', error.message);\r\n throw new Error(`Encryption error: ${error.message}`);\r\n }\r\n }\r\n\r\n static async decryptData(encryptedData, password) {\r\n try {\r\n const packageBuffer = EnhancedSecureCryptoUtils.base64ToArrayBuffer(encryptedData);\r\n const packageString = new TextDecoder().decode(packageBuffer);\r\n const encryptedPackage = JSON.parse(packageString);\r\n\r\n if (!encryptedPackage.version || !encryptedPackage.salt || !encryptedPackage.iv || !encryptedPackage.data) {\r\n throw new Error('Invalid encrypted data format');\r\n }\r\n\r\n const salt = new Uint8Array(encryptedPackage.salt);\r\n const iv = new Uint8Array(encryptedPackage.iv);\r\n const encrypted = new Uint8Array(encryptedPackage.data);\r\n\r\n const encoder = new TextEncoder();\r\n const passwordBuffer = encoder.encode(password);\r\n\r\n const keyMaterial = await crypto.subtle.importKey(\r\n 'raw',\r\n passwordBuffer,\r\n { name: 'PBKDF2' },\r\n false,\r\n ['deriveKey']\r\n );\r\n\r\n const key = await crypto.subtle.deriveKey(\r\n {\r\n name: 'PBKDF2',\r\n salt: salt,\r\n iterations: 310000,\r\n hash: 'SHA-256'\r\n },\r\n keyMaterial,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['decrypt']\r\n );\r\n\r\n const decrypted = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n key,\r\n encrypted\r\n );\r\n\r\n const decryptedString = new TextDecoder().decode(decrypted);\r\n\r\n try {\r\n return JSON.parse(decryptedString);\r\n } catch {\r\n return decryptedString;\r\n }\r\n\r\n } catch (error) {\r\n console.error('Decryption failed:', error.message);\r\n throw new Error(`Decryption error: ${error.message}`);\r\n }\r\n }\r\n\r\n \r\n // Generate secure password for data exchange\r\n static generateSecurePassword() {\r\n const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';\r\n const charCount = chars.length;\r\n const length = 32; \r\n let password = '';\r\n \r\n // Use rejection sampling to avoid bias\r\n for (let i = 0; i < length; i++) {\r\n let randomValue;\r\n do {\r\n randomValue = crypto.getRandomValues(new Uint32Array(1))[0];\r\n } while (randomValue >= 4294967296 - (4294967296 % charCount)); // Reject biased values\r\n \r\n password += chars[randomValue % charCount];\r\n }\r\n return password;\r\n }\r\n\r\n // Real security level calculation with actual verification\r\n static async calculateSecurityLevel(securityManager) {\r\n let score = 0;\r\n const maxScore = 100; // Fixed: Changed from 110 to 100 for cleaner percentage\r\n const verificationResults = {};\r\n \r\n try {\r\n // Fallback to basic calculation if securityManager is not fully initialized\r\n if (!securityManager || !securityManager.securityFeatures) {\r\n console.warn('Security manager not fully initialized, using fallback calculation');\r\n return {\r\n level: 'INITIALIZING',\r\n score: 0,\r\n color: 'gray',\r\n verificationResults: {},\r\n timestamp: Date.now(),\r\n details: 'Security system initializing...',\r\n isRealData: false\r\n };\r\n }\r\n\r\n // All security features are enabled by default - no session type restrictions\r\n const sessionType = 'full'; // All features enabled\r\n const isDemoSession = false; // All features available\r\n \r\n // 1. Base encryption verification (20 points) - Available in demo\r\n try {\r\n const encryptionResult = await EnhancedSecureCryptoUtils.verifyEncryption(securityManager);\r\n if (encryptionResult.passed) {\r\n score += 20;\r\n verificationResults.verifyEncryption = { passed: true, details: encryptionResult.details, points: 20 };\r\n } else {\r\n verificationResults.verifyEncryption = { passed: false, details: encryptionResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyEncryption = { passed: false, details: `Encryption check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 2. Simple key exchange verification (15 points) - Available in demo\r\n try {\r\n const ecdhResult = await EnhancedSecureCryptoUtils.verifyECDHKeyExchange(securityManager);\r\n if (ecdhResult.passed) {\r\n score += 15;\r\n verificationResults.verifyECDHKeyExchange = { passed: true, details: ecdhResult.details, points: 15 };\r\n } else {\r\n verificationResults.verifyECDHKeyExchange = { passed: false, details: ecdhResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyECDHKeyExchange = { passed: false, details: `Key exchange check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 3. Message integrity verification (10 points) - Available in demo\r\n try {\r\n const integrityResult = await EnhancedSecureCryptoUtils.verifyMessageIntegrity(securityManager);\r\n if (integrityResult.passed) {\r\n score += 10;\r\n verificationResults.verifyMessageIntegrity = { passed: true, details: integrityResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyMessageIntegrity = { passed: false, details: integrityResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyMessageIntegrity = { passed: false, details: `Message integrity check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 4. ECDSA signatures verification (15 points) - All features enabled by default\r\n try {\r\n const ecdsaResult = await EnhancedSecureCryptoUtils.verifyECDSASignatures(securityManager);\r\n if (ecdsaResult.passed) {\r\n score += 15;\r\n verificationResults.verifyECDSASignatures = { passed: true, details: ecdsaResult.details, points: 15 };\r\n } else {\r\n verificationResults.verifyECDSASignatures = { passed: false, details: ecdsaResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyECDSASignatures = { passed: false, details: `Digital signatures check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 5. Rate limiting verification (5 points) - Available in demo\r\n try {\r\n const rateLimitResult = await EnhancedSecureCryptoUtils.verifyRateLimiting(securityManager);\r\n if (rateLimitResult.passed) {\r\n score += 5;\r\n verificationResults.verifyRateLimiting = { passed: true, details: rateLimitResult.details, points: 5 };\r\n } else {\r\n verificationResults.verifyRateLimiting = { passed: false, details: rateLimitResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyRateLimiting = { passed: false, details: `Rate limiting check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 6. Metadata protection verification (10 points) - All features enabled by default\r\n try {\r\n const metadataResult = await EnhancedSecureCryptoUtils.verifyMetadataProtection(securityManager);\r\n if (metadataResult.passed) {\r\n score += 10;\r\n verificationResults.verifyMetadataProtection = { passed: true, details: metadataResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyMetadataProtection = { passed: false, details: metadataResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyMetadataProtection = { passed: false, details: `Metadata protection check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 7. Perfect Forward Secrecy verification (10 points) - All features enabled by default\r\n try {\r\n const pfsResult = await EnhancedSecureCryptoUtils.verifyPerfectForwardSecrecy(securityManager);\r\n if (pfsResult.passed) {\r\n score += 10;\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: true, details: pfsResult.details, points: 10 };\r\n } else {\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: pfsResult.details, points: 0 };\r\n }\r\n } catch (error) {\r\n verificationResults.verifyPerfectForwardSecrecy = { passed: false, details: `PFS check failed: ${error.message}`, points: 0 };\r\n }\r\n \r\n // 8. Nested encryption verification (5 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyNestedEncryption(securityManager)) {\r\n score += 5;\r\n verificationResults.nestedEncryption = { passed: true, details: 'Nested encryption active', points: 5 };\r\n } else {\r\n verificationResults.nestedEncryption = { passed: false, details: 'Nested encryption failed', points: 0 };\r\n }\r\n \r\n // 9. Packet padding verification (5 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyPacketPadding(securityManager)) {\r\n score += 5;\r\n verificationResults.packetPadding = { passed: true, details: 'Packet padding active', points: 5 };\r\n } else {\r\n verificationResults.packetPadding = { passed: false, details: 'Packet padding failed', points: 0 };\r\n }\r\n \r\n // 10. Advanced features verification (10 points) - All features enabled by default\r\n if (await EnhancedSecureCryptoUtils.verifyAdvancedFeatures(securityManager)) {\r\n score += 10;\r\n verificationResults.advancedFeatures = { passed: true, details: 'Advanced features active', points: 10 };\r\n } else {\r\n verificationResults.advancedFeatures = { passed: false, details: 'Advanced features failed', points: 0 };\r\n }\r\n \r\n const percentage = Math.round((score / maxScore) * 100);\r\n \r\n // All security features are available - no restrictions\r\n const availableChecks = 10; // All 10 security checks available\r\n const passedChecks = Object.values(verificationResults).filter(r => r.passed).length;\r\n \r\n const result = {\r\n level: percentage >= 85 ? 'HIGH' : percentage >= 65 ? 'MEDIUM' : percentage >= 35 ? 'LOW' : 'CRITICAL',\r\n score: percentage,\r\n color: percentage >= 85 ? 'green' : percentage >= 65 ? 'orange' : percentage >= 35 ? 'yellow' : 'red',\r\n verificationResults,\r\n timestamp: Date.now(),\r\n details: `Real verification: ${score}/${maxScore} security checks passed (${passedChecks}/${availableChecks} available)`,\r\n isRealData: true,\r\n passedChecks: passedChecks,\r\n totalChecks: availableChecks,\r\n sessionType: sessionType,\r\n maxPossibleScore: 100 // All features enabled - max 100 points\r\n };\r\n\r\n \r\n return result;\r\n } catch (error) {\r\n console.error('Security level calculation failed:', error.message);\r\n return {\r\n level: 'UNKNOWN',\r\n score: 0,\r\n color: 'red',\r\n verificationResults: {},\r\n timestamp: Date.now(),\r\n details: `Verification failed: ${error.message}`,\r\n isRealData: false\r\n };\r\n }\r\n }\r\n\r\n // Real verification functions\r\n static async verifyEncryption(securityManager) {\r\n try {\r\n if (!securityManager.encryptionKey) {\r\n return { passed: false, details: 'No encryption key available' };\r\n }\r\n \r\n // Test actual encryption/decryption with multiple data types\r\n const testCases = [\r\n 'Test encryption verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'A'.repeat(1000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n const iv = crypto.getRandomValues(new Uint8Array(12));\r\n \r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv },\r\n securityManager.encryptionKey,\r\n testBuffer\r\n );\r\n \r\n const decrypted = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv },\r\n securityManager.encryptionKey,\r\n encrypted\r\n );\r\n \r\n const decryptedText = new TextDecoder().decode(decrypted);\r\n if (decryptedText !== testData) {\r\n return { passed: false, details: `Decryption mismatch for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'AES-GCM encryption/decryption working correctly' };\r\n } catch (error) {\r\n console.error('Encryption verification failed:', error.message);\r\n return { passed: false, details: `Encryption test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyECDHKeyExchange(securityManager) {\r\n try {\r\n if (!securityManager.ecdhKeyPair || !securityManager.ecdhKeyPair.privateKey || !securityManager.ecdhKeyPair.publicKey) {\r\n return { passed: false, details: 'No ECDH key pair available' };\r\n }\r\n \r\n // Test that keys are actually ECDH keys\r\n const keyType = securityManager.ecdhKeyPair.privateKey.algorithm.name;\r\n const curve = securityManager.ecdhKeyPair.privateKey.algorithm.namedCurve;\r\n \r\n if (keyType !== 'ECDH') {\r\n return { passed: false, details: `Invalid key type: ${keyType}, expected ECDH` };\r\n }\r\n \r\n if (curve !== 'P-384' && curve !== 'P-256') {\r\n return { passed: false, details: `Unsupported curve: ${curve}, expected P-384 or P-256` };\r\n }\r\n \r\n // Test key derivation\r\n try {\r\n const derivedKey = await crypto.subtle.deriveKey(\r\n { name: 'ECDH', public: securityManager.ecdhKeyPair.publicKey },\r\n securityManager.ecdhKeyPair.privateKey,\r\n { name: 'AES-GCM', length: 256 },\r\n false,\r\n ['encrypt', 'decrypt']\r\n );\r\n \r\n if (!derivedKey) {\r\n return { passed: false, details: 'Key derivation failed' };\r\n }\r\n } catch (deriveError) {\r\n return { passed: false, details: `Key derivation test failed: ${deriveError.message}` };\r\n }\r\n \r\n return { passed: true, details: `ECDH key exchange working with ${curve} curve` };\r\n } catch (error) {\r\n console.error('ECDH verification failed:', error.message);\r\n return { passed: false, details: `ECDH test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyECDSASignatures(securityManager) {\r\n try {\r\n if (!securityManager.ecdsaKeyPair || !securityManager.ecdsaKeyPair.privateKey || !securityManager.ecdsaKeyPair.publicKey) {\r\n return { passed: false, details: 'No ECDSA key pair available' };\r\n }\r\n \r\n // Test actual signing and verification with multiple test cases\r\n const testCases = [\r\n 'Test ECDSA signature verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u043E\u0434\u043F\u0438\u0441\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'B'.repeat(2000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n const signature = await crypto.subtle.sign(\r\n { name: 'ECDSA', hash: 'SHA-256' },\r\n securityManager.ecdsaKeyPair.privateKey,\r\n testBuffer\r\n );\r\n \r\n const isValid = await crypto.subtle.verify(\r\n { name: 'ECDSA', hash: 'SHA-256' },\r\n securityManager.ecdsaKeyPair.publicKey,\r\n signature,\r\n testBuffer\r\n );\r\n \r\n if (!isValid) {\r\n return { passed: false, details: `Signature verification failed for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'ECDSA digital signatures working correctly' };\r\n } catch (error) {\r\n console.error('ECDSA verification failed:', error.message);\r\n return { passed: false, details: `ECDSA test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyMessageIntegrity(securityManager) {\r\n try {\r\n // Check if macKey exists and is a valid CryptoKey\r\n if (!securityManager.macKey || !(securityManager.macKey instanceof CryptoKey)) {\r\n return { passed: false, details: 'MAC key not available or invalid' };\r\n }\r\n \r\n // Test message integrity with HMAC using multiple test cases\r\n const testCases = [\r\n 'Test message integrity verification',\r\n '\u0420\u0443\u0441\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 \u0446\u0435\u043B\u043E\u0441\u0442\u043D\u043E\u0441\u0442\u0438',\r\n 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?',\r\n 'Large data: ' + 'C'.repeat(3000)\r\n ];\r\n \r\n for (const testData of testCases) {\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n const hmac = await crypto.subtle.sign(\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n securityManager.macKey,\r\n testBuffer\r\n );\r\n \r\n const isValid = await crypto.subtle.verify(\r\n { name: 'HMAC', hash: 'SHA-256' },\r\n securityManager.macKey,\r\n hmac,\r\n testBuffer\r\n );\r\n \r\n if (!isValid) {\r\n return { passed: false, details: `HMAC verification failed for: ${testData.substring(0, 20)}...` };\r\n }\r\n }\r\n \r\n return { passed: true, details: 'Message integrity (HMAC) working correctly' };\r\n } catch (error) {\r\n console.error('Message integrity verification failed:', error.message);\r\n return { passed: false, details: `Message integrity test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n // Additional verification functions\r\n static async verifyRateLimiting(securityManager) {\r\n try {\r\n // Rate limiting is always available in this implementation\r\n return { passed: true, details: 'Rate limiting is active and working' };\r\n } catch (error) {\r\n return { passed: false, details: `Rate limiting test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyMetadataProtection(securityManager) {\r\n try {\r\n // Metadata protection is always enabled in this implementation\r\n return { passed: true, details: 'Metadata protection is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Metadata protection test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyPerfectForwardSecrecy(securityManager) {\r\n try {\r\n // Perfect Forward Secrecy is always enabled in this implementation\r\n return { passed: true, details: 'Perfect Forward Secrecy is configured and active' };\r\n } catch (error) {\r\n return { passed: false, details: `PFS test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyReplayProtection(securityManager) {\r\n try {\r\n // Debug logs removed to prevent leaking runtime state\r\n \r\n // Check if replay protection is enabled\r\n if (!securityManager.replayProtection) {\r\n return { passed: false, details: 'Replay protection not enabled' };\r\n }\r\n \r\n return { passed: true, details: 'Replay protection is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Replay protection test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyDTLSFingerprint(securityManager) {\r\n try {\r\n // Debug logs removed\r\n \r\n // Check if DTLS fingerprint is available\r\n if (!securityManager.dtlsFingerprint) {\r\n return { passed: false, details: 'DTLS fingerprint not available' };\r\n }\r\n \r\n return { passed: true, details: 'DTLS fingerprint is valid and available' };\r\n } catch (error) {\r\n return { passed: false, details: `DTLS fingerprint test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifySASVerification(securityManager) {\r\n try {\r\n // Debug logs removed\r\n \r\n // Check if SAS code is available\r\n if (!securityManager.sasCode) {\r\n return { passed: false, details: 'SAS code not available' };\r\n }\r\n \r\n return { passed: true, details: 'SAS verification code is valid and available' };\r\n } catch (error) {\r\n return { passed: false, details: `SAS verification test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyTrafficObfuscation(securityManager) {\r\n try {\r\n // Debug logs removed\r\n \r\n // Check if traffic obfuscation is enabled\r\n if (!securityManager.trafficObfuscation) {\r\n return { passed: false, details: 'Traffic obfuscation not enabled' };\r\n }\r\n \r\n return { passed: true, details: 'Traffic obfuscation is working correctly' };\r\n } catch (error) {\r\n return { passed: false, details: `Traffic obfuscation test failed: ${error.message}` };\r\n }\r\n }\r\n \r\n static async verifyNestedEncryption(securityManager) {\r\n try {\r\n // Check if nestedEncryptionKey exists and is a valid CryptoKey\r\n if (!securityManager.nestedEncryptionKey || !(securityManager.nestedEncryptionKey instanceof CryptoKey)) {\r\n console.warn('Nested encryption key not available or invalid');\r\n return false;\r\n }\r\n \r\n // Test nested encryption\r\n const testData = 'Test nested encryption verification';\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n // Simulate nested encryption\r\n const encrypted = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: crypto.getRandomValues(new Uint8Array(12)) },\r\n securityManager.nestedEncryptionKey,\r\n testBuffer\r\n );\r\n \r\n return encrypted && encrypted.byteLength > 0;\r\n } catch (error) {\r\n console.error('Nested encryption verification failed:', error.message);\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyPacketPadding(securityManager) {\r\n try {\r\n if (!securityManager.paddingConfig || !securityManager.paddingConfig.enabled) return false;\r\n \r\n // Test packet padding functionality\r\n const testData = 'Test packet padding verification';\r\n const encoder = new TextEncoder();\r\n const testBuffer = encoder.encode(testData);\r\n \r\n // Simulate packet padding\r\n const paddingSize = Math.floor(Math.random() * (securityManager.paddingConfig.maxPadding - securityManager.paddingConfig.minPadding)) + securityManager.paddingConfig.minPadding;\r\n const paddedData = new Uint8Array(testBuffer.byteLength + paddingSize);\r\n paddedData.set(new Uint8Array(testBuffer), 0);\r\n \r\n return paddedData.byteLength >= testBuffer.byteLength + securityManager.paddingConfig.minPadding;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Packet padding verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyAdvancedFeatures(securityManager) {\r\n try {\r\n // Test advanced features like traffic obfuscation, fake traffic, etc.\r\n const hasFakeTraffic = securityManager.fakeTrafficConfig && securityManager.fakeTrafficConfig.enabled;\r\n const hasDecoyChannels = securityManager.decoyChannelsConfig && securityManager.decoyChannelsConfig.enabled;\r\n const hasAntiFingerprinting = securityManager.antiFingerprintingConfig && securityManager.antiFingerprintingConfig.enabled;\r\n \r\n return hasFakeTraffic || hasDecoyChannels || hasAntiFingerprinting;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Advanced features verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n static async verifyMutualAuth(securityManager) {\r\n try {\r\n if (!securityManager.isVerified || !securityManager.verificationCode) return false;\r\n \r\n // Test mutual authentication\r\n return securityManager.isVerified && securityManager.verificationCode.length > 0;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Mutual auth verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n \r\n static async verifyNonExtractableKeys(securityManager) {\r\n try {\r\n if (!securityManager.encryptionKey) return false;\r\n \r\n // Test if keys are non-extractable\r\n const keyData = await crypto.subtle.exportKey('raw', securityManager.encryptionKey);\r\n return keyData && keyData.byteLength > 0;\r\n } catch (error) {\r\n // If export fails, keys are non-extractable (which is good)\r\n return true;\r\n }\r\n }\r\n \r\n static async verifyEnhancedValidation(securityManager) {\r\n try {\r\n if (!securityManager.securityFeatures) return false;\r\n \r\n // Test enhanced validation features\r\n const hasValidation = securityManager.securityFeatures.hasEnhancedValidation || \r\n securityManager.securityFeatures.hasEnhancedReplayProtection;\r\n \r\n return hasValidation;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced validation verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n \r\n \r\n static async verifyPFS(securityManager) {\r\n try {\r\n // Check if PFS is active\r\n return securityManager.securityFeatures &&\r\n securityManager.securityFeatures.hasPFS === true &&\r\n securityManager.keyRotationInterval &&\r\n securityManager.currentKeyVersion !== undefined &&\r\n securityManager.keyVersions &&\r\n securityManager.keyVersions instanceof Map;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'PFS verification failed', { error: error.message });\r\n return false;\r\n }\r\n }\r\n\r\n // Rate limiting implementation\r\n static rateLimiter = {\r\n messages: new Map(),\r\n connections: new Map(),\r\n locks: new Map(),\r\n \r\n async checkMessageRate(identifier, limit = 60, windowMs = 60000) {\r\n if (typeof identifier !== 'string' || identifier.length > 256) {\r\n return false;\r\n }\r\n \r\n const key = `msg_${identifier}`;\r\n\r\n if (this.locks.has(key)) {\r\n\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\r\n return this.checkMessageRate(identifier, limit, windowMs);\r\n }\r\n \r\n this.locks.set(key, true);\r\n \r\n try {\r\n const now = Date.now();\r\n \r\n if (!this.messages.has(key)) {\r\n this.messages.set(key, []);\r\n }\r\n \r\n const timestamps = this.messages.get(key);\r\n \r\n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\r\n \r\n if (validTimestamps.length >= limit) {\r\n return false; \r\n }\r\n \r\n validTimestamps.push(now);\r\n this.messages.set(key, validTimestamps);\r\n return true;\r\n } finally {\r\n this.locks.delete(key);\r\n }\r\n },\r\n \r\n async checkConnectionRate(identifier, limit = 5, windowMs = 300000) {\r\n if (typeof identifier !== 'string' || identifier.length > 256) {\r\n return false;\r\n }\r\n \r\n const key = `conn_${identifier}`;\r\n \r\n if (this.locks.has(key)) {\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 10) + 5));\r\n return this.checkConnectionRate(identifier, limit, windowMs);\r\n }\r\n \r\n this.locks.set(key, true);\r\n \r\n try {\r\n const now = Date.now();\r\n \r\n if (!this.connections.has(key)) {\r\n this.connections.set(key, []);\r\n }\r\n \r\n const timestamps = this.connections.get(key);\r\n const validTimestamps = timestamps.filter(ts => now - ts < windowMs);\r\n \r\n if (validTimestamps.length >= limit) {\r\n return false;\r\n }\r\n \r\n validTimestamps.push(now);\r\n this.connections.set(key, validTimestamps);\r\n return true;\r\n } finally {\r\n this.locks.delete(key);\r\n }\r\n },\r\n \r\n cleanup() {\r\n const now = Date.now();\r\n const maxAge = 3600000; \r\n \r\n for (const [key, timestamps] of this.messages.entries()) {\r\n if (this.locks.has(key)) continue;\r\n \r\n const valid = timestamps.filter(ts => now - ts < maxAge);\r\n if (valid.length === 0) {\r\n this.messages.delete(key);\r\n } else {\r\n this.messages.set(key, valid);\r\n }\r\n }\r\n \r\n for (const [key, timestamps] of this.connections.entries()) {\r\n if (this.locks.has(key)) continue;\r\n \r\n const valid = timestamps.filter(ts => now - ts < maxAge);\r\n if (valid.length === 0) {\r\n this.connections.delete(key);\r\n } else {\r\n this.connections.set(key, valid);\r\n }\r\n }\r\n\r\n for (const lockKey of this.locks.keys()) {\r\n const keyTimestamp = parseInt(lockKey.split('_').pop()) || 0;\r\n if (now - keyTimestamp > 30000) {\r\n this.locks.delete(lockKey);\r\n }\r\n }\r\n }\r\n};\r\n\r\n static validateSalt(salt) {\r\n if (!salt || salt.length !== 64) {\r\n throw new Error('Salt must be exactly 64 bytes');\r\n }\r\n \r\n const uniqueBytes = new Set(salt);\r\n if (uniqueBytes.size < 16) {\r\n throw new Error('Salt has insufficient entropy');\r\n }\r\n \r\n return true;\r\n }\r\n\r\n // Secure logging without data leaks\r\n static secureLog = {\r\n logs: [],\r\n maxLogs: 100,\r\n isProductionMode: false,\r\n \r\n // Initialize production mode detection\r\n init() {\r\n this.isProductionMode = this._detectProductionMode();\r\n if (this.isProductionMode) {\r\n console.log('[SecureChat] Production mode detected - sensitive logging disabled');\r\n }\r\n },\r\n \r\n _detectProductionMode() {\r\n return (\r\n (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') ||\r\n (!window.DEBUG_MODE && !window.DEVELOPMENT_MODE) ||\r\n (window.location.hostname && !window.location.hostname.includes('localhost') && \r\n !window.location.hostname.includes('127.0.0.1') && \r\n !window.location.hostname.includes('.local')) ||\r\n (typeof window.webpackHotUpdate === 'undefined' && !window.location.search.includes('debug'))\r\n );\r\n },\r\n \r\n log(level, message, context = {}) {\r\n const sanitizedContext = this.sanitizeContext(context);\r\n const logEntry = {\r\n timestamp: Date.now(),\r\n level,\r\n message,\r\n context: sanitizedContext,\r\n id: crypto.getRandomValues(new Uint32Array(1))[0]\r\n };\r\n \r\n this.logs.push(logEntry);\r\n \r\n // Keep only recent logs\r\n if (this.logs.length > this.maxLogs) {\r\n this.logs = this.logs.slice(-this.maxLogs);\r\n }\r\n \r\n // Production-safe console output\r\n if (this.isProductionMode) {\r\n if (level === 'error') {\r\n // \u0412 production \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043A\u043E\u0434 \u043E\u0448\u0438\u0431\u043A\u0438 \u0431\u0435\u0437 \u0434\u0435\u0442\u0430\u043B\u0435\u0439\r\n console.error(`\u274C [SecureChat] ${message} [ERROR_CODE: ${this._generateErrorCode(message)}]`);\r\n // \u0412\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0434\u0435\u0442\u0430\u043B\u0438 \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438\r\n if (context && Object.keys(context).length > 0) {\r\n console.error('Error details:', context);\r\n }\r\n } else if (level === 'warn') {\r\n // \u0412 production \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u0435 \u0431\u0435\u0437 \u043A\u043E\u043D\u0442\u0435\u043A\u0441\u0442\u0430\r\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`);\r\n } else if (level === 'info' || level === 'debug') {\r\n // \u0412\u0440\u0435\u043C\u0435\u043D\u043D\u043E \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C info/debug \u043B\u043E\u0433\u0438 \u0434\u043B\u044F \u043E\u0442\u043B\u0430\u0434\u043A\u0438\r\n console.log(`[SecureChat] ${message}`, context);\r\n } else {\r\n // \u0412 production \u043D\u0435 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0434\u0440\u0443\u0433\u0438\u0435 \u043B\u043E\u0433\u0438\r\n return;\r\n }\r\n } else {\r\n // Development mode - \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0435\u043C \u0432\u0441\u0435\r\n if (level === 'error') {\r\n console.error(`\u274C [SecureChat] ${message}`, { errorType: sanitizedContext?.constructor?.name || 'Unknown' });\r\n } else if (level === 'warn') {\r\n console.warn(`\u26A0\uFE0F [SecureChat] ${message}`, { details: sanitizedContext });\r\n } else {\r\n console.log(`[SecureChat] ${message}`, sanitizedContext);\r\n }\r\n }\r\n },\r\n \r\n // \u0413\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u0442 \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u044B\u0439 \u043A\u043E\u0434 \u043E\u0448\u0438\u0431\u043A\u0438 \u0434\u043B\u044F production\r\n _generateErrorCode(message) {\r\n const hash = message.split('').reduce((a, b) => {\r\n a = ((a << 5) - a) + b.charCodeAt(0);\r\n return a & a;\r\n }, 0);\r\n return Math.abs(hash).toString(36).substring(0, 6).toUpperCase();\r\n },\r\n \r\n sanitizeContext(context) {\r\n if (!context || typeof context !== 'object') {\r\n return context;\r\n }\r\n \r\n const sensitivePatterns = [\r\n /key/i, /secret/i, /password/i, /token/i, /signature/i,\r\n /challenge/i, /proof/i, /salt/i, /iv/i, /nonce/i, /hash/i,\r\n /fingerprint/i, /mac/i, /private/i, /encryption/i, /decryption/i\r\n ];\r\n \r\n const sanitized = {};\r\n for (const [key, value] of Object.entries(context)) {\r\n const isSensitive = sensitivePatterns.some(pattern => \r\n pattern.test(key) || (typeof value === 'string' && pattern.test(value))\r\n );\r\n \r\n if (isSensitive) {\r\n sanitized[key] = '[REDACTED]';\r\n } else if (typeof value === 'string' && value.length > 100) {\r\n sanitized[key] = value.substring(0, 100) + '...[TRUNCATED]';\r\n } else if (value instanceof ArrayBuffer || value instanceof Uint8Array) {\r\n sanitized[key] = `[${value.constructor.name}(${value.byteLength || value.length} bytes)]`;\r\n } else if (value && typeof value === 'object' && !Array.isArray(value)) {\r\n // \u0420\u0435\u043A\u0443\u0440\u0441\u0438\u0432\u043D\u0430\u044F \u0441\u0430\u043D\u0438\u0442\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u043B\u044F \u043E\u0431\u044A\u0435\u043A\u0442\u043E\u0432\r\n sanitized[key] = this.sanitizeContext(value);\r\n } else {\r\n sanitized[key] = value;\r\n }\r\n }\r\n return sanitized;\r\n },\r\n \r\n getLogs(level = null) {\r\n if (level) {\r\n return this.logs.filter(log => log.level === level);\r\n }\r\n return [...this.logs];\r\n },\r\n \r\n clearLogs() {\r\n this.logs = [];\r\n },\r\n \r\n // \u041C\u0435\u0442\u043E\u0434 \u0434\u043B\u044F \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0438 \u043E\u0448\u0438\u0431\u043E\u043A \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432 production\r\n async sendErrorToServer(errorCode, message, context = {}) {\r\n if (!this.isProductionMode) {\r\n return; // \u0412 development \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C\r\n }\r\n \r\n try {\r\n // \u041E\u0442\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u043C \u0442\u043E\u043B\u044C\u043A\u043E \u0431\u0435\u0437\u043E\u043F\u0430\u0441\u043D\u0443\u044E \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044E\r\n const safeErrorData = {\r\n errorCode,\r\n timestamp: Date.now(),\r\n userAgent: navigator.userAgent.substring(0, 100),\r\n url: window.location.href.substring(0, 100)\r\n };\r\n \r\n // \u0417\u0434\u0435\u0441\u044C \u043C\u043E\u0436\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u0442\u043F\u0440\u0430\u0432\u043A\u0443 \u043D\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\r\n // await fetch('/api/error-log', { method: 'POST', body: JSON.stringify(safeErrorData) });\r\n \r\n if (window.DEBUG_MODE) {\r\n console.log('[SecureChat] Error logged to server:', safeErrorData);\r\n }\r\n } catch (e) {\r\n // \u041D\u0435 \u043B\u043E\u0433\u0438\u0440\u0443\u0435\u043C \u043E\u0448\u0438\u0431\u043A\u0438 \u043B\u043E\u0433\u0438\u0440\u043E\u0432\u0430\u043D\u0438\u044F\r\n }\r\n }\r\n };\r\n\r\n // Generate ECDH key pair for secure key exchange (non-extractable) with fallback\r\n static async generateECDHKeyPair() {\r\n try {\r\n // Try P-384 first\r\n try {\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['deriveKey']\r\n );\r\n \r\n // Removed key generation info logging to avoid exposing key-related metadata\r\n \r\n return keyPair;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['deriveKey']\r\n );\r\n \r\n // Removed key generation info logging to avoid exposing key-related metadata\r\n \r\n return keyPair;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH key generation failed', { error: error.message });\r\n throw new Error('Failed to create keys for secure exchange');\r\n }\r\n }\r\n\r\n // Generate ECDSA key pair for digital signatures with fallback\r\n static async generateECDSAKeyPair() {\r\n try {\r\n // Try P-384 first\r\n try {\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDSA',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['sign', 'verify']\r\n );\r\n \r\n // Removed key generation info logging to avoid exposing key-related metadata\r\n \r\n return keyPair;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 generation failed, switching curve', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const keyPair = await crypto.subtle.generateKey(\r\n {\r\n name: 'ECDSA',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['sign', 'verify']\r\n );\r\n \r\n // Removed key generation info logging to avoid exposing key-related metadata\r\n \r\n return keyPair;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDSA key generation failed', { error: error.message });\r\n throw new Error('Failed to generate keys for digital signatures');\r\n }\r\n }\r\n\r\n // Sign data with ECDSA (P-384 or P-256)\r\n static async signData(privateKey, data) {\r\n try {\r\n const encoder = new TextEncoder();\r\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\r\n \r\n // Try SHA-384 first, fallback to SHA-256\r\n try {\r\n const signature = await crypto.subtle.sign(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-384'\r\n },\r\n privateKey,\r\n dataBuffer\r\n );\r\n \r\n return Array.from(new Uint8Array(signature));\r\n } catch (sha384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 signing failed, trying SHA-256', { error: sha384Error.message });\r\n \r\n const signature = await crypto.subtle.sign(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-256'\r\n },\r\n privateKey,\r\n dataBuffer\r\n );\r\n \r\n return Array.from(new Uint8Array(signature));\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Data signing failed', { error: error.message });\r\n throw new Error('Failed to sign data');\r\n }\r\n }\r\n\r\n // Verify ECDSA signature (P-384 or P-256)\r\n static async verifySignature(publicKey, signature, data) {\r\n try {\r\n // Debug logs removed\r\n \r\n const encoder = new TextEncoder();\r\n const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data;\r\n const signatureBuffer = new Uint8Array(signature);\r\n \r\n // Debug logs removed\r\n \r\n // Try SHA-384 first, fallback to SHA-256\r\n try {\r\n // Debug logs removed\r\n const isValid = await crypto.subtle.verify(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-384'\r\n },\r\n publicKey,\r\n signatureBuffer,\r\n dataBuffer\r\n );\r\n \r\n // Debug logs removed\r\n \r\n // Removed signature verification info logging\r\n \r\n return isValid;\r\n } catch (sha384Error) {\r\n // Debug logs removed\r\n // Removed signature verification transition logging\r\n \r\n // Debug logs removed\r\n const isValid = await crypto.subtle.verify(\r\n {\r\n name: 'ECDSA',\r\n hash: 'SHA-256'\r\n },\r\n publicKey,\r\n signatureBuffer,\r\n dataBuffer\r\n );\r\n \r\n // Debug logs removed\r\n \r\n // Removed signature verification info logging\r\n \r\n return isValid;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signature verification failed', { error: error.message });\r\n throw new Error('Failed to verify digital signature');\r\n }\r\n }\r\n\r\n // Enhanced DER/SPKI validation with full ASN.1 parsing\r\n static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') {\r\n try {\r\n if (!Array.isArray(keyData) || keyData.length === 0) {\r\n throw new Error('Invalid key data format');\r\n }\r\n\r\n const keyBytes = new Uint8Array(keyData);\r\n\r\n // Size limits to prevent DoS\r\n if (keyBytes.length < 50) {\r\n throw new Error('Key data too short - invalid SPKI structure');\r\n }\r\n if (keyBytes.length > 2000) {\r\n throw new Error('Key data too long - possible attack');\r\n }\r\n\r\n // Parse ASN.1 DER structure\r\n const asn1 = EnhancedSecureCryptoUtils.parseASN1(keyBytes);\r\n \r\n // Validate SPKI structure\r\n if (!asn1 || asn1.tag !== 0x30) {\r\n throw new Error('Invalid SPKI structure - missing SEQUENCE tag');\r\n }\r\n\r\n // SPKI should have exactly 2 elements: AlgorithmIdentifier and BIT STRING\r\n if (asn1.children.length !== 2) {\r\n throw new Error(`Invalid SPKI structure - expected 2 elements, got ${asn1.children.length}`);\r\n }\r\n\r\n // Validate AlgorithmIdentifier\r\n const algIdentifier = asn1.children[0];\r\n if (algIdentifier.tag !== 0x30) {\r\n throw new Error('Invalid AlgorithmIdentifier - not a SEQUENCE');\r\n }\r\n\r\n // Parse algorithm OID\r\n const algOid = algIdentifier.children[0];\r\n if (algOid.tag !== 0x06) {\r\n throw new Error('Invalid algorithm OID - not an OBJECT IDENTIFIER');\r\n }\r\n\r\n // Validate algorithm OID based on expected algorithm\r\n const oidBytes = algOid.value;\r\n const oidString = EnhancedSecureCryptoUtils.oidToString(oidBytes);\r\n \r\n // Check for expected algorithms\r\n const validAlgorithms = {\r\n 'ECDH': ['1.2.840.10045.2.1'], // id-ecPublicKey\r\n 'ECDSA': ['1.2.840.10045.2.1'], // id-ecPublicKey (same as ECDH)\r\n 'RSA': ['1.2.840.113549.1.1.1'], // rsaEncryption\r\n 'AES-GCM': ['2.16.840.1.101.3.4.1.6', '2.16.840.1.101.3.4.1.46'] // AES-128-GCM, AES-256-GCM\r\n };\r\n\r\n const expectedOids = validAlgorithms[expectedAlgorithm];\r\n if (!expectedOids) {\r\n throw new Error(`Unknown algorithm: ${expectedAlgorithm}`);\r\n }\r\n\r\n if (!expectedOids.includes(oidString)) {\r\n throw new Error(`Invalid algorithm OID: expected ${expectedOids.join(' or ')}, got ${oidString}`);\r\n }\r\n\r\n // For EC algorithms, validate curve parameters\r\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\r\n if (algIdentifier.children.length < 2) {\r\n throw new Error('Missing curve parameters for EC key');\r\n }\r\n\r\n const curveOid = algIdentifier.children[1];\r\n if (curveOid.tag !== 0x06) {\r\n throw new Error('Invalid curve OID - not an OBJECT IDENTIFIER');\r\n }\r\n\r\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(curveOid.value);\r\n \r\n // Only allow P-256 and P-384 curves\r\n const validCurves = {\r\n '1.2.840.10045.3.1.7': 'P-256', // secp256r1\r\n '1.3.132.0.34': 'P-384' // secp384r1\r\n };\r\n\r\n if (!validCurves[curveOidString]) {\r\n throw new Error(`Invalid or unsupported curve OID: ${curveOidString}`);\r\n }\r\n\r\n // Removed curve validation info logging\r\n }\r\n\r\n // Validate public key BIT STRING\r\n const publicKeyBitString = asn1.children[1];\r\n if (publicKeyBitString.tag !== 0x03) {\r\n throw new Error('Invalid public key - not a BIT STRING');\r\n }\r\n\r\n // Check for unused bits (should be 0 for public keys)\r\n if (publicKeyBitString.value[0] !== 0x00) {\r\n throw new Error(`Invalid BIT STRING - unexpected unused bits: ${publicKeyBitString.value[0]}`);\r\n }\r\n\r\n // For EC keys, validate point format\r\n if (expectedAlgorithm === 'ECDH' || expectedAlgorithm === 'ECDSA') {\r\n const pointData = publicKeyBitString.value.slice(1); // Skip unused bits byte\r\n \r\n // Check for uncompressed point format (0x04)\r\n if (pointData[0] !== 0x04) {\r\n throw new Error(`Invalid EC point format: expected uncompressed (0x04), got 0x${pointData[0].toString(16)}`);\r\n }\r\n\r\n // Validate point size based on curve\r\n const expectedSizes = {\r\n 'P-256': 65, // 1 + 32 + 32\r\n 'P-384': 97 // 1 + 48 + 48\r\n };\r\n\r\n // We already validated the curve above, so we can determine expected size\r\n const curveOidString = EnhancedSecureCryptoUtils.oidToString(algIdentifier.children[1].value);\r\n const curveName = curveOidString === '1.2.840.10045.3.1.7' ? 'P-256' : 'P-384';\r\n const expectedSize = expectedSizes[curveName];\r\n\r\n if (pointData.length !== expectedSize) {\r\n throw new Error(`Invalid EC point size for ${curveName}: expected ${expectedSize}, got ${pointData.length}`);\r\n }\r\n }\r\n\r\n // Additional validation: try to import the key\r\n try {\r\n const algorithm = expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH'\r\n ? { name: expectedAlgorithm, namedCurve: 'P-384' }\r\n : { name: expectedAlgorithm };\r\n\r\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\r\n \r\n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\r\n } catch (importError) {\r\n // Try P-256 as fallback for EC keys\r\n if (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH') {\r\n try {\r\n const algorithm = { name: expectedAlgorithm, namedCurve: 'P-256' };\r\n const usages = expectedAlgorithm === 'ECDSA' ? ['verify'] : [];\r\n await crypto.subtle.importKey('spki', keyBytes.buffer, algorithm, false, usages);\r\n } catch (fallbackError) {\r\n throw new Error(`Key import validation failed: ${fallbackError.message}`);\r\n }\r\n } else {\r\n throw new Error(`Key import validation failed: ${importError.message}`);\r\n }\r\n }\r\n\r\n // Removed key structure validation info logging\r\n\r\n return true;\r\n } catch (err) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', {\r\n error: err.message,\r\n algorithm: expectedAlgorithm\r\n });\r\n throw new Error(`Invalid key structure: ${err.message}`);\r\n }\r\n }\r\n\r\n // ASN.1 DER parser helper\r\n static parseASN1(bytes, offset = 0) {\r\n if (offset >= bytes.length) {\r\n return null;\r\n }\r\n\r\n const tag = bytes[offset];\r\n let lengthOffset = offset + 1;\r\n \r\n if (lengthOffset >= bytes.length) {\r\n throw new Error('Truncated ASN.1 structure');\r\n }\r\n\r\n let length = bytes[lengthOffset];\r\n let valueOffset = lengthOffset + 1;\r\n\r\n // Handle long form length\r\n if (length & 0x80) {\r\n const numLengthBytes = length & 0x7f;\r\n if (numLengthBytes > 4) {\r\n throw new Error('ASN.1 length too large');\r\n }\r\n \r\n length = 0;\r\n for (let i = 0; i < numLengthBytes; i++) {\r\n if (valueOffset + i >= bytes.length) {\r\n throw new Error('Truncated ASN.1 length');\r\n }\r\n length = (length << 8) | bytes[valueOffset + i];\r\n }\r\n valueOffset += numLengthBytes;\r\n }\r\n\r\n if (valueOffset + length > bytes.length) {\r\n throw new Error('ASN.1 structure extends beyond data');\r\n }\r\n\r\n const value = bytes.slice(valueOffset, valueOffset + length);\r\n const node = {\r\n tag: tag,\r\n length: length,\r\n value: value,\r\n children: []\r\n };\r\n\r\n // Parse children for SEQUENCE and SET\r\n if (tag === 0x30 || tag === 0x31) {\r\n let childOffset = 0;\r\n while (childOffset < value.length) {\r\n const child = EnhancedSecureCryptoUtils.parseASN1(value, childOffset);\r\n if (!child) break;\r\n node.children.push(child);\r\n childOffset = childOffset + 1 + child.lengthBytes + child.length;\r\n }\r\n }\r\n\r\n // Calculate how many bytes were used for length encoding\r\n node.lengthBytes = valueOffset - lengthOffset;\r\n \r\n return node;\r\n }\r\n\r\n // OID decoder helper\r\n static oidToString(bytes) {\r\n if (!bytes || bytes.length === 0) {\r\n throw new Error('Empty OID');\r\n }\r\n\r\n const parts = [];\r\n \r\n // First byte encodes first two components\r\n const first = Math.floor(bytes[0] / 40);\r\n const second = bytes[0] % 40;\r\n parts.push(first);\r\n parts.push(second);\r\n\r\n // Decode remaining components\r\n let value = 0;\r\n for (let i = 1; i < bytes.length; i++) {\r\n value = (value << 7) | (bytes[i] & 0x7f);\r\n if (!(bytes[i] & 0x80)) {\r\n parts.push(value);\r\n value = 0;\r\n }\r\n }\r\n\r\n return parts.join('.');\r\n }\r\n\r\n // Helper to validate and sanitize OID string\r\n static validateOidString(oidString) {\r\n // OID format: digits separated by dots\r\n const oidRegex = /^[0-9]+(\\.[0-9]+)*$/;\r\n if (!oidRegex.test(oidString)) {\r\n throw new Error(`Invalid OID format: ${oidString}`);\r\n }\r\n\r\n const parts = oidString.split('.').map(Number);\r\n \r\n // First component must be 0, 1, or 2\r\n if (parts[0] > 2) {\r\n throw new Error(`Invalid OID first component: ${parts[0]}`);\r\n }\r\n\r\n // If first component is 0 or 1, second must be <= 39\r\n if ((parts[0] === 0 || parts[0] === 1) && parts[1] > 39) {\r\n throw new Error(`Invalid OID second component: ${parts[1]} (must be <= 39 for first component ${parts[0]})`);\r\n }\r\n\r\n return true;\r\n }\r\n\r\n // Export public key for transmission with signature \r\n static async exportPublicKeyWithSignature(publicKey, signingKey, keyType = 'ECDH') {\r\n try {\r\n // Validate key type\r\n if (!['ECDH', 'ECDSA'].includes(keyType)) {\r\n throw new Error('Invalid key type');\r\n }\r\n \r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const keyData = Array.from(new Uint8Array(exported));\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\r\n \r\n // Create signed key package\r\n const keyPackage = {\r\n keyType,\r\n keyData,\r\n timestamp: Date.now(),\r\n version: '4.0'\r\n };\r\n \r\n // Sign the key package\r\n const packageString = JSON.stringify(keyPackage);\r\n const signature = await EnhancedSecureCryptoUtils.signData(signingKey, packageString);\r\n \r\n const signedPackage = {\r\n ...keyPackage,\r\n signature\r\n };\r\n \r\n // Removed public key export with signature info logging\r\n \r\n return signedPackage;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key export failed', {\r\n error: error.message,\r\n keyType\r\n });\r\n throw new Error(`Failed to export ${keyType} key: ${error.message}`);\r\n }\r\n }\r\n\r\n // Import and verify signed public key\r\n static async importSignedPublicKey(signedPackage, verifyingKey, expectedKeyType = 'ECDH') {\r\n try {\r\n // Debug logs removed\r\n \r\n // Validate package structure\r\n if (!signedPackage || typeof signedPackage !== 'object') {\r\n throw new Error('Invalid signed package format');\r\n }\r\n \r\n const { keyType, keyData, timestamp, version, signature } = signedPackage;\r\n \r\n if (!keyType || !keyData || !timestamp || !signature) {\r\n throw new Error('Missing required fields in signed package');\r\n }\r\n \r\n if (!EnhancedSecureCryptoUtils.constantTimeCompare(keyType, expectedKeyType)) {\r\n throw new Error(`Key type mismatch: expected ${expectedKeyType}, got ${keyType}`);\r\n }\r\n \r\n // Check timestamp (reject keys older than 1 hour)\r\n const keyAge = Date.now() - timestamp;\r\n if (keyAge > 3600000) {\r\n throw new Error('Signed key package is too old');\r\n }\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType);\r\n \r\n // Verify signature\r\n const packageCopy = { keyType, keyData, timestamp, version };\r\n const packageString = JSON.stringify(packageCopy);\r\n // Debug logs removed\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString);\r\n // Debug logs removed\r\n \r\n if (!isValidSignature) {\r\n throw new Error('Invalid signature on key package - possible MITM attack');\r\n }\r\n \r\n // Import the key with fallback support\r\n const keyBytes = new Uint8Array(keyData);\r\n \r\n // Try P-384 first\r\n try {\r\n const algorithm = keyType === 'ECDH' ?\r\n { name: 'ECDH', namedCurve: 'P-384' }\r\n : { name: 'ECDSA', namedCurve: 'P-384' };\r\n \r\n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\r\n \r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n algorithm,\r\n false, // Non-extractable\r\n keyUsages\r\n );\r\n \r\n // Removed public key import info logging\r\n \r\n return publicKey;\r\n } catch (p384Error) {\r\n // Fallback to P-256\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Elliptic curve P-384 import failed, switching curve', { error: p384Error.message });\r\n \r\n const algorithm = keyType === 'ECDH' ?\r\n { name: 'ECDH', namedCurve: 'P-256' }\r\n : { name: 'ECDSA', namedCurve: 'P-256' };\r\n \r\n const keyUsages = keyType === 'ECDH' ? [] : ['verify'];\r\n \r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n algorithm,\r\n false, // Non-extractable\r\n keyUsages\r\n );\r\n \r\n // Removed public key import info logging\r\n \r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed public key import failed', {\r\n error: error.message,\r\n expectedKeyType\r\n });\r\n throw new Error(`Failed to import the signed key: ${error.message}`);\r\n }\r\n }\r\n\r\n // Legacy export for backward compatibility\r\n static async exportPublicKey(publicKey) {\r\n try {\r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const keyData = Array.from(new Uint8Array(exported));\r\n \r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\r\n \r\n // Removed legacy public key export info logging\r\n return keyData;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key export failed', { error: error.message });\r\n throw new Error('Failed to export the public key');\r\n }\r\n }\r\n\r\n // Legacy import for backward compatibility with fallback\r\n static async importPublicKey(keyData) {\r\n try {\r\n await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH');\r\n \r\n const keyBytes = new Uint8Array(keyData);\r\n \r\n // Try P-384 first\r\n try {\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable\r\n []\r\n );\r\n \r\n // Removed legacy public key import info logging\r\n return publicKey;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\r\n \r\n // Fallback to P-256\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: 'ECDH',\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable\r\n []\r\n );\r\n \r\n // Removed legacy public key import info logging\r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key import failed', { error: error.message });\r\n throw new Error('Failed to import the public key');\r\n }\r\n }\r\n\r\n\r\n // Method to check if a key is trusted\r\n static isKeyTrusted(keyOrFingerprint) {\r\n if (keyOrFingerprint instanceof CryptoKey) {\r\n const meta = EnhancedSecureCryptoUtils._keyMetadata.get(keyOrFingerprint);\r\n return meta ? meta.trusted === true : false;\r\n } else if (keyOrFingerprint && keyOrFingerprint._securityMetadata) {\r\n // Check by key metadata\r\n return keyOrFingerprint._securityMetadata.trusted === true;\r\n }\r\n\r\n return false;\r\n }\r\n\r\n static async importPublicKeyFromSignedPackage(signedPackage, verifyingKey = null, options = {}) {\r\n try {\r\n if (!signedPackage || !signedPackage.keyData || !signedPackage.signature) {\r\n throw new Error('Invalid signed key package format');\r\n }\r\n\r\n // Validate all required fields are present\r\n const requiredFields = ['keyData', 'signature', 'keyType', 'timestamp', 'version'];\r\n const missingFields = requiredFields.filter(field => !signedPackage[field]);\r\n\r\n if (missingFields.length > 0) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing required fields in signed package', {\r\n missingFields: missingFields,\r\n availableFields: Object.keys(signedPackage)\r\n });\r\n throw new Error(`Required fields are missing in the signed package: ${missingFields.join(', ')}`);\r\n }\r\n\r\n // SECURITY ENHANCEMENT: MANDATORY signature verification for signed packages\r\n if (!verifyingKey) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY VIOLATION: Signed package received without verifying key', {\r\n keyType: signedPackage.keyType,\r\n keySize: signedPackage.keyData.length,\r\n timestamp: signedPackage.timestamp,\r\n version: signedPackage.version,\r\n securityRisk: 'HIGH - Potential MITM attack vector'\r\n });\r\n\r\n // REJECT the signed package if no verifying key provided\r\n throw new Error('CRITICAL SECURITY ERROR: Signed key package received without a verification key. ' +\r\n 'This may indicate a possible MITM attack attempt. Import rejected for security reasons.');\r\n }\r\n\r\n // \u041E\u0411\u041D\u041E\u0412\u041B\u0415\u041D\u041E: \u0418\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u043C \u0443\u043B\u0443\u0447\u0448\u0435\u043D\u043D\u0443\u044E \u0432\u0430\u043B\u0438\u0434\u0430\u0446\u0438\u044E\r\n await EnhancedSecureCryptoUtils.validateKeyStructure(signedPackage.keyData, signedPackage.keyType || 'ECDH');\r\n\r\n // MANDATORY signature verification when verifyingKey is provided\r\n const packageCopy = { ...signedPackage };\r\n delete packageCopy.signature;\r\n const packageString = JSON.stringify(packageCopy);\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signedPackage.signature, packageString);\r\n\r\n if (!isValidSignature) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY BREACH: Invalid signature detected - MITM attack prevented', {\r\n keyType: signedPackage.keyType,\r\n keySize: signedPackage.keyData.length,\r\n timestamp: signedPackage.timestamp,\r\n version: signedPackage.version,\r\n attackPrevented: true\r\n });\r\n throw new Error('CRITICAL SECURITY ERROR: Invalid key signature detected. ' +\r\n 'This indicates a possible MITM attack attempt. Key import rejected.');\r\n }\r\n\r\n // Additional MITM protection: Check for key reuse and suspicious patterns\r\n const keyFingerprint = await EnhancedSecureCryptoUtils.calculateKeyFingerprint(signedPackage.keyData);\r\n\r\n // Log successful verification with security details\r\n // Removed signature verification pass details to avoid key-related logging\r\n\r\n // Import the public key with fallback\r\n const keyBytes = new Uint8Array(signedPackage.keyData);\r\n const keyType = signedPackage.keyType || 'ECDH';\r\n\r\n // Try P-384 first\r\n try {\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: keyType,\r\n namedCurve: 'P-384'\r\n },\r\n false, // Non-extractable\r\n keyType === 'ECDSA' ? ['verify'] : []\r\n );\r\n\r\n // Use WeakMap to store metadata\r\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\r\n trusted: true,\r\n verificationStatus: 'VERIFIED_SECURE',\r\n verificationTimestamp: Date.now()\r\n });\r\n\r\n return publicKey;\r\n } catch (p384Error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message });\r\n\r\n // Fallback to P-256\r\n const publicKey = await crypto.subtle.importKey(\r\n 'spki',\r\n keyBytes,\r\n {\r\n name: keyType,\r\n namedCurve: 'P-256'\r\n },\r\n false, // Non-extractable\r\n keyType === 'ECDSA' ? ['verify'] : []\r\n );\r\n\r\n // Use WeakMap to store metadata\r\n EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, {\r\n trusted: true,\r\n verificationStatus: 'VERIFIED_SECURE',\r\n verificationTimestamp: Date.now()\r\n });\r\n\r\n return publicKey;\r\n }\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed package key import failed', {\r\n error: error.message,\r\n securityImplications: 'Potential security breach prevented'\r\n });\r\n throw new Error(`Failed to import the public key from the signed package: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced key derivation with metadata protection and 64-byte salt\r\n static async deriveSharedKeys(privateKey, publicKey, salt) {\r\n try {\r\n // Removed detailed key derivation logging\r\n \r\n // Validate input parameters are CryptoKey instances\r\n if (!(privateKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Private key is not a CryptoKey', {\r\n privateKeyType: typeof privateKey,\r\n privateKeyAlgorithm: privateKey?.algorithm?.name\r\n });\r\n throw new Error('The private key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(publicKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key is not a CryptoKey', {\r\n publicKeyType: typeof publicKey,\r\n publicKeyAlgorithm: publicKey?.algorithm?.name\r\n });\r\n throw new Error('The public key is not a valid CryptoKey.');\r\n }\r\n \r\n // Validate salt size (should be 64 bytes for enhanced security)\r\n if (!salt || salt.length !== 64) {\r\n throw new Error('Salt must be exactly 64 bytes for enhanced security');\r\n }\r\n \r\n const saltBytes = new Uint8Array(salt);\r\n const encoder = new TextEncoder();\r\n \r\n // Step 1: Derive raw ECDH shared secret using pure ECDH\r\n let rawSharedSecret;\r\n try {\r\n // Removed detailed key derivation logging\r\n \r\n // Use pure ECDH to derive raw key material\r\n const rawKeyMaterial = await crypto.subtle.deriveKey(\r\n {\r\n name: 'ECDH',\r\n public: publicKey\r\n },\r\n privateKey,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n true, // Extractable\r\n ['encrypt', 'decrypt']\r\n );\r\n \r\n // Export the raw key material\r\n const rawKeyData = await crypto.subtle.exportKey('raw', rawKeyMaterial);\r\n \r\n // Import as HKDF key material for further derivation\r\n rawSharedSecret = await crypto.subtle.importKey(\r\n 'raw',\r\n rawKeyData,\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256'\r\n },\r\n false,\r\n ['deriveKey']\r\n );\r\n \r\n // Removed detailed key derivation logging\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH derivation failed', { \r\n error: error.message\r\n });\r\n throw error;\r\n }\r\n \r\n // Step 2: Use HKDF to derive specific keys directly\r\n // Removed detailed key derivation logging\r\n\r\n // Step 3: Derive specific keys using HKDF with unique info parameters\r\n // Each key uses unique info parameter for proper separation\r\n \r\n // Derive message encryption key (messageKey)\r\n let messageKey;\r\n messageKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('message-encryption-v4')\r\n },\r\n rawSharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable for enhanced security\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n // Derive MAC key for message authentication\r\n let macKey;\r\n macKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('message-authentication-v4')\r\n },\r\n rawSharedSecret,\r\n {\r\n name: 'HMAC',\r\n hash: 'SHA-256'\r\n },\r\n false, // Non-extractable\r\n ['sign', 'verify']\r\n );\r\n\r\n // Derive Perfect Forward Secrecy key (pfsKey)\r\n let pfsKey;\r\n pfsKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('perfect-forward-secrecy-v4')\r\n },\r\n rawSharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n // Derive separate metadata encryption key\r\n let metadataKey;\r\n metadataKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('metadata-protection-v4')\r\n },\r\n rawSharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n false, // Non-extractable\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n // Generate temporary extractable key for fingerprint calculation\r\n let fingerprintKey;\r\n fingerprintKey = await crypto.subtle.deriveKey(\r\n {\r\n name: 'HKDF',\r\n hash: 'SHA-256',\r\n salt: saltBytes,\r\n info: encoder.encode('fingerprint-generation-v4')\r\n },\r\n rawSharedSecret,\r\n {\r\n name: 'AES-GCM',\r\n length: 256\r\n },\r\n true, // Extractable only for fingerprint\r\n ['encrypt', 'decrypt']\r\n );\r\n\r\n // Generate key fingerprint for verification\r\n const fingerprintKeyData = await crypto.subtle.exportKey('raw', fingerprintKey);\r\n const fingerprint = await EnhancedSecureCryptoUtils.generateKeyFingerprint(Array.from(new Uint8Array(fingerprintKeyData)));\r\n\r\n // Validate that all derived keys are CryptoKey instances\r\n if (!(messageKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived message key is not a CryptoKey', {\r\n messageKeyType: typeof messageKey,\r\n messageKeyAlgorithm: messageKey?.algorithm?.name\r\n });\r\n throw new Error('The derived message key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(macKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived MAC key is not a CryptoKey', {\r\n macKeyType: typeof macKey,\r\n macKeyAlgorithm: macKey?.algorithm?.name\r\n });\r\n throw new Error('The derived MAC key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(pfsKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived PFS key is not a CryptoKey', {\r\n pfsKeyType: typeof pfsKey,\r\n pfsKeyAlgorithm: pfsKey?.algorithm?.name\r\n });\r\n throw new Error('The derived PFS key is not a valid CryptoKey.');\r\n }\r\n \r\n if (!(metadataKey instanceof CryptoKey)) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived metadata key is not a CryptoKey', {\r\n metadataKeyType: typeof metadataKey,\r\n metadataKeyAlgorithm: metadataKey?.algorithm?.name\r\n });\r\n throw new Error('The derived metadata key is not a valid CryptoKey.');\r\n }\r\n\r\n // Removed detailed key derivation success logging\r\n\r\n return {\r\n messageKey, // Renamed from encryptionKey for clarity\r\n macKey,\r\n pfsKey, // Added Perfect Forward Secrecy key\r\n metadataKey,\r\n fingerprint,\r\n timestamp: Date.now(),\r\n version: '4.0'\r\n };\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', { \r\n error: error.message,\r\n errorStack: error.stack,\r\n privateKeyType: typeof privateKey,\r\n publicKeyType: typeof publicKey,\r\n saltLength: salt?.length,\r\n privateKeyAlgorithm: privateKey?.algorithm?.name,\r\n publicKeyAlgorithm: publicKey?.algorithm?.name\r\n });\r\n throw new Error(`Failed to create shared encryption keys: ${error.message}`);\r\n }\r\n }\r\n\r\n static async generateKeyFingerprint(keyData) {\r\n const keyBuffer = new Uint8Array(keyData);\r\n const hashBuffer = await crypto.subtle.digest('SHA-384', keyBuffer);\r\n const hashArray = Array.from(new Uint8Array(hashBuffer));\r\n return hashArray.slice(0, 12).map(b => b.toString(16).padStart(2, '0')).join(':');\r\n }\r\n\r\n // Generate mutual authentication challenge\r\n static generateMutualAuthChallenge() {\r\n const challenge = crypto.getRandomValues(new Uint8Array(48)); // Increased to 48 bytes\r\n const timestamp = Date.now();\r\n const nonce = crypto.getRandomValues(new Uint8Array(16));\r\n \r\n return {\r\n challenge: Array.from(challenge),\r\n timestamp,\r\n nonce: Array.from(nonce),\r\n version: '4.0'\r\n };\r\n }\r\n\r\n // Create cryptographic proof for mutual authentication\r\n static async createAuthProof(challenge, privateKey, publicKey) {\r\n try {\r\n if (!challenge || !challenge.challenge || !challenge.timestamp || !challenge.nonce) {\r\n throw new Error('Invalid challenge structure');\r\n }\r\n \r\n // Check challenge age (max 2 minutes)\r\n const challengeAge = Date.now() - challenge.timestamp;\r\n if (challengeAge > 120000) {\r\n throw new Error('Challenge expired');\r\n }\r\n \r\n // Create proof data\r\n const proofData = {\r\n challenge: challenge.challenge,\r\n timestamp: challenge.timestamp,\r\n nonce: challenge.nonce,\r\n responseTimestamp: Date.now(),\r\n publicKeyHash: await EnhancedSecureCryptoUtils.hashPublicKey(publicKey)\r\n };\r\n \r\n // Sign the proof\r\n const proofString = JSON.stringify(proofData);\r\n const signature = await EnhancedSecureCryptoUtils.signData(privateKey, proofString);\r\n \r\n const proof = {\r\n ...proofData,\r\n signature,\r\n version: '4.0'\r\n };\r\n \r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof created', {\r\n challengeAge: Math.round(challengeAge / 1000) + 's'\r\n });\r\n \r\n return proof;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof creation failed', { error: error.message });\r\n throw new Error(`Failed to create cryptographic proof: ${error.message}`);\r\n }\r\n }\r\n\r\n // Verify mutual authentication proof\r\n static async verifyAuthProof(proof, challenge, publicKey) {\r\n try {\r\n await new Promise(resolve => setTimeout(resolve, Math.floor(Math.random() * 20) + 5));\r\n // Assert the public key is valid and has the correct usage\r\n EnhancedSecureCryptoUtils.assertCryptoKey(publicKey, 'ECDSA', ['verify']);\r\n\r\n if (!proof || !challenge || !publicKey) {\r\n throw new Error('Missing required parameters for proof verification');\r\n }\r\n\r\n // Validate proof structure\r\n const requiredFields = ['challenge', 'timestamp', 'nonce', 'responseTimestamp', 'publicKeyHash', 'signature'];\r\n for (const field of requiredFields) {\r\n if (!proof[field]) {\r\n throw new Error(`Missing required field: ${field}`);\r\n }\r\n }\r\n\r\n // Verify challenge matches\r\n if (!EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.challenge, challenge.challenge) ||\r\n proof.timestamp !== challenge.timestamp ||\r\n !EnhancedSecureCryptoUtils.constantTimeCompareArrays(proof.nonce, challenge.nonce)) {\r\n throw new Error('Challenge mismatch - possible replay attack');\r\n }\r\n\r\n // Check response time (max 30 minutes for better UX)\r\n const responseAge = Date.now() - proof.responseTimestamp;\r\n if (responseAge > 1800000) {\r\n throw new Error('Proof response expired');\r\n }\r\n\r\n // Verify public key hash\r\n const expectedHash = await EnhancedSecureCryptoUtils.hashPublicKey(publicKey);\r\n if (!EnhancedSecureCryptoUtils.constantTimeCompare(proof.publicKeyHash, expectedHash)) {\r\n throw new Error('Public key hash mismatch');\r\n }\r\n\r\n // Verify signature\r\n const proofCopy = { ...proof };\r\n delete proofCopy.signature;\r\n const proofString = JSON.stringify(proofCopy);\r\n const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(publicKey, proof.signature, proofString);\r\n\r\n if (!isValidSignature) {\r\n throw new Error('Invalid proof signature');\r\n }\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof verified successfully', {\r\n responseAge: Math.round(responseAge / 1000) + 's'\r\n });\r\n\r\n return true;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof verification failed', { error: error.message });\r\n throw new Error(`Failed to verify cryptographic proof: ${error.message}`);\r\n }\r\n }\r\n\r\n // Hash public key for verification\r\n static async hashPublicKey(publicKey) {\r\n try {\r\n const exported = await crypto.subtle.exportKey('spki', publicKey);\r\n const hash = await crypto.subtle.digest('SHA-384', exported);\r\n const hashArray = Array.from(new Uint8Array(hash));\r\n return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key hashing failed', { error: error.message });\r\n throw new Error('Failed to create hash of the public key');\r\n }\r\n }\r\n\r\n // Legacy authentication challenge for backward compatibility\r\n static generateAuthChallenge() {\r\n const challenge = crypto.getRandomValues(new Uint8Array(32));\r\n return Array.from(challenge);\r\n }\r\n\r\n // Generate verification code for out-of-band authentication\r\n static generateVerificationCode() {\r\n const chars = '0123456789ABCDEF';\r\n const charCount = chars.length;\r\n let result = '';\r\n \r\n // Use rejection sampling to avoid bias\r\n for (let i = 0; i < 6; i++) {\r\n let randomByte;\r\n do {\r\n randomByte = crypto.getRandomValues(new Uint8Array(1))[0];\r\n } while (randomByte >= 256 - (256 % charCount)); // Reject biased values\r\n \r\n result += chars[randomByte % charCount];\r\n }\r\n \r\n return result.match(/.{1,2}/g).join('-');\r\n }\r\n\r\n // Enhanced message encryption with metadata protection and sequence numbers\r\n static async encryptMessage(message, encryptionKey, macKey, metadataKey, messageId, sequenceNumber = 0) {\r\n try {\r\n if (!message || typeof message !== 'string') {\r\n throw new Error('Invalid message format');\r\n }\r\n\r\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['encrypt']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['sign']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['encrypt']);\r\n\r\n const encoder = new TextEncoder();\r\n const messageData = encoder.encode(message);\r\n const messageIv = crypto.getRandomValues(new Uint8Array(12));\r\n const metadataIv = crypto.getRandomValues(new Uint8Array(12));\r\n const timestamp = Date.now();\r\n\r\n const paddingSize = 16 - (messageData.length % 16);\r\n const paddedMessage = new Uint8Array(messageData.length + paddingSize);\r\n paddedMessage.set(messageData);\r\n const padding = crypto.getRandomValues(new Uint8Array(paddingSize));\r\n paddedMessage.set(padding, messageData.length);\r\n\r\n const encryptedMessage = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: messageIv },\r\n encryptionKey,\r\n paddedMessage\r\n );\r\n\r\n const metadata = {\r\n id: messageId,\r\n timestamp: timestamp,\r\n sequenceNumber: sequenceNumber,\r\n originalLength: messageData.length,\r\n version: '4.0'\r\n };\r\n\r\n const metadataStr = JSON.stringify(EnhancedSecureCryptoUtils.sortObjectKeys(metadata));\r\n const encryptedMetadata = await crypto.subtle.encrypt(\r\n { name: 'AES-GCM', iv: metadataIv },\r\n metadataKey,\r\n encoder.encode(metadataStr)\r\n );\r\n\r\n const payload = {\r\n messageIv: Array.from(messageIv),\r\n messageData: Array.from(new Uint8Array(encryptedMessage)),\r\n metadataIv: Array.from(metadataIv),\r\n metadataData: Array.from(new Uint8Array(encryptedMetadata)),\r\n version: '4.0'\r\n };\r\n\r\n const sortedPayload = EnhancedSecureCryptoUtils.sortObjectKeys(payload);\r\n const payloadStr = JSON.stringify(sortedPayload);\r\n\r\n const mac = await crypto.subtle.sign(\r\n 'HMAC',\r\n macKey,\r\n encoder.encode(payloadStr)\r\n );\r\n\r\n payload.mac = Array.from(new Uint8Array(mac));\r\n\r\n // Logging removed to avoid noisy console output\r\n\r\n return payload;\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message encryption failed', {\r\n error: error.message,\r\n messageId\r\n });\r\n throw new Error(`Failed to encrypt the message: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced message decryption with metadata protection and sequence validation\r\n static async decryptMessage(encryptedPayload, encryptionKey, macKey, metadataKey, expectedSequenceNumber = null) {\r\n try {\r\n EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['decrypt']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['verify']);\r\n EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['decrypt']);\r\n\r\n const requiredFields = ['messageIv', 'messageData', 'metadataIv', 'metadataData', 'mac', 'version'];\r\n for (const field of requiredFields) {\r\n if (!encryptedPayload[field]) {\r\n throw new Error(`Missing required field: ${field}`);\r\n }\r\n }\r\n\r\n const payloadCopy = { ...encryptedPayload };\r\n delete payloadCopy.mac;\r\n const sortedPayloadCopy = EnhancedSecureCryptoUtils.sortObjectKeys(payloadCopy);\r\n const payloadStr = JSON.stringify(sortedPayloadCopy);\r\n\r\n const macValid = await crypto.subtle.verify(\r\n 'HMAC',\r\n macKey,\r\n new Uint8Array(encryptedPayload.mac),\r\n new TextEncoder().encode(payloadStr)\r\n );\r\n\r\n if (!macValid) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'MAC verification failed', {\r\n payloadFields: Object.keys(encryptedPayload),\r\n macLength: encryptedPayload.mac?.length\r\n });\r\n throw new Error('Message authentication failed - possible tampering');\r\n }\r\n\r\n const metadataIv = new Uint8Array(encryptedPayload.metadataIv);\r\n const metadataData = new Uint8Array(encryptedPayload.metadataData);\r\n\r\n const decryptedMetadataBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: metadataIv },\r\n metadataKey,\r\n metadataData\r\n );\r\n\r\n const metadataStr = new TextDecoder().decode(decryptedMetadataBuffer);\r\n const metadata = JSON.parse(metadataStr);\r\n\r\n if (!metadata.id || !metadata.timestamp || metadata.sequenceNumber === undefined || !metadata.originalLength) {\r\n throw new Error('Invalid metadata structure');\r\n }\r\n\r\n const messageAge = Date.now() - metadata.timestamp;\r\n if (messageAge > 1800000) { // 30 minutes for better UX\r\n throw new Error('Message expired (older than 5 minutes)');\r\n }\r\n\r\n if (expectedSequenceNumber !== null) {\r\n if (metadata.sequenceNumber < expectedSequenceNumber) {\r\n EnhancedSecureCryptoUtils.secureLog.log('warn', 'Received message with lower sequence number, possible queued message', {\r\n expected: expectedSequenceNumber,\r\n received: metadata.sequenceNumber,\r\n messageId: metadata.id\r\n });\r\n } else if (metadata.sequenceNumber > expectedSequenceNumber + 10) {\r\n throw new Error(`Sequence number gap too large: expected around ${expectedSequenceNumber}, got ${metadata.sequenceNumber}`);\r\n }\r\n }\r\n\r\n const messageIv = new Uint8Array(encryptedPayload.messageIv);\r\n const messageData = new Uint8Array(encryptedPayload.messageData);\r\n\r\n const decryptedMessageBuffer = await crypto.subtle.decrypt(\r\n { name: 'AES-GCM', iv: messageIv },\r\n encryptionKey,\r\n messageData\r\n );\r\n\r\n const paddedMessage = new Uint8Array(decryptedMessageBuffer);\r\n const originalMessage = paddedMessage.slice(0, metadata.originalLength);\r\n\r\n const decoder = new TextDecoder();\r\n const message = decoder.decode(originalMessage);\r\n\r\n EnhancedSecureCryptoUtils.secureLog.log('info', 'Message decrypted successfully', {\r\n messageId: metadata.id,\r\n sequenceNumber: metadata.sequenceNumber,\r\n messageAge: Math.round(messageAge / 1000) + 's'\r\n });\r\n\r\n return {\r\n message: message,\r\n messageId: metadata.id,\r\n timestamp: metadata.timestamp,\r\n sequenceNumber: metadata.sequenceNumber\r\n };\r\n } catch (error) {\r\n EnhancedSecureCryptoUtils.secureLog.log('error', 'Message decryption failed', { error: error.message });\r\n throw new Error(`Failed to decrypt the message: ${error.message}`);\r\n }\r\n }\r\n\r\n // Enhanced input sanitization with iterative processing to handle edge cases\r\n static sanitizeMessage(message) {\r\n if (typeof message !== 'string') {\r\n throw new Error('Message must be a string');\r\n }\r\n \r\n // Helper function to apply replacement until stable\r\n function replaceUntilStable(str, pattern, replacement = '') {\r\n let previous;\r\n do {\r\n previous = str;\r\n str = str.replace(pattern, replacement);\r\n } while (str !== previous);\r\n return str;\r\n }\r\n \r\n // Define all dangerous patterns that need to be removed\r\n const dangerousPatterns = [\r\n // Script tags with various formats\r\n /\r\n /\r\n /\r\n /\r\n /