feat: implement comprehensive PWA force update system
- Add UpdateManager and UpdateChecker for automatic version detection - Add post-build script for meta.json generation and version injection - Enhance Service Worker with version-aware caching - Add .htaccess configuration for proper cache control This ensures all users receive the latest version after deployment without manual cache clearing.
This commit is contained in:
47
.github/workflows/deploy-with-cache-purge.yml
vendored
Normal file
47
.github/workflows/deploy-with-cache-purge.yml
vendored
Normal file
@@ -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."
|
||||
|
||||
189
.htaccess
Normal file
189
.htaccess
Normal file
@@ -0,0 +1,189 @@
|
||||
# SecureBit.chat - Apache Configuration
|
||||
# Comprehensive caching configuration for forced updates
|
||||
|
||||
# Enable mod_rewrite
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /
|
||||
</IfModule>
|
||||
|
||||
# ============================================
|
||||
# CRITICAL FILES - NO CACHING
|
||||
# ============================================
|
||||
|
||||
# meta.json - versioning file (never cache)
|
||||
<FilesMatch "meta\.json$">
|
||||
<IfModule mod_headers.c>
|
||||
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"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# HTML files - always fresh
|
||||
<FilesMatch "\.(html|htm)$">
|
||||
<IfModule mod_headers.c>
|
||||
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
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Service Worker - no cache
|
||||
<FilesMatch "sw\.js$">
|
||||
<IfModule mod_headers.c>
|
||||
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 "/"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# manifest.json - no cache
|
||||
<FilesMatch "manifest\.json$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
||||
Header set Pragma "no-cache"
|
||||
Header set Expires "0"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# ============================================
|
||||
# STATIC RESOURCES - AGGRESSIVE CACHING
|
||||
# ============================================
|
||||
|
||||
# JavaScript files in dist/ - no cache (for updates)
|
||||
<FilesMatch "^dist/.*\.(js|mjs)$">
|
||||
<IfModule mod_headers.c>
|
||||
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"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# JavaScript files with hashes in other locations - long cache
|
||||
<FilesMatch "\.(js|mjs)$">
|
||||
<IfModule mod_headers.c>
|
||||
# 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"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# CSS files - long cache
|
||||
<FilesMatch "\.css$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Images - long cache
|
||||
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico)$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Fonts - long cache
|
||||
<FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
Header set Access-Control-Allow-Origin "*"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# Audio/Video - long cache
|
||||
<FilesMatch "\.(mp3|mp4|webm|ogg)$">
|
||||
<IfModule mod_headers.c>
|
||||
Header set Cache-Control "public, max-age=31536000, immutable"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
|
||||
# ============================================
|
||||
# SECURITY
|
||||
# ============================================
|
||||
|
||||
# XSS Protection
|
||||
<IfModule mod_headers.c>
|
||||
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"
|
||||
</IfModule>
|
||||
|
||||
# Content Security Policy (already configured in HTML, but can add header)
|
||||
<IfModule mod_headers.c>
|
||||
# Header set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';"
|
||||
</IfModule>
|
||||
|
||||
# ============================================
|
||||
# GZIP COMPRESSION
|
||||
# ============================================
|
||||
|
||||
<IfModule mod_deflate.c>
|
||||
# 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
|
||||
</IfModule>
|
||||
|
||||
# ============================================
|
||||
# MIME TYPES
|
||||
# ============================================
|
||||
|
||||
<IfModule mod_mime.c>
|
||||
# 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
|
||||
</IfModule>
|
||||
|
||||
# ============================================
|
||||
# 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)
|
||||
<IfModule mod_rewrite.c>
|
||||
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]
|
||||
</IfModule>
|
||||
|
||||
# ============================================
|
||||
# LOGGING (optional)
|
||||
# ============================================
|
||||
|
||||
# Uncomment for debugging
|
||||
# LogLevel rewrite:trace3
|
||||
|
||||
16
README.md
16
README.md
@@ -1,4 +1,4 @@
|
||||
# SecureBit.chat v4.7.53
|
||||
# SecureBit.chat v4.7.55
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -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**
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
2
dist/app-boot.js
vendored
2
dist/app-boot.js
vendored
@@ -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
|
||||
|
||||
2
dist/app-boot.js.map
vendored
2
dist/app-boot.js.map
vendored
File diff suppressed because one or more lines are too long
36
dist/app.js
vendored
36
dist/app.js
vendored
@@ -1688,7 +1688,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
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) {
|
||||
if (webrtcManagerRef2.current && webrtcManagerRef2.current.isConnected()) {
|
||||
@@ -3235,9 +3235,22 @@ var EnhancedSecureP2PChat = () => {
|
||||
])
|
||||
]);
|
||||
};
|
||||
var UpdateCheckerWrapper = ({ children }) => {
|
||||
if (typeof window !== "undefined" && window.UpdateChecker) {
|
||||
return React.createElement(window.UpdateChecker, {
|
||||
debug: false
|
||||
}, children);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
function initializeApp() {
|
||||
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById("root"));
|
||||
const AppWithUpdateChecker = React.createElement(
|
||||
UpdateCheckerWrapper,
|
||||
null,
|
||||
React.createElement(EnhancedSecureP2PChat)
|
||||
);
|
||||
ReactDOM.render(AppWithUpdateChecker, document.getElementById("root"));
|
||||
} else {
|
||||
console.error("\u041C\u043E\u0434\u0443\u043B\u0438 \u043D\u0435 \u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u044B:", {
|
||||
hasCrypto: !!window.EnhancedSecureCryptoUtils,
|
||||
@@ -3258,5 +3271,22 @@ if (typeof window !== "undefined") {
|
||||
window.initializeApp = initializeApp;
|
||||
}
|
||||
}
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById("root"));
|
||||
if (window.EnhancedSecureCryptoUtils && window.EnhancedSecureWebRTCManager) {
|
||||
const UpdateCheckerWrapper2 = ({ children }) => {
|
||||
if (typeof window !== "undefined" && window.UpdateChecker) {
|
||||
return React.createElement(window.UpdateChecker, {
|
||||
debug: false
|
||||
}, children);
|
||||
}
|
||||
return children;
|
||||
};
|
||||
const AppWithUpdateChecker = React.createElement(
|
||||
UpdateCheckerWrapper2,
|
||||
null,
|
||||
React.createElement(EnhancedSecureP2PChat)
|
||||
);
|
||||
ReactDOM.render(AppWithUpdateChecker, document.getElementById("root"));
|
||||
} else {
|
||||
ReactDOM.render(React.createElement(EnhancedSecureP2PChat), document.getElementById("root"));
|
||||
}
|
||||
//# sourceMappingURL=app.js.map
|
||||
|
||||
6
dist/app.js.map
vendored
6
dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
11
index.html
11
index.html
@@ -147,13 +147,16 @@
|
||||
<link rel="stylesheet" href="src/styles/animations.css">
|
||||
<link rel="stylesheet" href="src/styles/components.css">
|
||||
<script src="src/scripts/fa-check.js"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1757383302"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=3"></script>
|
||||
<!-- Update Manager - система принудительного обновления -->
|
||||
<script src="src/utils/updateManager.js"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1767018751497"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1767018751497"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1757383304"></script>
|
||||
<script type="module" src="dist/app.js?v=1757383304"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1767018751497"></script>
|
||||
<script type="module" src="dist/app.js?v=1767018751497"></script>
|
||||
|
||||
<script src="src/scripts/pwa-register.js"></script>
|
||||
<script src="./src/pwa/install-prompt.js" type="module"></script>
|
||||
|
||||
@@ -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": "./",
|
||||
|
||||
10
meta.json
Normal file
10
meta.json
Normal file
@@ -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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
11
public/meta.json.example
Normal file
11
public/meta.json.example
Normal file
@@ -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"
|
||||
}
|
||||
|
||||
203
scripts/post-build.js
Normal file
203
scripts/post-build.js
Normal file
@@ -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
|
||||
};
|
||||
|
||||
94
scripts/purge-cloudflare-cache.js
Normal file
94
scripts/purge-cloudflare-cache.js
Normal file
@@ -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();
|
||||
|
||||
39
src/app.jsx
39
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'));
|
||||
// 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'));
|
||||
}
|
||||
290
src/components/UpdateChecker.jsx
Normal file
290
src/components/UpdateChecker.jsx
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
])
|
||||
]),
|
||||
|
||||
|
||||
540
src/utils/updateManager.js
Normal file
540
src/utils/updateManager.js
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
173
sw.js
173
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));
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user