diff --git a/.github/workflows/deploy-with-cache-purge.yml b/.github/workflows/deploy-with-cache-purge.yml
new file mode 100644
index 0000000..176bb14
--- /dev/null
+++ b/.github/workflows/deploy-with-cache-purge.yml
@@ -0,0 +1,47 @@
+name: Deploy Application
+
+on:
+ push:
+ branches:
+ - main
+ - master
+ workflow_dispatch:
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: '18'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build application
+ run: npm run build
+
+ - name: Deploy to server
+ # Добавьте ваш процесс деплоя здесь
+ # Например: rsync, scp, FTP, etc.
+ run: |
+ echo "Deploying to server..."
+ # Ваш скрипт деплоя
+ # Пример для rsync:
+ # rsync -avz --delete dist/ user@server:/var/www/securebit-chat/public/
+
+ # Автоматическая очистка кеша Cloudflare отключена
+ # Для ручной очистки используйте: node scripts/purge-cloudflare-cache.js
+ # Или очистите кеш через Cloudflare Dashboard
+
+ - name: Verify deployment
+ run: |
+ echo "✅ Deployment completed"
+ echo "Note: Cloudflare cache purge is disabled. Clear cache manually if needed."
+
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..2817b06
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,189 @@
+# SecureBit.chat - Apache Configuration
+# Comprehensive caching configuration for forced updates
+
+# Enable mod_rewrite
+
+ RewriteEngine On
+ RewriteBase /
+
+
+# ============================================
+# CRITICAL FILES - NO CACHING
+# ============================================
+
+# meta.json - versioning file (never cache)
+
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+ Header set X-Content-Type-Options "nosniff"
+
+
+
+# HTML files - always fresh
+
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+ # Remove ETag for validation
+ Header unset ETag
+ FileETag None
+
+
+
+# Service Worker - no cache
+
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+ Header set Service-Worker-Allowed "/"
+
+
+
+# manifest.json - no cache
+
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+
+
+
+# ============================================
+# STATIC RESOURCES - AGGRESSIVE CACHING
+# ============================================
+
+# JavaScript files in dist/ - no cache (for updates)
+
+
+ Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
+ Header set Pragma "no-cache"
+ Header set Expires "0"
+ Header set X-Content-Type-Options "nosniff"
+
+
+
+# JavaScript files with hashes in other locations - long cache
+
+
+ # Files with hashes in name - cache for one year
+ Header set Cache-Control "public, max-age=31536000, immutable"
+ Header set X-Content-Type-Options "nosniff"
+
+
+
+# CSS files - long cache
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
+
+
+# Images - long cache
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
+
+
+# Fonts - long cache
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+ Header set Access-Control-Allow-Origin "*"
+
+
+
+# Audio/Video - long cache
+
+
+ Header set Cache-Control "public, max-age=31536000, immutable"
+
+
+
+# ============================================
+# SECURITY
+# ============================================
+
+# XSS Protection
+
+ Header set X-XSS-Protection "1; mode=block"
+ Header set X-Content-Type-Options "nosniff"
+ Header set Referrer-Policy "strict-origin-when-cross-origin"
+ Header set X-Frame-Options "DENY"
+
+
+# Content Security Policy (already configured in HTML, but can add header)
+
+ # Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
+
+
+# ============================================
+# GZIP COMPRESSION
+# ============================================
+
+
+ # Compress text files
+ AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml
+
+ # Compress fonts
+ AddOutputFilterByType DEFLATE font/woff font/woff2 application/font-woff application/font-woff2
+
+
+# ============================================
+# MIME TYPES
+# ============================================
+
+
+ # JavaScript modules
+ AddType application/javascript .js .mjs
+ AddType application/json .json
+
+ # Fonts
+ AddType font/woff .woff
+ AddType font/woff2 .woff2
+ AddType application/font-woff .woff
+ AddType application/font-woff2 .woff2
+
+ # Service Worker
+ AddType application/javascript .js
+ AddType application/manifest+json .webmanifest
+
+
+# ============================================
+# CLOUDFLARE RULES
+# ============================================
+
+# Cloudflare can cache static files, but should not cache:
+# - meta.json
+# - index.html
+# - sw.js
+# - manifest.json
+
+# These rules are applied at Cloudflare Page Rules level
+# (see CLOUDFLARE_SETUP.md documentation)
+
+# ============================================
+# SPA FALLBACK
+# ============================================
+
+# If file not found, redirect to index.html (for SPA routing)
+
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_URI} !^/meta\.json$
+ RewriteCond %{REQUEST_URI} !^/sw\.js$
+ RewriteCond %{REQUEST_URI} !^/manifest\.json$
+ RewriteRule ^(.*)$ /index.html [L]
+
+
+# ============================================
+# LOGGING (optional)
+# ============================================
+
+# Uncomment for debugging
+# LogLevel rewrite:trace3
+
diff --git a/README.md b/README.md
index 74ec47f..4c25287 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# SecureBit.chat v4.7.53
+# SecureBit.chat v4.7.55
@@ -59,7 +59,7 @@ Community review is welcome. Bug reports and security feedback can be submitted
│ Web Version │ Desktop Apps │ Mobile (Coming) │
│ (This Repo) │ (Tauri v2) │ (Q1 2026) │
│ Browser PWA │ Windows/Mac/ │ iOS/Android │
-│ v4.7.53 │ Linux │ Native Apps │
+│ v4.7.55 │ Linux │ Native Apps │
│ │ v0.1.0 Beta │ │
└────────┬─────────┴────────┬─────────┴──────────┬───────────┘
│ │ │
@@ -100,7 +100,7 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
| Platform | Status | Version | Link |
|----------|--------|---------|------|
-| **Web Browser** | Production | v4.7.53 | [Launch Web App](https://securebitchat.github.io/securebit-chat/) |
+| **Web Browser** | Production | v4.7.55 | [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) |
@@ -123,7 +123,7 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
---
-## ✨ What's New in v4.7.53
+## ✨ What's New in v4.7.55
### Desktop Edition Release
@@ -198,7 +198,7 @@ SecureBit.chat is a revolutionary peer-to-peer messenger that prioritizes your p
## 🗺️ Roadmap
-**Current: v4.7.53** - Desktop Edition Available
+**Current: v4.7.55** - Desktop Edition Available
### Released Versions
@@ -417,7 +417,7 @@ Want to improve security? Contribute to the cryptographic core:
| 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-chat](https://github.com/SecureBitChat/securebit-chat)** | Web application (this repo) | ✅ Production v4.7.55 | 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 |
@@ -547,7 +547,7 @@ Want to improve security? Contribute to the core:
## Project Status
### Active Development
-- **Web Version** - Stable (v4.7.53), receiving bug fixes and improvements
+- **Web Version** - Stable (v4.7.55), 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)
@@ -575,7 +575,7 @@ Want to improve security? Contribute to the core:
---
-**Latest Release: v4.7.53** - Desktop Edition Available
+**Latest Release: v4.7.55** - Desktop Edition Available
**Desktop Apps: v0.1.0** - Public Beta Available
**Mobile Apps: Coming Q1 2026**
diff --git a/assets/tailwind.css b/assets/tailwind.css
index 59e2fae..3a7be6f 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}.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
+*,: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}.z-\[9999\]{z-index:9999}.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-2\.5{height:.625rem}.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-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity,1))}.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-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/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{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.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}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.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-blue-600{--tw-text-opacity:1;color:rgb(37 99 235/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-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity,1))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/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}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.\[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-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/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}}@media (prefers-color-scheme:dark){.dark\:border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.dark\:bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity,1))}.dark\:bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity,1))}.dark\:bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity,1))}.dark\:text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.dark\:text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.dark\:text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.dark\:hover\:text-white:hover,.dark\:text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}}
\ No newline at end of file
diff --git a/dist/app-boot.js b/dist/app-boot.js
index 7de930c..f982a04 100644
--- a/dist/app-boot.js
+++ b/dist/app-boot.js
@@ -15314,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.7.53")
+ }, "End-to-end freedom v4.7.55")
])
]),
// Status and Controls - Responsive
diff --git a/dist/app-boot.js.map b/dist/app-boot.js.map
index c8d17c2..67a52d0 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 textEl.style.wordWrap = 'break-word';\n textEl.style.overflowWrap = 'break-word';\n textEl.style.whiteSpace = 'normal';\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 /
-
-
+
+
+
+
+
-
-
+
+
diff --git a/manifest.json b/manifest.json
index eb0e1bd..304935a 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,5 +1,5 @@
{
- "name": "SecureBit.chat v4.7.53 - ECDH + DTLS + SAS",
+ "name": "SecureBit.chat v4.7.55 - ECDH + DTLS + SAS",
"short_name": "SecureBit",
"description": "P2P messenger with ECDH + DTLS + SAS security, military-grade cryptography and Lightning Network payments",
"start_url": "./",
diff --git a/meta.json b/meta.json
new file mode 100644
index 0000000..d67d68e
--- /dev/null
+++ b/meta.json
@@ -0,0 +1,10 @@
+{
+ "version": "1767018751497",
+ "buildVersion": "1767018751497",
+ "appVersion": "4.7.55",
+ "buildTime": "2025-12-29T14:32:31.569Z",
+ "buildId": "1767018751497-1b6431a",
+ "gitHash": "1b6431a",
+ "generated": true,
+ "generatedAt": "2025-12-29T14:32:31.571Z"
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 8167328..c256bb6 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,13 @@
{
"name": "securebit-chat",
- "version": "1.0.0",
+ "version": "4.7.55",
"description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html",
"scripts": {
- "build": "npm run build:css && npm run build:js",
+ "build": "npm run build:css && npm run build:js && npm run post-build",
"build:css": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --minify --content \"./index.html,./src/**/*.jsx,./src/**/*.js\"",
"build:js": "npx esbuild src/app.jsx --bundle --format=esm --outfile=dist/app.js --sourcemap && npx esbuild src/scripts/app-boot.js --bundle --format=esm --outfile=dist/app-boot.js --sourcemap && npx esbuild src/scripts/qr-local.js --bundle --format=esm --outfile=dist/qr-local.js --sourcemap",
+ "post-build": "node scripts/post-build.js",
"dev": "npm run build && python -m http.server 8000",
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
"serve": "npx http-server -p 8000",
diff --git a/public/meta.json.example b/public/meta.json.example
new file mode 100644
index 0000000..e9ebb54
--- /dev/null
+++ b/public/meta.json.example
@@ -0,0 +1,11 @@
+{
+ "version": "1735689600000",
+ "buildVersion": "1735689600000",
+ "appVersion": "1.0.0",
+ "buildTime": "2025-01-01T00:00:00.000Z",
+ "buildId": "1735689600000",
+ "gitHash": null,
+ "generated": true,
+ "generatedAt": "2025-01-01T00:00:00.000Z"
+}
+
diff --git a/scripts/post-build.js b/scripts/post-build.js
new file mode 100644
index 0000000..deb4183
--- /dev/null
+++ b/scripts/post-build.js
@@ -0,0 +1,203 @@
+/**
+ * post-build.js - Script for generating meta.json after build
+ *
+ * Generates meta.json file with unique build version (timestamp)
+ * for automatic update detection
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+// Configuration
+const CONFIG = {
+ // Path to public directory (project root where index.html is located)
+ publicDir: path.join(__dirname, '..'),
+
+ // meta.json filename
+ metaFileName: 'meta.json',
+
+ // Version format: 'timestamp' or 'semver'
+ versionFormat: 'timestamp'
+};
+
+/**
+ * Generate unique build version
+ */
+function generateBuildVersion() {
+ // Use timestamp for uniqueness of each build
+ const timestamp = Date.now();
+
+ // Optional: can add git commit hash
+ let gitHash = '';
+ try {
+ const { execSync } = require('child_process');
+ gitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim();
+ } catch (error) {
+ // Git not available or not initialized - ignore
+ }
+
+ return {
+ version: timestamp.toString(),
+ buildTime: new Date().toISOString(),
+ gitHash: gitHash || null,
+ buildId: `${timestamp}${gitHash ? `-${gitHash}` : ''}`
+ };
+}
+
+/**
+ * Read package.json to get application version
+ */
+function getAppVersion() {
+ try {
+ const packageJsonPath = path.join(__dirname, '..', 'package.json');
+ if (fs.existsSync(packageJsonPath)) {
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
+ return packageJson.version || '1.0.0';
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to read package.json:', error.message);
+ }
+ return '1.0.0';
+}
+
+/**
+ * Generate meta.json
+ */
+function generateMetaJson() {
+ try {
+ const buildInfo = generateBuildVersion();
+ const appVersion = getAppVersion();
+
+ const meta = {
+ // Build version (used for update checking)
+ version: buildInfo.version,
+ buildVersion: buildInfo.version,
+
+ // Application version from package.json
+ appVersion: appVersion,
+
+ // Additional information
+ buildTime: buildInfo.buildTime,
+ buildId: buildInfo.buildId,
+ gitHash: buildInfo.gitHash,
+
+ // Metadata
+ generated: true,
+ generatedAt: new Date().toISOString()
+ };
+
+ // Path to meta.json file (in project root where index.html is located)
+ const metaFilePath = path.join(CONFIG.publicDir, CONFIG.metaFileName);
+
+ // Create directory if it doesn't exist
+ const publicDir = path.dirname(metaFilePath);
+ if (!fs.existsSync(publicDir)) {
+ fs.mkdirSync(publicDir, { recursive: true });
+ console.log(`✅ Created directory: ${publicDir}`);
+ }
+
+ // Write meta.json
+ fs.writeFileSync(
+ metaFilePath,
+ JSON.stringify(meta, null, 2),
+ 'utf-8'
+ );
+
+ console.log('✅ meta.json generated successfully');
+ console.log(` Version: ${meta.version}`);
+ console.log(` Build Time: ${meta.buildTime}`);
+ if (meta.gitHash) {
+ console.log(` Git Hash: ${meta.gitHash}`);
+ }
+ console.log(` File: ${metaFilePath}`);
+
+ return meta;
+
+ } catch (error) {
+ console.error('❌ Failed to generate meta.json:', error);
+ process.exit(1);
+ }
+}
+
+/**
+ * Update versions in index.html
+ */
+function updateIndexHtmlVersions(buildVersion) {
+ try {
+ const indexHtmlPath = path.join(CONFIG.publicDir, 'index.html');
+
+ if (!fs.existsSync(indexHtmlPath)) {
+ console.warn('⚠️ index.html not found, skipping version update');
+ return;
+ }
+
+ let indexHtml = fs.readFileSync(indexHtmlPath, 'utf-8');
+
+ // Update versions in query parameters for JS files
+ // Pattern: src="dist/app.js?v=..." or src="dist/app-boot.js?v=..."
+ // Also replace BUILD_VERSION placeholder
+ indexHtml = indexHtml.replace(/\?v=BUILD_VERSION/g, `?v=${buildVersion}`);
+ indexHtml = indexHtml.replace(/\?v=(\d+)/g, `?v=${buildVersion}`);
+
+ fs.writeFileSync(indexHtmlPath, indexHtml, 'utf-8');
+ console.log('✅ index.html versions updated');
+
+ } catch (error) {
+ console.warn('⚠️ Failed to update index.html versions:', error.message);
+ }
+}
+
+/**
+ * Validate generated meta.json
+ */
+function validateMetaJson(meta) {
+ const requiredFields = ['version', 'buildVersion', 'buildTime'];
+ const missingFields = requiredFields.filter(field => !meta[field]);
+
+ if (missingFields.length > 0) {
+ throw new Error(`Missing required fields: ${missingFields.join(', ')}`);
+ }
+
+ if (!/^\d+$/.test(meta.version)) {
+ throw new Error(`Invalid version format: ${meta.version} (expected timestamp)`);
+ }
+
+ console.log('✅ meta.json validation passed');
+}
+
+// Main function
+function main() {
+ console.log('🔨 Generating meta.json...');
+ console.log(` Public directory: ${CONFIG.publicDir}`);
+
+ // Check if public directory exists
+ if (!fs.existsSync(CONFIG.publicDir)) {
+ console.error(`❌ Public directory not found: ${CONFIG.publicDir}`);
+ process.exit(1);
+ }
+
+ // Generate meta.json
+ const meta = generateMetaJson();
+
+ // Validate
+ validateMetaJson(meta);
+
+ // Update versions in index.html
+ updateIndexHtmlVersions(meta.version);
+
+ console.log('✅ Build metadata generation completed');
+}
+
+// Run script
+if (require.main === module) {
+ main();
+}
+
+// Export for use in other scripts
+module.exports = {
+ generateMetaJson,
+ generateBuildVersion,
+ getAppVersion,
+ validateMetaJson
+};
+
diff --git a/scripts/purge-cloudflare-cache.js b/scripts/purge-cloudflare-cache.js
new file mode 100644
index 0000000..36529ea
--- /dev/null
+++ b/scripts/purge-cloudflare-cache.js
@@ -0,0 +1,94 @@
+/**
+ * Скрипт для очистки кеша Cloudflare после деплоя
+ *
+ * Использование:
+ * CLOUDFLARE_API_TOKEN=your_token CLOUDFLARE_ZONE_ID=your_zone_id node scripts/purge-cloudflare-cache.js
+ */
+
+const https = require('https');
+
+const API_TOKEN = process.env.CLOUDFLARE_API_TOKEN;
+const ZONE_ID = process.env.CLOUDFLARE_ZONE_ID;
+const DOMAIN = process.env.CLOUDFLARE_DOMAIN || 'securebit.chat';
+
+if (!API_TOKEN || !ZONE_ID) {
+ console.error('❌ Missing required environment variables:');
+ console.error(' CLOUDFLARE_API_TOKEN - Cloudflare API Token');
+ console.error(' CLOUDFLARE_ZONE_ID - Cloudflare Zone ID');
+ process.exit(1);
+}
+
+// Критичные файлы для очистки
+const CRITICAL_FILES = [
+ `https://${DOMAIN}/meta.json`,
+ `https://${DOMAIN}/index.html`,
+ `https://${DOMAIN}/sw.js`,
+ `https://${DOMAIN}/manifest.json`
+];
+
+async function purgeCache(files) {
+ return new Promise((resolve, reject) => {
+ const data = JSON.stringify({
+ files: files
+ });
+
+ const options = {
+ hostname: 'api.cloudflare.com',
+ port: 443,
+ path: `/client/v4/zones/${ZONE_ID}/purge_cache`,
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${API_TOKEN}`,
+ 'Content-Type': 'application/json',
+ 'Content-Length': data.length
+ }
+ };
+
+ const req = https.request(options, (res) => {
+ let responseData = '';
+
+ res.on('data', (chunk) => {
+ responseData += chunk;
+ });
+
+ res.on('end', () => {
+ if (res.statusCode === 200) {
+ const result = JSON.parse(responseData);
+ if (result.success) {
+ resolve(result);
+ } else {
+ reject(new Error(JSON.stringify(result.errors)));
+ }
+ } else {
+ reject(new Error(`HTTP ${res.statusCode}: ${responseData}`));
+ }
+ });
+ });
+
+ req.on('error', (error) => {
+ reject(error);
+ });
+
+ req.write(data);
+ req.end();
+ });
+}
+
+async function main() {
+ console.log('🔄 Purging Cloudflare cache...');
+ console.log(` Zone ID: ${ZONE_ID}`);
+ console.log(` Domain: ${DOMAIN}`);
+ console.log(` Files: ${CRITICAL_FILES.length}`);
+
+ try {
+ const result = await purgeCache(CRITICAL_FILES);
+ console.log('✅ Cache purged successfully');
+ console.log(` Purged files: ${result.result.files?.length || 0}`);
+ } catch (error) {
+ console.error('❌ Failed to purge cache:', error.message);
+ process.exit(1);
+ }
+}
+
+main();
+
diff --git a/src/app.jsx b/src/app.jsx
index baef7a2..d7561e2 100644
--- a/src/app.jsx
+++ b/src/app.jsx
@@ -1924,7 +1924,7 @@
}
}
- handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.53 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
+ handleMessage(' SecureBit.chat Enhanced Security Edition v4.7.55 - ECDH + DTLS + SAS initialized. Ready to establish a secure connection with ECDH key exchange, DTLS fingerprint verification, and SAS authentication to prevent MITM attacks.', 'system');
const handleBeforeUnload = (event) => {
if (event.type === 'beforeunload' && !isTabSwitching) {
@@ -3747,9 +3747,25 @@
]);
};
+ // UpdateChecker компонент для автоматической проверки обновлений
+ const UpdateCheckerWrapper = ({ children }) => {
+ // Проверяем доступность UpdateChecker
+ if (typeof window !== 'undefined' && window.UpdateChecker) {
+ return React.createElement(window.UpdateChecker, {
+ debug: false
+ }, children);
+ }
+ // Fallback если UpdateChecker не загружен
+ return children;
+ };
+
function initializeApp() {
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
- ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
+ // Оборачиваем приложение в UpdateChecker для автоматической проверки обновлений
+ const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
+ React.createElement(EnhancedSecureP2PChat)
+ );
+ ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
} else {
console.error('Модули не загружены:', {
hasCrypto: !!window.EnhancedSecureCryptoUtils,
@@ -3776,5 +3792,20 @@
}
};
- // Render Enhanced Application
- ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
\ No newline at end of file
+ // Render Enhanced Application with UpdateChecker
+ if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
+ const UpdateCheckerWrapper = ({ children }) => {
+ if (typeof window !== 'undefined' && window.UpdateChecker) {
+ return React.createElement(window.UpdateChecker, {
+ debug: false
+ }, children);
+ }
+ return children;
+ };
+ const AppWithUpdateChecker = React.createElement(UpdateCheckerWrapper, null,
+ React.createElement(EnhancedSecureP2PChat)
+ );
+ ReactDOM.render(AppWithUpdateChecker, document.getElementById('root'));
+ } else {
+ ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById('root'));
+ }
\ No newline at end of file
diff --git a/src/components/UpdateChecker.jsx b/src/components/UpdateChecker.jsx
new file mode 100644
index 0000000..5fb0366
--- /dev/null
+++ b/src/components/UpdateChecker.jsx
@@ -0,0 +1,290 @@
+/**
+ * UpdateChecker - React component for automatic update checking
+ *
+ * Wraps the application and automatically detects new versions,
+ * showing a modal window with update progress
+ */
+
+const UpdateChecker = ({ children, onUpdateAvailable, debug = false }) => {
+ const [updateState, setUpdateState] = React.useState({
+ hasUpdate: false,
+ isUpdating: false,
+ progress: 0,
+ currentVersion: null,
+ newVersion: null,
+ showModal: false
+ });
+
+ const updateManagerRef = React.useRef(null);
+
+ // Initialize UpdateManager
+ React.useEffect(() => {
+ // Check that UpdateManager is available
+ if (typeof window === 'undefined' || !window.UpdateManager) {
+ console.error('❌ UpdateManager not found. Make sure updateManager.js is loaded.');
+ return;
+ }
+
+ // Create UpdateManager instance
+ updateManagerRef.current = new window.UpdateManager({
+ versionUrl: '/meta.json',
+ checkInterval: 60000, // 1 minute
+ checkOnLoad: true,
+ debug: debug,
+ onUpdateAvailable: (updateInfo) => {
+ setUpdateState(prev => ({
+ ...prev,
+ hasUpdate: true,
+ currentVersion: updateInfo.currentVersion,
+ newVersion: updateInfo.newVersion,
+ showModal: true
+ }));
+
+ // Call external callback if available
+ if (onUpdateAvailable) {
+ onUpdateAvailable(updateInfo);
+ }
+ },
+ onError: (error) => {
+ if (debug) {
+ console.warn('Update check error (non-critical):', error);
+ }
+ }
+ });
+
+ // Cleanup on unmount
+ return () => {
+ if (updateManagerRef.current) {
+ updateManagerRef.current.destroy();
+ }
+ };
+ }, [onUpdateAvailable, debug]);
+
+ // Force update handler
+ const handleForceUpdate = async () => {
+ if (!updateManagerRef.current || updateState.isUpdating) {
+ return;
+ }
+
+ setUpdateState(prev => ({
+ ...prev,
+ isUpdating: true,
+ progress: 0
+ }));
+
+ try {
+ // Simulate update progress
+ const progressSteps = [
+ { progress: 10, message: 'Saving data...' },
+ { progress: 30, message: 'Clearing Service Worker caches...' },
+ { progress: 50, message: 'Unregistering Service Workers...' },
+ { progress: 70, message: 'Clearing browser cache...' },
+ { progress: 90, message: 'Updating version...' },
+ { progress: 100, message: 'Reloading application...' }
+ ];
+
+ for (const step of progressSteps) {
+ await new Promise(resolve => setTimeout(resolve, 300));
+ setUpdateState(prev => ({
+ ...prev,
+ progress: step.progress
+ }));
+ }
+
+ // Start force update
+ await updateManagerRef.current.forceUpdate();
+
+ } catch (error) {
+ console.error('❌ Update failed:', error);
+ setUpdateState(prev => ({
+ ...prev,
+ isUpdating: false,
+ progress: 0
+ }));
+
+ // Show error to user
+ alert('Update error. Please refresh the page manually (Ctrl+F5 or Cmd+Shift+R)');
+ }
+ };
+
+ // Close modal (not recommended, but leaving the option)
+ const handleCloseModal = () => {
+ // Warn user
+ if (window.confirm('New version available. Update is recommended for security and stability. Continue without update?')) {
+ setUpdateState(prev => ({
+ ...prev,
+ showModal: false
+ }));
+ }
+ };
+
+ // Format version for display
+ const formatVersion = (version) => {
+ if (!version) return 'N/A';
+ // If version is timestamp, format as date
+ if (/^\d+$/.test(version)) {
+ const date = new Date(parseInt(version));
+ return date.toLocaleString('en-US', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ }
+ return version;
+ };
+
+ return React.createElement(React.Fragment, null, [
+ // Main application content
+ children,
+
+ // Update modal window
+ updateState.showModal && React.createElement('div', {
+ key: 'update-modal',
+ className: 'fixed inset-0 z-[9999] flex items-center justify-center bg-black/80 backdrop-blur-sm',
+ style: {
+ animation: 'fadeIn 0.3s ease-in-out'
+ }
+ }, [
+ React.createElement('div', {
+ key: 'modal-content',
+ className: 'bg-white dark:bg-gray-800 rounded-2xl shadow-2xl p-8 max-w-md w-full mx-4 border border-gray-200 dark:border-gray-700',
+ style: {
+ animation: 'slideUp 0.3s ease-out'
+ }
+ }, [
+ // Header
+ React.createElement('div', {
+ key: 'header',
+ className: 'text-center mb-6'
+ }, [
+ React.createElement('div', {
+ key: 'icon',
+ className: 'w-16 h-16 mx-auto mb-4 bg-blue-500/10 rounded-full flex items-center justify-center'
+ }, [
+ React.createElement('i', {
+ key: 'icon-fa',
+ className: 'fas fa-sync-alt text-blue-500 text-2xl animate-spin'
+ })
+ ]),
+ React.createElement('h2', {
+ key: 'title',
+ className: 'text-2xl font-bold text-gray-900 dark:text-white mb-2'
+ }, 'Update Available'),
+ React.createElement('p', {
+ key: 'subtitle',
+ className: 'text-gray-600 dark:text-gray-300 text-sm'
+ }, 'A new version of the application has been detected')
+ ]),
+
+ // Version information
+ React.createElement('div', {
+ key: 'version-info',
+ className: 'bg-gray-50 dark:bg-gray-900 rounded-lg p-4 mb-6 space-y-2'
+ }, [
+ React.createElement('div', {
+ key: 'current',
+ className: 'flex justify-between items-center'
+ }, [
+ React.createElement('span', {
+ key: 'current-label',
+ className: 'text-sm text-gray-600 dark:text-gray-400'
+ }, 'Current version:'),
+ React.createElement('span', {
+ key: 'current-value',
+ className: 'text-sm font-mono text-gray-900 dark:text-white'
+ }, formatVersion(updateState.currentVersion))
+ ]),
+ React.createElement('div', {
+ key: 'new',
+ className: 'flex justify-between items-center'
+ }, [
+ React.createElement('span', {
+ key: 'new-label',
+ className: 'text-sm text-gray-600 dark:text-gray-400'
+ }, 'New version:'),
+ React.createElement('span', {
+ key: 'new-value',
+ className: 'text-sm font-mono text-blue-600 dark:text-blue-400 font-semibold'
+ }, formatVersion(updateState.newVersion))
+ ])
+ ]),
+
+ // Update progress
+ updateState.isUpdating && React.createElement('div', {
+ key: 'progress',
+ className: 'mb-6'
+ }, [
+ React.createElement('div', {
+ key: 'progress-bar',
+ className: 'w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5 mb-2'
+ }, [
+ React.createElement('div', {
+ key: 'progress-fill',
+ className: 'bg-blue-500 h-2.5 rounded-full transition-all duration-300',
+ style: {
+ width: `${updateState.progress}%`
+ }
+ })
+ ]),
+ React.createElement('p', {
+ key: 'progress-text',
+ className: 'text-center text-sm text-gray-600 dark:text-gray-400'
+ }, `${updateState.progress}%`)
+ ]),
+
+ // Action buttons
+ !updateState.isUpdating && React.createElement('div', {
+ key: 'actions',
+ className: 'flex gap-3'
+ }, [
+ React.createElement('button', {
+ key: 'update-btn',
+ onClick: handleForceUpdate,
+ className: 'flex-1 bg-blue-500 hover:bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg transition-colors duration-200 flex items-center justify-center gap-2',
+ disabled: updateState.isUpdating
+ }, [
+ React.createElement('i', {
+ key: 'update-icon',
+ className: 'fas fa-download'
+ }),
+ React.createElement('span', {
+ key: 'update-text'
+ }, 'Update Now')
+ ]),
+ React.createElement('button', {
+ key: 'close-btn',
+ onClick: handleCloseModal,
+ className: 'px-4 py-3 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white transition-colors duration-200',
+ disabled: updateState.isUpdating
+ }, [
+ React.createElement('i', {
+ key: 'close-icon',
+ className: 'fas fa-times'
+ })
+ ])
+ ]),
+
+ // Update indicator
+ updateState.isUpdating && React.createElement('div', {
+ key: 'updating',
+ className: 'text-center'
+ }, [
+ React.createElement('p', {
+ key: 'updating-text',
+ className: 'text-sm text-gray-600 dark:text-gray-400'
+ }, 'Update in progress...')
+ ])
+ ])
+ ])
+ ]);
+};
+
+// Export for use
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = UpdateChecker;
+} else {
+ window.UpdateChecker = UpdateChecker;
+}
+
diff --git a/src/components/ui/Header.jsx b/src/components/ui/Header.jsx
index 0451b99..6217d1a 100644
--- a/src/components/ui/Header.jsx
+++ b/src/components/ui/Header.jsx
@@ -539,7 +539,7 @@ const EnhancedMinimalHeader = ({
React.createElement('p', {
key: 'subtitle',
className: 'text-xs sm:text-sm text-muted hidden sm:block'
- }, 'End-to-end freedom v4.7.53')
+ }, 'End-to-end freedom v4.7.55')
])
]),
diff --git a/src/utils/updateManager.js b/src/utils/updateManager.js
new file mode 100644
index 0000000..2d35ec4
--- /dev/null
+++ b/src/utils/updateManager.js
@@ -0,0 +1,540 @@
+/**
+ * UpdateManager - Comprehensive PWA update management system
+ *
+ * Automatically detects new application versions and forcefully
+ * updates all cache levels: Service Worker, browser cache, localStorage
+ *
+ * @class UpdateManager
+ */
+class UpdateManager {
+ constructor(options = {}) {
+ this.options = {
+ // URL for version check (meta.json)
+ versionUrl: options.versionUrl || '/meta.json',
+
+ // Update check interval (ms)
+ checkInterval: options.checkInterval || 60000, // 1 minute
+
+ // Local storage key for version
+ versionKey: options.versionKey || 'app_version',
+
+ // Keys for preserving critical data before cleanup
+ preserveKeys: options.preserveKeys || [
+ 'auth_token',
+ 'user_settings',
+ 'encryption_keys',
+ 'peer_connections'
+ ],
+
+ // Callback on update detection
+ onUpdateAvailable: options.onUpdateAvailable || null,
+
+ // Callback on error
+ onError: options.onError || null,
+
+ // Logging
+ debug: options.debug || false,
+
+ // Force check on load
+ checkOnLoad: options.checkOnLoad !== false,
+
+ // Request timeout
+ requestTimeout: options.requestTimeout || 10000
+ };
+
+ this.currentVersion = null;
+ this.serverVersion = null;
+ this.checkIntervalId = null;
+ this.isUpdating = false;
+ this.updatePromise = null;
+
+ // Initialization
+ this.init();
+ }
+
+ /**
+ * Initialize update manager
+ */
+ async init() {
+ try {
+ // Load current version from localStorage
+ this.currentVersion = this.getLocalVersion();
+
+ if (this.options.debug) {
+ console.log('🔄 UpdateManager initialized', {
+ currentVersion: this.currentVersion,
+ versionUrl: this.options.versionUrl
+ });
+ }
+
+ // Check version on load
+ if (this.options.checkOnLoad) {
+ await this.checkForUpdates();
+ }
+
+ // Start periodic check
+ this.startPeriodicCheck();
+
+ // Listen to Service Worker events
+ this.setupServiceWorkerListeners();
+
+ } catch (error) {
+ this.handleError('Init failed', error);
+ }
+ }
+
+ /**
+ * Get local version from localStorage
+ */
+ getLocalVersion() {
+ try {
+ return localStorage.getItem(this.options.versionKey) || null;
+ } catch (error) {
+ this.handleError('Failed to get local version', error);
+ return null;
+ }
+ }
+
+ /**
+ * Save version to localStorage
+ */
+ setLocalVersion(version) {
+ try {
+ localStorage.setItem(this.options.versionKey, version);
+ this.currentVersion = version;
+
+ if (this.options.debug) {
+ console.log('✅ Version saved:', version);
+ }
+ } catch (error) {
+ this.handleError('Failed to save version', error);
+ }
+ }
+
+ /**
+ * Check for updates on server
+ */
+ async checkForUpdates() {
+ // Prevent parallel checks
+ if (this.updatePromise) {
+ return this.updatePromise;
+ }
+
+ this.updatePromise = this._performCheck();
+ const result = await this.updatePromise;
+ this.updatePromise = null;
+
+ return result;
+ }
+
+ /**
+ * Perform version check
+ */
+ async _performCheck() {
+ try {
+ if (this.options.debug) {
+ console.log('🔍 Checking for updates...');
+ }
+
+ // Request meta.json with cache-busting
+ const response = await this.fetchWithTimeout(
+ `${this.options.versionUrl}?t=${Date.now()}`,
+ {
+ method: 'GET',
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ 'Pragma': 'no-cache',
+ 'Expires': '0'
+ }
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ const meta = await response.json();
+ this.serverVersion = meta.version || meta.buildVersion || null;
+
+ if (!this.serverVersion) {
+ throw new Error('Version not found in meta.json');
+ }
+
+ if (this.options.debug) {
+ console.log('📦 Server version:', this.serverVersion, 'Local:', this.currentVersion);
+ }
+
+ // Compare versions
+ if (this.currentVersion === null) {
+ // First load - save version
+ this.setLocalVersion(this.serverVersion);
+ return { hasUpdate: false, version: this.serverVersion };
+ }
+
+ if (this.currentVersion !== this.serverVersion) {
+ // New version detected
+ if (this.options.debug) {
+ console.log('🆕 New version detected!', {
+ current: this.currentVersion,
+ new: this.serverVersion
+ });
+ }
+
+ // Call callback
+ if (this.options.onUpdateAvailable) {
+ this.options.onUpdateAvailable({
+ currentVersion: this.currentVersion,
+ newVersion: this.serverVersion,
+ updateManager: this
+ });
+ }
+
+ return {
+ hasUpdate: true,
+ currentVersion: this.currentVersion,
+ newVersion: this.serverVersion
+ };
+ }
+
+ return { hasUpdate: false, version: this.serverVersion };
+
+ } catch (error) {
+ // Graceful degradation - if meta.json is unavailable, continue working
+ if (this.options.debug) {
+ console.warn('⚠️ Update check failed (non-critical):', error.message);
+ }
+
+ if (this.options.onError) {
+ this.options.onError(error);
+ }
+
+ return { hasUpdate: false, error: error.message };
+ }
+ }
+
+ /**
+ * Fetch with timeout
+ */
+ async fetchWithTimeout(url, options = {}) {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), this.options.requestTimeout);
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ signal: controller.signal
+ });
+ clearTimeout(timeoutId);
+ return response;
+ } catch (error) {
+ clearTimeout(timeoutId);
+ if (error.name === 'AbortError') {
+ throw new Error('Request timeout');
+ }
+ throw error;
+ }
+ }
+
+ /**
+ * Force application update
+ * Clears all cache levels and reloads the page
+ */
+ async forceUpdate() {
+ if (this.isUpdating) {
+ if (this.options.debug) {
+ console.log('⏳ Update already in progress...');
+ }
+ return;
+ }
+
+ this.isUpdating = true;
+
+ try {
+ if (this.options.debug) {
+ console.log('🚀 Starting force update...');
+ }
+
+ // Step 1: Preserve critical data
+ const preservedData = this.preserveCriticalData();
+
+ // Step 2: Clear Service Worker caches
+ await this.clearServiceWorkerCaches();
+
+ // Step 3: Unregister Service Workers
+ await this.unregisterServiceWorkers();
+
+ // Step 4: Clear browser cache (localStorage, sessionStorage)
+ this.clearBrowserCaches();
+
+ // Step 5: Update version
+ if (this.serverVersion) {
+ this.setLocalVersion(this.serverVersion);
+ }
+
+ // Step 6: Restore critical data
+ this.restoreCriticalData(preservedData);
+
+ // Step 7: Force reload with cache-busting
+ if (this.options.debug) {
+ console.log('🔄 Reloading page with new version...');
+ }
+
+ // Small delay to complete operations
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ // Reload with full cache bypass
+ window.location.href = `${window.location.pathname}?v=${Date.now()}&_update=true`;
+
+ } catch (error) {
+ this.handleError('Force update failed', error);
+ this.isUpdating = false;
+ throw error;
+ }
+ }
+
+ /**
+ * Preserve critical data before cleanup
+ */
+ preserveCriticalData() {
+ const data = {};
+
+ this.options.preserveKeys.forEach(key => {
+ try {
+ const value = localStorage.getItem(key);
+ if (value !== null) {
+ data[key] = value;
+ }
+ } catch (error) {
+ if (this.options.debug) {
+ console.warn(`⚠️ Failed to preserve ${key}:`, error);
+ }
+ }
+ });
+
+ if (this.options.debug) {
+ console.log('💾 Preserved critical data:', Object.keys(data));
+ }
+
+ return data;
+ }
+
+ /**
+ * Restore critical data after cleanup
+ */
+ restoreCriticalData(data) {
+ Object.entries(data).forEach(([key, value]) => {
+ try {
+ localStorage.setItem(key, value);
+ } catch (error) {
+ if (this.options.debug) {
+ console.warn(`⚠️ Failed to restore ${key}:`, error);
+ }
+ }
+ });
+
+ if (this.options.debug) {
+ console.log('✅ Restored critical data');
+ }
+ }
+
+ /**
+ * Clear all Service Worker caches
+ */
+ async clearServiceWorkerCaches() {
+ try {
+ if ('caches' in window) {
+ const cacheNames = await caches.keys();
+
+ if (this.options.debug) {
+ console.log('🗑️ Clearing Service Worker caches:', cacheNames);
+ }
+
+ await Promise.all(
+ cacheNames.map(cacheName => caches.delete(cacheName))
+ );
+
+ // Send message to Service Worker for cleanup
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.controller.postMessage({
+ type: 'CACHE_CLEAR'
+ });
+ }
+
+ if (this.options.debug) {
+ console.log('✅ Service Worker caches cleared');
+ }
+ }
+ } catch (error) {
+ this.handleError('Failed to clear SW caches', error);
+ }
+ }
+
+ /**
+ * Unregister all Service Workers
+ */
+ async unregisterServiceWorkers() {
+ try {
+ if ('serviceWorker' in navigator) {
+ const registrations = await navigator.serviceWorker.getRegistrations();
+
+ if (this.options.debug) {
+ console.log('🔌 Unregistering Service Workers:', registrations.length);
+ }
+
+ await Promise.all(
+ registrations.map(registration => {
+ // Send skipWaiting command before unregistering
+ if (registration.waiting) {
+ registration.waiting.postMessage({ type: 'SKIP_WAITING' });
+ }
+ if (registration.installing) {
+ registration.installing.postMessage({ type: 'SKIP_WAITING' });
+ }
+ return registration.unregister();
+ })
+ );
+
+ if (this.options.debug) {
+ console.log('✅ Service Workers unregistered');
+ }
+ }
+ } catch (error) {
+ this.handleError('Failed to unregister SW', error);
+ }
+ }
+
+ /**
+ * Clear browser caches (localStorage, sessionStorage)
+ */
+ clearBrowserCaches() {
+ try {
+ // Clear sessionStorage
+ sessionStorage.clear();
+
+ // Clear localStorage (except critical data that is already preserved)
+ const keysToRemove = [];
+ for (let i = 0; i < localStorage.length; i++) {
+ const key = localStorage.key(i);
+ if (key && !this.options.preserveKeys.includes(key) && key !== this.options.versionKey) {
+ keysToRemove.push(key);
+ }
+ }
+
+ keysToRemove.forEach(key => {
+ try {
+ localStorage.removeItem(key);
+ } catch (error) {
+ if (this.options.debug) {
+ console.warn(`⚠️ Failed to remove ${key}:`, error);
+ }
+ }
+ });
+
+ if (this.options.debug) {
+ console.log('✅ Browser caches cleared');
+ }
+ } catch (error) {
+ this.handleError('Failed to clear browser caches', error);
+ }
+ }
+
+ /**
+ * Start periodic update check
+ */
+ startPeriodicCheck() {
+ if (this.checkIntervalId) {
+ clearInterval(this.checkIntervalId);
+ }
+
+ this.checkIntervalId = setInterval(() => {
+ this.checkForUpdates();
+ }, this.options.checkInterval);
+
+ if (this.options.debug) {
+ console.log(`⏰ Periodic check started (${this.options.checkInterval}ms)`);
+ }
+ }
+
+ /**
+ * Stop periodic check
+ */
+ stopPeriodicCheck() {
+ if (this.checkIntervalId) {
+ clearInterval(this.checkIntervalId);
+ this.checkIntervalId = null;
+
+ if (this.options.debug) {
+ console.log('⏹️ Periodic check stopped');
+ }
+ }
+ }
+
+ /**
+ * Setup Service Worker event listeners
+ */
+ setupServiceWorkerListeners() {
+ if ('serviceWorker' in navigator) {
+ // Listen to Service Worker updates
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
+ if (this.options.debug) {
+ console.log('🔄 Service Worker controller changed');
+ }
+
+ // Check for updates after controller change
+ setTimeout(() => {
+ this.checkForUpdates();
+ }, 1000);
+ });
+
+ // Listen to messages from Service Worker
+ navigator.serviceWorker.addEventListener('message', (event) => {
+ if (event.data && event.data.type === 'SW_ACTIVATED') {
+ if (this.options.debug) {
+ console.log('✅ Service Worker activated');
+ }
+
+ // Check for updates after activation
+ setTimeout(() => {
+ this.checkForUpdates();
+ }, 1000);
+ }
+ });
+ }
+ }
+
+ /**
+ * Handle errors
+ */
+ handleError(message, error) {
+ const errorMessage = `${message}: ${error.message || error}`;
+
+ if (this.options.debug) {
+ console.error('❌ UpdateManager error:', errorMessage, error);
+ }
+
+ if (this.options.onError) {
+ this.options.onError(new Error(errorMessage));
+ }
+ }
+
+ /**
+ * Destroy manager (cleanup)
+ */
+ destroy() {
+ this.stopPeriodicCheck();
+ this.updatePromise = null;
+
+ if (this.options.debug) {
+ console.log('🗑️ UpdateManager destroyed');
+ }
+ }
+}
+
+// Export for use in modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = UpdateManager;
+} else {
+ window.UpdateManager = UpdateManager;
+}
+
diff --git a/sw.js b/sw.js
index 1af77f4..74bbf97 100644
--- a/sw.js
+++ b/sw.js
@@ -1,19 +1,46 @@
// SecureBit.chat Service Worker
-// Conservative PWA Edition v4.7.53 - Minimal Caching Strategy
+// Conservative PWA Edition v4.7.55 - Minimal Caching Strategy
+// Enhanced with version-aware cache management
-const CACHE_NAME = 'securebit-pwa-v4.7.53';
-const STATIC_CACHE = 'securebit-pwa-static-v4.7.53';
-const DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.53';
+// Dynamic version detection from meta.json
+let APP_VERSION = 'v4.7.55';
+let CACHE_NAME = 'securebit-pwa-v4.7.55';
+let STATIC_CACHE = 'securebit-pwa-static-v4.7.55';
+let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.55';
+
+// Load version from meta.json on install
+async function getAppVersion() {
+ try {
+ const response = await fetch('/meta.json?t=' + Date.now(), {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store, must-revalidate'
+ }
+ });
+ if (response.ok) {
+ const meta = await response.json();
+ const version = meta.version || meta.buildVersion || 'v4.7.55';
+ APP_VERSION = version;
+ CACHE_NAME = `securebit-pwa-${version}`;
+ STATIC_CACHE = `securebit-pwa-static-${version}`;
+ DYNAMIC_CACHE = `securebit-pwa-dynamic-${version}`;
+ return version;
+ }
+ } catch (error) {
+ console.warn('⚠️ Failed to load version from meta.json, using default');
+ }
+ return APP_VERSION;
+}
// Essential files for PWA offline functionality
+// DO NOT include JS files from dist/ - they should load from network for updates
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
- // Core PWA files only
- '/dist/app.js',
- '/dist/app-boot.js',
+ // DO NOT cache /dist/app.js and /dist/app-boot.js - they should be updated
+ // This allows the update system to work correctly
// Essential styles for PWA
'/src/styles/pwa.css',
@@ -73,34 +100,43 @@ self.addEventListener('message', (event) => {
self.addEventListener('install', (event) => {
event.waitUntil(
- caches.open(STATIC_CACHE)
- .then(async (cache) => {
-
- // Cache assets one by one to handle failures gracefully
- const cachePromises = STATIC_ASSETS.map(async (url) => {
- try {
- // Skip sensitive patterns
- if (SENSITIVE_PATTERNS.some(pattern => pattern.test(url))) {
- return;
+ getAppVersion().then(async (version) => {
+ console.log('📦 Service Worker installing with version:', version);
+
+ return caches.open(STATIC_CACHE)
+ .then(async (cache) => {
+
+ // Cache assets one by one to handle failures gracefully
+ const cachePromises = STATIC_ASSETS.map(async (url) => {
+ try {
+ // Skip sensitive patterns
+ if (SENSITIVE_PATTERNS.some(pattern => pattern.test(url))) {
+ return;
+ }
+
+ // Add cache-busting for meta.json
+ if (url.includes('meta.json')) {
+ url = url + '?t=' + Date.now();
+ }
+
+ await cache.add(url);
+ } catch (error) {
+ console.warn(`⚠️ Failed to cache ${url}:`, error.message);
+ // Continue with other assets even if one fails
}
-
- await cache.add(url);
- } catch (error) {
- console.warn(`⚠️ Failed to cache ${url}:`, error.message);
- // Continue with other assets even if one fails
- }
+ });
+
+ await Promise.allSettled(cachePromises);
+
+ // Force activation of new service worker
+ return self.skipWaiting();
+ })
+ .catch((error) => {
+ console.error('❌ Failed to open cache:', error);
+ // Still skip waiting to activate the service worker
+ return self.skipWaiting();
});
-
- await Promise.allSettled(cachePromises);
-
- // Force activation of new service worker
- return self.skipWaiting();
- })
- .catch((error) => {
- console.error('❌ Failed to open cache:', error);
- // Still skip waiting to activate the service worker
- return self.skipWaiting();
- })
+ })
);
});
@@ -108,17 +144,24 @@ self.addEventListener('install', (event) => {
self.addEventListener('activate', (event) => {
event.waitUntil(
- caches.keys().then(cacheNames => {
- return Promise.all(
- cacheNames.map(cacheName => {
- // Remove old caches
- if (cacheName !== STATIC_CACHE && cacheName !== DYNAMIC_CACHE && cacheName !== CACHE_NAME) {
- console.log(`🗑️ Removing old cache: ${cacheName}`);
- return caches.delete(cacheName);
- }
- })
- );
- }).then(() => {
+ getAppVersion().then(async (version) => {
+ console.log('✅ Service Worker activating with version:', version);
+
+ const cacheNames = await caches.keys();
+
+ // Remove all old caches that don't match current version
+ const deletePromises = cacheNames.map(cacheName => {
+ // Remove caches that don't match current version
+ if (cacheName !== STATIC_CACHE &&
+ cacheName !== DYNAMIC_CACHE &&
+ cacheName !== CACHE_NAME &&
+ cacheName.startsWith('securebit-pwa-')) {
+ console.log(`🗑️ Removing old cache: ${cacheName}`);
+ return caches.delete(cacheName);
+ }
+ });
+
+ await Promise.all(deletePromises);
// Notify all clients about the update
return self.clients.claim().then(() => {
@@ -126,6 +169,7 @@ self.addEventListener('activate', (event) => {
clients.forEach(client => {
client.postMessage({
type: 'SW_ACTIVATED',
+ version: version,
timestamp: Date.now()
});
});
@@ -135,7 +179,7 @@ self.addEventListener('activate', (event) => {
);
});
-// Удаляем дублирующийся код activate event
+// Removed duplicate activate event code
// Fetch event - handle requests with security-aware caching
self.addEventListener('fetch', (event) => {
@@ -157,6 +201,45 @@ self.addEventListener('fetch', (event) => {
return;
}
+ // Network-first for meta.json (never cache)
+ if (url.pathname === '/meta.json' || url.pathname.endsWith('/meta.json')) {
+ event.respondWith(
+ fetch(event.request, {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ 'Pragma': 'no-cache'
+ }
+ }).catch(() => {
+ // Fallback if network is unavailable
+ return new Response(JSON.stringify({
+ version: APP_VERSION,
+ error: 'Network unavailable'
+ }), {
+ headers: { 'Content-Type': 'application/json' }
+ });
+ })
+ );
+ return;
+ }
+
+ // Network-first for JS files from dist/ (don't cache for updates)
+ if (url.pathname.startsWith('/dist/') && (url.pathname.endsWith('.js') || url.pathname.endsWith('.mjs'))) {
+ event.respondWith(
+ fetch(event.request, {
+ cache: 'no-store',
+ headers: {
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
+ 'Pragma': 'no-cache'
+ }
+ }).catch(() => {
+ // Fallback if network is unavailable - return error
+ return new Response('Network unavailable', { status: 503 });
+ })
+ );
+ return;
+ }
+
event.respondWith(handleRequest(event.request));
});