v6.8.4: the relay password now changes every day
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
No accounts. No servers storing your messages. No installation required.
|
||||
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://snapcraft.io/securebit-chat)
|
||||
[](#install-as-an-app)
|
||||
[](#security-model)
|
||||
|
||||
+19
-19
@@ -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
@@ -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>
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
};
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3
-3
File diff suppressed because one or more lines are too long
Vendored
+6
-6
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
+2
-2
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
Reference in New Issue
Block a user