v6.8.4: the relay password now changes every day

This commit is contained in:
lockbitchat
2026-09-23 19:45:54 -04:00
parent 99da907eb7
commit f39e9022b7
32 changed files with 722 additions and 276 deletions
+9
View File
@@ -1,5 +1,14 @@
# Changelog
## v6.8.4 — The relay password now changes every day
When two people cannot connect directly, the app routes the call through our own
relay server. Until now every copy of the app carried the same password for that
server, valid for years, and anyone could copy it and push their own traffic
through it. The site now hands each app a fresh password that runs out after a
day, and only gives them out at a limited pace. Nothing changes for you: the app
picks up a new one on its own, including during long conversations.
## v6.8.3 — Private project files are no longer published with the site
A few files meant only for the developer's own machine — local editor settings and
+4
View File
@@ -11,6 +11,10 @@ COPY deploy/nginx.conf /etc/nginx/nginx.conf
# /sw.js, which is the real, caching worker.
COPY deploy/www-sw.js /etc/nginx/www-sw.js
# The TURN credential endpoint (POST /api/turn-credentials), run by nginx's njs
# module. The secret it signs with comes from the TURN_SECRET Fly secret.
COPY deploy/turn-credentials.js /etc/nginx/njs/turn-credentials.js
# Serve the repository (src/, assets/, libs/, dist/, config/, logo/, sw.js, ...).
COPY . /usr/share/nginx/html
+1 -1
View File
@@ -9,7 +9,7 @@
No accounts. No servers storing your messages. No installation required.
[![License: MIT](https://img.shields.io/badge/License-MIT-f0892a.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-6.8.3-3ecf8e.svg)](CHANGELOG.md)
[![Version](https://img.shields.io/badge/version-6.8.4-3ecf8e.svg)](CHANGELOG.md)
[![Get it from the Snap Store](https://snapcraft.io/securebit-chat/badge.svg)](https://snapcraft.io/securebit-chat)
[![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)
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ar/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ar.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/ar.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/de/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/de.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/de.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+37
View File
@@ -4,6 +4,14 @@
# long-immutable cache for hashed/static assets, security headers, honest 404s.
worker_processes auto;
# njs runs the TURN credential endpoint (deploy/turn-credentials.js). The official
# nginx image ships the module; it only has to be loaded.
load_module modules/ngx_http_js_module.so;
# nginx clears the environment of its workers; this keeps the coturn REST-API
# secret (a Fly secret) visible to that script and to nothing else.
env TURN_SECRET;
events { worker_connections 1024; }
http {
@@ -69,6 +77,8 @@ http {
# one-year immutable cache, which would freeze a sitemap for a year.
~^/robots\.txt$ "public, max-age=3600";
~^/sitemap\.xml$ "public, max-age=3600";
# Relay credentials are minted per request and must never be stored.
~^/api/ "no-store";
}
# CDN-Cache-Control is read by Cloudflare (and other CDNs) *independently* of the
@@ -88,8 +98,28 @@ http {
~^/config/ice-servers\.js$ "no-store";
~^/dist/ "no-store";
~^/src/i18n/ "no-store";
~^/api/ "no-store";
}
# ---- TURN credential endpoint ----
js_path /etc/nginx/njs/;
js_import turncreds from turn-credentials.js;
# Rate-limit by the real client. Behind Cloudflare every request arrives from
# an edge address, so keying on the connection would throttle unrelated users
# together; CF-Connecting-IP is the visitor. Requests that reach Fly directly
# carry no such header and fall back to Fly's own client address. A caller who
# forges the header can dodge the per-client limit, which is why there is
# also a global ceiling below — and coturn has its own quotas behind both.
map $http_cf_connecting_ip $turn_client {
"" $http_fly_client_ip;
default $http_cf_connecting_ip;
}
# A client needs one credential per day plus a few for reloads and new tabs.
limit_req_zone $turn_client zone=turn_per_client:2m rate=10r/m;
limit_req_zone $server_name zone=turn_global:1m rate=20r/s;
limit_req_status 429;
server {
listen 8080 default_server;
listen [::]:8080 default_server;
@@ -113,6 +143,13 @@ http {
add_header CDN-Cache-Control $sb_cdn_cache always;
add_header Service-Worker-Allowed "/" always;
# Short-lived TURN relay credentials. See deploy/turn-credentials.js.
location = /api/turn-credentials {
limit_req zone=turn_per_client burst=10 nodelay;
limit_req zone=turn_global burst=100 nodelay;
js_content turncreds.handle;
}
# Real asset files must return 404 when missing — never fall back to the
# HTML shell, which would be served with the wrong content type and break
# module/script loading (e.g. a missing config/ice-servers.js).
+114
View File
@@ -0,0 +1,114 @@
// Short-lived TURN credentials for SecureBit clients, served by nginx (njs) at
// POST /api/turn-credentials.
//
// WHY THIS EXISTS
// ---------------
// The relay used to be reached with one credential that was valid until 2038 and
// shipped inside every client. Anything shipped inside a client is public, so
// anyone could lift it and relay their own traffic through our server for years.
// Here the coturn REST-API secret stays on the server and each client asks for a
// credential that expires in a day. A lifted credential stops working on its own,
// and getting a new one means coming back here, where requests are rate-limited.
//
// WHAT IT DOES NOT DO
// -------------------
// It cannot prove the caller is our app. A browser cannot lie about Origin, so
// other WEBSITES cannot use our relay for their visitors; a script outside a
// browser can send any Origin it likes, or none (native apps send none). Those
// callers are bounded by the rate limit in nginx.conf and by coturn's own quotas.
//
// It is not a signalling service: it never sees a message, an SDP or who talks to
// whom. It hands out a relay credential and forgets the request.
//
// Plain ES module on purpose: njs runs it in nginx, and the test suite imports the
// same file under Node (both provide crypto.createHmac).
import crypto from 'crypto';
// Long enough to outlast a call: coturn re-checks the expiry on every refresh of
// an allocation, so a credential that expires mid-call drops the relayed leg.
// Clients fetch a fresh one well before this runs out.
const TTL_SECONDS = 24 * 60 * 60;
const ALLOWED_ORIGINS = [
'https://securebit.chat',
'https://securebit-chat.fly.dev',
// Desktop (Tauri) webviews: macOS/Linux, then Windows.
'tauri://localhost',
'http://tauri.localhost',
'https://tauri.localhost',
];
const TURN_URLS = [
'turn:turn.securebit.chat:3478?transport=udp',
'turn:turn.securebit.chat:3478?transport=tcp',
// Raw-IP fallback for clients whose WebRTC stack cannot resolve the name.
'turn:144.172.96.126:3478?transport=udp',
'turn:144.172.96.126:3478?transport=tcp',
'turns:turn.securebit.chat:443?transport=tcp',
];
/** coturn REST-API credential: username "<expiry>:<label>", password HMAC-SHA1. */
function makeCredential(secret, nowSeconds) {
const username = (Math.floor(nowSeconds) + TTL_SECONDS) + ':securebit';
const credential = crypto.createHmac('sha1', secret).update(username).digest('base64');
return { username: username, credential: credential };
}
/**
* Decide a request. Pure, so it can be tested without nginx.
* @returns {{status:number, body?:object, allowOrigin?:string}}
*/
// Written without destructuring or default parameters: njs does not parse them.
function decide(req) {
const method = req.method;
const origin = req.origin;
const secret = req.secret;
// A browser always sends Origin on a POST. A missing one is a native app or a
// script — let it through; the rate limit is what bounds it.
const hasOrigin = typeof origin === 'string' && origin.length > 0;
if (hasOrigin && ALLOWED_ORIGINS.indexOf(origin) === -1) return { status: 403 };
const allowOrigin = hasOrigin ? origin : undefined;
if (method === 'OPTIONS') return { status: 204, allowOrigin: allowOrigin };
if (method !== 'POST') return { status: 405 };
if (!secret) return { status: 503 };
const cred = makeCredential(secret, req.nowSeconds);
return {
status: 200,
allowOrigin: allowOrigin,
body: {
ttl: TTL_SECONDS,
iceServers: [{ urls: TURN_URLS, username: cred.username, credential: cred.credential }],
},
};
}
function handle(r) {
const result = decide({
method: r.method,
origin: r.headersIn['Origin'],
secret: process.env.TURN_SECRET,
nowSeconds: Date.now() / 1000,
});
if (result.allowOrigin) {
r.headersOut['Access-Control-Allow-Origin'] = result.allowOrigin;
r.headersOut['Access-Control-Allow-Methods'] = 'POST, OPTIONS';
r.headersOut['Access-Control-Max-Age'] = '600';
r.headersOut['Vary'] = 'Origin';
}
if (!result.body) {
r.return(result.status);
return;
}
r.headersOut['Content-Type'] = 'application/json';
r.return(result.status, JSON.stringify(result.body));
}
// njs accepts only a default export; the helpers ride along for the tests.
export default {
handle: handle, decide: decide, makeCredential: makeCredential,
TTL_SECONDS: TTL_SECONDS, ALLOWED_ORIGINS: ALLOWED_ORIGINS, TURN_URLS: TURN_URLS
};
+1 -1
View File
File diff suppressed because one or more lines are too long
+3 -3
View File
File diff suppressed because one or more lines are too long
Vendored
+6 -6
View File
File diff suppressed because one or more lines are too long
+4 -4
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,7 +7,7 @@ this document describes.
| | |
| --- | --- |
| Release | v6.8.3 |
| Release | v6.8.4 |
| Protocol version | 4.1 |
| Ratchet wire version | 1 |
@@ -347,5 +347,5 @@ no leg may stop a capture it borrowed.
## Scope
This describes the browser implementation as it stands in v6.8.3. It is not a
This describes the browser implementation as it stands in v6.8.4. It is not a
substitute for independent cryptographic review.
+2 -2
View File
@@ -153,7 +153,7 @@
</thead>
<tbody><tr>
<td>Release</td>
<td>v6.8.3</td>
<td>v6.8.4</td>
</tr>
<tr>
<td>Protocol version</td>
@@ -486,7 +486,7 @@
shared across every leg, and stopped when the call, the group or the tab ends;
no leg may stop a capture it borrowed.</p>
<h2 id="scope">Scope</h2>
<p>This describes the browser implementation as it stands in v6.8.3. It is not a
<p>This describes the browser implementation as it stands in v6.8.4. It is not a
substitute for independent cryptographic review.</p>
<nav class="more">
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/es/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/es.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/es.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/fa/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/fa.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/fa.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/fr/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/fr.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/fr.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/he/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/he.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/he.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/hi/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/hi.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/hi.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+18 -18
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,18 +287,18 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -486,12 +486,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ko/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ko.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/ko.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+7 -7
View File
@@ -1,10 +1,10 @@
{
"version": "1790203273757",
"buildVersion": "1790203273757",
"appVersion": "6.8.3",
"buildTime": "2026-09-23T22:41:13.797Z",
"buildId": "1790203273757-12e62db",
"gitHash": "12e62db",
"version": "1790206279080",
"buildVersion": "1790206279080",
"appVersion": "6.8.4",
"buildTime": "2026-09-23T23:31:19.119Z",
"buildId": "1790206279080-99da907",
"gitHash": "99da907",
"generated": true,
"generatedAt": "2026-09-23T22:41:13.798Z"
"generatedAt": "2026-09-23T23:31:19.120Z"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "securebit-chat",
"version": "6.8.3",
"version": "6.8.4",
"description": "Secure P2P Communication Application with End-to-End Encryption",
"main": "index.html",
"scripts": {
@@ -13,7 +13,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/control-frame-authorization.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/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.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 && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/i18n-build.test.mjs && node tests/docs-build.test.mjs && node tests/qr-bundle-deferred.test.mjs && node tests/icon-subset.test.mjs && node tests/i18n-runtime.test.mjs && node tests/language-switcher.test.mjs && node tests/language-suggestion.test.mjs && node tests/rtl-layout.test.mjs && node tests/asset-paths-locale-safe.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs && node tests/qr-scan-single-frame.test.mjs && node tests/mobile-chat-layout.test.mjs && node tests/group-crypto.test.mjs && node tests/groups-reducer.test.mjs && node tests/group-session-e2e.test.mjs && node tests/group-app-integration.test.mjs && node tests/group-sender.test.mjs && node tests/group-mesh.test.mjs && node tests/group-call.test.mjs && node tests/group-call-autoanswer.test.mjs && node tests/apple-motion.test.mjs && node tests/pwa-install-prompt-capture.test.mjs && node tests/theme-switching.test.mjs"
"test": "node tests/sas-verification.test.mjs && node tests/verification-gate.test.mjs && node tests/inbound-frame-authentication.test.mjs && node tests/control-frame-authorization.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/notification-ephemeral-privacy.test.mjs && node tests/key-derivation-compat.test.mjs && node tests/key-exchange-e2e.test.mjs && node tests/file-type-allowlist.test.mjs && node tests/voice-auto-accept.test.mjs && node tests/legacy-offer-purge.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 && node tests/session-recovery.test.mjs && node tests/qr-zip-bomb.test.mjs && node tests/ice-gathering-patience.test.mjs && node tests/version-consistency.test.mjs && node tests/i18n-build.test.mjs && node tests/docs-build.test.mjs && node tests/qr-bundle-deferred.test.mjs && node tests/icon-subset.test.mjs && node tests/i18n-runtime.test.mjs && node tests/language-switcher.test.mjs && node tests/language-suggestion.test.mjs && node tests/rtl-layout.test.mjs && node tests/asset-paths-locale-safe.test.mjs && node tests/double-ratchet.test.mjs && node tests/ratchet-integration.test.mjs && node tests/descriptor-sbq2.test.mjs && node tests/sbq2-key-exchange.test.mjs && node tests/qr-scan-single-frame.test.mjs && node tests/mobile-chat-layout.test.mjs && node tests/group-crypto.test.mjs && node tests/groups-reducer.test.mjs && node tests/group-session-e2e.test.mjs && node tests/group-app-integration.test.mjs && node tests/group-sender.test.mjs && node tests/group-mesh.test.mjs && node tests/group-call.test.mjs && node tests/group-call-autoanswer.test.mjs && node tests/apple-motion.test.mjs && node tests/pwa-install-prompt-capture.test.mjs && node tests/theme-switching.test.mjs && node tests/turn-credentials.test.mjs"
},
"keywords": [
"p2p",
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ru/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ru.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/ru.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+5
View File
@@ -1,5 +1,6 @@
import { installDebugWindowHooks } from './utils/debugWindowHooks.js';
import { loadIceSettings, saveIceSettings, clearIceSettings } from './network/iceSettingsStore.js';
import { startTurnCredentialRefresh } from './network/turnCredentials.js';
import {
sessionsReducer,
createInitialState,
@@ -3862,6 +3863,10 @@ import { GroupCallMedia, mediaErrorCode } from './group/groupCallMedia.js';
});
}, []);
// Our relay's credential is minted by the site and expires daily;
// fetch one now and keep it renewed. See network/turnCredentials.js.
React.useEffect(() => { startTurnCredentialRefresh(); }, []);
const addMessageWithAutoScroll = React.useCallback((message, type, opts = {}) => {
const newId = Date.now() + Math.random();
const newMessage = {
@@ -14104,9 +14104,33 @@ async processMessage(data) {
this._trackActiveTimer(r.retryTimer);
}
/**
* Hand the peer connection the ICE server list as it is NOW.
*
* A connection keeps the servers it was built with, and our relay's credential
* expires after a day (src/network/turnCredentials.js renews it in place on the
* shared list). A restart gathers new relay candidates, so it must present the
* current credential or a long-lived session would lose its relay path exactly
* when it needs one. Best effort: an engine that refuses the update restarts
* with what it had.
*/
_applyCurrentIceServers(pc) {
try {
if (!pc || typeof pc.getConfiguration !== 'function' || typeof pc.setConfiguration !== 'function') return;
const servers = this._config?.webrtc?.iceServers;
if (!Array.isArray(servers) || servers.length === 0) return;
pc.setConfiguration({ ...pc.getConfiguration(), iceServers: servers });
} catch (error) {
this._secureLog('warn', 'Could not refresh ICE servers before restart', {
errorType: error?.constructor?.name || 'Unknown'
});
}
}
async _sendIceRestartOffer() {
const pc = this.peerConnection;
if (!pc) return;
this._applyCurrentIceServers(pc);
// Rolling back to 'stable' first: a previous restart round-trip may have
// left a local offer pending that was never answered.
@@ -14189,6 +14213,7 @@ async processMessage(data) {
}
this._reconnect.phase = 'restarting';
this._applyCurrentIceServers(pc);
await pc.setRemoteDescription({ type: 'offer', sdp: data.sdp });
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
+130
View File
@@ -0,0 +1,130 @@
// Keeps the credential for SecureBit's own TURN relay fresh.
//
// The relay's credential is no longer something the client carries: it is minted
// by the site (POST /api/turn-credentials, see deploy/turn-credentials.js) and
// expires after a day. This module fetches one at startup and again before it
// runs out, and writes it into the ICE list the connection managers already hold.
//
// The update is made IN PLACE on the existing entry objects. Every manager keeps a
// reference to window.SECUREBIT_ICE_SERVERS rather than a copy, so the next peer
// connection — and the next in-band ICE restart, which re-reads the list — picks
// up the new credential without anything else having to know it changed.
//
// If the endpoint cannot be reached, nothing is changed and the entry keeps the
// credential it shipped with. Only our relay's entries are touched; a TURN server
// the user configured themselves is never modified.
const ENDPOINT = '/api/turn-credentials';
const OWN_RELAY_HOSTS = ['turn.securebit.chat', '144.172.96.126'];
const MAX_FIELD = 512;
// How long to wait before trying again after a failed fetch.
const RETRY_DELAYS_MS = [60_000, 5 * 60_000, 15 * 60_000];
function relayHost(url) {
const m = /^turns?:([^:?\s]+)/i.exec(String(url || '').trim());
return m ? m[1].toLowerCase() : null;
}
/** True when an ICE entry points at SecureBit's own relay. */
export function isOwnRelayEntry(entry) {
if (!entry || typeof entry !== 'object') return false;
const urls = Array.isArray(entry.urls) ? entry.urls : [entry.urls];
return urls.some((u) => OWN_RELAY_HOSTS.includes(relayHost(u)));
}
function isCleanField(value) {
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_FIELD) return false;
for (let i = 0; i < value.length; i++) {
const c = value.charCodeAt(i);
if (c < 0x20 || c === 0x7f) return false;
}
return true;
}
/**
* Write a fresh credential into every own-relay entry of `list`, in place.
* @returns {boolean} whether anything was updated
*/
export function applyTurnCredentials(list, cred) {
if (!Array.isArray(list) || !cred || !isCleanField(cred.username) || !isCleanField(cred.credential)) {
return false;
}
let updated = false;
for (const entry of list) {
if (!isOwnRelayEntry(entry)) continue;
entry.username = cred.username;
entry.credential = cred.credential;
updated = true;
}
return updated;
}
/** Pull the credential out of an endpoint response, or null if it is not usable. */
export function parseCredentialResponse(body, nowSeconds) {
const server = body && Array.isArray(body.iceServers) ? body.iceServers[0] : null;
if (!server || !isCleanField(server.username) || !isCleanField(server.credential)) return null;
// coturn REST-API usernames are "<expiry>:<label>"; refuse one already expired.
const expiry = Number(String(server.username).split(':')[0]);
if (!Number.isFinite(expiry) || expiry <= nowSeconds) return null;
const ttl = Number(body.ttl);
return {
username: server.username,
credential: server.credential,
expiry,
ttl: Number.isFinite(ttl) && ttl > 0 ? ttl : expiry - nowSeconds,
};
}
let started = false;
/**
* Start keeping the relay credential fresh. Safe to call more than once.
* @param {object} [opts]
* @param {() => Array} [opts.getList] the ICE list to update
*/
export function startTurnCredentialRefresh(opts = {}) {
if (started || typeof window === 'undefined' || typeof fetch !== 'function') return;
started = true;
const getList = opts.getList || (() => window.SECUREBIT_ICE_SERVERS);
let expiry = 0;
let ttl = 0;
let timer = null;
let failures = 0;
const schedule = (ms) => {
if (timer) clearTimeout(timer);
timer = setTimeout(refresh, ms);
};
async function refresh() {
timer = null;
try {
const res = await fetch(ENDPOINT, { method: 'POST', cache: 'no-store', credentials: 'omit' });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const cred = parseCredentialResponse(await res.json(), Date.now() / 1000);
if (!cred) throw new Error('unusable response');
applyTurnCredentials(getList(), cred);
expiry = cred.expiry;
ttl = cred.ttl;
failures = 0;
// Renew at half-life, so a call started just before renewal still has
// hours of validity left for the relay to refresh its allocation.
schedule(Math.max(60, ttl / 2) * 1000);
} catch (_) {
const delay = RETRY_DELAYS_MS[Math.min(failures, RETRY_DELAYS_MS.length - 1)];
failures++;
schedule(delay);
}
}
// Timers are throttled in background tabs and stop while a laptop sleeps; on
// coming back, renew straight away if the credential is past its half-life.
document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return;
const now = Date.now() / 1000;
if (!expiry || expiry - now < ttl / 2) refresh();
});
refresh();
}
+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
// 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 = '1790203273757';
const SW_BUILD_VERSION = '1790206279080';
// Locale subdirectories, rewritten by scripts/build-i18n.js. Each localized page is a
// separate document at its own URL, so the shell has to be cached and served per
+30
View File
@@ -114,6 +114,7 @@ function makeManager(overrides = {}) {
_attemptIceRestart: P._attemptIceRestart,
_scheduleReconnectRetry: P._scheduleReconnectRetry,
_sendIceRestartOffer: P._sendIceRestartOffer,
_applyCurrentIceServers: P._applyCurrentIceServers,
_currentRemoteDtlsFingerprint: P._currentRemoteDtlsFingerprint,
_assertSameRemoteIdentity: P._assertSameRemoteIdentity,
_handleIceRestartSignal: P._handleIceRestartSignal,
@@ -341,6 +342,35 @@ try {
mgr._resetReconnectState();
}
// ── a restart presents the ICE list as it is now ─────────────────────────
// Our relay's credential expires daily and is renewed in place on the shared
// list; a restart gathers new relay candidates, so it must hand the peer
// connection the current list rather than the one it was built with.
{
const iceServers = [{ urls: 'turn:turn.securebit.chat:3478', username: 'old', credential: 'old' }];
const applied = [];
const { mgr, sent } = makeManager({ isInitiator: true, _config: { webrtc: { iceServers } } });
mgr.peerConnection.getConfiguration = () => ({ iceServers: [], bundlePolicy: 'balanced' });
mgr.peerConnection.setConfiguration = (cfg) => applied.push(cfg);
iceServers[0].username = 'fresh';
mgr._reconnect.startedAt = Date.now();
await mgr._attemptIceRestart();
assert.equal(sent.length, 1, 'the restart still goes out');
assert.equal(applied.length, 1, 'the current ICE list is applied before restarting');
assert.equal(applied[0].iceServers[0].username, 'fresh');
assert.equal(applied[0].bundlePolicy, 'balanced', 'the rest of the configuration is kept');
// An engine that refuses the update must not stop the restart.
const { mgr: stubborn, sent: sent2 } = makeManager({ isInitiator: true, _config: { webrtc: { iceServers } } });
stubborn.peerConnection.getConfiguration = () => ({});
stubborn.peerConnection.setConfiguration = () => { throw new Error('InvalidModificationError'); };
stubborn._reconnect.startedAt = Date.now();
await stubborn._attemptIceRestart();
assert.equal(sent2.length, 1, 'a refused update still restarts with what it had');
mgr._resetReconnectState();
stubborn._resetReconnectState();
}
// ── role split: the offerer restarts, the answerer asks ─────────────────
{
const { mgr, sent } = makeManager({ isInitiator: true });
+92
View File
@@ -0,0 +1,92 @@
// The TURN credential endpoint (deploy/turn-credentials.js) and the web client
// that consumes it (src/network/turnCredentials.js).
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import endpoint from '../deploy/turn-credentials.js';
import { applyTurnCredentials, isOwnRelayEntry } from '../src/network/turnCredentials.js';
const SECRET = 'test-secret';
const NOW = 1_800_000_000;
// ---- endpoint ----
{
const res = endpoint.decide({ method: 'POST', origin: 'https://securebit.chat', secret: SECRET, nowSeconds: NOW });
assert.equal(res.status, 200);
assert.equal(res.allowOrigin, 'https://securebit.chat');
const server = res.body.iceServers[0];
assert.equal(server.username, `${NOW + endpoint.TTL_SECONDS}:securebit`);
// Exactly what coturn's use-auth-secret expects: base64(HMAC-SHA1(secret, username)).
const expected = crypto.createHmac('sha1', SECRET).update(server.username).digest('base64');
assert.equal(server.credential, expected);
assert.ok(server.urls.every((u) => /^turns?:/.test(u)));
}
// Other websites are refused: a browser cannot fake Origin.
assert.equal(endpoint.decide({ method: 'POST', origin: 'https://evil.example', secret: SECRET, nowSeconds: NOW }).status, 403);
assert.equal(endpoint.decide({ method: 'POST', origin: 'null', secret: SECRET, nowSeconds: NOW }).status, 403);
// Desktop webviews are allowed and get CORS back.
for (const origin of ['tauri://localhost', 'http://tauri.localhost', 'https://tauri.localhost']) {
const res = endpoint.decide({ method: 'POST', origin, secret: SECRET, nowSeconds: NOW });
assert.equal(res.status, 200, origin);
assert.equal(res.allowOrigin, origin);
}
// No Origin (native app / script) is served but gets no CORS header.
{
const res = endpoint.decide({ method: 'POST', origin: undefined, secret: SECRET, nowSeconds: NOW });
assert.equal(res.status, 200);
assert.equal(res.allowOrigin, undefined);
}
// Only POST mints; preflight is answered; a missing secret fails closed.
assert.equal(endpoint.decide({ method: 'GET', origin: 'https://securebit.chat', secret: SECRET, nowSeconds: NOW }).status, 405);
assert.equal(endpoint.decide({ method: 'OPTIONS', origin: 'https://securebit.chat', secret: SECRET, nowSeconds: NOW }).status, 204);
assert.equal(endpoint.decide({ method: 'POST', origin: 'https://securebit.chat', secret: '', nowSeconds: NOW }).status, 503);
// ---- web client: updating the shared ICE list in place ----
const ownEntry = () => ({
urls: ['turn:turn.securebit.chat:3478?transport=udp', 'turns:turn.securebit.chat:443?transport=tcp'],
username: '2147483647:securebit',
credential: 'old',
});
{
const stun = { urls: 'stun:stun.l.google.com:19302' };
const own = ownEntry();
const list = [stun, own];
const fresh = { username: `${NOW + 86400}:securebit`, credential: 'new' };
assert.equal(applyTurnCredentials(list, fresh), true);
// Same array, same objects: managers hold a reference to this list, so an
// in-place update is what makes the next connection use the new credential.
assert.equal(list[1], own);
assert.equal(own.username, fresh.username);
assert.equal(own.credential, 'new');
assert.equal(stun.username, undefined);
}
{
// A user's own TURN server is never touched.
const custom = { urls: 'turn:relay.example.org:3478', username: 'me', credential: 'mine' };
assert.equal(isOwnRelayEntry(custom), false);
assert.equal(applyTurnCredentials([custom], { username: 'x:securebit', credential: 'y' }), false);
assert.equal(custom.credential, 'mine');
// Raw-IP entries of our relay count as ours.
assert.equal(isOwnRelayEntry({ urls: ['turn:144.172.96.126:3478?transport=udp'] }), true);
assert.equal(isOwnRelayEntry({ urls: 'turn:evil.example.com?securebit.chat' }), false);
}
{
// Malformed responses are rejected rather than written into the ICE list.
const own = ownEntry();
for (const bad of [null, {}, { username: 'a', credential: 'b\n' }, { username: 'x'.repeat(600), credential: 'b' }, { username: 5, credential: 'b' }]) {
assert.equal(applyTurnCredentials([own], bad), false);
}
assert.equal(own.credential, 'old');
}
console.log('turn-credentials.test.mjs: all assertions passed');
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/uk/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/uk.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/uk.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/ur/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/ur.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/ur.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>
+19 -19
View File
@@ -30,18 +30,18 @@
<!-- PWA Manifest -->
<link rel="manifest" href="/zh/manifest.json">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Blocking, and above the stylesheet on purpose: this stamps data-theme on <html>
from localStorage while the parser is still in <head>, so the page paints in the
chosen theme on the first frame instead of flashing dark and correcting itself.
Cannot be inline (CSP is script-src 'self') and cannot be a module (deferred). -->
<script src="/src/scripts/theme-boot.js?v=1790203273757"></script>
<script src="/src/scripts/theme-boot.js?v=1790206279080"></script>
<!-- Blocking, and in <head> on purpose: beforeinstallprompt fires once and is
not replayed, so the listener has to exist before Chrome decides the page
is installable. Deferred/module scripts are already too late. -->
<script src="/src/scripts/pwa-install-capture.js?v=1790203273757"></script>
<script src="/src/scripts/pwa-install-capture.js?v=1790206279080"></script>
<!-- PWA Meta Tags -->
<meta name="mobile-web-app-capable" content="yes">
@@ -116,7 +116,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=1790203273757">
<link rel="apple-touch-icon" href="/logo/icon-180x180.png?v=1790206279080">
<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">
@@ -125,7 +125,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=1790203273757">
<link rel="apple-touch-icon" sizes="180x180" href="/logo/icon-180x180.png?v=1790206279080">
<!-- Microsoft Tiles -->
<meta name="msapplication-TileColor" content="#ff6b35">
@@ -264,7 +264,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=1790203273757"></script>
<script defer src="/config/ice-servers.js?v=1790206279080"></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 —
@@ -278,8 +278,8 @@
the end of <body>: 21 KB of CSS in total, but eight round trips before the
browser could paint, which GTmetrix measured at 441 ms on a throttled mobile
connection. Bytes were never the problem; requests were. -->
<link rel="stylesheet" href="/assets/app.css?v=1790203273757">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790203273757">
<link rel="stylesheet" href="/assets/app.css?v=1790206279080">
<link rel="icon" type="image/x-icon" href="/logo/favicon.ico?v=1790206279080">
<!-- Preload only the fonts needed for first paint. Inter is one variable file that
answers for every weight, so there is one to preload rather than the two of five
copies this used to name. fa-solid covers the bulk of UI icons; fa-regular and
@@ -287,19 +287,19 @@
<link rel="preload" href="/assets/fontawesome/webfonts/fa-solid-900.subset.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/assets/fonts/inter/files/inter-latin.woff2" as="font" type="font/woff2" crossorigin>
<!-- Non-critical CSS (FontAwesome ~102KB, Prism) loaded async — no longer blocks paint. -->
<script defer src="/src/scripts/load-async-css.js?v=1790203273757"></script>
<script defer src="/src/scripts/load-async-css.js?v=1790206279080"></script>
<noscript>
<link rel="stylesheet" href="/libs/prism/prism.css">
</noscript>
<script defer src="/src/scripts/fa-check.js?v=1790203273757"></script>
<script defer src="/src/scripts/fa-check.js?v=1790206279080"></script>
<!-- This locale's dictionary, registered before anything can ask for a string.
Only the default locale's is bundled (src/i18n/index.js imports it, because t()
falls back to it); every other page loads its own here instead of all thirteen
riding along inside dist/app.js. Absent on the default locale's own page. -->
<script type="module" src="/src/i18n/dict/zh.js?v=1790203273757"></script>
<script type="module" src="/src/i18n/dict/zh.js?v=1790206279080"></script>
<!-- Update Manager - система принудительного обновления -->
<script defer src="/src/utils/updateManager.js?v=1790203273757"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790203273757"></script>
<script defer src="/src/utils/updateManager.js?v=1790206279080"></script>
<script type="module" src="/src/components/UpdateChecker.jsx?v=1790206279080"></script>
<!-- /dist/qr-local.js is deliberately absent: app-boot.js fetches it once the
app has mounted and the browser is idle, so the 142 KB it weighs is off the
first load. src/components/QRScanner.js used to be here too and registered
@@ -487,12 +487,12 @@
</div>
</div>
</div>
<script type="module" src="/dist/app-boot.js?v=1790203273757"></script>
<script type="module" src="/dist/app.js?v=1790203273757"></script>
<script type="module" src="/dist/app-boot.js?v=1790206279080"></script>
<script type="module" src="/dist/app.js?v=1790206279080"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790203273757"></script>
<script src="/src/pwa/install-prompt.js?v=1790203273757" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790203273757" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790203273757"></script>
<script defer src="/src/scripts/pwa-register.js?v=1790206279080"></script>
<script src="/src/pwa/install-prompt.js?v=1790206279080" type="module"></script>
<script src="/src/pwa/pwa-manager.js?v=1790206279080" type="module"></script>
<script defer src="/src/scripts/pwa-offline-test.js?v=1790206279080"></script>
</body>
</html>