feat: implement comprehensive PWA force update system
Some checks failed
CodeQL Analysis / Analyze CodeQL (push) Has been cancelled
Deploy Application / deploy (push) Has been cancelled
Mirror to Codeberg / mirror (push) Has been cancelled
Mirror to PrivacyGuides / mirror (push) Has been cancelled

- 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:
lockbitchat
2025-12-29 10:51:07 -04:00
parent 1b6431a36b
commit 91c292a6cf
20 changed files with 1606 additions and 74 deletions

View 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
View 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

View File

@@ -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
View File

@@ -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

File diff suppressed because one or more lines are too long

36
dist/app.js vendored
View File

@@ -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

File diff suppressed because one or more lines are too long

View File

@@ -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>

View File

@@ -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
View 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"
}

View File

@@ -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
View 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
View 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
};

View 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();

View File

@@ -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'));
}

View 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;
}

View File

@@ -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
View 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
View File

@@ -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));
});