Fix the desktop download buttons; release v5.5.4
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:
@@ -1,5 +1,16 @@
|
||||
# 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
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -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: "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: "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")
|
||||
])
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+6
-4
@@ -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")
|
||||
]);
|
||||
}
|
||||
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 = {
|
||||
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" },
|
||||
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" },
|
||||
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" }
|
||||
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: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
|
||||
linux: { name: "Linux", format: ".AppImage", icon: "fab fa-linux", url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
|
||||
};
|
||||
const detectOS = () => {
|
||||
const ua = (navigator.userAgent || "") + " " + (navigator.platform || "");
|
||||
@@ -3589,7 +3591,7 @@ var EnhancedSecureP2PChat = () => {
|
||||
} 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(
|
||||
// Progress callback — drives the voice-note upload/download ring.
|
||||
(progress) => {
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
+22
-22
@@ -24,7 +24,7 @@
|
||||
|
||||
<!-- PWA Manifest -->
|
||||
<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 -->
|
||||
<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">
|
||||
|
||||
<!-- 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="60x60" href="./logo/icon-60x60.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="144x144" href="./logo/icon-144x144.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 -->
|
||||
<meta name="msapplication-TileColor" content="#ff6b35">
|
||||
@@ -183,7 +183,7 @@
|
||||
<!-- Render-blocking JS is deferred: classic deferred scripts and module scripts
|
||||
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. -->
|
||||
<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-dom/react-dom.production.min.js"></script>
|
||||
<!-- 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). -->
|
||||
<script defer src="libs/prism/prism.js"></script>
|
||||
<!-- Critical, paint-defining CSS stays render-blocking (avoids FOUC / layout shift). -->
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1785141222839">
|
||||
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1785141222839">
|
||||
<link rel="stylesheet" href="assets/tailwind.css?v=1785142410213">
|
||||
<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
|
||||
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. -->
|
||||
@@ -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-700.woff2" as="font" type="font/woff2" crossorigin>
|
||||
<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/animations.css?v=1785141222839">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1785141222839">
|
||||
<link rel="stylesheet" href="src/styles/main.css?v=1785142410213">
|
||||
<link rel="stylesheet" href="src/styles/animations.css?v=1785142410213">
|
||||
<link rel="stylesheet" href="src/styles/components.css?v=1785142410213">
|
||||
<!-- 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>
|
||||
<link rel="stylesheet" href="/assets/fontawesome/css/all.min.css">
|
||||
<link rel="stylesheet" href="libs/prism/prism.css">
|
||||
</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 - система принудительного обновления -->
|
||||
<script defer src="src/utils/updateManager.js?v=1785141222839"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1785141222839"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1785141222839"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1785141222839"></script>
|
||||
<script defer src="src/utils/updateManager.js?v=1785142410213"></script>
|
||||
<script type="module" src="src/components/UpdateChecker.jsx?v=1785142410213"></script>
|
||||
<script type="module" src="dist/qr-local.js?v=1785142410213"></script>
|
||||
<script type="module" src="src/components/QRScanner.js?v=1785142410213"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="dist/app-boot.js?v=1785141222839"></script>
|
||||
<script type="module" src="dist/app.js?v=1785141222839"></script>
|
||||
<script type="module" src="dist/app-boot.js?v=1785142410213"></script>
|
||||
<script type="module" src="dist/app.js?v=1785142410213"></script>
|
||||
|
||||
<script defer src="src/scripts/pwa-register.js?v=1785141222839"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1785141222839" type="module"></script>
|
||||
<script src="./src/pwa/pwa-manager.js?v=1785141222839" type="module"></script>
|
||||
<script defer src="./src/scripts/pwa-offline-test.js?v=1785141222839"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785141222839">
|
||||
<script defer src="src/scripts/pwa-register.js?v=1785142410213"></script>
|
||||
<script src="./src/pwa/install-prompt.js?v=1785142410213" 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=1785142410213"></script>
|
||||
<link rel="stylesheet" href="./src/styles/pwa.css?v=1785142410213">
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"version": "1785141222839",
|
||||
"buildVersion": "1785141222839",
|
||||
"appVersion": "5.5.3",
|
||||
"buildTime": "2026-07-27T08:33:42.891Z",
|
||||
"buildId": "1785141222839-6152a77",
|
||||
"gitHash": "6152a77",
|
||||
"version": "1785142410213",
|
||||
"buildVersion": "1785142410213",
|
||||
"appVersion": "5.5.4",
|
||||
"buildTime": "2026-07-27T08:53:30.254Z",
|
||||
"buildId": "1785142410213-fcee421",
|
||||
"gitHash": "fcee421",
|
||||
"generated": true,
|
||||
"generatedAt": "2026-07-27T08:33:42.893Z"
|
||||
"generatedAt": "2026-07-27T08:53:30.255Z"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "securebit-chat",
|
||||
"version": "5.5.3",
|
||||
"version": "5.5.4",
|
||||
"description": "Secure P2P Communication Application with End-to-End Encryption",
|
||||
"main": "index.html",
|
||||
"scripts": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"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",
|
||||
"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": [
|
||||
"p2p",
|
||||
|
||||
+17
-4
@@ -1578,10 +1578,23 @@ import {
|
||||
}
|
||||
|
||||
// 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 = {
|
||||
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' },
|
||||
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' },
|
||||
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' }
|
||||
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: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_x64-setup.exe` },
|
||||
linux: { name: 'Linux', format: '.AppImage', icon: 'fab fa-linux', url: `${SB_DESKTOP_RELEASE}/SecureBit.Chat_${SB_DESKTOP_VERSION}_amd64.AppImage` }
|
||||
};
|
||||
const detectOS = () => {
|
||||
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).
|
||||
manager.setFileTransferCallbacks(
|
||||
|
||||
@@ -559,7 +559,7 @@ const EnhancedMinimalHeader = ({
|
||||
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('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')
|
||||
])
|
||||
|
||||
@@ -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
|
||||
// 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.
|
||||
const SW_BUILD_VERSION = '1785141222839';
|
||||
const SW_BUILD_VERSION = '1785142410213';
|
||||
|
||||
// Load version from meta.json on install
|
||||
async function getAppVersion() {
|
||||
|
||||
@@ -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)`);
|
||||
Reference in New Issue
Block a user