Fix the desktop download buttons; release v5.5.4
CodeQL Analysis / Analyze CodeQL (push) Canceled after 0s
Deploy Application / deploy (push) Canceled after 0s
Mirror to Codeberg / mirror (push) Canceled after 0s
Mirror to PrivacyGuides / mirror (push) Canceled after 0s

The buttons still led to a dead GitHub page. 5.5.3 updated one of the two places
these links live — the platforms menu on the connection screen keeps its own
DOWNLOADS table, and it was missed, so it stayed on 0.1.0.

Why it looked like a working link that did nothing: the stale entries used
/releases/latest/download/<file>, and GitHub resolves `latest` by redirecting to
the newest tag. Once 0.3.0 shipped, a link written for 0.1.0 resolved to
/releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe — a file that never
existed under that tag. The browser navigated to GitHub and downloaded nothing.

Both places now build their URLs from a DESKTOP_VERSION constant with the tag
pinned, so the version is written once per file and a link cannot silently
become invalid when a new release goes out.

Adds tests/desktop-download-links.test.mjs, which fails the build if this drifts
again: every source must derive URLs from that constant, /latest/ and hardcoded
versions in filenames are rejected, and each generated URL is fetched to prove
the asset exists. SKIP_NETWORK=1 skips the fetches offline.
This commit is contained in:
lockbitchat
2026-07-27 04:53:44 -04:00
parent fcee4216e8
commit 60bf037ef9
13 changed files with 176 additions and 46 deletions
+11
View File
@@ -1,5 +1,16 @@
# Changelog # Changelog
## v5.5.4 — Fix the desktop download buttons
### Fixed
- **The download buttons still led to a dead GitHub page.** 5.5.3 updated one of the two places these links live; the platforms menu on the connection screen has its own `DOWNLOADS` table, and it was missed. It still pointed at 0.1.0.
- The stale links used `/releases/latest/download/<file>`. GitHub resolves `latest` by redirecting to the newest tag, so once 0.3.0 shipped, a link written for 0.1.0 became `/releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe` — a file that never existed under that tag. The button navigated to GitHub and downloaded nothing, which is why it looked like a working link that simply did nothing.
### Added
- `tests/desktop-download-links.test.mjs`, which fails the build if any source drifts: it requires every file to derive its URLs from one `DESKTOP_VERSION` constant, forbids `/releases/latest/download/` and hardcoded versions in filenames, and fetches each generated URL to prove the release asset is really there. Set `SKIP_NETWORK=1` to skip the fetches offline.
## v5.5.3 — Desktop downloads point at 0.3.0 ## v5.5.3 — Desktop downloads point at 0.3.0
### Fixed ### Fixed
+1 -1
View File
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required. No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-5.5.3-3ecf8e.svg)](CHANGELOG.md) [![Version](https://img.shields.io/badge/version-5.5.4-3ecf8e.svg)](CHANGELOG.md)
[![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app) [![PWA](https://img.shields.io/badge/PWA-installable-3ecf8e.svg)](#install-as-an-app)
[![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model) [![Encryption](https://img.shields.io/badge/crypto-ECDH%20P--384%20%C2%B7%20AES--256--GCM-blue.svg)](#security-model)
+1 -1
View File
@@ -18910,7 +18910,7 @@ Right-click or Ctrl+click to disconnect`,
React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [ React.createElement("div", { key: "txt", style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [ React.createElement("div", { key: "r1", style: { display: "flex", alignItems: "baseline", gap: "7px" } }, [
React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"), React.createElement("span", { key: "n", style: { fontSize: "16px", fontWeight: 800, letterSpacing: "-0.3px", color: "#e8e8eb" } }, "SecureBit"),
React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.5.3") React.createElement("span", { key: "v", style: { fontFamily: MONO, fontSize: "10px", fontWeight: 500, color: "#56565e" } }, "v5.5.4")
]), ]),
React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted") React.createElement("div", { key: "r2", className: "hidden sm:block", style: { fontSize: "11px", color: "#6b6b73", fontWeight: 500 } }, "End-to-end encrypted")
]) ])
+1 -1
View File
File diff suppressed because one or more lines are too long
Vendored
+6 -4
View File
@@ -1805,10 +1805,12 @@ var EnhancedConnectionSetup = ({
}, disabled: !hasInvite || connectionStatus === "connecting", style: { width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "9px", padding: "14px", borderRadius: "13px", border: "none", background: hasInvite && connectionStatus !== "connecting" ? C_ORANGE : "rgba(255,255,255,0.05)", color: hasInvite && connectionStatus !== "connecting" ? "#1a0f04" : "#56565e", fontFamily: "inherit", fontSize: "15px", fontWeight: 700, cursor: hasInvite && connectionStatus !== "connecting" ? "pointer" : "not-allowed", boxShadow: hasInvite && connectionStatus !== "connecting" ? "0 8px 24px rgba(240,137,42,0.28)" : "none" } }, connectionStatus === "connecting" ? "Processing\u2026" : "Connect") }, disabled: !hasInvite || connectionStatus === "connecting", style: { width: "100%", display: "inline-flex", alignItems: "center", justifyContent: "center", gap: "9px", padding: "14px", borderRadius: "13px", border: "none", background: hasInvite && connectionStatus !== "connecting" ? C_ORANGE : "rgba(255,255,255,0.05)", color: hasInvite && connectionStatus !== "connecting" ? "#1a0f04" : "#56565e", fontFamily: "inherit", fontSize: "15px", fontWeight: 700, cursor: hasInvite && connectionStatus !== "connecting" ? "pointer" : "not-allowed", boxShadow: hasInvite && connectionStatus !== "connecting" ? "0 8px 24px rgba(240,137,42,0.28)" : "none" } }, connectionStatus === "connecting" ? "Processing\u2026" : "Connect")
]); ]);
} }
const SB_DESKTOP_VERSION = "0.3.0";
const SB_DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${SB_DESKTOP_VERSION}`;
const DOWNLOADS = { const DOWNLOADS = {
mac: { name: "macOS", format: ".dmg \xB7 Apple Silicon & Intel", icon: "fab fa-apple", url: "https://github.com/SecureBitChat/securebit-desktop/releases/download/v0.1.0/SecureBit.Chat_0.1.0_x64.dmg" }, mac: { name: "macOS", format: ".dmg \xB7 Apple Silicon & Intel", icon: "fab fa-apple", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64.dmg` },
win: { name: "Windows", format: ".exe \xB7 64-bit installer", icon: "fab fa-windows", url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe" }, win: { name: "Windows", format: ".exe \xB7 64-bit installer", icon: "fab fa-windows", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
linux: { name: "Linux", format: ".AppImage", icon: "fab fa-linux", url: "https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage" } linux: { name: "Linux", format: ".AppImage", icon: "fab fa-linux", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
}; };
const detectOS = () => { const detectOS = () => {
const ua = (navigator.userAgent || "") + " " + (navigator.platform || ""); const ua = (navigator.userAgent || "") + " " + (navigator.platform || "");
@@ -3589,7 +3591,7 @@ var EnhancedSecureP2PChat = () => {
} catch (error) { } catch (error) {
} }
} }
handleMessage(" SecureBit.chat Enhanced Security Edition v5.5.3 - 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 v5.5.4 - 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");
manager.setFileTransferCallbacks( manager.setFileTransferCallbacks(
// Progress callback — drives the voice-note upload/download ring. // Progress callback — drives the voice-note upload/download ring.
(progress) => { (progress) => {
+2 -2
View File
File diff suppressed because one or more lines are too long
+22 -22
View File
@@ -24,7 +24,7 @@
<!-- PWA Manifest --> <!-- PWA Manifest -->
<link rel="manifest" href="./manifest.json"> <link rel="manifest" href="./manifest.json">
<link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785141222839"> <link rel="icon" type="image/x-icon" href="./logo/favicon.ico?v=1785142410213">
<!-- PWA Meta Tags --> <!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
@@ -90,7 +90,7 @@
<link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png"> <link rel="apple-touch-startup-image" media="screen and (device-width: 744px) and (device-height: 1133px) and (-webkit-device-pixel-ratio: 2) and (orientation: portrait)" href="./logo/splash/splash_screens/8.3__iPad_Mini_portrait.png">
<!-- Apple Touch Icons --> <!-- Apple Touch Icons -->
<link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785141222839"> <link rel="apple-touch-icon" href="./logo/icon-180x180.png?v=1785142410213">
<link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="./logo/icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="./logo/icon-60x60.png">
<link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png"> <link rel="apple-touch-icon" sizes="72x72" href="./logo/icon-72x72.png">
@@ -99,7 +99,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png"> <link rel="apple-touch-icon" sizes="120x120" href="./logo/icon-120x120.png">
<link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png"> <link rel="apple-touch-icon" sizes="144x144" href="./logo/icon-144x144.png">
<link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png"> <link rel="apple-touch-icon" sizes="152x152" href="./logo/icon-152x152.png">
<link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785141222839"> <link rel="apple-touch-icon" sizes="180x180" href="./logo/icon-180x180.png?v=1785142410213">
<!-- Microsoft Tiles --> <!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35"> <meta name="msapplication-TileColor" content="#ff6b35">
@@ -183,7 +183,7 @@
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts <!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
both execute in document order after parsing, so React still runs before the both execute in document order after parsing, so React still runs before the
app modules below, but the parser / first paint is no longer blocked. --> app modules below, but the parser / first paint is no longer blocked. -->
<script defer src="config/ice-servers.js?v=1785141222839"></script> <script defer src="config/ice-servers.js?v=1785142410213"></script>
<script defer src="libs/react/react.production.min.js"></script> <script defer src="libs/react/react.production.min.js"></script>
<script defer src="libs/react-dom/react-dom.production.min.js"></script> <script defer src="libs/react-dom/react-dom.production.min.js"></script>
<!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only — <!-- Prism syntax highlighting (vendored, offline). Tokenizes code as TEXT only —
@@ -191,8 +191,8 @@
Its CSS is loaded async via load-async-css.js (not paint-critical). --> Its CSS is loaded async via load-async-css.js (not paint-critical). -->
<script defer src="libs/prism/prism.js"></script> <script defer src="libs/prism/prism.js"></script>
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). --> <!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
<link rel="stylesheet" href="assets/tailwind.css?v=1785141222839"> <link rel="stylesheet" href="assets/tailwind.css?v=1785142410213">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785141222839"> <link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785142410213">
<!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI <!-- Preload only the fonts needed for first paint. fa-solid covers the bulk of UI
icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the icons; fa-regular/fa-brands are loaded on demand by their CSS (rarely on the
first screen). Inter latin 400/700 cover body text and headings/buttons. --> first screen). Inter latin 400/700 cover body text and headings/buttons. -->
@@ -200,31 +200,31 @@
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/assets/fonts/inter/files/inter-latin-400.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin> <link rel="preload" href="/assets/fonts/inter/files/inter-latin-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/assets/fonts/inter/inter.css"> <link rel="stylesheet" href="/assets/fonts/inter/inter.css">
<link rel="stylesheet" href="src/styles/main.css?v=1785141222839"> <link rel="stylesheet" href="src/styles/main.css?v=1785142410213">
<link rel="stylesheet" href="src/styles/animations.css?v=1785141222839"> <link rel="stylesheet" href="src/styles/animations.css?v=1785142410213">
<link rel="stylesheet" href="src/styles/components.css?v=1785141222839"> <link rel="stylesheet" href="src/styles/components.css?v=1785142410213">
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. --> <!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="src/scripts/load-async-css.js?v=1785141222839"></script> <script defer src="src/scripts/load-async-css.js?v=1785142410213"></script>
<noscript> <noscript>
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css"> <link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
<link rel="stylesheet" href="libs/prism/prism.css"> <link rel="stylesheet" href="libs/prism/prism.css">
</noscript> </noscript>
<script defer src="src/scripts/fa-check.js?v=1785141222839"></script> <script defer src="src/scripts/fa-check.js?v=1785142410213"></script>
<!-- Update Manager - система принудительного обновления --> <!-- Update Manager - система принудительного обновления -->
<script defer src="src/utils/updateManager.js?v=1785141222839"></script> <script defer src="src/utils/updateManager.js?v=1785142410213"></script>
<script type="module" src="src/components/UpdateChecker.jsx?v=1785141222839"></script> <script type="module" src="src/components/UpdateChecker.jsx?v=1785142410213"></script>
<script type="module" src="dist/qr-local.js?v=1785141222839"></script> <script type="module" src="dist/qr-local.js?v=1785142410213"></script>
<script type="module" src="src/components/QRScanner.js?v=1785141222839"></script> <script type="module" src="src/components/QRScanner.js?v=1785142410213"></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="dist/app-boot.js?v=1785141222839"></script> <script type="module" src="dist/app-boot.js?v=1785142410213"></script>
<script type="module" src="dist/app.js?v=1785141222839"></script> <script type="module" src="dist/app.js?v=1785142410213"></script>
<script defer src="src/scripts/pwa-register.js?v=1785141222839"></script> <script defer src="src/scripts/pwa-register.js?v=1785142410213"></script>
<script src="./src/pwa/install-prompt.js?v=1785141222839" type="module"></script> <script src="./src/pwa/install-prompt.js?v=1785142410213" type="module"></script>
<script src="./src/pwa/pwa-manager.js?v=1785141222839" type="module"></script> <script src="./src/pwa/pwa-manager.js?v=1785142410213" type="module"></script>
<script defer src="./src/scripts/pwa-offline-test.js?v=1785141222839"></script> <script defer src="./src/scripts/pwa-offline-test.js?v=1785142410213"></script>
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785141222839"> <link rel="stylesheet" href="./src/styles/pwa.css?v=1785142410213">
</body> </body>
</html> </html>
+7 -7
View File
@@ -1,10 +1,10 @@
{ {
"version": "1785141222839", "version": "1785142410213",
"buildVersion": "1785141222839", "buildVersion": "1785142410213",
"appVersion": "5.5.3", "appVersion": "5.5.4",
"buildTime": "2026-07-27T08:33:42.891Z", "buildTime": "2026-07-27T08:53:30.254Z",
"buildId": "1785141222839-6152a77", "buildId": "1785142410213-fcee421",
"gitHash": "6152a77", "gitHash": "fcee421",
"generated": true, "generated": true,
"generatedAt": "2026-07-27T08:33:42.893Z" "generatedAt": "2026-07-27T08:53:30.255Z"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "securebit-chat", "name": "securebit-chat",
"version": "5.5.3", "version": "5.5.4",
"description": "Secure P2P Communication Application with End-to-End Encryption", "description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html", "main": "index.html",
"scripts": { "scripts": {
@@ -11,7 +11,7 @@
"dev": "npm run build && python -m http.server 8000", "dev": "npm run build && python -m http.server 8000",
"watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch", "watch": "npx tailwindcss -i src/styles/tw-input.css -o assets/tailwind.css --watch",
"serve": "npx http-server -p 8000", "serve": "npx http-server -p 8000",
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/security-level-shape.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs" "test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/security-level-shape.test.mjs && node tests/desktop-download-links.test.mjs && node tests/file-transfer-consent.test.mjs && node tests/incoming-message-sanitization.test.mjs && node tests/outgoing-message-integrity.test.mjs && node tests/secure-chat-features.test.mjs && node tests/notification-meta-forwarding.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/webrtc-privacy-mode.test.mjs && node tests/indexeddb-metadata-encryption.test.mjs && node tests/disconnect-cleanup.test.mjs && node tests/timer-lifecycle.test.mjs && node tests/file-transfer-cleanup.test.mjs && node tests/file-transfer-ui-cleanup.test.mjs && node tests/file-transfer-callback-propagation.test.mjs && node tests/debug-window-hooks.test.mjs && node tests/inbound-message-rate-limit.test.mjs && node tests/file-transfer-chunk-rate-limit.test.mjs && node tests/ice-servers-validation.test.mjs && node tests/sessions-reducer.test.mjs && node tests/webrtc-sdp.test.mjs && node tests/webrtc-video.test.mjs && node tests/webrtc-adaptation.test.mjs"
}, },
"keywords": [ "keywords": [
"p2p", "p2p",
+17 -4
View File
@@ -1578,10 +1578,23 @@ import {
} }
// Desktop downloads (real GitHub release assets) + OS detection. // Desktop downloads (real GitHub release assets) + OS detection.
//
// Keep SB_DESKTOP_VERSION in sync with the constant of the same name in
// src/components/ui/DownloadApps.jsx tests/desktop-download-links.test.mjs
// fails the build if the two ever disagree.
//
// The tag is pinned rather than using /releases/latest/download/. Release
// filenames carry the version, and GitHub resolves `latest` by redirecting
// to the newest tag so a stale `latest` link becomes
// /download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe, a file that never
// existed, and the download 404s. A pinned tag keeps serving a real
// installer instead.
const SB_DESKTOP_VERSION = '0.3.0';
const SB_DESKTOP_RELEASE = `https://github.com/SecureBitChat/securebit-desktop/releases/download/v${SB_DESKTOP_VERSION}`;
const DOWNLOADS = { const DOWNLOADS = {
mac: { name: 'macOS', format: '.dmg · Apple Silicon & Intel', icon: 'fab fa-apple', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/download/v0.1.0/SecureBit.Chat_0.1.0_x64.dmg' }, mac: { name: 'macOS', format: '.dmg · Apple Silicon & Intel', icon: 'fab fa-apple', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64.dmg` },
win: { name: 'Windows', format: '.exe · 64-bit installer', icon: 'fab fa-windows', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_x64-setup.exe' }, win: { name: 'Windows', format: '.exe · 64-bit installer', icon: 'fab fa-windows', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
linux: { name: 'Linux', format: '.AppImage', icon: 'fab fa-linux', url: 'https://github.com/SecureBitChat/securebit-desktop/releases/latest/download/SecureBit.Chat_0.1.0_amd64.AppImage' } linux: { name: 'Linux', format: '.AppImage', icon: 'fab fa-linux', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
}; };
const detectOS = () => { const detectOS = () => {
const ua = (navigator.userAgent || '') + ' ' + (navigator.platform || ''); const ua = (navigator.userAgent || '') + ' ' + (navigator.platform || '');
@@ -3533,7 +3546,7 @@ import {
} }
} }
handleMessage(' SecureBit.chat Enhanced Security Edition v5.5.3 - 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 v5.5.4 - 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');
// Setup file transfer callbacks (id-bound to THIS session's manager). // Setup file transfer callbacks (id-bound to THIS session's manager).
manager.setFileTransferCallbacks( manager.setFileTransferCallbacks(
+1 -1
View File
@@ -559,7 +559,7 @@ const EnhancedMinimalHeader = ({
React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [ React.createElement('div', { key: 'txt', style: { lineHeight: 1.2, minWidth: 0 } }, [
React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [ React.createElement('div', { key: 'r1', style: { display: 'flex', alignItems: 'baseline', gap: '7px' } }, [
React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'), React.createElement('span', { key: 'n', style: { fontSize: '16px', fontWeight: 800, letterSpacing: '-0.3px', color: '#e8e8eb' } }, 'SecureBit'),
React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.5.3') React.createElement('span', { key: 'v', style: { fontFamily: MONO, fontSize: '10px', fontWeight: 500, color: '#56565e' } }, 'v5.5.4')
]), ]),
React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted') React.createElement('div', { key: 'r2', className: 'hidden sm:block', style: { fontSize: '11px', color: '#6b6b73', fontWeight: 500 } }, 'End-to-end encrypted')
]) ])
+1 -1
View File
@@ -11,7 +11,7 @@ let DYNAMIC_CACHE = 'securebit-pwa-dynamic-v4.7.56';
// Build stamp — rewritten by scripts/post-build.js on every release so this file's // Build stamp — rewritten by scripts/post-build.js on every release so this file's
// bytes change each deploy. That is what makes the browser detect a new Service Worker, // bytes change each deploy. That is what makes the browser detect a new Service Worker,
// reinstall it, drop stale caches and (via controllerchange) prompt the page to update. // reinstall it, drop stale caches and (via controllerchange) prompt the page to update.
const SW_BUILD_VERSION = '1785141222839'; const SW_BUILD_VERSION = '1785142410213';
// Load version from meta.json on install // Load version from meta.json on install
async function getAppVersion() { async function getAppVersion() {
+104
View File
@@ -0,0 +1,104 @@
// Desktop download links must actually download something.
//
// They have broken twice, both times the same way:
//
// 1. The version is written in more than one place — a `DOWNLOADS` table in
// app.jsx and a `DownloadApps` component — so bumping one left the other
// behind, and nobody noticed because the stale link still *looked* valid.
//
// 2. Links used /releases/latest/download/<file>. GitHub resolves `latest` by
// redirecting to the newest tag, so once 0.3.0 shipped, a link written for
// 0.1.0 turned into
// /releases/download/v0.3.0/SecureBit.Chat_0.1.0_x64-setup.exe
// — a file that never existed under that tag. The button navigated to
// GitHub and downloaded nothing.
//
// So: one version across every file, tags pinned, and — unless SKIP_NETWORK=1 —
// every generated URL is fetched to prove the asset is really there.
//
// Run with: node tests/desktop-download-links.test.mjs
import fs from 'fs';
import path from 'path';
import assert from 'node:assert/strict';
import { fileURLToPath } from 'url';
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (p) => fs.readFileSync(path.join(ROOT, p), 'utf8');
const SOURCES = ['src/app.jsx', 'src/components/ui/DownloadApps.jsx'];
// ---------- every source agrees on one version ----------
const versions = SOURCES.map((f) => {
const m = /(?:SB_)?DESKTOP_VERSION\s*=\s*'([^']+)'/.exec(read(f));
assert.ok(m, `${f} must declare a DESKTOP_VERSION constant instead of inlining the version`);
return { file: f, version: m[1] };
});
const unique = [...new Set(versions.map((v) => v.version))];
assert.equal(
unique.length, 1,
`every source must use the same desktop version, found: ${versions.map((v) => `${v.file}=${v.version}`).join(', ')}`
);
const VERSION = unique[0];
assert.match(VERSION, /^\d+\.\d+\.\d+$/, 'desktop version must be semver');
// ---------- no source may resolve through /latest/ ----------
// Comments explain *why* these forms are avoided and would otherwise match, so
// check the code only.
const stripComments = (src) =>
src
.replace(/\/\*[\s\S]*?\*\//g, '')
.split('\n')
.map((line) => line.replace(/(^|\s)\/\/.*$/, '$1'))
.join('\n');
for (const f of SOURCES) {
const code = stripComments(read(f));
assert.doesNotMatch(
code, /releases\/latest\/download/,
`${f} must pin the release tag: a stale /latest/ link resolves to the newest tag ` +
'with an old filename and 404s'
);
// And the version must never be written into a URL by hand.
const hardcoded = code.match(/SecureBit\.Chat_\d+\.\d+\.\d+_/g);
assert.equal(
hardcoded, null,
`${f} hardcodes a version in a filename (${hardcoded && hardcoded[0]}); build it from the constant`
);
}
// ---------- the URLs the app will actually open ----------
const urls = [
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64.dmg`,
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_x64-setup.exe`,
`https://github.com/SecureBitChat/securebit-desktop/releases/download/v${VERSION}/SecureBit.Chat_${VERSION}_amd64.AppImage`
];
if (process.env.SKIP_NETWORK === '1') {
console.log(`Desktop download link tests passed (v${VERSION}, network checks skipped)`);
process.exit(0);
}
let failed = 0;
for (const url of urls) {
try {
// Range keeps it to a few bytes: we only care that the asset resolves.
const res = await fetch(url, { headers: { Range: 'bytes=0-99' }, redirect: 'follow' });
const ok = res.status === 200 || res.status === 206;
if (!ok) failed++;
console.log(` ${ok ? 'ok ' : 'FAIL'} ${res.status} ${url.split('/').pop()}`);
} catch (err) {
failed++;
console.log(` FAIL ${url.split('/').pop()}${err.message}`);
}
}
assert.equal(
failed, 0,
'every desktop download link must resolve to a real release asset; ' +
'publish the release before bumping the version, or set SKIP_NETWORK=1 to skip this check'
);
console.log(`Desktop download link tests passed (v${VERSION}, ${urls.length} assets reachable)`);